The Trap in Every Hyperliquid Funding Backtest

Most code that pulls Hyperliquid funding rate history through an API in Python makes the same mistake on the first day: it treats every row of a rate series as a payment. Hyperliquid settles funding once an hour. If your series samples the live rate every 5 minutes, you have 12 rows per settlement, and sum(funding_rate) over a day returns roughly twelve times the carry that was actually paid. A BTC day of about 0.024% reads as about 0.29%. Your "funding arbitrage" backtest looks wonderful and is wrong.

The second mistake is quieter. Hyperliquid pays funding on the oracle price, not the mark price, and its fundingHistory endpoint never returns the oracle. Reconstruct a payment from the mark and you are off by the premium, which is exactly the thing funding exists to correct.

This post walks through the two grains we serve, the fields that say which one you have, the oracle fields added on 2026-10-23, and a paging loop in Python that gets the settled series without the surcharges Hyperliquid charges for long list reads.

What Does Hyperliquid's fundingHistory Return?

Hyperliquid's native fundingHistory info request returns the settled hourly prints for one coin with fundingRate, premium and time, at most ~500 entries per call, and it costs 20 weight plus an additional weight per 20 items against the 1,200-per-minute IP budget. Backfilling a year of hourly prints for fifty coins through it is a few hundred paged calls and a careful sleep schedule.

The live tail is on /api/v1/hyperliquid/funding-rates, which is available on a free key:

curl -H "X-API-Key: cdk_live_your_key" \
  "https://cryptodataapi.com/api/v1/hyperliquid/funding-rates?coin=BTC&limit=24"
{
  "coin": "BTC",
  "current_rate": 0.0000125,
  "history": [
    {"coin": "BTC", "funding_rate": 0.0000125, "premium": 0.0000311, "time": 1762124400000},
    "..."
  ]
}

current_rate is the live rate from the asset context; history[] is the settled prints, newest last, up to limit 100. symbol is accepted as an alias for coin, and an unrecognised query parameter is a 400 unknown_parameter. This is the right call for "what did BTC settle in the last day". It is the wrong call for a backtest, which is what /backtesting/funding is for.

Rate Samples vs Settled Payments: the Two Grains

/api/v1/backtesting/funding serves Hyperliquid funding in two grains, and the response tells you which one you are holding so the sum mistake cannot happen silently.

grain=snapshot_5m (default)grain=hourly
One row isA 5-minute sample of the live rate, with open_interest and mark_priceOne settled funding print from fundingHistory, with premium; OI and mark null
grain.kindrate_snapshotsettled_payment
grain.sum_okfalsetrue
grain.settlements_per_day24 (Hyperliquid), 3 (Binance)24
Rows per Hyperliquid dayAbout 28824
Daily carrymean(funding_rate) x 24sum(funding_rate)
FreshnessLive, 5-minute cadenceLoaded daily for the previous UTC day (about a 1-day lag)
Exchangeshyperliquid, binancehyperliquid only; binance is 400 grain_unavailable

Read grain.sum_ok before you aggregate, and the arithmetic is forced: sum the settled rows, average the sampled rows. Two coverage notes from the same response's coverage block: Hyperliquid rows exist locally from 2023-05-12 for BTC and ETH and from listing for other coins, but rows before 2026-05-09 under the default grain were loaded from the hourly archive, so they are one row an hour with null OI and mark, and rows before 2026-03-30 are settled prints seeded from the deep archive. mean x 24 stays correct on both; a plain sum does not. Binance rows start 2026-05-09 and Binance's settled prints are on /derivatives/binance/funding-rates.

How Is a Hyperliquid Funding Payment Priced?

A Hyperliquid funding payment is funding_rate x position_size x oracle_price. The oracle is the exchange's external reference price, and the premium that drives the rate is the mark's deviation from it. Hyperliquid's history API serves the rate and the premium but never the oracle, so until recently nobody reconstructing payments from history could price them exactly.

Since 2026-10-23 both grains of /backtesting/funding carry oracle_price and premium, and the default rows gained premium too. On grain=hourly rows, oracle_price and mark_price are our read of the asset context seconds after that settlement boundary, and oracle_as_of is that read's wall clock in milliseconds, so you can see how close to the boundary the read landed. The rules on null:

{
  "symbol": "BTC", "exchange": "hyperliquid",
  "grain": {"kind": "settled_payment", "sum_ok": true, "settlements_per_day": 24, "...": "..."},
  "data": [
    {"time": 1761958800000, "funding_rate": 0.0000125, "premium": 0.0000402,
     "oracle_price": 70412.0, "mark_price": 70415.0, "oracle_as_of": 1761958803210,
     "open_interest": null}
  ],
  "coverage": {"local_first": 1683849600000, "local_last": 1762041600000},
  "next_cursor": "...", "has_more": true
}

Example values; field names exact. For a 2 BTC long at that print: 0.0000125 x 2 x 70412 is about $1.76 paid by longs for that hour.

Python: Page the Settled Series With a Cursor

