Lección 4.3.2 · 65 min
Order flow imbalance, señales de microestructura y latencia
En intervalos de segundos, el precio medio se mueve sobre todo por el desequilibrio entre la oferta y la demanda en el primer nivel del libro. Esta lección calcula el order flow imbalance de Cont, Kukanov y Stoikov evento a evento, recupera la pendiente plantada en dos acciones sintéticas y su relación con la profundidad, y mide con seis perfiles de conexión, de una casa en España a una FPGA, cuánto de esa información llega a tiempo.
- Módulo
- 4.3 · Microestructura y ejecución
- Duración
- 65 min
Lecciones del nivel 4
4.1 · Teoría y optimización de carteras
- 4.1.1 Markowitz: frontera eficiente y restricciones reales
- 4.1.2 Estimación de covarianzas: shrinkage, modelos factoriales y denoising
- 4.1.3 Black-Litterman: del equilibrio del mercado a las visiones
- 4.1.4 Risk parity, mínima varianza, HRP y HERC
- 4.1.5 Optimización con CVaR, drawdown, rotación y costes
- 4.1.6 Modelos factoriales: CAPM, Fama-French, PCA y atribución
- 4.1.7 Rebalanceo y carteras de estrategias
- 4.1.8 Herramientas de carteras: Riskfolio-Lib, PyPortfolioOpt, skfolio y bt
4.2 · Gestión del riesgo y tamaño de posición
- 4.2.1 Riesgo por operación, stops y riesgo de ruina
- 4.2.2 Criterio de Kelly y volatility targeting: cuánto arriesgar
- 4.2.3 VaR, Expected Shortfall y backtesting del VaR
- 4.2.4 Stress testing y riesgos de correlación, liquidez, gap y contraparte
- 4.2.5 Control del drawdown y kill switch
- 4.2.6 Riesgo de derivados: griegas de cartera, margen y liquidaciones
4.3 · Microestructura y ejecución
4.4 · Brokers y APIs
- 4.4.1 Elegir bróker desde España o la UE: regulación, activos, costes y API
- 4.4.2 Interactive Brokers: TWS e IB Gateway, ib_async, contratos, datos y órdenes
- 4.4.3 Alpaca, Tradier, MetaTrader 5 con Python y OANDA v20
- 4.4.4 APIs de exchanges cripto: ccxt, Binance, Bybit, Kraken y Coinbase
- 4.4.5 Credenciales seguras: variables de entorno, gestores de secretos y permisos mínimos
4.5 · Sistema de trading en vivo
- 4.5.1 Arquitectura de un sistema en vivo: de los datos a la reconciliación
- 4.5.2 Gestión de órdenes: estados, fills parciales, cancelaciones y reconexiones
- 4.5.3 Paper trading, shadow trading y seguimiento del slippage
- 4.5.4 Programación de tareas y alertas: cron, APScheduler, Prefect, Telegram, Slack y email
- 4.5.5 Tolerancia a fallos: idempotencia, estado persistente y reconciliación al arrancar
- 4.5.6 NautilusTrader o LEAN en real: del backtest a producción con el mismo código
Qué mueve el precio medio en segundos
La lección 1.1.3 enseñó a leer el libro de órdenes, y la 4.3.1 midió cuánto mueve el precio una orden grande partida en muchas hijas a lo largo de horas. Esta lección baja a la escala de segundos y responde a una pregunta más básica: qué hace que el precio medio suba o baje un tick. En un libro con horquilla de un tick, el medio solo se mueve cuando una de las dos colas del primer nivel se agota (o cuando alguien mejora el precio dentro de la horquilla). Y una cola se agota por tres tipos de evento:
Fuente Mermaid
flowchart TB
L[Limitada nueva] --> C{¿Qué lado?}
M[Orden a mercado] --> C
X[Cancelación] --> C
C -- crece el bid --> U[Presión al alza]
C -- encoge el ask --> U
C -- crece el ask --> D[Presión a la baja]
C -- encoge el bid --> DUna compra a mercado y una cancelación de una venta limitada hacen lo mismo al libro: encogen la cola del ask. Una limitada de compra nueva al mejor precio la contrarresta. Cont, Kukanov y Stoikov (CKS) convirtieron esa observación en una sola variable, el order flow imbalance (OFI), y mostraron con datos TAQ de 50 acciones de la NYSE en abril de 2010 que, en intervalos de 10 segundos, el cambio del medio es lineal en el OFI, con una pendiente inversamente proporcional a la profundidad y un R² medio del 65 %1Nota 1 · R. Cont, A. Kukanov y S. Stoikov, «The Price Impact of Order Book Events», Journal of Financial Econometrics 12(1), 2014, pp. 47-88, doi:10.1093/jjfinec/nbt003; versión en arXiv:1011.6402 (definición del OFI, intervalos de 10 s, R² medio del 65 % para el OFI frente al 32 % del desequilibrio de operaciones, β inversamente proporcional a la profundidad), consultada el 22/10/2026..
Los datos de esta lección son el mejor bid y ask de dos acciones ficticias, ARVA y BELO, el 16 de septiembre de
2025 de 10:00 a 11:30 de Nueva York, una fila por cada evento del primer nivel. Salen del modelo estilizado de CKS:
órdenes limitadas, cancelaciones y órdenes a mercado que llegan como procesos de Poisson a un libro de un nivel con
horquilla de un tick. Son sintéticos, y la pendiente verdadera está publicada en flujo4_verdad.csv: la estimación se
puede comprobar contra ella.
import numpy as np
import pandas as pd
cot = pd.read_parquet("flujo4_cotizaciones.parquet")
print(cot.groupby("ticker").size().to_dict())
cot[cot["ticker"] == "BELO"].head(4){'ARVA': 55599, 'BELO': 66195}
ts ticker bid ask tam_bid tam_ask
55599 2025-09-16 14:00:00+00:00 BELO 61.99 62.0 600 600
55600 2025-09-16 14:00:00.002613+00:00 BELO 61.99 62.0 700 600
55601 2025-09-16 14:00:00.012461+00:00 BELO 61.99 62.0 600 600
55602 2025-09-16 14:00:00.112666+00:00 BELO 61.99 62.0 600 900El OFI de Cont, Kukanov y Stoikov
Para cada evento n del libro (un cambio de precio o de tamaño en el mejor bid o el mejor ask), CKS definen su contribución al desequilibrio comparando el estado nuevo con el anterior:
con y los precios del mejor bid y ask y , sus tamaños. Si los precios no cambian, las dos indicadoras de cada lado valen 1 y queda : lo que crece la demanda menos lo que crece la oferta. Si el bid sube un tick (la cola de venta se agotó y alguien mejora la compra), cuenta entera la cola nueva del bid; si baja, cuenta en negativo entera la cola que desapareció. El OFI de un intervalo es la suma de las de los eventos que caen en él.
def ofi_eventos(g):
"""Contribución de cada evento del primer nivel (CKS 2014)."""
b, a = g["bid"], g["ask"]
qb, qa = g["tam_bid"], g["tam_ask"]
b0, a0 = b.shift(), a.shift()
qb0, qa0 = qb.shift(), qa.shift()
e = ((b >= b0) * qb - (b <= b0) * qb0
- (a <= a0) * qa + (a >= a0) * qa0)
return e.fillna(0).astype("int64")
cot["e"] = cot.groupby("ticker", group_keys=False).apply(ofi_eventos)
cot["medio"] = (cot["bid"] + cot["ask"]) / 2
cot["prof"] = (cot["tam_bid"] + cot["tam_ask"]) / 2
cot.loc[cot["ticker"] == "BELO",
["bid", "ask", "tam_bid", "tam_ask", "e"]].head(6) bid ask tam_bid tam_ask e
55599 61.99 62.0 600 600 0
55600 61.99 62.0 700 600 100
55601 61.99 62.0 600 600 -100
55602 61.99 62.0 600 900 -300
55603 61.99 62.0 600 800 100
55604 61.99 62.0 500 800 -100La primera fila de cada acción no tiene evento anterior y vale cero. En las siguientes se lee la regla: el bid de BELO
pasa de 600 a 700 acciones (+100), vuelve a 600 (−100), el ask crece de 600 a 900 (−300: más oferta) y luego encoge a
800 (+100). El cálculo usa solo el evento actual y el anterior (shift), así que no mira el futuro. La agrupación por
ticker importa: sin ella, el primer evento de BELO se compararía con el último de ARVA.
La regresión: cambio del medio frente a OFI
CKS agregan en intervalos de 10 segundos y ajustan , con el cambio del precio medio entre el final del intervalo anterior y el final de este. El generador de los datos hizo lo mismo (regresión sin constante) con una simulación ocho veces más larga del mismo modelo: es el β de referencia, el valor poblacional que la sesión de 90 minutos debe recuperar. La aproximación teórica de CKS para un libro de un nivel es , con la profundidad media del primer nivel: hace falta consumir del orden de una cola entera para mover el medio medio tick.
def intervalos(g, freq="10s"):
k = g.set_index("ts")
ofi = k["e"].resample(freq).sum()
medio = k["medio"].resample(freq).last().ffill()
dp = medio.diff()
dp.iloc[0] = medio.iloc[0] - k["medio"].iloc[0]
prof = k["prof"].resample(freq).mean()
return pd.DataFrame({"ofi": ofi, "dp": dp, "prof": prof})
def ajuste(x, y):
"""Regresión sin constante: pendiente y R² (sin centrar)."""
beta = (x @ y) / (x @ x)
r2 = 1 - ((y - beta * x) ** 2).sum() / (y ** 2).sum()
return beta, r2
iv = {tk: intervalos(g) for tk, g in cot.groupby("ticker")}
ver = pd.read_csv("flujo4_verdad.csv").set_index("ticker")
filas = []
for tk, d in iv.items():
beta, r2 = ajuste(d["ofi"].to_numpy(), d["dp"].to_numpy())
ad = cot.loc[cot["ticker"] == tk, "prof"].mean()
filas.append({"ticker": tk, "intervalos": len(d),
"beta_e6": beta * 1e6, "r2": r2,
"cks_e6": 0.01 / (2 * ad) * 1e6,
"ref_e6": ver.loc[tk, "beta_referencia"] * 1e6,
"r2_ref": ver.loc[tk, "r2_referencia"]})
res = pd.DataFrame(filas).set_index("ticker")
res.round(2) intervalos beta_e6 r2 cks_e6 ref_e6 r2_ref
ticker
ARVA 540 2.38 0.67 2.72 2.44 0.68
BELO 540 4.26 0.68 4.40 4.36 0.67Con 540 intervalos por acción, la sesión recupera la pendiente con un error de un 2-3 %: 2,38 frente a 2,44 en ARVA y 4,26 frente a 4,36 en BELO (en millonésimas de dólar por acción de OFI). El R² es el del artículo: dos tercios de la varianza del cambio del medio se explican con una sola variable y sin constante. La aproximación teórica, 2,72 y 4,40, acierta el orden de magnitud y la proporción entre las dos acciones, aunque no la cifra exacta: es una aproximación de un modelo estilizado, y en el artículo la constante y el exponente de la relación entre β y la profundidad se estiman con los datos, no se fijan.
La figura de la cabecera dibuja los intervalos de BELO con la recta ajustada. La lectura en unidades de trading: con , un OFI de +2.350 acciones sube el medio un céntimo, un tick. En ARVA, con una cola media de 1.836 acciones por lado frente a las 1.136 de BELO, hacen falta unas 4.200.
La pendiente depende de la profundidad
La relación es lo que da contenido económico a la regresión: en un libro profundo, el mismo desequilibrio mueve menos el precio. CKS la comprueban estimando β en cada media hora y regresándola sobre la profundidad de esa media hora. En una sesión de 90 minutos hay pocas medias horas, así que la celda usa ventanas de 10 minutos (nueve por acción, 60 intervalos de 10 s cada una):
ventanas = []
for tk, d in iv.items():
for inicio, w in d.groupby(pd.Grouper(freq="10min")):
b, _ = ajuste(w["ofi"].to_numpy(), w["dp"].to_numpy())
ventanas.append({"ticker": tk, "inicio": inicio,
"beta_e6": b * 1e6, "prof": w["prof"].mean()})
vent = pd.DataFrame(ventanas)
x = 1 / vent["prof"].to_numpy()
y = vent["beta_e6"].to_numpy() * 1e-6
c = (x @ y) / (x @ x) # β = c / profundidad
print(f"c = β·profundidad = {c:.5f} USD (medio tick: 0.005)")
print(f"correlación β - 1/prof: {np.corrcoef(x, y)[0, 1]:.2f}")
vent.groupby("ticker")[["beta_e6", "prof"]].agg(["min", "max"]).round(2)c = β·profundidad = 0.00455 USD (medio tick: 0.005)
correlación β - 1/prof: 0.95
beta_e6 prof
min max min max
ticker
ARVA 2.04 2.83 1699.03 1857.01
BELO 3.52 4.81 1054.51 1143.34La constante sale 0,0046 USD, a un 9 % del medio tick que predice la aproximación de CKS. Casi toda la correlación entre β y 1/profundidad viene de la diferencia entre las dos acciones (BELO tiene unas tres quintas partes de la profundidad de ARVA y casi el doble de pendiente); dentro de cada acción, la profundidad apenas cambia de una ventana a otra (de 1.699 a 1.857 acciones en ARVA) y la β estimada en 60 intervalos varía más por ruido que por profundidad:
Para un sistema, la consecuencia práctica es que el impacto por acción no es una constante del activo: sube cuando el libro se adelgaza (aperturas, noticias, días de poco volumen) y una ejecución que use un β medio infravalora su coste justo cuando más importa. Es el mismo argumento que la lección 4.3.1 hace con la ley de la raíz cuadrada a escala de días.
Contemporáneo no es predictivo
Un R² de 0,67 invita a pensar en una señal. No lo es en la forma en que se ha calculado: el OFI del intervalo k explica el cambio del medio del mismo intervalo. La celda compara cuatro cosas: el OFI frente al desequilibrio de operaciones (compras menos ventas agresoras, la variable que mira quien solo tiene la cinta de operaciones), distintos tamaños de intervalo y el OFI de un intervalo frente al cambio del siguiente.
ops = pd.read_parquet("flujo4_operaciones.parquet")
signo = np.where(ops["lado_agresor"] == "compra", 1, -1)
ops["firmado"] = signo * ops["tamano"]
filas = []
for tk, g in cot.groupby("ticker"):
d = iv[tk]
ti = (ops[ops["ticker"] == tk].set_index("ts")["firmado"]
.resample("10s").sum().reindex(d.index, fill_value=0))
sig = d["ofi"].shift(1).fillna(0)
dp = d["dp"].to_numpy()
fila = {"ticker": tk,
"r2_ofi": ajuste(d["ofi"].to_numpy(), dp)[1],
"r2_trades": ajuste(ti.to_numpy(), dp)[1],
"r2_siguiente": ajuste(sig.to_numpy(), dp)[1]}
for f in ["1s", "60s"]:
d2 = intervalos(g, f)
fila["r2_" + f] = ajuste(d2["ofi"].to_numpy(),
d2["dp"].to_numpy())[1]
filas.append(fila)
pd.DataFrame(filas).set_index("ticker").round(3) r2_ofi r2_trades r2_siguiente r2_1s r2_60s
ticker
ARVA 0.670 0.373 0.003 0.329 0.818
BELO 0.676 0.418 0.001 0.296 0.769Tres resultados:
- El OFI explica más que las operaciones: 0,67 frente a 0,37 en ARVA y 0,68 frente a 0,42 en BELO. Las operaciones solo ven una parte de lo que mueve el libro; las cancelaciones y las limitadas nuevas no dejan rastro en la cinta. CKS encuentran lo mismo en datos reales: 65 % frente a 32 %.
- La escala importa: a 1 segundo el R² baja a 0,33 y 0,30 (el medio cambia en saltos de medio tick y muchos intervalos no tienen cambio aunque el OFI no sea cero); a 60 segundos sube a 0,82 y 0,77. Más agregación, menos ruido de la discretización.
- El OFI de un intervalo no dice nada del siguiente: 0,003 y 0,001. En el modelo, como en un mercado eficiente a esta escala, el desequilibrio ya se ha convertido en precio cuando se termina de medir.
Fuente Mermaid
flowchart LR E[Eventos del libro] --> O[OFI del intervalo] O --> P[Cambio del medio] O -. casi nada .-> S[Intervalo siguiente]
Lo que sí anticipa el siguiente movimiento a esta escala es el estado del libro, no el flujo: la lección 1.1.3 mostró que el desequilibrio entre las dos colas, , y el microprecio indican hacia dónde es más probable que se mueva el medio, porque la cola más delgada es la que antes se agota. Gould y Bonart lo documentan con datos de Nasdaq como predictor del siguiente tick2Nota 2 · M. D. Gould y J. Bonart, «Queue Imbalance as a One-Tick-Ahead Price Predictor in a Limit Order Book», arXiv:1512.03492, 2015, consultada el 22/10/2026.. Si esa información sirve o no depende de cuánto tarda en llegar la orden: es la pregunta del resto de la lección.
Latencia: de una casa en España a una FPGA
La latencia de ida y vuelta (RTT) es el tiempo entre enviar un mensaje y recibir la respuesta. Para una orden, lo que
cuenta es algo parecido: el dato sale del mercado, llega al sistema, el sistema decide y la orden vuelve al mercado. El
fichero latencia4_rtt.parquet tiene un ping cada 2 segundos durante 100 minutos para seis perfiles, con una mediana,
una dispersión, picos (congestión, Wi-Fi, recolección de basura) y pérdidas plantados:
lat = pd.read_parquet("latencia4_rtt.parquet")
orden = list(dict.fromkeys(lat["perfil"]))
resumen = (lat.groupby("perfil", sort=False)["rtt_us"]
.describe(percentiles=[0.5, 0.99, 0.999])
[["50%", "99%", "99.9%", "max"]])
resumen["perdidos"] = lat.groupby("perfil", sort=False)["perdido"].sum()
resumen.round(1) 50% 99% 99.9% max perdidos
perfil
retail_espana_nueva_york 88497.8 126363.4 506389.5 729917.2 11
retail_espana_frankfurt 31077.6 45943.1 184613.9 249154.2 4
vps_nueva_jersey 1206.5 2440.7 9636.6 38463.5 0
nube_misma_region 652.3 1237.4 4984.1 16316.9 0
colocado 27.8 44.3 53.9 69.1 0
fpga 1.6 1.8 1.9 4.5 0Cinco órdenes de magnitud separan al primer perfil del último. La figura usa escala logarítmica: la mediana es el cuadrado y la raya llega al percentil 99,9. Tres cosas que no se ven en una media:
- La cola es larga en todos los perfiles con red de por medio. Desde casa, el p99,9 de Nueva York es más de cinco veces la mediana y el máximo, 730 ms. Un sistema con un tiempo de espera de 200 ms vería varias veces por sesión una orden «sin respuesta» que en realidad llegó: es el origen de las órdenes duplicadas que tratan las lecciones 4.5.2 y 4.5.5.
- Hay pérdidas: 11 de 3.000 pings desde casa a Nueva York (0,4 %). Un protocolo como TCP las reintenta y se convierten en picos; uno que no reintenta, en mensajes perdidos.
- La distancia es un suelo. La celda siguiente lo calcula.
c_vacio = 299_792.458 # km/s
n_fibra = 1.47 # índice de refracción típico del vidrio
v = c_vacio / n_fibra
for tramo, km in [("Madrid - Nueva York", 5800),
("Madrid - Fráncfort", 1450),
("Nueva Jersey (mismo centro)", 50)]:
print(f"{tramo:<28s} {2 * km / v * 1000:7.2f} ms ida y vuelta")Madrid - Nueva York 56.88 ms ida y vuelta
Madrid - Fráncfort 14.22 ms ida y vuelta
Nueva Jersey (mismo centro) 0.49 ms ida y vueltaDesde casa, 57 de los 88 ms de mediana a Nueva York son luz en el vidrio; el resto son equipos, rutas y colas. Contratar mejor fibra puede rascar unos milisegundos; quitar los 57 exige mover la máquina. Por eso los perfiles siguientes no son «más rápidos» sino «más cerca»: un VPS en Nueva Jersey (1,2 ms), una máquina en la misma región de nube que la pasarela del bróker (0,65 ms), un servidor en el centro de datos del mercado (28 µs) y lógica en una FPGA junto al mercado (1,6 µs), donde ya no hay sistema operativo de por medio.
Lo que cuesta la latencia
La latencia cuesta de dos maneras. La primera: el precio se mueve mientras la orden viaja, y la orden se ejecuta a un precio distinto del que se vio. La segunda: el resto del libro (tamaños, posición en la cola) ha cambiado, y una estrategia que dependa de esa información decide sobre un libro que ya no existe. La celda mide las dos con los eventos de las cotizaciones: para 20.000 instantes al azar de la sesión, ¿ha cambiado el medio, y ha cambiado algo del primer nivel, en los siguientes RTT microsegundos?
rng = np.random.default_rng(432)
medianas = resumen["50%"]
filas = []
for tk, g in cot.groupby("ticker"):
t = g["ts"].astype("int64").to_numpy() # µs
m = g["medio"].to_numpy()
t_medio = t[1:][m[1:] != m[:-1]] # cambios del medio
u = np.sort(rng.integers(t[0], t[-1] - 1_000_000, 20_000))
for perfil, rtt in medianas.items():
fin = u + int(rtt)
cambia_m = (np.searchsorted(t_medio, fin, "right")
> np.searchsorted(t_medio, u, "right"))
cambia_l = (np.searchsorted(t, fin, "right")
> np.searchsorted(t, u, "right"))
filas.append({"ticker": tk, "perfil": perfil,
"medio_cambia": cambia_m.mean(),
"libro_cambia": cambia_l.mean()})
viejo = pd.DataFrame(filas).pivot(index="perfil", columns="ticker")
viejo.loc[orden].round(4) medio_cambia libro_cambia
ticker ARVA BELO ARVA BELO
perfil
retail_espana_nueva_york 0.0134 0.0250 0.5941 0.6526
retail_espana_frankfurt 0.0046 0.0097 0.2750 0.3138
vps_nueva_jersey 0.0003 0.0004 0.0131 0.0138
nube_misma_region 0.0001 0.0004 0.0072 0.0070
colocado 0.0000 0.0000 0.0004 0.0003
fpga 0.0000 0.0000 0.0000 0.0000En ARVA, con la mediana de casa a Nueva York, el precio medio ha cambiado en el 1,3 % de los casos cuando llega la orden, y en BELO, que cambia de medio el doble de veces por minuto, en el 2,5 %. Pero el primer nivel (precio o tamaño) ha cambiado en el 59 % y el 65 %: casi siempre hay otra cola. Desde un VPS en Nueva Jersey las dos cifras caen por debajo del 0,05 % y del 1,4 %; desde un servidor colocado, a cero en la práctica.
Traducido a coste: si el medio cambia en el 1,3 % de los envíos y cuando cambia lo hace medio tick en cualquier sentido, el desplazamiento esperado del precio es del orden de 0,013 × 0,005 ≈ 0,00007 USD por acción, menos de una centésima de punto básico en una acción de 180 USD, frente a la media horquilla de 0,27 puntos básicos que paga cualquier orden a mercado. Para una estrategia de minutos o de días, la latencia de un particular no es su coste; lo son la horquilla, el impacto (lección 4.3.1) y las comisiones (lección 2.4.7).
El cálculo cambia cuando hay competencia por la misma información. El desequilibrio de colas de la lección 1.1.3 es público: lo ve cualquiera con el primer nivel del libro. En el modelo que genera estos datos nadie actúa sobre él, así que el siguiente movimiento del medio tarda segundos en llegar y una orden lenta lo alcanza casi siempre. En un mercado real, cuando una cola se queda en pocas acciones, quien llega antes se lleva lo que queda al precio viejo y el resto paga el tick siguiente; la carrera la gana siempre el más rápido (entre la mediana colocada, 28 µs, y la de casa, 88.500 µs, hay un factor de más de 3.000). Las estrategias cuya ventaja es llegar antes (arbitraje entre centros, reaccionar al desequilibrio, creación de mercado con colas de prioridad) no están al alcance de una conexión doméstica, ni siquiera de un VPS.
Fuente Mermaid
flowchart TB M[Dato del mercado] --> R[Red de vuelta] R --> D[Decisión] D --> I[Red de ida] I --> B[Bróker y riesgo] B --> X[Mercado]
La cadena tiene más eslabones que la red: la decisión (el código, el intérprete, la recolección de basura) y los controles de riesgo del bróker antes de enviar la orden al mercado. En un sistema de baja frecuencia, la parte que hay que vigilar es la cola de esa cadena, no la mediana: un pico de 700 ms el día de una noticia, un reinicio de la pasarela, un paquete perdido. Eso se resuelve con diseño (tiempos de espera, identificadores de cliente, reconciliación; módulo 4.5), no con velocidad.
Errores típicos
- Usar el OFI del intervalo para operar en ese intervalo. Es sesgo de anticipación: el OFI se termina de conocer cuando el precio ya se ha movido.
- Agrupar mal. El
shiftsin agrupar por instrumento mezcla el último evento de un activo con el primero del siguiente; con varios días, también hay que cortar por sesión. - Medir la latencia con la media. La media esconde la cola, que es la que provoca los tiempos de espera agotados y las órdenes duplicadas. Hay que mirar el p99 y el p99,9 y contar las pérdidas.
- Confundir latencia de red con latencia del sistema. El ping mide la red; la decisión en Python, la serialización y los controles del bróker también cuentan, y se miden con marcas de tiempo propias en cada eslabón.
- Comprar velocidad para una estrategia que no la necesita. En un horizonte de minutos, pasar de 88 ms a 1 ms no cambia el resultado; un servidor más cerca sí puede tener sentido por fiabilidad (menos saltos, menos pérdidas).
Ejercicios
- Repite la regresión del OFI con intervalos de 30 segundos y de 5 minutos. ¿Cómo cambian β y R²? ¿Por qué con 5 minutos hay que desconfiar de la cifra aunque el R² sea alto?
Solución
filas = []
for tk, g in cot.groupby("ticker"):
for f in ["30s", "5min"]:
d = intervalos(g, f)
b, r2 = ajuste(d["ofi"].to_numpy(), d["dp"].to_numpy())
filas.append({"ticker": tk, "intervalo": f, "n": len(d),
"beta_e6": b * 1e6, "r2": r2})
pd.DataFrame(filas).round(2) ticker intervalo n beta_e6 r2
0 ARVA 30s 180 2.57 0.76
1 ARVA 5min 18 3.35 0.89
2 BELO 30s 180 4.21 0.74
3 BELO 5min 18 4.44 0.74Con 30 segundos el R² sube a 0,76 y 0,74 y β cambia poco (2,57 en ARVA, 4,21 en BELO, frente a 2,44 y 4,36 de referencia a 10 s): la relación lineal se mantiene al agregar, que es lo que CKS llaman estabilidad entre escalas. Con 5 minutos solo hay 18 intervalos por acción: ARVA da β = 3,35 con R² = 0,89, una pendiente un 37 % mayor que la de referencia con el mejor ajuste de la tabla. Con 18 puntos el error de estimación es grande y el R² alto refleja más la agregación que una relación más fuerte (con pocos puntos, unos pocos intervalos grandes dominan la regresión). CKS eligen 10 segundos como compromiso: muchos intervalos y suficientes eventos en cada uno.
- Separa el OFI en su parte de compra (la de los términos del bid) y su parte de venta (la de los términos del ask) y ajusta en BELO. ¿Son parecidas las dos pendientes? ¿Qué esperarías en el modelo?
Solución
g = cot[cot["ticker"] == "BELO"].copy()
b, a = g["bid"], g["ask"]
qb, qa = g["tam_bid"], g["tam_ask"]
g["e_b"] = ((b >= b.shift()) * qb - (b <= b.shift()) * qb.shift())
g["e_a"] = ((a >= a.shift()) * qa.shift() - (a <= a.shift()) * qa)
k = g.fillna(0).set_index("ts")
X = np.column_stack([k["e_b"].resample("10s").sum(),
k["e_a"].resample("10s").sum()])
y = iv["BELO"]["dp"].to_numpy()
coef, *_ = np.linalg.lstsq(X, y, rcond=None)
print("beta_b, beta_a (x1e-6):", (coef * 1e6).round(2))beta_b, beta_a (x1e-6): [4.28 4.24]Las dos pendientes salen casi iguales (4,28 y 4,24, frente a 4,26 de la conjunta), como corresponde a un modelo simétrico: las órdenes de compra y de venta llegan con las mismas tasas por lado. En datos reales pueden diferir (por ejemplo, en días de caídas fuertes) y la versión separada es una forma sencilla de comprobarlo.
- Diseña el tiempo de espera de un sistema que envía órdenes desde casa a una pasarela en Nueva York: si lo fijas en el p99,9 de la latencia, ¿cuántas respuestas por cada 10.000 envíos llegarían tarde (o no llegarían) en los datos? ¿Y si lo fijas en el doble de la mediana?
Solución
Con los datos de la primera celda de latencia: el p99,9 del perfil retail_espana_nueva_york está en unos 506 ms, así
que, por construcción, una de cada mil respuestas llega más tarde, a la que hay que sumar las pérdidas (11 de 3.000,
unas 37 por cada 10.000). Con el doble de la mediana (unos 177 ms) se pasarían del tiempo bastantes más: todos los picos
plantados (un 1 % de probabilidad, de 150 ms de media) más la cola de la parte lognormal. En los dos casos, una
respuesta tardía no significa que la orden no llegara: el sistema tiene que consultar por su identificador de cliente
antes de reenviar (lecciones 3.3.3 y 4.5.5). El tiempo de espera decide cuándo preguntar, no cuándo reenviar.
Footnotes
-
R. Cont, A. Kukanov y S. Stoikov, «The Price Impact of Order Book Events», Journal of Financial Econometrics 12(1), 2014, pp. 47-88, doi:10.1093/jjfinec/nbt003; versión en arXiv:1011.6402 (definición del OFI, intervalos de 10 s, R² medio del 65 % para el OFI frente al 32 % del desequilibrio de operaciones, β inversamente proporcional a la profundidad), consultada el 22/10/2026. ↩
-
M. D. Gould y J. Bonart, «Queue Imbalance as a One-Tick-Ahead Price Predictor in a Limit Order Book», arXiv:1512.03492, 2015, consultada el 22/10/2026. ↩
Respuesta plegada en cada pregunta
Autoevaluación
En un intervalo de 10 segundos, el tamaño del mejor bid de una acción pasa de 600 a 900 acciones sin que cambien los precios, y el del mejor ask, de 500 a 300. ¿Cuánto vale el OFI del intervalo si no hay más eventos?
- A+500: +300 por el bid que crece y +200 por el ask que encoge
- B+100: la diferencia entre los dos cambios
- C−500: el ask manda
- D0: los precios no cambian
Ver respuesta
Respuesta A. +500: +300 por el bid que crece y +200 por el ask que encoge
Con precios iguales, cada evento suma el cambio del tamaño del bid y resta el cambio del tamaño del ask: (900 − 600) − (300 − 500) = +500. Más demanda o menos oferta en el primer nivel empujan el medio hacia arriba.
En la sesión de BELO la regresión sin constante del cambio del medio a 10 s sobre el OFI da β ≈ 4,26 millonésimas de USD por acción, con una profundidad media de unas 1.136 acciones por lado. ¿Qué dice la aproximación de Cont, Kukanov y Stoikov?
- Aβ ≈ tick/(2·profundidad) = 0,01/(2·1.136) ≈ 4,4 millonésimas: el valor medido está cerca
- Bβ ≈ tick·profundidad: debería ser 11,4
- Cβ no depende de la profundidad
- Dβ ≈ 1/profundidad: unas 880 millonésimas
Ver respuesta
Respuesta A. β ≈ tick/(2·profundidad) = 0,01/(2·1.136) ≈ 4,4 millonésimas: el valor medido está cerca
En el modelo estilizado de un nivel, hace falta agotar una cola (≈ la profundidad) para mover el medio medio tick: β ≈ (tick/2)/profundidad. La sesión da 4,26 frente a 4,40 de la aproximación y 4,36 de la referencia larga del generador.
El OFI explica el 67 % de la varianza del cambio del medio en el mismo intervalo de 10 s. ¿Qué R² tiene el OFI de un intervalo para el cambio del intervalo siguiente en los datos?
- ATambién alrededor del 67 %
- BAlrededor del 30 %
- CPrácticamente cero (0,3 % en ARVA y 0,1 % en BELO)
- DNegativo
Ver respuesta
Respuesta C. Prácticamente cero (0,3 % en ARVA y 0,1 % en BELO)
La relación es contemporánea: el OFI es la forma en que el precio cambia, no un aviso de cómo cambiará. Para usarlo hay que conocerlo antes de que termine de mover el precio, y eso exige estar dentro del intervalo, es decir, latencia.
Una conexión de fibra doméstica en España hasta una pasarela en Nueva York tiene una mediana de ida y vuelta de unos 88 ms. ¿Qué parte de ese tiempo es física y no se puede comprar?
- ACasi nada: es todo congestión
- BUnos 57 ms: 5.800 km de fibra, ida y vuelta, a unos 204.000 km/s
- CExactamente 88 ms
- DUnos 6 ms
Ver respuesta
Respuesta B. Unos 57 ms: 5.800 km de fibra, ida y vuelta, a unos 204.000 km/s
La luz en el vidrio viaja a unas dos terceras partes de su velocidad en el vacío. 2 × 5.800 km / 204.000 km/s ≈ 57 ms. Lo que queda (equipos, rutas, colas) se puede reducir acercándose, pero la distancia solo se elimina moviéndose: VPS, nube en la región o servidor colocado.
Con la mediana de 88 ms, ¿con qué probabilidad ha cambiado el precio medio de ARVA entre que se ve una cotización y llega una orden, y con cuál ha cambiado algo del primer nivel (precio o tamaño)?
- ALas dos alrededor del 50 %
- BUn 1,3 % el medio y un 59 % el primer nivel: el precio casi siempre sigue ahí, pero el tamaño de las colas ya es otro
- CUn 59 % el medio y un 1,3 % el primer nivel
- DCero las dos
Ver respuesta
Respuesta B. Un 1,3 % el medio y un 59 % el primer nivel: el precio casi siempre sigue ahí, pero el tamaño de las colas ya es otro
ARVA cambia de medio unas nueve veces por minuto, pero su primer nivel se mueve unas diez veces por segundo. Para una orden a mercado en un horizonte de minutos, 88 ms casi no cuestan; para una estrategia que depende de la posición en la cola o del tamaño visible, el libro que se vio ya no existe.
Ideas clave
- El OFI de Cont, Kukanov y Stoikov suma, evento a evento, lo que crece la demanda y lo que se retira la oferta en el primer nivel (también cuando cambia el precio), y trata igual una venta a mercado que una cancelación de compra del mismo tamaño.
- En intervalos de 10 s el cambio del medio es lineal en el OFI: en los datos, β = 2,38 (ARVA) y 4,26 millonésimas de USD por acción (BELO), con R² de 0,67 y 0,68, frente a 2,44 y 4,36 de la verdad plantada.
- La pendiente es inversamente proporcional a la profundidad: β ≈ tick/(2·profundidad). En ventanas de 10 minutos, β·profundidad sale 0,0046 USD frente al medio tick de 0,005.
- El OFI es contemporáneo: explica el 67 % del cambio del mismo intervalo y casi nada del siguiente. Un desequilibrio de operaciones (solo trades) explica menos (R² 0,37 y 0,42), como en el artículo original (65 % frente a 32 %).
- La latencia de ida y vuelta va de 1,6 µs (FPGA) a 88 ms (casa en España con pasarela en Nueva York), con colas que llegan a 730 ms y paquetes perdidos. La física pone el suelo: unos 57 ms solo por la distancia Madrid-Nueva York.
- Para una orden a mercado en un horizonte de minutos, la latencia de un particular casi no mueve el precio medio; lo que cambia es el resto del libro y, sobre todo, las colas largas de la distribución, que son las que obligan a diseñar tiempos de espera y reintentos.