Depth of Market (DOM) Visualization Development
DOM (Depth of Market), also known as Level 2 data, is a visualization of the entire order book, not just the best price. Professional traders read DOM like a book: they see walls of liquidity, volume absorption, spoofing. A well-implemented DOM is one of the key arguments for attracting professionals to an exchange.
DOM Structure
DOM displays two columns: bid (buys) and ask (sells) with aggregated volumes at each price level.
BID ASK
Volume Price Price Volume
0.5 42,100 | 42,101 1.2
1.8 42,095 | 42,102 0.7
3.2 42,090 | 42,105 4.5 ← wall
0.4 42,085 | 42,110 0.9
2.1 42,080 | 42,115 1.1
A "wall" (wall) — an abnormally large volume at a level — often indicates a support/resistance zone. Traders track how these volumes appear, change, and disappear.
WebSocket Update Implementation
DOM requires minimal latency. Updates via WebSocket diff, client maintains local copy:
interface DOMState {
bids: Map<string, string>; // price -> size
asks: Map<string, string>;
sequence: number;
}
class DOMManager {
private state: DOMState = { bids: new Map(), asks: new Map(), sequence: 0 };
private ws: WebSocket;
async initialize(pair: string) {
// 1. Load snapshot
const snap = await fetch(`/api/v1/markets/${pair}/orderbook?depth=100`).then(r => r.json());
snap.bids.forEach(([p, s]: string[]) => this.state.bids.set(p, s));
snap.asks.forEach(([p, s]: string[]) => this.state.asks.set(p, s));
this.state.sequence = snap.sequence;
// 2. Subscribe to diff updates
this.ws = new WebSocket(`wss://api.exchange.com/ws`);
this.ws.send(JSON.stringify({ op: 'subscribe', channel: `orderbook.${pair}.100` }));
this.ws.onmessage = (e) => this.applyUpdate(JSON.parse(e.data));
}
private applyUpdate(msg: OrderBookDiff) {
if (msg.seq !== this.state.sequence + 1) {
this.reinitialize(); // gap — need new snapshot
return;
}
msg.bids.forEach(([p, s]: string[]) => {
if (s === '0') this.state.bids.delete(p);
else this.state.bids.set(p, s);
});
msg.asks.forEach(([p, s]: string[]) => {
if (s === '0') this.state.asks.delete(p);
else this.state.asks.set(p, s);
});
this.state.sequence = msg.seq;
this.notifyRenderers();
}
// Returns top N levels in desired format
getTopLevels(depth: number = 20) {
const bids = [...this.state.bids.entries()]
.map(([p, s]) => [parseFloat(p), parseFloat(s)] as [number, number])
.sort((a, b) => b[0] - a[0])
.slice(0, depth);
const asks = [...this.state.asks.entries()]
.map(([p, s]) => [parseFloat(p), parseFloat(s)] as [number, number])
.sort((a, b) => a[0] - b[0])
.slice(0, depth);
return { bids, asks };
}
}
DOM Component Rendering
High-frequency DOM updates (up to 20–50 per second on active pairs) require optimized rendering. Using standard React state with re-render on each update will cause performance issues.
import { useRef, useCallback } from 'react';
// Direct DOM manipulation for hot path
const DOMRow = React.memo(({ price, size, total, maxTotal, side, highlight }: RowProps) => {
const rowRef = useRef<HTMLDivElement>(null);
// Update DOM directly without React re-render
const update = useCallback((newSize: string, newTotal: number) => {
if (!rowRef.current) return;
const sizeEl = rowRef.current.querySelector('.size');
const depthEl = rowRef.current.querySelector('.depth-bar') as HTMLElement;
if (sizeEl) sizeEl.textContent = newSize;
if (depthEl) depthEl.style.width = `${(newTotal / maxTotal) * 100}%`;
}, [maxTotal]);
// Flash animation on change
const flash = useCallback((direction: 'up' | 'down') => {
rowRef.current?.classList.add(`flash-${direction}`);
setTimeout(() => rowRef.current?.classList.remove(`flash-${direction}`), 300);
}, []);
return (
<div ref={rowRef} className={`dom-row ${side}`}>
<div className="depth-bar" style={{ width: `${(total/maxTotal)*100}%` }} />
<span className="price">{formatPrice(price)}</span>
<span className="size">{formatSize(size)}</span>
<span className="total">{formatSize(total)}</span>
</div>
);
});
Professional DOM Visual Features
Change Highlighting
// Detect changes between updates
function diffDOMStates(prev: DOMLevel[], curr: DOMLevel[]) {
const changes: Map<string, 'increased' | 'decreased' | 'new' | 'removed'> = new Map();
const prevMap = new Map(prev.map(l => [l.price, l.size]));
const currMap = new Map(curr.map(l => [l.price, l.size]));
for (const [price, size] of currMap) {
const prevSize = prevMap.get(price);
if (!prevSize) changes.set(price, 'new');
else if (size > prevSize) changes.set(price, 'increased');
else if (size < prevSize) changes.set(price, 'decreased');
}
for (const price of prevMap.keys()) {
if (!currMap.has(price)) changes.set(price, 'removed');
}
return changes;
}
Tick Grouping
// User switches grouping: 1, 5, 10, 25, 100
function groupByTick(levels: DOMLevel[], tickSize: number): DOMLevel[] {
const grouped = new Map<number, number>();
for (const { price, size } of levels) {
const bucket = Math.floor(price / tickSize) * tickSize;
grouped.set(bucket, (grouped.get(bucket) ?? 0) + size);
}
return [...grouped.entries()]
.map(([price, size]) => ({ price, size }))
.sort((a, b) => b.price - a.price);
}
Cumulative Volume Visualization
Cumulative volume shows total liquidity up to each level — you can see how deep the book is:
function addCumulative(levels: DOMLevel[]): DOMLevelWithCum[] {
let cumulative = 0;
return levels.map(level => {
cumulative += level.size;
return { ...level, cumulative };
});
}
Performance
On active pairs (BTC/USDT on major exchanges) DOM can update 10–50 times/sec. Constraints:
- Throttle updates: no more than 10 renders/sec for DOM (human eye doesn't perceive faster)
- Virtual scrolling: if showing > 50 levels — FlashList or react-window
- Canvas rendering: for maximum performance can render DOM to Canvas instead of HTML
// Throttle rendering to 10 fps
const throttledRender = useCallback(
throttle((domData: DOMData) => {
setDisplayData(domData);
}, 100), // 100ms = 10 fps
[]
);
Development of DOM visualizer with real-time updates, tick grouping and change highlighting: 3–5 weeks.







