Backtest Hyperliquid data as a fixed experiment, not a loose collection of rows. Freeze the venue family, symbol, UTC window, route families, schema version, and coverage check before the strategy sees a value.
Core rule: pull one bounded window, record its response shape and request IDs, normalize timestamps to UTC, model execution costs, and keep one later period untouched until the strategy is locked.
Pull one bounded window
curl "https://api.0xarchive.io/v1/hyperliquid/trades/BTC?start=1767225600000&end=1767226200000&limit=100" \
-H "X-API-Key: $OXARCHIVE_API_KEY"
The request uses Unix milliseconds and a small result bound. Keep the returned meta.next_cursor chain, meta.request_id, and the exact query beside the output file.
Select the data
| Input | Use it when | Common failure |
|---|---|---|
| Trades | The strategy reacts to executed price, size, side, and direction | Assuming every signal fills at the next trade |
| L2 depth | Spread, displayed depth, and price impact are part of the rule | Using one snapshot as a continuous book |
| L4 depth | Queue position, resting order identity, or reconstruction matters | Calling lifecycle events a resting book |
| Candles | The rule is bar-based and does not need event order | Using a candle close with look-ahead from the same bar |
| Funding | Carry changes the return or position cost | Joining the first funding value after the decision |
| Open interest | Positioning regime is a feature or filter | Mixing snapshot time with trade time without a join rule |
| Liquidations | Forced-flow events are a signal or a mechanism to study | Treating projected levels or pending triggers as completed fills |
Coverage is route, symbol, and data-type specific. Check the catalog and status for the selected family before using an empty page as evidence that no events existed.
Assemble the pages
This bounded Python pull follows cursors, records request IDs, preserves the returned schema, normalizes timestamps, and reports gaps in the event stream.
import datetime as dt
import json
import os
from pathlib import Path
import requests
BASE = "https://api.0xarchive.io"
SYMBOL = "BTC"
START = 1767225600000
END = 1767226200000
LIMIT = 1000
URL = f"{BASE}/v1/hyperliquid/trades/{SYMBOL}"
headers = {"X-API-Key": os.environ["OXARCHIVE_API_KEY"]}
params = {"start": START, "end": END, "limit": LIMIT}
rows = []
request_ids = []
seen_cursors = set()
for page_number in range(20):
response = requests.get(URL, headers=headers, params=params, timeout=30)
response.raise_for_status()
envelope = response.json()
if envelope.get("success") is not True:
raise RuntimeError("API response was not successful")
meta = envelope.get("meta") or {}
page_rows = envelope.get("data") or []
rows.extend(page_rows)
if meta.get("request_id"):
request_ids.append(meta["request_id"])
cursor = meta.get("next_cursor")
if not cursor:
break
if cursor in seen_cursors:
raise RuntimeError("cursor repeated")
seen_cursors.add(cursor)
params["cursor"] = cursor
else:
raise RuntimeError("page bound reached")
def parse_timestamp(value):
if value is None:
return None
if isinstance(value, (int, float)):
return dt.datetime.fromtimestamp(value / 1000, tz=dt.timezone.utc)
return dt.datetime.fromisoformat(str(value).replace("Z", "+00:00")).astimezone(dt.timezone.utc)
times = [parse_timestamp(row.get("timestamp")) for row in rows]
missing_timestamps = sum(value is None for value in times)
ordered = [value for value in times if value is not None]
non_monotonic = sum(right < left for left, right in zip(ordered, ordered[1:]))
identity = [
(row.get("timestamp"), row.get("trade_id"), row.get("order_id"))
for row in rows
]
duplicate_rows = len(identity) - len(set(identity))
manifest = {
"symbol": SYMBOL,
"start_ms": START,
"end_ms": END,
"schema": sorted({key for row in rows for key in row}),
"rows": len(rows),
"pages": len(request_ids),
"request_ids": len(request_ids),
"missing_timestamps": missing_timestamps,
"non_monotonic_timestamps": non_monotonic,
"duplicate_rows": duplicate_rows,
"gap_policy": "inspect missing or non-monotonic intervals; do not interpolate event rows",
}
Path("btc-trades-manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
Path("btc-trades.jsonl").write_text("\n".join(json.dumps(row) for row in rows) + "\n")
For event data, do not infer a regular interval from row spacing. If the selected route has a missing interval or the returned sequence cannot be resolved, record it, stop or rebuild, and never interpolate market events into a complete-looking file.
Model the costs
Fees
Use the returned fee and fee-token fields when they are present. If the route does not carry a fee field for the selected venue family, store the fee schedule and its effective time beside the run. Do not subtract a guessed rate from every fill.
Funding
Join each position to the last funding observation at or before the decision time. Keep the funding timestamp and interval in the feature table. A later observation is look-ahead.
Slippage and depth
Use the order book that was observable at the decision time. L2 gives aggregated price levels. L4 gives resting orders and queue detail. Compare the simulated fill with the mark, spread, consumed depth, and resulting trade tape. A candle close is not an execution model.
Latency
Separate signal time, request time, order-send time, venue acceptance, matching, and data-arrival time. Run the strategy with a bounded latency assumption and keep the assumption in the manifest. Do not make a zero-latency fill look like a networked order.
Split the experiment
| Window | Role | Rule |
|---|---|---|
| Development | Feature design and strategy selection | May be read repeatedly |
| Validation | Parameter and implementation checks | Use after the strategy is specified |
| Holdout | Final estimate of out-of-sample behavior | Keep untouched until the decision is made |
Split by time, not by randomly shuffled rows. Keep the route, symbol, schema, cost assumptions, and gap policy identical across the three windows. If the holdout needs a new rule, it is no longer a holdout.
Native and archived data
The native Hyperliquid API returns the venue's own response. 0xArchive adds bounded historical routes, cursor envelopes, request IDs, coverage checks, replay guidance, and file-oriented exports. Compare the same symbol, UTC window, route grain, and field semantics before choosing a source. A matching row count is not enough. Compare timestamps, prices, sizes, side codes, missing values, and the treatment of empty pages.
Use the native rate-limit guidance when you collect directly. Use Historical Data API for route contracts and Point-in-time backtesting for reproducibility. The WebSocket backtesting reference covers replay when event order matters.
References
For depth, use the Hyperliquid order-book guide and the order-book API reference. For completed forced executions, use the liquidation guide and the liquidation API reference. Compare product delivery on the Hyperliquid data API, then check catalog, pricing, and status.
