Last updated: 2026-04-28 | Reading time: 12 minutes | Technical complexity: Intermediate

Introduction

When I was building a high-frequency arbitrage bot last quarter, I spent three frustrating weeks trying to reconstruct historical order book snapshots from Hyperliquid's websocket streams. The real-time data was abundant, but accessing clean historical order book data for backtesting felt like pulling teeth. That's when I discovered Tardis.dev's comprehensive market data relay—and the integration turned out to be remarkably straightforward once you understand the data structure.

This tutorial walks you through the complete workflow for retrieving Hyperliquid historical order book data using the Tardis API, with production-ready Python code that you can copy-paste and run immediately. Whether you're building a backtesting framework, training a market microstructure model, or analyzing liquidity patterns, by the end of this guide you'll have a fully functional data pipeline fetching order book snapshots with sub-second granularity.

What is Hyperliquid and Why Order Book Data Matters

Hyperliquid is a high-performance decentralized perpetual futures exchange that has gained significant traction among algorithmic traders. Unlike centralized exchanges, Hyperliquid offers on-chain settlement with CEX-level speed, making it attractive for strategies that require both decentralization and low latency.

The order book is the foundation of market microstructure analysis. It contains:

For backtesting purposes, historical order book data allows you to replay market conditions with precise liquidity information—something that simple OHLCV candles simply cannot provide.

Tardis.dev: The Data Relay Layer

Tardis.dev (note: HolySheep AI partners with Tardis for crypto market data relay) provides normalized, real-time and historical market data from over 40 exchanges including Binance, Bybit, OKX, Deribit, and Hyperliquid. The service handles the complexity of exchange-specific WebSocket protocols, message normalization, and data storage, delivering clean, consistent data through a unified API.

Key advantages of using Tardis for Hyperliquid data:

Prerequisites and Setup

Before diving into the code, ensure you have:

For production AI workloads that consume this market data, consider using HolySheep AI as your inference layer—we offer sub-50ms latency at $1 per dollar (saving 85%+ versus ¥7.3 pricing), with WeChat and Alipay payment support for convenient onboarding.

Python Code: Complete Integration Examples

Example 1: Fetching Historical Order Book Snapshots

#!/usr/bin/env python3
"""
Hyperliquid Order Book Data Fetcher using Tardis.dev API
Handles both REST polling and real-time streaming modes
"""

import requests
import pandas as pd
import time
from datetime import datetime, timedelta
from typing import Dict, List, Optional

============================================================

CONFIGURATION

============================================================

TARDIS_API_KEY = "YOUR_TARDIS_API_KEY" # Get from https://tardis.dev BASE_URL = "https://api.tardis.dev/v1"

Hyperliquid-specific exchange ID on Tardis

EXCHANGE_ID = "hyperliquid"

Trading pair configuration

