Lesson 1.3.1 · 55 min

Types of market data: EOD, bars, ticks, order book and reference

The same trading day can be stored in one row or in half a million. This lesson walks the full ladder with one session of the fictitious stock NOVA, from the order-by-order message to the daily bar, measures how much space each rung takes and reviews the pitfalls of tape, timestamps and time zones that come with each one.

Fig. — Rows and pandas memory of one NOVA session (2 June 2025) at each granularity, from the daily bar to order-by-order messages. *L3: five minutes of messages times 78
01/08
Module
1.3 · Market data sources
Duration
55 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

A ladder of aggregation

When people talk about “market data” they mean very different things: the closing price of each day, a five-minute candle, each trade, each change in the best price or each order that enters and leaves the book. They are not alternative sources but rungs of the same ladder. At the bottom are the messages emitted by the matching engine; everything else is an aggregation of them.

L3 messagesRebuilt bookL2 bookL1 quotesTradesIntraday barsDaily dataReference
Mermaid source
flowchart TB
  M[L3 messages] --> B[Rebuilt book]
  B --> L2[L2 book]
  L2 --> L1[L1 quotes]
  M --> T[Trades]
  T --> BA[Intraday bars]
  BA --> D[Daily data]
  R[Reference] -.-> D
  • L3 messages (market by order): every order that arrives, is modified, is canceled or is executed, with its identifier. This is what the exchanges' order-by-order protocols publish, such as Nasdaq's ITCH1Note 1 · Nasdaq TotalView-ITCH 5.0 (link in the resources): messages for adding, executing, canceling, deleting and replacing orders, with nanosecond timestamps since midnight..
  • L2 book (market by price): the book aggregated by price level, with the total size and the number of orders at each level (lesson 1.1.3).
  • L1 quotes (top of book): only the best bid and the best ask, with their sizes.
  • Trades (also called ticks): each execution, with price, size, venue and conditions.
  • Intraday bars: trades aggregated over intervals (1 minute, 5 minutes, 1 hour) as open, high, low, close and volume (OHLCV).
  • Daily data (EOD, end of day): one bar per session.
  • Reference data: what describes the instruments (specifications, corporate actions, symbols, universes). It is not aggregated from the messages, but it is needed to interpret and adjust them; that is why the arrow is dashed.

The rule of the ladder is asymmetric: from one rung you can go down to the next (aggregate), but not up. Trades build bars; bars do not give back the trades.

Six rungs of NOVA

The level's data has an example of each rung for the same fictitious stock, NOVA, and for the same session, June 2, 2025, output of a simulated order-by-order book (lesson 1.1.1). Starting from the top, the daily data:

IN [01]python
import numpy as np
import pandas as pd

diario = pd.read_csv("nova_diario_sin_ajustar.csv",
                     parse_dates=["fecha"]).set_index("fecha")
print(len(diario), "sessions;", diario.index.min().date(), "→",
      diario.index.max().date())
diario.loc["2025-05-30":"2025-06-03"]
OUT [01]Daily data
2011 sessions; 2018-01-02 → 2025-12-31
            apertura  maximo  minimo  cierre  volumen
fecha
2025-05-30     52.58   53.98   52.01   53.13  6106070
2025-06-02     53.35   53.93   52.55   53.12  8888714
2025-06-03     53.05   53.99   52.74   53.00  8044956

One row per session, eight years in 2,011 rows. It answers questions from days to years: trends, daily volatility, portfolios rebalanced at the close. It is the cheapest data to obtain and to store, and the one with the most adjustment pitfalls (lesson 1.4.3: this one is unadjusted, with the 2021 split in plain sight).

IN [02]python
barras = pd.read_parquet("nova_barras_5min.parquet").set_index("ts")
sesion = barras.loc["2025-06-02"]
print(len(barras), "bars in", barras.index.normalize().nunique(),
      "sessions;", len(sesion), "on June 2")
