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,95 @@
|
||||
"""Agent 08 — Sign-vote momentum ensemble (family=trend, slug=signvote).
|
||||
|
||||
The angle (assigned): a SIGN-VOTE ENSEMBLE of momentum across MANY lookbacks. For a
|
||||
dense ladder of horizons H in {10, 20, ..., 250} bars, each horizon casts a binary
|
||||
vote: +1 if the asset is up vs H bars ago (close[i] > close[i-H]), -1 if down. The
|
||||
raw direction is the MEAN of all the votes, a smooth number in [-1, +1]:
|
||||
+1.0 = every horizon agrees the trend is up (full long)
|
||||
0.0 = the ladder is split (no agreement) (flat)
|
||||
-1.0 = every horizon agrees the trend is down (full short)
|
||||
|
||||
Why a dense vote-ladder beats a single (or 3-horizon) momentum:
|
||||
* Robustness. No single lookback is special; the verdict is a consensus, so a chop
|
||||
that whipsaws one window is outvoted by the others. The committee de-risks
|
||||
GRADUALLY as horizons flip one by one — it doesn't lurch from full-long to
|
||||
full-short on one window crossing a threshold.
|
||||
* Anticipation. Near a top the FAST horizons flip down first while the slow ones
|
||||
are still up, so the mean vote slides from +1 toward 0 BEFORE the slow trend
|
||||
rolls over — exposure is cut into the turn, not after it. That is the whole point
|
||||
of the assignment: "anticipate the next move".
|
||||
|
||||
Long-short asymmetry: both curves trend up over the visible window, so a full-size
|
||||
symmetric short bleeds (it shorts every dip). A de-weighted short side (SHORT_W < 1)
|
||||
keeps the protection of going short the genuine, broad-consensus declines without the
|
||||
drag of fighting every pullback. SHORT_W=0.35 sits in the interior of a flat plateau.
|
||||
|
||||
Sizing: the consensus direction is fed to a causal vol-target so the two curves are
|
||||
risk-comparable and exposure shrinks into vol spikes (every crash is a vol spike) —
|
||||
this is what turns the ~77-79% buy&hold drawdown into a far smaller one at comparable
|
||||
PnL.
|
||||
|
||||
CAUSAL: every vote uses close[i]/close[i-H] (rows <= i only); the vol-target uses a
|
||||
trailing realized-vol window. No .shift(-k), no centered windows, no global fit.
|
||||
Verified by causality_ok (max_diff 0.0).
|
||||
|
||||
Tuning (split='train' only, combined A&B). A coarse->fine sweep over the ladder span,
|
||||
the step, SHORT_W, and the vol-target block found a WIDE plateau:
|
||||
* Ladder = 10..250 step 10 (25 horizons). Denser steps or a different top move
|
||||
sharpe_min by <0.05 -> the result is the consensus, not one cell.
|
||||
* SHORT_W plateau 0.10..0.30; TARGET_VOL trades PnL<->DD monotonically (0.22->DD .16,
|
||||
0.28->DD .21) at ~constant Sharpe; VOL_WIN=60 is the interior best (50/75 ~-0.05 Sh);
|
||||
LEV_CAP doesn't bind (vol-target rarely reaches the cap at these target vols).
|
||||
Chosen cell (interior on every axis -> robust, not a lucky spike):
|
||||
SHORT_W=0.15, TARGET_VOL=0.25, VOL_WIN=60, LEV_CAP=1.5
|
||||
-> train combined: pnl_mean ~1.68, maxdd_worst ~0.187, sharpe_min ~1.17.
|
||||
TARGET_VOL=0.25 is the balanced pick: vs the 0.30 cell it keeps the Sharpe (~1.18) and
|
||||
most of the PnL while cutting the worst drawdown 0.24->0.19 — the assignment's goal
|
||||
("comparable PnL at a MUCH smaller drawdown"). A single fast lookback is regime-fragile
|
||||
here; the dense sign-vote consensus both lifts the risk-adjusted return and roughly
|
||||
thirds the ~77-79% buy&hold drawdown.
|
||||
"""
|
||||
import numpy as np
|
||||
import blindlib as bl
|
||||
|
||||
# Dense ladder of momentum lookbacks (daily bars): 10, 20, ..., 250 -> 25 horizons.
|
||||
LOOKBACKS = tuple(range(10, 251, 10))
|
||||
SHORT_W = 0.15 # de-weight the short side (curves trend up); 0 -> long-flat
|
||||
TARGET_VOL = 0.25
|
||||
VOL_WIN_DAYS = 60
|
||||
LEV_CAP = 1.5
|
||||
|
||||
|
||||
def _vote(c: np.ndarray, h: int) -> np.ndarray:
|
||||
"""Binary momentum vote of horizon h, causal. +1 if up vs h bars ago, -1 if down.
|
||||
Undefined (0) for i < h (not enough history to vote)."""
|
||||
out = np.zeros(len(c))
|
||||
if h < len(c):
|
||||
out[h:] = np.sign(c[h:] / c[:-h] - 1.0)
|
||||
return out
|
||||
|
||||
|
||||
def signal(df):
|
||||
c = df["close"].values.astype(float)
|
||||
n = len(c)
|
||||
|
||||
# MEAN of the sign-votes across the whole ladder -> consensus direction in [-1,1].
|
||||
# Each horizon that has enough history contributes its +/-1 vote; we average only
|
||||
# over the horizons that are actually defined at bar i, so early bars (where the
|
||||
# long horizons can't vote yet) still produce a sensible consensus of the short
|
||||
# horizons rather than being diluted toward 0 by undefined long votes.
|
||||
vote_sum = np.zeros(n)
|
||||
vote_cnt = np.zeros(n)
|
||||
for h in LOOKBACKS:
|
||||
if h >= n:
|
||||
continue
|
||||
vote_sum[h:] += np.sign(c[h:] / c[:-h] - 1.0)
|
||||
vote_cnt[h:] += 1.0
|
||||
sig = np.where(vote_cnt > 0, vote_sum / np.maximum(vote_cnt, 1.0), 0.0)
|
||||
|
||||
# asymmetric long-short: keep the long full size, de-weight the short side
|
||||
raw = np.where(sig >= 0.0, sig, sig * SHORT_W)
|
||||
|
||||
# causal vol-targeting: shrinks size into vol spikes (every crash is a vol spike)
|
||||
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