---
title: "2.3.6 Market making: inventario, spread y Avellaneda-Stoikov"
description: "Intensidad de llegada, cotizaciones simétricas frente a Avellaneda-Stoikov, riesgo de inventario y selección adversa con una sesión sintética."
url: "https://pythonparatrading.com/curso-python-trading/nivel-2/market-making-avellaneda-stoikov/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-2/market-making-avellaneda-stoikov.png"
---

# 2.3.6 · Introducción al market making: inventario, spread y selección adversa (Avellaneda-Stoikov)

> Un creador de mercado cotiza compra y venta a la vez y gana la horquilla si las dos se ejecutan. Pierde por dos vías: el inventario que acumula cuando solo se ejecuta un lado y las órdenes de quien sabe más que él. Esta lección estima la intensidad de las órdenes de una sesión sintética, simula un creador con cotizaciones simétricas y otro con el modelo de Avellaneda y Stoikov, y descompone el resultado en horquilla, inventario y selección adversa.

- Curso: [Nivel 2: Estrategias sistemáticas y backtesting riguroso](https://pythonparatrading.com/curso-python-trading/nivel-2/)
- Módulo: 2.3 · Familias de estrategias
- Duración: 70 min
- Requisitos: [1.1.1 · Participantes y centros de negociación](https://pythonparatrading.com/curso-python-trading/nivel-1/participantes-y-centros-de-negociacion/), [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/)

## Al terminar esta lección

- Estimar la intensidad de ejecución de una cotización a distancia δ del medio, λ(δ) = A·exp(−kδ), con el flujo de órdenes.
- Medir la selección adversa con el marcado a varios horizontes (markout) de las órdenes informadas y no informadas.
- Simular un creador de mercado con cotizaciones simétricas y otro con el precio de reserva y la horquilla óptima de Avellaneda-Stoikov.
- Descomponer el resultado en horquilla capturada, inventario y selección adversa, y medir el riesgo de inventario con sesiones simuladas.

## El negocio de cotizar a los dos lados

Un **creador de mercado** (*market maker*) mantiene a la vez una orden limitada de compra por debajo del precio medio y
otra de venta por encima. Si llega una orden a mercado de cada lado, compra a su bid, vende a su ask y se queda la
diferencia: la horquilla. No apuesta por la dirección del precio; vende inmediatez a quien la necesita (lección 1.1.1).

```mermaid
flowchart TB
  M[Precio medio] --> C[Cotizar bid y ask]
  C --> F{¿Ejecución?}
  F -- un lado --> I[Inventario]
  F -- los dos --> H[Horquilla cobrada]
  I --> R[Riesgo de precio]
  R --> C
  H --> C
```

El problema está en la rama de la izquierda. Las órdenes no llegan emparejadas: si llegan cinco ventas seguidas, el
creador compra cinco veces y se queda largo, expuesto a que el precio baje. Y no todas las órdenes son iguales: algunas
vienen de quien sabe que el precio va a moverse, y esas ejecutan al creador justo antes de que el movimiento le haga
perder. Son las dos fuentes de pérdida que esta lección mide:

| Componente | Signo | De qué depende |
|---|---|---|
| Horquilla capturada | + | Distancia de las cotizaciones al medio y número de ejecuciones |
| Inventario | ± | Tamaño de la posición y volatilidad del precio mientras se mantiene |
| Selección adversa | − | Proporción de órdenes informadas y cuánto mueven el precio después |

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

Los datos son **sintéticos**: una sesión de una acción ficticia, `MMQ`, el 12 de marzo de 2025. El generador sigue el
modelo de Avellaneda y Stoikov con parámetros conocidos: órdenes a mercado como un proceso de Poisson de 0,9 por
segundo y lado; las no informadas, con un alcance exponencial de media 1/k (k = 40 por USD); y un 10 % de órdenes
informadas que mueven el medio 2 céntimos en los 30 segundos siguientes. Sirven para comprobar si los métodos recuperan
esos parámetros. Las simulaciones del creador son deliberadamente optimistas: no hay competencia en la cola, ni
comisiones, ni latencia.

</aside>

## El flujo de órdenes de una sesión

`mm2_flujo.parquet` tiene cada orden a mercado de la sesión: cuándo llega, de qué lado (`compra` toma la oferta,
`venta` el bid), su tamaño y su **alcance**, la distancia al medio hasta la que barre el libro. Una cotización de venta
a δ del medio se ejecuta con una compra si el alcance de esa compra es al menos δ. `mm2_medio.parquet` es el precio
medio al final de cada segundo.

```python
import numpy as np
import pandas as pd

flujo = pd.read_parquet("mm2_flujo.parquet")
medio = pd.read_parquet("mm2_medio.parquet")
inicio = pd.Timestamp("2025-03-12 13:30:00", tz="UTC")
T = len(medio)                   # segundos de la sesión
print(len(flujo), "órdenes,", T, "segundos")
print(flujo.groupby("lado")["tamano"].agg(["size", "mean"]).round(0))
print(f"informadas: {flujo['informada'].mean():.1%}")
flujo.groupby("informada")["alcance"].describe()[
    ["count", "mean", "50%", "max"]].round(4)
```
```output
42092 órdenes, 23400 segundos
         size   mean
lado
compra  20889  300.0
venta   21203  302.0
informadas: 10.0%
             count    mean    50%    max
informada
False      37903.0  0.0250  0.017  0.281
True        4189.0  0.0492  0.034  0.691
```

En 6,5 horas llegan 42.092 órdenes a mercado, casi las mismas de cada lado (0,89 y 0,91 por segundo), de unas 300
acciones de media. El 10,0 % son informadas, y su alcance es el doble: una media de 4,9 céntimos frente a 2,5 de las
no informadas. Quien sabe que el precio va a moverse está dispuesto a barrer más niveles del libro para ejecutar ya,
y eso tiene una consecuencia directa para el creador: cuanto más lejos del medio cotiza, mayor es la proporción de
informados entre las órdenes que le llegan.

## Intensidad de ejecución según la distancia al medio

El modelo de Avellaneda y Stoikov (2008) supone que una cotización a distancia δ del medio se ejecuta como un proceso
de Poisson de intensidad

$$
\lambda(\delta) = A\, e^{-k\delta}.
$$

$A$ es la intensidad de una cotización en el propio medio y $k$ dice lo deprisa que cae al alejarse. Se estima
contando, para cada δ, cuántas órdenes de un lado tienen un alcance de al menos δ y dividiendo por la duración de la
sesión; en escala logarítmica la relación es una recta de pendiente $-k$.

```python
deltas = np.arange(0, 0.1001, 0.005)

def intensidad(ordenes):
    lam = np.array([(ordenes["alcance"] >= d).sum() / T
                    for d in deltas])
    pend, ordenada = np.polyfit(deltas, np.log(lam), 1)
    return np.exp(ordenada), -pend, lam

filas = {}
for lado in ["compra", "venta"]:
    g = flujo[flujo["lado"] == lado]
    for nombre, sub in [("todas", g), ("no informadas",
                                       g[~g["informada"]])]:
        A, k, _ = intensidad(sub)
        filas[(lado, nombre)] = {"A_por_s": A, "k_por_usd": k}
est = pd.DataFrame(filas).T
print(est.round(3))
A = est.xs("todas", level=1)["A_por_s"].mean()
k = est.xs("todas", level=1)["k_por_usd"].mean()
print(f"\nparámetros del creador: A = {A:.2f} por s, "
      f"k = {k:.1f} por USD")
print(f"semihorquilla que maximiza δ·λ(δ): "
      f"1/k = {100 / k:.2f} céntimos")
```
```output
                      A_por_s  k_por_usd
compra todas            0.866     35.327
       no informadas    0.824     39.969
venta  todas            0.883     35.433
       no informadas    0.836     40.230

parámetros del creador: A = 0.87 por s, k = 35.4 por USD
semihorquilla que maximiza δ·λ(δ): 1/k = 2.83 céntimos
```

Con las no informadas, la estimación recupera casi exactamente los parámetros plantados: $A$ entre 0,82 y 0,84 por
segundo (verdad 0,81) y $k$ entre 40,0 y 40,2 por USD (verdad 40). Con todo el flujo, que es lo único que ve un
creador real, $A$ sube a 0,87 y $k$ baja a 35,4: las informadas, con más alcance, hacen la cola más gruesa. El creador
usará estos parámetros, los observables. La semihorquilla que maximiza el ingreso de un lado sin riesgo, $1/k$, es de
2,83 céntimos.

*Fig. — Intensidad de ejecución de una cotización según su distancia al medio en la sesión de MMQ, con el ajuste exponencial.* Puntos en escala logarítmica: ejecuciones por segundo y lado que recibiría una cotización a 0, 0,5, 1… hasta 10 céntimos del medio, desde unas 0,90 en el medio hasta 0,027 a 10 céntimos. Caen casi sobre una recta destacada, el ajuste 0,87 · exp(−35,4 · δ). Un rótulo recuerda la intensidad plantada de las órdenes no informadas, 0,81 · exp(−40 · δ).

La figura enseña la intensidad medida para cada distancia, en escala logarítmica, con la recta ajustada: los puntos
caen casi sobre ella. A un céntimo del medio, una cotización de venta se ejecuta unas 0,6 veces por segundo; a 5
céntimos, 0,15; a 10, menos de 0,03. Cada céntimo más de horquilla cuesta un 30 % de las ejecuciones.

## Selección adversa: lo que pasa después de cada orden

El **marcado** (*markout*) de una orden es cuánto se mueve el medio en su dirección en los $h$ segundos siguientes:
$\text{signo} \times (m_{t+h} - m_t)$. Para el creador que la ejecuta, es lo que pierde por la información que traía
la orden. Glosten y Milgrom (1985) explican la horquilla con este mecanismo: el creador no distingue a los informados y
cobra a todos lo suficiente para compensar lo que pierde con ellos.

```python
m = np.r_[medio["medio"].iloc[0], medio["medio"].to_numpy()]
# m[s] = medio al empezar el segundo s (el último conocido)
seg = np.floor((flujo["ts"] - inicio).dt.total_seconds()).astype(int)
seg = seg.clip(0, T - 1).to_numpy()
signo = np.where(flujo["lado"] == "compra", 1, -1)
informada = flujo["informada"].to_numpy()

markout = {}
for h in [1, 5, 10, 20, 30, 45, 60]:
    mo = 100 * signo * (m[np.minimum(seg + h, T)] - m[seg])
    markout[h] = {"informadas_cent": mo[informada].mean(),
                  "no_informadas_cent": mo[~informada].mean()}
pd.DataFrame(markout).T.round(3)
```
```output
    informadas_cent  no_informadas_cent
1             0.066               0.002
5             0.311               0.004
10            0.616               0.007
20            1.323              -0.010
30            1.998              -0.008
45            1.959              -0.001
60            1.945               0.009
```

Tras una orden informada, el medio se mueve en su dirección de forma casi lineal durante 30 segundos, hasta 2,0
céntimos, y después se queda ahí: es el impacto plantado. Tras una no informada no se mueve nada (de −0,010 a +0,009
céntimos). Una ejecución contra un informado cuesta al creador, de media, 2 céntimos por acción de movimiento en
contra, casi lo mismo que cobra de semihorquilla. La figura dibuja las dos curvas.

*Fig. — Selección adversa: movimiento medio del precio en la dirección de la orden en los 60 segundos siguientes.* Dos curvas. Tras las órdenes informadas, destacada, el medio se mueve en su dirección de forma casi lineal hasta 2,0 céntimos a los 30 segundos, marcados con una línea vertical, y después se queda plano. Tras las no informadas, la curva se queda en cero.

## Un simulador de creador de mercado

El simulador recorre la sesión segundo a segundo. Al empezar cada segundo, el creador fija su bid y su ask a partir
del último medio conocido y de su inventario, redondeados al tick (el bid hacia abajo y el ask hacia arriba). Cada
orden que llega en ese segundo le ejecuta 100 acciones si su alcance llega a su cotización. Las simplificaciones son
importantes y conviene decirlas: su cotización es la única del libro a ese precio, se ejecuta siempre entera, no paga
comisiones y la cambia sin latencia.

La contabilidad separa las tres partes del resultado. La **horquilla** es lo que cobra en cada ejecución frente al
medio de ese momento; el **inventario**, lo que gana o pierde la posición con los cambios del medio,
$\sum_t q_t\,(m_{t+1} - m_t)$; la suma de las dos es exactamente el resultado final valorado al medio. La **selección
adversa** se mide aparte, con el marcado a 30 segundos de las ejecuciones contra órdenes informadas: es la parte del
inventario que se pierde por ellas.

```python
TICK, LOTE = 0.01, 100
por_segundo = [[] for _ in range(T)]
for i, s in enumerate(seg):
    por_segundo[s].append(i)
alcance = flujo["alcance"].to_numpy()

def simular(cotizar):
    q, caja, horquilla, inventario = 0, 0.0, 0.0, 0.0
    ejec, q_seg = [], np.zeros(T)
    for s in range(T):
        bid, ask = cotizar(m[s], q, s)
        bid = np.floor(bid / TICK + 1e-9) * TICK
        ask = np.ceil(ask / TICK - 1e-9) * TICK
        for i in por_segundo[s]:
            if signo[i] == 1 and alcance[i] >= ask - m[s] - 1e-9:
                q -= LOTE; caja += ask * LOTE
                horquilla += (ask - m[s]) * LOTE
                ejec.append((s, -1, informada[i]))
            elif signo[i] == -1 and alcance[i] >= m[s] - bid - 1e-9:
                q += LOTE; caja -= bid * LOTE
                horquilla += (m[s] - bid) * LOTE
                ejec.append((s, 1, informada[i]))
        inventario += q * (m[s + 1] - m[s])
        q_seg[s] = q
    e = np.array(ejec)
    s_e, lado_e, inf_e = e[:, 0].astype(int), e[:, 1], e[:, 2] == 1
    mo = lado_e * (m[np.minimum(s_e + 30, T)] - m[s_e]) * LOTE
    res = {"resultado": caja + q * m[T], "horquilla": horquilla,
           "inventario": inventario,
           "sel_adversa_30s": mo[inf_e].sum(),
           "ejecuciones": len(e), "pct_informadas": 100 * inf_e.mean(),
           "q_desv": q_seg.std(), "q_max_abs": np.abs(q_seg).max()}
    return res, q_seg
```

La primera estrategia cotiza **simétrica** alrededor del medio, a la semihorquilla $1/k$ que maximiza el ingreso de
cada lado, sin mirar el inventario:

```python
def simetrica(medio_s, q, s):
    return medio_s - 1 / k, medio_s + 1 / k

res_sim, q_sim = simular(simetrica)
pd.Series(res_sim).round(1)
```
```output
resultado          26169.0
horquilla          41382.5
inventario        -15213.5
sel_adversa_30s    -4709.5
ejecuciones        12820.0
pct_informadas        17.1
q_desv              3553.9
q_max_abs          15500.0
dtype: float64
```

La horquilla capturada es de 41.383 USD en 12.820 ejecuciones de 100 acciones: unos 3,2 céntimos por acción, porque
la cotización a 2,83 céntimos se redondea hacia fuera al tick y el medio es múltiplo de medio céntimo. El inventario
se lleva 15.214 USD: sin control, la posición deriva como un paseo aleatorio y llega a 15.500 acciones en valor
absoluto, con una desviación típica de 3.554. El resultado final, 26.169 USD, depende sobre todo de en qué dirección
se movió el precio mientras el inventario era grande. La selección adversa, medida aparte, es de −4.710 USD: un 17,1 %
de las ejecuciones son contra informados, más que su 10 % del flujo, porque llegan más lejos.

## Avellaneda-Stoikov: precio de reserva y horquilla óptima

Avellaneda y Stoikov resuelven el problema de un creador con aversión al riesgo $\gamma$, inventario $q$ y un horizonte
$T$ en el que cierra la posición. Su solución aproximada tiene dos piezas. La primera es el **precio de reserva**, el
precio al que al creador le daría igual comprar o vender una unidad más:

$$
r_t = s_t - q_t\,\gamma\,\sigma^2\,(T - t).
$$

Con inventario positivo, $r$ queda por debajo del medio $s$; con inventario negativo, por encima. La segunda es la
**horquilla óptima** alrededor de $r$:

$$
\delta^a + \delta^b = \gamma\,\sigma^2\,(T - t) + \frac{2}{\gamma}\ln\left(1 + \frac{\gamma}{k}\right).
$$

El primer término es una prima por el riesgo de inventario, mayor cuanto más queda de sesión; el segundo depende de
la forma de la demanda ($k$) y, con $\gamma$ pequeño, tiende a $2/k$, la horquilla simétrica de antes. La diferencia
entre las dos estrategias está, sobre todo, en el desplazamiento por inventario.

```mermaid
flowchart TB
  Q[Inventario q] --> R[Precio de reserva]
  S[Medio s] --> R
  V[σ, γ, T - t] --> R
  R --> B[Bid = r - δ]
  R --> A[Ask = r + δ]
  K[k, γ] --> D[Semihorquilla δ]
  D --> B
  D --> A
```

La volatilidad del medio es de 0,0065 USD por √s (la del generador). Con $q$ en acciones, $\gamma$ tiene unidades de
1/USD y valores pequeños: con $\gamma = 10^{-4}$ y 100 acciones, el desplazamiento al abrir la sesión es de un céntimo.

```python
SIGMA = 0.0065

def avellaneda_stoikov(gamma):
    def cotizar(medio_s, q, s):
        resto = T - s
        r = medio_s - q * gamma * SIGMA**2 * resto
        semi = 0.5 * (gamma * SIGMA**2 * resto
                      + 2 / gamma * np.log(1 + gamma / k))
        return r - semi, r + semi
    return cotizar

res_as, q_as = simular(avellaneda_stoikov(1e-4))
pd.DataFrame({"simétrica": res_sim,
              "AS γ=1e-4": res_as}).round(1)
```
```output
                 simétrica  AS γ=1e-4
resultado          26169.0    38055.0
horquilla          41382.5    39647.5
inventario        -15213.5    -1592.5
sel_adversa_30s    -4709.5    -4453.0
ejecuciones        12820.0    13165.0
pct_informadas        17.1       16.6
q_desv              3553.9      225.7
q_max_abs          15500.0     1700.0
```

Con la misma sesión y casi la misma horquilla capturada (39.648 frente a 41.383 USD), el creador de Avellaneda-Stoikov
reduce la pérdida por inventario de 15.214 a 1.593 USD y su resultado sube a 38.055. La desviación típica del
inventario baja de 3.554 a 226 acciones y el máximo, de 15.500 a 1.700. La selección adversa casi no cambia (−4.453
frente a −4.710): el control de inventario no protege de los informados, que siguen llegando y siguen moviendo el
precio. Es una pérdida que solo se reduce cobrando más horquilla o detectando el flujo tóxico.

La figura de la cabecera enseña la diferencia: el inventario de la estrategia simétrica vaga durante toda la sesión
por miles de acciones; el de Avellaneda-Stoikov, destacado, oscila cerca de cero, porque cada compra baja el precio de
reserva y hace más probable la venta siguiente. La figura de abajo descompone el resultado de las dos.

*Fig. — Resultado de los dos creadores de mercado en la sesión de MMQ y sus componentes, en miles de USD.* Barras horizontales; AS = Avellaneda-Stoikov. Simétrica: horquilla +41,4, inventario −15,2, resultado +26,2, selección adversa a 30 segundos −4,7. Avellaneda-Stoikov: horquilla +39,6, inventario −1,6, resultado +38,1 (destacado), selección adversa −4,5. La selección adversa es parte del inventario: el resultado es horquilla más inventario.

```python
barrido = {g: simular(avellaneda_stoikov(g))[0]
           for g in [2e-5, 1e-4, 5e-4, 2e-3]}
pd.DataFrame(barrido).T[["resultado", "horquilla", "inventario",
                         "sel_adversa_30s", "q_desv"]].round(0)
```
```output
         resultado  horquilla  inventario  sel_adversa_30s  q_desv
0.00002    37652.0    40854.0     -3202.0          -4615.0   563.0
0.00010    38055.0    39647.0     -1592.0          -4453.0   226.0
0.00050    22447.0    22923.0      -475.0          -4457.0   136.0
0.00200   -76819.0   -76500.0      -319.0          -4224.0   105.0
```

Con $\gamma = 2 \times 10^{-5}$ el control es más flojo (desviación de 563 acciones) y el resultado parecido; con
$5 \times 10^{-4}$, el inventario casi no se mueve (136), pero la horquilla capturada baja a la mitad: el precio de
reserva se aleja tanto del medio, con poco inventario, que el creador regala parte de la horquilla para deshacerse de
él. Con $2 \times 10^{-3}$ la horquilla es negativa (−76.500 USD): con 100 acciones de inventario, el desplazamiento
supera la semihorquilla y una de las cotizaciones cruza el medio. El creador compra por encima del medio o vende por
debajo en cada ejecución. $\gamma$ no es un parámetro que se pueda elegir mirando solo el riesgo.

## El riesgo de inventario en muchas sesiones

Una sola sesión no dice cuánto varía el resultado. Con los parámetros estimados se pueden simular muchas: el medio
como un paseo aleatorio de volatilidad σ por segundo y, en cada segundo, la ejecución de cada lado con probabilidad
$1 - e^{-\lambda(\delta)}$ (al menos una orden que llegue a la cotización). Estas sesiones no tienen órdenes
informadas, así que aíslan el riesgo de inventario. Se simulan 200 sesiones a la vez, vectorizadas con NumPy, con
semilla fija.

```python
def sesiones(gamma, n=200, semilla=7):
    rng = np.random.default_rng(semilla)
    s_m = np.full(n, 48.0)
    q, caja = np.zeros(n), np.zeros(n)
    for s in range(T):
        resto = T - s
        if gamma == 0:
            r, semi = s_m, 1 / k
        else:
            r = s_m - q * gamma * SIGMA**2 * resto
            semi = 0.5 * (gamma * SIGMA**2 * resto
                          + 2 / gamma * np.log(1 + gamma / k))
        bid = np.floor((r - semi) / TICK + 1e-9) * TICK
        ask = np.ceil((r + semi) / TICK - 1e-9) * TICK
        p_ask = 1 - np.exp(-A * np.exp(-k * np.maximum(ask - s_m, 0)))
        p_bid = 1 - np.exp(-A * np.exp(-k * np.maximum(s_m - bid, 0)))
        vende = rng.random(n) < p_ask
        compra = rng.random(n) < p_bid
        q += LOTE * (compra.astype(float) - vende)
        caja += LOTE * (ask * vende - bid * compra)
        s_m = s_m + SIGMA * rng.standard_normal(n)
    return caja + q * s_m, q

mc = {}
for nombre, g in [("simétrica", 0), ("AS γ=2e-5", 2e-5),
                  ("AS γ=1e-4", 1e-4), ("AS γ=5e-4", 5e-4)]:
    pyg, q_fin = sesiones(g)
    mc[nombre] = {"media": pyg.mean(), "desv": pyg.std(),
                  "peor": pyg.min(),
                  "q_final_abs": np.abs(q_fin).mean()}
pd.DataFrame(mc).T.round(0)
```
```output
             media    desv     peor  q_final_abs
simétrica  36890.0  6385.0   6774.0       6732.0
AS γ=2e-5  36362.0   564.0  35028.0       1248.0
AS γ=1e-4  35344.0   398.0  34230.0        777.0
AS γ=5e-4  27902.0   309.0  27095.0        538.0
```

En 200 sesiones sin informados, la estrategia simétrica gana de media 36.890 USD con una desviación típica de 6.385, y
su peor sesión deja 6.774; el inventario medio al cierre es de 6.732 acciones. Avellaneda-Stoikov con
$\gamma = 10^{-4}$ gana algo menos de media (35.344), pero con una desviación típica 16 veces menor (398) y una peor
sesión de 34.230. Con $\gamma = 5 \times 10^{-4}$ la dispersión sigue bajando y la media cae a 27.902. Es el
intercambio que el modelo formaliza: menos riesgo de inventario a cambio de menos horquilla.

Llama la atención que el inventario al cierre no sea cero con Avellaneda-Stoikov (777 acciones de media con
$\gamma = 10^{-4}$). La fórmula multiplica el inventario por $T - t$, que tiende a cero al final de la sesión: en los
últimos minutos el creador deja de preocuparse por la posición, justo cuando tiene que cerrarla. Es un defecto conocido
de la versión con horizonte finito.

## Del modelo al mercado

El modelo de Avellaneda y Stoikov es el punto de partida de la literatura de market making, no una estrategia lista.
Guéant, Lehalle y Fernandez-Tapia (2013) resuelven el mismo problema con un límite de inventario y obtienen
aproximaciones de las cotizaciones óptimas que, lejos del final del horizonte, no dependen del tiempo, lo que evita el
defecto de la fórmula cerca del cierre. En un mercado real
faltan, además, piezas que la simulación ignora:

- **Competencia y cola.** Otros creadores cotizan en el mismo nivel y la prioridad de precio-tiempo decide quién se
  ejecuta (lecciones 1.1.3 y 2.4.8). La competencia estrecha la horquilla hasta que el beneficio esperado es pequeño.
- **Latencia.** Una cotización que no se actualiza a tiempo es la que eligen los informados: la selección adversa
  real depende mucho de la velocidad.
- **Costes y rebajas.** Las bolsas cobran por tomar liquidez y a menudo pagan una rebaja por ponerla; para un creador,
  esa rebaja puede ser una parte grande del margen.
- **Inventario entre días y saltos.** La posición que queda al cierre está expuesta al hueco de apertura (lección
  2.3.2), y una noticia puede mover el precio muchas horquillas de golpe.

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

El market making es una actividad de márgenes muy pequeños sobre muchas operaciones, con riesgo de pérdidas grandes en
pocos minutos. Exige infraestructura de baja latencia, acceso directo al mercado y control de riesgo automático. Los
resultados de esta lección salen de una simulación optimista con datos sintéticos y no estiman lo que ganaría nadie.

</aside>

## Ejercicios

1. Repite la estrategia simétrica con semihorquillas de 1, 2, 3 y 5 céntimos en la sesión de MMQ. ¿Qué semihorquilla
   da más horquilla capturada y cuál el mejor resultado?

**Solución.**

```python
def fija(d):
    return lambda medio_s, q, s: (medio_s - d, medio_s + d)

filas = {f"{100 * d:.0f} cent": simular(fija(d))[0]
         for d in [0.01, 0.02, 0.03, 0.05]}
pd.DataFrame(filas).T[["resultado", "horquilla", "inventario",
                       "ejecuciones", "pct_informadas"]].round(0)
```
```output
        resultado  horquilla  inventario  ejecuciones  pct_informadas
1 cent    14146.0    32910.0    -18764.0      26842.0            12.0
2 cent    20662.0    40980.0    -20317.0      18403.0            14.0
3 cent    26169.0    41382.0    -15213.0      12820.0            17.0
5 cent    22838.0    32584.0     -9745.0       6227.0            23.0
```

La horquilla capturada es máxima entre 2 y 3 céntimos (40.980 y 41.382 USD), cerca de $1/k$: con un céntimo hay el
doble de ejecuciones pero se cobra muy poco por cada una; con 5, se cobra mucho pero se ejecuta la mitad que con 3. El
mejor resultado está en 3 céntimos, aunque en una sola sesión el término de inventario (de −9.745 a −20.317 USD) lo
mueve tanto que el orden podría cambiar con otro día. La proporción de informadas sube del 12 % al 23 % al alejar la
cotización: cuanto más ancha la horquilla, más tóxico el flujo que la alcanza.

2. Comprueba la descomposición: para el creador de Avellaneda-Stoikov, la suma de horquilla e inventario tiene que
   dar el resultado final. ¿Qué parte del inventario explica la selección adversa?

**Solución.**

```python
print(f"horquilla + inventario = "
      f"{res_as['horquilla'] + res_as['inventario']:.1f}")
print(f"resultado              = {res_as['resultado']:.1f}")
print(f"selección adversa (30 s, informadas) = "
      f"{res_as['sel_adversa_30s']:.1f}")
```
```output
horquilla + inventario = 38055.0
resultado              = 38055.0
selección adversa (30 s, informadas) = -4453.0
```

La suma da exactamente el resultado, 38.055 USD: horquilla e inventario son una descomposición contable completa. La
selección adversa, −4.453 USD, es una medida de cuánto pierden en 30 segundos las posiciones abiertas contra
informados. El inventario total de Avellaneda-Stoikov es −1.593 USD, menos negativo que esa cifra: el resto de
movimientos del inventario ganó algo. Con los datos reales no hay columna `informada`, así que la selección adversa
se estima con el marcado de todas las ejecuciones, que mezcla lo informado con el ruido.

3. Un compañero propone «quitar» la selección adversa del simulador no cotizando nunca en los 30 segundos siguientes
   a una orden informada. ¿Qué error tiene la propuesta?

**Solución.**

Usa información que el creador no tiene: la columna `informada` existe porque los datos son sintéticos, pero en un
mercado real nadie sabe qué orden es informada cuando llega. Es anticipación en estado puro. Lo que sí se puede hacer
es estimar la toxicidad con lo observable (tamaño de la orden, alcance, desequilibrio reciente del flujo, movimiento
del medio tras las ejecuciones anteriores) y ensanchar la horquilla o retirarse cuando sube; y esa estimación hay que
contrastarla sin mirar la columna `informada`, que solo sirve para comprobar después si el método acierta.

## Autoevaluación

1. Con λ(δ) = 0,87·exp(−35,4·δ), ¿qué semihorquilla maximiza el ingreso esperado por segundo de un lado, δ·λ(δ), sin riesgo de inventario?

   - A. Un tick (1 céntimo)
   - B. δ = 1/k ≈ 2,8 céntimos
   - C. Cuanto más ancha, mejor
   - D. La mitad de la horquilla del mercado

   *Respuesta B: δ = 1/k ≈ 2,8 céntimos.* La derivada de δ·A·exp(−kδ) se anula en δ = 1/k. Más cerca del medio se ejecuta más a menudo pero se gana menos por ejecución; más lejos, al revés. Es también el límite de la horquilla de Avellaneda-Stoikov cuando la aversión al riesgo tiende a cero.

2. ¿Qué hace el precio de reserva de Avellaneda-Stoikov cuando el creador está largo de 500 acciones?

   - A. Sube, para comprar más
   - B. No cambia: solo cambia la horquilla
   - C. Baja por debajo del medio: el bid y el ask bajan, así que es más fácil vender y más difícil comprar, y el inventario tiende a volver a cero
   - D. Se iguala al último precio negociado

   *Respuesta C: Baja por debajo del medio: el bid y el ask bajan, así que es más fácil vender y más difícil comprar, y el inventario tiende a volver a cero.* r = s − q·γ·σ²·(T − t). Con q > 0, r < s. Las dos cotizaciones se desplazan hacia abajo con r: el ask queda más cerca del medio (se ejecuta más) y el bid más lejos (se ejecuta menos).

3. En la sesión simulada, el marcado a 30 segundos de las órdenes informadas es de +2,0 céntimos y el de las no informadas, de 0,0. ¿Qué significa para el creador?

   - A. Que cada vez que una orden informada le ejecuta, el medio se mueve 2 céntimos en su contra en los 30 segundos siguientes: pierde lo que ganó de horquilla y algo más
   - B. Nada: es ruido
   - C. Que debe operar solo con las informadas
   - D. Que el medio tiene tendencia

   *Respuesta A: Que cada vez que una orden informada le ejecuta, el medio se mueve 2 céntimos en su contra en los 30 segundos siguientes: pierde lo que ganó de horquilla y algo más.* Es la selección adversa de Glosten y Milgrom (1985): quien sabe más opera contra el creador justo cuando el precio va a moverse. Como no sabe quién es quién, el creador tiene que cobrar a todos una horquilla que compense lo que pierde con los informados.

4. Con un γ demasiado alto, el creador de Avellaneda-Stoikov de la lección pierde dinero en la horquilla. ¿Por qué?

   - A. Porque la fórmula está mal
   - B. Porque se ejecuta menos
   - C. Porque los costes son mayores
   - D. Porque con inventario el precio de reserva se aleja tanto del medio que una de las cotizaciones queda al otro lado: compra por encima del medio o vende por debajo

   *Respuesta D: Porque con inventario el precio de reserva se aleja tanto del medio que una de las cotizaciones queda al otro lado: compra por encima del medio o vende por debajo.* El desplazamiento q·γ·σ²·(T − t) crece con γ y con el inventario. Cuando supera la semihorquilla, el creador paga por deshacerse del inventario en lugar de cobrar. Las implementaciones reales limitan ese desplazamiento y el inventario máximo.

5. ¿Por qué el resultado de la simulación de la lección es mucho mejor de lo que obtendría un creador real?

   - A. Porque los datos son de un solo día
   - B. Porque supone que su cotización es la única del libro y se ejecuta entera siempre que la orden llega a su precio: sin cola, sin competencia por la prioridad, sin comisiones ni latencia
   - C. Porque usa el precio medio
   - D. Porque no hay órdenes informadas

   *Respuesta B: Porque supone que su cotización es la única del libro y se ejecuta entera siempre que la orden llega a su precio: sin cola, sin competencia por la prioridad, sin comisiones ni latencia.* En un libro real hay otros creadores en el mismo nivel y la prioridad de precio-tiempo decide quién se ejecuta (lección 2.4.8). La competencia estrecha la horquilla hasta que el beneficio esperado se acerca a cero, y la latencia hace que las cotizaciones viejas sean las que eligen los informados.

## Ideas clave

- La intensidad de ejecución cae de forma exponencial con la distancia al medio: en la sesión, λ(δ) = 0,87·exp(−35,4·δ) con todo el flujo, frente a 0,81·exp(−40·δ) de las no informadas.
- Un creador gana la horquilla y pierde por inventario y por selección adversa; el resultado se descompone en esas tres partes.
- Las órdenes informadas mueven el medio 2 céntimos en 30 segundos; las no informadas, nada: el marcado (markout) mide la selección adversa.
- Avellaneda-Stoikov desplaza las dos cotizaciones con el inventario (precio de reserva) y fija una horquilla que depende de γ, σ y k; reduce la desviación típica del inventario de miles de acciones a cientos.
- Con γ demasiado alto el precio de reserva cruza el medio y el creador paga por deshacerse del inventario.
- La simulación supone una cotización única, sin cola, comisiones ni latencia: es una herramienta para entender, no una estimación de beneficio.

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

- [Avellaneda y Stoikov (2008): High-frequency trading in a limit order book, Quantitative Finance 8(3)](https://doi.org/10.1080/14697680701381228) · doi.org
- [Glosten y Milgrom (1985): Bid, ask and transaction prices in a specialist market with heterogeneously informed traders, Journal of Financial Economics 14(1)](https://doi.org/10.1016/0304-405X(85)90044-3) · doi.org
- [Guéant, Lehalle y Fernandez-Tapia (2013): Dealing with the inventory risk, Mathematics and Financial Economics 7(4)](https://doi.org/10.1007/s11579-012-0087-0) · doi.org

### Datos

- [Datos del nivel 2: órdenes a mercado de la sesión de MMQ (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/mm2_flujo.parquet) · Descarga · mm2_flujo.parquet
- [Datos del nivel 2: precio medio por segundo de MMQ (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/mm2_medio.parquet) · Descarga · mm2_medio.parquet

Lección anterior: [2.3.5 · Factores (value, calidad, baja volatilidad, tamaño) y estrategias de eventos](https://pythonparatrading.com/curso-python-trading/nivel-2/factores-y-estrategias-de-eventos/)

Lección siguiente: [2.3.7 · Estrategias específicas de cripto: momentum, arbitraje de funding y basis](https://pythonparatrading.com/curso-python-trading/nivel-2/estrategias-cripto-momentum-funding-y-basis/)
