Overview
The Market Depth channel provides real-time order book data with support for both one-time queries and subscription-based updates.
Update Behavior
After successful subscription, the server sends a full snapshot of the order book as the first depth_update message. All subsequent messages are incremental updates containing only the changes since the last update.
Public depth updates exclude Retail Price Improvement (RPI) orders. The depth_update stream includes only regular order book liquidity.Private active order endpoints and private WebSocket streams return RPI orders. The exchange UI order book (web and mobile) displays RPI orders.
Understanding Updates
- First message (
past_update_id is absent): Full order book snapshot with all price levels
- Subsequent messages (
past_update_id is present): Only changed price levels
- Removed levels: Shown as
[price, "0"] - when amount is “0”, remove the price level from the local order book
If 10 seconds elapse without any order book changes, the server pushes a full snapshot again as a keepalive mechanism.
Maintaining a Local Order Book
To maintain an accurate local order book, follow the algorithm below:
- Subscribe to depth updates
- Receive the first message (full snapshot) - initialize the order book
- For each incremental update:
- If amount is “0” → remove the price level
- If amount is not “0” → update existing level or insert new level at the correct sorted position
- Keep order book sorted: asks ascending, bids descending
- Truncate to the desired limit after each update
Code Examples
type IDepth = [string, string];
interface OrderBook {
asks: IDepth[];
bids: IDepth[];
}
const ws = new WebSocket("wss://api.whitebit.com/ws");
const orderBook: OrderBook = { asks: [], bids: [] };
const LIMIT = 100;
ws.addEventListener("open", () => {
ws.send(
JSON.stringify({
id: 1,
method: "depth_subscribe",
params: ["ETH_BTC", LIMIT, "0", true]
}),
);
});
ws.addEventListener("message", (event: MessageEvent) => {
const message = JSON.parse(event.data.toString());
if (message.method === "depth_update") {
const updateData = message.params[0] as Partial<OrderBook & { past_update_id?: number }>;
const isFirstMessage = !updateData.past_update_id;
if (isFirstMessage) {
// First message or keepalive snapshot is a full snapshot - replace order book
orderBook.asks = updateData.asks ?? [];
orderBook.bids = updateData.bids ?? [];
} else {
// Subsequent messages are incremental updates
applyUpdates(orderBook.asks, updateData.asks, "ask");
applyUpdates(orderBook.bids, updateData.bids, "bid");
truncateOrderBook(orderBook.asks);
truncateOrderBook(orderBook.bids);
}
}
});
function applyUpdates(orderBookSide: IDepth[], updates: IDepth[] | undefined, side: "ask" | "bid") {
if (updates === undefined) return;
for (const [price, amount] of updates) {
// Find the index of an entry in orderBookSide matching the given price.
const priceIndex = orderBookSide.findIndex((level) => level[0] === price);
// If the amount is '0', remove the price level from the orderBookSide.
if (amount === "0") {
if (priceIndex !== -1) {
// Remove the existing price level since the amount is '0'.
orderBookSide.splice(priceIndex, 1);
}
} else {
// If the amount is not '0', either update the existing price level or add a new one.
if (priceIndex === -1) {
// Find the position where the new price level should be inserted.
const insertIndex = orderBookSide.findIndex((level) =>
side === "ask" ? level[0] > price : level[0] < price
);
if (insertIndex === -1) {
// Add to the end if there's no higher price level.
orderBookSide.push([price, amount]);
} else {
// Insert at the correct sorted position.
orderBookSide.splice(insertIndex, 0, [price, amount]);
}
} else {
// Update the amount for the existing price level.
orderBookSide[priceIndex][1] = amount;
}
}
}
}
function truncateOrderBook(orderBookSide: IDepth[]) {
if (orderBookSide.length > LIMIT) {
// Only truncate if the length exceeds the LIMIT
orderBookSide.splice(LIMIT);
}
}