Files
Adriano Dal Pastro 1afb1014c9 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>
2026-06-21 07:05:04 +00:00

69 lines
3.6 KiB
Python

"""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)