Appearance
Personalize your experience
Level Imbalance
Order Book ViewPer-level bid vs ask pressure, diverging from zero, with the cumulative drift through the book.
What it answers
Level by level, which side is heavier — and where does the weight accumulate? Open it from the order book toolbar with View → Level Imbalance, or from Settings → . The , bar and order entry stay in place; only the book pane changes.
Anatomy
How to read it
- Read the top first: near- imbalance is the most predictive.
- A run of bars all on one side is a consistent lean, not noise.
- Where the dashed drift line changes direction, the deeper book starts to offset the near book.
How it is computed
- and asks are paired by rank (level i on each side).
- Imbalance per pair is normalised to −1…1.
- Drift is the running sum of raw size differences, which shows magnitude as well as direction.
const rows = computeLevelImbalance(snapshot); // utils: pairs level i of each side
let running = 0;
for (const r of rows) {
r.imbalance; // (bidQty - askQty) / (bidQty + askQty)
r.isSignificant; // |imbalance| > 0.6
running += r.bidQty - r.askQty;
}Trading signals
Top-of-book imbalance
A strong L1–L3 imbalance is a well-documented short-horizon predictor of the next mid move, toward the light side.
Drift reversal
Bid heavy near the but ask heavy deep (the drift line ): moves in favour of the may stall once they reach that depth.
Settings that affect this view
Standalone HTML implementation
A complete, dependency-free page that implements this view on a canvas against the real-time book. It runs below exactly as written. The harness keeps a full local book from the REST snapshot and the 100ms diff stream (resyncing on any sequence gap) and reads from aggTrade. Change SYMBOL / GROUP for another market, or replace snapshot() with your own feed, and the draw() function works unchanged.
1<!doctype html>2<html lang="en">3<head>4<meta charset="utf-8">5<meta name="viewport" content="width=device-width,initial-scale=1">6<title>Level imbalance — order book view</title>7<style>8 html, body { margin: 0; height: 100%; background: #0b0e14; overflow: hidden; }9 canvas { display: block; width: 100%; height: 100%; }10 #status { position: fixed; left: 8px; bottom: 6px; font: 600 10px ui-monospace, Menlo, monospace;11 color: #cbd5e1; background: rgba(15,23,42,.85); border: 1px solid rgba(148,163,184,.3);12 border-radius: 4px; padding: 2px 6px; pointer-events: none; }13 #status i { display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: 5px; background: #f59e0b; }14</style>15</head>16<body>17<canvas id="c"></canvas>18<div id="status"><i></i>connecting…</div>19<script>20// ─── Canvas + palette ───────────────────────────────────────────────────────21const C = document.getElementById('c'), X = C.getContext('2d');22let W = 0, H = 0;23function fit() {24 const r = C.getBoundingClientRect(), d = Math.min(devicePixelRatio || 1, 2);25 W = r.width; H = r.height; C.width = W * d; C.height = H * d; X.setTransform(d, 0, 0, d, 0, 0);26}27addEventListener('resize', fit); fit();28const BID = '#0ecb81', ASK = '#f6465d', GRID = 'rgba(148,163,184,.16)';29const TEXT = '#e2e8f0', MUTED = '#64748b', ACCENT = '#8b5cf6', WARN = '#f59e0b';3031// ─── Feed settings ──────────────────────────────────────────────────────────32// Real-time Binance spot book. Change SYMBOL / GROUP for another market33// (e.g. 'ETHUSDT' with GROUP 0.1). If the exchange cannot be reached the page34// falls back to a synthetic book and says so in the status badge.35const SYMBOL = 'BTCUSDT';36const GROUP = 1; // aggregation step (price bucket)37const LEVELS = 30; // levels per side after grouping38const TICK = GROUP; // views size cells / buckets by the grouped step3940// ─── Real-time Binance book: REST snapshot + 100ms diff stream ──────────────41// Binance's documented procedure: buffer the diff stream, fetch a snapshot,42// drop events with u <= lastUpdateId, then apply in order; a gap43// (U != previous u + 1) means resnapshot. qty 0 deletes a level.44const live = { bids: new Map(), asks: new Map(), lastU: 0, lastE: 0, synced: false, buffer: [], ok: false, ups: [] };45const prints = [];46let source = 'connecting';47const statusEl = document.getElementById('status');48function setStatus(text, color) { statusEl.innerHTML = '<i style="background:' + color + '"></i>' + text; }4950function applyDiff(e) {51 for (const [p, q] of e.b) { const v = +q; v === 0 ? live.bids.delete(+p) : live.bids.set(+p, v); }52 for (const [p, q] of e.a) { const v = +q; v === 0 ? live.asks.delete(+p) : live.asks.set(+p, v); }53 live.lastU = e.u; live.lastE = e.E;54 const now = Date.now(); live.ups.push(now); while (now - live.ups[0] > 1000) live.ups.shift();55}56async function resnapshot() {57 live.synced = false;58 const r = await fetch('https://api.binance.com/api/v3/depth?symbol=' + SYMBOL + '&limit=1000');59 if (!r.ok) throw new Error('HTTP ' + r.status);60 const s = await r.json();61 live.bids = new Map(s.bids.map(([p, q]) => [+p, +q]));62 live.asks = new Map(s.asks.map(([p, q]) => [+p, +q]));63 live.lastU = s.lastUpdateId;64 const pending = live.buffer.filter(e => e.u > s.lastUpdateId); live.buffer = [];65 let first = true;66 for (const e of pending) {67 if (first && !(e.U <= live.lastU + 1 && e.u >= live.lastU + 1)) continue;68 first = false; applyDiff(e);69 }70 live.synced = true; live.ok = true; source = 'live';71}72// Snapshot failures fall back to the synthetic book and keep retrying; the page73// switches back to live as soon as a snapshot lands.74function trySnapshot() {75 resnapshot().catch(() => { fallback(); setTimeout(trySnapshot, 5000); });76}77function connect() {78 const s = SYMBOL.toLowerCase();79 let ws;80 try { ws = new WebSocket('wss://stream.binance.com:9443/stream?streams=' + s + '@depth@100ms/' + s + '@aggTrade'); }81 catch (e) { fallback(); return; }82 ws.onopen = () => trySnapshot();83 ws.onmessage = m => {84 const { stream, data } = JSON.parse(m.data);85 if (stream.endsWith('@aggTrade')) {86 // m = buyer is maker → the aggressor sold87 prints.push({ t: data.T, side: data.m ? 'sell' : 'buy', qty: +data.q, price: +data.p });88 if (prints.length > 400) prints.shift();89 return;90 }91 if (!live.synced) { live.buffer.push(data); if (live.buffer.length > 600) live.buffer.shift(); return; }92 if (data.u <= live.lastU) return;93 if (data.U > live.lastU + 1) { live.buffer = [data]; trySnapshot(); return; }94 applyDiff(data);95 };96 ws.onerror = () => { if (!live.ok) fallback(); };97 ws.onclose = () => { live.synced = false; setTimeout(connect, live.ok ? 2000 : 8000); };98}99setTimeout(() => { if (!live.ok) fallback(); }, 15000);100connect();101102// ─── Synthetic fallback (same shape as the live book) ───────────────────────103let center = 76500;104const base = { bid: [], ask: [] };105for (let i = 0; i < LEVELS; i++) { base.bid.push(0.4 + i * 0.5 + Math.random() * 3); base.ask.push(0.4 + i * 0.5 + Math.random() * 3); }106function fallback() {107 if (source === 'synthetic' || live.ok) return;108 source = 'synthetic';109 setStatus('SYNTHETIC · live feed unreachable, retrying', '#94a3b8');110}111function tickSynthetic() {112 if (Math.random() < 0.35) center += (Math.random() < 0.5 ? -1 : 1) * GROUP;113 for (const s of ['bid', 'ask']) for (let i = 0; i < LEVELS; i++) {114 let q = base[s][i] * (0.92 + Math.random() * 0.16);115 if (Math.random() < 0.003) q *= 5;116 if (q > 40) q *= 0.7;117 base[s][i] = q;118 }119 if (Math.random() < 0.6) {120 const side = Math.random() < 0.5 ? 'buy' : 'sell';121 prints.push({ t: Date.now(), side, qty: +(Math.random() ** 3 * 8 + 0.01).toFixed(3), price: side === 'buy' ? center + GROUP : center });122 if (prints.length > 400) prints.shift();123 }124}125function tick() { if (source === 'synthetic') tickSynthetic(); }126127// ─── Snapshot: group raw levels into LEVELS buckets per side ────────────────128let prev = new Map();129const r8 = v => Math.round(v * 1e8) / 1e8;130function group(map, side) {131 const out = new Map();132 for (const [p, q] of map) {133 const k = r8((side === 'bid' ? Math.floor(p / GROUP + 1e-9) : Math.ceil(p / GROUP - 1e-9)) * GROUP);134 out.set(k, (out.get(k) || 0) + q);135 }136 return [...out.entries()].sort((a, b) => (side === 'bid' ? b[0] - a[0] : a[0] - b[0])).slice(0, LEVELS);137}138function rawSynthetic(side) {139 const out = [];140 for (let i = 0; i < LEVELS; i++) {141 if ((i === 7 || i === 13) && Math.random() < 0.45) continue; // two flickering levels142 out.push([side === 'bid' ? center - i * GROUP : center + GROUP + i * GROUP, base[side][i]]);143 }144 return out;145}146function snapshot() {147 let rb, ra;148 if (source === 'live') {149 if (!live.synced) return null;150 rb = group(live.bids, 'bid'); ra = group(live.asks, 'ask');151 setStatus('LIVE · Binance ' + SYMBOL + ' · ' + live.ups.length + ' upd/s · ' + Math.max(0, Date.now() - live.lastE) + 'ms', '#22c55e');152 } else if (source === 'synthetic') {153 rb = rawSynthetic('bid'); ra = rawSynthetic('ask');154 } else return null;155 if (!rb.length || !ra.length) return null;156 const build = (levels, s) => {157 let tot = 0, notl = 0;158 return levels.map(([price, q]) => {159 const key = s + price;160 tot += q; notl += q * price;161 return { price, qty: q, total: tot, notional: q * price, cumNotional: notl,162 delta: prev.has(key) ? q - prev.get(key) : q, isNew: !prev.has(key) };163 });164 };165 const bids = build(rb, 'bid'), asks = build(ra, 'ask');166 const next = new Map();167 bids.forEach(l => next.set('bid' + l.price, l.qty));168 asks.forEach(l => next.set('ask' + l.price, l.qty));169 prev = next;170 const all = [...bids, ...asks], avg = all.reduce((s, l) => s + l.qty, 0) / all.length;171 all.forEach(l => { l.isWall = l.qty > avg * 3; });172 return { bids, asks, bestBid: bids[0].price, bestAsk: asks[0].price,173 mid: (bids[0].price + asks[0].price) / 2, t: Date.now(), prints };174}175176// ─── Helpers ────────────────────────────────────────────────────────────────177function txt(s, x, y, a = 'left', c = TEXT) { X.fillStyle = c; X.textAlign = a; X.textBaseline = 'middle'; X.fillText(s, x, y); }178function fmt(v, d = 2) { return Math.abs(v) >= 1e4 ? v.toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) : v.toFixed(d); }179function alpha(hex, a) { return hex + Math.round(Math.max(0, Math.min(1, a)) * 255).toString(16).padStart(2, '0'); }180function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }181182// ─── View ───────────────────────────────────────────────────────────────────183// For level i: (bidQty − askQty) / (bidQty + askQty), drawn left when bids are184// heavier and right when asks are. The dashed line is the running drift185// Σ(bid − ask) — where the book's weight accumulates as you go deeper.186function draw(X, W, H, b) {187 const n = Math.min(b.bids.length, b.asks.length), cx = W / 2, half = cx - 80, top = 18, rowH = (H - top) / n;188 let run = 0; const drift = [];189 for (let i = 0; i < n; i++) {190 const bq = b.bids[i].qty, aq = b.asks[i].qty, imb = (bq - aq) / (bq + aq), y = top + i * rowH;191 const col = imb >= 0 ? BID : ASK, len = Math.abs(imb) * half;192 X.fillStyle = alpha(col, Math.abs(imb) > 0.6 ? 0.9 : 0.45);193 X.fillRect(imb >= 0 ? cx - len : cx, y + 1, len, rowH - 2);194 run += bq - aq; drift.push([run, y + rowH / 2]);195 if (rowH > 11) { txt(fmt(b.bids[i].price), 72, y + rowH / 2, 'right', BID); txt(fmt(b.asks[i].price), W - 72, y + rowH / 2, 'left', ASK); }196 }197 const m = Math.max(...drift.map(d => Math.abs(d[0])), 1e-9);198 X.beginPath(); drift.forEach(([v, y], i) => (i ? X.lineTo : X.moveTo).call(X, cx - (v / m) * half, y));199 X.setLineDash([4, 3]); X.strokeStyle = TEXT; X.lineWidth = 1.5; X.stroke(); X.setLineDash([]);200 txt('◀ bid heavy', cx - 6, 9, 'right', BID); txt('ask heavy ▶', cx + 6, 9, 'left', ASK);201}202203// ─── Loop: redraw 4× a second (the live book updates every 100ms) ───────────204let last = 0;205(function loop(now) {206 requestAnimationFrame(loop);207 if (now - last < 250) return;208 last = now; tick();209 const b = snapshot();210 if (!b) return;211 X.clearRect(0, 0, W, H);212 X.font = '11px ui-monospace, Menlo, monospace';213 draw(X, W, H, b);214})(0);215</script>216</body>217</html>See also
Related articles
Related to Level Imbalance
- Depth Radar
Trading / Order Book / Views
Polar depth: the angle is distance from mid and the radius is cumulative depth — the book drawn as a shape.
- Order-Flow Imbalance (OFI)
Trading / Order Book / Views
Top-of-book order-flow imbalance per update, its cumulative drift and a live pressure gauge.
- 3D Book Towers
Trading / Order Book / Views
The book as a city of towers: one tower per price level for the latest fourteen slices of the book, older slices fading back.
- 3D Depth Canyon
Trading / Order Book / Views
The depth chart extruded through time. Cumulative bid and ask depth form two valley walls, and the mid runs along the valley floor.
- 3D Liquidity Terrain
Trading / Order Book / Views
The recorded book as a lit landscape: time across, price in depth, resting size as height. Walls are mountains, thin books are plains.
- 3D Ridgeline
Trading / Order Book / Views
Every price level as its own ridge of resting size through time, as in the book terrain chart: far ridges behind, near ridges in front, newest on the right.
- 3D Trade Cloud
Trading / Order Book / Views
Every print as a pillar at its time and price, its height the trade size, coloured by the aggressor, over the mid path.
- Book vs Tape Profile
Trading / Order Book / Views
What is resting at each price beside what has actually traded there — intent against action.
- Butterfly
Trading / Order Book / Views
Bid level N against ask level N, fanned out from a central spine — a level-by-level comparison of the two sides.