L2 Order Book (Level 2)
Stream aggregated order book data showing total size at each price level. The L2 book provides a consolidated view of market depth, with configurable levels and price aggregation for bandwidth optimization.
Clone our complete examples: github.com/dwellir-public/hyperliquid-orderbook-server-code-examples
How to Subscribe#
Send a subscription message to the WebSocket endpoint:
{
"method": "subscribe",
"subscription": {
"type": "l2Book",
"coin": "BTC",
"nSigFigs": 5,
"nLevels": 20
}
}
Subscription Parameters#
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
type | string | Yes | - | Must be "l2Book" |
coin | string | Yes | - | Trading pair symbol (e.g., "BTC", "ETH") |
nSigFigs | number | No | 5 | Price aggregation: 2-5 significant figures |
nLevels | number | No | 20 | Number of levels per side: 1-100 |
Understanding nSigFigs (Price Aggregation)#
The nSigFigs parameter controls how prices are rounded for aggregation:
| nSigFigs | BTC Example | Effect |
|---|---|---|
| 5 | $106,217 | Most granular - individual price points |
| 4 | $106,200 | Moderate aggregation |
| 3 | $106,000 | Coarse aggregation |
| 2 | $110,000 | Very coarse - major levels only |
Lower nSigFigs values reduce message frequency and bandwidth but sacrifice price precision.
Sample Response#
{
"channel": "l2Book",
"data": {
"coin": "BTC",
"time": 1767878802704,
"levels": [
[
{"px": "90057", "sz": "5.34526", "n": 25},
{"px": "90056", "sz": "0.01336", "n": 4},
{"px": "90055", "sz": "0.33772", "n": 5},
{"px": "90054", "sz": "0.01859", "n": 2},
{"px": "90053", "sz": "0.08936", "n": 2}
],
[
{"px": "90058", "sz": "7.46316", "n": 18},
{"px": "90059", "sz": "0.44694", "n": 5},
{"px": "90060", "sz": "0.00013", "n": 1},
{"px": "90061", "sz": "0.05576", "n": 3},
{"px": "90062", "sz": "0.9716", "n": 3}
]
]
}
}
In this example:
- Best bid: $90,057 with 5.34526 BTC across 25 orders
- Best ask: $90,058 with 7.46316 BTC across 18 orders
- Spread: $1 (approximately 1.1 basis points)
Response Field Reference#
| Field | Type | Description |
|---|---|---|
channel | string | Always "l2Book" for this subscription type |
data.coin | string | Trading pair symbol |
data.time | number | Unix timestamp in milliseconds |
data.levels | array | Two-element array: [bids, asks] |
levels[0] | array | Bid levels (buyers), sorted best (highest) to worst |
levels[1] | array | Ask levels (sellers), sorted best (lowest) to worst |
Level Object Fields#
| Field | Type | Description |
|---|---|---|
px | string | Price level |
sz | string | Total size at this price level |
n | number | Number of orders aggregated at this level |
Code Examples#
- Python
- JavaScript
- Go
#!/usr/bin/env python3
"""
L2 Order Book Stream - Aggregated price levels with spread display
"""
import asyncio
import json
import websockets
def display_orderbook(data):
"""Display the order book with spread calculation"""
if data.get("channel") != "l2Book":
return
coin = data["data"]["coin"]
levels = data["data"]["levels"]
bids = levels[0]
asks = levels[1]
print(f"\n{'='*55}")
print(f"{coin} Order Book")
print(f"{'='*55}")
# Display asks (sellers) - reverse for visual display
print("\nASKS (Sellers)")
print(f"{'Price':<15} {'Size':<15} {'Orders':<10}")
print("-" * 55)
for ask in reversed(asks[:5]):
print(f"${ask['px']:<14} {ask['sz']:<15} {ask['n']:<10}")
# Calculate and display spread
if bids and asks:
best_bid = float(bids[0]['px'])
best_ask = float(asks[0]['px'])
spread = best_ask - best_bid
spread_bps = (spread / best_bid) * 10000
print(f"\n{'─'*55}")
print(f"Spread: ${spread:.2f} ({spread_bps:.1f} bps)")
print(f"{'─'*55}\n")
# Display bids (buyers)
print("BIDS (Buyers)")
print(f"{'Price':<15} {'Size':<15} {'Orders':<10}")
print("-" * 55)
for bid in bids[:5]:
print(f"${bid['px']:<14} {bid['sz']:<15} {bid['n']:<10}")
async def stream_l2_book():
ws_url = "wss://your-instance.dwellir.com/ws"
async with websockets.connect(ws_url) as websocket:
# Subscribe to ETH L2 order book
await websocket.send(json.dumps({
"method": "subscribe",
"subscription": {
"type": "l2Book",
"coin": "ETH",
"nLevels": 10,
"nSigFigs": 5
}
}))
print("Subscribed to ETH L2 order book")
async for message in websocket:
data = json.loads(message)
display_orderbook(data)
if __name__ == "__main__":
asyncio.run(stream_l2_book())
const WebSocket = require('ws');
const wsUrl = 'wss://your-instance.dwellir.com/ws';
const ws = new WebSocket(wsUrl);
ws.on('open', () => {
// Subscribe to ETH L2 order book
ws.send(JSON.stringify({
method: 'subscribe',
subscription: {
type: 'l2Book',
coin: 'ETH',
nLevels: 10,
nSigFigs: 5
}
}));
console.log('Subscribed to ETH L2 order book\n');
});
ws.on('message', (rawData) => {
const data = JSON.parse(rawData);
if (data.channel === 'l2Book') {
displayOrderBook(data.data);
}
});
function displayOrderBook(bookData) {
const { coin, levels } = bookData;
const [bids, asks] = levels;
console.log(`\n${'='.repeat(55)}`);
console.log(`${coin} Order Book`);
console.log('='.repeat(55));
// Display asks (reversed)
console.log('\nASKS (Sellers)');
console.log(`${'Price'.padEnd(15)} ${'Size'.padEnd(15)} ${'Orders'.padEnd(10)}`);
console.log('-'.repeat(55));
asks.slice(0, 5).reverse().forEach(ask => {
console.log(`$${ask.px.padEnd(14)} ${ask.sz.padEnd(15)} ${String(ask.n).padEnd(10)}`);
});
// Calculate spread
if (bids.length && asks.length) {
const bestBid = parseFloat(bids[0].px);
const bestAsk = parseFloat(asks[0].px);
const spread = bestAsk - bestBid;
const spreadBps = (spread / bestBid) * 10000;
console.log(`\n${'-'.repeat(55)}`);
console.log(`Spread: $${spread.toFixed(2)} (${spreadBps.toFixed(1)} bps)`);
console.log('-'.repeat(55) + '\n');
}
// Display bids
console.log('BIDS (Buyers)');
console.log(`${'Price'.padEnd(15)} ${'Size'.padEnd(15)} ${'Orders'.padEnd(10)}`);
console.log('-'.repeat(55));
bids.slice(0, 5).forEach(bid => {
console.log(`$${bid.px.padEnd(14)} ${bid.sz.padEnd(15)} ${String(bid.n).padEnd(10)}`);
});
}
ws.on('error', (error) => console.error('WebSocket error:', error));
package main
import (
"encoding/json"
"fmt"
"log"
"strconv"
"github.com/gorilla/websocket"
)
type L2BookMessage struct {
Channel string `json:"channel"`
Data struct {
Coin string `json:"coin"`
Time int64 `json:"time"`
Levels [][]L2Level `json:"levels"`
} `json:"data"`
}
type L2Level struct {
Px string `json:"px"`
Sz string `json:"sz"`
N int `json:"n"`
}
func main() {
wsURL := "wss://your-instance.dwellir.com/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
log.Fatal("Connection failed:", err)
}
defer conn.Close()
// Subscribe to ETH L2 order book
subscription := map[string]interface{}{
"method": "subscribe",
"subscription": map[string]interface{}{
"type": "l2Book",
"coin": "ETH",
"nLevels": 10,
"nSigFigs": 5,
},
}
conn.WriteJSON(subscription)
fmt.Println("Subscribed to ETH L2 order book\n")
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Println("Read error:", err)
return
}
var bookMsg L2BookMessage
if err := json.Unmarshal(message, &bookMsg); err != nil {
continue
}
if bookMsg.Channel == "l2Book" {
displayOrderBook(bookMsg.Data.Coin, bookMsg.Data.Levels)
}
}
}
func displayOrderBook(coin string, levels [][]L2Level) {
if len(levels) < 2 {
return
}
bids := levels[0]
asks := levels[1]
fmt.Printf("\n%s\n%s Order Book\n%s\n",
"=======================================================",
coin,
"=======================================================")
// Calculate spread
if len(bids) > 0 && len(asks) > 0 {
bestBid, _ := strconv.ParseFloat(bids[0].Px, 64)
bestAsk, _ := strconv.ParseFloat(asks[0].Px, 64)
spread := bestAsk - bestBid
spreadBps := (spread / bestBid) * 10000
fmt.Printf("Best Bid: $%.2f | Best Ask: $%.2f | Spread: $%.2f (%.1f bps)\n\n",
bestBid, bestAsk, spread, spreadBps)
}
}
Liquidity Depth Calculator#
Calculate total liquidity within a price range:
class LiquidityAnalyzer:
def __init__(self):
self.current_book = None
def update(self, book_data):
self.current_book = book_data
def get_liquidity_within_percent(self, percent):
"""Calculate total liquidity within X% of mid price"""
if not self.current_book:
return None
levels = self.current_book["levels"]
bids, asks = levels[0], levels[1]
if not bids or not asks:
return None
best_bid = float(bids[0]['px'])
best_ask = float(asks[0]['px'])
mid_price = (best_bid + best_ask) / 2
threshold = mid_price * (percent / 100)
upper_bound = mid_price + threshold
lower_bound = mid_price - threshold
bid_liquidity = sum(
float(b['sz']) for b in bids
if float(b['px']) >= lower_bound
)
ask_liquidity = sum(
float(a['sz']) for a in asks
if float(a['px']) <= upper_bound
)
return {
'bid_liquidity': bid_liquidity,
'ask_liquidity': ask_liquidity,
'total_liquidity': bid_liquidity + ask_liquidity,
'imbalance': bid_liquidity / ask_liquidity if ask_liquidity > 0 else float('inf')
}
def get_depth_at_price(self, side, price_levels=5):
"""Get cumulative depth for top N price levels"""
if not self.current_book:
return []
levels = self.current_book["levels"]
book_side = levels[0] if side == 'bid' else levels[1]
cumulative = 0
depth = []
for level in book_side[:price_levels]:
cumulative += float(level['sz'])
depth.append({
'price': level['px'],
'size': level['sz'],
'cumulative': cumulative,
'orders': level['n']
})
return depth
Order Book Imbalance Signal#
Detect buying/selling pressure from book shape:
def calculate_imbalance(bids, asks, levels=5):
"""
Calculate order book imbalance.
Returns value from -1 (all asks) to +1 (all bids)
"""
bid_volume = sum(float(b['sz']) for b in bids[:levels])
ask_volume = sum(float(a['sz']) for a in asks[:levels])
total = bid_volume + ask_volume
if total == 0:
return 0
return (bid_volume - ask_volume) / total
def get_imbalance_signal(imbalance):
"""Convert imbalance to trading signal"""
if imbalance > 0.3:
return "STRONG_BUY"
elif imbalance > 0.1:
return "BUY"
elif imbalance < -0.3:
return "STRONG_SELL"
elif imbalance < -0.1:
return "SELL"
return "NEUTRAL"
Use Cases#
Market Making#
Monitor spread and depth to optimize quote placement:
- Spread monitoring: Track bid-ask spread for quote width
- Inventory management: Adjust quotes based on book imbalance
- Competition analysis: Count orders at each level to gauge competition
Liquidity Analysis#
Assess market depth for large order execution:
- Slippage estimation: Calculate expected price impact for order sizes
- Best execution: Find optimal order sizes and timing
- Market quality metrics: Track spread, depth, and resilience
Support/Resistance Detection#
Identify significant price levels from order concentration:
- Order clusters: Large
nvalues indicate many orders at a level - Size walls: High
szvalues suggest strong support/resistance - Level breaks: Monitor when significant levels get absorbed
Algorithmic Trading Signals#
Generate trading signals from book dynamics:
- Imbalance signals: Buy when bids dominate, sell when asks dominate
- Spread compression: Trade when spread narrows (high liquidity)
- Depth changes: Alert on sudden liquidity additions or removals
Subscription Management#
Adjusting Parameters#
Change depth or aggregation by resubscribing:
# Switch from 20 levels to 50 levels
await websocket.send(json.dumps({
"method": "unsubscribe",
"subscription": {"type": "l2Book", "coin": "BTC", "nLevels": 20}
}))
await websocket.send(json.dumps({
"method": "subscribe",
"subscription": {"type": "l2Book", "coin": "BTC", "nLevels": 50}
}))
Bandwidth Optimization#
For high-frequency updates, use lower nSigFigs:
{
"method": "subscribe",
"subscription": {
"type": "l2Book",
"coin": "BTC",
"nSigFigs": 3,
"nLevels": 10
}
}
This reduces message size and frequency while maintaining key price levels.
Get Access#
Ready to integrate real-time Hyperliquid L2 order book data?
- Volume Calculator - Estimate monthly message volume
- Contact Sales - Get your WebSocket credentials
- Dashboard - Manage your subscription
Stream institutional-grade Hyperliquid order book data with Dwellir's ultra-low latency infrastructure.