---
title: "1.1.3 Order book: bid, ask, spread and depth"
description: "How to read an order book: bid and ask, spread, depth, imbalance and microprice, and how to rebuild the L2 book from L3 messages."
url: "https://pythonparatrading.com/en/python-trading-course/level-1/order-book-bid-ask-spread-depth/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-1/order-book-bid-ask-spread-depth.png"
---

# 1.1.3 · 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.

- Course: [Level 1: Financial data, markets and analysis](https://pythonparatrading.com/en/python-trading-course/level-1/)
- Module: 1.1 · How markets work
- Duration: 60 min
- Prerequisites: [1.1.1 · Market participants and trading venues](https://pythonparatrading.com/en/python-trading-course/level-1/market-participants-and-trading-venues/), [1.1.2 · Sessions, auctions, holidays and calendars](https://pythonparatrading.com/en/python-trading-course/level-1/trading-sessions-auctions-and-market-calendars/)

## By the end of this lesson

- Read an order book: bid, ask, spread in ticks and in basis points, depth per level and cumulative depth.
- Tell L1, L2 and L3 data apart by what they contain, what they are for and what they do not show.
- Compute the book imbalance and the microprice, and measure with data whether they anticipate the next move of the mid price.
- Rebuild the book from L3 messages with price-time priority and check that it matches the L2 snapshots.
- Detect an iceberg order by its replenishments with the same identifier.

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

```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
```
```output
          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:

```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)
```
```output
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 = \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:

$$
P_{\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.

```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"])
```
```output
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.* Two step curves starting at the mid price, 52.745. To the right, cumulative asks rise fast to 79,119 shares at 52.84; to the left, cumulative bids rise to 31,871 in three levels and then barely grow, to 35,197 at 52.64. A highlighted line at 20,000 shares cuts the ask curve at the third level and the bid curve at the second.

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.

<aside class="callout" data-kind="aviso">

The lit book is not all the liquidity. The hidden parts of iceberg orders, the dark pool and the internalizer (lesson
1.1.1) are missing, and there is liquidity that is pulled within milliseconds when a large order arrives. The depth of
a snapshot is an optimistic bound on what can be executed in one go.

</aside>

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

```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)
```
```output
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.* Thirteen bars, one per half hour from 9:30 to 16:00 New York time. Almost all between 0.6% and 5.6% of snapshots with a spread wider than one cent; the last one, from 15:30 to 16:00, highlighted, at 11.1%.

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 $53 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.

```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)
```
```output
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.* Five bars by imbalance bucket, from −1 to 1. With the first level heavily loaded on the ask, the mid price falls 0.65 cents on average; in the middle buckets, −0.15, −0.04 and +0.09; with the first level heavily loaded on the bid, it rises 0.66 cents (highlighted).

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:

```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")
```
```output
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.

<aside class="callout" data-kind="riesgo">

That imbalance anticipates the next half cent is not a strategy. To exploit it you have to cross the spread, which
costs a whole cent, or be first in the queue, which requires speed. That is why it is an execution signal (when to
send an order that was going to be sent anyway) rather than a source of profit for a retail system.

</aside>

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

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

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

```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")
```
```output
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.

```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())
```
```output
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:

```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"])
```
```output
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`.

```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"))
```
```output
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:

```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()
```
```output
                             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.* Two step lines. From 14:00 the ask stays at 53.09 and the bid at 53.08; the stretch from 14:00:20 to 14:01:31 at 53.09, while a 9,000-share sell iceberg showing 300 is fully executed, is highlighted. Once it is exhausted, the ask rises to 53.15 in under 30 seconds.

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

```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)
```
```output
                 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.

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

```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")
```
```output
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.

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

## 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?

   - A. 1 basis point
   - B. About 1.9 basis points
   - C. 0.01 basis points
   - D. 19 basis points

   *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?

   - A. The executed trades
   - B. The identifier of each order
   - C. Who sent each order
   - D. The aggregated size at several price levels on each side, not only at the best one

   *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?

   - A. Exactly at the midpoint, 52.745
   - B. Close to the bid, around 52.741
   - C. Close to the ask, around 52.749
   - D. Below the bid

   *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?

   - A. An iceberg order: it shows 300 shares and replenishes the visible part from a hidden reserve
   - B. A data provider error
   - C. A market maker that cancels and resends
   - D. A stop order

   *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?

   - A. Against C, the largest
   - B. In equal parts among the three
   - C. Against the most recent
   - D. 200 against A, 100 against B and 200 against C

   *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

### Research

- [Limit Order Book - Aggregated Market Liquidity](https://pythonparatrading.com/en/limit-order-book-and-market-liquidity/) · Research-0008 · 11 min
- [Introduction to Volatility - Liquidity](https://pythonparatrading.com/en/introduction-to-volatility-liquidity/) · Research-0001 · 5 min

### Books

- [Bouchaud, Bonart, Donier and Gould: Trades, Quotes and Prices. Financial Markets Under the Microscope (Cambridge University Press, 2018)](https://doi.org/10.1017/9781316659335) · doi.org

### Data

- [Level 1 data: NOVA L2 book snapshots (Parquet)](https://pythonparatrading.com/curso/datos/nivel-1/nova_libro_l2.parquet) · Download · nova_libro_l2.parquet
- [Level 1 data: NOVA L3 book messages (Parquet)](https://pythonparatrading.com/curso/datos/nivel-1/nova_libro_l3.parquet) · Download · nova_libro_l3.parquet

Previous lesson: [1.1.2 · Sessions, auctions, holidays and calendars](https://pythonparatrading.com/en/python-trading-course/level-1/trading-sessions-auctions-and-market-calendars/)

Next lesson: [1.1.4 · Order types](https://pythonparatrading.com/en/python-trading-course/level-1/order-types-explained/)
