Files
PythagorasGoal/scripts/research/blind/agents/agent_39_effratio.py
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

100 lines
4.5 KiB
Python

"""Agent 39 — Efficiency-ratio / fractal GATE on a momentum signal (family=stat, slug=effratio).
THE ANGLE (assigned): take a plain momentum bet, but TRADE ONLY WHEN THE MOVE IS
"EFFICIENT". Efficiency = how straight the path is. We measure it with two
interchangeable causal fractal gauges and use them as an ON/OFF gate, NOT as an
adaptive average (that is the sibling KAMA angle). Here momentum decides DIRECTION
and the efficiency ratio decides WHETHER WE ARE ALLOWED TO TAKE THE TRADE.
EFFICIENCY GAUGES (both causal, both in [0,1], higher = straighter / more trending):
* Kaufman Efficiency Ratio (ER): net displacement / total path length over n bars.
ER[i] = |c[i]-c[i-n]| / sum_{k} |c[k]-c[k-1]|
ER -> 1 a clean directional move, ER -> 0 a random-walk chop.
* Fractal-dimension proxy (1 - normalized roughness): in chop the path's total
length is many times its displacement (high fractal dimension ~2 = plane-filling);
in a trend length ~ displacement (dimension ~1 = a line). We map this to an
efficiency score E_fd in [0,1] = ER itself is the cleanest such proxy, so the
primary gauge IS ER; we blend a SLOWER ER to require efficiency on two horizons.
DIRECTION (momentum): sign of a fast/slow EMA spread of price (a standard momentum
signal). This is the "plain momentum" the angle gates — not KAMA.
GATE: trade only when the (blended) efficiency ratio is above a CAUSAL expanding
quantile of its own history (the move is efficient ENOUGH for THIS curve right now).
In chop the gate is shut -> flat -> we skip the whipsaw that kills naked momentum.
LONG-SHORT: curves trend up structurally so a symmetric short bleeds (shorts the
dips). Keep the long full size, de-weight the short (SHORT_W) so the short only
protects the big EFFICIENT declines (a crash is a very efficient down-move -> the
gate is OPEN and momentum is down -> we are short exactly when it pays).
SIZING: causal vol_target so A and B are risk-comparable and every vol spike (= every
crash) auto-shrinks exposure -> the ~77-79% buy&hold drawdown collapses.
CAUSAL: EMA spread, ER (both horizons), the expanding-quantile gate, and vol_target
all use rows <= i only. No shift(-k), no centered window, no global fit. Verified by
causality_ok (max_diff ~0).
"""
import numpy as np
import pandas as pd
import blindlib as bl
# --- momentum (direction) --- [tuned on train, wide plateau]
EMA_FAST = 10
EMA_SLOW = 50
# --- efficiency gate (the angle) ---
ER_WIN = 25 # fast efficiency-ratio lookback (~1 month daily)
ER_WIN2 = 60 # slow efficiency-ratio lookback (require efficiency on 2 horizons)
ER_BLEND = 0.5 # weight of the slow ER in the blended gauge
ER_Q = 0.33 # expanding-quantile gate: trade only when eff above its own history
WARMUP = 60 # min bars before the expanding gate is trusted
# --- exposure ---
SHORT_W = 0.25 # de-weight the short side (curves trend up); 0 -> long-flat
TARGET_VOL = 0.30
VOL_WIN_DAYS = 25
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]))
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 _expanding_quantile(x: np.ndarray, q: float, warmup: int) -> np.ndarray:
"""Causal expanding quantile: thr[i] = q-quantile of x[0..i]. Impassable before warmup."""
return pd.Series(x).expanding(min_periods=warmup).quantile(q).values
def signal(df):
c = df["close"].values.astype(float)
n = len(c)
# DIRECTION: plain momentum = sign of fast-slow EMA spread
ef = bl.ema(c, EMA_FAST)
es = bl.ema(c, EMA_SLOW)
direction = np.sign(ef - es)
# EFFICIENCY GAUGE: blend a fast and a slow Kaufman efficiency ratio
er_fast = _efficiency_ratio(c, ER_WIN)
er_slow = _efficiency_ratio(c, ER_WIN2)
eff = (1.0 - ER_BLEND) * er_fast + ER_BLEND * er_slow
# GATE: only trade when efficiency is high relative to this curve's own past
thr = _expanding_quantile(eff, ER_Q, WARMUP)
active = np.where(np.isfinite(thr) & (eff >= thr), 1.0, 0.0)
raw = direction * active
raw = np.where(raw >= 0.0, raw, raw * SHORT_W) # de-weight the short side
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)