Lesson 6.6.1 · 60 min

When data becomes “big”: ticks, order books, options and universes

“Big data” is not a size, it is a relationship: data are big when they do not fit comfortably in the memory of the machine for what has to be done with them. This lesson puts figures on that relationship. It starts from a 36-byte message of Nasdaq's ITCH protocol, measures how much the same trade takes up in pandas, in Arrow and in Parquet, how much extra memory sorting, grouping or merging asks for, and with the statistics published by CTA, Nasdaq and OPRA it computes which market datasets fit in a laptop and which do not fit in any machine.

Fig. — In-memory size of six market data sets with the bytes per row measured in the lesson, against a 16 GB laptop and a 1 TB server. Records: CTA metrics (Q3 2026), OPRA projected capacity (July 2026) and assumptions for the bars
Level 6 lessons
  1. 6.1 · Classical and modern financial NLP

    1. 6.1.1 Tokenization, embeddings, financial dictionaries and FinBERT
    2. 6.1.2 Text sources: EDGAR, CNMV, transcripts and social media
    3. 6.1.3 Event studies with text signals, timestamp alignment and topic modeling
  2. 6.2 · LLMs for research and trading

    1. 6.2.1 How an LLM works: from the next token to the API
    2. 6.2.2 Structured prompting: JSON you can validate
    3. 6.2.3 Guidance, KPIs and tone: what an LLM extracts well
    4. 6.2.4 Lightweight fine-tuning for financial tasks: LoRA and QLoRA
    5. 6.2.5 Look-ahead bias in LLMs: the model that already knows the future
  3. 6.3 · Financial RAG

    1. 6.3.1 Embeddings and vector databases: FAISS, Chroma, pgvector and Qdrant
    2. 6.3.2 Chunking financial documents: tables, footnotes and XBRL
    3. 6.3.3 RAG over reports, prospectuses and KIDs, and how to evaluate it
  4. 6.4 · AI agents for trading

    1. 6.4.1 Tool-calling agents: tool calling, ReAct and planning
    2. 6.4.2 Agent frameworks and MCP
    3. 6.4.3 TradingAgents-style multi-agent architectures
    4. 6.4.4 Evidence and limits of agents
    5. 6.4.5 Guardrails for agents: the agent proposes, the engine decides
    6. 6.4.6 Research agents: hypotheses, alpha mining and review
  5. 6.5 · AI as the quant's copilot

    1. 6.5.1 Coding assistants with a test-first workflow
    2. 6.5.2 Automated review of backtests and code migrations
    3. 6.5.3 Risks of AI-generated code
  6. 6.6 · Big data for trading

    1. 6.6.1 When data becomes “big”: ticks, order books, options and universes
    2. 6.6.2 Parquet, Arrow, Polars streaming and out-of-core DuckDB
    3. 6.6.3 Dask, Spark and Ray: from one machine to a cluster
    4. 6.6.4 Time series databases: TimescaleDB, QuestDB, ClickHouse, ArcticDB and kdb+
    5. 6.6.5 Cloud storage, lakehouse (Delta, Iceberg) and GPU (RAPIDS, cuDF)
  7. 6.7 · Alternative data

    1. 6.7.1 Overview and size of the alternative data market
    2. 6.7.2 Types of alternative data: what each one measures
    3. 6.7.3 Responsible web scraping
    4. 6.7.4 Alternative data vendors and due diligence
    5. 6.7.5 From alternative data to a trading signal
    6. 6.7.6 Satellite imagery, computer vision and supply chains

“Big” is a relationship, not a size

A 10 GB file is small for a server with 1 TB of memory and huge for a 16 GB laptop. And a 2 GB table may not fit in that laptop if it has to be merged with another, sorted and grouped. The useful definition for a quant is operational: data are big when the computation you need does not fit comfortably in the memory of the machine you have. From there come the three questions of this lesson: how many records there are, how many bytes each one takes in the representation you use and how much extra memory each operation asks for.

