correction(crypto): cross-venue arb is MARGINAL not breakthrough (honest fixed-pair OOS)

The +14.9 OOS Sharpe was the max-min optimism (assumes daily free re-positioning to optimal venue
pair). Honest fixed-pair version (hold the entry-chosen pair, book its actual funding diff): OOS
Sharpe collapses +14.9->+0.8, OOS ann +15.3%->+1.1% (realistic ~breakeven net of cost), 0/12
configs robust OOS, feb month -8.2. Capture 0.68 was the tell (32% reshuffle loss). Cross-venue
arb joins the cost-walled/in-sample-only list. User's OOS insistence caught it. Only deployable
thing remains simple 60/40 (~0.7). Every crypto-funding variant marginal once honestly tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-06-07 19:10:13 +02:00
parent e731c2fc2f
commit 764fd99480

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""HONEST cross-venue OOS: track the FIXED venue pair chosen at entry (not the daily max-min).
The earlier sim booked daily max-min funding (assumes you always hold the optimal venue pair = too
optimistic; capture 0.68 shows ~32% reshuffle loss). Realistic: at entry pick A=argmax venue,
B=argmin; hold THAT pair; book realized = funding_A - funding_B on the FIXED pair each day; exit
when your pair's spread decays below exit. Clean OOS split (IS-pick config -> OOS-blind).
"""
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()]))
coins = list(panel)
T = len(dates)
F = {c: panel[c] for c in coins} # F[coin][date] = {venue: daily_funding}
split = int(0.60 * T)
print(f"HONEST cross-venue OOS (fixed held pair): {len(coins)} coins, {T} days")
print(f" IS {dates[0]}..{dates[split-1]} | OOS {dates[split]}..{dates[-1]}")
def hyst(t0, t1, K, entry, exit_, cost):
held = {} # coin -> (A, B)
rets, prev = [], set()
for t in range(t0, t1 - 1):
dt, dn = dates[t], dates[t + 1]
# exit: keep held coins whose FIXED-pair spread still > exit
keep = {}
for c, (A, B) in held.items():
ft = F[c].get(dt)
if ft and A in ft and B in ft and (ft[A] - ft[B]) > exit_:
keep[c] = (A, B)
held = keep
# entry: rank fresh coins by current max-min, add above entry hurdle
cands = []
for c in coins:
if c in held:
continue
ft = F[c].get(dt)
if ft and len(ft) >= 2:
A = max(ft, key=ft.get); B = min(ft, key=ft.get)
if ft[A] - ft[B] > entry:
cands.append((ft[A] - ft[B], c, A, B))
cands.sort(reverse=True, key=lambda x: x[0])
for sp, c, A, B in cands:
if len(held) >= K:
break
held[c] = (A, B)
# realized NEXT day on the FIXED pair (short A receive f_A, long B receive -f_B)
if held:
tot = 0.0
for c, (A, B) in held.items():
fn = F[c].get(dn)
if fn and A in fn and B in fn:
tot += fn[A] - fn[B]
realized = tot / len(held)
else:
realized = 0.0
cur = set(held)
turn = len(cur ^ prev) / max(len(cur), 1) if cur else 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 = [(cfg, hyst(0, split, *cfg, COST)[1], hyst(split, T, *cfg, COST)[1], hyst(split, T, *cfg, COST)[0]) for cfg in grid]
best = max(scored, key=lambda s: s[1])
cfg, is_sh, oos_sh, oos_ann = best
print(f"\n IS-BEST (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: {len(robust)}/{len(good)} IS-good configs also OOS Sharpe>1")
_, _, r = hyst(split, T, *cfg, COST)
mo = [dates[t][:7] for t in range(split, T - 1)]
print(" OOS per-month Sharpe:")
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}")
print("\n VERDICT (honest, fixed-pair): OOS Sharpe>1 realistic + months positive = genuinely deployable.")
if __name__ == "__main__":
main()