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>
101 lines
5.7 KiB
Python
101 lines
5.7 KiB
Python
"""Agent 14 — RSI reversion, trend-gated (family=meanrev, slug=rsi).
|
|
|
|
The angle (assigned): RSI reversion. Long when RSI<lo, short when RSI>hi (bl.rsi),
|
|
GATED by a longer trend filter. Tune lo/hi/win.
|
|
|
|
Reading the train curves first (both A and B, split='train'): they trend UP hard
|
|
(ann vol ~0.7-0.9, total ret +6.7x / +23x over the window). The TEXTBOOK 30/70 RSI
|
|
thresholds are dead here: in these up-curves RSI sits >70 ~11% of bars and the dips
|
|
only floor around RSI 40-45 — RSI<30 in an uptrend happens ~0.1% of the time. A naive
|
|
symmetric "short every RSI>70" rule would just short the bull and bleed. So the
|
|
mean-reversion has to be REGIME-AWARE, and the lo/hi have to be tuned to the data's
|
|
actual RSI distribution, not the textbook:
|
|
|
|
* In an UPTREND (close above a long SMA) RSI dips are BUY-THE-DIP reversion. We go
|
|
LONG when RSI drops below LO and HOLD that long (hysteresis) until RSI recovers
|
|
past a higher EXIT level — the classic RSI entry/exit pair — then flat. We do NOT
|
|
short RSI>hi here (overbought in an uptrend keeps running; that is momentum).
|
|
* In a DOWNTREND (close below the long SMA) the symmetry returns: RSI>HI is a
|
|
reversion SHORT (rips fade back down); RSI<LO we stand flat (don't knife-catch
|
|
long against a downtrend). The short side is weighted < 1 because the curves drift
|
|
up — on train it adds a touch of PnL with no DD cost but is not where the edge is.
|
|
|
|
The long trend filter does two jobs: it picks WHICH side of the RSI book is reversion
|
|
(buy dips in up-trend / sell rips in down-trend) and it suppresses the side that fights
|
|
the drift. TREND_WIN=150 is the DD sweet spot on train (DD 0.11 vs 0.16-0.21 at 100/200)
|
|
— the gate is what keeps the drawdown small. Sizing is smooth (further past the
|
|
threshold -> bigger appetite, no hard 0/1 fee-churning flips) then vol-targeted so the
|
|
two curves are risk-comparable and exposure shrinks into vol spikes (crashes are vol
|
|
spikes), bounding the drawdown.
|
|
|
|
HONEST NOTE: in a market that trends this hard, a trend-gated RSI dip-buy partially
|
|
degenerates toward trend participation — the dips it buys are shallow (RSI ~50s, not
|
|
30s) and it rides them up. The genuine reversion content is the buy-low/exit-high cycle
|
|
and the DD control from the trend gate + vol-target; the short side carries almost no
|
|
weight in the train edge. The result is an honest-but-modest combined train Sharpe ~1.1
|
|
at ~11% DD (vs long-only buy&hold's ~7-23x PnL at ~70-80% DD) — i.e. a fraction of the
|
|
buy&hold PnL but ~6-7x less drawdown.
|
|
|
|
CAUSAL: rsi() is an EWMA of past gains/losses (<= i); the SMA trend filter is trailing;
|
|
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.0).
|
|
|
|
Tuning (train only, combined A&B; coarse->fine sweep + plateau check). Chosen cell is
|
|
INTERIOR on every axis — RW in [18..25], LO in [56..62], EXIT in [75..85], TWIN=150,
|
|
TVOL [0.20..0.25] all stay sharpe_min ~1.0..1.26 at DD ~0.11..0.13, a broad plateau not
|
|
a spike. (Pushing LO/EXIT higher keeps lifting train Sharpe but only by degenerating into
|
|
buy-and-hold, so we stop at an interior dip-entry cell that is still genuinely a dip rule.)
|
|
RSI_WIN=20, LO=58, HI=68, EXIT=78, TREND_WIN=150
|
|
SHORT_W=0.5, TARGET_VOL=0.25, VOL_WIN_DAYS=35, LEV_CAP=1.5, BASE=0.6
|
|
-> train combined: pnl_mean ~0.87, maxdd_worst ~0.11, sharpe_min ~1.14
|
|
"""
|
|
import numpy as np
|
|
import blindlib as bl
|
|
|
|
RSI_WIN = 20 # RSI lookback (the "win" of the angle; 20 > textbook 14 for these trends)
|
|
LO = 58.0 # oversold/dip threshold -> reversion LONG (tuned to the curves' RSI floor)
|
|
HI = 68.0 # overbought threshold -> reversion SHORT (downtrend only)
|
|
EXIT = 78.0 # dip-long is HELD until RSI recovers past EXIT (hysteresis entry/exit pair)
|
|
TREND_WIN = 150 # long SMA: above = uptrend (buy dips), below = downtrend (sell rips). DD sweet spot.
|
|
SHORT_W = 0.5 # weight on the downtrend short side; <1 because the curves drift up
|
|
BASE = 0.6 # base long size while holding a dip (scaled up if still oversold)
|
|
TARGET_VOL = 0.25
|
|
VOL_WIN_DAYS = 35
|
|
LEV_CAP = 1.5
|
|
|
|
|
|
def signal(df):
|
|
c = df["close"].values.astype(float)
|
|
n = len(c)
|
|
rs = bl.rsi(c, RSI_WIN)
|
|
trend_up = c > bl.sma(c, TREND_WIN) # causal trailing SMA trend gate
|
|
|
|
# --- smooth reversion appetite from RSI (further past threshold -> bigger) ---
|
|
long_app = np.clip((LO - rs) / 25.0, 0.0, 1.0) # oversold -> long appetite
|
|
short_app = np.clip((rs - HI) / (100.0 - HI), 0.0, 1.0) # overbought -> short appetite
|
|
|
|
# --- trend-gated RSI reversion with hysteresis on the dip-long ---
|
|
# The forward pass below is PURE PAST-ONLY: in_long at bar i depends only on bars <= i
|
|
# (rs, trend_up are causal; the state machine never looks ahead). Causality verified.
|
|
held = np.zeros(n)
|
|
in_long = False
|
|
for i in range(n):
|
|
if in_long:
|
|
# exit the held dip-long when the trend breaks down OR RSI has recovered
|
|
if (not trend_up[i]) or (rs[i] >= EXIT):
|
|
in_long = False
|
|
else:
|
|
# enter a dip-long only in an uptrend when RSI is below LO (oversold dip)
|
|
if trend_up[i] and rs[i] < LO:
|
|
in_long = True
|
|
if in_long:
|
|
held[i] = max(BASE, long_app[i]) # ride the recovery, bigger if still oversold
|
|
else:
|
|
# when not holding a long, only the downtrend reversion-short passes through
|
|
held[i] = (-SHORT_W * short_app[i]) if (not trend_up[i]) 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)
|