TradePortfolio

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:

python
# 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 → period

Prerequisites

Python 3.9+, requests. A Pro or Team API key (settlement data requires Pro+).

bash
pip install requests

Full backtest

Fetches BTC settlement rates for Binance and Hyperliquid over 30 days, aligns by timestamp, and prints daily spread and cumulative carry.

python
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 continue skips those. For a full accounting, align programmatically by bucketing to the longer interval.
  • y is in bps, 8h-normalized. Divide by 10,000 for the decimal rate, then re-scale by intervalMinutes / 480 to get the per-period carry.
  • This does not model execution costs, borrow, or margin. Add those before drawing conclusions about live profitability.

Related