Multi-strat with foxhunt's own ideas. (1) Combine uncorrelated premia: ~0.72 Sharpe 2019-26 but ~=60/40, only when streams net-positive (traditional 2010-26 combine +0.51 < equity +0.68 = dilution). (2) Edge-decay-trust allocation (Page-Hinkley theta, resurrection) genuinely helps: +0.16->+0.27, correctly down-weights decayed streams. (3) Static risk layer crushed returns (one-way latch); ADAPTIVE layer (continuous self-recovering DD de-lever + Kelly-floor + z-score corr + EMA vol) beat it (+0.03->+0.14, maxDD -18.7->-14.5) -- value is drawdown control. (4) THE MOAT = cheap financing: adaptive 1x Sharpe +0.48 vs 2x +0.14; retail 6-7% margin kills leverage benefit. Funds lever ~0.7 Sharpe only via prime-brokerage SOFR+1-2%. Deployable best = ~1x adaptive-risk-managed diversified book (~0.5-0.7 Sharpe, unlevered), scales with capital. Foxhunt ideas improve execution (validated); engine value = risk-mgmt not alpha. Ceiling ~0.7 ironclad. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
86 lines
4.3 KiB
Python
86 lines
4.3 KiB
Python
#!/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()
|