SYMBOL = "BTC-PERP" # Hyperliquid perpetual contract class HyperliquidOrderBookFetcher: """Fetches and processes Hyperliquid historical order book data.""" def __init__(self, api_key: str): self.api_key = api_key self.headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } def get_available_symbols(self) -> List[Dict]: """Retrieve all available trading symbols for Hyperliquid.""" url = f"{BASE_URL}/exchanges/{EXCHANGE_ID}/symbols" response = requests.get(url, headers=self.headers) response.raise_for_status() return response.json() def fetch_order_book_snapshot( self, symbol: str, from_ts: int, to_ts: int, limit: int = 100 ) -> pd.DataFrame: """ Fetch historical order book snapshots within a time range. Args: symbol: Trading pair (e.g., "BTC-PERP") from_ts: Start timestamp in milliseconds to_ts: End timestamp in milliseconds limit: Maximum number of snapshots to return (max 1000) Returns: DataFrame with order book snapshots containing: - timestamp: Unix timestamp in milliseconds - asks: List of [price, quantity] pairs - bids: List of [price, quantity] pairs - spread: Best ask minus best bid - mid_price: Average of best bid and ask """ url = f"{BASE_URL}/exchanges/{EXCHANGE_ID}/orderbooks" params = { "symbol": symbol, "from": from_ts, "to": to_ts, "limit": limit, "format": "json" } response = requests.get(url, params=params, headers=self.headers) response.raise_for_status() raw_data = response.json() if not raw_data or len(raw_data) == 0: return pd.DataFrame() # Parse and structure the order book data snapshots = [] for entry in raw_data: snapshot = { "timestamp": entry.get("timestamp"), "date": pd.to_datetime(entry["timestamp"], unit="ms"), "asks": entry.get("asks", []), "bids": entry.get("bids", []), } # Calculate derived metrics if snapshot["asks"] and snapshot["bids"]: best_ask = float(snapshot["asks"][0][0]) best_bid = float(snapshot["bids"][0][0]) snapshot["best_ask"] = best_ask snapshot["best_bid"] = best_bid snapshot["spread"] = best_ask - best_bid snapshot["mid_price"] = (best_ask + best_bid) / 2 snapshot["spread_bps"] = (snapshot["spread"] / snapshot["mid_price"]) * 10000 # Calculate weighted mid price (order quantity weighted) total_bid_qty = sum(float(b[1]) for b in snapshot["bids"][:5]) total_ask_qty = sum(float(a[1]) for a in snapshot["asks"][:5]) snapshot["bid_depth_5"] = total_bid_qty snapshot["ask_depth_5"] = total_ask_qty snapshot["order_imbalance"] = (total_bid_qty - total_ask_qty) / (total_bid_qty + total_ask_qty) snapshots.append(snapshot) return pd.DataFrame(snapshots) def fetch_and_save_daily_data( self, symbol: str, date: str, output_path: str = "order_book_data.parquet" ) -> pd.DataFrame: """ Convenience method: Fetch full day of order book data at 1-minute intervals. Args: symbol: Trading pair date: Date string in format "YYYY-MM-DD" output_path: Path to save Parquet file Returns: DataFrame with all snapshots for the day """ # Parse date and convert to timestamps target_date = datetime.strptime(date, "%Y-%m-%d") from_ts = int(target_date.timestamp() * 1000) to_ts = int((target_date + timedelta(days=1)).timestamp() * 1000) print(f"Fetching {symbol} order book data for {date}...") all_snapshots = [] # Fetch in chunks (Tardis returns max 1000 per request) current_from = from_ts while current_from < to_ts: chunk_to = min(current_from + (60 * 60 * 1000), to_ts) # 1 hour chunks df = self.fetch_order_book_snapshot( symbol=symbol, from_ts=current_from, to_ts=chunk_to, limit=1000 ) if len(df) > 0: all_snapshots.append(df) print(f" Retrieved {len(df)} snapshots from {df['date'].min()} to {df['date'].max()}") current_from = chunk_to time.sleep(0.1) # Rate limiting if all_snapshots: combined_df = pd.concat(all_snapshots, ignore_index=True) combined_df.to_parquet(output_path) print(f"Saved {len(combined_df)} snapshots to {output_path}") return combined_df else: print("No data retrieved for the specified date.") return pd.DataFrame()

============================================================

USAGE EXAMPLE

============================================================

if __name__ == "__main__": # Initialize fetcher fetcher = HyperliquidOrderBookFetcher(api_key=TARDIS_API_KEY) # Check available symbols symbols = fetcher.get_available_symbols() print(f"Available Hyperliquid symbols: {len(symbols)}") for sym in symbols[:5]: print(f" - {sym.get('symbol')} ({sym.get('base')}/{sym.get('quote')})") # Fetch a specific day's data df = fetcher.fetch_and_save_daily_data( symbol="BTC-PERP", date="2026-04-20", output_path="hyperliquid_btc_perp_2026_04_20.parquet" ) if len(df) > 0: print("\n=== Data Summary ===") print(f"Total snapshots: {len(df)}") print(f"Time range: {df['date'].min()} to {df['date'].max()}") print(f"Average spread: {df['spread_bps'].mean():.2f} bps") print(f"Average order imbalance: {df['order_imbalance'].mean():.4f}") # Display sample data print("\n=== Sample Data (first 5 rows) ===") display_cols = ["date", "best_bid", "best_ask", "spread", "mid_price", "order_imbalance"] print(df[display_cols].head())

Example 2: Real-Time Order Book Streaming

#!/usr/bin/env python3
"""
Real-time Hyperliquid Order Book Streaming Client
Uses Tardis WebSocket API for live data
"""

