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>
This commit is contained in:
Adriano Dal Pastro
2026-06-21 07:05:04 +00:00
parent f5d30d88b9
commit 1afb1014c9
63 changed files with 6660 additions and 0 deletions
@@ -0,0 +1,89 @@
"""Agent 18 — Distance-from-MA reversion, trend-gated (family=meanrev, slug=dist_ma).
THE ANGLE (assigned): position = -tanh(scaled distance of price from its MA). Buy when price
is stretched BELOW its MA, sell when stretched ABOVE — a reversion-to-the-MA impulse, sized by
how far price has wandered. Tune the MA window and the tanh scale.
WHY THE PURE ANGLE LOSES, AND WHAT SURVIVES.
The naive symmetric form (-tanh(scale * (price/MA - 1)) traded both sides) is CATASTROPHIC on
these two curves: both trend up ~7x (A) / ~23x (B) over the train window, so shorting every
stretch ABOVE the MA just fights a relentless uptrend. Measured: the pure symmetric angle
returns -79%..-95% with sharpe ~ -0.5..-0.9 (it shorts the bull). A conditioning study of
next-bar return vs the normalized distance-from-MA confirms the asymmetry: the LARGEST
positive next-bar returns sit at the HIGHEST positive distance (that's momentum continuation,
NOT reversion — never short it), while the genuine reversion edge lives only on the DOWNSIDE
— when price is stretched well below its MA, the next bar bounces (+0.27%..+0.35% in the
deepest dip bin, pooled A&B). So the distance-from-MA reversion that actually exists here is
the short-horizon PULLBACK inside the prevailing trend, not a fade of the trend itself.
THE RULE.
impulse = -tanh(SCALE * z) where z = (price/SMA(MA) - 1) standardized by a trailing rolling
std (so A and B, with different vol, get comparable stretch units). impulse>0 = price below
its MA (a dip -> reversion says go long); impulse<0 = price above its MA (a rally -> short).
A TREND GATE then keeps only the reversion leg that agrees with the regime:
* UPTREND (price > SMA(SLOW)): take only the LONG impulse (buy the dip that bounces).
* DOWNTREND (price < SMA(SLOW)): take only the SHORT impulse (fade the dead-cat rally),
down-weighted by SHORT_W. Tuning drives SHORT_W -> 0: both curves trend up, so the
downtrend-short reversion only adds drawdown over this sample.
A causal vol_target sizes the impulse so the two series are risk-comparable and exposure
shrinks into vol spikes.
CAUSAL: SMA(MA), SMA(SLOW), the rolling std and vol_target at bar i use only rows <= i. No
shift(-k), no centered windows, no global fit. Verified by causality_ok (online-consistent).
TUNING (train only, combined A&B; coarse->fine, plateau not spike). A FAST MA (the distance is
a short-horizon pullback, not a slow-trend gap) is decisively better than a medium MA:
ma=3 beats ma=20+ by ~0.2 sharpe at lower DD. The chosen cell is interior on every axis:
MA 3..5 -> sharpe_min 0.69..0.81 ; SCALE 1.0..2.5 -> 0.72..0.76 (PnL rises, DD ~flat) ;
NORM_WIN 30..90 -> 0.75..0.80 ; SLOW 110..140 -> sharpe_min 0.74..0.81 (a real plateau).
SHORT_W 0->0.5 only lowers sharpe (the downtrend short fights the structural uptrend).
vol_target trades PnL<->DD ~linearly (sharpe flat), so TARGET_VOL is just the risk dial.
MA=3, NORM_WIN=60, SCALE=1.5, SLOW=130, SHORT_W=0.0, TARGET_VOL=0.30, VOL_WIN=30, LEV_CAP=2.0
-> train combined: pnl_mean ~0.70, maxdd_worst ~0.115, sharpe_min ~0.80
(a solid PnL at an ~11-12% drawdown: the reversion-in-trend harvests the pullback bounces
while sidestepping the deep declines, vs long-only buy&hold's huge PnL at ~70-80% DD.)
HONEST CAVEAT: the value here is the DROP IN DRAWDOWN (~6x lower than buy&hold), not beating
buy&hold's raw PnL on a 7x/23x bull run. The PURE assigned angle (symmetric fade) is a
loser on trending data — it only becomes positive once gated to the dip side of the trend.
"""
import numpy as np
import pandas as pd
import blindlib as bl
MA = 3 # fast SMA -> the distance is a SHORT-HORIZON pullback from price
NORM_WIN = 60 # trailing window standardizing the distance (so A & B are comparable)
SCALE = 1.5 # tanh scale on the standardized distance -> reversion impulse magnitude
SLOW = 130 # trend-regime SMA for the agreement gate
SHORT_W = 0.0 # weight on the (gated) downtrend-short leg; tuning -> 0 (long-flat best)
TARGET_VOL = 0.30
VOL_WIN_DAYS = 30
LEV_CAP = 2.0
def signal(df):
c = df["close"].values.astype(float)
n = len(c)
# distance of price from its (fast) MA, standardized by a trailing rolling std (causal).
dist = c / bl.sma(c, MA) - 1.0
sd = pd.Series(dist).rolling(NORM_WIN).std().values
zd = np.nan_to_num(dist / np.where(sd > 0, sd, np.nan), nan=0.0)
# the assigned angle: reversion impulse = -tanh(scaled distance).
# zd>0 (price above MA) -> impulse<0 (short the stretch)
# zd<0 (price below MA) -> impulse>0 (long the dip)
impulse = -np.tanh(SCALE * zd)
# trend-agreement gate: keep only the reversion leg that agrees with the regime.
up = c > bl.sma(c, SLOW)
raw = np.zeros(n)
long_ok = (impulse > 0) & up # buy the dip inside an uptrend
short_ok = (impulse < 0) & (~up) # fade the rally inside a downtrend (down-weighted)
raw[long_ok] = impulse[long_ok]
raw[short_ok] = impulse[short_ok] * SHORT_W
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)