Skip to content

Commit b70d514

Browse files
format code with black
1 parent 7dbdd60 commit b70d514

File tree

2 files changed

+41
-16
lines changed

2 files changed

+41
-16
lines changed

src/quant_research_starter/examples/benchmark/benchmark_factors.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
)
2121

2222

23-
def generate_synthetic_prices(n_assets: int = 500, n_days: int = 252 * 3) -> pd.DataFrame:
23+
def generate_synthetic_prices(
24+
n_assets: int = 500, n_days: int = 252 * 3
25+
) -> pd.DataFrame:
2426
"""Generate synthetic random walk price data for testing."""
2527
np.random.seed(42)
2628
returns = np.random.normal(0, 0.01, size=(n_days, n_assets))
@@ -36,7 +38,9 @@ def benchmark_factor(factor, prices: pd.DataFrame):
3638
_ = factor.compute(prices)
3739
end = time.time()
3840
elapsed = end - start
39-
print(f"{factor.name:<25} | Lookback: {factor.lookback:<5} | Time: {elapsed:.3f} sec")
41+
print(
42+
f"{factor.name:<25} | Lookback: {factor.lookback:<5} | Time: {elapsed:.3f} sec"
43+
)
4044

4145

4246
def main():

src/quant_research_starter/factors/volatility.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
except Exception:
3636
# Minimal Factor stub so this module can be inspected/tested in isolation.
3737
class Factor:
38-
def __init__(self, name: Optional[str] = None, lookback: Optional[int] = None):
38+
def __init__(
39+
self, name: Optional[str] = None, lookback: Optional[int] = None
40+
):
3941
self.name = name or "factor"
4042
self.lookback = lookback or 0
4143
self._values: Optional[pd.DataFrame] = None
@@ -47,6 +49,7 @@ def _validate_data(self, prices: pd.DataFrame) -> None:
4749
def __repr__(self) -> str:
4850
return f"<Factor name={self.name} lookback={self.lookback}>"
4951

52+
5053
# Constants
5154
TRADING_DAYS = 252
5255

@@ -92,15 +95,17 @@ def compute(self, prices: pd.DataFrame) -> pd.DataFrame:
9295
self._validate_data(prices)
9396

9497
if prices.shape[0] < self.lookback:
95-
raise ValueError(f"Need at least {self.lookback} rows of data to compute volatility")
98+
raise ValueError(
99+
f"Need at least {self.lookback} rows of data to compute volatility"
100+
)
96101

97102
# pct change -> returns
98103
returns = prices.pct_change()
99104

100105
# rolling std (population, ddof=0) and annualize
101-
vol = returns.rolling(window=self.lookback, min_periods=self.lookback).std(ddof=0) * np.sqrt(
102-
TRADING_DAYS
103-
)
106+
vol = returns.rolling(window=self.lookback, min_periods=self.lookback).std(
107+
ddof=0
108+
) * np.sqrt(TRADING_DAYS)
104109

105110
# Trim initial rows that don't correspond to a full window
106111
if self.lookback > 1:
@@ -147,26 +152,42 @@ def compute(self, prices: pd.DataFrame) -> pd.DataFrame:
147152

148153
# require enough rows to compute returns and rolling windows
149154
if prices.shape[0] < self.lookback + 1:
150-
raise ValueError(f"Need at least {self.lookback + 1} rows of data to compute idiosyncratic volatility")
155+
raise ValueError(
156+
f"Need at least {self.lookback + 1} rows of data to compute idiosyncratic volatility"
157+
)
151158

152159
# daily returns
153160
returns = prices.pct_change().dropna()
154161
if returns.shape[0] < self.lookback:
155-
raise ValueError(f"Need at least {self.lookback} non-NA return rows to compute idio-vol")
162+
raise ValueError(
163+
f"Need at least {self.lookback} non-NA return rows to compute idio-vol"
164+
)
156165

157166
# Market proxy: equal-weighted mean across assets
158167
market = returns.mean(axis=1)
159168

160169
# Rolling means for covariance decomposition
161-
returns_mean = returns.rolling(window=self.lookback, min_periods=self.lookback).mean()
162-
market_mean = market.rolling(window=self.lookback, min_periods=self.lookback).mean()
170+
returns_mean = returns.rolling(
171+
window=self.lookback, min_periods=self.lookback
172+
).mean()
173+
market_mean = market.rolling(
174+
window=self.lookback, min_periods=self.lookback
175+
).mean()
163176

164177
# Compute cov(ri, rm) via E[ri*rm] - E[ri]*E[rm]
165-
e_ri_rm = returns.mul(market, axis=0).rolling(window=self.lookback, min_periods=self.lookback).mean()
178+
e_ri_rm = (
179+
returns.mul(market, axis=0)
180+
.rolling(window=self.lookback, min_periods=self.lookback)
181+
.mean()
182+
)
166183
cov_with_mkt = e_ri_rm - returns_mean.mul(market_mean, axis=0)
167184

168185
# market variance (vector) -- guard zeros
169-
market_var = market.rolling(window=self.lookback, min_periods=self.lookback).var(ddof=0).replace(0, np.nan)
186+
market_var = (
187+
market.rolling(window=self.lookback, min_periods=self.lookback)
188+
.var(ddof=0)
189+
.replace(0, np.nan)
190+
)
170191

171192
# Beta: cov / var (division broadcasted over columns)
172193
beta = cov_with_mkt.div(market_var, axis=0)
@@ -178,9 +199,9 @@ def compute(self, prices: pd.DataFrame) -> pd.DataFrame:
178199
residuals = returns - predicted
179200

180201
# Rolling std of residuals (annualized)
181-
idio_vol = residuals.rolling(window=self.lookback, min_periods=self.lookback).std(ddof=0) * np.sqrt(
182-
TRADING_DAYS
183-
)
202+
idio_vol = residuals.rolling(
203+
window=self.lookback, min_periods=self.lookback
204+
).std(ddof=0) * np.sqrt(TRADING_DAYS)
184205

185206
# Trim to first full-window row
186207
if self.lookback > 1:

0 commit comments

Comments
 (0)