feat(harvest): premium-harvesting tested — simple 60/40 beats the sophisticated system
The strategy class we never tried: harvest structural premia + risk system, not predict. Diversified futures + risk-parity/vol-target/trend; clean 5-asset-class version (ES/ZN/GC/CL/BTC). Result: plain 60/40 (Sharpe +0.72, 2010-2026) BEATS risk-parity+vol-target (+0.46) and RP+trend (+0.33, trend hurts); 5-asset+crypto RP only ties 60/40 and loses to buy-hold equity. Meta-pattern now complete in BOTH games: simple beats/equals sophisticated in prediction AND harvesting. Constructive deliverable: a simple premium harvest (60/40 / risk-parity) IS a real deployable robust strategy (~0.5-0.72 Sharpe, low DD, no prediction, minimal complexity). The engine's sophistication was never the return-generator -- deployable path is light (harvest + risk overlay), engine's value is infra/discipline/product. (Also: CAISO intraday gate blocked by OASIS plumbing.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
90
scripts/surfer/portfolio_harvest.py
Normal file
90
scripts/surfer/portfolio_harvest.py
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The strategy class we never tested: HARVEST structural premia with the risk system, not predict.
|
||||
|
||||
Diversified multi-asset futures (equity/rates/metals/energy/ags/FX). Build a ladder:
|
||||
equal-weight -> inverse-vol (risk-parity) -> + vol-target -> + trend-overlay (long-flat by 252d trend,
|
||||
the crisis-alpha de-risk). Measure RISK-ADJUSTED (Sharpe, ann-vol, max-DD, Calmar, per-year) vs
|
||||
benchmarks (equal-weight, equity-only, 60/40). No prediction/alpha needed — just disciplined
|
||||
harvesting + risk control (what the engine is actually good at). Roll-zeroed returns (understates
|
||||
carry; fine for comparing the risk-management strategies).
|
||||
"""
|
||||
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
|
||||
|
||||
TARGET_VOL = 0.10 # 10% annualized portfolio vol
|
||||
|
||||
|
||||
def metrics(r):
|
||||
r = r[np.isfinite(r)]
|
||||
if len(r) < 50 or r.std() == 0:
|
||||
return dict(sr=float("nan"), ann=float("nan"), vol=float("nan"), dd=float("nan"), calmar=float("nan"))
|
||||
ann = r.mean() * 252; vol = r.std() * math.sqrt(252); sr = ann / vol
|
||||
eq = np.cumprod(1 + r); dd = float((eq / np.maximum.accumulate(eq) - 1).min())
|
||||
return dict(sr=sr, ann=ann, vol=vol, dd=dd, calmar=ann / (abs(dd) + 1e-9))
|
||||
|
||||
|
||||
def main():
|
||||
roots, days, close, op, inst, weekday = load_panel()
|
||||
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)
|
||||
|
||||
def port(weights):
|
||||
w = np.nan_to_num(weights)
|
||||
w = np.where(avail, w, 0.0)
|
||||
g = np.sum(np.abs(w), axis=1, keepdims=True); g[g == 0] = 1
|
||||
w = w / g # unit gross (fully invested)
|
||||
return np.sum(w[:-1] * R[1:], axis=1)
|
||||
|
||||
def voltarget(r):
|
||||
out = np.zeros_like(r)
|
||||
for t in range(63, len(r)):
|
||||
rv = np.std(r[t - 63:t]) * math.sqrt(252)
|
||||
out[t] = r[t] * min(3.0, TARGET_VOL / (rv + 1e-9)) # scale to target vol, cap 3x leverage
|
||||
return out
|
||||
|
||||
# weight schemes
|
||||
eqw = np.where(avail, 1.0, 0.0)
|
||||
rp = np.where(avail, iv, 0.0) # inverse-vol risk parity
|
||||
trend = (np.full_like(lc, np.nan)); trend[252:] = lc[252:] - lc[:-252]
|
||||
rp_trend = np.where(avail & (trend > 0), iv, 0.0) # trend-overlay: hold only uptrending assets
|
||||
|
||||
books = {
|
||||
"equal-weight": port(eqw),
|
||||
"risk-parity (inv-vol)": port(rp),
|
||||
"risk-parity + vol-target": voltarget(port(rp)),
|
||||
"RP + trend + vol-target": voltarget(port(rp_trend)),
|
||||
}
|
||||
# benchmarks
|
||||
es = roots.index("ES") if "ES" in roots else 0
|
||||
bench = {"equity-only (ES)": R[1:, es]}
|
||||
if "ZN" in roots:
|
||||
zn = roots.index("ZN")
|
||||
w6040 = np.zeros((T, N)); w6040[:, es] = 0.6; w6040[:, zn] = 0.4
|
||||
bench["60/40 (ES/ZN)"] = port(w6040)
|
||||
|
||||
print(f"\n===== PREMIUM HARVEST (diversified futures, {N} assets, {T} days, target {int(TARGET_VOL*100)}% vol) =====")
|
||||
print(f"{'strategy':>26} {'Sharpe':>7} {'ann%':>6} {'vol%':>6} {'maxDD%':>7} {'Calmar':>7}")
|
||||
for nm, r in {**bench, **books}.items():
|
||||
m = metrics(r)
|
||||
print(f"{nm:>26} {m['sr']:>+7.2f} {100*m['ann']:>+6.1f} {100*m['vol']:>5.1f} {100*m['dd']:>+7.1f} {m['calmar']:>7.2f}")
|
||||
print("\nper-year Sharpe (RP + trend + vol-target):")
|
||||
rtv = books["RP + trend + vol-target"]
|
||||
print(" " + " ".join(f"{y}:{metrics(rtv[year[1:]==y])['sr']:+.1f}" for y in range(2010, 2027) if (year[1:] == y).sum() > 100))
|
||||
print("\nVERDICT: if risk-managed harvest beats benchmarks on Sharpe/Calmar (esp lower maxDD) = the engine's")
|
||||
print("real job (risk management of real premia) delivers, no alpha needed. Modest but robust = the honest win.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user