---
title: "4.3.2 Order flow imbalance y latencia"
description: "El OFI de Cont, Kukanov y Stoikov en un libro sintético: β = tick/(2·profundidad), R², escalas de tiempo y lo que cuesta la latencia."
url: "https://pythonparatrading.com/curso-python-trading/nivel-4/order-flow-imbalance-y-latencia/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-4/order-flow-imbalance-y-latencia.png"
---

# 4.3.2 · 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.

- Curso: [Nivel 4: Carteras, riesgo, ejecución y trading en real](https://pythonparatrading.com/curso-python-trading/nivel-4/)
- Módulo: 4.3 · Microestructura y ejecución
- Duración: 65 min
- Requisitos: [1.1.3 · Libro de órdenes: bid, ask, spread y profundidad](https://pythonparatrading.com/curso-python-trading/nivel-1/libro-de-ordenes-bid-ask-y-profundidad/), [1.1.4 · Tipos de órdenes](https://pythonparatrading.com/curso-python-trading/nivel-1/tipos-de-ordenes/), [1.1.5 · Costes, liquidación, apalancamiento y margen](https://pythonparatrading.com/curso-python-trading/nivel-1/costes-liquidacion-apalancamiento-y-margen/), [2.4.7 · Modelado de costes: comisiones, horquilla, impacto, financiación y préstamo de títulos](https://pythonparatrading.com/curso-python-trading/nivel-2/modelado-de-costes/)

## Al terminar esta lección

- Calcular el order flow imbalance (OFI) de cada evento del primer nivel del libro y agregarlo por intervalos sin mirar el futuro.
- Estimar la pendiente β de la regresión del cambio del precio medio sobre el OFI, compararla con la verdad plantada y con la aproximación tick/(2·profundidad) de Cont, Kukanov y Stoikov.
- Distinguir una relación contemporánea (el OFI explica el cambio del mismo intervalo) de una señal que anticipa el siguiente, y medir el poder explicativo según la escala de tiempo.
- Describir las distribuciones de latencia de ida y vuelta de un particular, un VPS, la nube, un servidor colocado y una FPGA, con su mediana, su cola y sus pérdidas.
- Cuantificar qué cuesta la latencia: con qué probabilidad el libro visto ya no existe cuando llega la orden y cuánto se mueve el precio medio durante el viaje.

## 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:

```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 --> D
```

Una 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 %[^1].

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.

```python
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)
```
```output
{'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      900
```

## El 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:

$$
e_n = \mathbb{1}[b_n \ge b_{n-1}]\,q^b_n - \mathbb{1}[b_n \le b_{n-1}]\,q^b_{n-1}
    - \mathbb{1}[a_n \le a_{n-1}]\,q^a_n + \mathbb{1}[a_n \ge a_{n-1}]\,q^a_{n-1}
$$

con $b$ y $a$ los precios del mejor bid y ask y $q^b$, $q^a$ sus tamaños. Si los precios no cambian, las dos
indicadoras de cada lado valen 1 y queda $e_n = \Delta q^b_n - \Delta q^a_n$: 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 $e_n$ de los
eventos que caen en él.

```python
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)
```
```output
         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 -100
```

La 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 $\Delta P_k = \beta\,\mathrm{OFI}_k + \varepsilon_k$, con
$\Delta P_k$ 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 $\beta \approx \text{tick}/(2\cdot AD)$, con $AD$ la profundidad media del primer nivel: hace falta
consumir del orden de una cola entera para mover el medio medio tick.

```python
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)
```
```output
        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.67
```

Con 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
$\beta = 4{,}26\cdot 10^{-6}$, 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.

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

R² sin centrar. Una regresión sin constante se evalúa con $1 - \sum \hat\varepsilon^2 / \sum \Delta P^2$, sin restar
la media de $\Delta P$. Aquí la media del cambio es casi cero y las dos versiones coinciden en dos decimales; en una
serie con tendencia no coincidirían y el R² sin centrar saldría inflado. Si se compara con la tabla de un artículo,
hay que comprobar cuál usa.

</aside>

## La pendiente depende de la profundidad

La relación $\beta \propto 1/AD$ 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):

```python
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)
```
```output
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.34
```

La 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:

*Fig. — Pendiente β estimada en ventanas de 10 minutos frente a la profundidad media del primer nivel, con la curva ajustada β = 0,00455/profundidad y la aproximación de CKS, medio tick / profundidad.* Dieciocho puntos en dos grupos: BELO, con profundidad de 1.050 a 1.145 acciones y β de 3,5 a 4,8 millonésimas, y ARVA, con 1.700 a 1.860 acciones y β de 2,0 a 2,8. Dos curvas decrecientes pasan entre los grupos: la ajustada, destacada, y la de medio tick, algo por encima.

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.

```python
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)
```
```output
        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.769
```

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

```mermaid
flowchart LR
  E[Eventos del libro] --> O[OFI del intervalo]
  O --> P[Cambio del medio]
  O -. casi nada .-> S[Intervalo siguiente]
```

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

Un backtest que use el OFI de la barra para decidir la operación al precio de esa misma barra tiene sesgo de
anticipación (lección 0.1.1), aunque el código no mire ninguna fila futura: el OFI y el cambio del medio son la misma
cosa vista dos veces. La versión honesta usa el OFI hasta el instante de la decisión y el precio al que se podría
ejecutar después, con la latencia incluida. Con eso, en estos datos, no queda nada.

</aside>

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, $(q^b - q^a)/(q^b + q^a)$, 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 tick[^2]. 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:

```python
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)
```
```output
                              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         0
```

*Fig. — Latencia de ida y vuelta de seis perfiles de conexión: mediana (cuadrado), raya hasta el percentil 99,9 y máximo (círculo hueco); la vertical marca los 57 ms que tarda la luz en la fibra entre Madrid y Nueva York, ida y vuelta.* Eje horizontal logarítmico de 1 microsegundo a 1 segundo. Medianas: casa a Nueva York 88,5 ms, casa a Fráncfort 31,1 ms, VPS en Nueva Jersey 1,2 ms, nube en la misma región 0,65 ms, servidor colocado 27,8 µs y FPGA 1,6 µs. Las colas de los perfiles con red llegan a varias veces la mediana: el máximo de casa a Nueva York es 730 ms. La línea vertical destacada de 57 ms queda justo a la izquierda de la mediana de casa a Nueva York.

Cinco ó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.

```python
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")
```
```output
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 vuelta
```

Desde 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?

```python
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)
```
```output
                         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.0000
```

*Fig. — ARVA: probabilidad de que el primer nivel del libro (precio o tamaño) y el precio medio hayan cambiado cuando llega una orden enviada al ver una cotización, según la latencia mediana de cada perfil.* Dos curvas frente a la latencia en escala logarítmica. La del primer nivel sube del 0 % con FPGA y servidor colocado al 0,7 % en la nube, 1,3 % con VPS, 27,5 % desde casa a Fráncfort y 59 % desde casa a Nueva York, este último destacado. La del precio medio apenas se separa de cero: 1,3 % desde casa a Nueva York.

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

```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 `shift` sin 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

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

```python
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)
```
```output
  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.74