The range readers under /backtesting/* page by keyset. Pass next_cursor back as cursor with the same start and end, and stop when has_more is false. The route is Pro Plus and served by an isolated container limited to 2 requests a second per key sustained (burst 20) and 10,000 rows per page, so a year of hourly prints for one coin is one page.

import httpx

BASE = "https://cryptodataapi.com/api/v1/backtesting/funding"
H = {"X-API-Key": "cdk_live_your_key", "Accept-Encoding": "gzip"}

def settled_funding(symbol, start, end):
    rows, cursor = [], None
    while True:
        params = {"symbol": symbol, "exchange": "hyperliquid", "grain": "hourly",
                  "start": start, "end": end, "limit": 10000}
        if cursor:
            params["cursor"] = cursor
        body = httpx.get(BASE, params=params, headers=H, timeout=60).json()
        assert body["grain"]["sum_ok"], body["grain"]["kind"]   # never sum a rate_snapshot
        rows.extend(body["data"])
        if not body.get("has_more"):
            return rows
        cursor = body["next_cursor"]

rows = settled_funding("BTC", "2026-01-01", "2026-11-01")
daily = {}
for r in rows:
    day = r["time"] // 86_400_000
    daily[day] = daily.get(day, 0.0) + r["funding_rate"]        # 24 prints per day
print(len(rows), "prints;", sum(daily.values()) / len(daily) * 100, "% mean daily carry")

# price the carry on a fixed 1 BTC long where the oracle was observed
paid = sum(r["funding_rate"] * 1 * r["oracle_price"] for r in rows if r["oracle_price"] is not None)

The assert is the point of the post in one line. If you forget grain=hourly, it fails loudly instead of letting you sum 288 samples a day. Send Accept-Encoding: gzip; pages compress about ten to one. format=csv is available, with the paging state in X-Has-More / X-Next-Cursor headers.

How Do I Get Hyperliquid Funding History Back to 2023 in Bulk?

For a bulk download, skip paging and take the archive files from /api/v1/backtesting/archives and /archives/download, which return pre-signed URLs to Parquet. Two data types cover funding:

curl -H "X-API-Key: cdk_live_your_key" \
  "https://cryptodataapi.com/api/v1/backtesting/archives/download?data_type=funding_hourly&start=2026-10-23&end=2026-10-31"

Archives are Pro Plus. Without a subscription, an agent can buy single files over x402 at /archives/purchase: $1.00 per funding_hourly day-file and $3.00 per funding_deep month-file, with a 404 before any quote if the object does not exist. The archive purchase post walks through that flow.

Funding on the Candle Clock: hl-funding-bars

Most strategies do not want a funding series at all; they want to know what funding did during the same 4h bar a price signal fired on. /api/v1/backtesting/hl-funding-bars aligns both grains to the candle grid. Each row is one closed bar per coin with time (the bar open, equal to the matching /hyperliquid/candles bar's timestamp), funding_sum (carry actually settled in (time, time + interval], which sums across bars), n_settlements, funding_rate_mean (a level, do not sum), oi_open / oi_close, oi_change_pct, mark_close and n_snapshots.

The settlement boundary convention matters: a settlement at 16:00 pays for the hour before it, so it belongs to the bar that closes at 16:00. funding_sum is null with n_settlements 0 for the most recent day until the settled prints are archived; funding_rate_mean is there immediately. Bars begin 2026-03-30 because OI does; older settled funding alone is the hourly grain above. Pro Plus, window at most 93 days per call.

When to Use Which (and When Not To)

What this cannot do: price a payment exactly before 2026-10-23 (no oracle was captured), give Hyperliquid OI before 2026-03-30 (none exists), or compare Hyperliquid and Binance rows without normalising the 1-hour versus 8-hour settlement. For that last one, the cross-exchange funding post covers the arithmetic. Plans and the x402 prices are on /pricing.

Frequently asked questions

Why does summing Hyperliquid funding rate history give a number that is too high?

Because a sampled series has many rows per settlement. Hyperliquid settles hourly; a 5-minute sample has 12 rows per payment, so a naive sum overstates carry about 12x. Use /backtesting/funding?grain=hourly, where one row is one settled print and grain.sum_ok is true, or take mean x 24 on sampled rows.

Is a Hyperliquid funding payment calculated on the mark price or the oracle price?

The oracle price. A payment is funding_rate x position size x oracle_price. Since 2026-10-23 /backtesting/funding rows carry oracle_price, oracle_as_of and premium; they are null before that date and on Binance rows.

How far back does Hyperliquid funding history go on CryptoDataAPI?

Locally to 2023-05-12 for BTC and ETH and from listing for other coins. The funding_deep archive holds hourly prints per coin back to about 2024 as monthly Parquet files, and funding_hourly holds daily all-coin files with oracle columns from 2026-10-23.

How do I page /backtesting/funding in Python?

Request with symbol, exchange=hyperliquid, grain=hourly, start and end, then pass next_cursor back as cursor with the same window until has_more is false. Pages hold up to 10,000 rows and compress about 10x with gzip.

What does funding_sum mean on hl-funding-bars?

The carry actually settled inside that bar, from the bar open exclusive to the bar close inclusive, so a 16:00 settlement belongs to the bar closing at 16:00. It sums across bars and is null for the most recent day until the settled prints are archived.