test(crypto): clean OOS for cross-venue arb (IS-pick config -> OOS-blind apply)

The hysteresis params were chosen by looking at the data = in-sample. Clean OOS protocol (same
that killed PEAD): pick best config on first 60% by IS Sharpe, apply EXACT config blind to last
40%. Reports IS-best -> OOS Sharpe, robustness (configs IS>2 that also OOS>1), OOS per-month.
Decisive: OOS holds -> real; OOS collapses -> in-sample fit. Monitor armed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-06-07 19:03:30 +02:00
parent ee2215f4d3
commit 2b36929b5f

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""CLEAN OOS test of the cross-venue funding arb (hysteresis).
The hysteresis params (entry/exit/K) were chosen by looking at the data = in-sample. Honest test:
pick the best config on IN-SAMPLE (first 60% of days) by IS Sharpe, then apply that EXACT config
BLIND to OUT-OF-SAMPLE (last 40%). If OOS holds -> real; if it collapses -> in-sample fit.
Reports IS-best config + its OOS result, robustness across configs, and OOS per-month.
Taker cost 10bp (conservative). Sharpe is inflated by low vol + idealized fills (realistic ~ /2.5).
"""
import json
import math
import numpy as np
PANEL = "data/surfer/xvenue2/panel2.json"
COST = 0.0010
def main():
panel = json.load(open(PANEL))
dates = sorted(set().union(*[set(v) for v in panel.values()]))
di = {d: i for i, d in enumerate(dates)}
coins = list(panel)
T, N = len(dates), len(coins)
spread = np.full((T, N), np.nan)
for j, c in enumerate(coins):
for d, v in panel[c].items():
vals = list(v.values())
if len(vals) >= 2:
spread[di[d], j] = max(vals) - min(vals)
split = int(0.60 * T)
print(f"cross-venue OOS: {N} coins, {T} days | IS {dates[0]}..{dates[split-1]} | OOS {dates[split]}..{dates[-1]}")
def hyst(t0, t1, K, entry, exit_, cost):
held, rets, prev = {}, [], set()
for t in range(t0, t1 - 1):
s_t, s_n = spread[t], spread[t + 1]
held = {j: 1 for j in held if np.isfinite(s_t[j]) and s_t[j] > exit_}
if len(held) < K:
cand = [j for j in np.where(np.isfinite(s_t) & (s_t > entry))[0] if j not in held]
cand.sort(key=lambda j: -s_t[j])
for j in cand[:K - len(held)]:
held[j] = 1
cur = set(held)
turn = len(cur ^ prev) / max(len(cur), 1) if cur else 0
realized = (sum(s_n[j] for j in held if np.isfinite(s_n[j])) / len(held)) if held else 0.0
rets.append(realized - turn * (cost / 2)); prev = cur
r = np.array(rets)
ann = r.mean() * 365 if len(r) else float("nan")
vol = r.std() * math.sqrt(365) if len(r) else float("nan")
return ann, (ann / vol if vol > 0 else float("nan")), r
grid = [(K, e, x) for K in (10, 20) for e in (0.0005, 0.0010, 0.0020) for x in (0.0003, 0.0005, 0.0010) if x <= e]
scored = []
for cfg in grid:
_, is_sh, _ = hyst(0, split, *cfg, COST)
_, oos_sh, _ = hyst(split, T, *cfg, COST)
oa = hyst(split, T, *cfg, COST)[0]
scored.append((cfg, is_sh, oos_sh, oa))
best = max(scored, key=lambda s: s[1]) # pick by IS Sharpe ONLY
cfg, is_sh, oos_sh, oos_ann = best
print(f"\n IS-BEST config (chosen blind to OOS): K={cfg[0]} entry={cfg[1]*1e4:.0f}bp exit={cfg[2]*1e4:.0f}bp")
print(f" IS Sharpe {is_sh:+.1f} -> OOS Sharpe {oos_sh:+.1f} | OOS ann {100*oos_ann:+.1f}% (realistic ~/2.5 = {oos_sh/2.5:+.1f})")
good = [s for s in scored if s[1] > 2]
robust = [s for s in good if s[2] > 1]
print(f" robustness: of {len(good)} configs with IS Sharpe>2, {len(robust)} also OOS Sharpe>1")
# OOS per-month on the IS-best config
_, _, r = hyst(split, T, *cfg, COST)
mo = [dates[t][:7] for t in range(split, T - 1)]
print(" OOS per-month Sharpe (IS-best config):")
for m in sorted(set(mo)):
seg = r[np.array(mo) == m]
if len(seg) > 6 and seg.std() > 0:
print(f" {m}: {seg.mean()/seg.std()*math.sqrt(365):+.1f} (n={len(seg)})")
print("\n VERDICT: IS-best OOS Sharpe>1 (realistic, after /2.5) + most months positive = REAL out-of-sample.")
print(" If OOS collapses to ~0/negative = in-sample fit, joins the dead list.")
if __name__ == "__main__":
main()