Order Flow

Technical notes · how the numbers are built

Technical reference

Eight sockets, one bucket store, zero server-side flow data

Every number on the Order Flow page is produced inside your browser. This page documents where the data comes from, how each venue is normalised, the exact formulas, how memory stays bounded, and where the implementation intentionally differs from a full exchange tape.

🔌 8 websocket feeds 🧮 1-second buckets 🧵 No build step 🛡 Read-only

01Data path

exchange public WS → per-venue adapter → recordTrade()
→ session aggregates (spot / futures / whale)
→ 1-second bucket store → rolling window sums → DOM (throttled)

The dashboard server never sees trade data. public/orderflow.html loads public/js/orderflow.js, which opens eight public websocket connections directly from the browser. The page therefore works on a static host as well as on Render, and the hosting tier only serves three small static files.

One adapter per venue

Each entry in the STREAMS array declares a URL builder, an optional onOpen subscription message and an onMessage handler. Adapters only translate payloads into { exchange, market, side, price, qty }; all maths happens once, inside recordTrade().

Two presentation rules live outside the maths: the scheduleRender() throttle (200 ms) and the one-second tick() that prunes buckets, repaints windows and updates the clock.

02Feed inventory

#VenueEndpoint / subscriptionBucket
1Binance Spotwss://stream.binance.com:9443/ws/{sym}@aggTradeSpot
2Binance USD-Mwss://fstream.binance.com/ws/{sym}@aggTradeFutures
3Bybit Spotwss://stream.bybit.com/v5/public/spot · publicTrade.{SYM}Spot
4Bybit Linearwss://stream.bybit.com/v5/public/linear · publicTrade.{SYM}Futures
5OKXwss://ws.okx.com:443/ws/v5/public · trades on {SYM}-USDT and {SYM}-USDT-SWAPSpot / Futures
6Gate.io Spotwss://api.gateio.ws/ws/v4/ · spot.tradesSpot
7Gate.io Futureswss://fx-ws.gateio.ws/v4/ws/usdt · futures.tradesFutures
8Coinbasewss://ws-feed.exchange.coinbase.com · matches on {SYM}-USD and {SYM}-USDTSpot

The chip “Open feeds 6/8” counts sockets in the OPEN state, not markets that actually produced trades. A venue can be connected and still contribute nothing if the pair is not listed there.

Coinbase fallback

Coinbase rejects a subscription if any requested product is unknown, so the adapter sends {SYM}-USD and {SYM}-USDT together and, on an error frame, resubscribes to {SYM}-USD only. The flag resets whenever the pair changes.

03Normalisation rules

VenuePriceQuantitySide source
Binancepqm = true → sell (buyer is maker), otherwise buy
BybitpvS (Buy / Sell)
OKXpxszside (buy / sell)
Gate.io spotresult.priceresult.amountresult.side
Gate.io futuresprice|size|sign of size (positive = buy)
Coinbasepricesizeside (buy / sell)

Every adapter also handles batch frames: Bybit and OKX deliver arrays in data, Gate futures delivers an array in result, Gate spot usually a single object. Unknown or malformed values are dropped before they ever reach the aggregates:

if (!Number.isFinite(price) || !Number.isFinite(qty) || price <= 0 || qty <= 0) return;

Market mapping is fixed per stream, except OKX, which inspects instId for SWAP. A single OKX socket therefore feeds both the spot and the futures bucket.

Contract size normalisation

Futures venues quote size in contracts, not coins. Valuing a contract count at the coin price would overstate the flow by one to four orders of magnitude, so the adapters convert first:

Gate.io futures: qty = |size| × quanto_multiplier
OKX swap:       qty = sz × ctVal
  • Gate.io: multiplier is read once per pair from /futures/usdt/contracts/{'{'}SYM{'}'}_USDT (CORS-enabled), e.g. 0.0001 for BTC.
  • OKX: ctVal is read from the public instruments endpoint; if that host is unreachable, the instruments websocket channel is used as a fallback.
  • If neither source resolves a multiplier, the affected prints are dropped instead of being guessed. The Gate futures socket is then not opened at all, so the feed counter stays honest.
  • Binance, Bybit, OKX spot, Gate spot and Coinbase already report quantity in the base asset and need no conversion.

