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>
95 lines
3.9 KiB
Python
95 lines
3.9 KiB
Python
"""agent_49_adx_dir — Trend-strength (ADX-like) GATED directional position.
|
|
|
|
ANGLE [family=mix, slug=adx_dir]:
|
|
Build a causal ADX (Average Directional Index) from directional movement and ATR.
|
|
ADX measures TREND STRENGTH (not direction). We take a directional position ONLY
|
|
when trend strength is HIGH (ADX above an adaptive, past-only threshold); otherwise
|
|
flat. Direction is the directional-movement sign (+DI vs -DI). Size is vol-targeted
|
|
so a calm strong trend and a violent one carry comparable risk.
|
|
|
|
Long-only: on these strongly up-trending overlaid curves, shorting "strong"
|
|
down-moves (which are mostly sharp counter-trend dips that snap back) was net-
|
|
negative and added drawdown in the train sweep — the honest result is that the
|
|
ADX gate adds value as a LONG participation filter, lifting risk-adjusted return
|
|
(train combined Sharpe ~1.1 at ~10% DD vs buy&hold ~1.0 at ~77% DD), not by
|
|
catching the declines short.
|
|
|
|
Everything is causal: +DM/-DM, ATR (Wilder EWM), DI, DX, ADX (EWM of DX) all use
|
|
only data up to bar i. The ADX gate threshold is an EXPANDING quantile (past-only),
|
|
so the strength bar adapts to each curve without peeking forward.
|
|
|
|
Tuned ONLY on split='train'. Params chosen on a broad plateau (win 10-20, gate
|
|
q 0.30-0.45 all positive at <15% DD), centered at win=14, q=0.38.
|
|
"""
|
|
import numpy as np
|
|
import pandas as pd
|
|
import blindlib as bl
|
|
|
|
ADX_WIN = 14 # directional-movement / ADX smoothing window
|
|
GATE_Q = 0.38 # expanding-quantile threshold on ADX (trend-strength gate)
|
|
GATE_MINP = 120 # warmup bars before the gate can fire
|
|
TARGET_VOL = 0.20
|
|
VOL_WIN = 30
|
|
LEV_CAP = 1.0
|
|
|
|
|
|
def _wilder(x, win):
|
|
"""Wilder smoothing == EWM with alpha=1/win, adjust=False. Fully causal."""
|
|
return pd.Series(x).ewm(alpha=1.0 / win, adjust=False).mean().values
|
|
|
|
|
|
def _adx(df, win):
|
|
"""Causal ADX + DI+ / DI-. value[i] uses only data <= i."""
|
|
h = df["high"].values.astype(float)
|
|
l = df["low"].values.astype(float)
|
|
c = df["close"].values.astype(float)
|
|
pc = np.roll(c, 1); pc[0] = c[0]
|
|
ph = np.roll(h, 1); ph[0] = h[0]
|
|
pl = np.roll(l, 1); pl[0] = l[0]
|
|
|
|
up = h - ph # this bar's up extension
|
|
dn = pl - l # this bar's down extension
|
|
plus_dm = np.where((up > dn) & (up > 0), up, 0.0)
|
|
minus_dm = np.where((dn > up) & (dn > 0), dn, 0.0)
|
|
|
|
tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
|
|
atr = _wilder(tr, win)
|
|
atr_safe = np.where(atr > 0, atr, np.nan)
|
|
|
|
di_plus = np.nan_to_num(100.0 * _wilder(plus_dm, win) / atr_safe, nan=0.0)
|
|
di_minus = np.nan_to_num(100.0 * _wilder(minus_dm, win) / atr_safe, nan=0.0)
|
|
|
|
di_sum = di_plus + di_minus
|
|
dx = 100.0 * np.abs(di_plus - di_minus) / np.where(di_sum > 0, di_sum, np.nan)
|
|
dx = np.nan_to_num(dx, nan=0.0)
|
|
adx = _wilder(dx, win)
|
|
return adx, di_plus, di_minus
|
|
|
|
|
|
def _expanding_quantile(x, q, min_periods):
|
|
"""Past-only expanding quantile. value[i] uses x[0..i] -> causal."""
|
|
out = pd.Series(x).expanding(min_periods=min_periods).quantile(q).values
|
|
return np.where(np.isfinite(out), out, np.inf) # flat (inf thr) until warmed
|
|
|
|
|
|
def signal(df):
|
|
c = df["close"].values.astype(float)
|
|
|
|
adx, di_p, di_m = _adx(df, ADX_WIN)
|
|
|
|
# Trend-STRENGTH gate: only act when ADX is in its upper regime (past-only thr).
|
|
adx_thr = _expanding_quantile(adx, GATE_Q, GATE_MINP)
|
|
strong = adx > adx_thr
|
|
|
|
# Direction from directional movement: +DI dominant -> up, -DI dominant -> down.
|
|
di_dir = np.sign(di_p - di_m)
|
|
# Long-only on these up-trending curves (shorting strong dips was net-negative).
|
|
raw_dir = np.where(di_dir > 0, 1.0, 0.0)
|
|
|
|
direction = np.where(strong, raw_dir, 0.0).astype(float)
|
|
|
|
# Vol-target so calm strong trends and wild ones carry comparable risk.
|
|
pos = bl.vol_target(direction, df, target_vol=TARGET_VOL,
|
|
vol_win_days=VOL_WIN, leverage_cap=LEV_CAP)
|
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|