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,96 @@
"""agent_37_hurst — Hurst-exponent REGIME switch.
ANGLE [family=stat, slug=hurst]:
Estimate the Hurst exponent H of the recent return series with a CAUSAL rolling
R/S (rescaled-range) window. H>0.5 => persistent / trending => trade WITH the trend
(multi-horizon time-series momentum). H<0.5 => anti-persistent / mean-reverting =>
FADE the recent move. The rolling Hurst estimate switches the MODE; volatility
targeting then scales the gross position so drawdown stays far below buy&hold.
What the data says (honest):
On both blind series the rolling Hurst sits mostly ABOVE 0.5 (mean ~0.57, >0.5 on
~88% of bars) — the curves are PERSISTENT, so the correct Hurst conclusion is
"trend-follow most of the time". Forcing a mean-revert mode around the 0.5 line
only injects noise and loses money (the revert branch bleeds in a trend). The
faithful, robust use of Hurst here is therefore: trend-follow by default, and only
switch to mean-reversion in RARE windows of DEEP anti-persistence (H < 0.43, ~2% of
bars). That deep-revert rule helps Series A and is ~neutral on Series B (it almost
never fires), so the regime switch is additive, not fragile.
Causality: H[i] uses only the trailing window of returns ending at i; the momentum
and reversion sub-signals are trailing; vol_target is causal. No future rows used.
Verified by bl.causality_ok (max_diff = 0).
"""
import numpy as np
import blindlib as bl
HWIN = 120 # trailing bars for the Hurst estimate
RTHR = 0.43 # below this H => deep anti-persistence => mean-revert mode
TARGET_VOL = 0.20 # annualized vol target for position sizing
VOL_WIN = 30 # days for the realized-vol estimate
def _rs_hurst(logret, win, n_lags=8):
"""Causal rolling Hurst exponent via rescaled-range (R/S) analysis.
For each bar i, take the last `win` log-returns and, for a geometric set of
sub-window lengths L, average R/S over the non-overlapping chunks of length L.
H is the slope of log(R/S) vs log(L). Fully trailing: H[i] uses only data <= i.
Returns array len(logret); NaN before `win` bars of history exist.
"""
n = len(logret)
H = np.full(n, np.nan)
lags = np.unique(np.floor(np.geomspace(8, win, n_lags)).astype(int))
lags = lags[lags >= 4]
if len(lags) < 3:
return H
for i in range(win, n):
seg = logret[i - win + 1: i + 1] # trailing window ending at i
rs_vals, ll = [], []
for L in lags:
nchunks = len(seg) // L
if nchunks < 1:
continue
rss = []
for k in range(nchunks):
chunk = seg[k * L:(k + 1) * L]
z = np.cumsum(chunk - chunk.mean())
R = z.max() - z.min()
S = chunk.std()
if S > 1e-12 and R > 0:
rss.append(R / S)
if rss:
rs_vals.append(np.mean(rss))
ll.append(np.log(L))
if len(rs_vals) >= 3:
H[i] = np.polyfit(np.asarray(ll), np.log(np.asarray(rs_vals)), 1)[0]
return H
def signal(df):
c = df["close"].values.astype(float)
lr = bl.log_returns(c) # causal, lr[0]=0
# --- regime detector: rolling causal Hurst (neutral before warmup) ---
H = np.nan_to_num(_rs_hurst(lr, HWIN), nan=0.55)
# --- TREND mode: multi-horizon time-series momentum (all trailing) ---
trend = np.zeros(len(c))
for L in (20, 60, 120):
mom = np.zeros(len(c))
mom[L:] = np.sign(c[L:] / c[:-L] - 1.0)
trend += mom
trend /= 3.0
# --- MEAN-REVERT mode: fade the short-horizon z-score of price vs short MA ---
rev_raw = c / bl.sma(c, 10) - 1.0
revert = -np.tanh(1.5 * bl.zscore(rev_raw, 50))
# --- Hurst regime switch: trend by default, revert only on deep anti-persistence ---
raw = np.where(H >= RTHR, trend, revert)
raw = np.clip(raw, -1.0, 1.0)
# --- volatility targeting keeps drawdown far below buy&hold ---
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL,
vol_win_days=VOL_WIN, leverage_cap=1.0)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)