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:
82
scripts/surfer/growth_discipline_paper.py
Normal file
82
scripts/surfer/growth_discipline_paper.py
Normal 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()
|
||||
Reference in New Issue
Block a user