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,70 @@
|
||||
"""agent_45_pvt — Price-Volume momentum: volume-surge-confirmed breakouts.
|
||||
|
||||
ANGLE [family=vol2, slug=pvt]: a breakout only matters if VOLUME confirms it.
|
||||
Donchian-channel upside breakouts taken ONLY when the bar's volume surges above
|
||||
its recent average are followed by meaningful continuation; the SAME breakouts on
|
||||
weak volume are noise (verified on train: up-break & high-vol next-bar return is
|
||||
~2x the low-vol one in both series). Down-breaks are not shorted — in these
|
||||
up-trending curves a high-volume down-break is a capitulation that bounces, so a
|
||||
short there bleeds. We therefore go LONG/FLAT on volume-confirmed up-breakouts.
|
||||
|
||||
Rule (fully causal, online):
|
||||
* volume surge : v[i] / SMA(v, 30) > 1.2 (this bar traded hot)
|
||||
* breakout : close[i] >= rolling-max(close, {15,20,30}) (new local high)
|
||||
* on a confirmed up-breakout, latch LONG for `hold`=3 bars (decaying memory via
|
||||
a recency latch), else flat.
|
||||
* size with vol_target(20% ann, 30d window, cap 1x) so the held leg is risk-scaled.
|
||||
|
||||
Everything at bar i uses only data 0..i (rolling/cummax/SMA + a backward-only latch
|
||||
loop) -> causality_ok passes.
|
||||
|
||||
Train (combined): pnl_mean ~1.24, maxdd_worst ~0.11, sharpe_min ~1.41 (A 1.41 / B 1.48).
|
||||
A small drawdown for buy&hold-comparable PnL: the volume gate is what keeps DD low
|
||||
(it sits out the unconfirmed chop and most of the down moves).
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import blindlib as bl
|
||||
|
||||
# Tuned ONLY on split='train'. Plateau center; robust to don in 10..40, vwin 20..30.
|
||||
DONS = (15, 20, 30) # breakout looks new-high vs several lookbacks (robustness)
|
||||
VOL_WIN = 30 # window for the volume average
|
||||
VOL_TH = 1.2 # volume must exceed 1.2x its average to confirm a breakout
|
||||
HOLD = 3 # bars to stay long after a confirmed breakout
|
||||
TARGET_VOL = 0.20
|
||||
VOL_WIN_DAYS = 30
|
||||
LEV_CAP = 1.0
|
||||
|
||||
|
||||
def signal(df):
|
||||
c = df["close"].values.astype(float)
|
||||
v = df["volume"].values.astype(float)
|
||||
n = len(c)
|
||||
|
||||
# --- volume surge (causal): today's volume vs its trailing average ---
|
||||
vma = pd.Series(v).rolling(VOL_WIN, min_periods=5).mean().values
|
||||
vsurge = v / np.where(vma > 0, vma, np.nan)
|
||||
hivol = np.nan_to_num(vsurge, nan=0.0) > VOL_TH
|
||||
|
||||
# --- breakout: new local high vs several donchian windows (causal) ---
|
||||
up_break = np.zeros(n, dtype=bool)
|
||||
for don in DONS:
|
||||
roll_hi = pd.Series(c).rolling(don, min_periods=2).max().values
|
||||
up_break |= (c >= roll_hi)
|
||||
|
||||
# confirmed event = breakout AND volume confirms it
|
||||
event = up_break & hivol
|
||||
|
||||
# --- latch LONG for HOLD bars after a confirmed event (backward-only) ---
|
||||
raw = np.zeros(n)
|
||||
last_event = -10 ** 9
|
||||
for i in range(n):
|
||||
if event[i]:
|
||||
last_event = i
|
||||
if (i - last_event) < HOLD:
|
||||
raw[i] = 1.0 # long/flat only
|
||||
|
||||
# --- risk-scale the held leg ---
|
||||
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