Appearance
Personalize your experience
3D Book Towers
Order Book ViewThe book as a city of towers: one tower per price level for the latest fourteen slices of the book, older slices fading back.
What it answers
Which levels hold the size now, and were they there a few updates ago? Open it from the order book toolbar with View → 3D Book Towers, or from Settings → . The , bar and order entry stay in place; only the book pane changes.
Anatomy
How to read it
- Look for of towers at the same price across slices: persistent walls.
- A tower present only in the newest slice is fresh size; one that vanished from it was just or traded.
- Use Price side to line all slices up and the profile of the book over time.
How it is computed
- One unit cube in the vertex shader (36 vertices from vertex_index), drawn once per tower with per-instance position, footprint, height, colour, fade and glow.
- Up to a few thousand instances per frame; the instance is rewritten only when the book changes.
- 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.
for (let k = 0; k < 14; k++) {
const c = newest - k * stride; // one slice
for (const r of rows) if (size[c][r] > 0)
towers.push({ x: timeX(c), z: priceZ(r), h: Math.sqrt(size[c][r] / maxSize) * height,
colour: bid[c][r] >= ask[c][r] ? bidColor : askColor, fade: 1 - k / 14 * 0.7 });
}Trading signals
Skyscraper
A whitened tower close to the mid in every slice: the level that decides the next move.
Demolition
A tall tower in older slices that is missing from the newest: a wall removed. If price did not trade there, treat the earlier support as fake.
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>3D book towers — 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 ───────────────────────────────────────────────────────────────────183184// ─── Tiny 3D, no WebGL: recorded book + orbit camera + perspective ──────────185// The production view renders this on the GPU (WebGPU, then WebGL2). Here the186// same scene is projected on the CPU and painted far → near, so the page has187// no dependencies. World box: x = time (oldest −1 → now +1), z = price (low188// +1 → high −1), y = height.189const HIST = [], NCOL = 90, NROW = 44;190function record(b) {191 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]) });192 if (HIST.length > NCOL) HIST.shift();193}194// grid[c][r] = { bid, ask, cb, ca } bucketed around the newest mid.195function gridOf() {196 const mid = HIST[HIST.length - 1].mid, out = [];197 for (const s of HIST) {198 const col = Array.from({ length: NROW }, () => ({ bid: 0, ask: 0, cb: 0, ca: 0 }));199 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; } }200 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; } }201 let run = 0; for (let r = NROW / 2; r >= 0; r--) { run = Math.max(run, col[r].cb); col[r].cb = run; }202 run = 0; for (let r = NROW / 2; r < NROW; r++) { run = Math.max(run, col[r].ca); col[r].ca = run; }203 out.push(col);204 }205 return out;206}207// Slow automatic orbit; returns project(x, y, z) → [sx, sy, depth].208function camera(W, H, yaw0 = -0.45, pitch = 0.5) {209 const yaw = yaw0 + Math.sin(Date.now() / 9000) * 0.3;210 const cy = Math.cos(yaw), sy = Math.sin(yaw), cp = Math.cos(pitch), sp = Math.sin(pitch), d = 3.4;211 return (x, y, z) => {212 const x1 = cy * x - sy * z, z1 = sy * x + cy * z;213 const y2 = cp * y - sp * z1, z2 = sp * y + cp * z1;214 const depth = d - z2, f = Math.min(W * 0.9, H * 1.5) / depth;215 return [W / 2 + x1 * f, H * 0.6 - y2 * f, depth];216 };217}218const wx = (c, n) => (n > 1 ? (c / (n - 1)) * 2 - 1 : 1);219const wz = r => 1 - (r / (NROW - 1)) * 2;220function poly(pts, fill, stroke) {221 X.beginPath(); pts.forEach((p, i) => (i ? X.lineTo(p[0], p[1]) : X.moveTo(p[0], p[1]))); X.closePath();222 if (fill) { X.fillStyle = fill; X.fill(); }223 if (stroke) { X.strokeStyle = stroke; X.stroke(); }224}225function line(pts, stroke, width) {226 X.lineWidth = width; X.strokeStyle = stroke; X.beginPath();227 pts.forEach((p, i) => (i ? X.lineTo(p[0], p[1]) : X.moveTo(p[0], p[1]))); X.stroke();228}229function floor(P) {230 for (let k = 0; k <= 10; k++) {231 line([P(-1 + k / 5, 0, -1), P(-1 + k / 5, 0, 1)], GRID, 1);232 line([P(-1, 0, -1 + k / 5), P(1, 0, -1 + k / 5)], GRID, 1);233 }234}235function shade(hex, k) {236 const n = parseInt(hex.slice(1), 16), f = v => Math.max(0, Math.min(255, Math.round(v * k)));237 return 'rgb(' + f(n >> 16 & 255) + ',' + f(n >> 8 & 255) + ',' + f(n & 255) + ')';238}239240// Towers of resting size for the last slices of the book: one tower per level241// per slice, height √(size ÷ max), older slices fading back. Painted far →242// near with three visible faces (front, side, top).243function draw(X, W, H, b) {244 record(b);245 const g = gridOf(), n = g.length, P = camera(W, H);246 let max = 0; for (const col of g) for (const c of col) max = Math.max(max, c.bid + c.ask);247 floor(P);248 const boxes = [], stride = Math.max(1, Math.floor(n / 10));249 for (let k = 0; k < 10; k++) {250 const c = n - 1 - k * stride; if (c < 0) break;251 for (let r = 0; r < NROW; r++) {252 const s = g[c][r].bid + g[c][r].ask; if (!s) continue;253 boxes.push({ x: wx(c, n), z: wz(r), h: Math.sqrt(s / max) * 0.6, col: g[c][r].bid >= g[c][r].ask ? BID : ASK, fade: 1 - k * 0.07 });254 }255 }256 const sx = (2 / Math.max(1, n - 1)) * stride * 0.3, sz = (1 / NROW) * 0.85;257 boxes.sort((a, c) => P(c.x, 0, c.z)[2] - P(a.x, 0, a.z)[2]);258 for (const t of boxes) {259 X.globalAlpha = t.fade;260 const A = (dx, y, dz) => P(t.x + dx, y, t.z + dz);261 poly([A(-sx, 0, sz), A(sx, 0, sz), A(sx, t.h, sz), A(-sx, t.h, sz)], t.col, null); // front262 poly([A(sx, 0, sz), A(sx, 0, -sz), A(sx, t.h, -sz), A(sx, t.h, sz)], shade(t.col, 0.6), null); // side263 poly([A(-sx, t.h, sz), A(sx, t.h, sz), A(sx, t.h, -sz), A(-sx, t.h, -sz)], shade(t.col, 1.3), null); // top264 }265 X.globalAlpha = 1;266}267268// ─── Loop: redraw 4× a second (the live book updates every 100ms) ───────────269let last = 0;270(function loop(now) {271 requestAnimationFrame(loop);272 if (now - last < 250) return;273 last = now; tick();274 const b = snapshot();275 if (!b) return;276 X.clearRect(0, 0, W, H);277 X.font = '11px ui-monospace, Menlo, monospace';278 draw(X, W, H, b);279})(0);280</script>281</body>282</html>See also
Related articles
Related to 3D Book Towers
- Depth City
Trading / Order Book / Views
An isometric 3-D skyline of resting size along the price axis — towers are walls.
- 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.