Files
foxhunt/scripts/surfer/portfolio_harvest2.py
jgrusewski 244ccaaf0b 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>
2026-06-07 00:42:32 +02:00

99 lines
4.0 KiB
Python

#!/usr/bin/env python3
"""Clean premium-harvest: risk-parity across ~5 broad ASSET CLASSES (not 39 instruments).
Equity (ES), bonds (ZN), gold (GC), energy (CL), crypto (BTC) — genuinely uncorrelated premia.
Risk-parity (inv-vol) + vol-target + optional trend-overlay, vs 60/40 and buy-and-hold equity.
Runs 4-asset (2010-2026, longer) and 5-asset (+BTC, 2019-2026). The fair test of whether the
risk system adds value over plain 60/40 when applied to the RIGHT universe.
Futures = roll-zeroed price returns (understates commodity carry); BTC = clean close-close.
"""
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
import pit_sweep # 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 build():
roots, fdays, close, op, inst = load_panel()[:5]
lc, R, ov, intr = build_returns(close, op, inst)
fcol = {r: roots.index(r) for r in ["ES", "ZN", "GC", "CL"] if r in roots}
fut = {r: {int(fdays[t]): R[t, c] for t in range(len(fdays))} for r, c in fcol.items()}
syms, cdays, cc, cqv, cf = pit_sweep.load()
j = syms.index("BTCUSDT"); lcb = np.log(cc[:, j])
btc = {int(cdays[t]): (lcb[t] - lcb[t - 1]) for t in range(1, len(cdays)) if np.isfinite(lcb[t]) and np.isfinite(lcb[t - 1])}
return fut, btc
def run(assets, rets, label):
days = np.array(sorted(set.intersection(*[set(rets[a]) for a in assets])))
M = np.array([[rets[a][int(d)] for a in assets] for d in days]) # [T, K]
M = np.where(np.isfinite(M), M, 0.0)
T, K = M.shape
year = (1970 + days / 365.25).astype(int)
vol = np.full((T, K), np.nan)
for t in range(63, T):
vol[t] = M[t - 63:t].std(0)
iv = 1.0 / np.where(vol > 0, vol, np.nan)
trend = np.full((T, K), np.nan)
L = 126
for t in range(L, T):
trend[t] = M[t - L:t].sum(0)
def book(w):
w = np.nan_to_num(w); g = np.abs(w).sum(1, keepdims=True); g[g == 0] = 1
w = w / g
return np.sum(w[:-1] * M[1:], axis=1)
def vt(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, TVOL / (rv + 1e-9))
return out
rp = book(iv)
rp_t = book(np.where(trend > 0, iv, 0.0))
es_i = assets.index("ES") if "ES" in assets else 0
bench_eq = M[1:, es_i]
w60 = np.zeros((T, K)); w60[:, es_i] = 0.6
if "ZN" in assets:
w60[:, assets.index("ZN")] = 0.4
print(f"\n===== {label}: {assets} ({T} days) =====")
print(f"{'strategy':>24} {'Sharpe':>7} {'ann%':>6} {'maxDD%':>7} {'Calmar':>7}")
for nm, r in [("equity-only (ES)", bench_eq), ("60/40", book(w60)),
("risk-parity", rp), ("RP + vol-target", vt(rp)),
("RP + trend + vol-target", vt(rp_t))]:
m = metrics(r)
print(f"{nm:>24} {m['sr']:>+7.2f} {100*m['ann']:>+6.1f} {100*m['dd']:>+7.1f} {m['cal']:>7.2f}")
best = vt(rp)
print(" per-year Sharpe (RP+vol-target): " + " ".join(
f"{y}:{metrics(best[year[1:]==y])['sr']:+.1f}" for y in sorted(set(year)) if (year[1:] == y).sum() > 100))
def main():
fut, btc = build()
run(["ES", "ZN", "GC", "CL"], {**fut}, "4-asset (2010-2026)")
run(["ES", "ZN", "GC", "CL", "BTC"], {**fut, "BTC": btc}, "5-asset +crypto (2019-2026)")
print("\nVERDICT: if RP/vol-target beats 60/40 on Sharpe AND maxDD = risk system adds real value on the")
print("right universe (the engine's true job). If 60/40 still wins, the deployable answer is just simple harvest.")
if __name__ == "__main__":
main()