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>
90 lines
5.1 KiB
Python
90 lines
5.1 KiB
Python
"""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)
|