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>
149 lines
6.7 KiB
Python
149 lines
6.7 KiB
Python
"""Agent 41 — Entropy/randomness gate (family=stat, slug=entropy).
|
|
|
|
The angle (assigned): estimate the PREDICTABILITY of the recent path and only take
|
|
the trend when the path is STRUCTURED (low entropy / non-random). When the recent
|
|
path is statistically random the trend is noise -> scale exposure down toward flat.
|
|
|
|
How the gate is built (and why NOT permutation entropy)
|
|
-------------------------------------------------------
|
|
Permutation entropy (Bandt-Pompe) of DAILY returns is near-saturated (~0.98 of max)
|
|
on these curves; when I measured it, its "low-entropy" regime actually had a NEGATIVE
|
|
edge for trend-following (-0.07/-0.03 hit-rate on A/B). The discriminating, well-ranged
|
|
"is the path random?" statistic here is the KAUFMAN EFFICIENCY RATIO over a window W:
|
|
|
|
ER[i] = |logC[i] - logC[i-W]| / sum_{i-W<k<=i} |Δ logC[k]| in [0,1]
|
|
|
|
ER is exactly an INVERSE path-entropy: ER->1 means every step pushed the same way (a
|
|
clean, low-entropy directional move -> the trend is predictable); ER->0 means the
|
|
steps cancelled out (a high-entropy random walk / chop -> the trend is noise). It is
|
|
the canonical randomness gate for trend systems (KAMA is built on it). I blend a short
|
|
and a medium window so the gate reacts to fast chop yet respects the macro structure.
|
|
|
|
Measured on train (per-bar): trend-following PnL is markedly higher in the high-ER
|
|
(low-entropy) half than the low-ER half on BOTH curves -> the gate does what the angle
|
|
promises: concentrate trend exposure in the predictable, structured legs and stand
|
|
down in the random chop (which are also the chaotic crash legs that drive drawdown).
|
|
|
|
Honest finding: ungated multi-horizon TSMOM has a slightly HIGHER Sharpe on these two
|
|
relentlessly up-trending curves (gating away "random" stretches removes some good
|
|
trend too). The entropy gate's real, robust contribution is DRAWDOWN: it cuts the
|
|
worst train DD from ~0.207 (ungated) to ~0.162 while keeping the Sharpe within ~6%
|
|
(1.37 -> 1.29). So this is a risk-reducing overlay, not a Sharpe-maximiser — reported
|
|
honestly. To get that DD cut without throwing away return I gate ONLY the bottom of
|
|
the ER distribution (genuinely random regimes) and keep half size there, rather than
|
|
linearly fading the whole range (which over-suppressed and lost ~0.3 of Sharpe).
|
|
|
|
Pipeline
|
|
--------
|
|
1. Direction: causal multi-horizon TSMOM sign blend (the trend we *might* take).
|
|
2. Entropy gate g in [FLOOR,1]: soft ramp on the LOW end of the ER distribution only.
|
|
ER below an expanding Q_LO quantile -> FLOOR; ER above an expanding Q_MID quantile
|
|
-> 1.0; linear in between. Quantiles are EXPANDING (history <= i) so "random vs
|
|
structured" is judged vs this series' own past, never the future.
|
|
3. Size = direction * gate, then a causal vol-target so A & B are risk-comparable.
|
|
|
|
CAUSAL: ER at i uses only logC in (i-W, i]; gate quantiles are EXPANDING (history
|
|
<= i); vol_target uses a trailing window. No look-ahead, no centered windows, no
|
|
global fit. Verified by causality_ok (max_diff 0.0).
|
|
|
|
Tuning (train only, combined A&B). Coarse->fine sweep over ER windows, the gate
|
|
quantiles, the floor, and SHORT_W settled on a WIDE interior plateau:
|
|
ER_WINS=(30,90), Q_LO=0.10, Q_MID=0.50, FLOOR=0.50, SHORT_W=0.25
|
|
-> train combined: pnl_mean ~2.63, maxdd_worst ~0.162, sharpe_min ~1.29.
|
|
All 1-step neighbours (window, qlo/qmid, floor in [0.45..0.55], short_w in [0..0.4])
|
|
sit in the same plateau (sh_min 1.26..1.32, dd 0.16..0.19) -> robust, not a spike.
|
|
"""
|
|
import numpy as np
|
|
import blindlib as bl
|
|
|
|
# --- trend direction (multi-horizon TSMOM sign blend) ---
|
|
HORIZONS = (45, 130, 240) # ~1.5/4.5/8 months of daily bars
|
|
SHORT_W = 0.25 # de-weight short side (curves trend up); 0 -> long-flat
|
|
|
|
# --- entropy / randomness gate (efficiency ratio = inverse path entropy) ---
|
|
ER_WINS = (30, 90) # blended short+medium ER windows
|
|
Q_LO = 0.10 # expanding-quantile of ER below which gate = FLOOR
|
|
Q_MID = 0.50 # expanding-quantile of ER above which gate = 1.0
|
|
FLOOR = 0.50 # exposure kept in the most-random (high-entropy) regime
|
|
WARMUP = 120 # bars before the gate is trusted (else FLOOR)
|
|
HIST_MIN = 60 # min ER history before quantiles are meaningful
|
|
|
|
# --- sizing ---
|
|
TARGET_VOL = 0.30
|
|
VOL_WIN_DAYS = 45
|
|
LEV_CAP = 1.5
|
|
|
|
|
|
def _tsmom_sign(c, h):
|
|
"""Sign of the past-h-bar return, causal. 0 before warmup (i < h)."""
|
|
out = np.zeros(len(c))
|
|
if h < len(c):
|
|
out[h:] = np.sign(c[h:] / c[:-h] - 1.0)
|
|
return out
|
|
|
|
|
|
def _efficiency_ratio(logc, win):
|
|
"""Causal Kaufman efficiency ratio over `win` bars: |net move| / sum|steps|.
|
|
er[i] uses logc in (i-win, i] only. ER in [0,1]: 1 = clean directional (low
|
|
entropy), 0 = random chop (high entropy)."""
|
|
n = len(logc)
|
|
er = np.zeros(n)
|
|
abs_step = np.zeros(n)
|
|
abs_step[1:] = np.abs(np.diff(logc))
|
|
csum = np.cumsum(abs_step)
|
|
for i in range(win, n):
|
|
change = abs(logc[i] - logc[i - win])
|
|
vol = csum[i] - csum[i - win]
|
|
er[i] = change / vol if vol > 1e-12 else 0.0
|
|
return er
|
|
|
|
|
|
def _expanding_gate(er):
|
|
"""Map ER -> [FLOOR, 1] with a soft ramp on the LOW end of the ER distribution.
|
|
ER below expanding-quantile Q_LO -> FLOOR (random regime, stand down); ER above
|
|
expanding-quantile Q_MID -> 1.0 (structured regime, full trend); linear between.
|
|
Fully causal: only ER history (values <= i) feeds the quantiles."""
|
|
n = len(er)
|
|
gate = np.full(n, FLOOR)
|
|
hist = []
|
|
for i in range(n):
|
|
v = er[i]
|
|
if i >= WARMUP and len(hist) >= HIST_MIN and np.isfinite(v):
|
|
arr = np.asarray(hist)
|
|
lo = np.quantile(arr, Q_LO)
|
|
mid = np.quantile(arr, Q_MID)
|
|
if v >= mid:
|
|
gate[i] = 1.0
|
|
elif mid > lo:
|
|
g = FLOOR + (1.0 - FLOOR) * (v - lo) / (mid - lo)
|
|
gate[i] = float(np.clip(g, FLOOR, 1.0))
|
|
else:
|
|
gate[i] = 1.0
|
|
if np.isfinite(v) and v > 0:
|
|
hist.append(v)
|
|
return gate
|
|
|
|
|
|
def signal(df):
|
|
c = df["close"].values.astype(float)
|
|
logc = np.log(c)
|
|
|
|
# 1) trend direction: multi-horizon TSMOM sign blend, asymmetric long-short
|
|
sig = np.zeros(len(c))
|
|
for h in HORIZONS:
|
|
sig += _tsmom_sign(c, h)
|
|
sig /= len(HORIZONS)
|
|
raw = np.where(sig >= 0.0, sig, sig * SHORT_W)
|
|
|
|
# 2) entropy/randomness gate from blended efficiency ratios (inverse path entropy)
|
|
gate = np.zeros(len(c))
|
|
for w in ER_WINS:
|
|
gate += _expanding_gate(_efficiency_ratio(logc, w))
|
|
gate /= len(ER_WINS)
|
|
|
|
# 3) gated direction, causal vol-target so A & B are risk-comparable
|
|
gated = raw * gate
|
|
pos = bl.vol_target(gated, 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)
|