Skip to content
Market History

Reconstruct the top of book from L3 events

L3 events are every add, modify, cancel and fill on one venue’s book, order by order. Replay a session from its first event and you hold that book at any instant. This guide builds the book in plain Python, reads its top every second, and checks the result against the L2 rows, which are computed the same way.

You need the Python client and a key with bought bars. Ten events bill one bar: a median ticker’s day is about 1,000 events (100 bars), a busy one millions. Watch X-Bars-Charged, or try a quiet ticker first.

from collections import defaultdict
orders = {} # order_id -> [side, price, size]
levels = {
"B": defaultdict(lambda: [0, 0]),
"A": defaultdict(lambda: [0, 0]),
} # price -> [shares, orders]
def put(oid, side, price, size):
orders[oid] = [side, price, size]
level = levels[side][price]
level[0] += size
level[1] += 1
def take(oid):
order = orders.pop(oid, None)
if order:
side, price, size = order
level = levels[side][price]
level[0] -= size
level[1] -= 1
if level[1] == 0:
del levels[side][price]
return order
def top():
bids, asks = levels["B"], levels["A"]
bid = (b := max(bids), *bids[b]) if bids else (None, None, None)
ask = (a := min(asks), *asks[a]) if asks else (None, None, None)
return bid + ask # bid_px, bid_sz, bid_ct, ask_px, ask_sz, ask_ct
def apply(e):
act, oid = e["action"], e["order_id"]
if act == "A" and e["side"] in ("B", "A"):
take(oid) # an id added again replaces the old order
put(oid, e["side"], e["price"], e["size"])
elif act == "M": # new price and size, same side; size 0 removes it
old = take(oid)
if old and e["size"] > 0:
put(oid, old[0], e["price"], e["size"])
elif act == "C":
take(oid)
elif act == "F": # a resting order traded: it shrinks at its own price
old = take(oid)
if old and old[2] > e["size"]:
put(oid, old[0], old[1], old[2] - e["size"])
elif act == "R": # the venue cleared the book
orders.clear()
levels["B"].clear()
levels["A"].clear()
# T and B are prints and breaks: they rest nothing

F reduces the resting order named by order_id. Its price is the print, which can differ from the order’s own price, so the level the shares come off is the stored one.

from markethistory import Client
mh = Client() # reads MARKET_HISTORY_KEY
day = "2026-10-30"
mine, last, second = {}, (None,) * 6, None
for e in mh.each(mh.mbo, "AAPL", date=day, limit=100_000):
s = e["timestamp"][:19] # the event's second, UTC
if second is not None and s != second:
t = top() # the book after the last event of the previous second
if t != last:
mine[second] = last = t
second = s
apply(e)
if second is not None and top() != last:
mine[second] = top()

Events arrive in sequence order, the venue’s order of record, and a session starts from an empty book, so no snapshot is needed. Keying the second on the timestamp text keeps every nanosecond exact. A second gets a row only when its top differs from the row before, which is what the L2 endpoint serves.

served = {
r["timestamp"][:19]: (
r["bid_px"],
r["bid_sz"],
r["bid_ct"],
r["ask_px"],
r["ask_sz"],
r["ask_ct"],
)
for r in mh.each(mh.l2, "AAPL", date=day, limit=100_000)
}
differ = [s for s in served.keys() | mine.keys() if served.get(s) != mine.get(s)]
print(f"{len(served)} seconds served, {len(mine)} rebuilt, {len(differ)} differ")

Both come from the same events by the same rules, so the count that differs should be 0. From here the book is yours: levels holds every price level, so depth, imbalance or queue position at any event are a few lines more.

  • Order by sequence, never by timestamp. Across tickers, timestamps can step back by up to milliseconds; within one ticker’s pages the order is already right.
  • order_id is not a key forever. An id can come back after a cancel; take() before put() handles it.
  • One venue. This is that venue’s book, not the national best bid and offer.
  • Crossed or locked tops happen in the data as published. Do not drop them silently.