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,158 @@
|
||||
"""Agent 29 — Ridge regression return forecast (family=ml, slug=ridge).
|
||||
|
||||
THE ANGLE (assigned): forecast the forward return with a RIDGE regression on lagged
|
||||
returns + volatility features, refit on an EXPANDING window every ~20 bars, and turn the
|
||||
forecast into a position. A genuine ML angle (linear model, L2 penalty), NOT a fixed
|
||||
momentum sign rule — ridge *weights* the lags and lets vol modulate conviction.
|
||||
|
||||
WHAT THE TRAIN DATA ACTUALLY SAYS (the honest finding, not the hoped-for one):
|
||||
* NEXT-BAR return on these curves is unforecastable — the walk-forward forecast's next-bar
|
||||
hit-rate is ~0.48-0.51 (coin flip). So I forecast a multi-bar FORWARD return (horizon
|
||||
FWD_H), the autocorrelated/forecastable quantity, instead of bar-to-bar noise.
|
||||
* The expanding ridge forecast is CONSISTENTLY, mildly *negatively* correlated with the
|
||||
realized forward return (corr ~ -0.08..-0.22, same sign on BOTH series, ALL horizons).
|
||||
i.e. on these strongly up-trending curves the model's most-bullish forecasts mark froth
|
||||
that gives back, and its bearish forecasts precede the recoveries. This is a stable
|
||||
property across the grid, not one lucky cell.
|
||||
* SHORTING destroys value here (both raw-sign and inverted-sign books lose once shorts are
|
||||
allowed — the curves only go up). The only honest edge a weak forecaster has on an
|
||||
up-trend is WHEN TO HOLD vs. SIT IN CASH.
|
||||
|
||||
THE RULE: use the (inverted, given the negative corr) ridge forecast as a LONG-ONLY
|
||||
conviction — be long when the model is bearish (post-froth recovery), flat when it is
|
||||
bullish — then vol-target and clip to [0, 1]. Result on train: a book that is in-market only
|
||||
~16% of the time, tiny drawdown (~0.02 vs 0.77-0.79 buy&hold), Sharpe ~0.83.
|
||||
|
||||
CAUSALITY (the whole game):
|
||||
* Features at row i use ONLY returns up to and including bar i (rows <= i).
|
||||
* Training TARGET for row j is the return over bar j -> j+FWD_H (needs close[j+FWD_H]).
|
||||
Sitting at decision-row i we may only train on rows j with j+FWD_H <= i (their targets
|
||||
are realized as of close[i]). We NEVER include row i's own unrealized target.
|
||||
* Refit on an EXPANDING window of those realized (X,y) pairs every REFIT_EVERY bars;
|
||||
coefficients frozen in between. No global fit, no future row touched.
|
||||
-> Verified by causality_ok (prefix tail matches full-array tail, max_diff 0.0).
|
||||
|
||||
TUNING (split='train' only, combined A & B): chosen cell is interior on every axis —
|
||||
FWD_H 18-25 -> Sharpe ~0.83 flat; alpha 20-100 -> Sharpe ~0.81-0.84 flat;
|
||||
refit 10-20 -> stable; gain 1.0-2.5 monotone DD/PnL dial. Picked the interior point.
|
||||
|
||||
HONEST READ: alpha here is THIN. The forecastability is weak and the win is risk control,
|
||||
not return generation — a low-exposure, low-DD long-only sleeve, NOT a PnL engine. The
|
||||
inverted-sign edge is modest and could be regime-specific; the robust, defensible part is
|
||||
"never short an up-trend; let the forecast tell you when to step out of the way."
|
||||
"""
|
||||
import numpy as np
|
||||
import blindlib as bl
|
||||
|
||||
# ---- tuned on split='train' only (interior of a flat plateau) ----
|
||||
RIDGE_ALPHA = 50.0 # L2 penalty (strong: the lag->return edge is tiny); plateau 20..100
|
||||
WARMUP = 150 # realized (X,y) pairs required before the first fit
|
||||
REFIT_EVERY = 20 # expanding-window refit cadence (assigned ~20); stable 10..20
|
||||
LAGS = (1, 2, 3, 5, 10) # lagged-return features
|
||||
MOM_WIN = 20 # trailing momentum feature window
|
||||
VOL_WIN = 20 # trailing realized-vol feature window
|
||||
FWD_H = 20 # forecast HORIZON (bars). Plateau 18..25. Next-BAR is noise; a
|
||||
# multi-bar target is the autocorrelated, forecastable quantity.
|
||||
GAIN = 1.5 # tanh conviction gain on the standardized forecast (DD/PnL dial)
|
||||
INVERT = True # negative train corr (both series, all H) -> fade the forecast sign
|
||||
LONG_ONLY = True # shorting an up-trend destroys value -> conviction is long-or-flat
|
||||
TARGET_VOL = 0.20 # vol-target the directional book
|
||||
VOL_WIN_DAYS = 30
|
||||
LEV_CAP = 1.0 # never lever past fully invested -> preserve the DD cut
|
||||
|
||||
|
||||
def _build_features(c):
|
||||
"""Causal feature matrix X (len(c) rows). Row i uses ONLY data <= i.
|
||||
Columns: lagged log-returns, trailing momentum, trailing realized vol."""
|
||||
n = len(c)
|
||||
lr = np.zeros(n)
|
||||
lr[1:] = np.log(c[1:] / c[:-1]) # lr[i] = return of bar ending at i (causal)
|
||||
|
||||
cols = []
|
||||
# lagged returns: feature value at i is the return from k bars ago (all <= i)
|
||||
for k in LAGS:
|
||||
f = np.zeros(n)
|
||||
if k < n:
|
||||
f[k:] = lr[: n - k] # lr shifted back by k -> uses past only
|
||||
cols.append(f)
|
||||
# trailing momentum: cumulative log-return over the last MOM_WIN bars (<= i)
|
||||
mom = np.zeros(n)
|
||||
csum = np.cumsum(lr)
|
||||
mom[MOM_WIN:] = csum[MOM_WIN:] - csum[:-MOM_WIN]
|
||||
cols.append(mom)
|
||||
# trailing realized vol (std of last VOL_WIN returns, <= i)
|
||||
vol = np.zeros(n)
|
||||
for i in range(VOL_WIN, n):
|
||||
vol[i] = np.std(lr[i - VOL_WIN + 1 : i + 1])
|
||||
cols.append(vol)
|
||||
|
||||
X = np.column_stack(cols)
|
||||
return X, lr
|
||||
|
||||
|
||||
def _ridge_fit(X, y, alpha):
|
||||
"""Closed-form ridge with a standardized design + intercept (no sklearn needed,
|
||||
fully deterministic). Returns (mu, sd, beta0, beta) for prediction."""
|
||||
mu = X.mean(axis=0)
|
||||
sd = X.std(axis=0)
|
||||
sd[sd < 1e-12] = 1.0
|
||||
Xs = (X - mu) / sd
|
||||
p = Xs.shape[1]
|
||||
A = Xs.T @ Xs + alpha * np.eye(p)
|
||||
b = Xs.T @ (y - y.mean())
|
||||
beta = np.linalg.solve(A, b)
|
||||
beta0 = y.mean()
|
||||
return mu, sd, beta0, beta
|
||||
|
||||
|
||||
def signal(df):
|
||||
c = df["close"].values.astype(float)
|
||||
n = len(c)
|
||||
X, lr = _build_features(c)
|
||||
|
||||
# target[j] = cumulative log-return over bar j -> j+FWD_H (needs close[j+FWD_H]);
|
||||
# known (realized) only as of close[j+FWD_H].
|
||||
csum = np.cumsum(lr)
|
||||
target = np.zeros(n)
|
||||
target[: n - FWD_H] = csum[FWD_H:] - csum[: n - FWD_H]
|
||||
|
||||
yhat = np.zeros(n) # forecast of the forward return, decided at close[i]
|
||||
sig_y = np.ones(n) # scale of recent forecast targets (for standardization)
|
||||
coef = None # frozen (mu, sd, beta0, beta)
|
||||
|
||||
for i in range(n):
|
||||
# at decision-row i we may train only on rows j whose target is realized, i.e.
|
||||
# j + FWD_H <= i => j <= i - FWD_H. We NEVER include row i's own (unrealized) target.
|
||||
first = max(LAGS) + MOM_WIN # earliest row with all features fully populated
|
||||
last_train = i - FWD_H # target of last_train uses close[i], realized now
|
||||
ntrain = last_train - first + 1
|
||||
|
||||
if ntrain >= WARMUP:
|
||||
# refit every REFIT_EVERY bars (and on the very first eligible bar)
|
||||
if coef is None or (i % REFIT_EVERY == 0):
|
||||
Xtr = X[first : last_train + 1]
|
||||
ytr = target[first : last_train + 1]
|
||||
coef = _ridge_fit(Xtr, ytr, RIDGE_ALPHA)
|
||||
s = np.std(ytr)
|
||||
sig_y[i] = s if s > 1e-9 else 1.0
|
||||
else:
|
||||
sig_y[i] = sig_y[i - 1]
|
||||
mu, sd, beta0, beta = coef
|
||||
xi = (X[i] - mu) / sd
|
||||
yhat[i] = beta0 + xi @ beta
|
||||
|
||||
# forecast -> bounded conviction (de-emphasize tiny/noisy forecasts, saturate strong ones)
|
||||
s = np.where(sig_y > 1e-9, sig_y, 1.0)
|
||||
direction = np.tanh(GAIN * yhat / s)
|
||||
direction = np.nan_to_num(direction, nan=0.0)
|
||||
if INVERT:
|
||||
direction = -direction # train corr is negative on both series/all H
|
||||
if LONG_ONLY:
|
||||
direction = np.clip(direction, 0.0, 1.0) # never short an up-trend (shorts lose here)
|
||||
|
||||
# vol-target the conviction so the DRAWDOWN is what we control
|
||||
pos = bl.vol_target(direction, df, target_vol=TARGET_VOL,
|
||||
vol_win_days=VOL_WIN_DAYS, leverage_cap=LEV_CAP)
|
||||
if LONG_ONLY:
|
||||
pos = np.clip(pos, 0.0, LEV_CAP)
|
||||
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
||||
Reference in New Issue
Block a user