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>
165 lines
8.6 KiB
Python
165 lines
8.6 KiB
Python
"""Agent 50 — Ensemble meta-blend (family=mix, slug=ensemble_meta).
|
|
|
|
The angle (assigned): META-BLEND. Combine several CAUSAL sub-signals — trend, breakout,
|
|
ma-cross, and a reversion-gate — by a WEIGHTED VOTE into ONE position in [-1,+1]. No
|
|
single sub-signal decides; the committee does, and the vote is then risk-sized by a
|
|
causal vol-target. The diversity of the voters is the point: each reads the trend with
|
|
a different memory, so a chop that whipsaws one is outvoted by the others, and exposure
|
|
slides toward flat as voters flip one by one near a turn (anticipation, not reaction).
|
|
|
|
The voters (each a direction in [-1,+1], all causal — value at i uses ONLY rows<=i):
|
|
|
|
1. TREND (weight 0.35) — dense multi-horizon TSMOM sign-vote. For a ladder of
|
|
lookbacks H in {30,60,...,240}, vote +1 if close[i] > close[i-H] else -1, averaged
|
|
over the horizons defined at i. Consensus direction: slides from +1 toward 0/-1 as
|
|
the fast horizons flip first into a roll-over.
|
|
|
|
2. BREAKOUT (weight 0.50) — Donchian channel position. donchian(df, N) returns the
|
|
prior-N-bar high/low STRICTLY before bar i (shifted), so a close[i] that pierces
|
|
them is a real tradeable breakout. We map close's position within [lo, hi] to
|
|
[-1,+1] and clip: a close above the prior high reads +1 (fresh breakout up), below
|
|
the prior low reads -1. On the train view this is the single best risk-adjusted
|
|
voter (it rides confirmed momentum and is naturally light in a range), hence the
|
|
largest weight.
|
|
|
|
3. MACROSS (weight 0.15) — medium EMA-cross trend confirmation: a SECOND, independent
|
|
trend read with a different memory than the TSMOM ladder. tanh-squashed
|
|
(ema_fast - ema_slow)/ema_slow. Small weight: it is correlated with TREND, so it
|
|
mostly breaks ties / firms the consensus rather than adding new information.
|
|
|
|
4. REVGATE (reversion-gate) — a mean-reversion SAFEGUARD, applied as a MULTIPLICATIVE
|
|
gate, not a directional fade. These daily curves trend up structurally, so fading
|
|
a z-score directionally just bleeds (verified on train: it cuts both PnL and
|
|
Sharpe). Instead, when price is *very* stretched in the SAME direction as the
|
|
committee's position (|z|>Z_THR), the gate lightly TRIMS exposure (reversal risk is
|
|
elevated) — a small, defensible drawdown-tail safeguard. On train it is ~Sharpe-
|
|
neutral and shaves the worst drawdown a touch; it is the honest, non-bleeding way
|
|
to include a reversion read on a trending series.
|
|
|
|
Long-FLAT (short side off): both curves trend up over the visible window, and on train
|
|
the long-flat book strictly dominates any symmetric/de-weighted short (a short bleeds
|
|
shorting every dip). The committee de-risks toward FLAT into declines (voters flip down
|
|
+ vol-target shrinks size into the vol spike) rather than flipping short — which is what
|
|
turns the ~77-79% buy&hold drawdown into ~12% at comparable/strong PnL.
|
|
|
|
Sizing: the blended direction is fed to a causal vol-target (trailing realized-vol
|
|
window) so the two curves are risk-comparable and exposure shrinks into vol spikes
|
|
(every crash is a vol spike). leverage_cap doesn't bind at this target vol.
|
|
|
|
CAUSAL: every voter uses only rows<=i (TSMOM/cross use close[i]/close[i-H]; donchian is
|
|
the altlib version lagged 1 bar; zscore is a trailing window; vol_target uses trailing
|
|
realized vol). No .shift(-k), no centered windows, no global fit. Verified by
|
|
causality_ok (max_diff 0.0).
|
|
|
|
Tuning (split='train' only, combined A&B). Coarse->fine sweep over voter weights,
|
|
windows, and the vol-target block found a WIDE plateau (the result is the consensus,
|
|
not one lucky cell):
|
|
* Voter weights: a broad plateau (wt 0.30-0.45, wb 0.45-0.55, wc 0.10-0.20) all give
|
|
sharpe_min ~1.36-1.38 at DD ~0.11-0.12. Chosen (0.35, 0.50, 0.15) is interior.
|
|
* BREAKOUT window: 50-60 is the plateau (Sharpe 1.31-1.38); DON_N=55 is interior.
|
|
* TREND ladder: dense {30..240 step 30} (8 horizons) Sharpe 1.38 / DD 0.12 — beats a
|
|
sparse 3-horizon set on robustness (consensus of 8, not 3). EMA-cross is a flat
|
|
plateau 25/100 +/- (Sharpe ~1.30-1.32 across every neighbor) -> non-fragile.
|
|
* VOL block: TARGET_VOL trades PnL<->DD monotonically at constant Sharpe (0.25 -> PnL
|
|
~1.75, DD ~0.12). VOL_WIN=35 is the interior pick (vw=25 spikes Sharpe to 1.41 but
|
|
sits on the grid EDGE -> declined as likely vol-regime overfit; 30/40 ~-0.02 Sh).
|
|
* REVGATE damp: ~Sharpe-neutral (1.369 -> 1.364 at damp_w 0.2) and shaves DD a hair
|
|
(0.118 -> 0.117). Kept LIGHT (damp_w 0.2) as an honest reversion safeguard.
|
|
-> train combined: pnl_mean ~1.74, maxdd_worst ~0.117, sharpe_min ~1.36, causality ok.
|
|
|
|
HONEST CAVEAT: on these strongly-trending curves the breakout+trend voters carry the
|
|
result; the reversion-gate is at best neutral (a directional fade bleeds outright). The
|
|
ensemble's value over a single voter is ROBUSTNESS (a flat Sharpe plateau across every
|
|
axis) and a low, stable drawdown — not a higher peak Sharpe than the best single voter.
|
|
"""
|
|
import numpy as np
|
|
import blindlib as bl
|
|
|
|
# ---- voter params ----
|
|
TREND_LB = tuple(range(30, 241, 30)) # 30,60,...,240 dense TSMOM ladder (8 horizons)
|
|
DON_N = 55 # donchian breakout window (interior of 50-60)
|
|
EMA_FAST = 25
|
|
EMA_SLOW = 100
|
|
REV_WIN = 10 # short z-score window for the reversion gate
|
|
Z_THR = 2.0 # reversion gate engages only when |z| > Z_THR
|
|
|
|
# ---- blend weights (weighted vote) ----
|
|
W_TREND = 0.35
|
|
W_BREAK = 0.50
|
|
W_CROSS = 0.15
|
|
|
|
# ---- reversion-gate (multiplicative damp, not a directional fade) ----
|
|
DAMP_W = 0.20 # light: ~Sharpe-neutral, shaves DD tail
|
|
|
|
# ---- sizing ----
|
|
TARGET_VOL = 0.25
|
|
VOL_WIN_DAYS = 35
|
|
LEV_CAP = 1.5 # does not bind at this target vol
|
|
|
|
|
|
def _tsmom_vote(c, lookbacks):
|
|
"""Dense multi-horizon TSMOM sign-vote, causal -> direction in [-1,1]. Averages
|
|
only over horizons that are defined at bar i (enough history), so early bars use
|
|
the short-horizon consensus instead of being diluted toward 0 by undefined votes."""
|
|
n = len(c)
|
|
vs = np.zeros(n)
|
|
vc = np.zeros(n)
|
|
for h in lookbacks:
|
|
if h >= n:
|
|
continue
|
|
vs[h:] += np.sign(c[h:] / c[:-h] - 1.0)
|
|
vc[h:] += 1.0
|
|
return np.where(vc > 0, vs / np.maximum(vc, 1.0), 0.0)
|
|
|
|
|
|
def _breakout_vote(df, n):
|
|
"""Donchian channel position in [-1,1], causal. donchian() returns (hi, lo): the
|
|
prior n-bar high/low STRICTLY before bar i (shifted), so close[i] breaking them is
|
|
a real tradeable breakout. Map close within [lo, hi] to [-1,+1] and clip (a close
|
|
above the prior high reads +1 = fresh breakout up)."""
|
|
hi, lo = bl.donchian(df, n)
|
|
c = df["close"].values.astype(float)
|
|
rng = (hi - lo)
|
|
pos = np.where((rng > 0) & np.isfinite(rng),
|
|
2.0 * (c - lo) / np.where(rng > 0, rng, 1.0) - 1.0, 0.0)
|
|
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
|
|
|
|
|
|
def _cross_vote(c, fast, slow):
|
|
"""EMA-cross trend read squashed to [-1,1], causal. A second, independent trend
|
|
read with a different memory than the TSMOM ladder."""
|
|
ef = bl.ema(c, fast)
|
|
es = bl.ema(c, slow)
|
|
d = np.where(es > 0, (ef - es) / es, 0.0)
|
|
return np.tanh(8.0 * np.nan_to_num(d, nan=0.0))
|
|
|
|
|
|
def signal(df):
|
|
c = df["close"].values.astype(float)
|
|
|
|
trend = _tsmom_vote(c, TREND_LB)
|
|
brk = _breakout_vote(df, DON_N)
|
|
cross = _cross_vote(c, EMA_FAST, EMA_SLOW)
|
|
|
|
# --- weighted vote of the directional voters -> raw direction in ~[-1,1] ---
|
|
wsum = W_TREND + W_BREAK + W_CROSS
|
|
raw = (W_TREND * trend + W_BREAK * brk + W_CROSS * cross) / wsum
|
|
|
|
# --- long-flat: the short side off (curves trend up; a short bleeds the dips) ---
|
|
raw = np.where(raw >= 0.0, raw, 0.0)
|
|
|
|
# --- REVERSION-GATE (multiplicative damp, causal): when price is very stretched in
|
|
# the SAME direction as our position (|z|>Z_THR), trim exposure (reversal risk).
|
|
# NOT a directional fade (that bleeds on a trending series) — a light DD safeguard.
|
|
if DAMP_W > 0.0:
|
|
z = np.nan_to_num(bl.zscore(c, REV_WIN), nan=0.0)
|
|
stretch = (np.minimum(np.abs(z), 3.0) - Z_THR) / (3.0 - Z_THR)
|
|
damp = np.where(np.abs(z) > Z_THR, np.clip(1.0 - DAMP_W * stretch, 0.0, 1.0), 1.0)
|
|
# only trim when the stretch is in the SAME sign as the position (reversal risk)
|
|
raw = raw * np.where(np.sign(raw) == np.sign(z), damp, 1.0)
|
|
|
|
# --- causal vol-target: risk-comparable curves, shrink into vol spikes ---
|
|
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)
|