Lección 6.6.1 · 60 min

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.

Fig. — Tamaño en memoria de seis conjuntos de datos de mercado con los bytes por fila medidos en la lección, frente a un portátil de 16 GB y un servidor de 1 TB. Registros: métricas de CTA (3.er trimestre de 2026), capacidad proyectada de OPRA (julio de 2026) y supuestos para las barras
Lecciones del nivel 6
  1. 6.1 · NLP financiero clásico y moderno

    1. 6.1.1 Tokenización, embeddings, diccionarios financieros y FinBERT
    2. 6.1.2 Fuentes de texto: EDGAR, CNMV, transcripciones y redes sociales
    3. 6.1.3 Event studies con señales de texto, alineación temporal y topic modeling
  2. 6.2 · LLMs para investigación y trading

    1. 6.2.1 Cómo funciona un LLM: del siguiente token a la API
    2. 6.2.2 Prompting estructurado: JSON que se puede validar
    3. 6.2.3 Guidance, KPIs y tono: qué extrae bien un LLM
    4. 6.2.4 Fine-tuning ligero para tareas financieras: LoRA y QLoRA
    5. 6.2.5 Sesgo de anticipación en LLMs: el modelo que ya conoce el futuro
  3. 6.3 · RAG financiero

    1. 6.3.1 Embeddings y bases vectoriales: FAISS, Chroma, pgvector y Qdrant
    2. 6.3.2 Chunking de documentos financieros: tablas, notas al pie y XBRL
    3. 6.3.3 RAG sobre informes, folletos y KID, y cómo evaluarlo
  4. 6.4 · Agentes de IA para trading

    1. 6.4.1 Agentes con herramientas: tool calling, ReAct y planificación
    2. 6.4.2 Marcos de agentes y MCP
    3. 6.4.3 Arquitecturas multiagente tipo TradingAgents
    4. 6.4.4 Evidencia y límites de los agentes
    5. 6.4.5 Guardarraíles para agentes: el agente propone, el motor decide
    6. 6.4.6 Agentes de investigación: hipótesis, alpha mining y revisión
  5. 6.5 · IA como copiloto del quant

    1. 6.5.1 Asistentes de código con un flujo de tests primero
    2. 6.5.2 Revisión automática de backtests y migraciones de código
    3. 6.5.3 Riesgos del código generado por IA
  6. 6.6 · Big data para trading

    1. 6.6.1 Cuándo los datos son «big»: ticks, libros, opciones y universos
    2. 6.6.2 Parquet, Arrow, Polars en streaming y DuckDB fuera de memoria
    3. 6.6.3 Dask, Spark y Ray: de una máquina a un clúster
    4. 6.6.4 Bases de series temporales: TimescaleDB, QuestDB, ClickHouse, ArcticDB y kdb+
    5. 6.6.5 Almacenamiento en la nube, lakehouse (Delta, Iceberg) y GPU (RAPIDS, cuDF)
  7. 6.7 · Datos alternativos

    1. 6.7.1 Panorama y tamaño del mercado de datos alternativos
    2. 6.7.2 Tipos de datos alternativos: qué mide cada uno
    3. 6.7.3 Web scraping responsable
    4. 6.7.4 Proveedores y due diligence de datos alternativos
    5. 6.7.5 Del dato alternativo a la señal
    6. 6.7.6 Satélite, visión por computador y cadena de suministro

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

BarrasOperacionesCotizaciones L1Libro L2Libro L3
Fuente 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:

CampoDesplazamientoBytesContenido
Tipo de mensaje01A
Stock Locate12Código del valor en la sesión
Tracking Number32Número interno de Nasdaq
Timestamp56Nanosegundos desde medianoche
Order Reference Number118Identificador de la orden
Buy/Sell Indicator191B o S
Shares204Acciones
Stock248Símbolo, rellenado con espacios
Price324Precio 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.

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)      # 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"))
OUT [01]Dos millones de mensajes de 36 bytes
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.

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: 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())
OUT [02]Bytes por operación en cinco representaciones
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

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:

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 = {
    "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")
OUT [03]El pico de memoria de cuatro operaciones
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)

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:

ConjuntoRegistrosFuente
Operaciones de las cintas A y B de EE. UU.113,9 millones en el día pico del 3.er trimestre de 2026Métricas del SIP de CTA (CTS)
Cotizaciones de las cintas A y B2.761 millones en el día pico del 3.er trimestre de 2026Mé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 gzipServidor de muestras de Nasdaq
Barras de 1 minuto5.000 acciones × 390 minutos × 252 sesionesSupuesto
Barras diarias de un universo global50.000 instrumentos × 252 sesiones × 20 añosSupuesto

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:

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, 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)
OUT [04]Tamaño en memoria de cada conjunto
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:

IN [05]python
un_ano = int(311e9 * 252 * B_COTIZACION)
np.empty(un_ano, dtype=np.uint8)
OUT [05]Un año de OPRA en una matriz
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:

PalancaEjemploEfecto medido o calculado
Tipos compactosTextos repetidos como categorías170 a 24 bytes por operación (dividir por 7)
ColumnasLeer solo hora, valor, precio y acciones24 a 22 bytes por operación
FilasUna sesión, un mercado, los valores del universoProporcional
Agregar prontoDe operaciones a barras de 1 minuto2 millones de filas a unas pocas decenas de miles
Formato en discoParquet con zstd en vez de CSV10 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:

sínosínosíno¿Cabe con holgura?pandas o PolarsReducir¿Cabe ahora?Streaming local¿Cabe en un disco?Polars o DuckDBDistribuir
Fuente 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.

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
IN [06]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")
OUT [06]Un día de fotos del libro L2
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.

  1. 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
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["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")
OUT [07]Ordenar con objetos de Python
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.

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

Respuesta plegada en cada pregunta

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?

    • ALo mismo: pandas guarda los datos tal como llegan
    • BMenos, porque pandas comprime
    • CVarias 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
    • DExactamente el doble
    Ver respuesta

    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?

    • ANo: 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
    • BSí, sobran 6 GB
    • CSí, si se cierra el navegador
    • DDepende solo del procesador
    Ver respuesta

    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?

    • A311 millones
    • B3.110 millones
    • C311.000 millones (es la capacidad para la que se dimensiona el sistema, no el tráfico medio)
    • D31 billones
    Ver respuesta

    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?

    • AMontar un clúster de Spark
    • BReducir: 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
    • CComprar más RAM
    • DPasar a la nube
    Ver respuesta

    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?

    • AEs el volumen medio diario: sirve para presupuestar el almacenamiento de un año multiplicándola por 252
    • BEs el total de EE. UU., cintas A, B y C
    • CEs la capacidad máxima del sistema
    • DEs 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
    Ver respuesta

    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