#!/usr/bin/env python3 """growth-discipline — forward-tracker (NAV simulation), works exactly like multistrat_paper.py. Strategy: GD = w·QQQ (growth core) + (1-w)·multi-strat book (diversified low-dd sleeve), continuously rebalanced. Runs ALONGSIDE the live IBKR-paper book to compare forward: more CAGR, deeper drawdown. No capital, no orders — pure NAV tracking (the live book is the IBKR-paper one). Cron daily. python3 growth_discipline_paper.py status cum return + ann-Sharpe + maxDD + current weights python3 growth_discipline_paper.py run book new trading days, persist (cron) python3 growth_discipline_paper.py weights [USD] today's $ split (QQQ vs book) Env: GD_EQUITY_WEIGHT (default 0.70). Caveat: backtest GD-70 ~13%/yr CAGR, -38% maxDD (2006-26); execution-alpha (behavior gap, tax-harvest) is on top, not modeled here. """ import datetime import json import math import os import sys import numpy as np sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from multistrat_paper import build, book_series, yhist # noqa: E402 _REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) STATE = os.path.join(_REPO, "data/surfer/gd_paper_state.json") WEQ = float(os.environ.get("GD_EQUITY_WEIGHT", "0.70")) def build_gd(): """GD daily returns aligned to the book's trading dates.""" dates, R = build() # book instruments (SPY/IEF/GLD/PDBC/DBMF/BTC) book, _, _ = book_series(R) qp = yhist("QQQ") # {date: adjclose} q = np.array([qp.get(d, np.nan) for d in dates]) qr = np.zeros(len(dates)) qr[1:] = np.where(np.isfinite(q[1:]) & np.isfinite(q[:-1]), q[1:] / q[:-1] - 1, 0.0) gd = WEQ * qr + (1 - WEQ) * book return dates, gd def load_state(): if os.path.exists(STATE): return json.load(open(STATE)) return {"last_date": "", "days": 0, "sum_r": 0.0, "sumsq_r": 0.0, "equity": 1.0, "peak": 1.0, "max_dd": 0.0} def main(): cmd = sys.argv[1] if len(sys.argv) > 1 else "status" if cmd in ("run", "paper"): st = load_state(); dates, gd = build_gd() os.makedirs(os.path.dirname(STATE), exist_ok=True) if not st["last_date"]: st["last_date"] = dates[-1]; json.dump(st, open(STATE, "w")) print(f"GD forward tracking initialized from {dates[-1]} (w_QQQ={WEQ})"); return booked = 0 for i in range(1, len(dates)): if dates[i] <= st["last_date"]: continue r = float(gd[i]) st["days"] += 1; st["sum_r"] += r; st["sumsq_r"] += r * r st["equity"] *= (1 + r); st["peak"] = max(st["peak"], st["equity"]) st["max_dd"] = min(st["max_dd"], st["equity"] / st["peak"] - 1); st["last_date"] = dates[i]; booked += 1 json.dump(st, open(STATE, "w")) print(f"{datetime.date.today()} GD booked {booked} day(s) -> day {st['days']} (through {st['last_date']}) cum {100*(st['equity']-1):+.2f}%") elif cmd == "weights": cap = float(sys.argv[2]) if len(sys.argv) > 2 else 35000.0 print(f"GD target split for ${cap:,.0f} (continuously rebalanced):") print(f" {'QQQ (growth core)':>22}: {100*WEQ:>5.1f}% ${cap*WEQ:>10,.0f}") print(f" {'multi-strat book':>22}: {100*(1-WEQ):>5.1f}% ${cap*(1-WEQ):>10,.0f}") else: st = load_state() n = st["days"]; m = (st["sum_r"] / n) if n else 0; var = (st["sumsq_r"] / n - m * m) if n > 1 else 0 sh = (m * 252) / (math.sqrt(max(var, 1e-12)) * math.sqrt(252)) if n > 1 and var > 0 else float("nan") print(f"growth-discipline paper (w_QQQ={WEQ}) — day {n} (through {st['last_date'] or 'n/a'})") print(f" cum {100*(st['equity']-1):+.2f}% ann-Sharpe {sh:+.2f} maxDD {100*st['max_dd']:+.1f}%") print(f" (compare vs the live IBKR-paper book; GD = higher CAGR, deeper drawdown by design)") if __name__ == "__main__": main()