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>
114 lines
3.7 KiB
Python
114 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Deep multi-venue funding history (Binance + OKX + Hyperliquid, ~1yr) for a confident cross-venue
|
|
backtest. Binance & HL are deep (~330d); OKX ~3mo. Aggregate to daily per venue. Cache per coin.
|
|
Saves data/surfer/xvenue/panel2.json = {coin: {date: {bn, okx, hl}}}.
|
|
"""
|
|
import datetime
|
|
import json
|
|
import os
|
|
import time
|
|
import urllib.request
|
|
|
|
OUT = "data/surfer/xvenue2"
|
|
NCOINS = 35
|
|
DAYS = 330
|
|
|
|
|
|
def get(u, post=None):
|
|
data = json.dumps(post).encode() if post else None
|
|
h = {"User-Agent": "Mozilla/5.0", "Accept": "application/json"}
|
|
if post:
|
|
h["Content-Type"] = "application/json"
|
|
for a in range(4):
|
|
try:
|
|
return json.loads(urllib.request.urlopen(urllib.request.Request(u, data=data, headers=h), timeout=25).read())
|
|
except Exception:
|
|
if a == 3:
|
|
return None
|
|
time.sleep(3 * (a + 1))
|
|
return None
|
|
|
|
|
|
def base(s):
|
|
return s[:-4] if s.endswith("USDT") else s
|
|
|
|
|
|
def day_of(ms):
|
|
return datetime.datetime.utcfromtimestamp(int(ms) / 1000).strftime("%Y-%m-%d")
|
|
|
|
|
|
def binance_hist(coin):
|
|
r = get(f"https://fapi.binance.com/fapi/v1/fundingRate?symbol={coin}USDT&limit=1000")
|
|
d = {}
|
|
for x in (r or []):
|
|
k = day_of(x["fundingTime"]); d[k] = d.get(k, 0.0) + float(x["fundingRate"])
|
|
return d
|
|
|
|
|
|
def okx_hist(coin):
|
|
out, after = {}, None
|
|
for _ in range(6):
|
|
u = f"https://www.okx.com/api/v5/public/funding-rate-history?instId={coin}-USDT-SWAP&limit=100"
|
|
if after:
|
|
u += f"&after={after}"
|
|
r = get(u); data = (r or {}).get("data") or []
|
|
if not data:
|
|
break
|
|
for x in data:
|
|
k = day_of(x["fundingTime"]); out[k] = out.get(k, 0.0) + float(x["fundingRate"])
|
|
after = data[-1]["fundingTime"]; time.sleep(0.2)
|
|
return out
|
|
|
|
|
|
def hl_hist(coin, now_ms):
|
|
out, start = {}, now_ms - DAYS * 86400000
|
|
for _ in range(30):
|
|
r = get("https://api.hyperliquid.xyz/info", post={"type": "fundingHistory", "coin": coin, "startTime": start})
|
|
if not r:
|
|
break
|
|
for x in r:
|
|
k = day_of(x["time"]); out[k] = out.get(k, 0.0) + float(x["fundingRate"])
|
|
last = int(r[-1]["time"])
|
|
if last <= start or len(r) < 2:
|
|
break
|
|
start = last + 1
|
|
return out
|
|
|
|
|
|
def main():
|
|
os.makedirs(OUT, exist_ok=True)
|
|
now_ms = int(time.time() * 1000)
|
|
binv = {base(x["symbol"]): float(x["quoteVolume"]) for x in get("https://fapi.binance.com/fapi/v1/ticker/24hr") if x["symbol"].endswith("USDT")}
|
|
hlmeta = get("https://api.hyperliquid.xyz/info", post={"type": "metaAndAssetCtxs"})
|
|
hlcoins = {u["name"] for u in hlmeta[0]["universe"]}
|
|
coins = sorted([c for c in binv if c in hlcoins and binv[c] > 10e6], key=lambda c: -binv[c])[:NCOINS]
|
|
print(f"universe: {len(coins)} coins (Binance>$10M & on Hyperliquid)")
|
|
panel = {}
|
|
for i, c in enumerate(coins):
|
|
cf = f"{OUT}/{c}.json"
|
|
if os.path.exists(cf):
|
|
panel[c] = json.load(open(cf)); continue
|
|
bn = binance_hist(c); okx = okx_hist(c); hl = hl_hist(c, now_ms)
|
|
days = sorted(set(bn) | set(okx) | set(hl))
|
|
rec = {}
|
|
for d in days:
|
|
v = {}
|
|
if d in bn:
|
|
v["bn"] = bn[d]
|
|
if d in okx:
|
|
v["okx"] = okx[d]
|
|
if d in hl:
|
|
v["hl"] = hl[d]
|
|
if len(v) >= 2:
|
|
rec[d] = v
|
|
panel[c] = rec
|
|
json.dump(rec, open(cf, "w"))
|
|
print(f" {i+1}/{len(coins)} {c}: {len(rec)} days w/ >=2 venues (bn{len(bn)} okx{len(okx)} hl{len(hl)})")
|
|
time.sleep(0.3)
|
|
json.dump(panel, open(f"{OUT}/panel2.json", "w"))
|
|
print(f"DONE: {len(panel)} coins -> {OUT}/panel2.json")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|