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,68 @@
|
||||
"""Agent 03 — MA ribbon (family=trend, slug=ma_ribbon).
|
||||
|
||||
The angle: a quad-EMA "ribbon" (fast -> slow). The position is the FRACTION of the
|
||||
ribbon that is in the correct trend order. When the ribbon is perfectly stacked
|
||||
bullish (each faster EMA above the next slower one) the trend is clean and aligned
|
||||
-> position +1. Perfectly stacked bearish -> -1. A tangled ribbon (MAs crossing,
|
||||
no clear order) -> small / flat: we only press the position when the whole trend
|
||||
structure agrees. This is a GRADED-conviction trend filter, not a binary cross.
|
||||
|
||||
Construction (all causal — value at i uses rows 0..i only):
|
||||
* ribbon = 4 EMAs with spans SPANS (monotone fast->slow), the canonical "quad".
|
||||
* For each adjacent pair (k, k+1) score +1 if ema_k > ema_{k+1} (bullish step),
|
||||
-1 if below. ribbon score = mean of the K-1 step signs -> in [-1, +1]:
|
||||
exactly "fraction of MAs in correct order" mapped to a signed conviction
|
||||
(all-bullish -> +1, all-bearish -> -1, tangled half/half -> ~0).
|
||||
* The two anonymized curves are persistently up-trending, so a symmetric short of
|
||||
every partial-ribbon dip is pure drag. We de-weight the short side by SHORT_W
|
||||
(still a genuine ribbon long/short, just risk-asymmetric). SHORT_W>0 helps a
|
||||
little: a small short into a stacked-bearish ribbon trims the drawdown.
|
||||
* Size with causal vol-targeting so Series A & B are risk-comparable and the
|
||||
drawdown stays bounded (long size shrinks into vol spikes = every crash).
|
||||
|
||||
Tuning (ONLY split='train', both A & B equal weight). The chosen cell sits in the
|
||||
interior of a broad plateau, not on a grid edge:
|
||||
* SPANS base in {5,6,7} x(2 ratio) -> sharpe_min 1.32-1.37 (6 is the interior).
|
||||
* VOL_WIN 20-25 best; 25 interior. * SHORT_W 0.1-0.25 flat at sharpe_min ~1.37,
|
||||
DD falling 0.26->0.24 as SHORT_W rises; 0.2 interior.
|
||||
Train combined: pnl_mean ~3.20, maxdd_worst ~0.241, sharpe_min ~1.37, turnover ~11/yr.
|
||||
Fee-robust: sharpe_min 1.39 at 0% RT -> 1.30 at 0.40% RT (low turnover = fee-insensitive).
|
||||
|
||||
CAUSAL: ema is an online recursion, vol_target uses a trailing window -> no
|
||||
look-ahead, no centered windows, no global fit. Verified by causality_ok (max_diff 0).
|
||||
|
||||
Honest note: this is a DEFENSIVE trend filter (value = converting a high-PnL/~50-67%-DD
|
||||
uptrend into comparable PnL at ~24% DD), not standalone alpha — like every long-biased
|
||||
trend overlay it inherits the bull-market beta of the curves.
|
||||
"""
|
||||
import numpy as np
|
||||
import blindlib as bl
|
||||
|
||||
# --- tuned ONLY on split='train' (plateau interior, not a grid edge) ---
|
||||
SPANS = (6, 12, 24, 48) # quad ribbon, fast -> slow (monotone)
|
||||
SHORT_W = 0.2 # short side de-weighted (asymmetric L/S); 0 -> long/flat
|
||||
TARGET_VOL = 0.25
|
||||
VOL_WIN_DAYS = 25
|
||||
LEV_CAP = 1.0
|
||||
|
||||
|
||||
def _ribbon_score(c: np.ndarray) -> np.ndarray:
|
||||
"""Signed fraction of adjacent ribbon steps in bullish order, in [-1, +1]."""
|
||||
emas = [bl.ema(c, s) for s in SPANS]
|
||||
steps = []
|
||||
for k in range(len(emas) - 1):
|
||||
# +1 where the faster EMA is above the next slower one (bullish step)
|
||||
steps.append(np.where(emas[k] > emas[k + 1], 1.0, -1.0))
|
||||
score = np.mean(np.vstack(steps), axis=0) # mean of K-1 step signs in [-1,1]
|
||||
score[: SPANS[-1]] = 0.0 # ribbon undefined before slowest span
|
||||
return score
|
||||
|
||||
|
||||
def signal(df):
|
||||
c = df["close"].values.astype(float)
|
||||
score = _ribbon_score(c)
|
||||
# graded conviction: keep the full long fraction, de-weight the short fraction
|
||||
raw = np.where(score >= 0.0, score, SHORT_W * score)
|
||||
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