research: add US alpha exploration scripts

This commit is contained in:
2026-04-18 16:14:27 +08:00
parent 40ec3b828a
commit d0e8c97695
6 changed files with 1445 additions and 0 deletions

136
research/run_interaction.py Normal file
View 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()