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>
83 lines
3.9 KiB
Python
83 lines
3.9 KiB
Python
#!/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()
|