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:
92
scripts/surfer/xvenue_sim_oos2.py
Normal file
92
scripts/surfer/xvenue_sim_oos2.py
Normal 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()
|
||||
Reference in New Issue
Block a user