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,176 @@
|
||||
"""Agent 31 — Small MLPRegressor forward-return forecast (family=ml, slug=mlp_reg).
|
||||
|
||||
THE ANGLE (assigned): a SMALL MLPRegressor (sklearn, one hidden layer) forecasting the
|
||||
forward return from a causal feature vector, refit on an EXPANDING walk-forward window,
|
||||
turned into a vol-targeted position. A genuine nonlinear ML angle (a tiny neural net) — it
|
||||
can in principle pick up interactions the linear ridge/logistic models cannot — kept FAST
|
||||
(small net, few iterations, infrequent refit) to stay under the time budget.
|
||||
|
||||
WHAT THE TRAIN DATA ACTUALLY SAYS (the honest finding, mirroring ridge/logistic agents):
|
||||
* NEXT-BAR return on these curves is unforecastable (hit-rate ~coin flip). I forecast a
|
||||
multi-bar FORWARD return (horizon FWD_H), the autocorrelated/forecastable quantity.
|
||||
* The MLP forecast carries a weak, regime-dependent signal. On these strongly up-trending
|
||||
curves the robust, defensible win is RISK CONTROL — being long when the model is not
|
||||
bearish, stepping to cash (and only cautiously short) when it is — NOT a PnL engine.
|
||||
* The conviction is vol-targeted so the DRAWDOWN, not the raw forecast, is what we control.
|
||||
|
||||
CAUSALITY (the whole game):
|
||||
* Features at row i use ONLY data up to and including bar i (rows <= i): lagged log-
|
||||
returns, multi-horizon trailing momentum, trailing realized vol, RSI, distance-from-MA.
|
||||
* The TARGET for row j is the cumulative log-return over bar j -> j+FWD_H, which needs
|
||||
close[j+FWD_H]. Sitting at decision-row i we may train ONLY on rows whose target is
|
||||
already realized: j + FWD_H <= i => j <= i - FWD_H. Row i's own target is NEVER used.
|
||||
* The MLP is refit on the EXPANDING window of those realized (X, y) pairs at most every
|
||||
REFIT_EVERY bars; weights frozen in between. To keep refits deterministic AND fast we
|
||||
use a fixed random_state, a single small hidden layer, and a capped iteration budget.
|
||||
-> Verified by causality_ok (signal on a prefix must match signal on the full array).
|
||||
|
||||
TUNING (split='train' only, combined A & B): small net (one layer 8 units) + strong L2
|
||||
(alpha=3) so the thin edge is not overfit; FWD_H=15 (next-bar is noise); WARMUP=200 realized
|
||||
pairs; conviction = tanh(0.6 * zscored forecast) as a SMALL lean around a constant long base
|
||||
(0.3), clipped, then vol-targeted at 0.18 (cap 1.0). I measured the walk-forward forecast's
|
||||
correlation with the realized forward return directly: ~+0.01 on A, ~-0.05 on B, sign-hit
|
||||
~0.48 — i.e. NEAR ZERO and inconsistent in sign across the two series and across horizons
|
||||
10..40. So the forecast is treated as a weak modulation, not a directional engine.
|
||||
|
||||
HONEST READ: forward-return forecastability here is essentially absent and an MLP does NOT
|
||||
create it (corr ~0, sign-hit < 0.5). The defensible win is RISK CONTROL: a vol-targeted,
|
||||
long-biased book whose drawdown is ~4x smaller than buy&hold (train DD ~0.20 vs ~0.77-0.79).
|
||||
The MLP's contribution is marginal-but-positive on train — adding it to a flat long base lifts
|
||||
Sharpe_min 0.844->0.899 and PnL 0.40->0.55 — but this is a small lean, not alpha. The bulk of
|
||||
the result is the long bias + vol-targeting; the MLP forecast is a thin garnish. That thinness,
|
||||
and the inconsistent forecast sign across series, are the honest caveats for this angle.
|
||||
"""
|
||||
import warnings
|
||||
import numpy as np
|
||||
import blindlib as bl
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
try:
|
||||
from sklearn.neural_network import MLPRegressor
|
||||
_HAVE_SK = True
|
||||
except Exception: # pragma: no cover - sklearn expected present
|
||||
_HAVE_SK = False
|
||||
|
||||
# ---- tuned on split='train' only ----
|
||||
HIDDEN = (8,) # ONE small hidden layer (keep it tiny: edge is thin, refit fast)
|
||||
MLP_ALPHA = 3.0 # L2 penalty (STRONG: the lag->return edge is tiny -> resist overfit)
|
||||
MAX_ITER = 120 # capped optimizer iterations (speed; net is small so it converges)
|
||||
WARMUP = 200 # realized (X, y) pairs required before the first fit
|
||||
REFIT_EVERY = 40 # expanding-window refit cadence (infrequent -> MLP cost stays low)
|
||||
LAGS = (1, 2, 3, 5, 10) # lagged log-return features
|
||||
MOM_WINS = (10, 20, 40) # multi-horizon trailing-momentum features
|
||||
VOL_WIN = 20 # trailing realized-vol feature window
|
||||
RSI_WIN = 14 # RSI feature window
|
||||
MA_WIN = 50 # distance-from-MA feature window
|
||||
FWD_H = 15 # forecast HORIZON (bars). Next-bar is noise; multi-bar is forecastable.
|
||||
GAIN = 0.6 # tanh conviction gain on the standardized forecast (DD/PnL dial). LOW:
|
||||
# the forecast is near-noise (train corr ~0), so it only LIGHTLY trims.
|
||||
LONG_BASE = 0.30 # constant long bias the forecast modulates AROUND. The curves trend up
|
||||
# and the forecast carries no reliable sign, so the defensible book is
|
||||
# "mostly long, let the weak forecast lean it" — not "gate to cash on noise".
|
||||
INVERT = False # sign of the train forecast<->forward-return correlation (set by tuning)
|
||||
LONG_FLOOR = -0.30 # allow only shallow shorts (curves only trend up -> shorts mostly lose)
|
||||
TARGET_VOL = 0.18 # 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."""
|
||||
n = len(c)
|
||||
lr = np.zeros(n)
|
||||
lr[1:] = np.log(c[1:] / c[:-1]) # lr[i] = return of bar ending at i (causal)
|
||||
csum = np.cumsum(lr)
|
||||
cs2 = np.cumsum(lr * lr)
|
||||
|
||||
cols = []
|
||||
# lagged returns: value at i is the return k bars ago (all <= i)
|
||||
for k in LAGS:
|
||||
f = np.zeros(n)
|
||||
if k < n:
|
||||
f[k:] = lr[: n - k]
|
||||
cols.append(f)
|
||||
# multi-horizon trailing momentum: cumulative log-return over last w bars (<= i)
|
||||
for w in MOM_WINS:
|
||||
mom = np.zeros(n)
|
||||
mom[w:] = csum[w:] - csum[:-w]
|
||||
cols.append(mom)
|
||||
# trailing realized vol (std of last VOL_WIN returns, <= i)
|
||||
vol = np.zeros(n)
|
||||
for i in range(VOL_WIN, n):
|
||||
m = (csum[i] - csum[i - VOL_WIN]) / VOL_WIN
|
||||
v = (cs2[i] - cs2[i - VOL_WIN]) / VOL_WIN - m * m
|
||||
vol[i] = np.sqrt(max(v, 0.0))
|
||||
cols.append(vol)
|
||||
# RSI (causal, from blindlib), centered to ~[-0.5, 0.5]
|
||||
rsi = np.nan_to_num(bl.rsi(c, RSI_WIN), nan=50.0) / 100.0 - 0.5
|
||||
cols.append(rsi)
|
||||
# distance from a trailing MA (causal): log(close / sma)
|
||||
ma = np.nan_to_num(bl.sma(c, MA_WIN), nan=c[0])
|
||||
ma[ma <= 0] = 1e-9
|
||||
dist = np.log(np.maximum(c, 1e-9) / ma)
|
||||
dist[:MA_WIN] = 0.0
|
||||
cols.append(dist)
|
||||
|
||||
X = np.column_stack(cols)
|
||||
return X, lr, csum
|
||||
|
||||
|
||||
def signal(df):
|
||||
c = df["close"].values.astype(float)
|
||||
n = len(c)
|
||||
X, lr, csum = _build_features(c)
|
||||
|
||||
# target[j] = cumulative log-return over bar j -> j+FWD_H (needs close[j+FWD_H]);
|
||||
# realized (known) only as of close[j+FWD_H].
|
||||
target = np.zeros(n)
|
||||
target[: n - FWD_H] = csum[FWD_H:] - csum[: n - FWD_H]
|
||||
|
||||
first = max(max(LAGS), max(MOM_WINS), VOL_WIN, RSI_WIN, MA_WIN) # first fully-featured row
|
||||
yhat = np.zeros(n) # forecast of the forward return, decided at close[i]
|
||||
sig_y = np.ones(n) # scale of recent training targets (for standardization)
|
||||
coef = None # frozen (mu, sd, model)
|
||||
|
||||
for i in range(n):
|
||||
last_train = i - FWD_H # target of last_train uses close[i], realized now
|
||||
ntrain = last_train - first + 1
|
||||
if ntrain < WARMUP:
|
||||
continue
|
||||
if coef is None or (i % REFIT_EVERY == 0):
|
||||
Xtr = X[first : last_train + 1]
|
||||
ytr = target[first : last_train + 1]
|
||||
mu = Xtr.mean(axis=0)
|
||||
sd = Xtr.std(axis=0)
|
||||
sd[sd < 1e-12] = 1.0
|
||||
Xs = (Xtr - mu) / sd
|
||||
sy = ytr.std()
|
||||
sy = sy if sy > 1e-9 else 1.0
|
||||
ys = ytr / sy # standardize target so the net trains stably
|
||||
if _HAVE_SK:
|
||||
m = MLPRegressor(hidden_layer_sizes=HIDDEN, activation="tanh",
|
||||
alpha=MLP_ALPHA, solver="lbfgs", max_iter=MAX_ITER,
|
||||
random_state=0)
|
||||
m.fit(Xs, ys)
|
||||
coef = (mu, sd, m, sy)
|
||||
sig_y[i] = ytr.std() if ytr.std() > 1e-9 else 1.0
|
||||
else:
|
||||
sig_y[i] = sig_y[i - 1]
|
||||
if coef is not None:
|
||||
mu, sd, m, sy = coef
|
||||
xi = ((X[i] - mu) / sd).reshape(1, -1)
|
||||
yhat[i] = float(m.predict(xi)[0]) * sy
|
||||
|
||||
# forecast -> bounded conviction (de-emphasize tiny/noisy forecasts, saturate strong ones)
|
||||
s = np.where(sig_y > 1e-9, sig_y, 1.0)
|
||||
fc = np.tanh(GAIN * yhat / s) # weak MLP conviction (~noise) -> only a small lean
|
||||
fc = np.nan_to_num(fc, nan=0.0)
|
||||
if INVERT:
|
||||
fc = -fc
|
||||
# mostly-long book the forecast modulates around (NOT a gate-to-cash on a noisy forecast)
|
||||
direction = np.clip(LONG_BASE + fc, LONG_FLOOR, 1.0)
|
||||
|
||||
pos = bl.vol_target(direction, 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