Side is always taker-side

A “buy” means an aggressive buyer crossed the spread. That is the only consistent definition across venues, and it is what makes cross-exchange aggregation meaningful.

04Formulas

Per print

amount = price × qty
spot[buy|sell] += amount  ·  futures[buy|sell] += amount
if (amount ≥ 10000) whale[buy|sell] += amount
bucket[floor(now / 1000)] += amount on the same side

Per rolling window

windowBuy = Σ bucket.buy where now − bucket.t ≤ window.ms
windowSell = Σ bucket.sell where now − bucket.t ≤ window.ms
buyShare = windowBuy / (windowBuy + windowSell) × 100
net = windowBuy − windowSell

Combined read

combinedPct = (Σ buyShare of enabled windows) / enabledCount
combinedNet = (Σ net of enabled windows) / enabledCount
empty window contributes 50% and 0 net

Cards

total = spot + futures (whales are a subset, never added twice)
cardPct = buy / (buy + sell) × 100
cardVol = buy + sell

All arithmetic uses JavaScript doubles. Nothing is rounded while aggregating; rounding only happens when a value is written to the DOM, which is why the underlying ordering never drifts.

Windows are second-aligned

Because the store is bucketed per second, a window boundary lands on a whole second. The maximum difference versus an exact millisecond cutoff is one second of prints, which does not change the meaning of a 1m–1d window.

05Memory & rendering

A busy pair can print hundreds of trades per second. Keeping every print for a 24-hour window would need millions of objects, so the store is folded into one bucket per second.

StructureBoundPurpose
state.buckets≤ ~86,460 entries (24h + 60s)Buy/sell totals per second, pruned on every tick.
state.metrics6 numbersSession totals for spot, futures and whales.
state.pendingRows240Prints waiting for the next paint batch.
Tape DOM40 rowsNewest prints; older rows are removed.
state.pulse24 entriesThe mini bar strip beside the tape heading.

windowSums() walks the bucket array once, backwards, and freezes a window as soon as the cursor passes its cutoff — so nine windows cost roughly one pass, not nine. Rendering is coalesced to at most five repaints per second (RENDER_INTERVAL_MS = 200) no matter how fast prints arrive, and the tape is inserted as a single DocumentFragment.

Why the 1d window stays empty at first

The bucket store only learns about prints while the page is open. There is no historical backfill, so a 1d window fills gradually and needs a genuinely long, visible session.

06Socket lifecycle

EventBehaviour
Page load / pair switchconnectAll() closes anything open, rebuilds eight entries and opens them.
OpenOptional subscription frame is sent; the feed counter and status pill update.
Close (unexpected)That socket retries with exponential backoff: 1s, 2s, 4s, 8s, 16s, capped at 30s, plus 0–400 ms jitter.
Malformed frameParsing or adapter errors are swallowed; the feed keeps running.
Tab hiddenvisibilitychange closes every socket and marks the status “Tab hidden”.
Tab visible againAll feeds reconnect unless you paused manually.
Page leftpagehide sets a leaving flag, closes every socket and clears the retry timers.
Back/forward cachepageshow with persisted reconnects.
Pause buttonuserPaused blocks reconnection until you resume; the numbers keep their values.

No orphan sockets

Before closing, each entry bumps a generation counter and nulls its handlers, so late messages from an old connection cannot leak into the current session. Retry timers are cleared on every close-all.

07Time handling

The tape shows arrival time from your browser’s clock, formatted as 24-hour HH:MM:SS in your local zone. The full local date and time is available in the cell’s tooltip.