sesion.head(3)
OUT [02]5-minute bars
38940 bars in 502 sessions; 78 on June 2
                           apertura  maximo  minimo  cierre  volumen
ts
2025-06-02 13:30:00+00:00     53.35   53.48   53.28   53.46   285081
2025-06-02 13:35:00+00:00     53.46   53.48   53.22   53.24   220651
2025-06-02 13:40:00+00:00     53.24   53.47   53.23   53.35   187320

78 bars per normal session (six and a half hours in five-minute slices). They answer questions from minutes to days: intraday patterns, seasonality within the session, execution on a schedule. The timestamp is the start of the bar, in UTC: the first one, 13:30 UTC, is 9:30 in New York.

IN [03]python
op = pd.read_parquet("nova_operaciones.parquet")
cot = pd.read_parquet("nova_cotizaciones.parquet")
print(len(op), "trades;", len(cot), "quote changes")
print(op.iloc[1:3].to_string())
print(cot.iloc[1:3].to_string())
OUT [03]Trades and quotes
50946 trades; 129247 quote changes
                                ts  precio  tamano agresor     centro condicion
1 2025-06-02 13:30:00.301051+00:00   53.34     100   venta  PRINCIPAL   regular
2 2025-06-02 13:30:00.301052+00:00   53.34     100   venta  PRINCIPAL   regular
                                ts    bid    ask  tam_bid  tam_ask
1 2025-06-02 13:30:00.006643+00:00  53.34  53.36     1600      500
2 2025-06-02 13:30:00.075521+00:00  53.34  53.36     1600      800

Trades and quotes are the “tick-by-tick” data. Trades say what was traded, where and at what price; quotes, at what price one could trade at each instant. Together they make it possible to measure the effective bid-ask spread and slippage (lesson 1.1.5), classify buys and sells or build alternative bars (lesson 1.4.5).

IN [04]python
l2 = pd.read_parquet("nova_libro_l2.parquet")
l3 = pd.read_parquet("nova_libro_l3.parquet")
print(len(l2), "L2 rows (snapshots every 10 s, 10 levels per side)")
print(l2[l2["ts"] == l2["ts"].iloc[0]].head(4).to_string())
print(len(l3), "L3 messages in 5 minutes;",
      l3["evento"].value_counts().to_dict())
OUT [04]L2 book and L3 messages
44126 L2 rows (snapshots every 10 s, 10 levels per side)
                         ts lado  nivel  precio  tamano  ordenes
0 2025-06-02 13:30:00+00:00  bid      1   53.34    1200        3
1 2025-06-02 13:30:00+00:00  bid      2   53.33     400        2
2 2025-06-02 13:30:00+00:00  bid      3   53.32    1100        3
3 2025-06-02 13:30:00+00:00  bid      4   53.31    1100        2
7241 L3 messages in 5 minutes; {'alta': 3443, 'cancelacion': 2842, 'ejecucion': 746, 'modificacion': 210}

L2 says how much liquidity there was at each level; L3, who (which order) was providing it and at what position in the queue it stood. In five minutes of NOVA there are 7,241 messages, and most are not trades: they are adds and cancellations of orders that never execute. That noise is the raw material of microstructure studies (queues, priority, iceberg orders) and is almost never needed for a low-frequency system.

TypeOne row isAnswersTypical use
Daily (EOD)One sessionTrend, volatility, portfoliosDaily backtests, factor research
Intraday barsOne intervalIntraday patterns, schedulesIntraday systems, session seasonality
TradesOne executionWhat was traded, where, how muchVWAP, volume bars, costs
Quotes (L1)A change in the best priceAt what price one could tradeBid-ask spread, slippage, alignment
L2 bookOne level at one instantHow much liquidity there wasMarket impact, depth
L3 messagesOne event of one orderQueues and priorityMicrostructure, execution simulation
ReferenceOne instrument or eventWhat it is and what happened to itAdjustments, universes, specifications

How much space each rung takes

The difference between rungs is orders of magnitude. To measure it for one session, we count the rows and the memory they take in pandas. The L3 messages are only available for five minutes; multiplied by 78 they give an estimate for the full session:

