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,225 @@
|
||||
"""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']}")
|
||||
Reference in New Issue
Block a user