Files
foxhunt/scripts/surfer/xvenue_sim2.py
jgrusewski 027d73a504 research(crypto): cross-venue funding arb survives net-of-cost with hysteresis
Backtested the cross-venue funding arb on historical funding (Binance/OKX/Hyperliquid). Gross
+21%/yr, spreads persist (capture 0.71), but NAIVE daily rebalance is cost-killed (net Sharpe
-4.5, negative every month). HYSTERESIS (hold winners until spread decays, entry>10bp/exit>5bp)
flips net to +10-14%/yr market-neutral (Sharpe +11-15, but inflated by ~1% vol + idealized fills;
realistic ~3-6 / return ~10-14%). Turnover is the swing factor. First edge of the whole search to
survive the net-of-cost horde -- market-neutral, persistent, no spot leg (solves hedgeability),
operational not predictive. Caveats: 94d/one period, idealized fills, counterparty. Next: switch
live harness to hysteresis, deepen history to full year, micro-live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 18:33:28 +02:00

85 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""N-venue cross-venue funding backtest (Binance/OKX/Hyperliquid, deep history).
Per coin-day: spread = max-min daily funding across available venues (short max, long min).
Pick top-K by |spread|, book the REALIZED next-day max-min difference for the held pair, net of
turnover cost. Reports Sharpe/return by top-K, persistence, and PER-MONTH Sharpe (regime check).
"""
import json
import math
import os
import sys
import numpy as np
PANEL = "data/surfer/xvenue2/panel2.json"
COST_RT = 0.0010
HURDLE = 0.0005
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)
# per coin-day: best (max) and worst (min) venue funding, and which venues
hi = np.full((T, N), np.nan); lo = 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:
hi[di[d], j] = max(vals); lo[di[d], j] = min(vals)
spread = hi - lo # always >=0 (max-min)
print(f"N-venue backtest: {N} coins, {T} days ({dates[0]}..{dates[-1]})")
def sim(K, cost):
rets, prev = [], set()
for t in range(T - 1):
s_t, s_n = spread[t], spread[t + 1]
ok = np.isfinite(s_t) & np.isfinite(s_n) & (s_t > HURDLE)
idx = np.where(ok)[0]
if len(idx) == 0:
rets.append(0.0); continue
top = idx[np.argsort(-s_t[idx])[:K]]
realized = float(np.mean(s_n[top])) # next-day max-min still captured if pair persists
cur = set(coins[j] for j in top)
turn = len(cur ^ prev) / max(len(cur), 1)
rets.append(realized - turn * (cost / 2)); prev = cur
r = np.array(rets)
ann = r.mean() * 365; vol = r.std() * math.sqrt(365)
eq = np.cumprod(1 + r); dd = float((eq / np.maximum.accumulate(eq) - 1).min())
return ann, vol, (ann / vol if vol > 0 else float("nan")), dd, r
print(f"\n{'topK':>5} {'annNET%':>8} {'Sharpe':>7} {'maxDD%':>7} {'gross%':>7}")
R = {}
for K in [5, 10, 20]:
a, v, sh, dd, r = sim(K, COST_RT); g = sim(K, 0.0)[0]; R[K] = r
print(f"{K:>5} {100*a:>+8.1f} {sh:>+7.2f} {100*dd:>+7.1f} {100*g:>+7.0f}")
# persistence: top-spread coin still positive-spread direction next day. spread is max-min(>=0);
# the real persistence q = does the SAME venue stay highest? approximate via spread autocorr sign>0 always,
# so report realized/snapshot ratio = how much of today's spread you actually collect next day.
capt = []
for t in range(T - 1):
ok = np.isfinite(spread[t]) & np.isfinite(spread[t + 1]) & (spread[t] > HURDLE)
idx = np.where(ok)[0]
if len(idx):
top = idx[np.argsort(-spread[t][idx])[:10]]
capt.append(np.mean(spread[t + 1][top]) / max(np.mean(spread[t][top]), 1e-9))
print(f"\n capture ratio (next-day spread / today's spread, top-10): {np.mean(capt):.2f} (1.0=fully persists, ~0=collapses)")
# per-month Sharpe (regime check) on top-10
r10 = R[10]
mo = [dates[t][:7] for t in range(T - 1)]
print(" per-month Sharpe (top-10, net):")
for m in sorted(set(mo)):
seg = r10[np.array(mo) == m]
if len(seg) > 8 and seg.std() > 0:
print(f" {m}: {seg.mean()/seg.std()*math.sqrt(365):+.1f} (n={len(seg)})")
print("\n VERDICT: net Sharpe>1 across MONTHS + capture ratio>0.5 = robust deployable edge.")
if __name__ == "__main__":
main()