Разработка визуализации DOM (Depth of Market)
DOM (Depth of Market), также известный как Level 2 данные — это визуализация всего стакана ордеров, а не только лучшей цены. Профессиональные трейдеры читают DOM как книгу: видят стены ликвидности, поглощение объёмов, спуфинг. Хорошая реализация DOM — один из ключевых аргументов для привлечения профессионалов на биржу.
Структура DOM
DOM отображает два столбца: bid (покупки) и ask (продажи) с агрегированными объёмами на каждом ценовом уровне.
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
"Стена" (wall) — аномально большой объём на уровне — часто указывает на зону поддержки/сопротивления. Трейдеры отслеживают как эти объёмы появляются, изменяются и исчезают.
Реализация WebSocket обновлений
DOM требует минимальной latency. Обновления через WebSocket diff, клиент поддерживает локальную копию:
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. Загружаем снэпшот
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. Подписываемся на 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 — нужен новый снэпшот
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();
}
// Возвращает топ N уровней в нужном формате
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 компонента
Высокочастотные обновления DOM (до 20–50 раз/сек на активных парах) требуют оптимизированного рендеринга. Использование обычного React state с re-render при каждом обновлении даст проблемы с производительностью.
import { useRef, useCallback } from 'react';
// Прямое DOM manipulation для hot path
const DOMRow = React.memo(({ price, size, total, maxTotal, side, highlight }: RowProps) => {
const rowRef = useRef<HTMLDivElement>(null);
// Обновляем DOM напрямую без 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 при изменении
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>
);
});
Визуальные особенности профессионального DOM
Подсветка изменений
// 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;
}
Тик группировка
// Пользователь переключает группировку: 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
Накопительный объём показывает суммарную ликвидность до каждого уровня — видно, насколько глубокий стакан:
function addCumulative(levels: DOMLevel[]): DOMLevelWithCum[] {
let cumulative = 0;
return levels.map(level => {
cumulative += level.size;
return { ...level, cumulative };
});
}
Производительность
На активных парах (BTC/USDT на крупных биржах) DOM может обновляться 10–50 раз/сек. Ограничения:
- Throttle updates: не более 10 рендеров/сек для DOM (человеческий глаз не воспринимает быстрее)
- Virtual scrolling: если показываем > 50 уровней — FlashList или react-window
- Canvas rendering: для максимальной производительности можно рендерить DOM в Canvas вместо HTML
// Throttle рендеринга до 10 fps
const throttledRender = useCallback(
throttle((domData: DOMData) => {
setDisplayData(domData);
}, 100), // 100ms = 10 fps
[]
);
Разработка DOM визуализатора с real-time обновлениями, группировкой тиков и подсветкой изменений: 3–5 недель.