Wes McKinney, the creator of pandas, summed it up in 2017 in a rule of thumb: with pandas you should have 5 to 10 times the size of the data in RAM; for a 10 GB dataset, 64 to 128 GB. The rule predates pandas 3 and Arrow as the default format, but the mechanism behind it is still there and is measured below: data take more space in memory than in the file, and each operation creates intermediate copies.

Lesson 1.3.1 measured one session of the fictitious stock NOVA at each granularity, from the daily bar to the L3 book messages: from 1 row to more than half a million. Here we go from one stock to a market.

BarsTradesL1 quotesL2 bookL3 book
Mermaid source
flowchart TB
  B[Bars] --> T[Trades]
  T --> Q[L1 quotes]
  Q --> L2[L2 book]
  L2 --> L3[L3 book]
  • Bars (daily or minute): one record per instrument and period, with open, high, low, close and volume.
  • Trades: one record per execution.
  • L1 quotes: the best bid and the best ask, one record per change.
  • L2 book: depth aggregated by price level, as snapshots or as per-level updates.
  • L3 book (market by order): every order and every change to every order; with it, the full book is reconstructed.

Each step to the right multiplies the number of records. Options multiply it again: each underlying has hundreds or thousands of series (expirations by strikes by type) and each one is quoted on several exchanges.

A 36-byte message

Nasdaq's TotalView-ITCH 5.0 protocol disseminates the L3 book of its market as a sequence of binary messages. The specification fixes every field: unsigned integers in network byte order (big endian), ASCII text padded with spaces, the time in nanoseconds since midnight in 6 bytes and prices as integers with 4 implied decimals. The message for a new order (Add Order, type A) takes 36 bytes:

FieldOffsetBytesContent
Message Type01A
Stock Locate12Code of the security in the session
Tracking Number32Nasdaq internal number
Timestamp56Nanoseconds since midnight
Order Reference Number118Order identifier
Buy/Sell Indicator191B or S
Shares204Shares
Stock248Symbol, padded with spaces
Price324Price with 4 implied decimals

The first cell generates a synthetic session of 2 million trades in 500 fictitious securities (T000 to T499, with the activity concentrated in the first ones, as in a real market) and writes it in that binary format. NumPy reads the binary without copying it with a structured type that reproduces the table; since NumPy has no 6-byte integers, the time is read as a 2-byte integer and a 4-byte one. The data are synthetic: they are not market data.

IN [01]python
import gc
import os
import tracemalloc

import numpy as np
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq

rng = np.random.default_rng(661)
N = 2_000_000
VALORES = np.array([f"T{i:03d}" for i in range(500)])
peso = 1 / np.arange(1, 501) ** 1.1
i_valor = rng.choice(500, N, p=peso / peso.sum())
ns = np.sort(rng.integers(34_200, 57_600, N) * 10**9      # 9:30-16:00
             + rng.integers(0, 10**9, N))
precio = np.round(rng.gamma(2.0, 40.0, 500)[i_valor]
                  * np.exp(rng.normal(0, 0.002, N)), 2)
acciones = rng.geometric(0.02, N).astype(np.int32)
lado = rng.choice(np.array(["B", "S"]), N)

ADD_ORDER = np.dtype([
    ("tipo", "S1"), ("locate", ">u2"), ("tracking", ">u2"),
    ("ts_alto", ">u2"), ("ts_bajo", ">u4"), ("referencia", ">u8"),
    ("lado", "S1"), ("acciones", ">u4"), ("valor", "S8"),
    ("precio", ">u4")])
msg = np.zeros(N, ADD_ORDER)
msg["tipo"], msg["locate"] = b"A", i_valor + 1
msg["ts_alto"], msg["ts_bajo"] = ns >> 32, ns & 0xFFFFFFFF
msg["referencia"] = np.arange(1, N + 1)
msg["lado"] = lado.astype("S1")
msg["acciones"] = acciones
msg["valor"] = np.char.ljust(VALORES[i_valor], 8).astype("S8")
msg["precio"] = np.round(precio * 10_000).astype(np.uint32)
binario = msg.tobytes()

