Files
PythagorasGoal/scripts/research/blind/agents/agent_45_pvt.py
T
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

71 lines
3.0 KiB
Python

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