1afb1014c9
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>
147 lines
6.7 KiB
Python
147 lines
6.7 KiB
Python
"""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)
|