Files
PythagorasGoal/scripts/research/blind/_skeptic_perturb.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

226 lines
9.1 KiB
Python

"""Adversarial parameter-perturbation harness for the 3 blind survivors.
Re-implements each signal parameterized; perturbs each key param +/-25% (and larger
jumps), re-evaluates OOS (test slice, A & B) and train. Reports min/median/max OOS
Sharpe across the grid and the train->test Sharpe decay. Also a fee bump to 0.20% RT.
"""
import sys
import numpy as np
import pandas as pd
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/blind")
import blindlib as bl
FEE_BASE = 0.0005 # 0.10% RT
FEE_BUMP = 0.001 # 0.20% RT
def _masks(series):
df = bl.load(series, "full")
cut = bl.split_cut(series)
test = np.zeros(len(df), bool); test[cut:] = True
train = np.zeros(len(df), bool); train[:cut] = True
return df, train, test
# ---------------- agent_04 MACD ----------------
def macd_signal(df, FAST=26, SLOW=52, SIGNAL=9, SLOPE_W=0.20, SHORT_W=0.5,
TARGET_VOL=0.20, VOL_WIN=30, LEV_CAP=1.0):
c = df["close"].values.astype(float)
macd = bl.ema(c, FAST) - bl.ema(c, SLOW)
signal_line = bl.ema(macd, SIGNAL)
hist = macd - signal_line
base = np.where(np.sign(hist) == np.sign(macd), np.sign(macd), 0.0)
slope = np.sign(np.diff(hist, prepend=hist[0]))
raw = (1.0 - SLOPE_W) * base + SLOPE_W * slope
raw = np.clip(raw, -1.0, 1.0)
raw = np.where(raw < 0, raw * SHORT_W, raw)
raw = np.nan_to_num(raw, nan=0.0)
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL, vol_win_days=VOL_WIN,
leverage_cap=LEV_CAP)
return np.clip(pos, -1.0, 1.0)
# ---------------- agent_06 accel ----------------
def _lagged_diff(x, lag):
out = np.zeros(len(x))
if lag < len(x):
out[lag:] = x[lag:] - x[:-lag]
return out
def accel_signal(df, FAST=28, LAG=30, Z_WIN=200, KV=1.5, KA=1.5, W_VEL=0.4,
W_ACC=0.6, SHORT_W=0.0, TARGET_VOL=0.27, VOL_WIN=25, LEV_CAP=1.5):
c = df["close"].values.astype(float)
lr = np.zeros(len(c)); lr[1:] = np.log(c[1:] / c[:-1])
vel = bl.ema(lr, FAST)
acc = _lagged_diff(vel, LAG)
zv = np.nan_to_num(bl.zscore(vel, Z_WIN), nan=0.0)
za = np.nan_to_num(bl.zscore(acc, Z_WIN), nan=0.0)
raw = W_VEL * np.tanh(KV * zv) + W_ACC * np.tanh(KA * za)
raw = np.clip(raw, -1.0, 1.0)
raw = np.where(raw >= 0.0, raw, raw * SHORT_W)
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL, vol_win_days=VOL_WIN,
leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
# ---------------- agent_23 vol_of_vol ----------------
def _expanding_pctl_rank(x, min_hist):
n = len(x); rank = np.full(n, np.nan); seen = []
for i in range(n):
v = x[i]
if np.isfinite(v):
seen.append(v)
if len(seen) >= min_hist:
rank[i] = float(np.mean(np.asarray(seen) <= v))
return rank
def _tsmom_sign(c, h):
out = np.zeros(len(c))
if h < len(c):
out[h:] = np.sign(c[h:] / c[:-h] - 1.0)
return out
def _vol_of_vol(rv, win):
rv_s = pd.Series(rv)
logrv = np.log(rv_s.where(rv_s > 0))
dlog = logrv.diff()
return dlog.rolling(win, min_periods=max(5, win // 2)).std().values
def vov_signal(df, RV_WIN=30, VOV_WIN=40, PCTL=0.80, HORIZONS=(25, 60, 120),
TARGET_VOL=0.22, VOL_WIN=45, LEV_CAP=1.5, MIN_HIST=60):
c = df["close"].values.astype(float)
bpy = bl.bars_per_day(df) * 365.25
rv = bl.realized_vol(bl.simple_returns(c), RV_WIN, bpy)
vov = _vol_of_vol(rv, VOV_WIN)
rank = _expanding_pctl_rank(vov, MIN_HIST)
stable = np.isfinite(rank) & (rank <= PCTL)
sig = np.zeros(len(c))
for h in HORIZONS:
sig += _tsmom_sign(c, h)
sig /= len(HORIZONS)
raw = np.where(stable, sig, 0.0)
pos = bl.vol_target(raw, df, target_vol=TARGET_VOL, vol_win_days=VOL_WIN,
leverage_cap=LEV_CAP)
return np.clip(np.nan_to_num(pos, nan=0.0), -1.0, 1.0)
def score(sig_fn, kwargs, fee=FEE_BASE):
"""Return dict of train & test sharpe/pnl, averaged over A&B (min/mean)."""
out = {}
for s in ("A", "B"):
df, train, test = _masks(s)
tgt = sig_fn(df, **kwargs)
rtr = bl.eval_target(df, tgt, fee_side=fee, metric_mask=train)
rte = bl.eval_target(df, tgt, fee_side=fee, metric_mask=test)
out[s] = dict(tr_sh=rtr["sharpe"], tr_pnl=rtr["pnl"],
te_sh=rte["sharpe"], te_pnl=rte["pnl"], te_dd=rte["maxdd"])
# combined: min across A,B (the agents tuned on sharpe_min)
te_sh_min = min(out["A"]["te_sh"], out["B"]["te_sh"])
tr_sh_min = min(out["A"]["tr_sh"], out["B"]["tr_sh"])
te_sh_mean = 0.5 * (out["A"]["te_sh"] + out["B"]["te_sh"])
te_pnl_mean = 0.5 * (out["A"]["te_pnl"] + out["B"]["te_pnl"])
return dict(out=out, te_sh_min=te_sh_min, tr_sh_min=tr_sh_min,
te_sh_mean=te_sh_mean, te_pnl_mean=te_pnl_mean)
def perturb_grid(sig_fn, base, grid):
"""grid: {param: [values]}. Sweep one param at a time around base."""
base_sc = score(sig_fn, base)
rows = []
for p, vals in grid.items():
for v in vals:
kw = dict(base); kw[p] = v
sc = score(sig_fn, kw)
rows.append(dict(param=p, val=v, te_sh_min=sc["te_sh_min"],
te_sh_mean=round(sc["te_sh_mean"], 3),
te_pnl_mean=round(sc["te_pnl_mean"], 3),
tr_sh_min=sc["tr_sh_min"]))
return base_sc, rows
if __name__ == "__main__":
import json
pd.set_option("display.width", 160)
pd.set_option("display.max_rows", 300)
print("="*70)
print("AGENT 04 — MACD")
print("="*70)
base04 = dict(FAST=26, SLOW=52, SIGNAL=9, SLOPE_W=0.20, SHORT_W=0.5,
TARGET_VOL=0.20, VOL_WIN=30, LEV_CAP=1.0)
b, rows = perturb_grid(macd_signal, base04, dict(
FAST=[20, 22, 26, 30, 32, 39], # +/-25% + bigger
SLOW=[39, 45, 52, 60, 65, 78],
SIGNAL=[5, 7, 9, 11, 13, 18],
SLOPE_W=[0.10, 0.15, 0.20, 0.25, 0.30, 0.40],
SHORT_W=[0.0, 0.25, 0.375, 0.5, 0.625, 0.75, 1.0],
VOL_WIN=[15, 22, 30, 38, 45, 60],
TARGET_VOL=[0.15, 0.20, 0.25, 0.30],
))
print("BASE:", json.dumps({k: b[k] for k in ("tr_sh_min","te_sh_min","te_sh_mean","te_pnl_mean")}))
print(" per-series base:", b["out"])
print(pd.DataFrame(rows).to_string(index=False))
print("\n" + "="*70)
print("AGENT 06 — ACCEL")
print("="*70)
base06 = dict(FAST=28, LAG=30, Z_WIN=200, KV=1.5, KA=1.5, W_VEL=0.4,
W_ACC=0.6, SHORT_W=0.0, TARGET_VOL=0.27, VOL_WIN=25, LEV_CAP=1.5)
b, rows = perturb_grid(accel_signal, base06, dict(
FAST=[21, 24, 28, 32, 35, 42],
LAG=[20, 26, 30, 36, 40, 50],
Z_WIN=[140, 160, 200, 240, 260, 320],
KV=[1.0, 1.2, 1.5, 1.8, 2.0, 3.0],
KA=[1.0, 1.2, 1.5, 1.8, 2.0, 3.0],
W_ACC=[0.3, 0.45, 0.6, 0.75, 0.9, 1.0],
TARGET_VOL=[0.18, 0.22, 0.27, 0.32],
VOL_WIN=[18, 22, 25, 30, 35],
))
print("BASE:", json.dumps({k: b[k] for k in ("tr_sh_min","te_sh_min","te_sh_mean","te_pnl_mean")}))
print(" per-series base:", b["out"])
print(pd.DataFrame(rows).to_string(index=False))
print("\n" + "="*70)
print("AGENT 23 — VOL_OF_VOL")
print("="*70)
base23 = dict(RV_WIN=30, VOV_WIN=40, PCTL=0.80, HORIZONS=(25, 60, 120),
TARGET_VOL=0.22, VOL_WIN=45, LEV_CAP=1.5, MIN_HIST=60)
b, rows = perturb_grid(vov_signal, base23, dict(
RV_WIN=[22, 26, 30, 34, 38, 45],
VOV_WIN=[30, 35, 40, 45, 50, 60],
PCTL=[0.60, 0.70, 0.76, 0.80, 0.84, 0.90, 1.00],
TARGET_VOL=[0.18, 0.22, 0.26, 0.30],
VOL_WIN=[34, 40, 45, 55, 60],
MIN_HIST=[40, 60, 90],
))
print("BASE:", json.dumps({k: b[k] for k in ("tr_sh_min","te_sh_min","te_sh_mean","te_pnl_mean")}))
print(" per-series base:", b["out"])
# horizons sweep separately (tuple param)
hz_rows = []
for hz in [(20,50,100),(25,60,120),(30,70,140),(20,40,80),(40,90,180),(15,30,60)]:
kw = dict(base23); kw["HORIZONS"] = hz
sc = score(vov_signal, kw)
hz_rows.append(dict(param="HORIZONS", val=str(hz), te_sh_min=sc["te_sh_min"],
te_sh_mean=round(sc["te_sh_mean"],3),
te_pnl_mean=round(sc["te_pnl_mean"],3), tr_sh_min=sc["tr_sh_min"]))
print(pd.DataFrame(rows + hz_rows).to_string(index=False))
# ---- FEE BUMP to 0.20% RT, base params ----
print("\n" + "="*70)
print("FEE BUMP 0.10% -> 0.20% RT (base params)")
print("="*70)
for name, fn, base in [("MACD", macd_signal, base04),
("ACCEL", accel_signal, base06),
("VOV", vov_signal, base23)]:
lo = score(fn, base, fee=FEE_BASE)
hi = score(fn, base, fee=FEE_BUMP)
print(f"{name:6s} te_sh_min {lo['te_sh_min']:+.3f} -> {hi['te_sh_min']:+.3f} | "
f"te_sh_mean {lo['te_sh_mean']:+.3f} -> {hi['te_sh_mean']:+.3f} | "
f"te_pnl_mean {lo['te_pnl_mean']:+.3f} -> {hi['te_pnl_mean']:+.3f}")
print(f" per-series @0.20%: A te_sh {score(fn,base,fee=FEE_BUMP)['out']['A']['te_sh']} "
f"B te_sh {score(fn,base,fee=FEE_BUMP)['out']['B']['te_sh']}")