Files
foxhunt/scripts/surfer/xvenue_sim3.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

80 lines
3.3 KiB
Python

#!/usr/bin/env python3
"""Low-turnover rescue test: gross cross-venue spread is +21% with 0.71 persistence, but naive daily
rebalancing is cost-killed. Test if HYSTERESIS (hold a coin until its spread decays below an exit
floor) + lower maker-fee cost flips net positive. If yes -> real but needs patient execution; if no
-> cost-walled."""
import json
import math
import numpy as np
panel = json.load(open("data/surfer/xvenue2/panel2.json"))
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)
def sim(K, entry, exit_, cost):
held = {} # col -> True
rets = []
for t in range(T - 1):
s_t, s_n = spread[t], spread[t + 1]
# drop held coins whose spread decayed below exit (or data gone)
held = {j: 1 for j in held if np.isfinite(s_t[j]) and s_t[j] > exit_}
# fill up to K from fresh entries above entry hurdle
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
if not held:
rets.append(0.0); continue
w = 1.0 / len(held)
realized = sum(w * s_n[j] for j in held if np.isfinite(s_n[j]))
# turnover: this sim only changes the set on entry/exit crossings (low churn)
rets.append(realized)
sim._prev = set(held)
return np.array(rets)
def turnover_cost(K, entry, exit_, cost):
"""Re-run tracking set changes to charge cost properly."""
held, rets, prev = {}, [], set()
for t in range(T - 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
return np.array(rets)
def stats(r):
ann = r.mean() * 365; vol = r.std() * math.sqrt(365)
return ann, (ann / vol if vol > 0 else float("nan"))
print(f"low-turnover rescue: {N} coins, {T} days")
print(f"{'config':>34} {'annNET%':>8} {'Sharpe':>7}")
for K in [10, 20]:
for entry, exit_ in [(0.0005, 0.0003), (0.0010, 0.0005), (0.0020, 0.0010)]:
for cost, cl in [(0.0006, "maker6bp"), (0.0010, "taker10bp")]:
r = turnover_cost(K, entry, exit_, cost)
a, sh = stats(r)
print(f" K={K:>2} entry{entry*1e4:.0f}/exit{exit_*1e4:.0f}bp {cl:>9} {100*a:>+8.1f} {sh:>+7.2f}")
print("\nVERDICT: any config net Sharpe>1 = edge survives with patient/low-turnover execution.")
print("If all still negative = cost-walled even with hysteresis -> not deployable.")