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.

Fig. — NOVA order book at 15:00 New York time on 2 June 2025: visible size by level, bids on the left and asks on the right
03/05
Module
1.1 · How markets work
Duration
60 min
Updated
Level 1 lessons
  1. 1.1 · How markets work

    1. 1.1.1 Market participants and trading venues
    2. 1.1.2 Sessions, auctions, holidays and calendars
    3. 1.1.3 Order book: bid, ask, spread and depth
    4. 1.1.4 Order types
    5. 1.1.5 Costs, settlement, leverage and margin
  2. 1.2 · Asset classes

    1. 1.2.1 Stocks: splits, dividends and corporate actions
    2. 1.2.2 ETFs: replication, tracking error, leveraged ETFs and UCITS
    3. 1.2.3 Futures: specifications, margin, roll and contango
    4. 1.2.4 Forex: pairs, pips, lots, rollover and CFDs
    5. 1.2.5 Options: calls, puts, intrinsic value and time value
    6. 1.2.6 Crypto: spot, perpetuals, funding and custody
    7. 1.2.7 Fixed income and interest rates; CFDs and leveraged products
  3. 1.3 · Market data sources

    1. 1.3.1 Types of market data: EOD, bars, ticks, order book and reference
    2. 1.3.2 Free market data sources
    3. 1.3.3 Retail and prosumer market data APIs
    4. 1.3.4 Professional market data providers
    5. 1.3.5 Crypto and on-chain data
    6. 1.3.6 Brokers as market data sources
    7. 1.3.7 Market data licensing and terms of use
    8. 1.3.8 How to choose a market data provider
  4. 1.4 · Financial data engineering

    1. 1.4.1 Downloading over REST and websockets: pagination, rate limits and retries
    2. 1.4.2 Data cleaning: missing values, bad ticks, gaps and time zones
    3. 1.4.3 Adjusting for splits and dividends: adjusted and unadjusted prices
    4. 1.4.4 Universes without survivorship bias: delistings, mergers and symbol changes
    5. 1.4.5 Building bars: time, tick, volume, dollar and imbalance
    6. 1.4.6 Continuous futures series: roll rules, difference adjustment and ratio adjustment
    7. 1.4.7 Storage: CSV, Parquet, partitioning, DuckDB, TimescaleDB and ArcticDB
    8. 1.4.8 Reproducible pipeline: download, validation, incremental updates and quality tests
  5. 1.5 · Fundamental and macro data

    1. 1.5.1 Financial statements, ratios, XBRL and SEC EDGAR
    2. 1.5.2 Macro data, economic calendars, estimates and transcripts
  6. 1.6 · Financial exploratory analysis

    1. 1.6.1 Returns, volatility, rolling correlations and drawdown
    2. 1.6.2 Stylized facts: fat tails, clustering, autocorrelation and leverage
    3. 1.6.3 Seasonality: day of the week, month, turn of the month and intraday sessions
  7. 1.7 · Programmed technical analysis

    1. 1.7.1 Technical indicators in Python
    2. 1.7.2 Your own implementation versus TA-Lib: validating results
    3. 1.7.3 Candlestick patterns and support and resistance
    4. 1.7.4 Multi-timeframe analysis without look-ahead

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:

sum by priceonly the firstexecutionsL3: each orderL2: by levelL1: best priceTrades
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]
LevelWhat it containsIn the course dataWhat it is for
L1 (top of book)Best bid and best ask with their sizesnova_cotizaciones.parquet, one row per changeSpread, mid price, aligning trades (1.1.1)
L2 (market by price)Several levels per side, with size and number of ordersnova_libro_l2.parquet, 10 levels every 10 sDepth, impact of a large order, imbalance
L3 (market by order)Each order: add, cancel, modify and execution, with its identifiernova_libro_l3.parquet, 5 minutesPriority 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.

IN [01]python
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
OUT [01]L2 snapshot at 15:00 New York time
          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       6

The 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:

IN [02]python
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)
OUT [02]Spread, mid price, imbalance and microprice
bid                52.7400
ask                52.7500
medio              52.7450
horquilla_ticks     1.0000
horquilla_pb        1.8959
desequilibrio       0.7556
microprecio        52.7488
dtype: float64

The spread is one tick, 1.9 basis points over the mid price. First-level imbalance ranges from −1 (only selling) to +1 (only buying):

I=qb−qaqb+qaI = \frac{q_b - q_a}{q_b + q_a}

Here it is 0.76: almost the whole first level is buying. The microprice weights each price by the size of the opposite side:

Pmicro=Pb qa+Pa qbqb+qaP_{\text{micro}} = \frac{P_b\, q_a + P_a\, q_b}{q_b + q_a}

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.

IN [03]python
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"])
OUT [03]Cumulative depth on each side
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      NaN
Fig. — Cumulative depth of the NOVA book at 15:00 New York time: shares available from the mid price out to each price

Within 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:

IN [04]python
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)
OUT [04]Spread of the L2 snapshots by half hour
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.111
Fig. — Share of NOVA L2 snapshots with a spread wider than one tick in each half hour of 2 June 2025

96.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 53andwouldbe10withthestockat53 and would be 10 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.

IN [05]python
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)
OUT [05]Imbalance and 10-second mid price change
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   596
Fig. — Average change in the NOVA mid price over the next 10 seconds by first-level book imbalance, 2 June 2025

The 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:

IN [06]python
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")
OUT [06]Mid versus microprice as a prediction
mid          root mean squared error: 0.867 cent
microprice   root mean squared error: 0.736 cent

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

Market buy1st, oldest2nd orderLatest arrival
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.

IN [07]python
l3 = pd.read_parquet("nova_libro_l3.parquet")
print(len(l3), "messages")
print(l3["evento"].value_counts().to_string())
l3.iloc[358:364]
OUT [07]L3 messages
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     100

The 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:

IN [08]python
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:

IN [09]python
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")
OUT [09]The 31 snapshots of the window, rebuilt
31 of 31 snapshots match
345 live orders at 14:05:00

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

IN [10]python
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())
OUT [10]L3 executions versus the tape
L3: 78705
tape, venues with a book: 78705
tape, all venues: 134726

The 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:

IN [11]python
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"])
OUT [11]The best bid queue at 14:05:00
price: 53.17 · orders: 34 · shares: 14140
   id_orden  tamano
0  41034369    1078
1  41034404     200
2  41034422     100
3  41034426      62
4  41034433     500

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

IN [12]python
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"))
OUT [12]Orders with more than one add
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.224465

Order 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:

IN [13]python
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()
OUT [13]The best price while the iceberg lasts
                             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     4975
Fig. — NOVA best bid and best ask between 14:00 and 14:02:30 UTC on 2 June 2025, with the sell iceberg order at 53.09

For 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

  1. 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
IN [14]python
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)
OUT [14]L1 measures of the first snapshot
                 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.7488

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

  1. 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
IN [15]python
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")
OUT [15]Shares ahead in the sell queue
selling at 53.18: 4654 shares ahead
selling at 53.19: 37172 shares ahead

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

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

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

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

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

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

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

Resources

Data