Appearance
Personalize your experience
Book vs Tape Profile
Order Book ViewWhat is resting at each price beside what has actually traded there — intent against action.
What it answers
Is the resting liquidity actually trading, and where has volume concentrated? Open it from the order book toolbar with View → Book vs Tape Profile, or from Settings → . The , bar and order entry stay in place; only the book pane changes.
Anatomy
How to read it
- resting size beside large , with an unchanged price: absorption — the level is defended.
- resting size with no : untested liquidity, which may be when tested.
- Price above VWAP with buy-dominated bars: buyers have been in control.
- POC is the price that attracted the most activity; price often returns to it.
How it is computed
- Each print is bucketed to the current and counted once (deduplicated by trade id).
- Resting bars come from the book at the same buckets.
- The absorption ratio in the is ÷ resting size at that price.
for (const p of prints) {
if (seen.has(p.id)) continue; seen.add(p.id);
const bucket = Math.round(p.price / step) * step;
const bin = profile.get(bucket) ?? { buy: 0, sell: 0 };
bin[p.side] += p.quantity; profile.set(bucket, bin);
}
const vwap = Σ(price × volume) / Σ(volume);
const poc = argmax(price → buy + sell);
const absorption = (bin.buy + bin.sell) / resting.quantity;Trading signals
Absorption
High into a wall that does not means a passive participant is absorbing aggression. Fade the aggressor near that level.
Exhaustion
Volume drying up at a level that previously traded heavily: interest has moved elsewhere.
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>Book vs tape profile — 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// Left: what is resting at each price now. Right: what has actually traded there184// (buy / sell split). POC = the most-traded price; VWAP = volume-weighted price.185// Big resting size that keeps absorbing prints without moving is a real wall.186const traded = new Map(); let seen = 0;187function draw(X, W, H, b) {188 for (; seen < b.prints.length; seen++) {189 const p = b.prints[seen], k = Math.round(p.price / TICK) * TICK, v = traded.get(k) || { buy: 0, sell: 0 };190 v[p.side] += p.qty; traded.set(k, v);191 }192 if (b.prints.length >= 400) seen = b.prints.length - 1;193 const hi = b.asks.at(-1).price, lo = b.bids.at(-1).price, rows = Math.round((hi - lo) / TICK) + 1, rowH = (H - 20) / rows, cx = W / 2;194 const rest = new Map([...b.bids, ...b.asks].map(l => [l.price, l]));195 const maxR = Math.max(...[...rest.values()].map(l => l.qty));196 let maxT = 1e-9, poc = null, vn = 0, vd = 0;197 for (const [p, v] of traded) { const t = v.buy + v.sell; vn += p * t; vd += t; if (p >= lo && p <= hi && t > maxT) { maxT = t; poc = p; } }198 for (let i = 0; i < rows; i++) {199 const p = hi - i * TICK, y = 18 + i * rowH, l = rest.get(p), v = traded.get(p);200 if (l) { const len = Math.sqrt(l.qty / maxR) * (cx - 50); X.fillStyle = alpha(p > b.mid ? ASK : BID, 0.6); X.fillRect(cx - 40 - len, y, len, rowH - 1); }201 if (v) { const t = v.buy + v.sell, len = (t / maxT) * (cx - 50), bl = (v.buy / t) * len;202 X.fillStyle = alpha(BID, 0.8); X.fillRect(cx + 40, y, bl, rowH - 1); X.fillStyle = alpha(ASK, 0.8); X.fillRect(cx + 40 + bl, y, len - bl, rowH - 1); }203 if (i % Math.ceil(13 / rowH) === 0) txt(fmt(p), cx, y + rowH / 2, 'center', p > b.mid ? ASK : BID);204 }205 const guide = (p, col, label) => { const y = 18 + ((hi - p) / TICK) * rowH + rowH / 2; X.strokeStyle = col; X.beginPath(); X.moveTo(0, y); X.lineTo(W, y); X.stroke(); txt(label, W - 4, y - 7, 'right', col); };206 if (poc !== null) guide(poc, ACCENT, 'POC ' + fmt(poc));207 if (vd > 0) guide(vn / vd, WARN, 'VWAP ' + fmt(vn / vd));208 txt('◀ resting', cx - 44, 8, 'right', MUTED); txt('traded ▶', cx + 44, 8, 'left', MUTED);209}210211// ─── Loop: redraw 4× a second (the live book updates every 100ms) ───────────212let last = 0;213(function loop(now) {214 requestAnimationFrame(loop);215 if (now - last < 250) return;216 last = now; tick();217 const b = snapshot();218 if (!b) return;219 X.clearRect(0, 0, W, H);220 X.font = '11px ui-monospace, Menlo, monospace';221 draw(X, W, H, b);222})(0);223</script>224</body>225</html>See also
Related articles
Related to Book vs Tape Profile
- 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.
- 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.
- 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.
- Depth City
Trading / Order Book / Views
An isometric 3-D skyline of resting size along the price axis — towers are walls.