leido = np.frombuffer(binario, ADD_ORDER)      # no copy
m = leido[0]
hora = (int(m["ts_alto"]) << 32) | int(m["ts_bajo"])
print(f"{ADD_ORDER.itemsize} bytes per message; "
      f"{len(binario) / 2**20:.1f} MiB in total")
print(m["valor"].decode().strip(), m["lado"].decode(),
      int(m["acciones"]), int(m["precio"]) / 10_000,
      pd.Timedelta(hora, "ns"))
OUT [01]Two million 36-byte messages
36 bytes per message; 68.7 MiB in total
T001 S 55 36.52 0 days 09:30:00.018014726

Two million messages take 68.7 MiB, exactly 36 bytes each, and reading them costs nothing: np.frombuffer interprets the bytes in place, without copying or converting them. The first one is a sale of 55 shares of T001 at 36.52 at 9:30:00.018. It is the most compact format in this lesson and the least convenient: the integers are in network order, the symbol carries spaces, the time is split and the price is an integer. To analyze it, it has to be converted, and the conversion has a price in memory.

The same trade in pandas, Arrow and Parquet

From the binary to an analysis table: time as a timestamp, security, price, shares, execution venue and side. Five representations of the same data are measured. In pandas, memory_usage(deep=True), which with Python objects counts the size of each object as if it were not shared with any other cell: it is an upper bound. In Arrow, nbytes. In Parquet, the size of the file compressed with zstd, which is deleted afterwards.

IN [02]python
mercado = rng.choice(np.array(list("NQPZKVJX")), N)
base = pd.Timestamp("2026-10-22")
columnas = {"ts": base + pd.to_timedelta(ns, "ns"),
            "valor": VALORES[i_valor], "precio": precio,
            "acciones": acciones, "mercado": mercado,
            "lado": lado}
TEXTOS = ["valor", "mercado", "lado"]

con_str = pd.DataFrame(columnas)            # pandas 3: str type
con_objetos = con_str.astype({c: object for c in TEXTOS})
compacto = con_str.astype({c: "category" for c in TEXTOS})
tabla = pa.Table.from_pandas(compacto, preserve_index=False)
pq.write_table(tabla, "operaciones.parquet", compression="zstd")

bytes_fila = {
    "ITCH binary (Add Order)": ADD_ORDER.itemsize,
    "pandas, Python objects":
        con_objetos.memory_usage(deep=True).sum() / N,
    "pandas, str type (Arrow)":
        con_str.memory_usage(deep=True).sum() / N,
    "pandas, categories": compacto.memory_usage(deep=True).sum() / N,
    "Arrow, dictionaries": tabla.nbytes / N,
    "Parquet zstd (disk)": os.path.getsize("operaciones.parquet") / N,
}
os.remove("operaciones.parquet")
for nombre, b in bytes_fila.items():
    print(f"{nombre:<27} {b:6.1f} B/row   "
          f"{b * N / 2**20:6.1f} MiB")
print(compacto.dtypes.astype(str).to_dict())
OUT [02]Bytes per trade in five representations
ITCH binary (Add Order)       36.0 B/row     68.7 MiB
pandas, Python objects       173.0 B/row    330.0 MiB
pandas, str type (Arrow)      50.0 B/row     95.4 MiB
pandas, categories            24.0 B/row     45.8 MiB
Arrow, dictionaries           24.0 B/row     45.8 MiB
Parquet zstd (disk)           10.0 B/row     19.1 MiB
{'ts': 'datetime64[ns]', 'valor': 'category', 'precio': 'float64', 'acciones': 'int32', 'mercado': 'category', 'lado': 'category'}
Fig. — Bytes per trade for the same synthetic session of 2 million trades in six representations, from the binary message to the Parquet file

