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>
83 lines
3.7 KiB
Python
83 lines
3.7 KiB
Python
#!/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()
|