"""Evaluate TrendRiderV5 vs V3 baseline and benchmarks. Run: uv run python -m research.trend_rider_v5_eval """ from __future__ import annotations import argparse import os import sys from dataclasses import asdict from itertools import product import numpy as np import pandas as pd sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from research.trend_rider_robustness import ( buy_hold_weights, evaluate_weights, load_price_panel, portfolio_returns, ) from strategies.permanent import TrendRiderV3 from strategies.trend_rider_v5 import TrendRiderV5 IS_START = "2015-01-02" IS_END = "2020-12-31" OOS_START = "2021-01-01" OOS_END = "2026-05-07" FULL_START = IS_START FULL_END = OOS_END def _fmt(x: float) -> str: return f"{x * 100:7.2f}%" def print_eval(label: str, ev) -> None: print( f" {label:<32s} " f"CAGR {_fmt(ev.cagr)} Vol {_fmt(ev.volatility)} " f"Sharpe {ev.sharpe:5.2f} MDD {_fmt(ev.max_drawdown)} " f"Calmar {ev.calmar:5.2f} X {ev.final_multiple:6.2f} " f"Sw {ev.switches:4d} Turn {ev.avg_daily_turnover*100:5.2f}%" ) def evaluate_panel(name: str, weights: pd.DataFrame, prices: pd.DataFrame, start: str, end: str, transaction_cost: float = 0.001): return evaluate_weights(name, weights, prices[weights.columns], transaction_cost=transaction_cost, start=start, end=end) def annual_returns_table(weights_map: dict, prices: pd.DataFrame, transaction_cost: float = 0.001) -> pd.DataFrame: out = {} for name, w in weights_map.items(): rets = portfolio_returns(w, prices[w.columns], transaction_cost=transaction_cost) rets = rets[(rets.index >= FULL_START) & (rets.index <= FULL_END)] out[name] = (1.0 + rets).groupby(rets.index.year).prod() - 1.0 return pd.DataFrame(out) def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--transaction-cost", type=float, default=0.001) parser.add_argument("--out-dir", default="data") parser.add_argument("--vol-target", type=float, default=0.30) args = parser.parse_args() os.makedirs(args.out_dir, exist_ok=True) prices = load_price_panel() print(f"Panel: {prices.index.min().date()} to {prices.index.max().date()}, {prices.shape[1]} cols") candidates = { "V3 default": TrendRiderV3(), "V5 default": TrendRiderV5(), # Tighter panic detection "V5 panic 1.4 / 3%": TrendRiderV5( panic_vol_ratio=1.4, panic_peak_drop_pct=0.03 ), "V5 panic 1.5 / 3.5%": TrendRiderV5( panic_vol_ratio=1.5, panic_peak_drop_pct=0.035 ), "V5 panic 1.8 / 5%": TrendRiderV5( panic_vol_ratio=1.8, panic_peak_drop_pct=0.05 ), # Combine panic + harder promote "V5 panic+conserv": TrendRiderV5( promote_thresholds=(0.45, 0.70), demote_thresholds=(0.35, 0.55), panic_vol_ratio=1.4, panic_peak_drop_pct=0.03, ), # No panic at all (pure conviction) "V5 no panic": TrendRiderV5( panic_vol_ratio=99.0, panic_peak_drop_pct=0.99 ), } weights_map = {} print("\n=== Generating signals ===") for name, strat in candidates.items(): weights_map[name] = strat.generate_signals(prices) print("\n=== FULL period (2015-01 → 2026-05) ===") rows = [] for name, w in weights_map.items(): ev = evaluate_panel(name, w, prices, FULL_START, FULL_END, args.transaction_cost) rows.append(asdict(ev) | {"name": name}) print_eval(name, ev) spy_w = buy_hold_weights(prices, "SPY") qqq_w = buy_hold_weights(prices, "QQQ") bench = { "SPY B&H": evaluate_panel("SPY B&H", spy_w, prices, FULL_START, FULL_END, 0.0), "QQQ B&H": evaluate_panel("QQQ B&H", qqq_w, prices, FULL_START, FULL_END, 0.0), } for name, ev in bench.items(): print_eval(name, ev) print("\n=== IS (2015 → 2020) ===") for name, w in weights_map.items(): ev = evaluate_panel(name, w, prices, IS_START, IS_END, args.transaction_cost) print_eval(name, ev) for name, w in [("SPY B&H", spy_w), ("QQQ B&H", qqq_w)]: ev = evaluate_panel(name, w, prices, IS_START, IS_END, 0.0) print_eval(name, ev) print("\n=== OOS (2021 → 2026-05) ===") for name, w in weights_map.items(): ev = evaluate_panel(name, w, prices, OOS_START, OOS_END, args.transaction_cost) print_eval(name, ev) for name, w in [("SPY B&H", spy_w), ("QQQ B&H", qqq_w)]: ev = evaluate_panel(name, w, prices, OOS_START, OOS_END, 0.0) print_eval(name, ev) print("\n=== Annual returns ===") annual = annual_returns_table(weights_map, prices, args.transaction_cost) annual = annual.applymap(lambda x: f"{x*100:6.1f}%") print(annual.to_string()) pd.DataFrame(rows).to_csv(os.path.join(args.out_dir, "v5_eval_full.csv"), index=False) annual.to_csv(os.path.join(args.out_dir, "v5_eval_annual.csv")) if __name__ == "__main__": main()