feat(harvest): Sharpe-ceiling question answered — triangulated ~0.7-0.9 for retail
4 omnisearch researchers + 60/40+trend-sleeve test. Three independent confirmations converge: (1) our lab: 60/40+trend cuts maxDD (-20.5->-16.5pct) but no Sharpe lift (naive trend=0.04 standalone); (2) literature: retail ceiling ~0.7-1.0, AQR QSPIX 0.46 live vs 0.7 target, ~73pct backtest->live haircut, 35-58pct alpha decay; (3) adversarial: Sharpe 2-6 is infrastructure-gated (HFT/Medallion), not software. Exceptions: crypto funding carry (only retail ~1.5-2.5 path, but crypto+tail); proper trend ETF (DBMF) on 60/40 (~0.8 + lower DD). Vol-selling = Sharpe mirage. Deployable best: 60/40 (~0.71) or 60/40+DBMF (~0.8). The lever for more dollars is capital, not Sharpe. Definitive: ~0.7-0.9 is the retail ceiling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
93
scripts/surfer/portfolio_harvest3.py
Normal file
93
scripts/surfer/portfolio_harvest3.py
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The one evidence-backed non-crypto lever: add a diversified TREND sleeve to 60/40.
|
||||
|
||||
Research verdict: trend/managed-futures is the only premium genuinely uncorrelated-to-equity AND
|
||||
convex in crises (made money in 2022 when stocks+bonds both fell). We tested trend standalone
|
||||
(marginal) and as a filter (hurt) — never as a DIVERSIFYING SLEEVE on 60/40, the specific thing
|
||||
the evidence supports. Build a long/SHORT diversified time-series-momentum book (vol-targeted),
|
||||
blend with 60/40 at various weights, measure Sharpe + maxDD + crisis-year behavior. Honest
|
||||
question: does 60/40 + trend beat plain 60/40 on risk-adjusted terms (~0.7 -> ~0.8-0.9)?
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from signal_sweep import load_panel, build_returns # noqa: E402
|
||||
|
||||
TVOL = 0.10
|
||||
|
||||
|
||||
def metrics(r):
|
||||
r = np.asarray(r); r = r[np.isfinite(r)]
|
||||
if len(r) < 50 or r.std() == 0:
|
||||
return dict(sr=float("nan"), ann=float("nan"), dd=float("nan"), cal=float("nan"))
|
||||
ann = r.mean() * 252; vol = r.std() * math.sqrt(252)
|
||||
eq = np.cumprod(1 + r); dd = float((eq / np.maximum.accumulate(eq) - 1).min())
|
||||
return dict(sr=ann / vol, ann=ann, dd=dd, cal=ann / (abs(dd) + 1e-9))
|
||||
|
||||
|
||||
def voltarget(r, win=63):
|
||||
out = np.zeros_like(r)
|
||||
for t in range(win, len(r)):
|
||||
rv = np.std(r[t - win:t]) * math.sqrt(252)
|
||||
out[t] = r[t] * min(3.0, TVOL / (rv + 1e-9))
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
roots, days, close, op, inst = load_panel()[:5]
|
||||
lc, R, ov, intr = build_returns(close, op, inst)
|
||||
T, N = close.shape
|
||||
year = (1970 + days / 365.25).astype(int)
|
||||
vol63 = np.full_like(lc, np.nan)
|
||||
for t in range(63, T):
|
||||
vol63[t] = np.nanstd(R[t - 63:t], axis=0)
|
||||
iv = 1.0 / np.where(vol63 > 0, vol63, np.nan)
|
||||
avail = np.isfinite(close) & np.isfinite(vol63) & (vol63 > 0)
|
||||
|
||||
es, zn = roots.index("ES"), roots.index("ZN")
|
||||
r6040 = 0.6 * R[1:, es] + 0.4 * R[1:, zn] # plain 60/40 (daily returns)
|
||||
|
||||
# diversified long/SHORT time-series-momentum (CTA) sleeve, vol-targeted
|
||||
tsig = np.full_like(lc, np.nan)
|
||||
tsig[252:] = np.sign(lc[252:] - lc[:-252]) # long if 12mo up, short if down
|
||||
w = np.where(avail & np.isfinite(tsig), tsig * iv, 0.0)
|
||||
g = np.sum(np.abs(w), axis=1, keepdims=True); g[g == 0] = 1
|
||||
w = w / g
|
||||
rtrend_raw = np.sum(w[:-1] * R[1:], axis=1)
|
||||
rtrend = voltarget(rtrend_raw) # the managed-futures sleeve
|
||||
|
||||
# normalize both legs to ~TVOL so blend weights are meaningful, then blend
|
||||
def norm(r):
|
||||
return voltarget(r)
|
||||
base = norm(r6040)
|
||||
sleeve = rtrend
|
||||
yr = year[1:]
|
||||
|
||||
print(f"\n===== 60/40 + TREND SLEEVE ({N}-asset long/short CTA, vol-targeted) =====")
|
||||
print(f"{'blend':>22} {'Sharpe':>7} {'ann%':>6} {'maxDD%':>7} {'Calmar':>7}")
|
||||
blends = [("100% 60/40", 1.0, 0.0), ("80/20 +trend", 0.8, 0.2), ("70/30 +trend", 0.7, 0.3),
|
||||
("60/40 +trend", 0.6, 0.4), ("50/50 +trend", 0.5, 0.5), ("100% trend", 0.0, 1.0)]
|
||||
rows = {}
|
||||
for nm, a, b in blends:
|
||||
r = a * base + b * sleeve
|
||||
rows[nm] = r; m = metrics(r)
|
||||
print(f"{nm:>22} {m['sr']:>+7.2f} {100*m['ann']:>+6.1f} {100*m['dd']:>+7.1f} {m['cal']:>7.2f}")
|
||||
# correlation of the two legs (the diversification engine)
|
||||
mask = np.isfinite(base) & np.isfinite(sleeve) & (base != 0) & (sleeve != 0)
|
||||
corr = np.corrcoef(base[mask], sleeve[mask])[0, 1]
|
||||
print(f"\n corr(60/40, trend) = {corr:+.2f} (negative/low = the diversification that lifts Sharpe + cuts DD)")
|
||||
print(" CRISIS-YEAR check — Sharpe by year (does trend save the equity/bond drawdowns?):")
|
||||
for nm in ["100% 60/40", "70/30 +trend"]:
|
||||
r = rows[nm]
|
||||
py = " ".join(f"{y}:{metrics(r[yr==y])['sr']:+.1f}" for y in [2015, 2018, 2020, 2022, 2025] if (yr == y).sum() > 100)
|
||||
print(f" {nm:>14}: {py}")
|
||||
print("\nVERDICT: if 70/30 or 60/40+trend Sharpe > plain 60/40 AND maxDD smaller (esp 2022) = the evidence-backed")
|
||||
print("lever works; ~0.7 -> ~0.8-0.9 with crisis protection. If not, 60/40 is genuinely the retail ceiling.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user