IN [05]python
def mb(df):
    return df.memory_usage(deep=True).sum() / 1e6

dia = diario.loc[["2025-06-02"]]
l3_bytes_fila = l3.memory_usage(deep=True).sum() / len(l3)
tamanos = pd.DataFrame({
    "filas": [len(dia), len(sesion), len(op), len(cot), len(l2),
              len(l3) * 78],
    "MB": [mb(dia), mb(sesion), mb(op), mb(cot), mb(l2),
           l3_bytes_fila * len(l3) * 78 / 1e6],
}, index=["daily", "5-min bars", "trades", "quotes",
          "L2 book (10 s)", "L3 messages (estimated)"])
tamanos["filas"] = tamanos["filas"].astype(int)
tamanos.round(4)
OUT [05]Rows and memory per session
                          filas       MB
daily                         1   0.0000
5-min bars                   78   0.0037
trades                    50946   3.4860
quotes                   129247   5.1700
L2 book (10 s)            44126   2.2506
L3 messages (estimated)  564798  33.0479

The figure in the header plots the table on a logarithmic scale. From the daily bar to L3 there are almost six orders of magnitude: one row versus more than half a million. And that is for a fictitious stock of average liquidity in a single book. Multiplied by thousands of stocks and years of history, the difference decides the infrastructure: ten years of daily data for a whole market fit on a laptop; the L3 messages of a single exchange for one year do not (lesson 1.4.7). It also decides the price: providers charge by granularity, historical depth and number of exchanges (lessons 1.3.3 and 1.3.4).

IN [06]python
sesiones, valores = 252, 3_000
anual_gb = tamanos["MB"] * sesiones * valores / 1e3
anual_gb.round(3).rename("GB per year (3,000 stocks)")
OUT [06]One year of a market, roughly
daily                          0.036
5-min bars                     2.830
trades                      2635.384
quotes                      3908.529
L2 book (10 s)              1701.422
L3 messages (estimated)    24984.211
Name: GB per year (3,000 stocks), dtype: float64

The calculation is crude, because NOVA is not representative of a whole market and on-disk formats compress much more than pandas in memory (Parquet, lesson 1.4.7). But the order of magnitude is what matters: with daily data, one year of 3,000 stocks is a few tens of megabytes; with trades and quotes, several terabytes; with L3, tens of terabytes.

Thirty seconds of NOVA

The ladder shows clearly over a short interval: from 14:00:00 to 14:00:30 UTC (10:00 in New York), how many events each granularity records:

IN [07]python
ini = pd.Timestamp("2025-06-02 14:00:00", tz="UTC")
fin = ini + pd.Timedelta("30s")
en = lambda df: df[(df["ts"] >= ini) & (df["ts"] < fin)]
print("L3 messages:  ", len(en(l3[l3["ts"] > ini])))
print("quotes:       ", len(en(cot)))
print("trades:       ", len(en(op)))
print("L2 snapshots: ", en(l2)["ts"].nunique())
print("5-min bars:    1 (in progress)")
precios = en(cot)[["bid", "ask"]].drop_duplicates()
print("distinct bid/ask pairs:", precios.values.tolist())
OUT [07]Half a minute at each granularity
L3 messages:   656
quotes:        145
trades:        79
L2 snapshots:  3
5-min bars:    1 (in progress)
distinct bid/ask pairs: [[53.08, 53.09]]
Fig. — Thirty seconds of NOVA, 14:00:00 to 14:00:30 UTC on 2 June 2025: cumulative events at each granularity, from order-by-order messages to the 5-minute bar

In half a minute there are 656 order messages, 145 changes in the best bid or ask (almost all of them in size: in those 30 seconds the price does not move from 53.08/53.09) and 79 trades; L2 takes three snapshots and the five-minute bar that contains them all has not even closed yet. The figure accumulates the events second by second. (The first 358 rows of L3, the initial snapshot of the book at exactly 14:00:00, are excluded from the count.)

