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:
jgrusewski
2026-06-07 01:00:27 +02:00
parent 244ccaaf0b
commit 0fc3eac2ab

View 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()