---
title: "6.6.1 Cuándo los datos son «big» en trading"
description: "Cuánto ocupan ticks, libros L2 y L3, OPRA y universos globales con cifras de CTA, Nasdaq y OPRA, y la memoria de pandas y Arrow medida con datos generados."
url: "https://pythonparatrading.com/curso-python-trading/nivel-6/cuando-los-datos-son-big/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-6/cuando-los-datos-son-big.png"
---

# 6.6.1 · Cuándo los datos son «big»: ticks, libros, opciones y universos

> «Big data» no es un tamaño, es una relación: los datos son grandes cuando no caben con holgura en la memoria de la máquina para lo que hay que hacer con ellos. Esta lección pone cifras a esa relación. Parte de un mensaje de 36 bytes del protocolo ITCH de Nasdaq, mide cuánto ocupa la misma operación en pandas, en Arrow y en Parquet, cuánta memoria extra pide ordenar, agrupar o unir, y con las estadísticas publicadas por CTA, Nasdaq y OPRA calcula qué conjuntos de datos de mercado caben en un portátil y cuáles no caben en ninguna máquina.

- Curso: [Nivel 6: IA generativa, LLMs, agentes, big data y datos alternativos](https://pythonparatrading.com/curso-python-trading/nivel-6/)
- Módulo: 6.6 · Big data para trading
- Duración: 60 min
- Requisitos: [1.3.1 · Tipos de datos de mercado: EOD, barras, ticks, libro y referencia](https://pythonparatrading.com/curso-python-trading/nivel-1/tipos-de-datos-de-mercado/), [1.4.7 · Almacenamiento: CSV, Parquet, particionado, DuckDB, TimescaleDB y ArcticDB](https://pythonparatrading.com/curso-python-trading/nivel-1/almacenamiento-parquet-duckdb-y-bases-temporales/), [0.5.4 · Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas](https://pythonparatrading.com/curso-python-trading/nivel-0/polars-lazy-y-expresiones/)

## Al terminar esta lección

- Calcular el tamaño en memoria de un conjunto de datos de mercado a partir del número de registros y de los bytes por fila de cada representación.
- Medir la memoria de una tabla en pandas con cadenas, con el tipo `str`, con categorías, en Arrow y en Parquet, y explicar de dónde sale cada diferencia.
- Medir el pico de memoria de operaciones habituales (ordenar, agrupar, unir, remuestrear) y usarlo para estimar cuándo deja de caber un cálculo.
- Situar tick data, libros L2 y L3, OPRA y universos globales respecto a la memoria de una máquina con cifras de fuentes primarias y supuestos declarados.
- Decidir entre reducir, procesar por bloques, usar un motor fuera de memoria o distribuir, según cuánto excede el cálculo a la memoria.

## «Big» es una relación, no un tamaño

Un fichero de 10 GB es pequeño para un servidor con 1 TB de memoria y enorme para un portátil de 16 GB. Y una tabla
de 2 GB puede no caber en ese portátil si hay que unirla con otra, ordenarla y agruparla. La definición útil para un
quant es operativa: **los datos son grandes cuando el cálculo que necesitas no cabe con holgura en la memoria de la
máquina que tienes**. De ahí salen las tres preguntas de esta lección: cuántos registros hay, cuántos bytes ocupa cada
uno en la representación que uses y cuánta memoria extra pide cada operación.

Wes McKinney, creador de pandas, lo resumió en 2017 en una regla práctica: con pandas conviene tener **de 5 a 10
veces** el tamaño de los datos en RAM; para un conjunto de 10 GB, de 64 a 128 GB. La regla es de antes de pandas 3 y
de Arrow como formato por defecto, pero el mecanismo que la explica sigue ahí y se mide más abajo: los datos
ocupan más en memoria que en el fichero, y cada operación crea copias intermedias.

La lección 1.3.1 midió una sesión de la acción ficticia NOVA en cada granularidad, de la barra diaria a los mensajes
del libro L3: de 1 fila a más de medio millón. Aquí se pasa de una acción a un mercado.

```mermaid
flowchart TB
  B[Barras] --> T[Operaciones]
  T --> Q[Cotizaciones L1]
  Q --> L2[Libro L2]
  L2 --> L3[Libro L3]
```

- **Barras** (diarias o de minutos): un registro por instrumento y periodo, con apertura, máximo, mínimo, cierre y
  volumen.
- **Operaciones** (*trades*): un registro por ejecución.
- **Cotizaciones L1** (*quotes*): el mejor precio de compra y de venta, un registro por cada cambio.
- **Libro L2**: la profundidad agregada por nivel de precio, en fotos o en actualizaciones por nivel.
- **Libro L3** (*market by order*): cada orden y cada cambio de cada orden; con él se reconstruye el libro completo.

Cada paso a la derecha multiplica el número de registros. Las opciones lo multiplican otra vez: cada subyacente
tiene cientos o miles de series (vencimientos por strikes por tipo) y cada una cotiza en varios mercados.

## Un mensaje de 36 bytes

El protocolo TotalView-ITCH 5.0 de Nasdaq difunde el libro L3 de su mercado como una secuencia de mensajes binarios. La
especificación fija cada campo: enteros sin signo en orden de bytes de red (*big endian*), textos ASCII rellenos con
espacios, la hora en **nanosegundos desde medianoche** en 6 bytes y los precios como enteros con 4 decimales
implícitos. El mensaje de una orden nueva (*Add Order*, tipo `A`) ocupa 36 bytes:

| Campo | Desplazamiento | Bytes | Contenido |
|---|---|---|---|
| Tipo de mensaje | 0 | 1 | `A` |
| Stock Locate | 1 | 2 | Código del valor en la sesión |
| Tracking Number | 3 | 2 | Número interno de Nasdaq |
| Timestamp | 5 | 6 | Nanosegundos desde medianoche |
| Order Reference Number | 11 | 8 | Identificador de la orden |
| Buy/Sell Indicator | 19 | 1 | `B` o `S` |
| Shares | 20 | 4 | Acciones |
| Stock | 24 | 8 | Símbolo, rellenado con espacios |
| Price | 32 | 4 | Precio con 4 decimales implícitos |

La primera celda genera una sesión sintética de 2 millones de operaciones en 500 valores ficticios (`T000` a `T499`,
con la actividad concentrada en los primeros, como en un mercado real) y la escribe en ese formato binario. NumPy lee
el binario sin copiarlo con un tipo estructurado que reproduce la tabla; como NumPy no tiene enteros de 6 bytes, la hora
se lee como un entero de 2 bytes y otro de 4. Los datos son sintéticos: no son datos de mercado.

```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)      # sin copiar
m = leido[0]
hora = (int(m["ts_alto"]) << 32) | int(m["ts_bajo"])
print(f"{ADD_ORDER.itemsize} bytes por mensaje; "
      f"{len(binario) / 2**20:.1f} MiB en total")
print(m["valor"].decode().strip(), m["lado"].decode(),
      int(m["acciones"]), int(m["precio"]) / 10_000,
      pd.Timedelta(hora, "ns"))
```
```output
36 bytes por mensaje; 68.7 MiB en total
T001 S 55 36.52 0 days 09:30:00.018014726
```

Dos millones de mensajes ocupan 68,7 MiB, exactamente 36 bytes cada uno, y leerlos no cuesta nada: `np.frombuffer`
interpreta los bytes en su sitio, sin copiarlos ni convertirlos. El primero es una venta de 55 acciones de `T001` a
36,52 a las 9:30:00,018. Es el formato más compacto de esta lección y el menos cómodo: los enteros están en orden de
red, el símbolo lleva espacios, la hora va partida y el precio es un entero. Para analizar hay que convertirlo, y la
conversión tiene un precio en memoria.

## La misma operación en pandas, Arrow y Parquet

Del binario a una tabla de análisis: hora como fecha, valor, precio, acciones, mercado de ejecución y lado. Se miden
cinco representaciones de los mismos datos. En pandas, `memory_usage(deep=True)`, que con objetos de Python cuenta el
tamaño de cada objeto como si no se compartiera con ninguna otra celda: es un límite superior. En Arrow, `nbytes`. En
Parquet, el tamaño del fichero comprimido con zstd, que se borra después.

```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: tipo str
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 = {
    "binario ITCH (Add Order)": ADD_ORDER.itemsize,
    "pandas, objetos de Python":
        con_objetos.memory_usage(deep=True).sum() / N,
    "pandas, tipo str (Arrow)":
        con_str.memory_usage(deep=True).sum() / N,
    "pandas, categorías": compacto.memory_usage(deep=True).sum() / N,
    "Arrow, diccionarios": tabla.nbytes / N,
    "Parquet zstd (disco)": 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/fila   "
          f"{b * N / 2**20:6.1f} MiB")
print(compacto.dtypes.astype(str).to_dict())
```
```output
binario ITCH (Add Order)      36.0 B/fila     68.7 MiB
pandas, objetos de Python    173.0 B/fila    330.0 MiB
pandas, tipo str (Arrow)      50.0 B/fila     95.4 MiB
pandas, categorías            24.0 B/fila     45.8 MiB
Arrow, diccionarios           24.0 B/fila     45.8 MiB
Parquet zstd (disco)          10.0 B/fila     19.1 MiB
{'ts': 'datetime64[ns]', 'valor': 'category', 'precio': 'float64', 'acciones': 'int32', 'mercado': 'category', 'lado': 'category'}
```

*Fig. — Bytes por operación de la misma sesión sintética de 2 millones de operaciones en seis representaciones, del mensaje binario al fichero Parquet.* Barras horizontales: binario ITCH 36 bytes; pandas con objetos de Python 173 (destacado); pandas con el tipo str 50; pandas con categorías 24; Arrow con diccionarios 24; Parquet con zstd en disco 10.

La misma sesión ocupa de 10 a 173 bytes por operación según cómo se guarde:

- Con **objetos de Python** (el `object` de pandas 2), cada texto es un objeto con su cabecera (más de 40 bytes por
  una cadena de cuatro letras) y la columna guarda un puntero de 8 bytes a cada uno: 173 bytes por operación, casi
  cinco veces el mensaje binario. La cifra es un límite superior, porque `memory_usage(deep=True)` cuenta cada objeto
  aunque varias celdas compartan el mismo, pero da el orden de magnitud.
- Con el **tipo `str`** de pandas 3, que guarda los textos en un array de Arrow (los caracteres seguidos más un
  desplazamiento de 8 bytes por celda), baja a 50.
- Con **categorías**, cada texto repetido se guarda una vez y cada fila lleva un código pequeño: 2 bytes para 500
  valores, 1 para 8 mercados o 2 lados. Quedan 24 bytes por fila, menos que el binario, porque solo se guardan los
  campos que se analizan: 8 de la hora, 8 del precio, 4 de las acciones y 4 de los tres códigos.
- **Arrow** con diccionarios (la traducción de las categorías) ocupa lo mismo, 24 bytes.
- **Parquet** con zstd baja a 10 bytes en disco: comprime bien porque las horas están ordenadas y los códigos se
  repiten. Pero es un formato de disco, no de memoria: para trabajar con los datos hay que descomprimirlos.

Pasar de objetos a categorías divide la memoria por siete sin perder información. Es la primera palanca, y casi
siempre la más barata.

## Lo que pide cada operación

Que una tabla quepa en memoria no significa que se pueda trabajar con ella. Cada operación de pandas crea resultados
intermedios: un índice de ordenación, las claves de los grupos, la tabla unida antes de devolverla. El módulo
`tracemalloc` de la biblioteca estándar registra las reservas de memoria de Python y de NumPy (no las de Arrow, que
tiene su propio gestor; por eso se mide con la tabla de categorías, que está toda en NumPy) y da el **pico** de memoria
desde que se activa. La celda mide cuatro operaciones habituales sobre el primer millón de operaciones, con el pico
expresado en veces el tamaño de la tabla:

```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 = {
    "ordenar por valor y hora":
        lambda d: d.sort_values(["valor", "ts"]),
    "VWAP por valor":
        lambda d: ((d["precio"] * d["acciones"])
                   .groupby(d["valor"], observed=True).sum()
                   / d.groupby("valor", observed=True)
                   ["acciones"].sum()),
    "barras de 1 minuto por valor":
        lambda d: d.groupby(["valor", d["ts"].dt.floor("min")],
                            observed=True)["precio"]
                   .agg(["first", "max", "min", "last"]),
    "unir con el 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"tabla: {tam / 2**20:.1f} MiB")
for nombre, veces in picos.items():
    print(f"{nombre:<30} pico {veces:4.1f} veces la tabla")
```
```output
tabla: 22.9 MiB
ordenar por valor y hora       pico  2.8 veces la tabla
VWAP por valor                 pico  1.3 veces la tabla
barras de 1 minuto por valor   pico  3.2 veces la tabla
unir con el sector             pico  5.9 veces la tabla
```

*Fig. — Pico de memoria de cuatro operaciones de pandas sobre un millón de operaciones con categorías, en veces el tamaño de la tabla (además de la tabla).* Barras horizontales: ordenar por valor y hora 2,8 veces; VWAP por valor 1,3; barras de 1 minuto por valor 3,2; unir con el sector 5,9 (destacado). Una línea marca 1 vez, el tamaño de la tabla.

La tabla de un millón de operaciones ocupa 22,9 MiB, y ninguna operación se conforma con eso. Ordenar por valor y hora
pide 2,8 veces el tamaño de la tabla, además de la tabla: el índice de ordenación y la copia ordenada. La VWAP por
valor, que reduce un millón de filas a 500, pide 1,3. Las barras de un minuto, con una clave de grupo nueva (la hora
truncada), 3,2. Y la unión con una tabla de 500 sectores, la más inocente en apariencia, 5,9 veces: pandas construye
los índices de las dos tablas y la tabla resultado completa antes de devolverla.

Son las cifras de esta tabla y de esta versión de pandas, no constantes universales, pero dan la idea que hay detrás de
la regla de McKinney: un análisis encadena varias de estas operaciones y conserva varios resultados intermedios, así
que el pico de un cuaderno de trabajo típico es de varias veces el tamaño de los datos.

## Cuánto ocupa cada mercado

Con los bytes por fila y los picos medidos, falta el número de registros. Las cifras de esta sección salen de
publicaciones de los propios sistemas (consultadas el 23 de octubre de 2026) o son supuestos declarados:

| Conjunto | Registros | Fuente |
|---|---|---|
| Operaciones de las cintas A y B de EE. UU. | 113,9 millones en el día pico del 3.er trimestre de 2026 | Métricas del SIP de CTA (CTS) |
| Cotizaciones de las cintas A y B | 2.761 millones en el día pico del 3.er trimestre de 2026 | Métricas del SIP de CTA (CQS) |
| Opciones de EE. UU. (OPRA) | 311.000 millones de mensajes al día: capacidad proyectada para julio de 2026 (253.000 millones para octubre de 2025) | Aviso de SIAC de 15 de septiembre de 2025 |
| Libro L3 de Nasdaq (TotalView-ITCH) | Ficheros de muestra de las sesiones del 8 al 12 de diciembre de 2025: de 7,9 a 12,6 GB por sesión, comprimidos con gzip | Servidor de muestras de Nasdaq |
| Barras de 1 minuto | 5.000 acciones × 390 minutos × 252 sesiones | Supuesto |
| Barras diarias de un universo global | 50.000 instrumentos × 252 sesiones × 20 años | Supuesto |

Dos advertencias. Las cifras de CTA son las del **día pico** de cada trimestre, no la media, y solo cubren las cintas A
y B (las acciones cotizadas en NYSE y en otros mercados que no son Nasdaq; las de Nasdaq van por otro SIP, el de UTP).
Y la de OPRA es la **capacidad** para la que se dimensiona el sistema, no el tráfico de un día normal. Las dos sirven
para dimensionar una infraestructura, que es para lo que se publican.

Los bytes por fila de las cotizaciones y de las barras se miden igual que los de las operaciones, con tablas de una
fila con sus columnas en los tipos compactos:

```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, categorías"]
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 por fila: operación {B_OPERACION:.0f}, cotización "
      f"{B_COTIZACION}, barra {B_BARRA}")

CONJUNTOS = {                      # (registros, bytes por registro)
    "operaciones CTA, día pico": (113.9e6, B_OPERACION),
    "operaciones CTA, 252 días pico": (113.9e6 * 252, B_OPERACION),
    "cotizaciones CTA, día pico": (2_761e6, B_COTIZACION),
    "OPRA, un día a capacidad": (311e9, B_COTIZACION),
    "barras 1 min, 5.000 acciones, 1 año":
        (5_000 * 390 * 252, B_BARRA),
    "barras diarias, 50.000 valores, 20 años":
        (50_000 * 252 * 20, B_BARRA),
}
pico_union = 1 + round(picos["unir con el 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=["conjunto", "millones", "GB", "GB al unir"]
).set_index("conjunto")
tamanos.round(1)
```
```output
bytes por fila: operación 24, cotización 35, barra 52
                                         millones       GB  GB al unir
conjunto
operaciones CTA, día pico                   113.9      2.7        18.9
operaciones CTA, 252 días pico            28702.8    689.0      4753.8
cotizaciones CTA, día pico                 2761.0     96.6       666.8
OPRA, un día a capacidad                 311000.0  10885.0     75106.5
barras 1 min, 5.000 acciones, 1 año         491.4     25.6       176.3
barras diarias, 50.000 valores, 20 años     252.0     13.1        90.4
```

Con 24 bytes por operación, 35 por cotización y 52 por barra:

- El **día pico de operaciones** de las cintas A y B (113,9 millones) ocupa 2,7 GB en memoria: cabe en un portátil,
  pero unirlo con otra tabla ya pide 19 GB, más de lo que tiene uno de 16 GB. Un año de días como ese, 689 GB, no
  cabe en ninguno.
- El **día pico de cotizaciones** (2.761 millones) ocupa 96,6 GB: ya no cabe en un portátil, pero sí en una estación
  de trabajo o un servidor con 128 o 256 GB, y con la unión pide 670 GB.
- El **libro L3 de un solo mercado**, Nasdaq, ocupa de 7,9 a 12,6 GB por sesión en sus ficheros de muestra, ya
  comprimidos con gzip; descomprimidos y convertidos en tabla, varias veces más. Con 36 bytes por mensaje, 12,6 GB
  comprimidos son cientos de millones de mensajes en un día.
- **OPRA** a su capacidad proyectada, 311.000 millones de mensajes en un día, serían casi 11 TB en memoria. No hay una
  máquina con esa memoria; ni siquiera el disco de un portátil lo guarda.
- Las **barras** de un universo grande son moderadas: 25,6 GB las de un minuto de 5.000 acciones durante un año y 13,1
  GB las diarias de 50.000 valores durante 20 años. Un problema de una máquina, no de un clúster.

La figura de la cabecera lo resume en escala logarítmica, frente a la memoria de un portátil de 16 GB y de un servidor
grande de 1 TB.

En una escala lineal, la barra de OPRA mediría unas 4.000 veces la del día de operaciones. El salto entre lo que trabaja un
quant con barras o con las operaciones de un día y lo que exige el libro de opciones completo es de tres a cuatro
órdenes de magnitud. Por eso «big» significa cosas distintas en cada mesa: para un equipo que investiga con barras
diarias, un universo global cabe en un portátil; para uno que modeliza la microestructura de las opciones, ningún
ordenador es suficiente y los datos se filtran (subyacentes, vencimientos, franjas) antes de llegar a la memoria.

Un año de OPRA a su capacidad, en una sola reserva de memoria, deja de ser un problema de pandas: el sistema operativo
no puede darla. NumPy calcula el tamaño y lo pide; Linux, con su política de memoria por defecto, se niega antes de
tocar un byte:

```python raises
un_ano = int(311e9 * 252 * B_COTIZACION)
np.empty(un_ano, dtype=np.uint8)
```
```output
numpy._core._exceptions._ArrayMemoryError: Unable to allocate 2.44 PiB for an array with shape (2743020000000000,) and data type uint8
```

## Qué hacer cuando no cabe

Lo primero no es una herramienta nueva, sino **reducir**. Casi ningún análisis necesita todas las columnas, todas las
filas y la máxima precisión a la vez:

| Palanca | Ejemplo | Efecto medido o calculado |
|---|---|---|
| Tipos compactos | Textos repetidos como categorías | 170 a 24 bytes por operación (dividir por 7) |
| Columnas | Leer solo hora, valor, precio y acciones | 24 a 22 bytes por operación |
| Filas | Una sesión, un mercado, los valores del universo | Proporcional |
| Agregar pronto | De operaciones a barras de 1 minuto | 2 millones de filas a unas pocas decenas de miles |
| Formato en disco | Parquet con zstd en vez de CSV | 10 bytes por operación, y solo se leen las columnas pedidas |

Si con eso no basta, el orden de las lecciones siguientes es el orden en que conviene escalar:

```mermaid
flowchart TB
  A[¿Cabe con holgura?] -->|sí| P[pandas o Polars]
  A -->|no| R[Reducir]
  R --> B[¿Cabe ahora?]
  B -->|sí| P
  B -->|no| S[Streaming local]
  S --> C[¿Cabe en un disco?]
  C -->|sí| S2[Polars o DuckDB]
  C -->|no| D[Distribuir]
```

- **En una máquina, fuera de memoria** (lección 6.6.2): Parquet particionado, Arrow, Polars en modo lazy y streaming y
  DuckDB con un límite de memoria procesan por bloques conjuntos mucho mayores que la RAM. Un día pico de cotizaciones de
  CTA (unos 97 GB en memoria) es un problema de una máquina con buen disco, no de un clúster.
- **Distribuir** (lección 6.6.3): Dask, Spark o Ray, cuando el cálculo no cabe ni en el disco de una máquina o hay que
  repartirlo para que acabe a tiempo.
- **Almacenar para consultar** (lección 6.6.4): bases de series temporales y ArcticDB, cuando los datos llegan cada día
  y se consultan por rangos de fechas y valores.
- **Nube y lakehouse** (lección 6.6.5): cuando el volumen o el equipo lo justifican.

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

Las cifras de esta lección dimensionan, no presupuestan. Un proveedor de datos entrega los suyos en su propio formato
(a menudo comprimido y con más campos de los que necesitas), y las cifras de tráfico cambian cada trimestre. Antes de
contratar o de diseñar un sistema, el cálculo se rehace con las cifras de ese proveedor y de ese mercado: registros por
día, bytes por registro en tu representación y pico de tus operaciones.

</aside>

## Ejercicios

1. Calcula cuánto ocupa un día de fotos del libro L2 con 10 niveles por lado (precio en `float64` y tamaño en `int32`
   por nivel, más hora y valor), cada 100 milisegundos, para los 500 valores de la lección durante las 6,5 horas de
   sesión. ¿Cabe en 16 GB con el pico de la unión?

**Solución.**

```python
niveles, lados = 10, 2
b_foto = 8 + 2 + niveles * lados * (8 + 4)     # ts, valor, niveles
fotos = 500 * 6.5 * 3600 * 10                   # cada 100 ms
gb = fotos * b_foto / 1e9
print(f"{fotos:,.0f} fotos de {b_foto} bytes: {gb:.1f} GB; "
      f"con el pico de la unión, {gb * pico_union:.1f} GB")
```
```output
117,000,000 fotos de 250 bytes: 29.2 GB; con el pico de la unión, 201.8 GB
```

Cada foto ocupa 250 bytes (8 de la hora, 2 del valor y 20 niveles de 12 bytes) y un día tiene 117 millones de fotos:
29,2 GB en memoria y 202,9 GB con el pico de la unión. No cabe en 16 GB. Opciones: menos niveles (los cinco mejores
dividen el tamaño casi por dos), menos frecuencia (una foto por segundo lo divide por diez), solo los valores del
universo de la estrategia o guardar los cambios del libro en lugar de fotos completas.

2. Mide el pico de ordenar la tabla con objetos de Python (el primer millón de filas) y compáralo con el de la tabla de
   categorías. ¿Cuánta memoria pide en total, tabla incluida?

**Solución.**

```python
muestra_obj = con_objetos.iloc[:1_000_000].copy()
tam_obj = muestra_obj.memory_usage(deep=True).sum()
p_obj = pico(OPERACIONES["ordenar por valor y hora"], muestra_obj)
p_cat = picos["ordenar por valor y hora"] * tam
for nombre, t_, p_ in [("objetos", tam_obj, p_obj),
                       ("categorías", tam, p_cat)]:
    print(f"{nombre:<11} tabla {t_ / 2**20:6.1f} MiB, pico "
          f"{p_ / 2**20:5.1f} MiB, total "
          f"{(t_ + p_) / 2**20:6.1f} MiB")
```
```output
objetos     tabla  165.0 MiB, pico  63.5 MiB, total  228.5 MiB
categorías  tabla   22.9 MiB, pico  63.5 MiB, total   86.4 MiB
```

El pico de ordenar es el mismo, 63,5 MiB, en las dos tablas: lo dominan los arrays de enteros con los que pandas
calcula el orden, que no dependen de cómo se guarden los textos. Lo que cambia es la tabla: 165,0 MiB con objetos
frente a 22,9 con categorías. En total, 228,5 frente a 86,4 MiB: 2,6 veces más memoria para hacer lo mismo.

3. En Parquet, una operación ocupa unos 10 bytes. ¿Significa eso que un día pico de operaciones de CTA en Parquet
   (unos 1,1 GB) se puede analizar con 2 GB de RAM?

**Solución.**

No. Los 10 bytes por operación son en disco y comprimidos. Al leer el fichero entero, Parquet se descomprime en Arrow o
en pandas y vuelve a ocupar sus 24 bytes por fila: 2,7 GB, más de lo que hay. Lo que sí permite Parquet es no leerlo
entero: solo las columnas necesarias, solo los grupos de filas que pasan un filtro, y un motor en streaming (Polars,
DuckDB) que agrega por bloques y nunca tiene la tabla completa en memoria. Con eso, una VWAP por valor de todo el día
cabe en mucho menos de 2 GB. Es el tema de la lección 6.6.2.

## Autoevaluación

1. Un mensaje Add Order de Nasdaq TotalView-ITCH 5.0 ocupa 36 bytes. ¿Cuánto ocupa la misma información en un DataFrame de pandas con los textos como objetos de Python?

   - A. Lo mismo: pandas guarda los datos tal como llegan
   - B. Menos, porque pandas comprime
   - C. Varias veces más: cada texto es un objeto de Python con su cabecera y un puntero de 8 bytes, y los enteros y precios se ensanchan a 8 bytes; con categorías baja a menos que el binario
   - D. Exactamente el doble

   *Respuesta C: Varias veces más: cada texto es un objeto de Python con su cabecera y un puntero de 8 bytes, y los enteros y precios se ensanchan a 8 bytes; con categorías baja a menos que el binario.* En la lección, la tabla con objetos de Python cuenta unos 170 bytes por operación según `memory_usage(deep=True)`, el tipo `str` de pandas 3 (Arrow por debajo) 50 y las categorías 24, menos que los 36 del mensaje porque se guardan solo los campos que se usan.

2. Una tabla ocupa 10 GB en memoria. ¿Basta un portátil de 16 GB para trabajar con ella en pandas?

   - A. No: ordenarla o unirla pide entre una y casi seis veces su tamaño adicional (lo medido en la lección), y la regla de McKinney, 5 a 10 veces la RAM del tamaño de los datos, apunta a 64-128 GB
   - B. Sí, sobran 6 GB
   - C. Sí, si se cierra el navegador
   - D. Depende solo del procesador

   *Respuesta A: No: ordenarla o unirla pide entre una y casi seis veces su tamaño adicional (lo medido en la lección), y la regla de McKinney, 5 a 10 veces la RAM del tamaño de los datos, apunta a 64-128 GB.* Lo que cuenta es el pico de la operación, no el tamaño de la tabla. En la lección, una unión pidió 5,9 veces el tamaño de la tabla además de la tabla, y una agrupación por minuto, 3,2.

3. Según la proyección de capacidad publicada en septiembre de 2025, ¿cuántos mensajes al día debe poder emitir OPRA en julio de 2026?

   - A. 311 millones
   - B. 3.110 millones
   - C. 311.000 millones (es la capacidad para la que se dimensiona el sistema, no el tráfico medio)
   - D. 31 billones

   *Respuesta C: 311.000 millones (es la capacidad para la que se dimensiona el sistema, no el tráfico medio).* El aviso de SIAC de 15 de septiembre de 2025 proyecta 253.000 millones de mensajes diarios para octubre de 2025 y 311.000 millones para julio de 2026. A 35 bytes por mensaje, un día a esa capacidad son casi 11 TB.

4. ¿Qué es lo primero que hay que intentar cuando un cálculo no cabe en memoria?

   - A. Montar un clúster de Spark
   - B. Reducir: leer solo las columnas y filas necesarias, usar tipos compactos (categorías, enteros pequeños) y agregar pronto; después, procesar por bloques o con un motor fuera de memoria en la misma máquina
   - C. Comprar más RAM
   - D. Pasar a la nube

   *Respuesta B: Reducir: leer solo las columnas y filas necesarias, usar tipos compactos (categorías, enteros pequeños) y agregar pronto; después, procesar por bloques o con un motor fuera de memoria en la misma máquina.* Reducir cuesta poco y a menudo basta: en la lección, pasar las cadenas a categorías divide la memoria por siete. Las lecciones 6.6.2 y 6.6.3 tratan lo siguiente: formatos columnares y motores en streaming, y después la distribución.

5. Las métricas de CTA dan 113,9 millones de operaciones en el día pico del tercer trimestre de 2026. ¿Para qué sirve esa cifra y para qué no?

   - A. Es el volumen medio diario: sirve para presupuestar el almacenamiento de un año multiplicándola por 252
   - B. Es el total de EE. UU., cintas A, B y C
   - C. Es la capacidad máxima del sistema
   - D. Es el día de más tráfico del trimestre en las cintas A y B: sirve para dimensionar la memoria y el ancho de banda que hacen falta en el peor día, y multiplicada por 252 da una cota superior, no una estimación

   *Respuesta D: Es el día de más tráfico del trimestre en las cintas A y B: sirve para dimensionar la memoria y el ancho de banda que hacen falta en el peor día, y multiplicada por 252 da una cota superior, no una estimación.* El documento de CTA da el pico diario de cada trimestre, solo de las cintas A y B (las acciones de Nasdaq van por el SIP de UTP). Dimensionar con el pico es lo prudente para la memoria; para el almacenamiento de un año hace falta la media.

## Ideas clave

- Los datos son «big» en relación con la memoria de una máquina y con lo que hay que hacer con ellos: la regla práctica de Wes McKinney (2017) para pandas es tener de 5 a 10 veces el tamaño de los datos en RAM.
- La misma operación ocupa 36 bytes en un mensaje ITCH, unos 170 en pandas con objetos de Python, 50 con el tipo `str`, 24 con categorías o en Arrow y unos 10 en Parquet con zstd.
- Las operaciones piden memoria extra: medida con `tracemalloc`, ordenar pide 2,8 veces el tamaño de la tabla, agrupar en barras de un minuto 3,2 y unir 5,9.
- Con cifras publicadas: el día pico de operaciones de las cintas A y B de CTA (113,9 millones, 2,7 GB) cabe en la memoria de un portátil; el de cotizaciones (2.761 millones, unos 97 GB) no; un día de OPRA a su capacidad proyectada (311.000 millones de mensajes) son casi 11 TB y no caben en ninguna máquina.
- Antes de escalar se reduce: columnas, filas, tipos y agregación temprana. Después, por este orden: formatos columnares y motores fuera de memoria en una máquina (6.6.2), distribución (6.6.3), bases de series temporales (6.6.4) y nube (6.6.5).

## Recursos

### Research

- [Big Data Management 1: introducción al big data para trading con ArcticDB](https://pythonparatrading.com/big-data-para-trading/) · Research-0019 · 16 min

### Documentación

- [Nasdaq TotalView-ITCH 5.0: especificación](https://www.nasdaqtrader.com/content/technicalsupport/specifications/dataproducts/NQTVITCHspecification.pdf) · nasdaqtrader.com
- [Nasdaq: ficheros de muestra de TotalView-ITCH](https://emi.nasdaq.com/ITCH/Nasdaq%20ITCH/) · emi.nasdaq.com
- [CTA Plan: métricas de los SIP de las cintas A y B (3T 2026)](https://www.ctaplan.com/publicdocs/ctaplan/CTA_SIP_Metrics_3Q_2026.pdf) · ctaplan.com
- [SIAC / OPRA: proyecciones de capacidad revisadas (15 de septiembre de 2025)](https://cdn.opraplan.com/documents/notices/OPRA_Capacity_Projections_Update_0925.pdf) · cdn.opraplan.com
- [McKinney, W. (2017): Apache Arrow and the «10 Things I Hate About pandas»](https://wesmckinney.com/blog/apache-arrow-pandas-internals/) · wesmckinney.com
- [Python: tracemalloc](https://docs.python.org/3/library/tracemalloc.html) · docs.python.org

Lección anterior: [6.5.3 · Riesgos del código generado por IA](https://pythonparatrading.com/curso-python-trading/nivel-6/riesgos-del-codigo-generado-por-ia/)

Lección siguiente: [6.6.2 · Parquet, Arrow, Polars en streaming y DuckDB fuera de memoria](https://pythonparatrading.com/curso-python-trading/nivel-6/parquet-arrow-polars-streaming-y-duckdb/)
