Funding arb backtest
A walkthrough for backtesting a delta-neutral funding rate arbitrage strategy using the Settlement endpoint. The recipe below fetches per-settlement events for two venues, computes the spread at each tick, and nets out the realized carry over any date range — in about 30 lines of Python.
Why the Settlement endpoint, not Historical
GET /funding/historical aggregates rates into fixed-size buckets (4-hour beyond 7 days). Using it for P&L attribution inflates or deflates carry by up to 5× when a venue settles every 1 hour or every 8 hours — the bucket size doesn't match the settlement interval.
GET /funding/settlement returns one row per actual settlement event, with the exact rate and intervalMinutes baked in. Converting to raw decimal is unambiguous:
# y is in bps_8h. intervalMinutes tells you the actual settlement period.
raw_rate = row["y"] / 10_000 # bps → decimal
period_rate = raw_rate * (row["intervalMinutes"] / 480) # 8h-normalize → periodPrerequisites
Python 3.9+, requests. A Pro or Team API key (settlement data requires Pro+).
pip install requestsFull backtest
Fetches BTC settlement rates for Binance and Hyperliquid over 30 days, aligns by timestamp, and prints daily spread and cumulative carry.
import requests
from collections import defaultdict
API_KEY = "lk_live_your_key_here"
BASE = "https://api.loris.tools"
def fetch_settlement(symbol, exchanges, start, end):
r = requests.get(
f"{BASE}/funding/settlement",
headers={"X-Api-Key": API_KEY},
params={"symbol": symbol, "exchanges": ",".join(exchanges),
"start": start, "end": end},
timeout=30,
)
r.raise_for_status()
return r.json()["series"] # { exchange: [{t, y, intervalMinutes}] }
def period_pnl(row):
"""Convert one settlement event to realized carry (decimal, per-period)."""
raw_rate = row["y"] / 10_000
return raw_rate * (row["intervalMinutes"] / 480)
# ── fetch ──────────────────────────────────────────────────────────────────
series = fetch_settlement(
symbol = "BTC",
exchanges = ["binance", "hyperliquid"],
start = "2026-05-01T00:00:00Z",
end = "2026-05-31T23:59:59Z",
)
# ── align by timestamp ─────────────────────────────────────────────────────
by_ts = defaultdict(dict)
for exchange, rows in series.items():
for row in rows:
by_ts[row["t"]][exchange] = period_pnl(row)
# ── compute spread ─────────────────────────────────────────────────────────
# Long Hyperliquid (collect funding), short Binance (pay funding).
# Net carry = HL_rate - BNB_rate at each coincident tick.
cumulative = 0.0
for ts in sorted(by_ts):
tick = by_ts[ts]
if "binance" not in tick or "hyperliquid" not in tick:
continue # skip ticks where only one venue settled
spread = tick["hyperliquid"] - tick["binance"]
cumulative += spread
print(f"{ts} spread={spread:+.6f} cumulative={cumulative:+.6f}")
print(f"\nTotal carry over period: {cumulative:+.4%}")Notes
- Binance settles every 8 hours; Hyperliquid every 1 hour. The ticks won't always coincide — the inner
continueskips those. For a full accounting, align programmatically by bucketing to the longer interval. yis in bps, 8h-normalized. Divide by 10,000 for the decimal rate, then re-scale byintervalMinutes / 480to get the per-period carry.- This does not model execution costs, borrow, or margin. Add those before drawing conclusions about live profitability.