Lesson 1.1.3 · 60 min
Order book: bid, ask, spread and depth
The order book is the list of what others are willing to buy and sell, and at what price. This lesson reads it at its three levels of detail on the NOVA session: the best price, the ten-level ladder and the order-by-order messages, which are used to rebuild the book and uncover an iceberg order.
- Module
- 1.1 · How markets work
- Duration
- 60 min
- Prerequisites
- 1.1.1 · Market participants and trading venues, 1.1.2 · Sessions, auctions, holidays and calendars
- Author
- Jesús Cuesta
- Updated
Level 1 lessons
1.1 · How markets work
1.2 · Asset classes
- 1.2.1 Stocks: splits, dividends and corporate actions
- 1.2.2 ETFs: replication, tracking error, leveraged ETFs and UCITS
- 1.2.3 Futures: specifications, margin, roll and contango
- 1.2.4 Forex: pairs, pips, lots, rollover and CFDs
- 1.2.5 Options: calls, puts, intrinsic value and time value
- 1.2.6 Crypto: spot, perpetuals, funding and custody
- 1.2.7 Fixed income and interest rates; CFDs and leveraged products
1.3 · Market data sources
- 1.3.1 Types of market data: EOD, bars, ticks, order book and reference
- 1.3.2 Free market data sources
- 1.3.3 Retail and prosumer market data APIs
- 1.3.4 Professional market data providers
- 1.3.5 Crypto and on-chain data
- 1.3.6 Brokers as market data sources
- 1.3.7 Market data licensing and terms of use
- 1.3.8 How to choose a market data provider
1.4 · Financial data engineering
- 1.4.1 Downloading over REST and websockets: pagination, rate limits and retries
- 1.4.2 Data cleaning: missing values, bad ticks, gaps and time zones
- 1.4.3 Adjusting for splits and dividends: adjusted and unadjusted prices
- 1.4.4 Universes without survivorship bias: delistings, mergers and symbol changes
- 1.4.5 Building bars: time, tick, volume, dollar and imbalance
- 1.4.6 Continuous futures series: roll rules, difference adjustment and ratio adjustment
- 1.4.7 Storage: CSV, Parquet, partitioning, DuckDB, TimescaleDB and ArcticDB
- 1.4.8 Reproducible pipeline: download, validation, incremental updates and quality tests
1.5 · Fundamental and macro data
1.6 · Financial exploratory analysis
1.7 · Programmed technical analysis
What an order book is
In continuous trading, every limit order that is not executed on arrival stays waiting in the order book: a sorted list of what other participants are willing to buy and sell. The book has two sides:
- Bid (buys). Buy orders sorted from highest to lowest price. The first one is the best bid: the highest price someone is paying right now.
- Ask or offer (sells). Sell orders sorted from lowest to highest price. The first one is the best ask: the lowest price at which someone is selling right now.
The distance between the best ask and the best bid is the bid-ask spread. If someone wants to buy right away, they pay the ask; if they want to sell right away, they receive the bid. That is why the spread is the first cost of trading (lesson 1.1.5). The mid price is the point between the two, and it is the usual reference for “the price” at a given instant.
Each level is a price with orders. Its size is the sum of the shares of all the orders at that price. Depth is how much there is in the book: at one level, in the first five, within one cent of the mid… Prices move from tick to tick, the minimum price increment: for NOVA, as for most US stocks, one cent.
The data for this lesson is NOVA's lit book, the fictitious stock of lesson 1.1.1, on June 2, 2025. Remember the simulator's simplification: there is a single lit book, the aggregate of the venues with a book.
Three levels of detail: L1, L2 and L3
Providers sell the book at three degrees of detail, and each one is obtained by aggregating the next:
Mermaid source
flowchart TB L3[L3: each order] -->|sum by price| L2[L2: by level] L2 -->|only the first| L1[L1: best price] L3 -->|executions| T[Trades]
| Level | What it contains | In the course data | What it is for |
|---|---|---|---|
| L1 (top of book) | Best bid and best ask with their sizes | nova_cotizaciones.parquet, one row per change | Spread, mid price, aligning trades (1.1.1) |
| L2 (market by price) | Several levels per side, with size and number of orders | nova_libro_l2.parquet, 10 levels every 10 s | Depth, impact of a large order, imbalance |
| L3 (market by order) | Each order: add, cancel, modify and execution, with its identifier | nova_libro_l3.parquet, 5 minutes | Priority queue, icebergs, exact reconstruction of the book |
The more detail, the more data volume and the more expensive (lesson 1.3.1). For NOVA, five minutes of L3 are 7,241 messages: at that rate the full session would exceed half a million, against 129,247 L1 changes in the whole day. For a system that trades daily, L1 and bars are enough; to study execution costs you need L2; to model the queue or the arrival of orders, L3.
A snapshot of the book, level by level
NOVA's L2 snapshots have one row per level and side, every 10 seconds. This is the one at 19:00:00 UTC, 15:00 New York time, laid out as a ladder: buys on the left, sells on the right.
import numpy as np
import pandas as pd
l2 = pd.read_parquet("nova_libro_l2.parquet")
foto = l2[l2["ts"] == "2025-06-02 19:00:00+00:00"]
bid = foto[foto["lado"] == "bid"].set_index("nivel")
ask = foto[foto["lado"] == "ask"].set_index("nivel")
escalera = pd.concat({
"bid": bid[["ordenes", "tamano", "precio"]],
"ask": ask[["precio", "tamano", "ordenes"]],
}, axis=1)
escalera bid ask
ordenes tamano precio precio tamano ordenes
nivel
1 36.0 13432.0 52.74 52.75 1870 5
2 44.0 9380.0 52.73 52.76 9900 18
3 41.0 9059.0 52.72 52.77 13280 29
4 5.0 1800.0 52.71 52.78 17322 54
5 2.0 600.0 52.70 52.79 14521 49
6 1.0 100.0 52.69 52.80 5142 30
7 3.0 526.0 52.67 52.81 9303 22
8 1.0 300.0 52.64 52.82 4300 11
9 NaN NaN NaN 52.83 1229 8
10 NaN NaN NaN 52.84 2252 6The figure at the top draws the same ladder. Several things are visible that are not in a price chart. The buy side only has eight levels at that instant, and between 52.69 and 52.67 the 52.68 is missing: a “level” is a price with orders, not every tick. The first buy level has 13,432 shares spread over 36 orders; the sell level, only 1,870 in 5. And across the ten levels there are more than twice as many shares for sale as for purchase. The snapshot is an instant: ten seconds later it will be different.
The L1 measures come from the first level on each side:
def medidas_l1(foto):
b = foto[(foto["lado"] == "bid") & (foto["nivel"] == 1)].iloc[0]
a = foto[(foto["lado"] == "ask") & (foto["nivel"] == 1)].iloc[0]
medio = (b["precio"] + a["precio"]) / 2
horquilla = a["precio"] - b["precio"]
qb, qa = b["tamano"], a["tamano"]
return pd.Series({
"bid": b["precio"], "ask": a["precio"], "medio": medio,
"horquilla_ticks": round(horquilla / 0.01),
"horquilla_pb": 1e4 * horquilla / medio,
"desequilibrio": (qb - qa) / (qb + qa),
"microprecio": (b["precio"] * qa + a["precio"] * qb)
/ (qb + qa),
})
medidas_l1(foto).round(4)bid 52.7400
ask 52.7500
medio 52.7450
horquilla_ticks 1.0000
horquilla_pb 1.8959
desequilibrio 0.7556
microprecio 52.7488
dtype: float64The spread is one tick, 1.9 basis points over the mid price. First-level imbalance ranges from −1 (only selling) to +1 (only buying):
Here it is 0.76: almost the whole first level is buying. The microprice weights each price by the size of the opposite side:
With 13,432 shares waiting to buy at 52.74 and only 1,870 selling at 52.75, the likely outcome is that the ask is depleted before the bid, and the mid price rises half a cent. The microprice, 52.7488, is already almost at the ask.
Cumulative depth
The practical question is how many shares can be bought or sold without straying too far from the mid price. Cumulative depth answers it: for each price, the sum of what there is from the best level up to it.
medio = medidas_l1(foto)["medio"]
prof = foto.sort_values(["lado", "nivel"]).copy()
prof["acumulado"] = prof.groupby("lado")["tamano"].cumsum()
prof["distancia_cent"] = (100 * (prof["precio"] - medio).abs()).round(1)
print(prof.groupby("lado")["tamano"].sum().to_string())
prof.pivot(index="nivel", columns="lado",
values=["distancia_cent", "acumulado"])lado
ask 79119
bid 35197
distancia_cent acumulado
lado ask bid ask bid
nivel
1 0.5 0.5 1870.0 13432.0
2 1.5 1.5 11770.0 22812.0
3 2.5 2.5 25050.0 31871.0
4 3.5 3.5 42372.0 33671.0
5 4.5 4.5 56893.0 34271.0
6 5.5 5.5 62035.0 34371.0
7 6.5 7.5 71338.0 34897.0
8 7.5 10.5 75638.0 35197.0
9 8.5 NaN 76867.0 NaN
10 9.5 NaN 79119.0 NaNWithin 2.5 cents of the mid there are 31,871 shares on the bid (three levels) and 25,050 on the ask. Beyond that, the sell side keeps growing while the buy side stays almost flat: across 10 levels, 35,197 against 79,119. A buy order for 20,000 shares would need the first three sell levels; a sell order of the same size would be filled with the first buy level and part of the second. How much it costs in each case is the order's impact, which lesson 1.1.4 computes by walking the book and lesson 1.1.5 turns into a cost.
The spread over the session
In a liquid stock priced at a few tens of dollars, the spread usually sits at the minimum, one tick. The session's 2,340 snapshots let you see when it widens:
n1 = l2[l2["nivel"] == 1].pivot(index="ts", columns="lado",
values=["precio", "tamano"])
pb, pa = n1[("precio", "bid")], n1[("precio", "ask")]
qb, qa = n1[("tamano", "bid")], n1[("tamano", "ask")]
ticks = ((pa - pb) / 0.01).round()
pbs = 1e4 * (pa - pb) / ((pa + pb) / 2)
hora_ny = n1.index.tz_convert("America/New_York")
tabla = pd.DataFrame({"ticks": ticks.values, "pb": pbs.values,
"mas_de_1": (ticks > 1).values}, index=hora_ny)
print(ticks.value_counts().sort_index().to_string())
tabla.resample("30min").mean().round(3)1.0 2252
2.0 87
3.0 1
ticks pb mas_de_1
ts
2025-06-02 09:30:00-04:00 1.022 1.916 0.022
2025-06-02 10:00:00-04:00 1.039 1.947 0.039
2025-06-02 10:30:00-04:00 1.028 1.925 0.028
2025-06-02 11:00:00-04:00 1.033 1.926 0.028
2025-06-02 11:30:00-04:00 1.044 1.940 0.044
2025-06-02 12:00:00-04:00 1.039 1.933 0.039
2025-06-02 12:30:00-04:00 1.028 1.914 0.028
2025-06-02 13:00:00-04:00 1.056 1.963 0.056
2025-06-02 13:30:00-04:00 1.006 1.883 0.006
2025-06-02 14:00:00-04:00 1.044 1.957 0.044
2025-06-02 14:30:00-04:00 1.022 1.931 0.022
2025-06-02 15:00:00-04:00 1.022 1.940 0.022
2025-06-02 15:30:00-04:00 1.111 2.102 0.11196.2% of the snapshots have the minimum spread of one tick. The rest are spread over the day, more often in the last half hour, when order arrival speeds up before the closing auction (lesson 1.1.2) and market makers adjust their quotes. In basis points the spread depends on the price: the same cent is 1.88 basis points with the stock at 10. To compare costs across assets, the spread is measured in basis points; to understand the mechanics, in ticks.
Does the book anticipate the next price?
If the first sell level is almost empty and the buy level is full, what usually happens next is that the ask is depleted and the mid price rises. With NOVA's snapshots this can be measured: for each snapshot, the imbalance and the change of the mid price until the next snapshot, ten seconds later.
medio_s = (pa + pb) / 2
deseq = (qb - qa) / (qb + qa)
cambio = 100 * (medio_s.shift(-1) - medio_s) # cents
df = pd.DataFrame({"deseq": deseq, "cambio": cambio}).dropna()
df["tramo"] = pd.cut(df["deseq"], [-1, -0.6, -0.2, 0.2, 0.6, 1],
include_lowest=True)
print(f"correlation: {df['deseq'].corr(df['cambio']):.2f}")
df.groupby("tramo", observed=True)["cambio"].agg(
["mean", "size"]).round(3)correlation: 0.56
mean size
tramo
(-1.001, -0.6] -0.645 599
(-0.6, -0.2] -0.146 374
(-0.2, 0.2] -0.036 388
(0.2, 0.6] 0.088 382
(0.6, 1.0] 0.661 596The relationship is clear and almost symmetric: when the first level is heavily loaded on the sell side (imbalance below −0.6), the mid price falls on average 0.65 cents over the next ten seconds; when it is heavily loaded on the buy side, it rises 0.66. The correlation is 0.56. Why is the microprice a better estimate of the “true” price than the mid? Because it already incorporates that information. Comparing both with the mid price of the next snapshot:
micro = (pb * qa + pa * qb) / (qb + qa)
siguiente = medio_s.shift(-1)
ok = siguiente.notna()
for nombre, pred in (("mid", medio_s), ("microprice", micro)):
rmse = np.sqrt(((siguiente - pred)[ok] ** 2).mean())
print(f"{nombre:<12} root mean squared error: {100 * rmse:.3f} cent")mid root mean squared error: 0.867 cent
microprice root mean squared error: 0.736 centThe microprice reduces the prediction error of the next mid price from 0.867 to 0.736 cents, 15%. This is synthetic data, in which the relationship has been built cleanly; in real data it also appears, but weaker and at very short horizons, seconds or less.
Price-time priority
When an aggressive order arrives, against which order in the book is it executed? In almost all equity markets, by price-time priority: first the best price and, within the same price, the order that arrived first. Each level is a queue.
Mermaid source
flowchart LR C[Market buy] --> A[1st, oldest] A --> B[2nd order] B --> N[Latest arrival]
The consequences are concrete. A new order goes to the end of the queue at its price. Reducing the size of an order keeps its place; increasing it or changing its price amounts to cancelling it and sending another, which goes to the end. And a limit order at the best bid is not executed until all those ahead of it have been executed or cancelled. That is why “the price touched my limit” does not mean “my order was executed” (lesson 1.1.4).
From L3 to L2: rebuilding the book
The L3 file has the order-by-order messages between 14:00 and 14:05 UTC. The first 358 rows, all stamped 14:00:00, are the initial snapshot: every live order, in priority order. After that comes one message per change.
l3 = pd.read_parquet("nova_libro_l3.parquet")
print(len(l3), "messages")
print(l3["evento"].value_counts().to_string())
l3.iloc[358:364]7241 messages
evento
alta 3443
cancelacion 2842
ejecucion 746
modificacion 210
ts id_orden evento lado precio tamano
358 2025-06-02 14:00:00.111345+00:00 41031081 alta bid 53.03 200
359 2025-06-02 14:00:00.121651+00:00 41031083 alta bid 52.99 97
360 2025-06-02 14:00:00.154247+00:00 41030881 cancelacion ask 53.12 100
361 2025-06-02 14:00:00.178017+00:00 41031084 alta ask 53.10 1000
362 2025-06-02 14:00:00.209129+00:00 41030590 cancelacion bid 53.04 200
363 2025-06-02 14:00:00.260941+00:00 41030785 cancelacion ask 53.13 100The rules for each message: alta (add) appends an order to the end of the queue at its price; cancelacion
(cancel) removes it; modificacion (modify) sets the new size and keeps priority; ejecucion (execution)
subtracts what was executed and, if it reaches zero, the order leaves. With a Python dictionary, which preserves
insertion order, that is all it takes to maintain the queues:
def aplica(libro, m):
"""Apply an L3 message to the book {id: [side, price, size]}."""
if m.evento == "alta":
libro[m.id_orden] = [m.lado, m.precio, m.tamano]
elif m.evento == "cancelacion":
del libro[m.id_orden]
elif m.evento == "modificacion":
libro[m.id_orden][2] = m.tamano # keeps its place
elif m.evento == "ejecucion":
libro[m.id_orden][2] -= m.tamano
if libro[m.id_orden][2] == 0:
del libro[m.id_orden]
def a_l2(libro, n=10):
"""Aggregate the orders by price: the n-level L2."""
df = pd.DataFrame(list(libro.values()),
columns=["lado", "precio", "tamano"])
g = (df.groupby(["lado", "precio"])["tamano"]
.agg(tamano="sum", ordenes="size").reset_index())
bids = g[g["lado"] == "bid"].nlargest(n, "precio")
asks = g[g["lado"] == "ask"].nsmallest(n, "precio")
out = pd.concat([bids, asks])
out["nivel"] = out.groupby("lado").cumcount() + 1
cols = ["lado", "nivel", "precio", "tamano", "ordenes"]
return out[cols].reset_index(drop=True)Now the messages are replayed in order and, at each instant with an L2 snapshot (every 10 seconds), the rebuilt book is compared with the snapshot. Each snapshot includes all the messages with a timestamp less than or equal to its own:
fotos = pd.date_range("2025-06-02 14:00:00", "2025-06-02 14:05:00",
freq="10s", tz="UTC")
libro, iguales, i = {}, 0, 0
mensajes = list(l3.itertuples(index=False))
for t in fotos:
while i < len(mensajes) and mensajes[i].ts <= t:
aplica(libro, mensajes[i])
i += 1
ref = l2[l2["ts"] == t].drop(columns="ts").reset_index(drop=True)
iguales += a_l2(libro).equals(ref)
print(f"{iguales} of {len(fotos)} snapshots match")
print(len(libro), "live orders at 14:05:00")31 of 31 snapshots match
345 live orders at 14:05:00All 31 snapshots match exactly: levels, prices, sizes and number of orders. It is the check worth always doing with your own L3 feed: if the rebuilt book drifts from the provider's L2, a message is missing or a rule has been applied wrongly. Another cheap check: the L3 executions must add up to the volume of the venues with a book on the trade tape.
op = pd.read_parquet("nova_operaciones.parquet")
ventana = op[(op["ts"] > "2025-06-02 14:00:00+00:00")
& (op["ts"] <= "2025-06-02 14:05:00+00:00")]
con_libro = ventana["centro"].isin(["PRINCIPAL", "MTF_A", "MTF_B"])
print("L3:", l3.loc[l3["evento"] == "ejecucion", "tamano"].sum())
print("tape, venues with a book:",
ventana.loc[con_libro, "tamano"].sum())
print("tape, all venues:", ventana["tamano"].sum())L3: 78705
tape, venues with a book: 78705
tape, all venues: 134726The 78,705 shares executed against the book match the tape. The rest of the volume in those five minutes was matched in the dark pool and in the internalizer, which do not go through the book: the most complete L3 feed of an exchange does not show all the trading in the stock.
With the rebuilt book you can see the full queue of a level, which no L2 shows:
cola = [(oid, o[2]) for oid, o in libro.items()
if o[0] == "bid" and o[1] == max(
v[1] for v in libro.values() if v[0] == "bid")]
print("price:", libro[cola[0][0]][1], "· orders:", len(cola),
"· shares:", sum(q for _, q in cola))
pd.DataFrame(cola[:5], columns=["id_orden", "tamano"])price: 53.17 · orders: 34 · shares: 14140
id_orden tamano
0 41034369 1078
1 41034404 200
2 41034422 100
3 41034426 62
4 41034433 500The first order in the queue has 1,078 shares and will be executed before any other at that price. A new buy order at 53.17 would have 14,140 shares ahead of it: before its turn comes, someone has to sell all of that, or the orders ahead have to be cancelled.
The iceberg order
An iceberg order shows only part of its size. When the visible part is fully executed, the system replenishes
another slice from the hidden reserve, with the same identifier and at the end of the queue. In L2 only the visible
slice is seen. In L3 the repetition gives it away: a normal order has a single alta.
altas = l3[l3["evento"] == "alta"].groupby("id_orden").size()
print(altas[altas > 1].to_string())
ice = l3[l3["id_orden"] == altas.idxmax()]
print(ice.groupby("evento")["tamano"].agg(["size", "sum"]).to_string())
print("price:", ice["precio"].iloc[0], "· side:", ice["lado"].iloc[0])
print("from", ice["ts"].min().strftime("%H:%M:%S"),
"to", ice["ts"].max().strftime("%H:%M:%S.%f"))id_orden
41031312 30
size sum
evento
alta 30 9000
ejecucion 86 9000
price: 53.09 · side: ask
from 14:00:20 to 14:01:31.224465Order 41031312 sells at 53.09. It appears 30 times with 300 shares (the first one and 29 replenishments) and is executed in 86 pieces that add up to 9,000 shares, all it had, between 14:00:20 and 14:01:31. Anyone watching L2 saw a few hundred shares for sale at 53.09; in reality there were 9,000. The effect on the price shows in the quote:
cot = pd.read_parquet("nova_cotizaciones.parquet").set_index("ts")
tramo = cot["2025-06-02 14:00:10":"2025-06-02 14:02:00"]
tramo.resample("10s").last() bid ask tam_bid tam_ask
ts
2025-06-02 14:00:10+00:00 53.08 53.09 3631 13073
2025-06-02 14:00:20+00:00 53.08 53.09 12431 10914
2025-06-02 14:00:30+00:00 53.08 53.09 19151 2361
2025-06-02 14:00:40+00:00 53.08 53.09 26743 653
2025-06-02 14:00:50+00:00 53.08 53.09 32909 71
2025-06-02 14:01:00+00:00 53.08 53.09 37482 259
2025-06-02 14:01:10+00:00 53.08 53.09 41363 259
2025-06-02 14:01:20+00:00 53.08 53.09 38533 249
2025-06-02 14:01:30+00:00 53.09 53.10 7681 54
2025-06-02 14:01:40+00:00 53.11 53.12 6156 89
2025-06-02 14:01:50+00:00 53.13 53.15 3946 4475
2025-06-02 14:02:00+00:00 53.13 53.15 3850 4975For a little over a minute, the ask does not move from 53.09 even though the visible size for sale drops to a few tens or hundreds of shares and the bid piles up tens of thousands: every time the visible slice runs out, another one appears. When the reserve is exhausted, at 14:01:31, the ask goes from 53.09 to 53.15 in less than thirty seconds. For the buyer, the iceberg is liquidity they did not expect; for whoever studies the book, an imbalance that “should” have moved the price and did not.
Exercises
- Compute the spread in basis points, the imbalance and the microprice of the L2 snapshot at 13:30:00 UTC, the first of the session, and compare them with those at 15:00 New York time.
Solution
primera = l2[l2["ts"] == "2025-06-02 13:30:00+00:00"]
pd.DataFrame({"13:30 UTC": medidas_l1(primera),
"19:00 UTC": medidas_l1(foto)}).round(4) 13:30 UTC 19:00 UTC
bid 53.3400 52.7400
ask 53.3600 52.7500
medio 53.3500 52.7450
horquilla_ticks 2.0000 1.0000
horquilla_pb 3.7488 1.8959
desequilibrio 0.4118 0.7556
microprecio 53.3541 52.7488In the first snapshot, right after the opening auction, the spread is two ticks (53.34 to 53.36) and the book is still thin: 1,200 shares at the best bid and 500 at the best ask. The imbalance is positive and the microprice sits above the mid, as at 15:00, but with twice the spread. The first seconds of continuous trading are the most expensive to cross.
- With the book rebuilt at 14:05:00, how many shares would a new sell order at the best ask have ahead of it? And one at the second sell level?
Solution
asks = sorted({o[1] for o in libro.values() if o[0] == "ask"})
for precio in asks[:2]:
delante = sum(o[2] for o in libro.values()
if o[0] == "ask" and o[1] <= precio)
print(f"selling at {precio}: {delante} shares ahead")selling at 53.18: 4654 shares ahead
selling at 53.19: 37172 shares aheadAt equal price, the new order goes to the end of the queue, so it has the whole level ahead of it: 4,654 shares at 53.18. At 53.19 it would have 37,172 ahead. At the second level it has that entire level ahead (32,518 shares) plus the first one, which is executed before because it has a better price. The further from the best price, the more likely the order never gets executed if the price turns around.
- A provider offers you L1 for free, L2 for a monthly fee and L3 for a much higher fee. Your system trades once a day, with orders of 500 shares in very liquid stocks. Which level do you need and what would you use each one for?
Solution
L1 and bars are enough to trade: the size (500 shares) is far below the typical depth of the first level, so the cost is essentially half the spread, and the spread comes from L1. An L2 feed would be useful to check from time to time that the depth is still sufficient (for example, at the open or on news days) and to estimate the impact if the size grows. L3 adds nothing at that horizon: it is for modeling queues, icebergs and order arrival, things that matter to those who trade in seconds or provide liquidity.
Each answer is folded under its question
Self-assessment
NOVA's best bid is 52.74 and the best ask 52.75. What is the spread in basis points over the mid price?
- A1 basis point
- BAbout 1.9 basis points
- C0.01 basis points
- D19 basis points
Show answer
Answer B. About 1.9 basis points
The spread is $0.01, one tick. Over the mid price, 52.745, it is 0.01 / 52.745 = 0.000190, that is, 1.90 basis points. In ticks it is the smallest possible; in basis points it depends on the stock price.
What does an L2 data feed contain that an L1 feed does not?
- AThe executed trades
- BThe identifier of each order
- CWho sent each order
- DThe aggregated size at several price levels on each side, not only at the best one
Show answer
Answer D. The aggregated size at several price levels on each side, not only at the best one
L1 is the best bid and the best ask with their sizes. L2 adds the following price levels, each with its total size and, sometimes, the number of orders. The identifier of each order is L3; who sent it is published by no level.
In the 15:00 New York snapshot, the best bid has 13,432 shares and the best ask 1,870. Where is the microprice?
- AExactly at the midpoint, 52.745
- BClose to the bid, around 52.741
- CClose to the ask, around 52.749
- DBelow the bid
Show answer
Answer C. Close to the ask, around 52.749
The microprice weights each price by the size of the opposite side: bid × ask size + ask × bid size, divided by the sum. With much more buying than selling at the first level, it moves toward the ask: the thin side is the one about to be depleted.
A sell order appears again and again with the same identifier at the end of the 53.09 queue, always with 300 shares, every time it is fully executed. What is it?
- AAn iceberg order: it shows 300 shares and replenishes the visible part from a hidden reserve
- BA data provider error
- CA market maker that cancels and resends
- DA stop order
Show answer
Answer A. An iceberg order: it shows 300 shares and replenishes the visible part from a hidden reserve
An iceberg has a visible part and a reserve. When the visible part is used up, the system replenishes another slice with the same identifier and puts it at the end of the queue, because the new part loses time priority. In L2 only the visible slice is seen; in L3 the repeated identifier gives it away.
A market buy order for 500 shares arrives and at the best ask there are three orders: A (200, the oldest), B (100) and C (400). How is it allocated?
- AAgainst C, the largest
- BIn equal parts among the three
- CAgainst the most recent
- D200 against A, 100 against B and 200 against C
Show answer
Answer D. 200 against A, 100 against B and 200 against C
With price-time priority, within the same price the order that arrived first is executed first. A is filled completely, then B, and C is executed for the remainder (200) and keeps 200 in the book, retaining its place.
Key points
- The book has two sides: buys (bid) sorted from highest to lowest price and sells (ask) from lowest to highest. The distance between the best ones is the spread.
- L1 is the best price on each side; L2, several levels with aggregated size; L3, each order with its identifier. Each level is obtained by aggregating the next one.
- Depth says how much can be executed without moving the price much, and it changes from one instant to the next: NOVA's lit book at 15:00 had 35,197 shares on the bid and 79,119 on the ask across ten levels.
- First-level imbalance anticipates the next move of the mid price in the NOVA data; the microprice folds it into a single number.
- Price-time priority decides who is executed first. With L3 messages the exact book is rebuilt, and the resulting L2 matches the 31 snapshots of the window.