The asymmetry of the ladder can be checked in the other direction: the trades rebuild the bars and the bars rebuild the daily data. They must match the aggregated files:

IN [08]python
ohlcv = {"precio": ["first", "max", "min", "last"], "tamano": "sum"}
# the closing auction (20:00:00 UTC) goes in the last bar
cierre = op["condicion"] == "subasta_cierre"
ts_barra = op["ts"].mask(cierre, op["ts"] - pd.Timedelta("1us"))
mias = op.set_index(ts_barra).resample("5min").agg(ohlcv)
mias.columns = ["apertura", "maximo", "minimo", "cierre", "volumen"]
print(len(mias), "bars; equal:", np.allclose(mias, sesion))

dia_mio = pd.Series({"apertura": sesion["apertura"].iloc[0],
                     "maximo": sesion["maximo"].max(),
                     "minimo": sesion["minimo"].min(),
                     "cierre": sesion["cierre"].iloc[-1],
                     "volumen": sesion["volumen"].sum()})
print("Day equal:", np.allclose(dia_mio, dia.iloc[0]))
OUT [08]From trades to bars and to the day
78 bars; equal: True
Day equal: True

The 78 bars come out exactly from the trades, and the daily data from the bars. There is one detail: the closing auction crosses at 16:00:00 New York time (20:00:00 UTC), right on the edge, and a plain resample would open a 79th bar at 20:00 just for it. The provider of these data counts it in the 15:55 bar, and to match it you have to do the same (here, by moving its timestamp back one microsecond when grouping). It is the kind of convention you need to know for two sources to agree.

Consolidated tape or a single venue

In the US, all exchanges and venues send their trades and quotes to a consolidator, which publishes the consolidated tape: for stocks listed on the NYSE and on other regional exchanges, the CTS (trades) and CQS (quotes) systems of the Consolidated Tape Association2Note 2 · Consolidated Tape Association, overview page (link in the resources).. A provider can give the consolidated tape or the proprietary feed of a single exchange, and the numbers change a lot, as seen in lesson 1.1.1:

IN [09]python
def barra_dia(t):
    return pd.Series({
        "apertura": t["precio"].iloc[0], "maximo": t["precio"].max(),
        "minimo": t["precio"].min(), "cierre": t["precio"].iloc[-1],
        "volumen": t["tamano"].sum(), "operaciones": len(t),
        "vwap": (t["precio"] * t["tamano"]).sum() / t["tamano"].sum()})

pd.DataFrame({"consolidated": barra_dia(op),
              "primary only": barra_dia(
                  op[op["centro"] == "PRINCIPAL"])}).round(3)
OUT [09]The same day, two tapes
             consolidated  primary only
apertura           53.350        53.350
maximo             53.930        53.930
minimo             52.550        52.550
cierre             53.120        53.120
volumen       8888714.000   3564476.000
operaciones     50946.000     25776.000
vwap               53.319        53.291

With the tape of a single venue, the day's volume is 40% of the real one, there are half as many trades and the VWAP drops from 53.319 to 53.291. Open and close match because they are set by the primary venue's auctions; the high and the low also match that day, but they do not have to: the session's extreme price can be crossed on another venue. Before comparing volumes across sources, or computing a VWAP to measure an execution, you need to know which tape is being used.

Timestamps and time zones

Every row of market data carries a timestamp, and every timestamp has a convention. Three questions must always be asked:

  1. Which event is the timestamp from? The matching engine's (when the event happens) or the receipt one (when whoever records the data receives it). Professional providers usually give both: Databento, for example, publishes ts_event and ts_recv, and warns that the market's and the receiver's clocks are not necessarily synchronized3Note 3 · Databento, “Common fields, enums and types” (link in the resources)..
  2. Start or end of the interval? In bars, a 14:00 timestamp can be the start (14:00–14:05) or the end (13:55–14:00). Confusing them is looking into the future.
  3. Which time zone? UTC, the exchange's time or that of the computer of whoever downloaded the data. Dates without a time (daily data) are the session date, not an instant.

