1afb1014c9
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>
135 lines
5.1 KiB
Python
135 lines
5.1 KiB
Python
"""verify_top — adversarial second layer on the OOS leaderboard winners.
|
|
|
|
The auto causality-guard already kills look-ahead. This asks the harder questions the
|
|
2026-06-20 sweep taught us to ask before believing ANY directional BTC/ETH edge:
|
|
|
|
1. TREND-IN-DISGUISE? Correlate each candidate's OOS net returns to a canonical
|
|
multi-horizon TSMOM (TP01 archetype) on the SAME blind curves. corr>0.7 => it is
|
|
just trend-beta of an up-trending pair, not new alpha.
|
|
2. FEE-ROBUST? Re-score OOS at 0.20% round-trip (4x the per-side baseline). A real
|
|
edge survives; a turnover-churner dies.
|
|
3. STABILITY? Split the OOS tail into K contiguous blocks; drop each in turn and
|
|
recompute Sharpe. Report the worst (jackknife) — a result resting on one block is
|
|
regime-luck, not an edge.
|
|
|
|
uv run python scripts/research/blind/verify_top.py [--top 10]
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import importlib.util
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
sys.path.insert(0, str(HERE))
|
|
import blindlib as bl # noqa: E402
|
|
|
|
AGENTS = HERE / "agents"
|
|
|
|
|
|
def _sig(path: Path):
|
|
spec = importlib.util.spec_from_file_location(path.stem, path)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod.signal
|
|
|
|
|
|
def _trend_baseline(df):
|
|
"""Canonical TP01-style multi-horizon TSMOM, long-flat, vol-targeted (the thing a
|
|
new directional edge must beat / be orthogonal to)."""
|
|
c = df["close"].values.astype(float)
|
|
r = bl.simple_returns(c)
|
|
sig = np.zeros(len(c))
|
|
for H in (30, 90, 180):
|
|
m = np.zeros(len(c))
|
|
m[H:] = c[H:] / c[:-H] - 1.0
|
|
sig += np.sign(m)
|
|
direction = np.clip(sig / 3.0, 0, 1) # long-flat
|
|
return bl.vol_target(direction, df, 0.20, 30, 1.0)
|
|
|
|
|
|
def _net(signal_fn, series):
|
|
"""OOS net-return vector (test slice) for a signal on a series."""
|
|
df = bl.load(series, "full")
|
|
cut = bl.split_cut(series)
|
|
tgt = np.nan_to_num(np.asarray(signal_fn(df), float), nan=0.0)
|
|
rep = bl.eval_target(df, tgt, bl.FEE_SIDE,
|
|
metric_mask=np.r_[np.zeros(cut, bool), np.ones(len(df) - cut, bool)])
|
|
# eval_target returns net over the masked region via _metrics; recompute net here
|
|
c = df["close"].values.astype(float)
|
|
r = bl.simple_returns(c)
|
|
pos = np.zeros(len(tgt)); pos[1:] = np.clip(tgt, -1, 1)[:-1]
|
|
net = pos * r - bl.FEE_SIDE * np.abs(np.diff(pos, prepend=0.0))
|
|
return net[cut:], df["datetime"].values[cut:]
|
|
|
|
|
|
def _sharpe(net):
|
|
net = net[np.isfinite(net)]
|
|
return float(np.mean(net) / np.std(net) * np.sqrt(365.25)) if len(net) > 2 and np.std(net) > 0 else 0.0
|
|
|
|
|
|
def _fee_oos_sharpe(signal_fn, series, fee_side):
|
|
df = bl.load(series, "full"); cut = bl.split_cut(series)
|
|
c = df["close"].values.astype(float); r = bl.simple_returns(c)
|
|
tgt = np.clip(np.nan_to_num(np.asarray(signal_fn(df), float)), -1, 1)
|
|
pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1]
|
|
net = pos * r - fee_side * np.abs(np.diff(pos, prepend=0.0))
|
|
return _sharpe(net[cut:])
|
|
|
|
|
|
def verify(name: str) -> dict:
|
|
sig = _sig(AGENTS / f"{name}.py")
|
|
out = {"name": name}
|
|
corrs, jk_worst, fee_sh = [], [], []
|
|
for s in ("A", "B"):
|
|
net, _ = _net(sig, s)
|
|
bnet, _ = _net(_trend_baseline, s)
|
|
m = min(len(net), len(bnet))
|
|
a, b = net[-m:], bnet[-m:]
|
|
mask = np.isfinite(a) & np.isfinite(b)
|
|
corr = float(np.corrcoef(a[mask], b[mask])[0, 1]) if mask.sum() > 3 else 0.0
|
|
corrs.append(corr)
|
|
# jackknife: drop each of K blocks, Sharpe of the rest
|
|
K = 6
|
|
blocks = np.array_split(np.arange(len(net)), K)
|
|
shs = []
|
|
for j in range(K):
|
|
keep = np.concatenate([blocks[k] for k in range(K) if k != j])
|
|
shs.append(_sharpe(net[keep]))
|
|
jk_worst.append(min(shs))
|
|
fee_sh.append(_fee_oos_sharpe(sig, s, 0.001)) # 0.20% RT
|
|
out["corr_to_trend"] = round(float(np.mean(corrs)), 2)
|
|
out["jackknife_worst_sharpe"] = round(float(min(jk_worst)), 2)
|
|
out["fee020_sharpe_min"] = round(float(min(fee_sh)), 2)
|
|
out["verdict"] = (
|
|
"TREND-IN-DISGUISE" if out["corr_to_trend"] > 0.7 else
|
|
"weak/luck" if out["jackknife_worst_sharpe"] < 0.2 else
|
|
"ORTHOGONAL-CANDIDATE")
|
|
return out
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(); ap.add_argument("--top", type=int, default=10)
|
|
args = ap.parse_args()
|
|
lb = json.loads((HERE / "leaderboard.json").read_text())
|
|
top = [r["name"] for r in lb["valid"][:args.top]]
|
|
# baseline self-correlation sanity
|
|
print(f"\n Adversarial verify of top {len(top)} (corr vs canonical TSMOM trend baseline):\n")
|
|
print(f" {'strategy':<26} {'corr_trend':>10} {'jk_worst_Sh':>12} {'fee0.20%_Sh':>12} verdict")
|
|
print(f" {'-'*78}")
|
|
rows = []
|
|
for name in top:
|
|
v = verify(name); rows.append(v)
|
|
print(f" {name[:26]:<26} {v['corr_to_trend']:>10.2f} {v['jackknife_worst_sharpe']:>12.2f} "
|
|
f"{v['fee020_sharpe_min']:>12.2f} {v['verdict']}")
|
|
(HERE / "verify_top.json").write_text(json.dumps(rows, indent=2))
|
|
print(f"\n -> {HERE/'verify_top.json'}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|