The same session takes from 10 to 173 bytes per trade depending on how it is stored:

  • With Python objects (the object of pandas 2), each string is an object with its header (more than 40 bytes for a four-letter string) and the column stores an 8-byte pointer to each one: 173 bytes per trade, almost five times the binary message. The figure is an upper bound, because memory_usage(deep=True) counts each object even if several cells share the same one, but it gives the order of magnitude.
  • With pandas 3's str type, which stores the strings in an Arrow array (the characters one after another plus an 8-byte offset per cell), it drops to 50.
  • With categories, each repeated string is stored once and each row carries a small code: 2 bytes for 500 securities, 1 for 8 venues or 2 sides. That leaves 24 bytes per row, less than the binary, because only the fields that are analyzed are stored: 8 for the time, 8 for the price, 4 for the shares and 4 for the three codes.
  • Arrow with dictionaries (the translation of categories) takes the same, 24 bytes.
  • Parquet with zstd drops to 10 bytes on disk: it compresses well because the times are sorted and the codes repeat. But it is a disk format, not a memory format: to work with the data they have to be decompressed.

Going from objects to categories divides memory by seven without losing information. It is the first lever, and almost always the cheapest.

What each operation asks for

A table fitting in memory does not mean you can work with it. Each pandas operation creates intermediate results: a sort index, the group keys, the merged table before returning it. The standard library's tracemalloc module records the memory allocations of Python and NumPy (not those of Arrow, which has its own allocator; that is why it is measured with the categories table, which is all in NumPy) and gives the memory peak since it was started. The cell measures four common operations on the first million trades, with the peak expressed as a multiple of the size of the table:

IN [03]python
muestra = compacto.iloc[:1_000_000].copy()
tam = muestra.memory_usage(deep=True).sum()
sectores = pd.DataFrame({"valor": VALORES,
                         "sector": np.arange(500) % 11})
OPERACIONES = {
    "sort by security and time":
        lambda d: d.sort_values(["valor", "ts"]),
    "VWAP per security":
        lambda d: ((d["precio"] * d["acciones"])
                   .groupby(d["valor"], observed=True).sum()
                   / d.groupby("valor", observed=True)
                   ["acciones"].sum()),
    "1-minute bars per security":
        lambda d: d.groupby(["valor", d["ts"].dt.floor("min")],
                            observed=True)["precio"]
                   .agg(["first", "max", "min", "last"]),
    "merge with the sector":
        lambda d: d.merge(sectores.astype({"valor": "category"}),
                          on="valor"),
}

def pico(funcion, datos):
    gc.collect()
    tracemalloc.start()
    resultado = funcion(datos)
    maximo = tracemalloc.get_traced_memory()[1]
    tracemalloc.stop()
    del resultado
    return maximo

picos = {k: pico(f, muestra) / tam for k, f in OPERACIONES.items()}
print(f"table: {tam / 2**20:.1f} MiB")
for nombre, veces in picos.items():
    print(f"{nombre:<30} peak {veces:4.1f} times the table")
OUT [03]The memory peak of four operations
table: 22.9 MiB
sort by security and time      peak  2.8 times the table
VWAP per security              peak  1.3 times the table
1-minute bars per security     peak  3.2 times the table
merge with the sector          peak  5.9 times the table
Fig. — Peak memory of four pandas operations on one million trades with categoricals, in multiples of the table size (on top of the table)

The table of one million trades takes 22.9 MiB, and no operation settles for that. Sorting by security and time asks for 2.8 times the size of the table, on top of the table: the sort index and the sorted copy. The VWAP per security, which reduces a million rows to 500, asks for 1.3. The one-minute bars, with a new group key (the truncated time), 3.2. And the merge with a table of 500 sectors, the most innocent in appearance, 5.9 times: pandas builds the indexes of the two tables and the complete result table before returning it.

These are the figures for this table and this version of pandas, not universal constants, but they convey the idea behind McKinney's rule: an analysis chains several of these operations and keeps several intermediate results, so the peak of a typical working notebook is several times the size of the data.

