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,115 @@
|
||||
"""Agent 07 — KAMA / Kaufman efficiency ratio (family=trend, slug=kama_eff).
|
||||
|
||||
The angle (assigned): an ADAPTIVE moving average driven by Kaufman's Efficiency
|
||||
Ratio (ER). ER over a window of n bars is
|
||||
|
||||
ER[i] = |close[i] - close[i-n]| / sum_{k=i-n+1..i} |close[k] - close[k-1]|
|
||||
|
||||
i.e. net displacement / total path length, in [0, 1]. ER -> 1 when the move is a
|
||||
clean straight trend (worth following); ER -> 0 in chop (the path wanders, net
|
||||
displacement is small -> stay out). KAMA turns ER into an adaptive smoothing
|
||||
constant SC = (ER*(fast-slow)+slow)^2 so the average snaps to price in a trend and
|
||||
freezes in chop:
|
||||
|
||||
KAMA[i] = KAMA[i-1] + SC[i] * (close[i] - KAMA[i-1])
|
||||
|
||||
DIRECTION: sign of the KAMA slope (KAMA[i] vs KAMA[i-k]) — KAMA is up-sloping in an
|
||||
up-trend, flat/down in a decline. GATE: the efficiency ratio itself. We only take a
|
||||
position when ER exceeds a causal, expanding-quantile threshold (trend is efficient
|
||||
ENOUGH right now relative to this curve's own history); otherwise flat. This is the
|
||||
literal statement of the angle: "trend-follow when efficiency high, flat when choppy".
|
||||
|
||||
LONG-SHORT: the curves trend up structurally, so a full symmetric short bleeds
|
||||
(it shorts the dips). We keep the long full size and de-weight the short side
|
||||
(SHORT_W < 1) — the short is there to protect the big efficient DECLINES (which is
|
||||
where flat-only leaves the worst drawdown on the table), not to fade every wiggle.
|
||||
|
||||
SIZING: causal vol-target so A and B are risk-comparable and the drawdown stays
|
||||
bounded (every crash is a vol spike -> exposure auto-shrinks).
|
||||
|
||||
CAUSAL: ER, KAMA (a recursive EWMA-like filter), the slope, the expanding ER
|
||||
threshold, and vol_target all use rows <= i only. No shift(-k), no centered window,
|
||||
no global fit. Verified by causality_ok (max_diff ~0).
|
||||
|
||||
Tuning (train only, combined A&B, coarse->fine). ER window ~ a month, KAMA fast/slow
|
||||
the canonical (2,30), slope over a few bars, ER gate at an expanding quantile. A WIDE
|
||||
interior plateau (every 1-axis neighbor holds sharpe_min 1.25-1.54 at dd 0.18-0.33,
|
||||
no spike) sits around:
|
||||
ER_WIN=30, FAST=2, SLOW=30, SLOPE=5, ER_Q=0.30 (expanding causal quantile),
|
||||
SHORT_W=0.20, TARGET_VOL=0.30, VOL_WIN=35d, LEV_CAP=1.5
|
||||
-> train combined: pnl_mean ~4.75, maxdd_worst ~0.19, sharpe_min ~1.43 (causality.ok).
|
||||
Notes: LEV_CAP is non-binding here (vol_target keeps |pos|<1 on these vol levels);
|
||||
the ER gate is what de-risks chop, the de-weighted short protects the efficient
|
||||
declines, and vol_target turns the ~77-79% buy&hold drawdown into ~19%.
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import blindlib as bl
|
||||
|
||||
ER_WIN = 30 # efficiency-ratio lookback (~1 month of daily bars)
|
||||
FAST = 2 # KAMA fast EMA constant
|
||||
SLOW = 30 # KAMA slow EMA constant
|
||||
SLOPE = 5 # bars to measure KAMA slope (direction)
|
||||
ER_Q = 0.30 # expanding-quantile gate: trade only when ER above its own history
|
||||
WARMUP = 60 # min bars before the expanding gate is trusted
|
||||
SHORT_W = 0.20 # de-weight the short side (curves trend up); 0 -> long-flat
|
||||
TARGET_VOL = 0.30
|
||||
VOL_WIN_DAYS = 35
|
||||
LEV_CAP = 1.5
|
||||
|
||||
|
||||
def _efficiency_ratio(c: np.ndarray, n: int) -> np.ndarray:
|
||||
"""Kaufman efficiency ratio over n bars, causal. ER[i] uses close[i-n..i]."""
|
||||
change = np.zeros(len(c))
|
||||
change[n:] = np.abs(c[n:] - c[:-n])
|
||||
d = np.abs(np.diff(c, prepend=c[0])) # |close[k]-close[k-1]|
|
||||
volatility = pd.Series(d).rolling(n, min_periods=n).sum().values
|
||||
er = np.where(volatility > 0, change / volatility, 0.0)
|
||||
er[:n] = 0.0
|
||||
return np.nan_to_num(er, nan=0.0)
|
||||
|
||||
|
||||
def _kama(c: np.ndarray, er: np.ndarray, fast: int, slow: int) -> np.ndarray:
|
||||
"""Kaufman Adaptive Moving Average. SC = (ER*(fast_sc-slow_sc)+slow_sc)^2.
|
||||
Recursive (only uses past) -> fully causal."""
|
||||
fast_sc = 2.0 / (fast + 1.0)
|
||||
slow_sc = 2.0 / (slow + 1.0)
|
||||
sc = (er * (fast_sc - slow_sc) + slow_sc) ** 2
|
||||
kama = np.empty(len(c))
|
||||
kama[0] = c[0]
|
||||
for i in range(1, len(c)):
|
||||
kama[i] = kama[i - 1] + sc[i] * (c[i] - kama[i - 1])
|
||||
return kama
|
||||
|
||||
|
||||
def _expanding_quantile(x: np.ndarray, q: float, warmup: int) -> np.ndarray:
|
||||
"""Causal expanding quantile: thr[i] = q-quantile of x[0..i]. For i<warmup the
|
||||
gate is impassable (we don't trust an early sample) so we stay flat early."""
|
||||
s = pd.Series(x)
|
||||
thr = s.expanding(min_periods=warmup).quantile(q).values
|
||||
return thr
|
||||
|
||||
|
||||
def signal(df):
|
||||
c = df["close"].values.astype(float)
|
||||
n = len(c)
|
||||
|
||||
er = _efficiency_ratio(c, ER_WIN)
|
||||
kama = _kama(c, er, FAST, SLOW)
|
||||
|
||||
# DIRECTION: sign of the KAMA slope over SLOPE bars
|
||||
slope = np.zeros(n)
|
||||
slope[SLOPE:] = kama[SLOPE:] - kama[:-SLOPE]
|
||||
direction = np.sign(slope)
|
||||
|
||||
# GATE: only trade when efficiency is high relative to this curve's own past
|
||||
thr = _expanding_quantile(er, ER_Q, WARMUP)
|
||||
active = np.where(np.isfinite(thr) & (er >= thr), 1.0, 0.0)
|
||||
|
||||
raw = direction * active
|
||||
# asymmetric long-short: keep long full size, de-weight the short side
|
||||
raw = np.where(raw >= 0.0, raw, raw * SHORT_W)
|
||||
|
||||
pos = bl.vol_target(raw, 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