"""Long-history (2010+) validation for TrendRider V9 vs V7. Reuses the Yahoo fetch of research/lab/trend_rider_long.py (ETF_UNIVERSE + SPY/SHY, auto-adjusted, per-ticker with retries). Runs each variant through main.strategy_positions + main.simulate_live_book with the same live-book parameters as the earlier long-run study ($10k, 5bp, $2/order, 10% band). PYTHONPATH=. uv run python -m research.tr9_long [--refresh] """ from __future__ import annotations import os import pickle import sys import pandas as pd import trader # noqa: F401 (registry side effects) from main import simulate_live_book, strategy_positions from research.lab.trend_rider_long import fetch from strategies.permanent import ETF_UNIVERSE from trader import RESIZE_BAND, STRATEGY_REGISTRY import research.tr9_lab as lab OUT = os.path.join("data", "research", "tr9_long.pkl") VARIANTS = { # baseline registry names "trend_rider_v7": lambda: STRATEGY_REGISTRY["trend_rider_v7"](), "trend_rider_v7_vt24": lambda: STRATEGY_REGISTRY["trend_rider_v7_vt24"](), "trend_rider_v7_vt32": lambda: STRATEGY_REGISTRY["trend_rider_v7_vt32"](), "trend_rider_v7_vt36": lambda: STRATEGY_REGISTRY["trend_rider_v7_vt36"](), "trend_rider_v9": lambda: STRATEGY_REGISTRY["trend_rider_v9"](), "trend_rider_v9_vt32": lambda: STRATEGY_REGISTRY["trend_rider_v9_vt32"](), "trend_rider_v10": lambda: STRATEGY_REGISTRY["trend_rider_v10"](), "trend_rider_v10_vt32": lambda: STRATEGY_REGISTRY["trend_rider_v10_vt32"](), "trend_rider_v10_vt36": lambda: STRATEGY_REGISTRY["trend_rider_v10_vt36"](), # V9 candidates "v9vt24_t20": lambda: lab.TrendRiderV9(target_vol=0.24, min_lev=0.5, trail_pct=0.20), "v9vt28_t20": lambda: lab.TrendRiderV9(trail_pct=0.20), "v9vt32_t20": lambda: lab.TrendRiderV9(target_vol=0.32, min_lev=0.7, trail_pct=0.20), "v9vt34_t20": lambda: lab.TrendRiderV9(target_vol=0.34, min_lev=0.72, trail_pct=0.20), "v9vt34_t20_h10": lambda: lab.TrendRiderV9(target_vol=0.34, min_lev=0.72, trail_pct=0.20, pt_min_hold=10), "v9vt34_t20_thr28_h10": lambda: lab.TrendRiderV9(target_vol=0.34, min_lev=0.72, trail_pct=0.20, pt_min_hold=10, pt_threshold=0.28), "v9vt34_t20_thr28_b08_h10": lambda: lab.TrendRiderV9( target_vol=0.34, min_lev=0.72, trail_pct=0.20, pt_min_hold=10, pt_threshold=0.28, pt_band=0.08), "v9vt32_t20_thr28_h10": lambda: lab.TrendRiderV9(target_vol=0.32, min_lev=0.7, trail_pct=0.20, pt_min_hold=10, pt_threshold=0.28), "v9vt36_t20": lambda: lab.TrendRiderV9(target_vol=0.36, min_lev=0.75, trail_pct=0.20), "v9vt36_t22": lambda: lab.TrendRiderV9(target_vol=0.36, min_lev=0.75, trail_pct=0.22), "v9vt28_notrail": lambda: lab.TrendRiderV9(), } BLOCKS = [("2010-03-01", "2013-02-28"), ("2013-03-01", "2016-09-26"), ("2016-09-27", "2021-09-26"), ("2021-09-27", "2026-09-25"), ("2010-03-01", "2026-09-25")] def block_stats(eq: pd.Series, a: str, b: str) -> dict: r = eq.pct_change().dropna().loc[a:b] e = (1 + r).cumprod() return {"cagr": e.iloc[-1] ** (252 / len(r)) - 1, "sharpe": r.mean() / r.std() * 252 ** 0.5, "mdd": float(((e / e.cummax()) - 1).min())} def main() -> None: refresh = "--refresh" in sys.argv if refresh or not os.path.exists(OUT): tickers = sorted(set(ETF_UNIVERSE) | {"SPY", "SHY"}) close, opens = fetch(tickers) with open(OUT, "wb") as fh: pickle.dump({"close": close, "opens": opens}, fh) d = pickle.load(open(OUT, "rb")) close, opens = d["close"], d["opens"] print("panel", close.shape, close.index.min().date(), close.index.max().date()) rows = [] for name, factory in VARIANTS.items(): held = strategy_positions(factory(), close, open_data=opens) eq, _ = simulate_live_book(held, close, 10_000, 0.0005, 2.0, 0, 0, 50.0, RESIZE_BAND) full = block_stats(eq, *BLOCKS[-1]) per = {f"{a[:4]}-{b[:4]}": block_stats(eq, a, b) for a, b in BLOCKS[:-1]} rows.append({"name": name, **full, **{k: v["sharpe"] for k, v in per.items()}}) df = pd.DataFrame(rows) pd.set_option("display.width", 220) print("\nfull-period 2010-2026:") print(df[["name", "cagr", "sharpe", "mdd"]].to_string( index=False, formatters={"cagr": "{:.1%}".format, "sharpe": "{:.2f}".format, "mdd": "{:.1%}".format})) print("\nblock Sharpes:") print(df[["name", "2010-2013", "2013-2016", "2016-2021", "2021-2026"]].to_string( index=False, formatters={c: "{:.2f}".format for c in ["2010-2013", "2013-2016", "2016-2021", "2021-2026"]})) if __name__ == "__main__": main()