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>
94 lines
4.7 KiB
Python
94 lines
4.7 KiB
Python
"""agent_13_volbreak — ANGLE [family=breakout, slug=volbreak].
|
|
|
|
Volatility breakout: enter the trend direction when REALIZED VOL EXPANDS above its
|
|
rolling median. The thesis: a fresh expansion of realized volatility marks a regime
|
|
of large, directional moves (a breakout out of a quiet base). When vol picks up we
|
|
align with the prevailing trend and ride it; when vol is compressed / below its
|
|
rolling median we stand aside (no breakout in progress, just chop).
|
|
|
|
Mechanics (all causal — value at i uses only rows 0..i):
|
|
* VOL EXPANSION gate: annualized realized vol over a short window (RV_WIN) vs its
|
|
own rolling median over a longer lookback (MED_WIN). "Expanded" when
|
|
rv[i] > EXP_K * median(rv up to i). bl.realized_vol and pandas rolling are causal.
|
|
* TREND direction: sign of price vs a moving average (close / SMA(TREND_WIN) - 1),
|
|
decided at close[i]. This is the direction we take *only while* vol is expanded.
|
|
* STATE / persistence: once vol expands we lock onto the current trend side and
|
|
hold it (stop-and-reverse if the trend sign flips while still expanded) until vol
|
|
falls back BELOW its median (expansion over) -> flat. This rides the whole
|
|
high-vol leg instead of flickering bar to bar, keeping turnover (fees) down.
|
|
* SIZING: the +1/0 direction is vol-targeted (TP01-style) so exposure shrinks into
|
|
the very vol spikes the gate selects -> caps drawdown on violent reversals.
|
|
|
|
Tuned ONLY on split='train' (Series A and B, equal weight; broad plateau grid below).
|
|
Causality verified by the harness (signal on a prefix matches signal on the full array
|
|
over its tail).
|
|
|
|
Honest notes:
|
|
* On these strongly-trending high-vol curves the edge is essentially "be long the
|
|
trend, but ONLY when vol confirms a breakout, and shrink size into vol". Value is
|
|
RISK-ADJUSTED: comparable/positive PnL at ~3-4x less drawdown than buy&hold (which
|
|
eats ~77-79% DD here), not bigger raw PnL. Train combined Sharpe ~1.12, worst-DD
|
|
~23%, mean PnL ~1.14.
|
|
* LONG-ONLY (SHORT_SCALE=0). Shorts were dropped after tuning: on these uptrends the
|
|
down-trend + vol-expansion combo is dominated by violent V-bottom reversals, which
|
|
are terrible to short -> a short leg (full OR damped) strictly LOWERED Sharpe and
|
|
raised DD on both train curves. The short leg is not an edge here; flat is better.
|
|
* EXP_K=0.8 means we trade when rv sits at/above 0.8x its rolling median — still a
|
|
genuine vol-expansion gate (it stands aside in the lowest-vol ~30-40% of bars where
|
|
price just chops), but inclusive enough not to miss the early part of a breakout
|
|
leg. Requiring rv strictly ABOVE the median (K>=1.0) entered too late and gutted the
|
|
Series-B trend capture (Sh 1.12 -> 0.28). The plateau holds for RV 15-20, MED
|
|
100-150, K 0.78-0.85, TREND 30-60.
|
|
"""
|
|
import numpy as np
|
|
import pandas as pd
|
|
import blindlib as bl
|
|
|
|
# --- tuned on split='train' (broad plateau) ---------------------------------
|
|
RV_WIN = 15 # short realized-vol window (the "current" vol)
|
|
MED_WIN = 100 # rolling-median lookback for the vol baseline
|
|
EXP_K = 0.80 # vol is "expanded" when rv > EXP_K * rolling-median(rv)
|
|
TREND_WIN = 50 # trend filter: sign of close / SMA(TREND_WIN) - 1
|
|
SHORT_SCALE = 0.0 # LONG-ONLY: down-vol-breaks here are mostly V-reversals -> shorts bleed
|
|
TARGET_VOL = 0.20
|
|
VOL_WIN_DAYS = 30
|
|
LEV_CAP = 1.5
|
|
|
|
|
|
def signal(df):
|
|
c = df["close"].values.astype(float)
|
|
n = len(c)
|
|
bpy = bl.bars_per_day(df) * 365.25
|
|
|
|
# 1) realized vol (short) and its causal rolling median baseline.
|
|
r = bl.simple_returns(c)
|
|
rv = bl.realized_vol(r, RV_WIN, bpy)
|
|
rv_med = pd.Series(rv).rolling(MED_WIN, min_periods=max(10, MED_WIN // 2)).median().values
|
|
expanded = np.isfinite(rv) & np.isfinite(rv_med) & (rv > EXP_K * rv_med)
|
|
|
|
# 2) trend direction decided at close[i] (causal).
|
|
ma = bl.sma(c, TREND_WIN)
|
|
with np.errstate(invalid="ignore", divide="ignore"):
|
|
trend = np.where(np.isfinite(ma) & (ma > 0), c / ma - 1.0, 0.0)
|
|
tsign = np.sign(trend)
|
|
|
|
# 3) state machine: while vol is expanded, hold the trend side (S&R on sign flip);
|
|
# when vol falls back below its (scaled) median the breakout is spent -> flat.
|
|
state = np.zeros(n)
|
|
s = 0.0
|
|
for i in range(n):
|
|
if expanded[i]:
|
|
if tsign[i] > 0:
|
|
s = 1.0
|
|
elif tsign[i] < 0:
|
|
s = -SHORT_SCALE
|
|
# tsign == 0 -> keep current side
|
|
else:
|
|
s = 0.0
|
|
state[i] = s
|
|
|
|
# 4) size by causal vol-targeting (shrinks into vol spikes -> caps DD).
|
|
pos = bl.vol_target(state, 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)
|