Files
PythagorasGoal/scripts/research/blind/agents/agent_35_rls.py
T
Adriano Dal Pastro 1afb1014c9 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>
2026-06-21 07:05:04 +00:00

81 lines
3.6 KiB
Python

"""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)