The first matters when venues are combined: each feed arrives with its own latency, and sorting by the receipt timestamp changes the order of the events. A simulation shows it with assumed latencies of a few hundred microseconds, different for each venue:

IN [10]python
rng = np.random.default_rng(7)
media_us = {"PRINCIPAL": 150, "MTF_A": 400, "MTF_B": 600,
            "OSCURO": 900, "INTERNALIZADOR": 1_200}
lat = rng.exponential(op["centro"].map(media_us).to_numpy())
op["ts_recv"] = op["ts"] + pd.to_timedelta(lat.round(), unit="us")
orden_evento = op.sort_values("ts", kind="stable").index
orden_recv = op.sort_values("ts_recv", kind="stable").index
cambia = (orden_evento != orden_recv).mean()
print(f"Trades in a different position when sorted by receipt: "
      f"{cambia:.1%}")
OUT [10]Sorting by receipt changes the order
Trades in a different position when sorted by receipt: 28.7%

With latencies of this size, 28.7% of the trades change position in the sequence. For a low-frequency system it is irrelevant; for aligning trades with quotes to the microsecond (lesson 1.1.1) or for measuring who gets ahead of whom, it is the difference between a correct study and a false one.

The second and third questions affect any intraday system:

IN [11]python
b = sesion.head(2).copy()
b["fin"] = b.index + pd.Timedelta("5min")
b.index = b.index.tz_convert("America/New_York")
b[["fin", "apertura", "cierre"]].assign(
    fin=lambda x: x["fin"].dt.tz_convert("America/New_York"))
OUT [11]Bar start, UTC and New York time
                                                fin  apertura  cierre
ts
2025-06-02 09:30:00-04:00 2025-06-02 09:35:00-04:00     53.35   53.46
2025-06-02 09:35:00-04:00 2025-06-02 09:40:00-04:00     53.46   53.24
Fig. — The same 5-minute bar under two timestamp conventions: NOVA's first bar on 2 June 2025 runs from 9:30 to 9:35 New York time; its close is only known at 9:35, even though the file labels it 13:30 UTC (schematic)

The bar that starts at 9:30 New York time ends at 9:35, and its close is not known until then. A signal that uses that close can only be executed from 9:35 onward. If the provider labeled bars by their end, the same bar would carry the 9:35 timestamp; mixing two sources with different conventions shifts one of them by five minutes. And when converting from UTC to local time, daylight saving time moves the UTC time of the open (13:30 in summer, 14:30 in winter; lesson 0.4.4): that is why it is best to store in UTC and convert only to display or to filter by time of day.

Reference data

The last type has no price, but without it prices cannot be understood. Three examples from the level:

IN [12]python
spec = pd.read_csv("fix_especificaciones.csv")
eventos = pd.read_csv("nova_eventos.csv", parse_dates=["fecha_ex"])
miembros = pd.read_csv("universo_miembros.csv",
                       parse_dates=["alta", "baja"])
print(spec[["multiplicador", "tick", "valor_tick", "meses"]]
      .to_string(index=False))
print(eventos[eventos["tipo"] != "dividendo"].to_string(index=False))
print(f"{len(miembros)} tickers have been in the index; "
      f"{miembros['baja'].isna().sum()} remain today")
OUT [12]Specifications, events and universe
 multiplicador  tick  valor_tick   meses
            50  0.25        12.5 H,M,U,Z
  fecha_ex                     tipo  ratio  importe fecha_pago
2021-08-23                    split    4.0      NaN 2021-08-20
2023-12-14 dividendo_extraordinario    NaN      1.5 2024-01-04
57 tickers have been in the index; 40 remain today
  • Contract specifications: multiplier, tick size, expiry months and settlement rules of a future (lesson 1.2.3). Without the multiplier, a 10-point move cannot be converted into money.
  • Corporate actions: splits, dividends, name changes, mergers. They are used to adjust the series (lesson 1.4.3). NOVA's 4-for-1 split in 2021 is in this table; without it, the daily price appears to fall 75% in one day.
  • Universes: which stocks made up an index on each date. 57 tickers have been in an index that always has 40 members; using only today's 40 is survivorship bias (lesson 1.4.4).

