Files
foxhunt/scripts/surfer/multistrat_vol_test.py
jgrusewski ce8f8cbd46 test: ML vol-nowcaster (Mamba2/CfC/TLOB proxy) doesn't help risk layer
User asked if advanced architectures help. Tested ML vol-nowcaster (GB proxy) vs EMA for risk-layer
de-lever timing: GB OOS vol-corr +0.09 < EMA +0.18 (EMA captures clustering better); downstream
identical Sharpe +0.72, GB maxDD slightly worse. ML doesn't help -> CfC/Mamba2 won't either (more
overfit). Return-prediction already dead (IC 0.004); TLOB's order-book signal is latency-gated.
Fancy architectures add nothing; engine value = simple adaptive risk/allocation. Simple beats fancy,
confirmed once more. Adaptive multistrat book (EMA-vol, ~0.72 Sharpe, -8.5% maxDD) is the answer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 20:14:25 +02:00

84 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""Does an ML vol-nowcaster (proxy for CfC/Mamba2) improve the risk layer's de-lever timing vs EMA?
Target: next-20d realized vol of the book's combination. Forecasters: EMA (baseline, what the book
uses) vs gradient-boosting (walk-forward, leak-free) on vol features. Compare (1) OOS vol-forecast
skill, (2) downstream: vol-targeting with ML-vol vs EMA-vol -> Sharpe / maxDD / realized-vol stability.
If GB ~ EMA downstream, the heavier CfC/Mamba2 won't help (more capacity = more overfit). Vol clusters,
so this is the fair shot.
"""
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 # noqa: E402
from multistrat_book_v2 import edge_decay_trust # noqa: E402
from sklearn.ensemble import HistGradientBoostingRegressor # noqa: E402
TV = 0.10
def ema(x, span):
a = 2 / (span + 1); out = np.zeros_like(x); m = 0.0
for t in range(len(x)):
m = a * x[t] + (1 - a) * m; out[t] = m
return out
def main():
streams, days, year = build_streams()
names = list(streams)
M = np.column_stack([volnorm(streams[n]) for n in names])
TH = np.column_stack([edge_decay_trust(M[:, i]) for i in range(len(names))])
tw = TH / np.maximum(TH.sum(1, keepdims=True), 1e-9)
combo = np.nansum(tw * np.nan_to_num(M), axis=1)
T = len(combo)
# realized next-20d vol (target) and features (all causal)
H = 20
fwd = np.full(T, np.nan)
for t in range(T - H):
fwd[t] = combo[t + 1:t + 1 + H].std() * math.sqrt(252)
sq = combo ** 2
f_ema20 = np.sqrt(ema(sq, 20) * 252) # EMA vol (the baseline the book uses)
feats = np.column_stack([
np.sqrt(ema(sq, 10) * 252), np.sqrt(ema(sq, 20) * 252), np.sqrt(ema(sq, 60) * 252),
np.abs(combo), np.array([combo[max(0, t - 5):t].std() for t in range(T)]) * math.sqrt(252),
np.array([combo[max(0, t - 60):t].std() for t in range(T)]) * math.sqrt(252)])
# walk-forward GB vol forecast
gb_fc = np.full(T, np.nan); INIT, STEP = 252, 60
for s in range(INIT, T - H, STEP):
e = min(s + STEP, T - H)
tr = np.arange(63, s)
ok = np.isfinite(fwd[tr]) & np.isfinite(feats[tr]).all(1)
gb = HistGradientBoostingRegressor(max_depth=3, max_iter=150, learning_rate=0.05, min_samples_leaf=30).fit(feats[tr][ok], fwd[tr][ok])
gb_fc[s:e] = gb.predict(feats[s:e])
valid = np.arange(INIT, T - H)
v = valid[np.isfinite(fwd[valid]) & np.isfinite(gb_fc[valid])]
print(f"VOL-NOWCASTER TEST — {T} days, OOS {len(v)}")
print(f" OOS vol-forecast corr w/ realized: EMA {np.corrcoef(f_ema20[v], fwd[v])[0,1]:+.2f} GB {np.corrcoef(gb_fc[v], fwd[v])[0,1]:+.2f}")
print(f" OOS RMSE (lower=better): EMA {np.sqrt(np.mean((f_ema20[v]-fwd[v])**2)):.3f} GB {np.sqrt(np.mean((gb_fc[v]-fwd[v])**2)):.3f}")
# downstream: vol-target with each forecast (leverage = TV/forecast, capped 1x, floor 0.3)
def book(fc):
L = np.clip(TV / (fc + 1e-9), 0.3, 1.0)
r = np.zeros(T); r[1:] = L[:-1] * combo[1:]
rr = r[v[0]:v[-1]]; rr = rr[np.isfinite(rr)]
ann = rr.mean() * 252; vol = rr.std() * math.sqrt(252)
eq = np.cumprod(1 + rr); dd = float((eq / np.maximum.accumulate(eq) - 1).min())
return ann / vol if vol > 0 else float("nan"), dd, vol
se, dde, ve = book(f_ema20); sg, ddg, vg = book(gb_fc)
print(f" downstream book: EMA-vol Sharpe {se:+.2f} maxDD {100*dde:+.1f}% realizedVol {100*ve:.1f}%")
print(f" GB-vol Sharpe {sg:+.2f} maxDD {100*ddg:+.1f}% realizedVol {100*vg:.1f}%")
print("\n VERDICT: GB-vol downstream Sharpe/maxDD meaningfully > EMA-vol = an ML nowcaster helps the risk")
print(" layer (then CfC/Mamba2 worth it). If ~equal = EMA already captures vol-clustering; keep it simple.")
if __name__ == "__main__":
main()