backtest: growth-discipline multi-premia (QQQ core + trend sleeve + book)
User's '10x boring' idea tested. GD-70 (70%QQQ/15%trend/15%book) has highest Sharpe-excess (0.67-0.84 vs QQQ 0.65, book 0.39); trend sleeve = genuine crisis-alpha. GD@1.3x cheap-lev ~= QQQ return with shallower drawdown (-48 vs -53%). VRP(PUTW) does NOT add (dilutes); carry no clean retail ETF. Clean stackable premia = equity-core + trend-sleeve + execution-alpha (behavior gap/tax/cheap-lev, on top, not in backtest). GD genuinely beats both the over-conservative book and panic-prone raw QQQ. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
113
scripts/surfer/growth_discipline_backtest.py
Normal file
113
scripts/surfer/growth_discipline_backtest.py
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Growth-discipline multi-premia backtest: high-equity (QQQ) core + trend crisis-alpha sleeve +
|
||||
diversified book, mechanically rebalanced, optional cheap-futures leverage. vs QQQ / book / 70-30.
|
||||
Also tests adding VRP (PUTW put-write) as a sleeve. Honest: execution-alpha (behavior gap, tax-loss
|
||||
harvest) is real but counterfactual/account-dependent -> NOT in the backtest; noted as +1-3%/yr on top.
|
||||
|
||||
python3 growth_discipline_backtest.py [--fin 0.03]
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from multistrat_paper import book_series # noqa: E402
|
||||
|
||||
|
||||
def yh(s):
|
||||
try:
|
||||
r = json.loads(urllib.request.urlopen(urllib.request.Request(
|
||||
f"https://query1.finance.yahoo.com/v8/finance/chart/{s}?interval=1d&range=25y",
|
||||
headers={"User-Agent": "Mozilla/5.0"}), timeout=30).read())["chart"]["result"][0]
|
||||
a = r["indicators"].get("adjclose", [{}])[0].get("adjclose") or r["indicators"]["quote"][0]["close"]
|
||||
return {datetime.datetime.utcfromtimestamp(t).strftime("%Y-%m-%d"): float(c) for t, c in zip(r["timestamp"], a) if c}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
FIN = float(sys.argv[sys.argv.index("--fin") + 1]) if "--fin" in sys.argv else 0.03
|
||||
|
||||
|
||||
def build(syms):
|
||||
data = {s: yh(s) for s in syms}
|
||||
common = sorted(set.intersection(*[set(data[s]) for s in syms]))
|
||||
yrs = (datetime.date.fromisoformat(common[-1]) - datetime.date.fromisoformat(common[0])).days / 365.25
|
||||
T = len(common)
|
||||
P = {s: np.array([data[s][d] for d in common]) for s in syms}
|
||||
R = {s: np.concatenate([[0], P[s][1:] / P[s][:-1] - 1]) for s in syms}
|
||||
return common, yrs, T, P, R
|
||||
|
||||
|
||||
def trend_sleeve(P, R, syms, T):
|
||||
"""12-1 TS-momentum long/short on syms, equal-risk = managed-futures proxy."""
|
||||
Pm = np.column_stack([P[s] for s in syms]); Rm = np.column_stack([R[s] for s in syms])
|
||||
lc = np.log(Pm); sg = np.zeros((T, len(syms))); sg[252:] = np.sign(lc[252:] - lc[:-252])
|
||||
vol = np.full((T, len(syms)), np.nan)
|
||||
for t in range(63, T):
|
||||
vol[t] = Rm[t - 63:t].std(0)
|
||||
iv = 1 / np.where(vol > 0, vol, np.nan); w = np.nan_to_num(sg * iv)
|
||||
g = np.abs(w).sum(1, keepdims=True); g[g == 0] = 1; w = w / g
|
||||
tr = np.zeros(T); tr[1:] = np.sum(w[:-1] * Rm[1:], axis=1); return tr
|
||||
|
||||
|
||||
def report(title, cands, yrs):
|
||||
def stats(r):
|
||||
r = np.nan_to_num(r); eq = np.cumprod(1 + r); c = eq[-1] ** (1 / yrs) - 1
|
||||
v = r.std() * math.sqrt(252); dd = float((eq / np.maximum.accumulate(eq) - 1).min())
|
||||
sh = (r.mean() * 252 - FIN) / v if v > 0 else 0
|
||||
return c, v, sh, dd
|
||||
def fv(c, P0=360000, pmt=8000, years=20):
|
||||
rm = c / 12; N = years * 12; gg = (1 + rm) ** N; return P0 * gg + pmt * ((gg - 1) / rm)
|
||||
print(f"\n=== {title} ===")
|
||||
print(f" {'strategy':>30} | CAGR | vol | Shrp* | maxDD | $360k+$8k/mo 20y")
|
||||
for nm, r in cands:
|
||||
c, v, sh, dd = stats(r)
|
||||
print(f" {nm:>30} | {100*c:>4.1f}% | {100*v:>3.0f}% | {sh:>+4.2f} | {100*dd:>+4.0f}% | ${fv(c):>13,.0f}")
|
||||
|
||||
|
||||
def main():
|
||||
# A) long history 2006-2026
|
||||
common, yrs, T, P, R = build(["QQQ", "SPY", "IEF", "GLD", "DBC"])
|
||||
tr = trend_sleeve(P, R, ["SPY", "IEF", "GLD", "DBC"], T)
|
||||
R4 = np.column_stack([R[s] for s in ["SPY", "IEF", "GLD", "DBC"]])
|
||||
book, _, _ = book_series(np.column_stack([R4, tr]))
|
||||
qqq = R["QQQ"]
|
||||
def lev(r, L): return L * r - (L - 1) * FIN / 252
|
||||
gd70 = 0.70 * qqq + 0.15 * tr + 0.15 * book
|
||||
gd60 = 0.60 * qqq + 0.20 * tr + 0.20 * book
|
||||
report(f"A) Long history {common[0]}..{common[-1]} ({yrs:.0f}y), fin {100*FIN:.0f}%", [
|
||||
("QQQ buy-hold", qqq),
|
||||
("book (diversified)", book),
|
||||
("70/30 QQQ+book", 0.7 * qqq + 0.3 * book),
|
||||
("GD-70 (70Q/15trend/15book)", gd70),
|
||||
("GD-60 (60Q/20trend/20book)", gd60),
|
||||
("GD-70 @1.2x cheap lev", lev(gd70, 1.2)),
|
||||
("GD-70 @1.3x cheap lev", lev(gd70, 1.3)),
|
||||
], yrs)
|
||||
print(" *Sharpe = excess over financing. (Execution-alpha — behavior gap ~1-2%/yr, tax-loss")
|
||||
print(" harvest ~0.5-1.5%/yr taxable — is REAL but counterfactual, NOT in these numbers: add on top.)")
|
||||
|
||||
# B) recent window incl VRP (PUTW) + real managed-futures (DBMF)
|
||||
common2, yrs2, T2, P2, R2 = build(["QQQ", "SPY", "IEF", "GLD", "DBC", "PUTW", "DBMF"])
|
||||
tr2 = trend_sleeve(P2, R2, ["SPY", "IEF", "GLD", "DBC"], T2)
|
||||
book2, _, _ = book_series(np.column_stack([np.column_stack([R2[s] for s in ["SPY", "IEF", "GLD", "DBC"]]), tr2]))
|
||||
q2 = R2["QQQ"]; putw = R2["PUTW"]; dbmf = R2["DBMF"]
|
||||
base = 0.70 * q2 + 0.15 * dbmf + 0.15 * book2 # GD with REAL managed-futures ETF
|
||||
with_vrp = 0.60 * q2 + 0.15 * dbmf + 0.15 * book2 + 0.10 * putw
|
||||
report(f"B) Recent {common2[0]}..{common2[-1]} ({yrs2:.0f}y) — does VRP add?", [
|
||||
("QQQ buy-hold", q2),
|
||||
("GD (70Q/15DBMF/15book)", base),
|
||||
("GD + 10% VRP(PUTW)", with_vrp),
|
||||
("PUTW alone (VRP)", putw),
|
||||
("DBMF alone (trend)", dbmf),
|
||||
], yrs2)
|
||||
print(" carry: no clean liquid retail ETF (DBV-type funds are tiny/closed) -> not testable cleanly here.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user