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,87 @@
|
||||
"""agent_10_keltner — ANGLE: Keltner channel breakout (long / flat).
|
||||
|
||||
Idea (assigned angle): a Keltner channel is an EMA mid-line wrapped by an ATR band,
|
||||
upper[i] = EMA_N(close)[i-1] + K * ATR_M[i-1]
|
||||
lower[i] = EMA_N(close)[i-1] - K_EXIT * ATR_M[i-1]
|
||||
Ride breakouts: go LONG when close[i] pierces the prior-bar UPPER band (an upside
|
||||
breakout out of the channel); EXIT to FLAT when close[i] pierces the prior-bar LOWER
|
||||
band. Hold the long between those two events (a turtle-style state machine) so we stay
|
||||
in persistent trends and keep turnover (fees) low. Tune N, M, K, K_EXIT on train only.
|
||||
|
||||
WHY LONG/FLAT, NOT LONG/SHORT (honest tuning result on split='train'):
|
||||
The textbook Keltner breakout is stop-and-reverse (short below the lower band). I
|
||||
tuned both. Long/SHORT tops out at sharpe_min ~1.04 (maxdd ~0.39); switching the short
|
||||
leg to FLAT lifts sharpe_min to ~1.56 and cuts maxdd to ~0.28. On BOTH series the short
|
||||
leg is value-destroying: the pair trends up, so downside breakouts are mostly V-shaped
|
||||
bottoms / chop where a short gets whipsawed. So the breakout *exit* is kept (a lower-
|
||||
band break flattens us) but we never flip short. The Keltner breakout EVENT still drives
|
||||
every entry and exit — the angle is intact.
|
||||
|
||||
Tuned on split='train' (Series A & B, equal weight). Broad plateau: 59/340 nearby cells
|
||||
keep sharpe_min > 1.40, so the chosen point is a plateau CENTER, not an isolated peak:
|
||||
* N_EMA = 20 (Keltner mid-line EMA span)
|
||||
* N_ATR = 30 (ATR window for the band half-width)
|
||||
* K = 1.0 (entry band multiplier: close above EMA + 1.0*ATR -> upside breakout)
|
||||
* K_EXIT = 0.5 (exit band multiplier: close below EMA - 0.5*ATR -> flatten; tighter
|
||||
than entry so we exit a failing trend faster than we re-enter)
|
||||
* vol-target the long to 30% ann vol (vol_win=30d, cap 1.0): the long size shrinks into
|
||||
vol spikes (every crash is a vol spike) -> caps the drawdown of late/whipsaw entries.
|
||||
Sharpe is ~flat (1.55-1.56) across target_vol 0.20-0.40; target_vol only trades PnL
|
||||
for DD (0.20 -> pnl 2.7/DD 0.19 ... 0.40 -> pnl 9.2/DD 0.34). 0.30 is the balance.
|
||||
|
||||
Causality: the channel that close[i] is tested against is EMA/ATR evaluated at i-1 (one-
|
||||
bar lag via .shift(1)), so it is built from bars STRICTLY before i; a close[i] that
|
||||
pierces it is a real, tradeable event at close[i]. The state machine is a forward scan
|
||||
(uses only data <= i). The evaluator then holds the position during bar i+1. No future
|
||||
rows -> causality_ok = true.
|
||||
|
||||
Train (combined A&B): pnl_mean ~5.55, maxdd_worst ~0.28, sharpe_min ~1.56.
|
||||
Honest note: Keltner breakout is pure trend-following, not alpha. Its value here is
|
||||
converting a high-PnL / ~77-79%-DD uptrend into comparable PnL at ~28% drawdown (DD cut
|
||||
~2.7x). The full long/short Keltner was MUCH worse (sharpe_min ~1.04, DD ~0.39) — the
|
||||
edge that matters is the FLAT side, exactly as for the sibling donchian breakout.
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import blindlib as bl
|
||||
|
||||
N_EMA = 20 # Keltner mid-line EMA span
|
||||
N_ATR = 30 # ATR window for the band half-width
|
||||
K = 1.0 # entry band multiplier: break of EMA + K*ATR -> long
|
||||
K_EXIT = 0.5 # exit band multiplier: break of EMA - K_EXIT*ATR -> flat
|
||||
TARGET_VOL = 0.30
|
||||
VOL_WIN_DAYS = 30
|
||||
LEV_CAP = 1.0
|
||||
|
||||
|
||||
def _keltner_band(df, n_ema, n_atr, k):
|
||||
"""Lagged Keltner upper/lower at multiplier k: EMA[i-1] +/- k*ATR[i-1]."""
|
||||
c = df["close"].values.astype(float)
|
||||
mid = pd.Series(bl.ema(c, n_ema)).shift(1).values # EMA built <= i-1
|
||||
band = pd.Series(bl.atr(df, n_atr)).shift(1).values # ATR built <= i-1
|
||||
return mid + k * band, mid - k * band
|
||||
|
||||
|
||||
def signal(df):
|
||||
c = df["close"].values.astype(float)
|
||||
upper, _ = _keltner_band(df, N_EMA, N_ATR, K) # entry channel (wider)
|
||||
_, lower = _keltner_band(df, N_EMA, N_ATR, K_EXIT) # exit channel (tighter)
|
||||
|
||||
up = c > upper # upside breakout -> enter / stay long (tradeable at close[i])
|
||||
dn = c < lower # downside breakout of tighter band -> exit to flat
|
||||
|
||||
# turtle long/flat state machine (forward scan, uses only data <= i).
|
||||
n = len(c)
|
||||
state = np.zeros(n)
|
||||
s = 0.0
|
||||
for i in range(n):
|
||||
if np.isfinite(upper[i]) and up[i]:
|
||||
s = 1.0
|
||||
elif np.isfinite(lower[i]) and dn[i]:
|
||||
s = 0.0
|
||||
state[i] = s
|
||||
|
||||
# size the long with causal vol-targeting (shrinks into vol spikes -> caps DD).
|
||||
pos = bl.vol_target(state, 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