Appearance

Personalize your experience

Mode
Accent Color
Direction
Level Persistence

Level Persistence

Order Book View

How long each level has rested, and which levels flicker in and out — a spoofing detector.

What it answers

Which liquidity is stable, and which keeps appearing and vanishing? Open it from the order book toolbar with View → Level Persistence, 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 Level Persistence 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. Ages and flicker rates become meaningful after about 30 seconds.

Anatomy

Bar length
√(size ÷ largest size).
Brightness
Time the level has rested in its current stint, relative to the oldest visible level.
White cap / outline
Placed in the last 3 seconds.
Amber hatching / ⚠
Flickering: at least 3 appear/vanish flips and 6 or more per minute.
Right column
Current resting age.

How to read it

  1. Bright long bars are patient, stable liquidity, and the most trustworthy levels.
  2. Hatched levels keep coming and going. Do not lean on them.
  3. A hatched level that disappears whenever price nears it is the spoofing pattern.

How it is computed

  • Each (side, price) is tracked across updates: when its current stint started, when it was first seen, and how many times it flipped.
  • Flicker rate = flips ÷ minutes since first seen.
  • Levels absent for more than 5 minutes are forgotten.
Core calculation
const life = lives.get(key) ?? { since: now, first: now, flips: 0, present: true };
if (!life.present) { life.present = true; life.since = now; life.flips++; }   // reappeared
// …on the next update, if missing: life.present = false; life.flips++
const ratePerMin = life.flips / Math.max(0.25, (now - life.first) / 60000);
const flickering = life.flips >= 3 && ratePerMin >= 6;

Trading signals

Spoof suspect

, flickering, never traded: discount the level entirely when reading imbalance.

Patient wall

, long-resting and stable: a real participant. Expect it to act as support or resistance.

!
Caution:Price moving across a level boundary also removes and re-adds levels at the edge of the visible range. Judge flicker on levels well inside the book.

Settings that affect this view

Levels
Number of tracked .
Price grouping
Changing it resets tracking (the buckets change).

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.

Level persistence — standalone HTML, live Binance feed, no dependencies
orderbook-persistence.html
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Level persistence — 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 ───────────────────────────────────────────────────────────────────// How long each level has rested (brighter = older) and how often it appears// and vanishes. A level that flips several times a minute while never trading// is a spoofing suspect — hatched in amber. (Two levels here flicker on purpose.)const life = new Map();function draw(X, W, H, b) {  const now = Date.now(), present = new Set();  for (const [lv, s] of [[b.bids, 'bid'], [b.asks, 'ask']]) for (const l of lv) {    const k = s + l.price; present.add(k); const e = life.get(k);    if (!e) life.set(k, { since: now, first: now, flips: 0, on: true });    else if (!e.on) { e.on = true; e.since = now; e.flips++; }  }  for (const [k, e] of life) if (e.on && !present.has(k)) { e.on = false; e.flips++; }  const rows = [...[...b.asks].reverse().map(l => [l, 'ask', ASK]), ...b.bids.map(l => [l, 'bid', BID])];  const rowH = (H - 4) / rows.length, maxQ = Math.max(...rows.map(([l]) => l.qty));  let oldest = 30000; for (const [l, s] of rows) oldest = Math.max(oldest, now - life.get(s + l.price).since);  rows.forEach(([l, s, col], i) => {    const e = life.get(s + l.price), age = now - e.since, y = 2 + i * rowH, len = Math.sqrt(l.qty / maxQ) * (W - 160);    const rate = e.flips / Math.max(0.25, (now - e.first) / 60000), flicker = e.flips >= 3 && rate >= 6;    X.fillStyle = alpha(col, 0.12 + clamp(age / oldest, 0, 1) * 0.78); X.fillRect(70, y + 0.5, len, rowH - 1);    if (flicker) { X.fillStyle = WARN; X.fillRect(64, y, 3, rowH); }    if (rowH > 10) { txt(fmt(l.price), 64, y + rowH / 2, 'right', col); txt((flicker ? '⚠ ' : '') + (age / 1000).toFixed(0) + 's', W - 6, y + rowH / 2, 'right', flicker ? WARN : 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 →