Reference data takes little space, changes little and nobody looks at it until it fails. And when it fails, the error gives no warning: it produces a plausible and false backtest.

Which data for which question

The practical rule is to use the highest rung (the most aggregated) that answers the question. Each finer rung multiplies the cost, the volume of data and the ways of getting it wrong.

days or moreminutes or hourssecondsyesnoHorizon?Daily dataIntraday barsTrades and L1Queues, priority?L3 messagesL2 book
Mermaid source
flowchart TB
  P{Horizon?} -- days or more --> D[Daily data]
  P -- minutes or hours --> B[Intraday bars]
  P -- seconds --> T[Trades and L1]
  T --> C{Queues, priority?}
  C -- yes --> L3[L3 messages]
  C -- no --> L2[L2 book]

A daily system like those of levels 2 and 3 lives on daily data, properly adjusted and free of survivorship bias. An intraday one needs bars and, to estimate costs, trades and quotes. Only microstructure research or high-frequency execution justify the full book.

Exercises

  1. Build 1-minute bars with NOVA's trades from June 2 and count how many the session has. How many would there be with 30-minute bars? Where does the closing auction fall in each case?
Solution
IN [13]python
for regla in ("1min", "30min"):
    b = op.set_index("ts")["precio"].resample(regla).ohlc()
    print(regla, "→", len(b), "intervals;", "last:",
          b.index[-1].strftime("%H:%M"), "UTC")
OUT [13]1- and 30-minute bars
1min → 391 intervals; last: 20:00 UTC
30min → 14 intervals; last: 20:00 UTC

With resample on the trades, the last interval is the one that contains the 20:00:00 UTC closing auction: 391 one-minute intervals (390 of the continuous session plus one at 20:00 with only the auction) and 14 of 30 minutes (13 plus the auction one). To get the 390 bars of the session you have to decide where the auction goes, as in the reconstruction of the 5-minute bars.

  1. Using the trades, compute what percentage of NOVA's volume is traded in the first and in the last half hour of the session, and compare it with the share it would have if volume were uniform.
Solution
IN [14]python
v = op.set_index("ts")["tamano"].resample("30min").sum()
cuota = 100 * v / v.sum()
print(f"First half hour: {cuota.iloc[0]:.1f} %")
print(f"Last half hour (with the auction): "
      f"{cuota.iloc[-2:].sum():.1f} %")
print(f"Uniform: {100 / 13:.1f} % per half hour")
OUT [14]U-shaped volume
First half hour: 13.5 %
Last half hour (with the auction): 20.1 %
Uniform: 7.7 % per half hour

The first half hour concentrates 13.5% of the volume and the last one, with the closing auction, 20.1%, versus the 7.7% they would get with uniform volume. It is the U-shaped profile seen in almost every stock (lessons 1.1.2 and 1.6.3): that is why execution algorithms spread orders following a volume profile and not in equal parts.

  1. A provider offers “tick-by-tick data” for a stock at a price. What questions would you ask before buying it?
Solution

What exactly it contains: only trades, or quotes as well; whether it is L1, L2 or L3. Which tape: consolidated or from a single exchange, and which venues it includes (dark pools, internalizers, off-hours trades). Which timestamps: event, receipt or both, with what precision and in which time zone. Which trade conditions it includes (auctions, blocks, corrected or canceled trades). Whether the data is adjusted for corporate actions and how the corporate actions are delivered. How far back the history goes and whether it includes delisted stocks. And the license: what you are allowed to do with it (lesson 1.3.7).

