Skip to main content

How to Backtest Hyperliquid with Historical Data

9 min read

Select, pull, normalize, and validate bounded Hyperliquid data before testing on an untouched holdout window.

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

InputUse it whenCommon failure
TradesThe strategy reacts to executed price, size, side, and directionAssuming every signal fills at the next trade
L2 depthSpread, displayed depth, and price impact are part of the ruleUsing one snapshot as a continuous book
L4 depthQueue position, resting order identity, or reconstruction mattersCalling lifecycle events a resting book
CandlesThe rule is bar-based and does not need event orderUsing a candle close with look-ahead from the same bar
FundingCarry changes the return or position costJoining the first funding value after the decision
Open interestPositioning regime is a feature or filterMixing snapshot time with trade time without a join rule
LiquidationsForced-flow events are a signal or a mechanism to studyTreating 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

WindowRoleRule
DevelopmentFeature design and strategy selectionMay be read repeatedly
ValidationParameter and implementation checksUse after the strategy is specified
HoldoutFinal estimate of out-of-sample behaviorKeep 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.