Appearance

Personalize your experience

Mode
Accent Color
Direction
3D Ridgeline

3D Ridgeline

Order Book View

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.

What it answers

How has the size at each price level changed over the last minutes, level by level? Open it from the order book toolbar with View → 3D Ridgeline, 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 3D Ridgeline 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. The ridges grow from the right as book updates arrive (the time axis spans at least 24 updates).

Anatomy

Ridge
One price bucket (two to four book buckets folded together, about 44 ridges). Its height over time is √(size ÷ the largest size in the ).
Crest line
A constant- anti-aliased line along the top of each ridge, in the bid colour below the mid and the ask colour above it.
Curtain
The face under each crest, fading from the ridge colour into the . It is opaque, so nearer ridges hide the ones behind at any camera angle.
Depth
Ridges further from the shade slightly darker, like a waterfall .
Time axis
Oldest on the left, now on the . The recording grows to 240 updates.
Hover
The ridge under the pointer brightens and thickens, the others dim, and a card shows the price, size, depth and time.

How to read it

  1. Look for ridges that stay high across the whole : that have their place.
  2. A ridge that drops to the just as the mid approaches it is liquidity before it could trade.
  3. Ridges that rise and fall together on one side are a book shifting as a block, typical of market makers re-quoting.
  4. Orbit to the Price side preset to the profile of every level at one instant.

How it is computed

  • Each book update is bucketed onto a price grid around an near the mid (bucket = a whole multiple of the book step, sized so the visible book fits). The anchor shifts by whole buckets when the mid drifts a quarter of the .
  • The grid is a ring of 240 ; a new update overwrites the oldest column and only that texture row is uploaded.
  • Ridge heights are computed in the vertex shader from the texture: the max over the ridge's , smoothed 1-2-1 across neighbouring updates.
  • Two passes per ridge: the curtain (depth-writing strip) and the crest (screen-space quads, blended, no depth write).
  • Rendered on the GPU: WebGPU first (one page-wide device, pipelines compiled once and shared by every 3D view), then WebGL2, then Canvas 2D. The recorded book lives in a 240 × float texture; each book update uploads one texture row, never the whole history, and frames are drawn only when the book, the camera or the changes, and not at all while the pane is off screen.
Core calculation
// per ridge, per time column (vertex shader)
const raw = (c: number) => Math.max(...rowsOf(ridge).map(r => Math.sqrt((bid[c][r] + ask[c][r]) / maxSize)));
const height = (raw(c - 1) + 2 * raw(c) + raw(c + 1)) / 4;
// colour: bid below the latest mid, ask above it, darker further from the touch
const hue = (ridgeRow < midRow ? bidColor : askColor) * (1 - 0.25 * distanceFromTouch);

Trading signals

Held wall

A high ridge flat across the close to the mid: a level that price has to trade through.

Pulled liquidity

A ridge collapsing as the mid nears it: the order was cancelled rather than , a common spoofing pattern.

Stacking

Successive ridges rising on one side over time: liquidity building behind the , often before a push the other way.

!
Caution:Heights use a of size, and a ridge shows the largest of the it folds together. Read exact sizes from the card, not from the height.

Settings that affect this view

Renderer badge (top right)
Shows WebGPU, WebGL2 or Canvas and the measured CPU time per frame. Click it to cycle auto → WebGPU → WebGL2 → Canvas; the choice is remembered.
Camera bar (top left)
Front, , Top, Price side and Low presets (keys 1–5), auto-rotate (Space), reset (R or double-click), and −/+ to lower or raise the heights.
Bid / ask colours, skin
Every , ridge, tower and pillar uses the book colours.
Glow
Brightens the tallest peaks and walls (off on light themes).
Grid & size labels
grid on or off.
Levels per side, grouping
More levels widen the recorded price ; the bucket size adapts so the band always fits.

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.

