1afb1014c9
Flotta di 52 subagenti "esperti di segnali" su storico BTC/ETH ANONIMIZZATO (Series A/B rebased a 100, calendario sintetico, split 70/30) — non sanno cosa siano. Ognuno scrive un signal(df)->position causale (script o ML), tunato solo sul train. Orchestratore valuta su PnL e maxDD nel test held-out. Harness cieco leak-free (riusabile): - make_blind.py: export anonimo + overlay; blindlib.py: evaluator con shift della posizione + GUARDIA DI CAUSALITA' online (squalifica ogni look-ahead, ML incluso); blind_eval.py CLI; score_all.py giudice OOS; verify_top.py (corr-al-trend, fee-stress, jackknife). - 52/52 passano la guardia (zero leak su tutta la flotta). Esito OOS (benchmark buy&hold: -7% PnL, 68% DD): - top = macd (+21%, DD 11%, Sh 0.84), accel, vol_of_vol, regime_switch, rf, obv — tutti trend/vol-regime. Sharpe OOS ~0.84 decade dal train ~1.4. Mean-rev e ML in fondo. - 3 scettici indipendenti: REFUTED. regime-luck (top-5 bar = 67-102% del PnL); trend-redundancy (HAC alpha t=+0.9..+1.5, nessuno >1.96 — TSMOM travestito); overfit (accel/vov knife-edge). Verdetto: ri-conferma CIECA e indipendente del soffitto direzionale ~1.3. macd = classe-TP01, forward-monitor non deploy. Diario 2026-06-21-blind-signal-fleet.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
101 lines
4.6 KiB
Python
101 lines
4.6 KiB
Python
"""agent_20_regime_switch — ANGLE [family=vol, slug=regime_switch].
|
|
|
|
Regime switch on the realized-vol PERCENTILE (expanding / online):
|
|
|
|
* Compute short-window realized vol rv[i] at each bar.
|
|
* Rank it against its EXPANDING percentile (the causal "typical" vol seen so far) —
|
|
a self-calibrating threshold that needs no magic vol level and adapts as the series
|
|
evolves (no peeking at the full-sample distribution).
|
|
* LOW-VOL regime (rv-rank <= PCTL): TREND-FOLLOW. Quiet, orderly markets are where
|
|
momentum persists, so we ride the prevailing (multi-horizon) trend.
|
|
* HIGH-VOL regime (rv-rank > PCTL): stand aside (FLAT). High realized vol is where
|
|
trends whipsaw / V-reverse and where the big drawdowns are born; the cleanest
|
|
expression of the "regime switch" is to refuse directional exposure there.
|
|
|
|
The trend leg is a multi-horizon TSMOM SIGN blend (slow horizons ~1/2/4 months): a
|
|
single lookback is regime-fragile, the blend keeps the slow macro trend while the fast
|
|
horizon cuts exposure early into a turn. Final size is a trailing vol-target, so the
|
|
position also shrinks into vol within the low-vol regime.
|
|
|
|
CAUSAL: rv uses a trailing window; the percentile rank is EXPANDING (only past bars);
|
|
each TSMOM sign uses close[i]/close[i-H]; vol_target uses a trailing realized-vol
|
|
window. No look-ahead, no centered windows, no global fit. Verified by causality_ok
|
|
(max_diff 0.0).
|
|
|
|
Tuned ONLY on split='train' (Series A & B, equal weight). A coarse->fine sweep found a
|
|
WIDE plateau: HZ=(25,60,120), PCTL in [0.60..0.70], VW in [35..55], RV in [15..25] all
|
|
give sharpe_min ~1.25-1.30 at DD ~0.17-0.19. The chosen cell is interior on every axis
|
|
(robust, not a lucky spike):
|
|
RV_WIN=20, PCTL=0.65, HORIZONS=(25,60,120), TARGET_VOL=0.22, VOL_WIN=45, LEV_CAP=1.5
|
|
-> train combined: pnl_mean ~2.0, maxdd_worst ~0.18, sharpe_min ~1.30.
|
|
|
|
Honest notes:
|
|
* The high-vol leg is LONG-FLAT (not revert). A lightly-weighted contrarian leg in
|
|
high vol helped marginally with a single-MA trend, but once the trend is the slow
|
|
multi-horizon SIGN blend the reversion leg only added drag -> flat is strictly
|
|
better here. The value is RISK-ADJUSTED: comparable/positive PnL at ~4x less
|
|
drawdown than buy&hold (which eats ~77-79% DD on these curves), by sitting out the
|
|
high-realized-vol regime where the violent declines happen.
|
|
* Loosening the gate (PCTL ~0.65, not 0.50) is what lifts both Sharpe and PnL: the
|
|
bottom ~half of the vol distribution is too restrictive and misses the early,
|
|
still-low-vol part of the trend legs. The plateau is wide enough that the exact
|
|
percentile is not load-bearing.
|
|
"""
|
|
import numpy as np
|
|
import blindlib as bl
|
|
|
|
RV_WIN = 20 # short realized-vol window ("current" vol)
|
|
PCTL = 0.65 # expanding vol-percentile gate: trend-follow when rank <= this
|
|
HORIZONS = (25, 60, 120) # multi-horizon TSMOM sign blend (~1/2/4 months of daily bars)
|
|
TARGET_VOL = 0.22
|
|
VOL_WIN_DAYS = 45
|
|
LEV_CAP = 1.5
|
|
MIN_HIST = 60 # warmup before the expanding percentile is trusted
|
|
|
|
|
|
def _expanding_pctl_rank(x: np.ndarray, min_hist: int) -> np.ndarray:
|
|
"""rank[i] = fraction of finite x[0..i] that are <= x[i] (causal, expanding).
|
|
NaN until `min_hist` finite values have accumulated."""
|
|
n = len(x)
|
|
rank = np.full(n, np.nan)
|
|
seen: list[float] = []
|
|
for i in range(n):
|
|
v = x[i]
|
|
if np.isfinite(v):
|
|
seen.append(v)
|
|
if len(seen) >= min_hist:
|
|
rank[i] = float(np.mean(np.asarray(seen) <= v))
|
|
return rank
|
|
|
|
|
|
def _tsmom_sign(c: np.ndarray, h: int) -> np.ndarray:
|
|
"""Sign of the past-h-bar return, causal. 0 for i < h."""
|
|
out = np.zeros(len(c))
|
|
if h < len(c):
|
|
out[h:] = np.sign(c[h:] / c[:-h] - 1.0)
|
|
return out
|
|
|
|
|
|
def signal(df):
|
|
c = df["close"].values.astype(float)
|
|
bpy = bl.bars_per_day(df) * 365.25
|
|
|
|
# 1) short-window realized vol and its EXPANDING percentile rank (causal).
|
|
rv = bl.realized_vol(bl.simple_returns(c), RV_WIN, bpy)
|
|
rank = _expanding_pctl_rank(rv, MIN_HIST)
|
|
low_vol = np.isfinite(rank) & (rank <= PCTL) # the LOW-VOL regime we trade
|
|
|
|
# 2) multi-horizon TSMOM sign blend -> graded direction in [-1, +1] (causal).
|
|
sig = np.zeros(len(c))
|
|
for h in HORIZONS:
|
|
sig += _tsmom_sign(c, h)
|
|
sig /= len(HORIZONS)
|
|
|
|
# 3) regime switch: trend-follow ONLY in the low-vol regime, else flat.
|
|
raw = np.where(low_vol, sig, 0.0)
|
|
|
|
# 4) causal vol-targeting (shrinks size into vol -> caps DD).
|
|
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
|
|
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|