const pad2 = (n) => String(n).padStart(2, '0');
`${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
  • No timezone label is appended — the clock is already local, and printing “PKT” would be wrong for every other visitor.
  • Exchange payload timestamps are not used: five venues, three timestamp formats, and no shared clock.
  • Buckets are aligned with Math.floor(now / 1000) * 1000, so window boundaries are stable.
  • Session start, elapsed time, next auto-refresh and the live clock all derive from the same Date.now() source.

Your machine’s clock matters

If your operating system clock is skewed, arrival times and session timers will be skewed by the same amount. Enable automatic time sync if the seconds look wrong.

08Known limitations

LimitationEffect
No historical backfillWindows only contain prints seen while the page was open and connected.
No order bookDepth, cancels, icebergs and spoofing are invisible; the tool measures executions only.
Aggregated Binance eventsaggTrade merges same-price fills of one taker order into a single print.
Second-resolution windowsWindow edges snap to whole seconds; sub-second sequencing is not modelled.
Uneven venue coverageA pair may exist on two of five venues; the aggregate is then dominated by those two.
Contract-size lookupsIf a futures multiplier cannot be resolved, those prints are skipped rather than valued 1:1 (see section 03).
Silent feedsA socket can be open while the venue sends no prints (regional restriction or unlisted market). “Open feeds 8/8” means connected, not contributing.
Coinbase side semanticsThe adapter passes match.side through exactly as delivered — Coinbase defines it against the resting order, unlike the taker-side venues. Treat Coinbase as supporting colour, not as a directional tie-breaker.
Browser clockArrival times inherit any skew in the operating system clock.
Network restrictionsSome ISPs or regions block specific exchange websockets; those feeds stay unreachable until they are.
Session-scoped totalsCards and tape reset on reload, pair change, auto-refresh or manual reset — nothing is persisted.
Float arithmeticSums use IEEE-754 doubles; displayed values are rounded, so cent-level differences are normal.
Not an execution toolNo orders, no API keys, no account access, no position awareness.

Do not treat this as a market-wide tape

It is a five-venue sample of public prints. Dark pools, OTC desks, internal matching and every venue not in the list are outside the measurement.

09File map & extending

FileRole
public/orderflow.htmlPage shell: top bar, controls, timeframe row, cards, tape, footer.
public/css/orderflow.cssAll tool styles, namespaced .of-*; reuses the theme tokens from styles.css.
public/js/orderflow.jsAdapters, aggregates, bucket store, rendering, lifecycle.
public/orderflow-guide.htmlTrader-facing explanation.
public/orderflow-technical.htmlThis reference.
public/orderflow-disclaimer.htmlRisk, data and liability notice.
public/js/pages.jsShared theme toggle and TOC scroll-spy for the documentation pages.

Adding a venue

  1. Add one object to STREAMS with key, name, url(coin), optional onOpen(ws, coin) and onMessage(data, emit, coin, ws).
  2. Emit { exchange, market: 'Spot' | 'Futures', side: 'buy' | 'sell', price, qty }.
  3. Add a colour to EXCHANGE_COLORS.
  4. Update the feed-count copy on this page and in the guide.

Other knobs

  • Whale threshold: WHALE_THRESHOLD (default 10000).
  • Tape depth: MAX_TAPE_ROWS (default 40).
  • History depth: MAX_LOG_MS (default 24h + 60s).
  • Timeframes: the TIMEFRAMES array; pills are generated from it, so the HTML does not need editing.
  • Auto-refresh options: the AUTO_REFRESH_OPTIONS array.
  • Repaint rate: RENDER_INTERVAL_MS (default 200 ms).

Keep the CSS namespaced

The correlation dashboard is a separate page with generic selectors for header, table, td and friends. New Order Flow styles must stay inside .of-* classes so nothing bleeds across pages.

10Verification checklist

1

Syntax

node --check public/js/orderflow.js should pass with no output.

2

Server suite

npm test covers the correlation engine and must stay green; the Order Flow page does not touch it.

3

Live page

Open / (Home); the status pill turns live, “Open feeds” climbs, and rows start filling the tape.

4

Pause

Press Pause feed: the pill shows Paused, “Open feeds” drops to 0, and the numbers freeze instead of resetting.

5

Hidden tab

Switch tabs for a few seconds and come back: the feeds reconnect and only the newest prints appear.

6

Pair switch

Search ETHUSDT: the pair label, quantity header and session clock reset; no BTC prints remain.

7

Deep links

Every anchor in this page and in the guide should resolve to a real section id.

Debug hook

A read-only helper is exposed for smoke tests: window.__ofDebug() returns the coin, print count, bucket count, open feeds and the three metric buckets. It performs no writes and is safe to leave in production.