doc+tool: CAGR-maximizer reference plan + side-by-side strategy comparison
Honest reference plan (docs/.../2026-06-08-cagr-reference-plan.md): CAGR builds wealth not Sharpe; more CAGR always = more drawdown; only free CAGR is drag reduction; book's 0.96 Sharpe was an rf=0 artifact (excess-over-financing, equities win); leverage moat = cheap financing. Recommendation: max equity + low drag + contribute + never sell, optional 1.2-1.3x futures if you survive -65%. strategy_compare.py: re-runnable side-by-side (book / SPY 1x / SPY 1.3x / 60-40 / 70-30 / overlay) with backtest stats + the P0+PMT trajectory. Configurable --p0 --pmt --fin --years. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
98
scripts/surfer/strategy_compare.py
Normal file
98
scripts/surfer/strategy_compare.py
Normal file
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Side-by-side comparison of the wealth candidates (the CAGR reference plan, re-runnable).
|
||||
|
||||
Backtest (2006-2026, incl 2008) + the $360k+$8k/mo 20y trajectory for: adaptive multi-strat book,
|
||||
SPY buy-hold, SPY 1.3x (cheap futures financing), 60/40, 70/30 SPY+book, SPY+adaptive overlay.
|
||||
Honest: CAGR builds wealth; more CAGR = more drawdown. See docs/.../2026-06-08-cagr-reference-plan.md.
|
||||
|
||||
python3 strategy_compare.py # full table
|
||||
python3 strategy_compare.py --p0 100000 --pmt 2000 --fin 0.03 --years 20
|
||||
"""
|
||||
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 arg(flag, default, cast=float):
|
||||
return cast(sys.argv[sys.argv.index(flag) + 1]) if flag in sys.argv else default
|
||||
|
||||
|
||||
def yh(s):
|
||||
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"]
|
||||
ts = r["timestamp"]
|
||||
return {datetime.datetime.utcfromtimestamp(t).strftime("%Y-%m-%d"): float(c) for t, c in zip(ts, a) if c is not None}
|
||||
|
||||
|
||||
def stats(r):
|
||||
r = np.nan_to_num(r)
|
||||
eq = np.cumprod(1 + r); cagr = eq[-1] ** (252 / len(r)) - 1
|
||||
vol = r.std() * math.sqrt(252)
|
||||
dd = float((eq / np.maximum.accumulate(eq) - 1).min())
|
||||
rf = 0.03; sharpe = (r.mean() * 252 - rf) / vol if vol > 0 else 0 # excess-over-financing Sharpe
|
||||
return cagr, vol, sharpe, dd
|
||||
|
||||
|
||||
def main():
|
||||
P0 = arg("--p0", 360000.0); PMT = arg("--pmt", 8000.0); FIN = arg("--fin", 0.03); YEARS = int(arg("--years", 20))
|
||||
syms = ["SPY", "IEF", "GLD", "DBC"]
|
||||
data = {s: yh(s) for s in syms}
|
||||
dates = sorted(set.intersection(*[set(d) for d in data.values()]))
|
||||
T = len(dates)
|
||||
Pm = np.column_stack([np.array([data[s][d] for d in dates]) for s in syms])
|
||||
R = np.zeros((T, 4)); R[1:] = Pm[1:] / Pm[:-1] - 1
|
||||
spy, ief = R[:, 0], R[:, 1]
|
||||
|
||||
# multi-strat book (proxy: SPY/IEF/GLD/DBC + DIY trend sleeve)
|
||||
lc = np.log(Pm); sg = np.zeros((T, 4)); sg[252:] = np.sign(lc[252:] - lc[:-252])
|
||||
vol = np.full((T, 4), np.nan)
|
||||
for t in range(63, T):
|
||||
vol[t] = R[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
|
||||
trend = np.zeros(T); trend[1:] = np.sum(w[:-1] * R[1:], axis=1)
|
||||
book, _, _ = book_series(np.column_stack([R, trend]))
|
||||
|
||||
# SPY + adaptive overlay (vol-target 15% + 200d trend + drawdown de-lever)
|
||||
lp = np.log(Pm[:, 0]); ma = np.array([lp[max(0, t - 200):t].mean() if t >= 200 else lp[t] for t in range(T)])
|
||||
ovr = np.zeros(T); eqo = pk = 1.0
|
||||
for t in range(63, T - 1):
|
||||
rv = spy[t - 63:t].std() * math.sqrt(252); vs = min(1.0, 0.15 / (rv + 1e-9))
|
||||
tr = 1.0 if lp[t] > ma[t] else 0.3
|
||||
dd = eqo / pk - 1; dds = float(np.clip(1 + 3 * min(0, dd + 0.07), 0.4, 1.0))
|
||||
e = float(np.clip(vs * tr * dds, 0, 1.0)); ovr[t + 1] = e * spy[t + 1]; eqo *= (1 + ovr[t + 1]); pk = max(pk, eqo)
|
||||
|
||||
cands = {
|
||||
"SPY buy-hold (1.0x)": spy,
|
||||
"SPY 1.3x (cheap fin)": 1.3 * spy - 0.3 * FIN / 252,
|
||||
"60/40 (SPY/IEF)": 0.6 * spy + 0.4 * ief,
|
||||
"70/30 SPY+book": 0.7 * spy + 0.3 * book,
|
||||
"SPY+adaptive overlay": ovr,
|
||||
"adaptive multi-strat book": book,
|
||||
}
|
||||
|
||||
def fv(cagr):
|
||||
rm = cagr / 12; N = YEARS * 12; gg = (1 + rm) ** N
|
||||
return P0 * gg + PMT * ((gg - 1) / rm)
|
||||
|
||||
print(f"STRATEGY COMPARISON — {dates[0]}..{dates[-1]} | ${P0:,.0f} + ${PMT:,.0f}/mo, {YEARS}y, fin {100*FIN:.0f}%")
|
||||
print(f" {'strategy':>26} | CAGR | vol | Sharpe* | maxDD | {YEARS}y wealth")
|
||||
for nm, r in cands.items():
|
||||
c, v, sh, dd = stats(r)
|
||||
print(f" {nm:>26} | {100*c:>4.1f}% | {100*v:>3.0f}% | {sh:>+5.2f} | {100*dd:>+5.0f}% | ${fv(c):>13,.0f}")
|
||||
print(" *Sharpe = excess over financing (the leverage-relevant one). More CAGR ALWAYS = deeper maxDD.")
|
||||
print(" Reference plan: docs/superpowers/specs/2026-06-08-cagr-reference-plan.md")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user