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:
@@ -0,0 +1,78 @@
|
||||
"""Agent 16 — Z-score reversion to SMA, trend-gated (family=meanrev, slug=zrev).
|
||||
|
||||
THE ANGLE (assigned): reversion of price to its SMA via a CAUSAL rolling z-score —
|
||||
short positive extremes / long negative extremes — WITH A TREND-AGREEMENT GATE.
|
||||
|
||||
Why the gate is the whole story here. Naive z-reversion (short every z>+thr, long every
|
||||
z<-thr against a price-vs-SMA z-score) LOSES on these two curves: both trend up ~8x/24x
|
||||
over the sample, so a positive z-extreme above a medium SMA is usually momentum that keeps
|
||||
going (study: z>1.5 -> next-bar +0.005/+0.008, NOT a reversal), and shorting it just fights
|
||||
the trend. The reversion that actually exists is the SHORT-HORIZON pullback inside the
|
||||
prevailing trend:
|
||||
|
||||
* In an UPTREND (price > slow SMA), a negative z-extreme (a dip below the FAST SMA) is a
|
||||
pullback that bounces -> go LONG. (study: UP & z<-1 -> next-bar +0.003 .. +0.012.)
|
||||
* In a DOWNTREND (price < slow SMA), a positive z-extreme (a rally above the FAST SMA) is
|
||||
a dead-cat that fades -> go SHORT. (study: DOWN & z>+1 -> next-bar ~0 .. -0.004.)
|
||||
* A z-extreme that DISAGREES with the trend (rally in an uptrend / dip in a downtrend) is
|
||||
momentum/continuation, not reversion -> stay FLAT (those bins are where naive z-reversion
|
||||
bleeds: UP & z>1 -> +0.003 continuation; you must NOT short it).
|
||||
|
||||
So the position is the reversion impulse (-z, clipped to extremes) FILTERED by trend
|
||||
agreement: keep only longs in uptrends and shorts in downtrends. A causal vol-target then
|
||||
sizes it so A and B are risk-comparable and exposure shrinks into vol spikes.
|
||||
|
||||
CAUSAL: zscore(c, FAST) and sma(c, SLOW) at i use only rows <= i; the trend gate and
|
||||
vol_target are trailing. No shift(-k), no centered windows, no global fit. Verified by
|
||||
causality_ok.
|
||||
|
||||
Tuning (train only, combined A&B; coarse->fine sweep). A CONTINUOUS reversion impulse
|
||||
(-z, saturating) gated by the trend beats sparse extreme-only entries (more of the dips are
|
||||
captured while the gate keeps the trend on your side). The chosen cell is interior on every
|
||||
axis and is a plateau, not a spike: FAST 2..3, SLOW 100..150, Z_SAT 1.5..2.0 all stay in
|
||||
sharpe_min ~0.6..0.8 at DD ~0.06..0.12; SHORT_W 0->0.5 only lowers sharpe_min (the downtrend
|
||||
short reversion fights the structural uptrend). vol_target scales PnL<->DD linearly (sharpe
|
||||
flat), so TARGET_VOL just sets the risk dial.
|
||||
FAST=2, SLOW=120, Z_SAT=1.75, SHORT_W=0.0, TARGET_VOL=0.30, VOL_WIN_DAYS=30, LEV_CAP=2.0
|
||||
-> train combined: pnl_mean ~0.31, maxdd_worst ~0.11, sharpe_min ~0.78
|
||||
(a modest PnL at a ~10% drawdown — the reversion-in-trend captures the bounces while
|
||||
sidestepping the big declines, vs long-only buy&hold's huge PnL at ~70-80% DD).
|
||||
"""
|
||||
import numpy as np
|
||||
import blindlib as bl
|
||||
|
||||
FAST = 2 # short SMA for the reversion z-score (the "stretch from SMA" detector)
|
||||
SLOW = 120 # slow SMA defining the trend regime for the agreement gate
|
||||
Z_SAT = 1.75 # z magnitude that saturates the reversion impulse to +-1
|
||||
SHORT_W = 0.0 # weight on the (gated) short leg; tuning -> 0 (long-flat best on train)
|
||||
TARGET_VOL = 0.30
|
||||
VOL_WIN_DAYS = 30
|
||||
LEV_CAP = 2.0
|
||||
|
||||
|
||||
def signal(df):
|
||||
c = df["close"].values.astype(float)
|
||||
n = len(c)
|
||||
|
||||
z = np.nan_to_num(bl.zscore(c, FAST), nan=0.0) # price-vs-fast-SMA, standardized (causal)
|
||||
slow = bl.sma(c, SLOW) # trend regime line (causal)
|
||||
uptrend = c > slow # boolean trend gate
|
||||
|
||||
# reversion impulse = -z: long when price is stretched BELOW its SMA (dip, z<0),
|
||||
# short when stretched ABOVE (rally, z>0). Proportional, saturating at +-Z_SAT.
|
||||
impulse = np.clip(-z / Z_SAT, -1.0, 1.0) # -z direction = reversion to the SMA
|
||||
|
||||
# TREND-AGREEMENT GATE: keep ONLY longs in an uptrend and shorts in a downtrend.
|
||||
# A z-extreme that DISAGREES with the trend (rally in an uptrend / dip in a downtrend)
|
||||
# is momentum/continuation, not reversion -> stay FLAT. The short leg is gated AND
|
||||
# down-weighted by SHORT_W (tuning drives it to 0: both curves trend up, so the
|
||||
# downtrend-short reversion only adds drawdown here).
|
||||
raw = np.zeros(n)
|
||||
long_ok = (impulse > 0) & uptrend # buy the dip inside an uptrend
|
||||
short_ok = (impulse < 0) & (~uptrend) # fade the rally inside a downtrend
|
||||
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)
|
||||
Reference in New Issue
Block a user