Appearance

Personalize your experience

Mode
Accent Color
Direction
Liquidity Heatmap

Liquidity Heatmap

Order Book View

Resting size at every price, over time — where liquidity has been sitting, not just where it is now.

What it answers

Where has liquidity been sitting, and did price respect it? Open it from the order book toolbar with View → Liquidity Heatmap, or from Settings → . The , bar and order entry stay in place; only the book pane changes.

Connecting to Binance BTC/USDT…
Connecting to Binance BTC/USDT…
Real time: the production Liquidity Heatmap renderer on the live Binance spot book (REST snapshot + 100ms diff stream + trades). Switch symbol or source above; hover for a readout, click to pick a price. Fills from the right; about a minute at four updates a second.

Anatomy

Column
One book update. The newest column is on the ; up to 240 are kept.
Cell brightness
√(size ÷ largest size in the ). The keeps levels visible without washing out walls.
Cell colour
Bid colour below the mid, ask colour above it.
White line
The mid price .
Bubbles
at their time and price; area tracks size, colour is the aggressor.
Right axis
Price scale with the current mid highlighted.

How to read it

  1. Look for bands that persist across many — those are that stayed put.
  2. Follow the mid line into a : if it stops or reverses there, the liquidity was real.
  3. Watch for a bright that disappears just as price approaches it — a (often spoofed) wall.
  4. Clusters of bubbles at one price with an unchanged mean the level absorbed — an iceberg or a defended level.

How it is computed

  • On every book update the current levels are appended as a column of (price, size) .
  • The is centred on the current mid and spans the widest side of the current book.
  • Brightness is normalised to the largest size anywhere in the visible , so a wall dims older, smaller .
  • are placed by timestamp between the first and last column and by price on the shared .
Core calculation
// One column per update
heat.push({ t: snapshot.timestamp, mid: snapshot.midPrice,
            bids: snapshot.bids.map(l => [l.price, l.quantity]),
            asks: snapshot.asks.map(l => [l.price, l.quantity]) });
if (heat.length > 240) heat.shift();

// Cell intensity
const maxQty = Math.max(...heat.flatMap(c => [...c.bids, ...c.asks].map(([, q]) => q)));
const intensity = (qty: number) => Math.sqrt(qty / maxQty);   // 0..1
const alpha = 0.08 + intensity(qty) * 0.85;

Trading signals

Defended level

A that price touches repeatedly while it stays bright and cluster on it: a buyer or seller is absorbing. Expect a or a sharp break once it is exhausted.

Spoofing

A bright appears ahead of price, holds while price is far away and vanishes as price gets close. Treat the level as fake.

Vacuum

Dark gaps between bands are thin liquidity; price tends to move fast through them.

Iceberg

Heavy at a price where the never dims: displayed size is being refilled from hidden size.

!
Caution:History is -side and starts when the view opens; it resets when the symbol or changes. Use a coarser grouping on fast books so bands are a few pixels tall.

Settings that affect this view

Price grouping
Changes the height. Coarser merges nearby orders into readable bands.
Levels
How far from mid the history reaches.
Trade prints
Shows or hides the .
Glow
Adds a glow to the mid .

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.

