A Hyperliquid backtest built on candles fills every order at a price, not against a book, and it never pays a fee. How wrong that is depends on the coin. On the sample day BTC's book was deep and one tick wide, so a $100k market order cost half a tick over mid, while the taker fee on the tape cost fifty times that. HYPE held about $0.2M in its 25 visible levels: the same $100k order paid 2.2 bps over mid at a typical moment, and a $1M order did not fit in the visible book 99% of the day. This guide loads the historical order book in pandas, prices every simulated order by walking the levels resting at that block, and charges the fee from the tape. Every figure comes from the free sample day and the script shown.
Hyperliquid produces a block about every 71.5 ms. On the sample day BTC's book changed in 68.7% of them, 824,716 book states, against the 15,999 book snapshots the chain's archive published, one every 5.4 seconds. A snapshot series sees about one BTC book in fifty. The info API only returns the book as it is now.
And the orders that matter are exactly the ones that walk the book. Rebuilding each taker order from its fills, 5.3% of BTC taker orders (11.9% on HYPE) filled at more than one price, but they carried 61% of the day's taker notional on both coins. A backtest that prices those at the mid or the last trade is wrong on most of the volume that moves anything.
Hyperliquid data is sold per day with a coin filter: pick a range, the tables and the coins, and the bundle keeps one parquet file per table per coin per day. Three ways in:
2026-08-12, any coins up to 1.0 GB, on a free account. Costs no market·day claims.
Pull it →Every perp, spot pair and HIP-3 market since 2025-01-25, on a Hyperliquid plan or add-on.
What's in the archive →Explorer and up: request a range of days for a set of coins, poll, stream the bundle. Snippet below.
API reference →import requests, timeAPI = "https://tickfoundry.com/api/v1" # Explorer and upH = {"Authorization": "Bearer tf_live_…"} # minted in the dashboardjob = requests.post(f"{API}/hyperliquid/downloads", headers=H, json={"start": "2026-08-01", "end": "2026-08-31","datasets": ["hl_l2", "hl_trades"],"coins": ["HYPE"],}).json()while (s := requests.get(f"{API}/downloads/{job['id']}", headers=H).json())["status"] == "staging":time.sleep(10)open("hyperliquid.zip", "wb").write(requests.get(s["url"], headers=H).content)
Building this yourself — pulling the chain's order-event archive out of requester-pays S3 and replaying it into books — runs to around $10,000 in egress and compute.
A bundle is table-first: hl_l2/date=D/<COIN>/l2.parquet, the same for hl_l1 and hl_trades, plus quality/date=D/<COIN>.json. Rows key on msg_seq, the block height, which is the ordering to trust; ts_src_ms is the block's own time. The book table has one row per block in which anything in the 25 levels changed, as wide columns bid_px_1 … ask_sz_25 with the resting-order count per level in bid_n_* / ask_n_*. Sizes are in coin units.
import pandas as pd# the free sample day, pulled from /catalog/hyperliquid and unzippedbook = pd.read_parquet("hl_l2/date=2026-08-12/BTC/l2.parquet") # 25-level book, a row per block it changedtrades = pd.read_parquet("hl_trades/date=2026-08-12/BTC/trades.parquet") # every fill: taker side, wallet, feetop = pd.read_parquet("hl_l1/date=2026-08-12/BTC/l1.parquet") # top of book, every change# msg_seq is the block height and orders everything; ts_src_ms is block timebook = book.sort_values("msg_seq")book["ts"] = pd.to_datetime(book.ts_src_ms, unit="ms", utc=True)book["mid"] = (book.best_bid + book.best_ask) / 2# or every coin and day in the pull at once; date comes from the pathbooks = pd.read_parquet("hl_l2", columns=["coin", "msg_seq", "ts_src_ms", "best_bid", "best_ask"])
Full column reference and coverage caveats in the Hyperliquid schema docs.
Pricing a market order is then arithmetic on each row: take dollars from each level until the order is filled, and divide. Because rows are irregular (a busy second has dozens, a quiet one none), weight each book state by how long it was live, so the statistics answer "what would this order have cost at a random moment of the day". Where the 25 levels hold less than the order, the result is NaN rather than a guess.
# hl_slippage.py (core) — the full script is linked aboveimport numpy as npdef walk(px, sz, usd):"""Average fill price of a market order for `usd` notional, taking levelsbest-first (px/sz: rows x 25). NaN where the 25 levels hold less."""cost = px * sz # $ resting at each levelcum = np.nancumsum(cost, axis=1)take = np.minimum(cost, np.maximum(usd - (cum - cost), 0)) # $ taken per levelqty = np.nansum(take / px, axis=1)with np.errstate(divide="ignore", invalid="ignore"):return np.where(cum[:, -1] >= usd, usd / qty, np.nan)levels = range(1, 26)ask_px = book[[f"ask_px_{i}" for i in levels]].to_numpy()ask_sz = book[[f"ask_sz_{i}" for i in levels]].to_numpy() # size in coin unitsmid = book.mid.to_numpy()live_ms = book.ts_src_ms.shift(-1).sub(book.ts_src_ms).fillna(0).to_numpy() # time-weightvwap = walk(ask_px, ask_sz, 1_000_000)slip_bps = (vwap / mid - 1) * 1e4 # worse than mid, per book stateprint("fits in 25 levels:", live_ms[~np.isnan(vwap)].sum() / live_ms.sum())
| market order | fits in 25 levels | vs mid · median | vs mid · p90 | vs touch · median |
|---|---|---|---|---|
| buy $10,000 | 100.0% | 0.08 | 0.08 | 0.00 |
| sell $10,000 | 100.0% | 0.08 | 0.08 | 0.00 |
| buy $100,000 | 100.0% | 0.08 | 0.48 | 0.00 |
| sell $100,000 | 100.0% | 0.08 | 0.46 | 0.00 |
| buy $1,000,000 | 100.0% | 0.35 | 1.31 | 0.27 |
| sell $1,000,000 | 100.0% | 0.29 | 1.26 | 0.21 |
| market order | fits in 25 levels | vs mid · median | vs mid · p90 | vs touch · median |
|---|---|---|---|---|
| buy $10,000 | 100.0% | 0.33 | 1.37 | 0.21 |
| sell $10,000 | 100.0% | 0.32 | 1.41 | 0.19 |
| buy $100,000 | 96.4% | 2.20 | 3.32 | 2.08 |
| sell $100,000 | 97.4% | 2.37 | 3.44 | 2.26 |
| buy $1,000,000 | 0.9% | — | — | — |
| sell $1,000,000 | 0.4% | — | — | — |
2026-08-12 UTC · slippage in bps, average fill price worse than the reference · time-weighted over the 23.8 hours in the day's folder · "fits" is the share of the day the 25 levels could absorb the whole order; a $1M HYPE order fit too rarely for its cost to mean anything, so it is left blank.
Read it against the assumptions most backtests make. On BTC, filling at the mid is off by half a tick up to about $200k, and a $1M order costs 0.35 bps at the median and 1.3 bps one moment in ten. The dashed line is the bigger number: the median taker fee on the same day's tape was 4.3 bps, so on BTC the fee, not the book, decides whether a short-horizon strategy survives. On HYPE the book is the problem: a $100k order walks deep into the visible 25 levels and pays 2.2 bps at the median, and by $200k the visible book is too thin for more than half the day.
# every taker order of the day, rebuilt from its fills: one order id, one blockt = trades.assign(usd=trades.price * trades["size"])orders = t.groupby(["oid", "msg_seq"]).agg(side=("side", "first"), usd=("usd", "sum"),prices=("price", "nunique"), fee=("fee", "sum"))walked = orders[orders.prices > 1]print(f"{len(walked) / len(orders):.1%} of taker orders filled at more than one price,")print(f"{walked.usd.sum() / orders.usd.sum():.1%} of taker notional")
The medians above describe an ordinary moment. The expensive moments are the fast ones, and they are where a candle backtest books its best trades. At 12:30 UTC on the sample day BTC fell 34.8 bps in a minute, with a 52.6 bps high-to-low range inside it. The ask side's 25 levels, which held $8.7M at the median that day, dropped as low as $196k; the spread widened from one tick to 5.45 bps; and a $1M market buy that costs 0.35 bps at a typical moment cost 0.76 bps at that minute's median and 9.41 bps at its worst instant, and did not fit in the visible book for 3.8% of the minute.
HYPE's fastest minute came at 14:36 UTC, up 52.2 bps. Its 25 levels thinned to about $71k a side from a median near $195k, and a $100k buy that costs 2.2 bps at the median cost up to 34.9 bps at the worst instant of that minute. A backtest priced off candles never sees either number.
The sample day covers every coin on the venue, not just these two. Pull the ones you trade for 2026-08-12 on a free account, and the script runs on them unchanged: python hl_slippage.py . SOL ETH.