How much each market takes up

With the bytes per row and the measured peaks, the number of records is what is missing. The figures in this section come from publications of the systems themselves (consulted on October 23, 2026) or are declared assumptions:

DatasetRecordsSource
Trades on US tapes A and B113.9 million on the peak day of Q3 2026CTA SIP metrics (CTS)
Quotes on tapes A and B2,761 million on the peak day of Q3 2026CTA SIP metrics (CQS)
US options (OPRA)311 billion messages a day: projected capacity for July 2026 (253 billion for October 2025)SIAC notice of September 15, 2025
Nasdaq L3 book (TotalView-ITCH)Sample files for the sessions of December 8 to 12, 2025: 7.9 to 12.6 GB per session, compressed with gzipNasdaq sample server
1-minute bars5,000 stocks × 390 minutes × 252 sessionsAssumption
Daily bars of a global universe50,000 instruments × 252 sessions × 20 yearsAssumption

Two caveats. The CTA figures are those of the peak day of each quarter, not the average, and they only cover tapes A and B (stocks listed on NYSE and on exchanges other than Nasdaq; Nasdaq stocks go through another SIP, UTP's). And the OPRA figure is the capacity the system is sized for, not the traffic of a normal day. Both serve to size an infrastructure, which is what they are published for.

The bytes per row of quotes and bars are measured the same way as those of trades, with one-row tables with their columns in compact types:

IN [04]python
def bytes_por_fila(**tipos):
    vacia = pd.DataFrame({c: pd.Series([0], dtype=t)
                          for c, t in tipos.items()})
    return vacia.memory_usage(index=False).sum()

B_OPERACION = bytes_fila["pandas, categories"]
B_COTIZACION = bytes_por_fila(
    ts="datetime64[ns]", valor="int16", compra="float64",
    venta="float64", tam_compra="int32", tam_venta="int32",
    mercado="int8")
B_BARRA = bytes_por_fila(
    ts="datetime64[ns]", valor="int32", apertura="float64",
    maximo="float64", minimo="float64", cierre="float64",
    volumen="int64")
print(f"bytes per row: trade {B_OPERACION:.0f}, quote "
      f"{B_COTIZACION}, bar {B_BARRA}")

CONJUNTOS = {                      # (records, bytes per record)
    "CTA trades, peak day": (113.9e6, B_OPERACION),
    "CTA trades, 252 peak days": (113.9e6 * 252, B_OPERACION),
    "CTA quotes, peak day": (2_761e6, B_COTIZACION),
    "OPRA, one day at capacity": (311e9, B_COTIZACION),
    "1-min bars, 5,000 stocks, 1 year":
        (5_000 * 390 * 252, B_BARRA),
    "daily bars, 50,000 securities, 20 years":
        (50_000 * 252 * 20, B_BARRA),
}
pico_union = 1 + round(picos["merge with the sector"], 1)   # 1 + 5.9
tamanos = pd.DataFrame(
    [(k, n / 1e6, n * b / 1e9, n * b * pico_union / 1e9)
     for k, (n, b) in CONJUNTOS.items()],
    columns=["dataset", "millions", "GB", "GB when merging"]
).set_index("dataset")
tamanos.round(1)
OUT [04]In-memory size of each dataset
bytes per row: trade 24, quote 35, bar 52
                                         millions       GB  GB when merging
dataset
CTA trades, peak day                        113.9      2.7             18.9
CTA trades, 252 peak days                 28702.8    689.0           4753.8
CTA quotes, peak day                       2761.0     96.6            666.8
OPRA, one day at capacity                311000.0  10885.0          75106.5
1-min bars, 5,000 stocks, 1 year            491.4     25.6            176.3
daily bars, 50,000 securities, 20 years     252.0     13.1             90.4

With 24 bytes per trade, 35 per quote and 52 per bar:

  • The peak trade day of tapes A and B (113.9 million) takes 2.7 GB in memory: it fits in a laptop, but merging it with another table already asks for 19 GB, more than a 16 GB one has. A year of days like that, 689 GB, fits in none.
  • The peak quote day (2,761 million) takes 96.6 GB: it no longer fits in a laptop, but it does in a workstation or a server with 128 or 256 GB, and with the merge it asks for 670 GB.
  • The L3 book of a single exchange, Nasdaq, takes 7.9 to 12.6 GB per session in its sample files, already compressed with gzip; decompressed and turned into a table, several times more. At 36 bytes per message, 12.6 GB compressed is hundreds of millions of messages in one day.
  • OPRA at its projected capacity, 311 billion messages in one day, would be almost 11 TB in memory. There is no machine with that much memory; not even a laptop's disk can hold it.
  • The bars of a large universe are moderate: 25.6 GB for the one-minute bars of 5,000 stocks over a year and 13.1 GB for the daily bars of 50,000 securities over 20 years. A one-machine problem, not a cluster problem.

The figure at the top sums it up on a logarithmic scale, against the memory of a 16 GB laptop and of a large 1 TB server.

On a linear scale, the OPRA bar would be about 4,000 times as long as the one for the trade day. The jump between what a quant works with using bars or a day's trades and what the full options book requires is three to four orders of magnitude. That is why “big” means different things on each desk: for a team that researches with daily bars, a global universe fits in a laptop; for one that models options microstructure, no computer is enough and the data are filtered (underlyings, expirations, time windows) before reaching memory.

A year of OPRA at its capacity, in a single memory allocation, stops being a pandas problem: the operating system cannot provide it. NumPy computes the size and requests it; Linux, with its default memory policy, refuses before touching a byte:

IN [05]python
un_ano = int(311e9 * 252 * B_COTIZACION)
np.empty(un_ano, dtype=np.uint8)
OUT [05]A year of OPRA in one array
numpy._core._exceptions._ArrayMemoryError: Unable to allocate 2.44 PiB for an array with shape (2743020000000000,) and data type uint8

What to do when it does not fit

The first step is not a new tool, but to reduce. Almost no analysis needs all the columns, all the rows and maximum precision at the same time:

LeverExampleMeasured or computed effect
Compact typesRepeated strings as categories170 to 24 bytes per trade (divide by 7)
ColumnsRead only time, security, price and shares24 to 22 bytes per trade
RowsOne session, one venue, the securities in the universeProportional
Aggregate earlyFrom trades to 1-minute bars2 million rows to a few tens of thousands
On-disk formatParquet with zstd instead of CSV10 bytes per trade, and only the requested columns are read

If that is not enough, the order of the following lessons is the order in which it pays to scale:

yesnoyesnoyesnoFits with room to spare?pandas or PolarsReduceFits now?Local streamingFits on one disk?Polars or DuckDBDistribute
Mermaid source
flowchart TB
  A[Fits with room to spare?] -->|yes| P[pandas or Polars]
  A -->|no| R[Reduce]
  R --> B[Fits now?]
  B -->|yes| P
  B -->|no| S[Local streaming]
  S --> C[Fits on one disk?]
  C -->|yes| S2[Polars or DuckDB]
  C -->|no| D[Distribute]
  • On one machine, out of core (lesson 6.6.2): partitioned Parquet, Arrow, Polars in lazy and streaming mode and DuckDB with a memory limit process in chunks datasets much larger than RAM. A CTA peak quote day (about 97 GB in memory) is a problem for one machine with a good disk, not for a cluster.
  • Distribute (lesson 6.6.3): Dask, Spark or Ray, when the computation does not fit even on one machine's disk or has to be split so that it finishes in time.
  • Store for querying (lesson 6.6.4): time series databases and ArcticDB, when the data arrive every day and are queried by date ranges and securities.
  • Cloud and lakehouse (lesson 6.6.5): when the volume or the team justifies it.

Exercises

  1. Compute how much one day of L2 book snapshots with 10 levels per side takes (price as float64 and size as int32 per level, plus time and security), every 100 milliseconds, for the lesson's 500 securities during the 6.5 hours of the session. Does it fit in 16 GB with the merge peak?
Solution
IN [06]python
niveles, lados = 10, 2
b_foto = 8 + 2 + niveles * lados * (8 + 4)     # ts, security, levels
fotos = 500 * 6.5 * 3600 * 10                   # every 100 ms
gb = fotos * b_foto / 1e9
print(f"{fotos:,.0f} snapshots of {b_foto} bytes: {gb:.1f} GB; "
      f"with the merge peak, {gb * pico_union:.1f} GB")
OUT [06]One day of L2 book snapshots
117,000,000 snapshots of 250 bytes: 29.2 GB; with the merge peak, 201.8 GB

Each snapshot takes 250 bytes (8 for the time, 2 for the security and 20 levels of 12 bytes) and a day has 117 million snapshots: 29.2 GB in memory and 201.8 GB with the merge peak. It does not fit in 16 GB. Options: fewer levels (the best five divide the size almost by two), lower frequency (one snapshot per second divides it by ten), only the securities in the strategy's universe, or storing the book changes instead of full snapshots.

  1. Measure the peak of sorting the table with Python objects (the first million rows) and compare it with that of the categories table. How much memory does it ask for in total, table included?
Solution
IN [07]python
muestra_obj = con_objetos.iloc[:1_000_000].copy()
tam_obj = muestra_obj.memory_usage(deep=True).sum()
p_obj = pico(OPERACIONES["sort by security and time"], muestra_obj)
p_cat = picos["sort by security and time"] * tam
for nombre, t_, p_ in [("objects", tam_obj, p_obj),
                       ("categories", tam, p_cat)]:
    print(f"{nombre:<11} table {t_ / 2**20:6.1f} MiB, peak "
          f"{p_ / 2**20:5.1f} MiB, total "
          f"{(t_ + p_) / 2**20:6.1f} MiB")
OUT [07]Sorting with Python objects
objects     table  165.0 MiB, peak  63.5 MiB, total  228.5 MiB
categories  table   22.9 MiB, peak  63.5 MiB, total   86.4 MiB

The peak of sorting is the same, 63.5 MiB, in both tables: it is dominated by the integer arrays pandas uses to compute the order, which do not depend on how the strings are stored. What changes is the table: 165.0 MiB with objects against 22.9 with categories. In total, 228.5 against 86.4 MiB: 2.6 times more memory to do the same thing.

  1. In Parquet, a trade takes about 10 bytes. Does that mean that a CTA peak trade day in Parquet (about 1.1 GB) can be analyzed with 2 GB of RAM?
Solution

No. The 10 bytes per trade are on disk and compressed. When the whole file is read, Parquet is decompressed into Arrow or pandas and goes back to taking its 24 bytes per row: 2.7 GB, more than there is. What Parquet does allow is not to read it whole: only the necessary columns, only the row groups that pass a filter, and a streaming engine (Polars, DuckDB) that aggregates in chunks and never holds the complete table in memory. With that, a VWAP per security for the whole day fits in much less than 2 GB. That is the topic of lesson 6.6.2.

Each answer is folded under its question

Self-assessment

  1. An Add Order message of Nasdaq TotalView-ITCH 5.0 takes 36 bytes. How much does the same information take in a pandas DataFrame with the text stored as Python objects?

    • AThe same: pandas stores the data as they arrive
    • BLess, because pandas compresses
    • CSeveral times more: each string is a Python object with its header and an 8-byte pointer, and integers and prices are widened to 8 bytes; with categories it drops below the binary
    • DExactly double
    Show answer

    Answer C. Several times more: each string is a Python object with its header and an 8-byte pointer, and integers and prices are widened to 8 bytes; with categories it drops below the binary

    In the lesson, the table with Python objects counts about 170 bytes per trade according to memory_usage(deep=True), pandas 3's str type (Arrow underneath) 50 and categories 24, less than the message's 36 because only the fields that are used are stored.

  2. A table takes 10 GB in memory. Is a 16 GB laptop enough to work with it in pandas?

    • ANo: sorting or merging it asks for between one and almost six times its size in addition (as measured in the lesson), and McKinney's rule, 5 to 10 times the data size in RAM, points to 64–128 GB
    • BYes, there are 6 GB to spare
    • CYes, if the browser is closed
    • DIt depends only on the processor
    Show answer

    Answer A. No: sorting or merging it asks for between one and almost six times its size in addition (as measured in the lesson), and McKinney's rule, 5 to 10 times the data size in RAM, points to 64–128 GB

    What counts is the operation's peak, not the size of the table. In the lesson, a merge asked for 5.9 times the size of the table on top of the table, and a grouping by minute, 3.2.

  3. According to the capacity projection published in September 2025, how many messages a day must OPRA be able to disseminate in July 2026?

    • A311 million
    • B3.11 billion
    • C311 billion (it is the capacity the system is sized for, not the average traffic)
    • D31 trillion
    Show answer

    Answer C. 311 billion (it is the capacity the system is sized for, not the average traffic)

    SIAC's notice of September 15, 2025 projects 253 billion daily messages for October 2025 and 311 billion for July 2026. At 35 bytes per message, one day at that capacity is almost 11 TB.

  4. What is the first thing to try when a computation does not fit in memory?

    • ASet up a Spark cluster
    • BReduce: read only the necessary columns and rows, use compact types (categories, small integers) and aggregate early; then, process in chunks or with an out-of-core engine on the same machine
    • CBuy more RAM
    • DMove to the cloud
    Show answer

    Answer B. Reduce: read only the necessary columns and rows, use compact types (categories, small integers) and aggregate early; then, process in chunks or with an out-of-core engine on the same machine

    Reducing costs little and is often enough: in the lesson, turning strings into categories divides memory by seven. Lessons 6.6.2 and 6.6.3 cover what comes next: columnar formats and streaming engines, and then distribution.

  5. CTA's metrics give 113.9 million trades on the peak day of the third quarter of 2026. What is that figure good for and what is it not good for?

    • AIt is the average daily volume: it can be used to budget a year of storage by multiplying it by 252
    • BIt is the US total, tapes A, B and C
    • CIt is the maximum capacity of the system
    • DIt is the busiest day of the quarter on tapes A and B: it serves to size the memory and bandwidth needed on the worst day, and multiplied by 252 it gives an upper bound, not an estimate
    Show answer

    Answer D. It is the busiest day of the quarter on tapes A and B: it serves to size the memory and bandwidth needed on the worst day, and multiplied by 252 it gives an upper bound, not an estimate

    The CTA document gives the daily peak of each quarter, only for tapes A and B (Nasdaq stocks go through the UTP SIP). Sizing with the peak is the prudent choice for memory; for a year of storage, the average is needed.

Key points

  • Data are “big” relative to the memory of a machine and to what has to be done with them: Wes McKinney's (2017) rule of thumb for pandas is to have 5 to 10 times the size of the data in RAM.
  • The same trade takes 36 bytes in an ITCH message, about 170 in pandas with Python objects, 50 with the str type, 24 with categories or in Arrow and about 10 in Parquet with zstd.
  • Operations ask for extra memory: measured with tracemalloc, sorting asks for 2.8 times the size of the table, grouping into one-minute bars 3.2 and merging 5.9.
  • With published figures: the peak trade day of CTA's tapes A and B (113.9 million, 2.7 GB) fits in a laptop's memory; the quote day (2,761 million, about 97 GB) does not; one day of OPRA at its projected capacity (311 billion messages) is almost 11 TB and fits in no machine.
  • Before scaling, reduce: columns, rows, types and early aggregation. Then, in this order: columnar formats and out-of-core engines on one machine (6.6.2), distribution (6.6.3), time series databases (6.6.4) and the cloud (6.6.5).

Resources