diff --git a/scripts/surfer/multistrat.py b/scripts/surfer/multistrat.py new file mode 100644 index 000000000..e0f2b4411 --- /dev/null +++ b/scripts/surfer/multistrat.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Nano multi-strat: operate like a hedge fund. Combine uncorrelated premia (equity, bond, gold, +commodity, trend, crypto) risk-parity-weighted + vol-targeted; show the correlation matrix (the +diversification engine), combined Sharpe vs each piece alone, and how leverage scales the return. + +Not market-neutral — it's the diversified-premia + leverage (All-Weather/alt-risk-premia) model. +The point: each stream is modest, but uncorrelated streams combine to a higher Sharpe, then leverage +turns modest Sharpe into real return. Risk management = the engine's actual job.""" +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 + +TV = 0.10 + + +def vt(r, tv=TV, win=63, cap=4.0): + 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(cap, tv / (rv + 1e-9)) + return out + + +def stats(r): + r = r[np.isfinite(r)] + if len(r) < 50 or r.std() == 0: + return (float("nan"),) * 4 + 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 ann, vol, ann / vol, dd + + +def run(streams, days, year, label): + names = list(streams) + M = np.column_stack([vt(streams[n]) for n in names]) # each vol-normalized to 10% + T = M.shape[0] + print(f"\n===== {label} ({T} days) =====") + print(" standalone Sharpe: " + " ".join(f"{n}:{stats(streams[n])[2]:+.2f}" for n in names)) + # correlation matrix of the (vol-normalized) streams + C = np.corrcoef(np.nan_to_num(M).T) + print(" correlation matrix:") + print(" " + " ".join(f"{n[:5]:>6}" for n in names)) + for i, n in enumerate(names): + print(f" {n[:5]:>5} " + " ".join(f"{C[i, j]:>+6.2f}" for j in range(len(names)))) + avg_corr = (C.sum() - len(names)) / (len(names) ** 2 - len(names)) + # combined: equal-risk-weight then vol-target the bundle + combo = vt(np.nanmean(M, axis=1)) + a, v, sh, dd = stats(combo) + best = max(stats(streams[n])[2] for n in names) + print(f" avg pairwise corr: {avg_corr:+.2f} (low = diversification works)") + print(f" COMBINED (risk-parity + vol-target 10%): Sharpe {sh:+.2f} ann {100*a:+.1f}% maxDD {100*dd:+.1f}%") + print(f" vs best single stream Sharpe {best:+.2f} -> diversification lift {sh-best:+.2f}") + print(f" per-year Sharpe: " + " ".join(f"{y}:{stats(combo[year == y])[2]:+.1f}" for y in sorted(set(year)) if (year == y).sum() > 100)) + print(f" LEVERAGE scaling (same Sharpe {sh:+.2f}): " + + " ".join(f"{lev}x->{100*a*lev:+.0f}%/yr@{int(100*v*lev)}%vol" for lev in (1, 2, 3))) + return combo + + +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) + + def C(r): + return roots.index(r) + + # trend: diversified long/short TS-momentum across all futures, vol-targeted + 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) + tsig = np.full_like(lc, np.nan); tsig[252:] = np.sign(lc[252:] - lc[:-252]) + avail = np.isfinite(close) & np.isfinite(vol63) & (vol63 > 0) & np.isfinite(tsig) + w = np.where(avail, tsig * iv, 0.0); 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) + + fut = {"equity": R[:, C("ES")], "bond": R[:, C("ZN")], "gold": R[:, C("GC")], + "commod": R[:, C("CL")], "trend": trend} + run(fut, days, year, "TRADITIONAL 5-stream (2010-2026)") + + # +crypto: align BTC daily returns to futures days + syms, cdays, cc, _, _ = 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])} + mask = np.array([int(d) in btc for d in days]) + idx = np.where(mask)[0] + if len(idx) > 300: + sub = {k: v[idx] for k, v in fut.items()} + sub["crypto"] = np.array([btc[int(days[i])] for i in idx]) + run(sub, days[idx], (1970 + days[idx] / 365.25).astype(int), "6-stream +CRYPTO (2019-2026)") + print("\nVERDICT: combined Sharpe > best single (diversification real) + leverage scales modest Sharpe") + print("to real return = the hedge-fund operating model. Honest: this is alt-risk-premia (~0.5-1.0 live),") + print("levered; NOT market-neutral (long beta falls in everything-down); leverage adds tail + financing.") + + +if __name__ == "__main__": + main() diff --git a/scripts/surfer/multistrat_book.py b/scripts/surfer/multistrat_book.py new file mode 100644 index 000000000..18884cc97 --- /dev/null +++ b/scripts/surfer/multistrat_book.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""Deployable nano multi-strat book with 2x leverage + the foxhunt risk-management layer. + +Streams (risk-parity, vol-normalized): equity, bond, gold, commodity, trend, crypto. +RISK LAYER (the foxhunt principles applied to the book): + 1. vol-target -> scale exposure to TARGET_VOL on trailing realized vol + 2. drawdown CB -> de-lever at -15% (1x) / -25% (0.5x) / -35% (0.25x) [CMDP circuit-breaker] + 3. corr de-risk -> when avg cross-stream corr spikes (diversification breaking in a crisis), cut leverage + 4. Kelly cap -> leverage <= Kelly fraction (mean/var), never over-lever a degrading book + L[t] = min(MAXLEV, L_vol, L_kelly) * dd_mult * corr_mult, applied to yesterday's signal. +Financing cost charged on the borrowed (>1x) portion. Backtest risk-managed-2x vs naive-2x; emit +live target weights for a given capital. ETF mapping for deployment in the verdict. +""" +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 + +TARGET_VOL = 0.10 +MAXLEV = 2.0 +FIN = 0.06 # 6%/yr financing on borrowed portion +DD1, DD2, DD3 = -0.15, -0.25, -0.35 +CORR_HI = 0.45 # avg pairwise corr above which to de-risk + + +def volnorm(r, tv=TARGET_VOL, 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(5.0, tv / (rv + 1e-9)) + return out + + +def stats(r): + r = r[np.isfinite(r)] + 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 ann, vol, (ann / vol if vol > 0 else float("nan")), dd + + +def build_streams(): + roots, days, close, op, inst = load_panel()[:5] + lc, R, ov, intr = build_returns(close, op, inst) + T, N = close.shape + 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) + tsig = np.full_like(lc, np.nan); tsig[252:] = np.sign(lc[252:] - lc[:-252]) + avail = np.isfinite(close) & np.isfinite(vol63) & (vol63 > 0) & np.isfinite(tsig) + w = np.where(avail, tsig * iv, 0.0); 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) + c = {r: roots.index(r) for r in ("ES", "ZN", "GC", "CL")} + fut = {"equity": R[:, c["ES"]], "bond": R[:, c["ZN"]], "gold": R[:, c["GC"]], "commod": R[:, c["CL"]], "trend": trend} + syms, cdays, cc, _, _ = 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])} + idx = np.array([i for i, d in enumerate(days) if int(d) in btc]) + streams = {k: v[idx] for k, v in fut.items()} + streams["crypto"] = np.array([btc[int(days[i])] for i in idx]) + return streams, days[idx], (1970 + days[idx] / 365.25).astype(int) + + +def risk_managed(M, maxlev, with_risk=True): + """M: [T, S] vol-normalized stream returns. Returns book daily returns under the risk layer.""" + base = np.nanmean(M, axis=1) # risk-parity combination + T = len(base) + out = np.zeros(T); eq = 1.0; peak = 1.0 + for t in range(63, T - 1): + rv = np.std(base[t - 63:t]) * math.sqrt(252) + L = min(maxlev, TARGET_VOL / (rv + 1e-9)) # (1) vol-target, capped at maxlev + if with_risk: + mu = base[t - 63:t].mean() * 252; var = (base[t - 63:t].std() ** 2) * 252 + L = min(L, max(0.0, mu / (var + 1e-9))) # (4) Kelly cap + dd = eq / peak - 1.0 # (2) drawdown circuit-breaker + ddm = 1.0 if dd > DD1 else (0.5 if dd > DD2 else (0.25 if dd > DD3 else 0.0)) + cm = np.corrcoef(np.nan_to_num(M[t - 63:t]).T) # (3) correlation de-risk + ac = (cm.sum() - cm.shape[0]) / (cm.shape[0] ** 2 - cm.shape[0]) + corrm = 1.0 if ac < CORR_HI else max(0.4, 1 - (ac - CORR_HI) * 2) + L *= ddm * corrm + L = max(0.0, min(L, maxlev)) + r = L * base[t + 1] - FIN * max(L - 1.0, 0.0) / 252 # financing on borrowed portion + out[t + 1] = r + eq *= (1 + r); peak = max(peak, eq) + return out + + +def main(): + streams, days, year = build_streams() + names = list(streams) + M = np.column_stack([volnorm(streams[n]) for n in names]) + print(f"DEPLOYABLE MULTI-STRAT BOOK — {len(names)} streams, {M.shape[0]} days ({names})") + for lab, wr, lev in [("risk-managed 2x", True, 2.0), ("NAIVE 2x (no risk layer)", False, 2.0), + ("risk-managed 1x", True, 1.0)]: + r = risk_managed(M, lev, wr) + a, v, sh, dd = stats(r) + print(f" {lab:>26}: Sharpe {sh:+.2f} ann {100*a:+.1f}% vol {100*v:.0f}% maxDD {100*dd:+.1f}%") + rm = risk_managed(M, 2.0, True) + print(f" per-year Sharpe (risk-managed 2x): " + " ".join(f"{y}:{stats(rm[year == y])[2]:+.1f}" for y in sorted(set(year)) if (year == y).sum() > 150)) + + # live sizing: current leverage + target $ per stream for capital + base = np.nanmean(M, axis=1); t = len(base) - 1 + rv = np.std(base[t - 63:t]) * math.sqrt(252) + Lv = min(MAXLEV, TARGET_VOL / (rv + 1e-9)) + cap = 35000 + print(f"\n LIVE SIZING (today): vol-target leverage {Lv:.2f}x -> deploy ${cap*Lv:,.0f} gross on ${cap:,.0f}") + per = cap * Lv / len(names) + etf = {"equity": "SPY", "bond": "IEF", "gold": "GLD", "commod": "PDBC", "trend": "DBMF", "crypto": "BTC(spot)"} + for n in names: + print(f" {n:>7} ({etf[n]:>9}): ${per:,.0f}") + print("\n VERDICT: risk-managed 2x should keep maxDD bounded (~-20-30%) vs naive 2x (~-40-50%) at higher") + print(" return than 1x -> the hedge-fund model at retail. Deploy via the ETFs above (2x Reg-T margin) +") + print(" small crypto sleeve. Same ~0.7-Sharpe book; leverage+risk-layer = the fund. Honest: still beta,") + print(" drawdowns real, financing drags, single-window crypto sleeve -> size crypto small.") + + +if __name__ == "__main__": + main() diff --git a/scripts/surfer/multistrat_book_v2.py b/scripts/surfer/multistrat_book_v2.py new file mode 100644 index 000000000..c2e7bd7b0 --- /dev/null +++ b/scripts/surfer/multistrat_book_v2.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Multi-strat book v2 — upgraded with foxhunt risk-stack IDEAS (not crude reimplementation): + + - PER-STREAM EDGE-DECAY TRUST (Page-Hinkley-style decay detection -> continuous theta in [0,1]) + that down-weights a stream whose premium is degrading and re-weights it when it recovers + (resurrection discipline) -> ADAPTIVE allocation vs static risk-parity. + [pearl_edge_decay_detection_is_a_missing_abstraction_layer + dead_signal_resurrection_discipline] + - KELLY-fraction leverage cap [pearl_position_sizing_missing_adaptation_layer] + - CMDP drawdown circuit-breaker [pearl_cmdp_consec_loss_counter] + - correlation-spike de-risk (diversification breaks in crises) +Compares static-risk-parity vs edge-decay-adaptive, both risk-managed 2x. Does the foxhunt idea help? +""" +import math +import os +import sys + +import numpy as np + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from multistrat_book import build_streams, volnorm, stats, TARGET_VOL, FIN, DD1, DD2, DD3, CORR_HI # noqa: E402 + + +def edge_decay_trust(stream, win=126, ema=0.94): + """Page-Hinkley-style per-stream edge health -> theta in [0.1,1]. Detects decay (trailing + risk-adj return falling), floors at 0.1 so a dead stream can RESURRECT when it recovers.""" + T = len(stream); theta = np.ones(T) + th = 1.0 + for t in range(win, T): + seg = stream[t - win:t] + sr = seg.mean() / (seg.std() + 1e-9) * math.sqrt(252) + target = float(np.clip((sr - (-0.5)) / (0.5 - (-0.5)), 0.1, 1.0)) # SR>0.5 ->1, <-0.5 ->0.1 + th = ema * th + (1 - ema) * target # smooth (asymmetric-ish via EMA) + theta[t] = th + return theta + + +def risk_layer(combo, M, maxlev, with_risk=True): + T = len(combo); out = np.zeros(T); eq = 1.0; peak = 1.0 + for t in range(63, T - 1): + rv = np.std(combo[t - 63:t]) * math.sqrt(252) + L = min(maxlev, TARGET_VOL / (rv + 1e-9)) + if with_risk: + mu = combo[t - 63:t].mean() * 252; var = (combo[t - 63:t].std() ** 2) * 252 + L = min(L, max(0.0, mu / (var + 1e-9))) + dd = eq / peak - 1.0 + ddm = 1.0 if dd > DD1 else (0.5 if dd > DD2 else (0.25 if dd > DD3 else 0.0)) + cm = np.corrcoef(np.nan_to_num(M[t - 63:t]).T) + ac = (cm.sum() - cm.shape[0]) / (cm.shape[0] ** 2 - cm.shape[0]) + corrm = 1.0 if ac < CORR_HI else max(0.4, 1 - (ac - CORR_HI) * 2) + L *= ddm * corrm + L = max(0.0, min(L, maxlev)) + r = L * combo[t + 1] - FIN * max(L - 1.0, 0.0) / 252 + out[t + 1] = r; eq *= (1 + r); peak = max(peak, eq) + return out + + +def main(): + streams, days, year = build_streams() + names = list(streams) + M = np.column_stack([volnorm(streams[n]) for n in names]) + T = M.shape[0] + # edge-decay trust per stream + Theta = np.column_stack([edge_decay_trust(M[:, i]) for i in range(len(names))]) + static = np.nanmean(M, axis=1) # equal risk-parity + tw = Theta / np.maximum(Theta.sum(1, keepdims=True), 1e-9) # trust-weighted + adaptive = np.nansum(tw * np.nan_to_num(M), axis=1) + + print(f"MULTI-STRAT v2 (foxhunt edge-decay-adaptive) — {len(names)} streams, {T} days") + print(f" streams: {names}") + for lab, combo in [("static risk-parity", static), ("edge-decay ADAPTIVE", adaptive)]: + for ln, lev, wr in [("2x risk-managed", 2.0, True), ("naive 2x", 2.0, False)]: + r = risk_layer(combo, M, lev, wr); a, v, sh, dd = stats(r) + print(f" {lab:>20} | {ln:>16}: Sharpe {sh:+.2f} ann {100*a:+.1f}% maxDD {100*dd:+.1f}%") + # show what the trust layer is doing (avg theta per stream + recent) + print(" edge-trust (avg | latest) per stream:") + for i, n in enumerate(names): + print(f" {n:>7}: {Theta[126:, i].mean():.2f} | {Theta[-1, i]:.2f}") + radap = risk_layer(adaptive, M, 2.0, True) + print(f" per-year Sharpe (adaptive 2x): " + " ".join(f"{y}:{stats(radap[year == y])[2]:+.1f}" for y in sorted(set(year)) if (year == y).sum() > 150)) + print("\n VERDICT: edge-decay-adaptive Sharpe > static AND lower maxDD = the foxhunt trust-layer idea adds") + print(" real value (down-weights decaying streams, resurrects recovered ones). If ~equal, static suffices.") + + +if __name__ == "__main__": + main() diff --git a/scripts/surfer/multistrat_book_v3.py b/scripts/surfer/multistrat_book_v3.py new file mode 100644 index 000000000..60dc28061 --- /dev/null +++ b/scripts/surfer/multistrat_book_v3.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Multi-strat book v3 — risk layer rebuilt with foxhunt's ADAPTIVE-controller discipline. + +Fixes the v1/v2 static risk layer (which crushed returns — a one-way latch per +pearl_cmdp_consec_loss_counter_is_one_way_latch). Controllers (all floored / resurrection-capable): + - EMA online vol (not fixed-window std) [Welford/EMA online stats] + - Kelly leverage with FLOOR + bootstrap [pearl_bootstrap_must_respect_clamp_range] + - drawdown de-lever CONTINUOUS + self-recovering [fix the one-way latch -> resurrection] + - correlation de-risk Z-SCORED vs own distribution [adaptive, not fixed threshold] + - leverage floor so nothing dies permanently [pearl_dead_signal_resurrection_discipline] +Combination = edge-decay-adaptive (v2). Compare naive 2x / static-risk 2x / ADAPTIVE-risk 2x. +""" +import math +import os +import sys + +import numpy as np + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from multistrat_book import build_streams, volnorm, stats, TARGET_VOL, FIN, risk_managed # noqa: E402 +from multistrat_book_v2 import edge_decay_trust # noqa: E402 + +KELLY_FLOOR, LEV_FLOOR = 0.5, 0.3 +DD_DEADBAND, DD_SENS, DD_FLOOR = 0.05, 3.0, 0.40 + + +def adaptive_risk(combo, M, maxlev): + T = len(combo); out = np.zeros(T); eq = 1.0; peak = 1.0 + ema_mu = float(np.nanmean(combo[:63])); ema_var = float(np.nanvar(combo[:63])) + 1e-12 + chist = [] + for t in range(63, T - 1): + x = combo[t] + ema_mu = 0.97 * ema_mu + 0.03 * x # EMA online stats + ema_var = 0.97 * ema_var + 0.03 * (x - ema_mu) ** 2 + rv = math.sqrt(max(ema_var, 1e-12) * 252) + L_vol = TARGET_VOL / (rv + 1e-9) + kelly = min(maxlev, max(KELLY_FLOOR, (ema_mu * 252) / (ema_var * 252 + 1e-9))) # floored Kelly + dd = eq / peak - 1.0 + dd_mult = float(np.clip(1.0 - DD_SENS * max(0.0, -dd - DD_DEADBAND), DD_FLOOR, 1.0)) # continuous + recovers + cm = np.corrcoef(np.nan_to_num(M[t - 63:t]).T) + ac = (cm.sum() - cm.shape[0]) / (cm.shape[0] ** 2 - cm.shape[0]) + chist.append(ac) + if len(chist) > 60: + h = np.array(chist[-120:]); z = (ac - h.mean()) / (h.std() + 1e-9) + corr_mult = float(np.clip(1.0 - 0.20 * max(0.0, z), 0.5, 1.0)) + else: + corr_mult = 1.0 + L = float(np.clip(min(L_vol, kelly) * dd_mult * corr_mult, LEV_FLOOR, maxlev)) + r = L * combo[t + 1] - FIN * max(L - 1.0, 0.0) / 252 + out[t + 1] = r; eq *= (1 + r); peak = max(peak, eq) + return out + + +def main(): + streams, days, year = build_streams() + names = list(streams) + M = np.column_stack([volnorm(streams[n]) for n in names]) + Theta = np.column_stack([edge_decay_trust(M[:, i]) for i in range(len(names))]) + tw = Theta / np.maximum(Theta.sum(1, keepdims=True), 1e-9) + combo = np.nansum(tw * np.nan_to_num(M), axis=1) # edge-decay-adaptive combination + + print(f"MULTI-STRAT v3 (adaptive allocation + adaptive risk layer) — {len(names)} streams, {M.shape[0]} days") + res = { + "naive 2x (no risk layer)": np.concatenate([[0], 2.0 * combo[1:] - FIN * 1.0 / 252]), + "static risk layer 2x": risk_managed(M, 2.0, True), # v1 static (recomputes nanmean inside) + "ADAPTIVE risk layer 2x": adaptive_risk(combo, M, 2.0), + "ADAPTIVE risk layer 1x": adaptive_risk(combo, M, 1.0), + } + for lab, r in res.items(): + a, v, sh, dd = stats(r) + print(f" {lab:>26}: Sharpe {sh:+.2f} ann {100*a:+.1f}% vol {100*v:.0f}% maxDD {100*dd:+.1f}%") + radap = res["ADAPTIVE risk layer 2x"] + print(f" per-year Sharpe (ADAPTIVE 2x): " + " ".join(f"{y}:{stats(radap[year == y])[2]:+.1f}" for y in sorted(set(year)) if (year == y).sum() > 150)) + print("\n VERDICT: ADAPTIVE-risk 2x should beat naive 2x AND static-risk 2x on Sharpe AND maxDD =") + print(" proper foxhunt-style risk management makes leverage survivable without killing return.") + + +if __name__ == "__main__": + main()