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>
167 lines
5.9 KiB
Python
167 lines
5.9 KiB
Python
"""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()
|