```

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

2. 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 $\Delta P = \beta_b\,\mathrm{OFI}^b + \beta_a\,\mathrm{OFI}^a$ en BELO. ¿Son parecidas las dos pendientes?
   ¿Qué esperarías en el modelo?

**Solución.**

```python
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))
```
```output
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.

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

[^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](https://doi.org/10.1093/jjfinec/nbt003); versión en
    [arXiv:1011.6402](https://arxiv.org/abs/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.
[^2]: M. D. Gould y J. Bonart, «Queue Imbalance as a One-Tick-Ahead Price Predictor in a Limit Order Book»,
    [arXiv:1512.03492](https://arxiv.org/abs/1512.03492), 2015, consultada el 22/10/2026.

## Autoevaluación

1. 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
   - D. 0: los precios no cambian

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

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

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

3. 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?

   - A. También alrededor del 67 %
   - B. Alrededor del 30 %
   - C. Prácticamente cero (0,3 % en ARVA y 0,1 % en BELO)
   - D. Negativo

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

4. 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?

   - A. Casi nada: es todo congestión
   - B. Unos 57 ms: 5.800 km de fibra, ida y vuelta, a unos 204.000 km/s
   - C. Exactamente 88 ms
   - D. Unos 6 ms

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

5. 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)?

   - A. Las dos alrededor del 50 %
   - 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
   - C. Un 59 % el medio y un 1,3 % el primer nivel
   - D. Cero las dos

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

## Recursos

### Research

- [Liquidez Order Book - Trading Liquidez Agregada](https://pythonparatrading.com/limit-order-book-y-liquidez-de-mercado/) · Research-0008 · 11 min
- [Modelos Cuantitativos y Ejecución Algorítmica](https://pythonparatrading.com/modelos-cuantitativos-ejecucion-algoritmica/) · Research-0009 · 11 min

### Documentación

- [Cont, Kukanov y Stoikov (2014): The Price Impact of Order Book Events, Journal of Financial Econometrics 12(1)](https://doi.org/10.1093/jjfinec/nbt003) · doi.org
- [Cont, Kukanov y Stoikov: versión en arXiv (1011.6402)](https://arxiv.org/abs/1011.6402) · arxiv.org
- [Gould y Bonart (2015): Queue Imbalance as a One-Tick-Ahead Price Predictor in a Limit Order Book (arXiv)](https://arxiv.org/abs/1512.03492) · arxiv.org

### Datos

- [Datos del nivel 4: mejor bid y ask de ARVA y BELO, evento a evento (Parquet)](https://pythonparatrading.com/curso/datos/nivel-4/flujo4_cotizaciones.parquet) · Descarga · flujo4_cotizaciones.parquet
- [Datos del nivel 4: latencias de ida y vuelta de seis perfiles de conexión (Parquet)](https://pythonparatrading.com/curso/datos/nivel-4/latencia4_rtt.parquet) · Descarga · latencia4_rtt.parquet

Lección anterior: [4.3.1 · Implementation shortfall, VWAP, TWAP y Almgren-Chriss](https://pythonparatrading.com/curso-python-trading/nivel-4/implementation-shortfall-vwap-twap-y-almgren-chriss/)

Lección siguiente: [4.4.1 · Elegir bróker desde España o la UE: regulación, activos, costes y API](https://pythonparatrading.com/curso-python-trading/nivel-4/elegir-broker-desde-espana-y-la-ue/)