import json
import asyncio
import aiohttp
from typing import Callable, Optional
from dataclasses import dataclass
from datetime import datetime

For production use, integrate with your HolySheep AI workflow

Sign up at https://www.holysheep.ai/register for sub-50ms inference

@dataclass class OrderBookLevel: """Represents a single price level in the order book.""" price: float quantity: float @classmethod def from_list(cls, data: list) -> "OrderBookLevel": return cls(price=float(data[0]), quantity=float(data[1])) class OrderBookState: """Maintains current order book state with efficient updates.""" def __init__(self, symbol: str): self.symbol = symbol self.bids: dict[float, float] = {} # price -> quantity self.asks: dict[float, float] = {} self.last_update_ts: Optional[int] = None self.message_count: int = 0 def apply_snapshot(self, bids: list, asks: list, timestamp: int): """Apply a full order book snapshot.""" self.bids = {float(b[0]): float(b[1]) for b in bids} self.asks = {float(a[0]): float(a[1]) for a in asks} self.last_update_ts = timestamp self.message_count += 1 def apply_delta(self, updates: dict, timestamp: int): """Apply incremental order book update.""" self.last_update_ts = timestamp self.message_count += 1 # Process bid updates if "b" in updates: # bids for level in updates["b"]: price, qty = float(level[0]), float(level[1]) if qty == 0: self.bids.pop(price, None) else: self.bids[price] = qty # Process ask updates if "a" in updates: # asks for level in updates["a"]: price, qty = float(level[0]), float(level[1]) if qty == 0: self.asks.pop(price, None) else: self.asks[price] = qty @property def best_bid(self) -> Optional[float]: return max(self.bids.keys()) if self.bids else None @property def best_ask(self) -> Optional[float]: return min(self.asks.keys()) if self.asks else None @property def mid_price(self) -> Optional[float]: if self.best_bid and self.best_ask: return (self.best_bid + self.best_ask) / 2 return None @property def spread_bps(self) -> Optional[float]: if self.mid_price and self.spread: return (self.spread / self.mid_price) * 10000 return None @property def spread(self) -> Optional[float]: if self.best_bid and self.best_ask: return self.best_ask - self.best_bid return None def get_top_levels(self, n: int = 10) -> dict: """Get top N levels from both sides.""" sorted_bids = sorted(self.bids.items(), reverse=True)[:n] sorted_asks = sorted(self.asks.items())[:n] return { "bids": [(p, q) for p, q in sorted_bids], "asks": [(p, q) for p, q in sorted_asks] } def get_order_imbalance(self, depth: int = 5) -> float: """Calculate order imbalance ratio.""" top_bids = sorted(self.bids.keys(), reverse=True)[:depth] top_asks = sorted(self.asks.keys())[:depth] bid_volume = sum(self.bids[p] for p in top_bids) ask_volume = sum(self.asks[p] for p in top_asks) if bid_volume + ask_volume == 0: return 0.0 return (bid_volume - ask_volume) / (bid_volume + ask_volume) class HyperliquidStreamClient: """ Real-time streaming client for Hyperliquid order book data via Tardis WebSocket. """ WS_BASE_URL = "wss://api.tardis.dev/v1/stream" def __init__(self, api_key: str): self.api_key = api_key self.session: Optional[aiohttp.ClientSession] = None self.websocket: Optional[aiohttp.ClientWebSocketResponse] = None self.order_books: dict[str, OrderBookState] = {} self.is_connected: bool = False self._run_task: Optional[asyncio.Task] = None async def connect(self, symbols: list[str]): """Establish WebSocket connection and subscribe to symbols.""" self.session = aiohttp.ClientSession() # Construct subscription message subscribe_msg = { "method": "subscribe", "params": { "channel": "orderbook", "exchange": "hyperliquid", "symbols": symbols }, "id": 1 } try: self.websocket = await self.session.ws_connect( self.WS_BASE_URL, headers={"Authorization": f"Bearer {self.api_key}"} ) await self.websocket.send_json(subscribe_msg) self.is_connected = True # Initialize order book state for each symbol for symbol in symbols: self.order_books[symbol] = OrderBookState(symbol) print(f"Connected to Tardis WebSocket, subscribed to: {symbols}") except aiohttp.ClientError as e: print(f"Failed to connect: {e}") self.is_connected = False raise async def _message_handler(self, callback: Optional[Callable] = None): """Process incoming WebSocket messages.""" async for msg in self.websocket: if msg.type == aiohttp.WSMsgType.TEXT: try: data = json.loads(msg.data) await self._process_message(data, callback) except json.JSONDecodeError: print(f"Invalid JSON: {msg.data}") elif msg.type == aiohttp.WSMsgType.ERROR: print(f"WebSocket error: {msg.data}") break elif msg.type == aiohttp.WSMsgType.CLOSED: print("WebSocket connection closed") break async def _process_message(self, data: dict, callback: Optional[Callable]): """Process and route incoming data messages.""" msg_type = data.get("type") or data.get("channel") if msg_type == "subscribed": print(f"Subscription confirmed: {data}") return if msg_type == "orderbook": payload = data.get("data", {}) symbol = payload.get("symbol") timestamp = data.get("timestamp", 0) if symbol not in self.order_books: self.order_books[symbol] = OrderBookState(symbol) ob = self.order_books[symbol] # Apply update (Tardis sends both snapshots and deltas) if "snapshot" in payload: ob.apply_snapshot( payload["snapshot"]["bids"], payload["snapshot"]["asks"], timestamp ) elif "update" in payload: ob.apply_delta(payload["update"], timestamp) # Invoke callback with current state if callback: await callback(symbol, ob, timestamp) async def start_streaming( self, symbols: list[str], callback: Optional[Callable] = None, duration_seconds: Optional[int] = None ): """ Start streaming order book data. Args: symbols: List of trading symbols to subscribe to callback: Async function called on each update: func(symbol, orderbook, timestamp) duration_seconds: Optional streaming duration limit """ await self.connect(symbols) async def default_callback(symbol: str, ob: OrderBookState, ts: int): """Default logging callback for demonstration.""" if ob.message_count % 100 == 0: # Log every 100 messages print(f"[{datetime.fromtimestamp(ts/1000):%H:%M:%S.%f}] " f"{symbol}: bid={ob.best_bid:.2f} ask={ob.best_ask:.2f} " f"spread={ob.spread_bps:.2f}bps imbalance={ob.get_order_imbalance():.4f}") handler = callback or default_callback self._run_task = asyncio.create_task(self._message_handler(handler)) if duration_seconds: await asyncio.sleep(duration_seconds) await self.stop() else: await self._run_task async def stop(self): """Gracefully close the WebSocket connection.""" self.is_connected = False if self._run_task: self._run_task.cancel() try: await self._run_task except asyncio.CancelledError: pass if self.websocket: await self.websocket.close() if self.session: await self.session.close() print("Connection closed")

