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