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:
Adriano Dal Pastro
2026-06-21 07:05:04 +00:00
parent f5d30d88b9
commit 1afb1014c9
63 changed files with 6660 additions and 0 deletions
@@ -0,0 +1,146 @@
"""Agent 34 — kNN analog matching (family=ml, slug=knn_analog).
THE ANGLE (assigned): find the PAST windows most similar to the CURRENT window and
predict the average forward move from how those analogs played out — fully causal.
HOW IT WORKS
* At each decision row i, build a normalized "shape" descriptor of the recent window
(the last W bars of standardized log-returns) plus a couple of slow-context features
(trailing momentum & realized vol). This is the QUERY.
* The DATABASE of analogs is every past anchor j whose forward outcome is already
realized as of close[i] (i.e. j + FWD_H <= i). Each anchor stores its descriptor and
its realized forward log-return over j -> j+FWD_H.
* Distance = Euclidean on the standardized descriptors. Take the K nearest analogs,
weight them by 1/(eps+dist), and the forecast is the weighted-average forward return
of those neighbors. "What happened next, the last K times the tape looked like this."
* Forecast -> bounded conviction (tanh of the standardized forecast).
CAUSALITY (the whole game):
* The query descriptor at i uses ONLY returns up to and including bar i.
* An anchor j is admissible ONLY if its forward window is complete as of i
(j + FWD_H <= i). We never peek at row i's own unrealized future, nor any j past i.
* Descriptor standardization uses each window's own mean/std (self-contained), so no
global statistics leak across the cut.
-> Verified by causality_ok (signal on a prefix matches the full-array tail).
WHAT THE TRAIN DATA SAYS (honest): next-bar direction on these curves is a coin flip, so
analogs are matched on SHAPE and asked for a multi-bar forward move (FWD_H). Like the other
ML angles on these strongly up-trending curves, shorting destroys value (the tape only goes
up), so the analog forecast is used as a LONG-vs-FLAT conviction with vol-targeting to cap
the drawdown — the win is risk control / staying out of the froth, not return generation.
"""
import numpy as np
import blindlib as bl
# ---- tuned on split='train' only ----
W = 10 # window length (bars) of the shape descriptor; interior opt (6/14/18 worse)
FWD_H = 15 # forward horizon predicted by the analogs (bars); interior (8/12 much worse)
K = 30 # number of nearest neighbors; flat plateau 20..50, K=30 = best DD
MOM_WIN = 40 # trailing-momentum context feature window; flat 40..60
VOL_WIN = 20 # trailing realized-vol context feature window
CTX_WEIGHT = 2.0 # weight of slow-context (regime) features vs the micro shape window.
# The REGIME analog (where in the trend, what vol) carries most of the
# edge here; up-weighting it lifts PnL 0.71->1.31 AND cuts DD. Flat 1.5..2.5.
WARMUP = 200 # min anchors in the database before we trust the forecast
GAIN = 8.0 # tanh conviction gain on the standardized forecast; smooth DD/PnL dial
LONG_ONLY = True # shorting an up-trend loses -> conviction is long-or-flat
TARGET_VOL = 0.20
VOL_WIN_DAYS = 30
LEV_CAP = 1.0
def _descriptors(c):
"""Causal feature matrix. Row i's descriptor uses ONLY data <= i.
Columns: W standardized log-returns of the trailing window + 2 context features."""
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)
# trailing momentum over MOM_WIN bars (<= i), trailing vol over VOL_WIN bars (<= i)
mom = np.zeros(n)
mom[MOM_WIN:] = csum[MOM_WIN:] - csum[:-MOM_WIN]
vol = np.zeros(n)
for i in range(VOL_WIN, n):
vol[i] = np.std(lr[i - VOL_WIN + 1 : i + 1])
D = W + 2
desc = np.full((n, D), np.nan)
for i in range(W, n):
win = lr[i - W + 1 : i + 1] # last W returns, all <= i
s = np.std(win)
if s < 1e-12:
s = 1.0
desc[i, :W] = (win - np.mean(win)) / s # standardized shape (location/scale free)
desc[i, W] = mom[i]
desc[i, W + 1] = vol[i]
return desc, lr
def signal(df):
c = df["close"].values.astype(float)
n = len(c)
desc, lr = _descriptors(c)
# forward log-return target[j] over bar j -> j+FWD_H (needs close[j+FWD_H]); realized
# (admissible) only once i >= j+FWD_H.
csum = np.cumsum(lr)
fwd = np.full(n, np.nan)
fwd[: n - FWD_H] = csum[FWD_H:] - csum[: n - FWD_H]
first = W # earliest fully-formed descriptor
yhat = np.zeros(n)
scale = np.ones(n) # CAUSAL trailing scale of the forecast (expanding std)
# online over admissible anchors so the shape window (already unit-scale) and context
# are comparable; computed causally.
for i in range(first, n):
last_anchor = i - FWD_H # anchors j <= last_anchor have realized fwd
if last_anchor < first + WARMUP:
continue
# admissible anchor descriptors & their realized forward returns
Xj = desc[first : last_anchor + 1]
yj = fwd[first : last_anchor + 1]
ok = np.isfinite(Xj).all(axis=1) & np.isfinite(yj)
if ok.sum() < WARMUP:
continue
Xj = Xj[ok]
yj = yj[ok]
q = desc[i].copy()
if not np.isfinite(q).all():
continue
# scale the 2 context columns by their (causal) std across the anchor set so they
# don't dominate / vanish vs the W unit-scale shape columns.
ctx_sd = np.std(Xj[:, W:], axis=0)
ctx_sd[ctx_sd < 1e-12] = 1.0
Xs = Xj.copy()
qs = q.copy()
Xs[:, W:] = (Xj[:, W:] / ctx_sd) * CTX_WEIGHT
qs[W:] = (q[W:] / ctx_sd) * CTX_WEIGHT
d = np.sqrt(np.sum((Xs - qs) ** 2, axis=1)) # Euclidean distance to every anchor
k = min(K, len(d))
idx = np.argpartition(d, k - 1)[:k] # K nearest (unordered ok)
dk = d[idx]
wk = 1.0 / (1e-6 + dk) # inverse-distance weights
yhat[i] = np.sum(wk * yj[idx]) / np.sum(wk) # weighted-avg forward move
# CAUSAL forecast scale: the realized-forward-return std over the SAME admissible
# anchor set (rows <= i-FWD_H). Self-contained, uses no future row. This is what
# standardizes the conviction without leaking a global statistic.
s = float(np.std(yj))
scale[i] = s if s > 1e-9 else 1.0
# standardize each forecast by its own causal trailing scale -> bounded conviction.
direction = np.tanh(GAIN * yhat / scale)
direction = np.nan_to_num(direction, nan=0.0)
if LONG_ONLY:
direction = np.clip(direction, 0.0, 1.0)
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)