research: add US alpha exploration scripts
This commit is contained in:
136
research/run_interaction.py
Normal file
136
research/run_interaction.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
Evaluate interaction/ensemble strategies on 1/3/5/10y PIT windows with a
|
||||
proper 500-day warmup preload, so 252d-warmup strategies are active from the
|
||||
measurement start.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import warnings
|
||||
|
||||
import pandas as pd
|
||||
|
||||
import research.pit_backtest as pit
|
||||
from research.alpha_factors import AlphaFactorStrategy
|
||||
from research.interaction_alpha import (MultiplicativeFactorStrategy,
|
||||
SubStrategyEnsemble, VotingFactorStrategy,
|
||||
default_ensemble)
|
||||
from strategies.factor_combo import FactorComboStrategy
|
||||
from strategies.recovery_momentum import RecoveryMomentumStrategy
|
||||
|
||||
warnings.filterwarnings("ignore", category=FutureWarning)
|
||||
warnings.filterwarnings("ignore", category=RuntimeWarning)
|
||||
|
||||
DATA_DIR = "data"
|
||||
BENCHMARK = "SPY"
|
||||
|
||||
|
||||
def load():
|
||||
raw = pit.load_pit_prices()
|
||||
masked = pit.pit_universe(raw)
|
||||
masked[BENCHMARK] = raw[BENCHMARK]
|
||||
return masked
|
||||
|
||||
|
||||
def warmup_slice(df, years, warmup_days=500):
|
||||
measurement_start = df.index[-1] - pd.DateOffset(years=years)
|
||||
cutoff = max(df.index[0], measurement_start - pd.Timedelta(days=warmup_days * 1.5))
|
||||
return df[df.index >= cutoff], measurement_start
|
||||
|
||||
|
||||
def measure(eq, start, name=""):
|
||||
eq = eq[eq.index >= start]
|
||||
eq = eq / eq.iloc[0] * 10_000
|
||||
return pit.summarize(eq, name=name)
|
||||
|
||||
|
||||
def make_configs(mkt_ret):
|
||||
pair = ["mom_12_1", "recovery_63"]
|
||||
return {
|
||||
"Mult(mom12×rec63) eq, tn=10":
|
||||
lambda: MultiplicativeFactorStrategy(
|
||||
factor_names=pair, top_n=10, rebal_freq=21, mkt_returns=mkt_ret),
|
||||
"Mult(mom12×rec63) eq, tn=15":
|
||||
lambda: MultiplicativeFactorStrategy(
|
||||
factor_names=pair, top_n=15, rebal_freq=21, mkt_returns=mkt_ret),
|
||||
"Mult(mom12×rec63) eq, rebal=10":
|
||||
lambda: MultiplicativeFactorStrategy(
|
||||
factor_names=pair, top_n=10, rebal_freq=10, mkt_returns=mkt_ret),
|
||||
"Mult(mom12×rec63) sig^2, tn=15":
|
||||
lambda: MultiplicativeFactorStrategy(
|
||||
factor_names=pair, top_n=15, rebal_freq=21, mkt_returns=mkt_ret,
|
||||
weighting="signal", signal_concentration=2.0),
|
||||
"Mult(mom12×rec63) sig^4, tn=15":
|
||||
lambda: MultiplicativeFactorStrategy(
|
||||
factor_names=pair, top_n=15, rebal_freq=21, mkt_returns=mkt_ret,
|
||||
weighting="signal", signal_concentration=4.0),
|
||||
"Mult(mom12×rec63) disp-scale":
|
||||
lambda: MultiplicativeFactorStrategy(
|
||||
factor_names=pair, top_n=10, rebal_freq=21, mkt_returns=mkt_ret,
|
||||
dispersion_scale=True),
|
||||
"Mult(mom12×rec63) inv_vol":
|
||||
lambda: MultiplicativeFactorStrategy(
|
||||
factor_names=pair, top_n=10, rebal_freq=21, mkt_returns=mkt_ret,
|
||||
weighting="inv_vol"),
|
||||
"Ensemble3 (RM/upcap/mult)":
|
||||
lambda: default_ensemble(mkt_ret),
|
||||
"Recovery+Mom Top10":
|
||||
lambda: RecoveryMomentumStrategy(top_n=10),
|
||||
"fc_up_cap+mom_gap":
|
||||
lambda: FactorComboStrategy("up_cap+mom_gap", rebal_freq=21, top_n=10),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
print("Loading PIT data…")
|
||||
masked = load()
|
||||
tickers = [c for c in masked.columns if c != BENCHMARK]
|
||||
mkt_ret = masked[BENCHMARK].pct_change(fill_method=None)
|
||||
print(f" shape={masked.shape} range={masked.index[0].date()} → {masked.index[-1].date()}")
|
||||
|
||||
rows = []
|
||||
for years in (10, 5, 3, 1):
|
||||
sliced, start = warmup_slice(masked, years, warmup_days=500)
|
||||
prices = sliced[tickers]
|
||||
print(f"\n--- {years}y window "
|
||||
f"(measure {start.date()} → {sliced.index[-1].date()}, "
|
||||
f"warmup from {sliced.index[0].date()}) ---")
|
||||
spy = sliced[BENCHMARK].dropna()
|
||||
spy_eq = (spy / spy.iloc[0]) * 10_000
|
||||
m = measure(spy_eq, start, "")
|
||||
rows.append({"years": years, "strategy": "SPY buy-and-hold",
|
||||
**{k: v for k, v in m.items() if k != "name"}})
|
||||
configs = make_configs(mkt_ret)
|
||||
for name, factory in configs.items():
|
||||
strat = factory()
|
||||
eq = pit.backtest(strategy=strat, prices=prices,
|
||||
initial_capital=10_000, transaction_cost=0.001)
|
||||
m = measure(eq, start, "")
|
||||
rows.append({"years": years, "strategy": name,
|
||||
**{k: v for k, v in m.items() if k != "name"}})
|
||||
tail = [r for r in rows if r["years"] == years]
|
||||
tail.sort(key=lambda r: r["Sharpe"], reverse=True)
|
||||
for r in tail:
|
||||
print(f" {r['strategy']:<34s} CAGR={r['CAGR']*100:>6.1f}% "
|
||||
f"Sharpe={r['Sharpe']:>5.2f} Sortino={r['Sortino']:>5.2f} "
|
||||
f"MaxDD={r['MaxDD']*100:>6.1f}% Calmar={r['Calmar']:>5.2f}")
|
||||
|
||||
df = pd.DataFrame(rows)
|
||||
df.to_csv(os.path.join(DATA_DIR, "interaction_results.csv"), index=False)
|
||||
|
||||
print("\n=== Cross-window CAGR summary (sorted by 10y Sharpe) ===")
|
||||
pv = df.pivot(index="strategy", columns="years", values="CAGR")
|
||||
pv.columns = [f"CAGR_{y}y" for y in pv.columns]
|
||||
sh10 = df[df["years"] == 10].set_index("strategy")["Sharpe"]
|
||||
pv["Sharpe_10y"] = sh10
|
||||
pv = pv.sort_values("Sharpe_10y", ascending=False)
|
||||
print(pv.to_string(formatters={
|
||||
"CAGR_10y": "{:.1%}".format, "CAGR_5y": "{:.1%}".format,
|
||||
"CAGR_3y": "{:.1%}".format, "CAGR_1y": "{:.1%}".format,
|
||||
"Sharpe_10y": "{:.2f}".format,
|
||||
}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user