#!/usr/bin/env python """Full pre-registered battery for the RVOL-ORB study. Pre-reg: /fp-data/studies/2026-08-25-rvol-orb-prereg.md Runs: primary cell, monotonicity, entry A vs B, friction sensitivity, year-by-year, IS/OOS split, and the 7-instrument replication set. """ import numpy as np import pandas as pd from scipy.stats import spearmanr import rvol_orb as R SYMS = ["NQ", "ES", "RTY", "YM", "CL", "GC", "ZN", "6E"] OOS_START = 2020 def t(a): return R.tstat(a) def line(k, n, m, tt, hit, extra=""): print(f"{k:>22} {n:>6} {m:>10.3f} {tt:>7.2f} {hit:>6.1f} {extra}") def main(): cache = {} print("=" * 92) print("PRIMARY — NQ · 5m OR · Entry B (confirm) · Q5 · 1.0 pt friction · pass bar t > 3.0") print("=" * 92) df = R.run("NQ", confirm=True, cost_mult=1.0) cache[("NQ", True, 1.0)] = df q5 = df[df["q"] == 5]["net"].to_numpy() print(f" n = {len(q5)} mean net = {q5.mean():+.3f} pt t = {t(q5):+.2f} " f"hit = {100*(q5>0).mean():.1f}% total = {q5.sum():+.1f} pt") verdict = "PASS" if (t(q5) > 3.0 and q5.mean() > 0) else "FAIL" print(f" >>> PRIMARY {verdict}") print("\n" + "=" * 92) print("SECONDARY 1 — monotonicity across RVOL quintiles (the distinctive Zarattini claim)") print("=" * 92) print(f"{'bucket':>22} {'n':>6} {'mean_net':>10} {'t':>7} {'hit%':>6} rvol range") means = [] for q in range(1, 6): sub = df[df["q"] == q] v = sub["net"].to_numpy() means.append(v.mean()) line(f"Q{q}", len(v), v.mean(), t(v), 100 * (v > 0).mean(), f" {sub['rvol'].min():.2f}–{sub['rvol'].max():.2f}") rho = spearmanr(list(range(5)), means).statistic print(f" Spearman rho(Q1->Q5) = {rho:+.3f} (Zarattini on stocks: strictly increasing)") print("\n" + "=" * 92) print("SECONDARY 2 — entry rule: A (naive touch) vs B (desk confirm)") print("=" * 92) print(f"{'cell':>22} {'n':>6} {'mean_net':>10} {'t':>7} {'hit%':>6}") for confirm, nm in [(False, "A naive"), (True, "B confirm")]: d = cache.get(("NQ", confirm, 1.0)) if d is None: d = R.run("NQ", confirm=confirm, cost_mult=1.0) cache[("NQ", confirm, 1.0)] = d for scope, sel in [("all", d), ("Q5", d[d["q"] == 5])]: v = sel["net"].to_numpy() line(f"{nm} · {scope}", len(v), v.mean(), t(v), 100 * (v > 0).mean()) print("\n" + "=" * 92) print("SECONDARY 3 — friction sensitivity (NQ, Entry B, base cost 1.0 pt)") print("=" * 92) print(f"{'cell':>22} {'n':>6} {'mean_net':>10} {'t':>7} {'hit%':>6}") for mult, tag in [(0.0, "0.0 pt (gross)"), (0.5, "0.5 pt"), (1.0, "1.0 pt"), (2.0, "2.0 pt")]: d = cache.get(("NQ", True, mult)) if d is None: d = R.run("NQ", confirm=True, cost_mult=mult) cache[("NQ", True, mult)] = d for scope, sel in [("all", d), ("Q5", d[d["q"] == 5])]: v = sel["net"].to_numpy() line(f"{tag} · {scope}", len(v), v.mean(), t(v), 100 * (v > 0).mean()) print("\n" + "=" * 92) print("SECONDARY 4 — year-by-year, NQ Q5 (Mesfin's failure mode: one year masking flat ones)") print("=" * 92) d = cache[("NQ", True, 1.0)] sub = d[d["q"] == 5] pos = 0 yrs = sorted(sub["year"].unique()) print(f"{'year':>22} {'n':>6} {'mean_net':>10} {'total':>10}") for y in yrs: v = sub[sub["year"] == y]["net"].to_numpy() if len(v) == 0: continue pos += v.mean() > 0 line(str(y), len(v), v.mean(), float("nan"), float("nan"), f"{v.sum():>10.1f}") print(f" years with positive mean: {pos}/{len(yrs)}") is_ = sub[sub["year"] < OOS_START]["net"].to_numpy() oos = sub[sub["year"] >= OOS_START]["net"].to_numpy() print(f" IS (<{OOS_START}): n={len(is_):4d} mean={is_.mean():+.3f} t={t(is_):+.2f}") print(f" OOS (>={OOS_START}): n={len(oos):4d} mean={oos.mean():+.3f} t={t(oos):+.2f}") print("\n" + "=" * 92) print("SECONDARY 5 — replication set, each instrument's own Q5, Entry B, desk friction") print("=" * 92) print(f"{'symbol':>22} {'n':>6} {'mean_net':>10} {'t':>7} {'hit%':>6} p99 RVOL") repl = 0 for s in SYMS: try: ds = R.run(s, confirm=True, cost_mult=1.0) except Exception as e: print(f"{s:>22} ERROR {e}") continue if ds is None or len(ds) == 0: print(f"{s:>22} no data") continue v = ds[ds["q"] == 5]["net"].to_numpy() tt = t(v) if s != "NQ" and tt > 2.0: repl += 1 p99 = np.percentile(ds["rvol"].to_numpy(), 99) line(s, len(v), v.mean(), tt, 100 * (v > 0).mean(), f" {p99:.2f}") print(f" instruments replicating at t > 2.0 (excl. NQ): {repl}/7 (pre-reg needs >= 4)") print("\n" + "=" * 92) print("VERDICT vs pre-registered falsification criteria") print("=" * 92) c1 = t(q5) > 3.0 and q5.mean() > 0 c2 = rho > 0 c3 = repl >= 4 print(f" 1. primary t > 3.0 and positive .......... {'PASS' if c1 else 'FAIL'} (t={t(q5):+.2f})") print(f" 2. monotonicity rho > 0 .................. {'PASS' if c2 else 'FAIL'} (rho={rho:+.3f})") print(f" 3. >= 4/7 instruments replicate t > 2.0 .. {'PASS' if c3 else 'FAIL'} ({repl}/7)") print(f"\n H1 = {'CONFIRMED' if (c1 and c2 and c3) else ('CONTESTED' if c1 else 'REFUTED')}") if __name__ == "__main__": main()