Files
PythagorasGoal/scripts/research/blind/agents/agent_11_squeeze.py
T
Adriano Dal Pastro 1afb1014c9 research(blind): 52 agenti ciechi su curve anonime BTC/ETH — orchestratore valuta PnL/maxDD, niente di nuovo regge
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>
2026-06-21 07:05:04 +00:00

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)