feat: add regime and breakout alpha modules
This commit is contained in:
34
research/event_factors.py
Normal file
34
research/event_factors.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
TRAILING_HIGH_WINDOW = 60
|
||||
COMPRESSION_WINDOW = 20
|
||||
VOLUME_WINDOW = 20
|
||||
|
||||
|
||||
def breakout_after_compression_score(
|
||||
close: pd.DataFrame,
|
||||
high: pd.DataFrame,
|
||||
low: pd.DataFrame,
|
||||
volume: pd.DataFrame,
|
||||
) -> pd.DataFrame:
|
||||
"""Score breakout setups and shift the result so it is tradable next day."""
|
||||
close = close.sort_index()
|
||||
high = high.reindex(index=close.index, columns=close.columns).sort_index()
|
||||
low = low.reindex(index=close.index, columns=close.columns).sort_index()
|
||||
volume = volume.reindex(index=close.index, columns=close.columns).sort_index()
|
||||
|
||||
trailing_high = close.rolling(TRAILING_HIGH_WINDOW, min_periods=TRAILING_HIGH_WINDOW).max()
|
||||
proximity_to_high = close / trailing_high.replace(0, np.nan)
|
||||
|
||||
recent_high = high.rolling(COMPRESSION_WINDOW, min_periods=COMPRESSION_WINDOW).max()
|
||||
recent_low = low.rolling(COMPRESSION_WINDOW, min_periods=COMPRESSION_WINDOW).min()
|
||||
recent_mid = (recent_high + recent_low) / 2
|
||||
compressed_range = -((recent_high - recent_low) / recent_mid.replace(0, np.nan))
|
||||
|
||||
median_volume = volume.rolling(VOLUME_WINDOW, min_periods=VOLUME_WINDOW).median()
|
||||
abnormal_volume = volume / median_volume.replace(0, np.nan)
|
||||
|
||||
score = proximity_to_high + compressed_range + abnormal_volume
|
||||
return score.shift(1)
|
||||
23
research/regime_filters.py
Normal file
23
research/regime_filters.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import pandas as pd
|
||||
|
||||
|
||||
LONG_MA_WINDOW = 200
|
||||
RS_WINDOW = 63
|
||||
|
||||
|
||||
def build_regime_filter(etf_close: pd.DataFrame, market_col: str = "SPY") -> pd.Series:
|
||||
"""Return a next-day tradable regime flag based on market trend and ETF leadership."""
|
||||
prices = etf_close.sort_index()
|
||||
if market_col not in prices.columns:
|
||||
raise KeyError(f"{market_col} not found in etf_close")
|
||||
|
||||
market = prices[market_col]
|
||||
market_ma = market.rolling(LONG_MA_WINDOW, min_periods=LONG_MA_WINDOW).mean()
|
||||
market_ok = market.gt(market_ma)
|
||||
|
||||
rs = prices.pct_change(RS_WINDOW, fill_method=None)
|
||||
non_market_rs = rs.drop(columns=[market_col], errors="ignore")
|
||||
leader_ok = non_market_rs.gt(rs[market_col], axis=0).any(axis=1)
|
||||
|
||||
regime = (market_ok & leader_ok).astype(bool)
|
||||
return regime.shift(1, fill_value=False)
|
||||
Reference in New Issue
Block a user