feat: growth-discipline forward-tracker (NAV sim, alongside the live book)

Works exactly like multistrat_paper.py (daily cron, status/run/weights, idempotent, forward-start,
catch-up) but tracks the GD strategy: 70% QQQ + 30% multi-strat book, continuously rebalanced. NAV
simulation (no orders) running alongside the live IBKR-paper book to compare forward: GD = higher
CAGR, deeper drawdown by design. Cron 12/15/18 UTC. GD_EQUITY_WEIGHT configurable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-06-08 09:45:35 +02:00
parent a3f309dd7e
commit 02b851ca8b

View File

@@ -0,0 +1,82 @@
#!/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()