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>
109 lines
5.9 KiB
Python
109 lines
5.9 KiB
Python
"""Agent 15 — Bollinger-band reversion, low-vol gated (family=meanrev, slug=bbands).
|
|
|
|
The angle (assigned): fade touches of the Bollinger bands (bl.bbands), only in a
|
|
low-vol regime. Tune win, k.
|
|
|
|
What the train curves actually say (A & B, split='train', diagnosed before coding):
|
|
both trend UP hard (+6.7x / +23x, ann vol ~0.7-0.9). The TEXTBOOK symmetric band-fade
|
|
is a LOSER here and the data is blunt about why:
|
|
|
|
* UPPER-band touch -> CONTINUATION, not reversion. fwd-5bar after a close>=upper is
|
|
+3.4%/+2.7% (A/B) even when we restrict to the low-vol regime. In a bull, riding the
|
|
upper band is momentum; shorting it just bleeds against the drift. So the SHORT side
|
|
of the classic fade is dead and we do NOT take it.
|
|
* LOWER-band touch is reversion ONLY when it is a DIP IN AN UPTREND. close<=lower while
|
|
price is above a long SMA -> fwd-5bar +3.5%/+7.2% (A/B): the band stretch snaps back
|
|
up. The same lower touch in a DOWNTREND / high-vol continues DOWN (A high-vol lo-touch
|
|
fwd-5 = -3.9%): a real knife. So the reversion we keep is the buy-the-dip-in-uptrend
|
|
leg, and we gate it OFF in downtrends and in high vol.
|
|
|
|
Hence the rule is an HONEST, one-sided Bollinger reversion: LONG the lower-band touch,
|
|
but only while (a) close is above a long trend SMA and (b) realized vol is in its lower
|
|
regime (the assigned low-vol gate). %b drives a smooth appetite (deeper below the band ->
|
|
bigger), the long is HELD with hysteresis until price mean-reverts back through the mid
|
|
band, then flat. Sizing is vol-targeted so the two curves are risk-comparable and exposure
|
|
shrinks into vol spikes (which are exactly the regime where the dip-buy fails).
|
|
|
|
HONEST NOTE: in a market trending this hard a trend+lowvol-gated dip-buy partially
|
|
degenerates toward trend participation — the genuine reversion content is the buy-below-band
|
|
/ exit-at-mid cycle plus the DD control from the gates + vol-target. The symmetric short-the-
|
|
upper-band leg that "Bollinger reversion" classically implies carries NEGATIVE edge on these
|
|
curves, so taking it would only add drawdown; the result is therefore a modest-but-real
|
|
reversion edge, NOT a high-PnL alpha. A negative result for the *symmetric* fade is itself a
|
|
finding (documented above).
|
|
|
|
CAUSAL: bbands/sma/realized_vol are trailing (value at i uses bars <= i); the hold-state is
|
|
a forward cumulative pass over PAST bars only; vol_target uses trailing realized vol. No
|
|
shift(-k), no centered windows, no global fit. Verified by causality_ok (max_diff ~0).
|
|
|
|
Tuning (train only, combined A&B; coarse->fine sweep + plateau check). The chosen cell is
|
|
interior on every axis and sits on a stable plateau (neighbouring K in [1.8..2.2],
|
|
TREND_WIN in [100..150], VOL_PCT in [0.65..0.85], ENTRY_PB in [0..0.1] all give
|
|
sharpe_min ~0.43-0.48 at DD ~0.08, sharpe_mean ~0.74-0.80):
|
|
BB_WIN=20, BB_K=2.0, TREND_WIN=120, VOL_WIN=20, VOL_PCT=0.65,
|
|
ENTRY_PB=0.10 (touch lower band), EXIT_PB=0.50 (exit at the MID band),
|
|
TARGET_VOL=0.25, VOL_WIN_DAYS=30, LEV_CAP=1.5, BASE=1.0
|
|
-> train combined: pnl_mean ~0.29, maxdd_worst ~0.08, sharpe_min ~0.48 (A binds; B ~1.1).
|
|
Exiting at the mid band (not higher) is the binding choice: Series A's dips are shallow and
|
|
fizzle, so holding the reversion past mid turns Series A negative (Sharpe 0.48 -> -0.0).
|
|
"""
|
|
import numpy as np
|
|
import blindlib as bl
|
|
import pandas as pd
|
|
|
|
BB_WIN = 20 # Bollinger lookback ("win" of the angle)
|
|
BB_K = 2.0 # band width in std ("k" of the angle)
|
|
TREND_WIN = 120 # long SMA: dip-buy only ABOVE it (reversion lives in the uptrend)
|
|
VOL_WIN = 20 # realized-vol lookback for the low-vol gate
|
|
VOL_PCT = 0.65 # low-vol gate: only act when rolling vol is below its expanding p65
|
|
ENTRY_PB = 0.10 # enter when %b <= this (close at/below the lower band)
|
|
EXIT_PB = 0.50 # exit when %b >= this (price has mean-reverted to the MID band)
|
|
TARGET_VOL = 0.25
|
|
VOL_WIN_DAYS = 30
|
|
LEV_CAP = 1.5
|
|
BASE = 1.0 # full size while holding a dip-long (the events are sparse; ride the snap-back)
|
|
|
|
|
|
def _expanding_quantile_below(x, q):
|
|
"""Causal: at bar i, is x[i] at/below the q-quantile of x[0..i]? (expanding, no leak)."""
|
|
s = np.asarray(x, float)
|
|
thr = pd.Series(s).expanding(min_periods=30).quantile(q).values
|
|
out = s <= thr
|
|
out[~np.isfinite(thr)] = False
|
|
return out
|
|
|
|
|
|
def signal(df):
|
|
c = df["close"].values.astype(float)
|
|
n = len(c)
|
|
up, mid, lo = bl.bbands(c, BB_WIN, BB_K) # causal trailing bands
|
|
band_w = up - lo
|
|
# %b: 0 = at the lower band, 0.5 = at the mid band, 1 = at the upper band.
|
|
pb = np.where(np.isfinite(band_w) & (band_w > 0), (c - lo) / band_w, np.nan)
|
|
trend_up = c > bl.sma(c, TREND_WIN) # causal trend gate
|
|
|
|
r = bl.simple_returns(c)
|
|
rv = bl.realized_vol(r, VOL_WIN, 365.0) # causal trailing realized vol
|
|
low_vol = _expanding_quantile_below(rv, VOL_PCT) # causal expanding low-vol regime gate
|
|
|
|
# One-sided Bollinger reversion: buy the lower-band touch (dip) in uptrend + low-vol,
|
|
# HOLD with hysteresis until %b mean-reverts back up to the MID band, then flat. The
|
|
# symmetric upper-band SHORT is a proven loser on these curves (continuation), so flat.
|
|
# Forward pass is PURE PAST-ONLY: in_long at i depends only on bars <= i.
|
|
held = np.zeros(n)
|
|
in_long = False
|
|
for i in range(n):
|
|
if in_long:
|
|
# exit when the dip has mean-reverted to the mid band, or the trend breaks
|
|
if (not trend_up[i]) or (np.isfinite(pb[i]) and pb[i] >= EXIT_PB):
|
|
in_long = False
|
|
else:
|
|
# enter a dip-long: %b at/below the lower band, in uptrend, in low-vol regime
|
|
if trend_up[i] and low_vol[i] and np.isfinite(pb[i]) and pb[i] <= ENTRY_PB:
|
|
in_long = True
|
|
held[i] = BASE if in_long else 0.0
|
|
|
|
pos = bl.vol_target(held, 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)
|