3D ridgeline — standalone HTML, live Binance feed, no dependencies
orderbook-ridgeline.html
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>3D ridgeline — 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 ───────────────────────────────────────────────────────────────────// ─── Tiny 3D, no WebGL: recorded book + orbit camera + perspective ──────────// The production view renders this on the GPU (WebGPU, then WebGL2). Here the// same scene is projected on the CPU and painted far → near, so the page has// no dependencies. World box: x = time (oldest −1 → now +1), z = price (low// +1 → high −1), y = height.const HIST = [], NCOL = 90, NROW = 44;function record(b) {  HIST.push({ t: b.t, mid: b.mid, bids: b.bids.map(l => [l.price, l.qty, l.total]), asks: b.asks.map(l => [l.price, l.qty, l.total]) });  if (HIST.length > NCOL) HIST.shift();}// grid[c][r] = { bid, ask, cb, ca } bucketed around the newest mid.function gridOf() {  const mid = HIST[HIST.length - 1].mid, out = [];  for (const s of HIST) {    const col = Array.from({ length: NROW }, () => ({ bid: 0, ask: 0, cb: 0, ca: 0 }));    for (const [p, q, tot] of s.bids) { const r = Math.round((p - mid) / TICK) + NROW / 2; if (r >= 0 && r < NROW) { col[r].bid += q; col[r].cb = tot; } }    for (const [p, q, tot] of s.asks) { const r = Math.round((p - mid) / TICK) + NROW / 2; if (r >= 0 && r < NROW) { col[r].ask += q; col[r].ca = tot; } }    let run = 0; for (let r = NROW / 2; r >= 0; r--) { run = Math.max(run, col[r].cb); col[r].cb = run; }    run = 0; for (let r = NROW / 2; r < NROW; r++) { run = Math.max(run, col[r].ca); col[r].ca = run; }    out.push(col);  }  return out;}// Slow automatic orbit; returns project(x, y, z) → [sx, sy, depth].function camera(W, H, yaw0 = -0.45, pitch = 0.5) {  const yaw = yaw0 + Math.sin(Date.now() / 9000) * 0.3;  const cy = Math.cos(yaw), sy = Math.sin(yaw), cp = Math.cos(pitch), sp = Math.sin(pitch), d = 3.4;  return (x, y, z) => {    const x1 = cy * x - sy * z, z1 = sy * x + cy * z;    const y2 = cp * y - sp * z1, z2 = sp * y + cp * z1;    const depth = d - z2, f = Math.min(W * 0.9, H * 1.5) / depth;    return [W / 2 + x1 * f, H * 0.6 - y2 * f, depth];  };}const wx = (c, n) => (n > 1 ? (c / (n - 1)) * 2 - 1 : 1);const wz = r => 1 - (r / (NROW - 1)) * 2;function poly(pts, fill, stroke) {  X.beginPath(); pts.forEach((p, i) => (i ? X.lineTo(p[0], p[1]) : X.moveTo(p[0], p[1]))); X.closePath();  if (fill) { X.fillStyle = fill; X.fill(); }  if (stroke) { X.strokeStyle = stroke; X.stroke(); }}function line(pts, stroke, width) {  X.lineWidth = width; X.strokeStyle = stroke; X.beginPath();  pts.forEach((p, i) => (i ? X.lineTo(p[0], p[1]) : X.moveTo(p[0], p[1]))); X.stroke();}function floor(P) {  for (let k = 0; k <= 10; k++) {    line([P(-1 + k / 5, 0, -1), P(-1 + k / 5, 0, 1)], GRID, 1);    line([P(-1, 0, -1 + k / 5), P(1, 0, -1 + k / 5)], GRID, 1);  }}function shade(hex, k) {  const n = parseInt(hex.slice(1), 16), f = v => Math.max(0, Math.min(255, Math.round(v * k)));  return 'rgb(' + f(n >> 16 & 255) + ',' + f(n >> 8 & 255) + ',' + f(n & 255) + ')';}// Every price level is a ridge: its resting size through time. Ridges are// painted far → near with a background-coloured underlay, so nearer ridges// hide those behind, then a crisp crest line in the side's colour.function draw(X, W, H, b) {  record(b);  const g = gridOf(), n = g.length, P = camera(W, H, -0.2, 0.55);  let max = 0; for (const col of g) for (const c of col) max = Math.max(max, c.bid + c.ask);  floor(P);  for (let r = NROW - 2; r >= 0; r -= 2) {                  // high price (far) first    const z = wz(r), pts = [];    for (let c = 0; c < n; c++) {      const s = Math.max(g[c][r].bid + g[c][r].ask, g[c][r + 1].bid + g[c][r + 1].ask);   // two rows per ridge      pts.push(P(wx(c, n), Math.sqrt(s / (max || 1)) * 0.45, z));    }    poly([P(wx(0, n), 0, z), ...pts, P(wx(n - 1, n), 0, z)], '#0b0e14', null);    line(pts, r < NROW / 2 ? BID : ASK, 1.4);  }  const m = P(1, 0, wz(NROW / 2)); txt(fmt(b.mid), m[0] + 6, m[1], 'left', TEXT);}// ─── 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 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.

  • Depth City

    Trading / Order Book / Views

    An isometric 3-D skyline of resting size along the price axis — towers are walls.

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