Разработка скринера по глубине рынка
Скринер по глубине рынка анализирует order book нескольких инструментов одновременно и находит аномалии: крупные стены ликвидности, дисбаланс bid/ask, аномальный объём на уровнях. Это инструмент для трейдеров, которые читают стакан и принимают решения на основе market microstructure.
Что анализирует скринер глубины
Bid/Ask imbalance: если на bid-стороне объём в 3× больше чем на ask — избыточный buy pressure. Сильный сигнал для краткосрочного движения вверх.
Liquidity walls: аномально крупный ордер на одном уровне. Может быть реальной поддержкой/сопротивлением или spoofing (ордер выставляется и снимается до исполнения).
Spread: широкий спред = низкая ликвидность = высокий slippage при входе.
Depth at price levels: сколько нужно объёма чтобы "съесть" стакан на 1%, 2%, 5%.
Архитектура скринера
interface MarketDepthSnapshot {
symbol: string;
exchange: string;
timestamp: number;
bids: [price: number, size: number][];
asks: [price: number, size: number][];
}
interface DepthMetrics {
symbol: string;
bidVolume: number; // суммарный объём на N уровнях bid
askVolume: number; // суммарный объём на N уровнях ask
imbalance: number; // bid / (bid + ask), 0.5 = нейтрально
spread: number; // % спред
spreadUSD: number; // абсолютный спред в USD
bidWall: WallInfo | null;
askWall: WallInfo | null;
liquidationAt1Pct: number; // объём нужный для 1% движения
liquidationAt2Pct: number;
}
interface WallInfo {
price: number;
size: number;
sizeUSD: number;
relativeSize: number; // во сколько раз больше среднего уровня
}
Вычисление метрик
function calculateDepthMetrics(
snapshot: MarketDepthSnapshot,
levels: number = 20
): DepthMetrics {
const bids = snapshot.bids.slice(0, levels);
const asks = snapshot.asks.slice(0, levels);
const midPrice = (bids[0][0] + asks[0][0]) / 2;
const bidVolume = bids.reduce((sum, [, size]) => sum + size, 0);
const askVolume = asks.reduce((sum, [, size]) => sum + size, 0);
const imbalance = bidVolume / (bidVolume + askVolume);
const spread = (asks[0][0] - bids[0][0]) / midPrice * 100;
// Поиск стен: уровень с объёмом > avg * threshold
const avgBidSize = bidVolume / bids.length;
const avgAskSize = askVolume / asks.length;
const wallThreshold = 3.0; // в 3× больше среднего = стена
const bidWall = bids.reduce((max, [price, size]) => {
if (size > avgBidSize * wallThreshold) {
if (!max || size > max.size) {
return { price, size, sizeUSD: size * price,
relativeSize: size / avgBidSize };
}
}
return max;
}, null as WallInfo | null);
// Ликвидность на 1% движения
const priceAt1PctDown = midPrice * 0.99;
const liquidationAt1Pct = bids
.filter(([price]) => price >= priceAt1PctDown)
.reduce((sum, [, size]) => sum + size * midPrice, 0);
return {
symbol: snapshot.symbol,
bidVolume: bidVolume * midPrice,
askVolume: askVolume * midPrice,
imbalance,
spread,
spreadUSD: asks[0][0] - bids[0][0],
bidWall,
askWall: null, // аналогично для ask
liquidationAt1Pct,
liquidationAt2Pct: 0, // аналогично
};
}
Скринер таблица
// Сортируемая таблица с метриками по всем парам
function DepthScreener() {
const [metrics, setMetrics] = useState<DepthMetrics[]>([]);
const [sort, setSort] = useState<{ field: keyof DepthMetrics; direction: 'asc' | 'desc' }>(
{ field: 'imbalance', direction: 'desc' }
);
const [filter, setFilter] = useState({ minImbalance: 0.6, maxSpread: 0.1 });
// Фильтруем и сортируем
const filtered = metrics
.filter(m => m.imbalance >= filter.minImbalance && m.spread <= filter.maxSpread)
.sort((a, b) => {
const val = (x: DepthMetrics) => x[sort.field] as number;
return sort.direction === 'desc' ? val(b) - val(a) : val(a) - val(b);
});
return (
<table>
<thead>
<SortableHeader field="symbol" label="Symbol" {...sort} onSort={setSort} />
<SortableHeader field="imbalance" label="Imbalance" {...sort} onSort={setSort} />
<SortableHeader field="spread" label="Spread %" {...sort} onSort={setSort} />
<SortableHeader field="bidVolume" label="Bid Volume" {...sort} onSort={setSort} />
<SortableHeader field="liquidationAt1Pct" label="Liq @1%" {...sort} onSort={setSort} />
</thead>
<tbody>
{filtered.map(m => (
<DepthMetricRow key={m.symbol} metrics={m} />
))}
</tbody>
</table>
);
}
Алерты по условиям
interface DepthAlert {
symbol: string;
condition: 'imbalance_spike' | 'wall_appeared' | 'wall_removed' | 'spread_widened';
threshold: number;
notifyVia: ('ui' | 'telegram' | 'webhook')[];
}
class DepthAlertEngine {
private prevSnapshots = new Map<string, DepthMetrics>();
checkAlerts(current: DepthMetrics, alerts: DepthAlert[]) {
const prev = this.prevSnapshots.get(current.symbol);
if (!prev) {
this.prevSnapshots.set(current.symbol, current);
return;
}
for (const alert of alerts) {
if (alert.symbol !== current.symbol) continue;
switch (alert.condition) {
case 'imbalance_spike':
if (current.imbalance >= alert.threshold && prev.imbalance < alert.threshold) {
this.triggerAlert(alert, `Imbalance spike on ${current.symbol}: ${(current.imbalance * 100).toFixed(1)}%`);
}
break;
case 'wall_appeared':
if (current.bidWall && !prev.bidWall && current.bidWall.sizeUSD >= alert.threshold) {
this.triggerAlert(alert, `Bid wall appeared on ${current.symbol}: $${(current.bidWall.sizeUSD/1000).toFixed(0)}k`);
}
break;
}
}
this.prevSnapshots.set(current.symbol, current);
}
}
Сбор данных
Скринер требует WebSocket подключений к биржам. Для 50 пар — 50 WebSocket каналов. Менеджер соединений:
class MultiExchangeDepthFeed {
private connections = new Map<string, WebSocket>();
private onUpdate: (snapshot: MarketDepthSnapshot) => void;
subscribe(symbol: string, exchange: 'binance' | 'okx' | 'bybit') {
const wsUrl = this.getWSUrl(exchange, symbol);
const ws = new WebSocket(wsUrl);
ws.onmessage = (e) => {
const snapshot = this.parseMessage(exchange, JSON.parse(e.data));
if (snapshot) this.onUpdate(snapshot);
};
ws.onclose = () => {
setTimeout(() => this.subscribe(symbol, exchange), 3000);
};
this.connections.set(`${exchange}:${symbol}`, ws);
}
}
Разработка скринера по глубине рынка для 20–50 пар с real-time обновлениями, сортировкой, фильтрами и алертами: 4–6 недель.







