research: extensive V7 optimization and V8 (TMF) evaluation
Research scripts exploring paths beyond V7+VT36: - regime_stock_picker_eval: V3 regime + S&P 500 stock picking - v7_parameter_sweep: VT range (20-48%) + adaptive PT variants - v7_synthetic_leverage_eval: synthetic 2x/3x leveraged individual stocks - v7_breakthrough_eval/fixed: ensemble, cross-market, alt regime engines - v7_three_ideas_eval: TMF risk-off, PT entry reset, fast exit - v7_trade_audit: full 10y trade log and alpha attribution - sota_ranking: comprehensive cross-strategy ranking Key findings: - VT36 is optimal risk-return tradeoff (+7% vs VT28, Sharpe ~flat) - PT30 is structural optimum for 3x ETFs (all adaptive variants worse) - V8 (TMF risk-off) debunked: +5% was 1-day lookahead bias artifact - V3 regime engine irreplaceable (all simplified alternatives fail) - PT mechanism is dominant alpha source (+15.6pp ann, +0.58 Sharpe) V8 strategy file kept for reference (not registered). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
166
research/sota_ranking.py
Normal file
166
research/sota_ranking.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""Rank all top strategies head-to-head on the same 10-year PIT-safe data."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
import data_manager
|
||||
import metrics
|
||||
import universe_history as uh
|
||||
from main import backtest
|
||||
from trader import STRATEGY_REGISTRY, ETF_STRATEGY_UNIVERSES, MIXED_STRATEGY_EXTRA_TICKERS, filter_tradable_tickers
|
||||
from universe import UNIVERSES
|
||||
|
||||
YEARS = 10
|
||||
CAPITAL = 100_000
|
||||
TX_COST = 0.001
|
||||
FIXED_FEE = 2.0
|
||||
|
||||
# Only the most promising strategies — skip redundant freq variants
|
||||
CANDIDATES = [
|
||||
# ETF tactical allocation
|
||||
"trend_rider_v7",
|
||||
"trend_rider_v7_vt24",
|
||||
"trend_rider_v7_vt32",
|
||||
"trend_rider_v3_vt28",
|
||||
"trend_rider_v3_vt32",
|
||||
"trend_rider_v5_us",
|
||||
"trend_rider_v5_panic",
|
||||
"trend_rider_v3_us",
|
||||
# V6 hybrids (stock + regime)
|
||||
"trend_rider_v6",
|
||||
"trend_rider_v6_top10",
|
||||
# Stock pickers
|
||||
"recovery_mom_top10",
|
||||
"recovery_mom_top20",
|
||||
"trend_following",
|
||||
"fc_rec_mfilt_deep_upvol_monthly",
|
||||
"fc_rec_mfilt_deep_upvol_daily",
|
||||
# Ensembles
|
||||
"ensemble_alpha_top10",
|
||||
"sharpe_boosted_ensemble_top8",
|
||||
"risk_managed_ensemble_top10",
|
||||
"enhanced_factor_combo_top10",
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 95)
|
||||
print(" COMPREHENSIVE STRATEGY RANKING (10y PIT-safe)")
|
||||
print("=" * 95)
|
||||
|
||||
# Load S&P 500 + PIT
|
||||
print("\n[1] Loading data...")
|
||||
universe = UNIVERSES["us"]
|
||||
tickers = universe["fetch"]()
|
||||
pit_intervals = uh.load_sp500_history()
|
||||
hist_tickers = uh.all_tickers_ever(pit_intervals)
|
||||
|
||||
# Collect all ETF tickers needed
|
||||
all_etf = set()
|
||||
for name in CANDIDATES:
|
||||
base = name.removeprefix("sim_")
|
||||
if base in ETF_STRATEGY_UNIVERSES:
|
||||
all_etf.update(ETF_STRATEGY_UNIVERSES[base])
|
||||
if base in MIXED_STRATEGY_EXTRA_TICKERS:
|
||||
all_etf.update(MIXED_STRATEGY_EXTRA_TICKERS[base])
|
||||
all_etf.update(["SPY", "GLD", "DBC", "SHY", "TQQQ", "UPRO", "TLT", "IEF"])
|
||||
|
||||
all_tickers = sorted(set(tickers + hist_tickers + list(all_etf)))
|
||||
print(f" {len(all_tickers)} tickers to download...")
|
||||
|
||||
stock_data = data_manager.update("us", all_tickers, with_open=False)
|
||||
if isinstance(stock_data, tuple):
|
||||
stock_data = stock_data[0]
|
||||
|
||||
cutoff = stock_data.index[-1] - pd.DateOffset(years=YEARS)
|
||||
stock_data = stock_data[stock_data.index >= cutoff]
|
||||
stock_data = uh.mask_prices(stock_data, pit_intervals)
|
||||
|
||||
stock_tickers = [t for t in stock_data.columns
|
||||
if t not in all_etf and stock_data[t].notna().any()]
|
||||
|
||||
# Also load pure ETF panel (for pure-ETF strategies that use separate data)
|
||||
etf_list = sorted(all_etf)
|
||||
etf_data = data_manager.update("etfs", etf_list, with_open=False)
|
||||
if isinstance(etf_data, tuple):
|
||||
etf_data = etf_data[0]
|
||||
etf_cutoff = etf_data.index[-1] - pd.DateOffset(years=YEARS)
|
||||
etf_data = etf_data[etf_data.index >= etf_cutoff]
|
||||
|
||||
print(f" Stocks: {len(stock_tickers)}, ETFs: {len(etf_list)}")
|
||||
print(f" Period: {stock_data.index[0].date()} → {stock_data.index[-1].date()}")
|
||||
|
||||
# Run strategies
|
||||
print("\n[2] Running strategies...")
|
||||
results: list[tuple[str, dict]] = []
|
||||
|
||||
for name in CANDIDATES:
|
||||
if name not in STRATEGY_REGISTRY:
|
||||
print(f" SKIP {name} (not in registry)")
|
||||
continue
|
||||
|
||||
base = name.removeprefix("sim_")
|
||||
print(f" {name}...", end=" ", flush=True)
|
||||
|
||||
try:
|
||||
if base in ETF_STRATEGY_UNIVERSES:
|
||||
# Pure ETF strategy
|
||||
etf_tickers = ETF_STRATEGY_UNIVERSES[base]
|
||||
tradable = [t for t in etf_tickers if t in etf_data.columns]
|
||||
strategy = STRATEGY_REGISTRY[name]()
|
||||
eq = backtest(strategy, etf_data[tradable],
|
||||
initial_capital=CAPITAL, transaction_cost=TX_COST,
|
||||
fixed_fee=FIXED_FEE)
|
||||
elif base in MIXED_STRATEGY_EXTRA_TICKERS:
|
||||
# Mixed: stocks + ETFs in one panel
|
||||
extra = MIXED_STRATEGY_EXTRA_TICKERS[base]
|
||||
panel_cols = stock_tickers + [t for t in extra if t in stock_data.columns]
|
||||
panel = stock_data[[c for c in panel_cols if c in stock_data.columns]]
|
||||
strategy = STRATEGY_REGISTRY[name]()
|
||||
eq = backtest(strategy, panel,
|
||||
initial_capital=CAPITAL, transaction_cost=TX_COST,
|
||||
fixed_fee=FIXED_FEE)
|
||||
else:
|
||||
# Pure stock strategy
|
||||
strategy = STRATEGY_REGISTRY[name](top_n=10)
|
||||
eq = backtest(strategy, stock_data[stock_tickers],
|
||||
initial_capital=CAPITAL, transaction_cost=TX_COST,
|
||||
fixed_fee=FIXED_FEE)
|
||||
|
||||
m = metrics.raw_summary(eq)
|
||||
results.append((name, m))
|
||||
print(f"Ann={m['annualizedReturn']*100:.1f}%")
|
||||
except Exception as e:
|
||||
print(f"FAILED: {e}")
|
||||
|
||||
# SPY benchmark
|
||||
spy = stock_data["SPY"].dropna()
|
||||
spy_eq = (spy / spy.iloc[0]) * CAPITAL
|
||||
spy_m = metrics.raw_summary(spy_eq)
|
||||
results.append(("SPY (benchmark)", spy_m))
|
||||
|
||||
# Sort by annualized return
|
||||
results.sort(key=lambda x: x[1]["annualizedReturn"], reverse=True)
|
||||
|
||||
print(f"\n[3] Ranking ({YEARS}y, ${CAPITAL:,.0f}, tx={TX_COST*100:.1f}bps + ${FIXED_FEE:.0f}/trade)")
|
||||
print("=" * 110)
|
||||
print(f"{'#':<4} {'Strategy':<40} {'Ann%':>8} {'Vol%':>8} {'Sharpe':>8} {'Sortino':>8} {'MaxDD%':>8} {'Calmar':>8}")
|
||||
print("-" * 110)
|
||||
|
||||
for i, (name, m) in enumerate(results, 1):
|
||||
print(f"{i:<4} {name:<40} "
|
||||
f"{m['annualizedReturn']*100:>7.1f}% "
|
||||
f"{m['annualizedVolatility']*100:>7.1f}% "
|
||||
f"{m['sharpeRatio']:>8.2f} "
|
||||
f"{m['sortinoRatio']:>8.2f} "
|
||||
f"{m['maxDrawdown']*100:>7.1f}% "
|
||||
f"{m['calmarRatio']:>8.2f}")
|
||||
print("=" * 110)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user