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>
135 lines
6.8 KiB
Python
135 lines
6.8 KiB
Python
"""agent_11_squeeze — ANGLE [family=breakout, slug=squeeze].
|
|
|
|
Range-compression (NR / Bollinger-squeeze) THEN expansion: after a low-volatility
|
|
"coil", price tends to break out and run. We (1) detect the squeeze causally, (2) wait
|
|
for the breakout out of the coil, (3) enter in the breakout direction, vol-targeted.
|
|
|
|
Mechanics (all causal — value at i uses only rows 0..i):
|
|
* SQUEEZE detector: Bollinger bandwidth = (BB_upper - BB_lower) / mid, using a
|
|
rolling window ending at i. A bar is "coiled" when its bandwidth sits in the low
|
|
tail of its own EXPANDING history (causal percentile, no future). This is the
|
|
classic Bollinger-squeeze / NR proxy: bands pinch when realized vol compresses.
|
|
* BREAKOUT trigger: a Donchian channel built STRICTLY from bars < i (bl.donchian
|
|
shifts by 1). When close[i] pierces the prior N-bar high -> upside expansion;
|
|
pierces the prior N-bar low -> downside expansion. The break is only ARMED if we
|
|
were recently in a squeeze (coil within the last LOOKBACK bars) — that is the
|
|
whole thesis: expansion out of compression, not a random breakout.
|
|
* STATE machine: once a squeeze-armed breakout fires, carry that side (stop-and-
|
|
reverse on the opposite squeeze-armed breakout) so we ride the post-coil
|
|
expansion and keep turnover low. Decay to flat if the move stalls back inside
|
|
the channel for a while (the coil's energy is spent).
|
|
* SIZING: the +/-1 direction is vol-targeted (TP01-style) so exposure shrinks into
|
|
vol spikes -> caps drawdown on whipsaws / failed breakouts.
|
|
|
|
Tuned ONLY on split='train' (Series A and B, equal weight). Causality verified by the
|
|
harness (signal on a prefix matches signal on the full array over its tail).
|
|
|
|
Honest notes:
|
|
* Squeeze-breakout is trend-following with a regime filter. On these trending curves
|
|
it captures up-legs with ~3x less drawdown than buy&hold (DD ~29% vs ~70-80%) at
|
|
only ~25-33% time-in-market; the cost is failed-breakout whipsaws after a fake-out
|
|
coil. Value is risk-adjusted, not raw PnL.
|
|
* Shorts were dropped (SHORT_SCALE=0): on both train curves the downside-breakout leg
|
|
was a net loser (coils on an uptrend mostly fake out down -> V-bottoms), so the
|
|
long/flat version is strictly better on Sharpe AND drawdown.
|
|
* ABLATION CAVEAT: a pure Donchian breakout with the SAME hold/exit logic but NO coil
|
|
gate scores marginally HIGHER on train (Sh ~1.05 / PnL ~1.34) than the coil-gated
|
|
version. The squeeze gate trims turnover and DD but is NOT the source of the edge
|
|
here — the edge is the breakout + vol-target. Kept the coil gate because the
|
|
assigned angle is *squeeze*; it is a mild, honest improvement on risk, not magic.
|
|
"""
|
|
import numpy as np
|
|
import pandas as pd
|
|
import blindlib as bl
|
|
|
|
# --- tuned on split='train' (broad plateau, see header / grid in commit) ------
|
|
BB_WIN = 20 # Bollinger window for bandwidth
|
|
BB_K = 2.0 # Bollinger multiplier
|
|
SQ_PCTL = 0.45 # bandwidth below this expanding-percentile = coil (sub-median
|
|
# compression; tighter pctl over-filters and loses good breaks)
|
|
DON_WIN = 25 # Donchian breakout lookback
|
|
ARM_LOOKBACK = 15 # breakout must occur within this many bars of a coil
|
|
HOLD_BARS = 40 # ride the post-coil expansion for ~this many bars, then decay
|
|
STALL_BARS = 12 # if price falls back inside the channel this long, exit early
|
|
SHORT_SCALE = 0.0 # downside-breakout sizing (0 = long/flat; coils on these
|
|
# uptrends mostly fake out to the downside, so shorts bleed)
|
|
TARGET_VOL = 0.20
|
|
VOL_WIN_DAYS = 30
|
|
LEV_CAP = 1.0
|
|
|
|
|
|
def _expanding_pctl_rank(x: np.ndarray, min_n: int = 60) -> np.ndarray:
|
|
"""Causal expanding percentile rank of x[i] within x[0..i]. rank in [0,1].
|
|
rank = fraction of past (<=i) values that are <= x[i]. Uses only rows 0..i."""
|
|
n = len(x)
|
|
out = np.full(n, np.nan)
|
|
# incremental sorted insertion would be O(n log n); n~2000 so an O(n^2) pass is
|
|
# fine (<30s). Keep it simple and obviously causal.
|
|
for i in range(n):
|
|
xi = x[i]
|
|
if not np.isfinite(xi):
|
|
continue
|
|
window = x[: i + 1]
|
|
valid = window[np.isfinite(window)]
|
|
if len(valid) < min_n:
|
|
continue
|
|
out[i] = float(np.mean(valid <= xi))
|
|
return out
|
|
|
|
|
|
def signal(df):
|
|
c = df["close"].values.astype(float)
|
|
n = len(c)
|
|
|
|
# 1) Bollinger bandwidth (causal) -> squeeze when bandwidth is in its low tail.
|
|
upper, mid, lower = bl.bbands(c, BB_WIN, BB_K)
|
|
with np.errstate(invalid="ignore", divide="ignore"):
|
|
bw = (upper - lower) / np.where(np.abs(mid) > 0, mid, np.nan)
|
|
bw_rank = _expanding_pctl_rank(bw, min_n=max(60, BB_WIN * 2))
|
|
coil = np.nan_to_num(bw_rank, nan=1.0) <= SQ_PCTL # True where compressed
|
|
|
|
# "recently coiled" = a coil within the last ARM_LOOKBACK bars (causal).
|
|
coil_recent = (
|
|
pd.Series(coil.astype(float)).rolling(ARM_LOOKBACK, min_periods=1).max().values > 0
|
|
)
|
|
|
|
# 2) Donchian breakout (prior-bar channel; bl.donchian already shifts by 1).
|
|
don_hi, don_lo = bl.donchian(df, DON_WIN)
|
|
up_break = np.isfinite(don_hi) & (c > don_hi)
|
|
dn_break = np.isfinite(don_lo) & (c < don_lo)
|
|
|
|
# 3) state machine: arm breakouts only when they expand out of a recent coil.
|
|
# The thesis is that the EDGE lives in the expansion right after the coil, so
|
|
# we ride a fired breakout for HOLD_BARS then decay to flat (the coil's energy
|
|
# is spent). A fresh squeeze-armed breakout re-arms / re-times the hold. We
|
|
# exit early if price collapses back inside the channel (failed breakout).
|
|
state = np.zeros(n)
|
|
s = 0.0
|
|
age = 0 # bars since the active breakout fired
|
|
inside_count = 0 # consecutive bars back inside the channel since trigger
|
|
for i in range(n):
|
|
armed = coil_recent[i]
|
|
fired = False
|
|
if armed and up_break[i]:
|
|
s = 1.0; age = 0; inside_count = 0; fired = True
|
|
elif armed and dn_break[i]:
|
|
s = -SHORT_SCALE; age = 0; inside_count = 0; fired = (SHORT_SCALE > 0)
|
|
|
|
if not fired and s != 0.0:
|
|
age += 1
|
|
# failed-breakout guard: price back inside the prior channel
|
|
in_channel = True
|
|
if np.isfinite(don_hi[i]) and c[i] > don_hi[i]:
|
|
in_channel = False
|
|
if np.isfinite(don_lo[i]) and c[i] < don_lo[i]:
|
|
in_channel = False
|
|
inside_count = inside_count + 1 if in_channel else 0
|
|
if inside_count >= STALL_BARS or age >= HOLD_BARS:
|
|
s = 0.0; age = 0; inside_count = 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)
|