Footnotes

  1. Nasdaq TotalView-ITCH 5.0 (link in the resources): messages for adding, executing, canceling, deleting and replacing orders, with nanosecond timestamps since midnight. ↩

  2. Consolidated Tape Association, overview page (link in the resources). ↩

  3. Databento, “Common fields, enums and types” (link in the resources). ↩

Each answer is folded under its question

Self-assessment

  1. You want to study whether a daily moving average crossover strategy works on US stocks since 2005. What type of data do you need?

    • AL3 messages from every exchange
    • BDaily (EOD) data with corporate action adjustments and a universe free of survivorship bias
    • CQuotes with the best bid and ask
    • DL2 snapshots every second
    Show answer

    Answer B. Daily (EOD) data with corporate action adjustments and a universe free of survivorship bias

    A rule that decides once a day on the close only needs daily OHLCV. The critical part is not the granularity but the adjustments for splits and dividends and a universe that includes the stocks that stopped trading (lessons 1.4.3 and 1.4.4). Finer data would only add cost.

  2. What is the difference between L2 and L3 data?

    • ANone
    • BL2 is trades and L3 is quotes
    • CL3 is delayed L2
    • DL2 aggregates the book by price level (total size and number of orders); L3 gives each individual order with its identifier and its adds, cancellations and executions
    Show answer

    Answer D. L2 aggregates the book by price level (total size and number of orders); L3 gives each individual order with its identifier and its adds, cancellations and executions

    With L3 (market by order) you can rebuild the book and see the queue at each price; with L2 (market by price), only the total of each level. L2 can be derived from L3, but not the other way around.

  3. A 5-minute bar is stamped 14:00. Your provider labels bars by their start. When is its close known?

    • AAt 14:05, when the bar ends
    • BAt 14:00
    • CAt 13:55
    • DAt the close of the session
    Show answer

    Answer A. At 14:05, when the bar ends

    A bar labeled by its start contains what happens from 14:00 to 14:05; its close only exists at 14:05. Using it as if it were available at 14:00 is looking five minutes into the future. Each provider chooses its convention, and you have to check it.

  4. You compare NOVA's daily volume from two providers: one gives 3.56 million shares and the other 8.89 million. What is the most likely explanation?

    • AOne of the two is wrong
    • BOne includes dividends
    • COne uses only the trades of the primary venue and the other the consolidated tape of all venues
    • DOne uses daylight saving time
    Show answer

    Answer C. One uses only the trades of the primary venue and the other the consolidated tape of all venues

    In the NOVA session, the primary venue accounts for 3,564,476 shares and the consolidated tape for 8,888,714. Before comparing volumes, VWAP or highs and lows, you need to know which tape each source uses.

  5. What is reference data?

    • AThe official closing prices
    • BThe information that describes instruments and their life: contract specifications, corporate actions, calendars, symbols and universe membership
    • CThe data of a benchmark index
    • DData delayed by 15 minutes
    Show answer

    Answer B. The information that describes instruments and their life: contract specifications, corporate actions, calendars, symbols and universe membership

    Without multiplier, tick size, expiries, splits, symbol changes or index entry and exit dates, prices cannot be interpreted or adjusted. Reference data takes little space, but an error in it spoils everything else.

Key points

  • Market data forms a ladder: order-by-order messages (L3) → book by levels (L2) → best price (L1, quotes) and trades → bars → daily data. Each rung is derived from the previous one, never the other way around.
  • For NOVA, one session is 1 row of daily data, 78 rows of 5-minute bars, 50,946 trades, 129,247 quotes and on the order of half a million L3 messages.
  • Granularity is chosen by the question: a daily rule does not need ticks; a study of execution costs needs trades and quotes; one of queues and priority, L3.
  • Consolidated tape and a single venue give different volumes, highs, lows and VWAP. For NOVA, the primary venue accounts for only 40% of the volume.
  • Every timestamp has its convention: event or receipt, bar start or bar end, UTC or local time. Getting it wrong is a source of look-ahead bias.
  • Reference data (specifications, corporate actions, universes) takes little space and conditions everything.

Resources

Data