============================================================

USAGE EXAMPLE: Real-time Order Imbalance Detector

============================================================

async def imbalance_detector(symbol: str, ob: OrderBookState, timestamp: int): """Detect significant order imbalances for trading signals.""" imbalance = ob.get_order_imbalance(depth=5) threshold = 0.3 # 30% imbalance threshold if abs(imbalance) > threshold: signal = "BUY" if imbalance > 0 else "SELL" print(f"\n🚨 SIGNAL: {signal}") print(f" Symbol: {symbol}") print(f" Imbalance: {imbalance:.4f} (threshold: ±{threshold})") print(f" Bid depth: {sum(ob.bids[p] for p in sorted(ob.bids.keys(), reverse=True)[:5]):.4f}") print(f" Ask depth: {sum(ob.asks[p] for p in sorted(ob.asks.keys())[:5]):.4f}") print(f" Time: {datetime.fromtimestamp(timestamp/1000)}\n") async def main(): """Example: Stream BTC-PERP order book for 60 seconds.""" client = HyperliquidStreamClient(api_key="YOUR_TARDIS_API_KEY") try: print("Starting Hyperliquid order book stream...") print("Press Ctrl+C to stop early\n") await client.start_streaming( symbols=["BTC-PERP", "ETH-PERP"], callback=imbalance_detector, duration_seconds=60 ) except KeyboardInterrupt: print("\nInterrupted by user") finally: await client.stop() if __name__ == "__main__": asyncio.run(main())

Example 3: Backtesting Integration with pandas

