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>
97 lines
4.1 KiB
Python
97 lines
4.1 KiB
Python
"""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)
|