2020
2121from __future__ import annotations
2222
23+ import warnings
2324from typing import Optional
2425
2526import numpy as np
3536 except Exception :
3637 # Minimal Factor stub so this module can be inspected/tested in isolation.
3738 class Factor :
38- def __init__ (
39- self , name : Optional [str ] = None , lookback : Optional [int ] = None
40- ):
39+ def __init__ (self , name : Optional [str ] = None , lookback : Optional [int ] = None ):
4140 self .name = name or "factor"
4241 self .lookback = lookback or 0
4342 self ._values : Optional [pd .DataFrame ] = None
@@ -49,7 +48,6 @@ def _validate_data(self, prices: pd.DataFrame) -> None:
4948 def __repr__ (self ) -> str :
5049 return f"<Factor name={ self .name } lookback={ self .lookback } >"
5150
52-
5351# Constants
5452TRADING_DAYS = 252
5553
@@ -95,17 +93,15 @@ def compute(self, prices: pd.DataFrame) -> pd.DataFrame:
9593 self ._validate_data (prices )
9694
9795 if prices .shape [0 ] < self .lookback :
98- raise ValueError (
99- f"Need at least { self .lookback } rows of data to compute volatility"
100- )
96+ raise ValueError (f"Need at least { self .lookback } rows of data to compute volatility" )
10197
10298 # pct change -> returns
10399 returns = prices .pct_change ()
104100
105101 # rolling std (population, ddof=0) and annualize
106- vol = returns .rolling (window = self .lookback , min_periods = self .lookback ).std (
107- ddof = 0
108- ) * np . sqrt ( TRADING_DAYS )
102+ vol = returns .rolling (window = self .lookback , min_periods = self .lookback ).std (ddof = 0 ) * np . sqrt (
103+ TRADING_DAYS
104+ )
109105
110106 # Trim initial rows that don't correspond to a full window
111107 if self .lookback > 1 :
@@ -152,42 +148,26 @@ def compute(self, prices: pd.DataFrame) -> pd.DataFrame:
152148
153149 # require enough rows to compute returns and rolling windows
154150 if prices .shape [0 ] < self .lookback + 1 :
155- raise ValueError (
156- f"Need at least { self .lookback + 1 } rows of data to compute idiosyncratic volatility"
157- )
151+ raise ValueError (f"Need at least { self .lookback + 1 } rows of data to compute idiosyncratic volatility" )
158152
159153 # daily returns
160154 returns = prices .pct_change ().dropna ()
161155 if returns .shape [0 ] < self .lookback :
162- raise ValueError (
163- f"Need at least { self .lookback } non-NA return rows to compute idio-vol"
164- )
156+ raise ValueError (f"Need at least { self .lookback } non-NA return rows to compute idio-vol" )
165157
166158 # Market proxy: equal-weighted mean across assets
167159 market = returns .mean (axis = 1 )
168160
169161 # Rolling means for covariance decomposition
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 ()
162+ returns_mean = returns .rolling (window = self .lookback , min_periods = self .lookback ).mean ()
163+ market_mean = market .rolling (window = self .lookback , min_periods = self .lookback ).mean ()
176164
177165 # Compute cov(ri, rm) via E[ri*rm] - E[ri]*E[rm]
178- e_ri_rm = (
179- returns .mul (market , axis = 0 )
180- .rolling (window = self .lookback , min_periods = self .lookback )
181- .mean ()
182- )
166+ e_ri_rm = returns .mul (market , axis = 0 ).rolling (window = self .lookback , min_periods = self .lookback ).mean ()
183167 cov_with_mkt = e_ri_rm - returns_mean .mul (market_mean , axis = 0 )
184168
185169 # market variance (vector) -- guard zeros
186- market_var = (
187- market .rolling (window = self .lookback , min_periods = self .lookback )
188- .var (ddof = 0 )
189- .replace (0 , np .nan )
190- )
170+ market_var = market .rolling (window = self .lookback , min_periods = self .lookback ).var (ddof = 0 ).replace (0 , np .nan )
191171
192172 # Beta: cov / var (division broadcasted over columns)
193173 beta = cov_with_mkt .div (market_var , axis = 0 )
@@ -199,9 +179,9 @@ def compute(self, prices: pd.DataFrame) -> pd.DataFrame:
199179 residuals = returns - predicted
200180
201181 # Rolling std of residuals (annualized)
202- idio_vol = residuals .rolling (
203- window = self . lookback , min_periods = self . lookback
204- ). std ( ddof = 0 ) * np . sqrt ( TRADING_DAYS )
182+ idio_vol = residuals .rolling (window = self . lookback , min_periods = self . lookback ). std ( ddof = 0 ) * np . sqrt (
183+ TRADING_DAYS
184+ )
205185
206186 # Trim to first full-window row
207187 if self .lookback > 1 :
0 commit comments