#!/usr/bin/env python3 """ HV-RSI: IS/OOS temporal-stability runner + R2K universe run. Runs the HV-RSI portfolio simulation over an in-sample and an out-of-sample window for a given IndexCon universe, in a single process so the quote cache is reused across windows. IMPORTANT framing: no parameters are fit on the IS window. All parameters come verbatim from Glenn Osborne's HV-RSI v1.0 doc. This split is therefore a TEMPORAL STABILITY check (does the system hold up in the recent unseen window), not an overfitting check. Default split: IS : 2005-01-01 -> 2017-12-31 (13 yrs) OOS : 2018-01-01 -> 2026-06-01 (~8.4 yrs, incl. 2020 COVID + 2022 bear) Usage: python run_oos.py --index SPY_SandP_500 python run_oos.py --index IWM_Russell_2000 """ import argparse from datetime import date from pathlib import Path import polars as pl from prototype import run_simulation, compute_metrics, save_trades WINDOWS = { "IS": (date(2005, 1, 1), date(2017, 12, 31)), "OOS": (date(2018, 1, 1), date(2026, 6, 1)), } def fmt_metrics(label: str, m: dict) -> dict: """Flatten metrics into a single reporting row.""" if m.get("n_trades", 0) == 0: return {"window": label, "n_trades": 0} return { "window": label, "n_trades": m["n_trades"], "win_rate": round(m["win_rate"], 4), "avg_return": round(m["avg_return"], 4), "avg_win": round(m["avg_win"], 4), "avg_loss": round(m["avg_loss"], 4), "profit_factor": round(m["profit_factor"], 3), "max_drawdown": round(m["max_drawdown"], 4), "cagr": round(m["cagr"], 4), "return_dd": round(m["return_dd"], 3), "avg_hold_days": round(m["avg_hold_days"], 2), "avg_exposure_pct": round(m["avg_exposure_pct"], 4), "final_equity": round(m["final_equity"], 0), "years": round(m["years"], 2), "exit_target": m["exit_reasons"].get("target", 0), "exit_time": m["exit_reasons"].get("time", 0), "exit_eod": m["exit_reasons"].get("eod_close", 0), } def main(): parser = argparse.ArgumentParser(description="HV-RSI IS/OOS runner") parser.add_argument("--index", default="SPY_SandP_500", help="IndexCon membership file name") args = parser.parse_args() rows = [] for label, (start, end) in WINDOWS.items(): print("\n" + "#" * 64) print(f"# {args.index} — {label} window: {start} to {end}") print("#" * 64) result = run_simulation(args.index, start, end, dry_run=False) m = compute_metrics(result) rows.append(fmt_metrics(label, m)) # Persist per-window trades + equity out_dir = Path(__file__).parent / "output" / args.index / f"oos_{label}" save_trades(result, out_dir) # Combined report print("\n" + "=" * 64) print(f"HV-RSI IS/OOS SUMMARY — {args.index}") print("=" * 64) df = pl.DataFrame(rows) with pl.Config(tbl_cols=-1, tbl_width_chars=200): print(df) out_csv = Path(__file__).parent / "output" / args.index / "oos_summary.parquet" out_csv.parent.mkdir(parents=True, exist_ok=True) df.write_parquet(out_csv) print(f"\nSaved summary -> {out_csv}") if __name__ == "__main__": main()