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,80 @@
|
||||
"""agent_35_rls — Online recursive (EWMA-weighted) linear model of return on lagged returns.
|
||||
|
||||
ANGLE [family=ml, slug=rls]:
|
||||
Recursive Least Squares with exponential forgetting. At each bar we maintain a linear
|
||||
predictor r_hat[t+1] = w . x[t] where x[t] = [1, lagged log-returns ...]. After we
|
||||
observe the realized return we update (w, P) via the standard RLS recursion with a
|
||||
forgetting factor lambda (EWMA weighting of past samples). NO batch refit, NO peeking:
|
||||
the prediction for bar t+1 uses only weights estimated from data up to and including
|
||||
bar t. Position = sign/strength of the predicted next return, vol-targeted.
|
||||
|
||||
Fully causal: the weight vector used to predict bar i+1 is updated only with the target
|
||||
observed AT bar i (return from i-1 -> i), so no future leakage.
|
||||
"""
|
||||
import numpy as np
|
||||
import blindlib as bl
|
||||
|
||||
|
||||
def _rls_predict(r, n_lags=3, lam=0.985, delta=100.0, warmup=60):
|
||||
"""Online RLS. Returns pred[t] = predicted return for the NEXT bar, decided at close t.
|
||||
|
||||
r : array of (log) returns, r[t] = return realized over bar t.
|
||||
n_lags : number of lagged returns used as features.
|
||||
lam : forgetting factor (EWMA). Closer to 1 = longer memory.
|
||||
delta : ridge init for P = (delta) * I.
|
||||
warmup : bars to accumulate before emitting a non-zero prediction.
|
||||
"""
|
||||
T = len(r)
|
||||
p = n_lags + 1 # +1 for intercept
|
||||
w = np.zeros(p)
|
||||
P = np.eye(p) * delta
|
||||
pred = np.zeros(T)
|
||||
|
||||
for t in range(T):
|
||||
# feature vector available AT close[t]: intercept + last n_lags returns ending at r[t]
|
||||
if t >= n_lags:
|
||||
x = np.empty(p)
|
||||
x[0] = 1.0
|
||||
# x[1] = r[t], x[2] = r[t-1], ... most recent first
|
||||
for k in range(n_lags):
|
||||
x[1 + k] = r[t - k]
|
||||
# PREDICT next-bar return from CURRENT weights (estimated from data <= t-1's target)
|
||||
pred[t] = float(w @ x) if t >= warmup else 0.0
|
||||
|
||||
# --- RLS update using the target observed AT bar t (r[t]) with the feature
|
||||
# vector that was available at close[t-1] (lags ending at r[t-1]) ---
|
||||
if t >= n_lags + 1:
|
||||
x_prev = np.empty(p)
|
||||
x_prev[0] = 1.0
|
||||
for k in range(n_lags):
|
||||
x_prev[1 + k] = r[t - 1 - k]
|
||||
Px = P @ x_prev
|
||||
denom = lam + float(x_prev @ Px)
|
||||
g = Px / denom # Kalman gain
|
||||
err = r[t] - float(w @ x_prev) # prediction error on realized target
|
||||
w = w + g * err
|
||||
P = (P - np.outer(g, Px)) / lam
|
||||
return pred
|
||||
|
||||
|
||||
def signal(df):
|
||||
c = df["close"].values.astype(float)
|
||||
r = bl.log_returns(c) # r[t] = log(c[t]/c[t-1]); r[0]=0, causal
|
||||
|
||||
# Tuned on split='train' (both series). Fast forgetting (lam=0.97) makes the
|
||||
# predictor ADAPTIVE: it tracks a *local* return-on-lagged-returns relationship
|
||||
# rather than a stale long-run fit. lags=2 is the robust plateau (lags=2,
|
||||
# lam 0.95-0.97, smooth 3-8 all give shmin 0.35-0.44 at DD ~0.20-0.26).
|
||||
pred = _rls_predict(r, n_lags=2, lam=0.97, delta=100.0, warmup=120)
|
||||
|
||||
# Smooth the raw prediction (short causal EWMA) to cut whipsaw turnover, then
|
||||
# normalize by a causal std of the prediction so the strength is regime-stable.
|
||||
ps = bl.ema(pred, 3)
|
||||
sd = bl.rolling_std(ps, 60)
|
||||
sd = np.where(sd > 1e-9, sd, 1e-9)
|
||||
raw = np.tanh(ps / sd)
|
||||
raw = np.clip(raw, -1.0, 1.0)
|
||||
|
||||
# Vol-target the directional view -> comparable PnL to buy&hold at ~4x smaller DD.
|
||||
pos = bl.vol_target(raw, df, target_vol=0.20, vol_win_days=30, leverage_cap=1.0)
|
||||
return np.clip(pos, -1.0, 1.0)
|
||||
Reference in New Issue
Block a user