Liquidity heatmap — standalone HTML, live Binance feed, no dependencies
orderbook-heatmap.html
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Liquidity heatmap — order book view</title><style>  html, body { margin: 0; height: 100%; background: #0b0e14; overflow: hidden; }  canvas { display: block; width: 100%; height: 100%; }  #status { position: fixed; left: 8px; bottom: 6px; font: 600 10px ui-monospace, Menlo, monospace;            color: #cbd5e1; background: rgba(15,23,42,.85); border: 1px solid rgba(148,163,184,.3);            border-radius: 4px; padding: 2px 6px; pointer-events: none; }  #status i { display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: 5px; background: #f59e0b; }</style></head><body><canvas id="c"></canvas><div id="status"><i></i>connecting…</div><script>// ─── Canvas + palette ───────────────────────────────────────────────────────const C = document.getElementById('c'), X = C.getContext('2d');let W = 0, H = 0;function fit() {  const r = C.getBoundingClientRect(), d = Math.min(devicePixelRatio || 1, 2);  W = r.width; H = r.height; C.width = W * d; C.height = H * d; X.setTransform(d, 0, 0, d, 0, 0);}addEventListener('resize', fit); fit();const BID = '#0ecb81', ASK = '#f6465d', GRID = 'rgba(148,163,184,.16)';const TEXT = '#e2e8f0', MUTED = '#64748b', ACCENT = '#8b5cf6', WARN = '#f59e0b';// ─── Feed settings ──────────────────────────────────────────────────────────// Real-time Binance spot book. Change SYMBOL / GROUP for another market// (e.g. 'ETHUSDT' with GROUP 0.1). If the exchange cannot be reached the page// falls back to a synthetic book and says so in the status badge.const SYMBOL = 'BTCUSDT';const GROUP = 1;            // aggregation step (price bucket)const LEVELS = 30;          // levels per side after groupingconst TICK = GROUP;         // views size cells / buckets by the grouped step// ─── Real-time Binance book: REST snapshot + 100ms diff stream ──────────────// Binance's documented procedure: buffer the diff stream, fetch a snapshot,// drop events with u <= lastUpdateId, then apply in order; a gap// (U != previous u + 1) means resnapshot. qty 0 deletes a level.const live = { bids: new Map(), asks: new Map(), lastU: 0, lastE: 0, synced: false, buffer: [], ok: false, ups: [] };const prints = [];let source = 'connecting';const statusEl = document.getElementById('status');function setStatus(text, color) { statusEl.innerHTML = '<i style="background:' + color + '"></i>' + text; }function applyDiff(e) {  for (const [p, q] of e.b) { const v = +q; v === 0 ? live.bids.delete(+p) : live.bids.set(+p, v); }  for (const [p, q] of e.a) { const v = +q; v === 0 ? live.asks.delete(+p) : live.asks.set(+p, v); }  live.lastU = e.u; live.lastE = e.E;  const now = Date.now(); live.ups.push(now); while (now - live.ups[0] > 1000) live.ups.shift();}async function resnapshot() {  live.synced = false;  const r = await fetch('https://api.binance.com/api/v3/depth?symbol=' + SYMBOL + '&limit=1000');  if (!r.ok) throw new Error('HTTP ' + r.status);  const s = await r.json();  live.bids = new Map(s.bids.map(([p, q]) => [+p, +q]));  live.asks = new Map(s.asks.map(([p, q]) => [+p, +q]));  live.lastU = s.lastUpdateId;  const pending = live.buffer.filter(e => e.u > s.lastUpdateId); live.buffer = [];  let first = true;  for (const e of pending) {    if (first && !(e.U <= live.lastU + 1 && e.u >= live.lastU + 1)) continue;    first = false; applyDiff(e);  }  live.synced = true; live.ok = true; source = 'live';}// Snapshot failures fall back to the synthetic book and keep retrying; the page// switches back to live as soon as a snapshot lands.function trySnapshot() {  resnapshot().catch(() => { fallback(); setTimeout(trySnapshot, 5000); });}function connect() {  const s = SYMBOL.toLowerCase();  let ws;  try { ws = new WebSocket('wss://stream.binance.com:9443/stream?streams=' + s + '@depth@100ms/' + s + '@aggTrade'); }  catch (e) { fallback(); return; }  ws.onopen = () => trySnapshot();  ws.onmessage = m => {    const { stream, data } = JSON.parse(m.data);    if (stream.endsWith('@aggTrade')) {      // m = buyer is maker → the aggressor sold      prints.push({ t: data.T, side: data.m ? 'sell' : 'buy', qty: +data.q, price: +data.p });      if (prints.length > 400) prints.shift();      return;    }    if (!live.synced) { live.buffer.push(data); if (live.buffer.length > 600) live.buffer.shift(); return; }    if (data.u <= live.lastU) return;    if (data.U > live.lastU + 1) { live.buffer = [data]; trySnapshot(); return; }    applyDiff(data);  };  ws.onerror = () => { if (!live.ok) fallback(); };  ws.onclose = () => { live.synced = false; setTimeout(connect, live.ok ? 2000 : 8000); };}setTimeout(() => { if (!live.ok) fallback(); }, 15000);connect();// ─── Synthetic fallback (same shape as the live book) ───────────────────────let center = 76500;const base = { bid: [], ask: [] };for (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); }function fallback() {  if (source === 'synthetic' || live.ok) return;  source = 'synthetic';  setStatus('SYNTHETIC · live feed unreachable, retrying', '#94a3b8');}function tickSynthetic() {  if (Math.random() < 0.35) center += (Math.random() < 0.5 ? -1 : 1) * GROUP;  for (const s of ['bid', 'ask']) for (let i = 0; i < LEVELS; i++) {    let q = base[s][i] * (0.92 + Math.random() * 0.16);    if (Math.random() < 0.003) q *= 5;    if (q > 40) q *= 0.7;    base[s][i] = q;  }  if (Math.random() < 0.6) {    const side = Math.random() < 0.5 ? 'buy' : 'sell';    prints.push({ t: Date.now(), side, qty: +(Math.random() ** 3 * 8 + 0.01).toFixed(3), price: side === 'buy' ? center + GROUP : center });    if (prints.length > 400) prints.shift();  }}function tick() { if (source === 'synthetic') tickSynthetic(); }// ─── Snapshot: group raw levels into LEVELS buckets per side ────────────────let prev = new Map();const r8 = v => Math.round(v * 1e8) / 1e8;function group(map, side) {  const out = new Map();  for (const [p, q] of map) {    const k = r8((side === 'bid' ? Math.floor(p / GROUP + 1e-9) : Math.ceil(p / GROUP - 1e-9)) * GROUP);    out.set(k, (out.get(k) || 0) + q);  }  return [...out.entries()].sort((a, b) => (side === 'bid' ? b[0] - a[0] : a[0] - b[0])).slice(0, LEVELS);}function rawSynthetic(side) {  const out = [];  for (let i = 0; i < LEVELS; i++) {    if ((i === 7 || i === 13) && Math.random() < 0.45) continue;     // two flickering levels    out.push([side === 'bid' ? center - i * GROUP : center + GROUP + i * GROUP, base[side][i]]);  }  return out;}function snapshot() {  let rb, ra;  if (source === 'live') {    if (!live.synced) return null;    rb = group(live.bids, 'bid'); ra = group(live.asks, 'ask');    setStatus('LIVE · Binance ' + SYMBOL + ' · ' + live.ups.length + ' upd/s · ' + Math.max(0, Date.now() - live.lastE) + 'ms', '#22c55e');  } else if (source === 'synthetic') {    rb = rawSynthetic('bid'); ra = rawSynthetic('ask');  } else return null;  if (!rb.length || !ra.length) return null;  const build = (levels, s) => {    let tot = 0, notl = 0;    return levels.map(([price, q]) => {      const key = s + price;      tot += q; notl += q * price;      return { price, qty: q, total: tot, notional: q * price, cumNotional: notl,               delta: prev.has(key) ? q - prev.get(key) : q, isNew: !prev.has(key) };    });  };  const bids = build(rb, 'bid'), asks = build(ra, 'ask');  const next = new Map();  bids.forEach(l => next.set('bid' + l.price, l.qty));  asks.forEach(l => next.set('ask' + l.price, l.qty));  prev = next;  const all = [...bids, ...asks], avg = all.reduce((s, l) => s + l.qty, 0) / all.length;  all.forEach(l => { l.isWall = l.qty > avg * 3; });  return { bids, asks, bestBid: bids[0].price, bestAsk: asks[0].price,           mid: (bids[0].price + asks[0].price) / 2, t: Date.now(), prints };}// ─── Helpers ────────────────────────────────────────────────────────────────function txt(s, x, y, a = 'left', c = TEXT) { X.fillStyle = c; X.textAlign = a; X.textBaseline = 'middle'; X.fillText(s, x, y); }function fmt(v, d = 2) { return Math.abs(v) >= 1e4 ? v.toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) : v.toFixed(d); }function alpha(hex, a) { return hex + Math.round(Math.max(0, Math.min(1, a)) * 255).toString(16).padStart(2, '0'); }function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }// ─── View ───────────────────────────────────────────────────────────────────// Every update becomes one column; each cell's brightness is the resting size// at that price at that moment. Persistent bands are liquidity that stayed;// bubbles are trades at their time and price.const hist = [];function draw(X, W, H, b) {  hist.push({ t: b.t, mid: b.mid, cells: [...b.bids, ...b.asks].map(l => [l.price, l.qty, l.price < b.mid]) });  if (hist.length > 240) hist.shift();  const axis = 70, lo = b.mid - 20 * TICK, hi = b.mid + 20 * TICK;  const yOf = p => H - ((p - lo) / (hi - lo)) * H, cell = (H * TICK) / (hi - lo);  let maxQ = 1e-9; for (const c of hist) for (const [, q] of c.cells) maxQ = Math.max(maxQ, q);  const cw = (W - axis) / 240, x0 = W - axis - hist.length * cw;  hist.forEach((col, i) => {    for (const [p, q, isBid] of col.cells) {      if (p < lo || p > hi) continue;      X.fillStyle = alpha(isBid ? BID : ASK, 0.08 + Math.sqrt(q / maxQ) * 0.85);      X.fillRect(x0 + i * cw, yOf(p) - cell / 2, Math.ceil(cw) + 0.5, cell);    }  });  X.beginPath(); hist.forEach((c, i) => (i ? X.lineTo : X.moveTo).call(X, x0 + i * cw, yOf(c.mid)));  X.strokeStyle = '#fff'; X.lineWidth = 1.3; X.stroke();  const t0 = hist[0].t, span = Math.max(1, b.t - t0);  for (const p of b.prints) {    if (p.t < t0) continue;    X.beginPath(); X.arc(x0 + ((p.t - t0) / span) * hist.length * cw, yOf(p.price), 2 + Math.sqrt(p.qty) * 2.5, 0, 7);    X.fillStyle = alpha(p.side === 'buy' ? BID : ASK, 0.4); X.fill();  }  for (let i = 0; i <= 8; i++) { const p = lo + ((hi - lo) * i) / 8; txt(fmt(p), W - 4, yOf(p), 'right', MUTED); }}// ─── Loop: redraw 4× a second (the live book updates every 100ms) ───────────let last = 0;(function loop(now) {  requestAnimationFrame(loop);  if (now - last < 250) return;  last = now; tick();  const b = snapshot();  if (!b) return;  X.clearRect(0, 0, W, H);  X.font = '11px ui-monospace, Menlo, monospace';  draw(X, W, H, b);})(0);</script></body></html>

See also

Related articles

  • 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.

  • Concentration

    Trading / Order Book / Views

    Lorenz curve and Gini coefficient of each side — how few levels hold the liquidity.

  • Depth Bands

    Trading / Order Book / Views

    Liquidity within 5 / 10 / 25 / 50 / 100 bp of mid, over time — how much depth is actually close.

Browse all knowledge base articles →