Development of Crypto Market Depth Screener
Market depth screener analyzes order books of multiple instruments simultaneously and finds anomalies: large liquidity walls, bid/ask imbalance, abnormal volume at levels. This is a tool for traders who read the stack and make decisions based on market microstructure.
What Screener Analyzes
Bid/Ask imbalance: if bid-side volume 3x more than ask—excess buy pressure. Strong signal for short-term upward move.
Liquidity walls: anomalously large order at single level. Could be real support/resistance or spoofing (order placed and canceled before execution).
Spread: wide spread = low liquidity = high slippage on entry.
Depth at price levels: how much volume needed to "eat" the stack by 1%, 2%, 5%.
Screener Architecture
interface MarketDepthSnapshot {
symbol: string;
exchange: string;
timestamp: number;
bids: [price: number, size: number][];
asks: [price: number, size: number][];
}
interface DepthMetrics {
symbol: string;
bidVolume: number; // total volume at N bid levels
askVolume: number; // total volume at N ask levels
imbalance: number; // bid / (bid + ask), 0.5 = neutral
spread: number; // % spread
spreadUSD: number; // absolute spread in USD
bidWall: WallInfo | null;
askWall: WallInfo | null;
liquidationAt1Pct: number; // volume needed for 1% move
liquidationAt2Pct: number;
}
interface WallInfo {
price: number;
size: number;
sizeUSD: number;
relativeSize: number; // how many times larger than average level
}
Computing Metrics
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;
// Find walls: level with volume > avg * threshold
const avgBidSize = bidVolume / bids.length;
const avgAskSize = askVolume / asks.length;
const wallThreshold = 3.0; // 3x larger = wall
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);
// Liquidity at 1% move
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, // similarly for ask
liquidationAt1Pct,
liquidationAt2Pct: 0, // similarly
};
}
Screener Table
// Sortable table with metrics across all pairs
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 });
// Filter and sort
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>
);
}
Condition-Based Alerts
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);
}
}
Data Collection
Screener requires WebSocket connections to exchanges. For 50 pairs—50 WebSocket channels. Connection manager:
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);
}
}
Development of market depth screener for 20–50 pairs with real-time updates, sorting, filters and alerts: 4–6 weeks.







