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>
80 lines
3.9 KiB
Python
80 lines
3.9 KiB
Python
#!/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()
|