Appearance

Personalize your experience

Mode
Accent Color
Direction
Order-Flow Imbalance (OFI)

Order-Flow Imbalance (OFI)

Order Book View

Top-of-book order-flow imbalance per update, its cumulative drift and a live pressure gauge.

What it answers

Is the being lifted or hit, now? Open it from the order book toolbar with View → Order- Imbalance (OFI), 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 Order-Flow Imbalance (OFI) 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. Needs a few updates; keeps the last 300.

Anatomy

Bars
OFI per update. Up (bid colour) is buy pressure, down (ask colour) is sell pressure.
Violet line
OFI over the , on its own scale.
Gauge (outer arc)
Normalised pressure over the last 40 updates: Σ OFI ÷ Σ |OFI|, −100%…+100%.
Gauge (inner arc)
Top-5 at this instant, for comparison.

How to read it

  1. Sustained positive OFI with a rising line: buyers are adding at the bid and lifting the ask.
  2. If the gauge and the inner imbalance disagree, the book looks one way but is going the other — trust the flow.
  3. OFI without a price change: liquidity is being absorbed.

How it is computed

  • For consecutive top-of-book states (bid₀, bidQ₀, ask₀, askQ₀) → (bid, bidQ, ask, askQ):
  • e = 1[bid ≥ bid₀]·bidQ − 1[bid ≤ bid₀]·bidQ₀ − 1[ask ≤ ask₀]·askQ + 1[ask ≥ ask₀]·askQ₀
  • This is the from Cont, Kukanov & Stoikov (2014). Over short horizons mid-price changes are approximately linear in OFI.
Core calculation
let e = 0;
if (bid >= prev.bid) e += bidQty;
if (bid <= prev.bid) e -= prev.bidQty;
if (ask <= prev.ask) e -= askQty;
if (ask >= prev.ask) e += prev.askQty;
// gauge
const recent = series.slice(-40).map(p => p.ofi);
const pressure = recent.reduce((s, v) => s + v, 0) / recent.reduce((s, v) => s + Math.abs(v), 0);

Trading signals

Short-horizon direction

Positive OFI predicts upward mid moves over the next few seconds; it is a core input in many HFT models.

Divergence

Price falling while OFI rises: sellers are hitting into passive buying that is likely to hold.

!
Caution:OFI is computed from the aggregated . With coarse , queue changes inside a bucket out. Use Auto (raw ) grouping for the purest signal.

Settings that affect this view

Price grouping
Affects what counts as a change.
Glow
Gauge and line glow.

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.

Order-flow imbalance — standalone HTML, live Binance feed, no dependencies
orderbook-ofi.html
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Order-flow imbalance — 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 ───────────────────────────────────────────────────────────────────// Cont–Kukanov–Stoikov OFI at the touch, per update://   e = 1[bid ≥ bid₀]·bidQ − 1[bid ≤ bid₀]·bidQ₀ − 1[ask ≤ ask₀]·askQ + 1[ask ≥ ask₀]·askQ₀// Positive = buying pressure. Bars per update, cumulative OFI in violet, and a// gauge of normalised pressure over the last 40 updates.const hist = []; let prevTop = null;function draw(X, W, H, b) {  const bb = b.bids[0], ba = b.asks[0]; let e = 0;  if (prevTop) {    if (bb.price >= prevTop.bp) e += bb.qty; if (bb.price <= prevTop.bp) e -= prevTop.bq;    if (ba.price <= prevTop.ap) e -= ba.qty; if (ba.price >= prevTop.ap) e += prevTop.aq;  }  prevTop = { bp: bb.price, bq: bb.qty, ap: ba.price, aq: ba.qty };  hist.push(e); if (hist.length > 200) hist.shift();  const gw = Math.min(220, W * 0.32), R = W - gw - 10, zero = H / 2, half = zero - 14, m = Math.max(1e-9, ...hist.map(Math.abs));  const bw = R / hist.length;  let run = 0; const cum = hist.map(v => (run += v)), mc = Math.max(1e-9, ...cum.map(Math.abs));  hist.forEach((v, i) => { const len = (Math.abs(v) / m) * half; X.fillStyle = alpha(v >= 0 ? BID : ASK, 0.75); X.fillRect(i * bw, v >= 0 ? zero - len : zero, Math.max(1, bw - 0.5), len); });  X.beginPath(); cum.forEach((v, i) => (i ? X.lineTo : X.moveTo).call(X, i * bw, zero - (v / mc) * half)); X.strokeStyle = ACCENT; X.lineWidth = 1.6; X.stroke();  const recent = hist.slice(-40), gross = recent.reduce((s, v) => s + Math.abs(v), 0) || 1, p = clamp(recent.reduce((s, v) => s + v, 0) / gross, -1, 1);  const gx = R + 10 + gw / 2, gy = H * 0.62, gr = Math.min(gw / 2 - 12, H * 0.45);  X.lineWidth = 8; X.lineCap = 'round';  X.strokeStyle = alpha(ASK, 0.3); X.beginPath(); X.arc(gx, gy, gr, Math.PI, Math.PI * 1.5); X.stroke();  X.strokeStyle = alpha(BID, 0.3); X.beginPath(); X.arc(gx, gy, gr, Math.PI * 1.5, Math.PI * 2); X.stroke();  const ang = Math.PI * 1.5 + p * (Math.PI / 2);  X.strokeStyle = p >= 0 ? BID : ASK; X.beginPath(); X.arc(gx, gy, gr, Math.min(Math.PI * 1.5, ang), Math.max(Math.PI * 1.5, ang)); X.stroke();  X.lineWidth = 2; X.lineCap = 'butt'; X.strokeStyle = TEXT; X.beginPath(); X.moveTo(gx, gy); X.lineTo(gx + Math.cos(ang) * (gr - 4), gy + Math.sin(ang) * (gr - 4)); X.stroke();  txt((p >= 0 ? '+' : '') + (p * 100).toFixed(0) + '%', gx, gy + 18, 'center', p >= 0 ? BID : ASK);}// ─── 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

  • Level Imbalance

    Trading / Order Book / Views

    Per-level bid vs ask pressure, diverging from zero, with the cumulative drift through the book.

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

Browse all knowledge base articles →