DOCS
L3 / market-by-order
Order-by-order depth: every individual resting order with its exchange order id, queue priority and nanosecond timestamp.
requiresL3 / Market-by-order or above
The l3 channel streams depth per order instead of per price level. Every individual resting order is its own event — placed, modified, cancelled or filled — carrying the exchange order id, its queue priority at that price and a nanosecond exchange timestamp. It's included from L3 / Market-by-order or above.
L2 tells you there are 42 contracts bid at 23481.25. L3 tells you those 42 are seven separate orders, in what queue order, and which one just pulled. That's the difference between seeing size and seeing intent — queue position, iceberg detection and order-lifetime studies all need L3.
Subscribe
{ "op": "subscribe", "channel": "l3", "symbols": ["NQ"] }from tickstream import Tickstream
ts = Tickstream("sk_live_…")
for e in ts.stream("NQ", channel="l3"):
print(e["action"], e["order_id"], e["side"], e["price"], e["size"])
L3 is available for the CME futures roots (NQ, ES, YM,
RTY…) — the same symbols as Level 2. Subscribing to
l3 does not replace book: you can run both on one connection, and the
aggregated book frames are built from this very order feed.
The l3 message
{
"type": "l3",
"symbol": "NQ",
"contract": "NQZ6",
"order_id": "7412998336104",
"action": "new",
"side": "bid",
"price": 23481.25,
"size": 3,
"priority": 110934567812,
"seq": 49437907,
"ord": 1789775234000188,
"snap": false,
"snap_i": 0,
"snap_rows": 0,
"ts": 1753900800,
"ns": 412903771
} | Field | Type | Description |
|---|---|---|
contract | string | The front-month contract the root resolved to when the frame was sent, e.g. NQZ6. |
order_id | string | Exchange order id. Stable for the whole life of the order — this is what lets you track one participant's order across modifications. |
action | string | new (order added), change (size/price modified) or delete (cancelled or fully filled). |
side | string | bid or ask. |
price | number | Limit price of this order. |
size | number | Displayed size of this order (not the level total). |
priority | number | Exchange queue priority at that price — lower means closer to the front of the queue. A large opaque number (around 1.1e11); compare, don't interpret. |
seq | number | Exchange sequence number. The authoritative event order — sort by this, not by arrival. |
snap | boolean | The opening image. Whenever our upstream feed starts or restarts — announced to every subscriber by a {"type":"feed"} frame — the feed broadcasts every resting order as it stood at that moment, each one carrying snap: true and action: "new". Reset your book when the first one arrives and apply the rest as ordinary adds. The anchor is seq, not the clock: the image asserts that at that exchange sequence the resting orders were exactly these. Its ts is our capture time, because the exchange sends no clock with a snapshot. A new subscription joins mid-stream — there is no per-client replay of the image, so seed from the replay or wait for the next one |
ord | number | Our ingest ordinal: strictly increasing, never reused, assigned in arrival order. It exists because seq does not always separate two events — over a full NQ session eleven keys collide on (ns, order_id, action), one of them covering seven distinct price changes under a single seq. Use it only to break those ties. Where seq separates events, seq wins; ord orders arrival at our socket, not matching at the exchange. Opaque — compare with <, never read it as a timestamp. |
snap_i / snap_rows | integer | Inside an opening image: this order's 1-based position and the image's total row count, so you know when it is complete. 0 outside an image. |
ts | integer | Exchange timestamp in Unix seconds. |
ns | integer | Nanoseconds within that second, as stamped by the exchange. |
Rebuilding a book from L3
An order feed is a delta feed: apply each event to a map keyed by order_id and you hold
the exact book. Aggregate that map by price whenever you need a level view.
# an L3 stream IS the book — aggregate it whenever you want a level view
book = {} # order_id -> (side, price, size)
def apply(e):
if e["action"] in ("new", "change"):
book[e["order_id"]] = (e["side"], e["price"], e["size"])
elif e["action"] == "delete":
book.pop(e["order_id"], None)
def levels(side):
out = {}
for s, px, sz in book.values():
if s == side:
out[px] = out.get(px, 0) + sz
return sorted(out.items(), reverse=(side == "bid"))
gaps are visible, never silent
If your connection can't keep up, we send a {"type":"warning","warning":{"code":"l3_lagged","dropped":N}}
frame instead of quietly skipping events — a book rebuilt across a hidden gap is wrong in ways
that are very hard to notice. On that warning, drop your local book and resubscribe.
GET /v1/l3 — replay
Replay recorded order-level events over REST. Retention is a rolling ~7 days, and only
NQ and ES are recorded for replay (the live l3 channel covers more symbols).
The response is {symbol, start, end, count, truncated, contract, contractSince, events: [...]};
each event has the stream's order fields (order_id, action, side,
price, size, priority, seq, ord, snap,
snap_i, snap_rows, ns) but no type or symbol, and its
ts is in microseconds, not seconds.
Param Required Description symbolyes The instrument, e.g. NQ. start / endno ISO-8601 UTC or Unix seconds. Max window 1 hour per request — an L3 hour is hundreds of thousands of events. Defaults: end = now, start = 5 minutes before. limitno Max events (default 50,000, max 500,000).
# start/end must fall inside the last ~7 days
curl "https://api.tick-stream.xyz/v1/l3?symbol=NQ&start=2026-09-25T13:30:00Z&end=2026-09-25T14:00:00Z" \
-H "Authorization: Bearer sk_live_…"
no L3 history before we recorded it
There is no vendor archive for order-level data — nobody sells CME MBO history back in time, and
our own tick/L2 archive (2019 →) does not contain it either. L3 replay is a rolling ~7-day window
of our own recording. If you need L3 for a specific study, tell us the symbol so it's in the
recording set:
support@tick-stream.xyz.
plan Without the package, subscribing to l3 returns an error frame {"type":"error","error":{"code":"plan_required",…}} (the socket stays open), and /v1/l3 returns HTTP 403 plan_required. See what the packages include.