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>
84 lines
3.9 KiB
Python
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()
|