#!/usr/bin/env python3
"""
Backtesting Example: Order Book Imbalance Strategy on Hyperliquid Data
Combines Tardis historical data with strategy execution simulation
"""

import pandas as pd
import numpy as np
from pathlib import Path
from dataclasses import dataclass
from typing import List, Tuple
import matplotlib.pyplot as plt

Configuration

TARDIS_API_KEY = "YOUR_TARDIS_API_KEY"

Strategy parameters

IMBALANCE_THRESHOLD = 0.25 # Enter when imbalance exceeds this SPREAD_THRESHOLD_BPS = 5.0 # Minimum spread to consider trade POSITION_SIZE = 1.0 # Contracts per trade CLOSEOUT_THRESHOLD = 0.05 # Exit when imbalance reverts below this @dataclass class Trade: """Represents a single executed trade.""" entry_time: pd.Timestamp entry_price: float direction: str # "LONG" or "SHORT" size: float exit_time: Optional[pd.Timestamp] = None exit_price: Optional[float] = None pnl: Optional[float] = None def close(self, exit_time: pd.Timestamp, exit_price: float): self.exit_time = exit_time self.exit_price = exit_price if self.direction == "LONG": self.pnl = (exit_price - self.entry_price) * self.size else: self.pnl = (self.entry_price - exit_price) * self.size class OrderBookBacktester: """ Backtests strategies using historical Hyperliquid order book data. """ def __init__(self, data_path: str): self.df = pd.read_parquet(data_path) self.df = self.df.sort_values("timestamp").reset_index(drop=True) self.trades: List[Trade] = [] self.current_position: Optional[Trade] = None # Precompute features self._compute_features() def _compute_features(self): """Compute additional features for strategy decisions.""" # Rolling order imbalance self.df["imbalance_rolling"] = self.df["order_imbalance"].rolling(5).mean() # Price momentum self.df["price_change"] = self.df["mid_price"].pct_change() self.df["price_momentum"] = self.df["price_change"].rolling(10).sum() # Volume proxy (order book depth) self.df["total_depth"] = self.df["bid_depth_5"] + self.df["ask_depth_5"] # Fill NaN values self.df = self.df.fillna(method="ffill").fillna(0) def run_backtest(self) -> pd.DataFrame: """ Execute the backtest simulation. Strategy logic: 1. Enter LONG when positive imbalance exceeds threshold AND spread is wide enough 2. Enter SHORT when negative imbalance exceeds threshold AND spread is wide enough 3. Exit when imbalance reverts (approaches zero) """ print(f"Running backtest on {len(self.df)} order book snapshots...") for idx, row in self.df.iterrows(): timestamp = row["date"] mid_price = row["mid_price"] imbalance = row["imbalance_rolling"] spread_bps = row["spread_bps"] # Check if we should enter a position if self.current_position is None: # Check LONG entry if imbalance > IMBALANCE_THRESHOLD and spread_bps > SPREAD_THRESHOLD_BPS: self.current_position = Trade( entry_time=timestamp, entry_price=mid_price, direction="LONG", size=POSITION_SIZE ) print(f"[{timestamp}] LONG entered @ {mid_price:.4f}") # Check SHORT entry elif imbalance < -IMBALANCE_THRESHOLD and spread_bps > SPREAD_THRESHOLD_BPS: self.current_position = Trade( entry_time=timestamp, entry_price=mid_price, direction="SHORT", size=POSITION_SIZE ) print(f"[{timestamp}] SHORT entered @ {mid_price:.4f}") # Check if we should exit current position else: should_exit = False # Exit LONG when imbalance reverts if self.current_position.direction == "LONG": if imbalance < CLOSEOUT_THRESHOLD: should_exit = True # Exit SHORT when imbalance reverts elif self.current_position.direction == "SHORT": if imbalance > -CLOSEOUT_THRESHOLD: should_exit = True if should_exit: self.current_position.close(timestamp, mid_price) self.trades.append(self.current_position) pnl = self.current_position.pnl direction = self.current_position.direction print(f"[{timestamp}] {direction} closed @ {mid_price:.4f}, PnL: {pnl:.4f}") self.current_position = None # Close any remaining position at end if self.current_position: last_row = self.df.iloc[-1] self.current_position.close(last_row["date"], last_row["mid_price"]) self.trades.append(self.current_position) print(f"[END] Position closed @ {last_row['mid_price']:.4f}") return self._generate_results() def _generate_results(self) -> pd.DataFrame: """Generate performance metrics and trade history.""" if not self.trades: print("No trades executed during backtest period.") return pd.DataFrame() trades_df = pd.DataFrame([ { "entry_time": t.entry_time, "exit_time": t.exit_time, "direction": t.direction, "entry_price": t.entry_price, "exit_price": t.exit_price, "pnl": t.pnl, "duration_minutes": (t.exit_time - t.entry_time).total_seconds() / 60 } for t in self.trades ]) # Calculate metrics total_pnl = trades_df["pnl"].sum() winning_trades = trades_df[trades_df["pnl"] > 0] losing_trades = trades_df[trades_df["pnl"] <= 0] win_rate = len(winning_trades) / len(trades_df) * 100 avg_win = winning_trades["pnl"].mean() if len(winning_trades) > 0 else 0 avg_loss = losing_trades["pnl"].mean() if len(losing_trades) > 0 else 0 profit_factor = abs(winning_trades["pnl"].sum() / losing_trades["pnl"].sum()) if len(losing_trades) > 0 and losing_trades["pnl"].sum() != 0 else float('inf') print("\n" + "=" * 60) print("BACKTEST RESULTS") print("=" * 60) print(f"Total Trades: {len(trades_df)}") print(f"Win Rate: {win_rate:.2f}%") print(f"Total PnL: {total_pnl:.4f}") print(f"Average Win: {avg_win:.4f}") print(f"Average Loss: {avg_loss:.4f}") print(f"Profit Factor: {profit_factor:.2f}") print(f"Max Drawdown: {self._calculate_max_drawdown(trades_df):.4f}") print("=" * 60) return trades_df def _calculate_max_drawdown(self, trades_df: pd.DataFrame) -> float: """Calculate maximum drawdown from cumulative PnL.""" if len(trades_df) == 0: return 0.0 cumulative = trades_df["pnl"].cumsum() running_max = cumulative.cummax() drawdown = cumulative - running_max return drawdown.min()

