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
5154TRADING_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