============================================================

USAGE

============================================================

if __name__ == "__main__": # Load pre-fetched data (from Example 1) data_path = "hyperliquid_btc_perp_2026_04_20.parquet" if Path(data_path).exists(): backtester = OrderBookBacktester(data_path) results = backtester.run_backtest() else: print(f"Data file not found: {data_path}") print("Run Example 1 first to fetch order book data.")

Data Format Reference

Tardis.dev normalizes Hyperliquid order book data into a consistent format across all exchanges. Here's the structure you'll receive:

Field Type Description Example
timestamp integer Unix timestamp in milliseconds 1713567600000
symbol string Trading pair identifier BTC-PERP
asks array List of [price, quantity] for asks [[64250.5, 2.1], [64251.0, 0.5]]
bids array List of [price, quantity] for bids [[64249.5, 1.8], [64248.0, 3.2]]
type string Message type: "snapshot" or "update" snapshot

API Rate Limits and Quotas

Understanding Tardis API limits is crucial for production deployments:

Plan Tier Historical Data Access Requests/Minute WebSocket Connections Price (Monthly)
Free Last 30 days 60 1 $0
Starter Last 12 months 300 3 $49
Pro Full history 1000 10 $199
Enterprise Full history + custom Unlimited Unlimited Custom

Who This Tutorial Is For

This Guide is Perfect For:

This Guide is NOT For:

Pricing and ROI Analysis

When calculating the return on investment for order book data access, consider these factors:

Cost Factor Tardis.dev (Starter) Direct Exchange API HolySheep AI (for downstream processing)
Monthly Cost $49 $0 (free tier) $1 per $1 (¥1 pricing)
Data Normalization Included Custom implementation N/A
Historical Depth 12 months Varies by exchange N/A
Implementation Time 1-2 days 2-4 weeks Hours (for AI inference)
Maintenance Overhead Low High Minimal

ROI Calculation: If your development time is valued at $100/hour, building custom exchange integrations (20+ hours) versus using Tardis (4 hours) saves approximately $1,600 in development costs. The $49 monthly subscription