Lección 2.3.6 · 70 min

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.

Fig. — Inventario del creador de mercado en la sesión de MMQ: cotizaciones simétricas frente a Avellaneda-Stoikov (γ = 1e-4)
Lecciones del nivel 2
  1. 2.1 · Estadística y econometría para trading

    1. 2.1.1 Contrastes de hipótesis, p-valores, bootstrap y regresión con errores robustos
    2. 2.1.2 Estacionariedad: ADF, KPSS, diferenciación y diferenciación fraccional
    3. 2.1.3 Modelos ARMA y ARIMA: autocorrelación simple (ACF) y parcial (PACF)
    4. 2.1.4 Modelos de volatilidad: ARCH, GARCH, EGARCH y GJR con la librería arch
    5. 2.1.5 Cointegración: Engle-Granger, Johansen y vida media de la reversión
    6. 2.1.6 Filtro de Kalman para hedge ratios dinámicos
    7. 2.1.7 Detección de regímenes: modelos de Markov ocultos y cambios estructurales
    8. 2.1.8 El problema de las pruebas múltiples: Bonferroni, FDR y el «factor zoo»
  2. 2.2 · Anatomía de un sistema de trading

    1. 2.2.1 Hipótesis de ventaja: riesgo, comportamiento, estructura e información
    2. 2.2.2 Universo, señal, filtro, entrada, salida, sizing y gestión del riesgo
    3. 2.2.3 Frecuencias: intradía, swing y posicional, y la capacidad de una estrategia
    4. 2.2.4 Diario de investigación y registro de experimentos contra el data snooping
  3. 2.3 · Familias de estrategias

    1. 2.3.1 Seguimiento de tendencia y momentum
    2. 2.3.2 Reversión a la media: RSI y Bollinger, reversión intradía y huecos
    3. 2.3.3 Pairs trading y arbitraje estadístico: distancia, cointegración y Kalman
    4. 2.3.4 Carry en divisas, futuros y bonos; estacionalidad y efectos calendario
    5. 2.3.5 Factores (value, calidad, baja volatilidad, tamaño) y estrategias de eventos
    6. 2.3.6 Introducción al market making: inventario, spread y selección adversa (Avellaneda-Stoikov)
    7. 2.3.7 Estrategias específicas de cripto: momentum, arbitraje de funding y basis
  4. 2.4 · Backtesting: motores y paradigmas

    1. 2.4.1 Backtest vectorizado desde cero: matrices, desfase, costes y límites
    2. 2.4.2 Backtest por eventos desde cero: bucle, cartera, órdenes y fills
    3. 2.4.3 vectorbt: barridos masivos de parámetros y simulación de carteras
    4. 2.4.4 backtesting.py y backtrader: dos motores didácticos y su estado
    5. 2.4.5 NautilusTrader: arquitectura, datos, estrategias y el mismo código en backtest y en real
    6. 2.4.6 LEAN/QuantConnect y Zipline Reloaded; bt para asignación de activos
    7. 2.4.7 Modelado de costes: comisiones, horquilla, impacto, financiación y préstamo de títulos
    8. 2.4.8 Modelado de fills: órdenes limitadas, cola, fills parciales, huecos y stops
  5. 2.5 · Sesgos y validación

    1. 2.5.1 Sesgos de backtest: anticipación, supervivencia, selección y narrativa
    2. 2.5.2 In-sample y out-of-sample: walk-forward anclado y móvil
    3. 2.5.3 Validación cruzada combinatoria purgada para estrategias
    4. 2.5.4 Deflated Sharpe Ratio y Probability of Backtest Overfitting (PBO)
    5. 2.5.5 Monte Carlo: remuestreo de trades, bootstrap de bloques y permutaciones de señales
    6. 2.5.6 Sensibilidad de parámetros: mesetas frente a picos, robustez entre activos y periodos
    7. 2.5.7 Datos sintéticos para pruebas de estrés: GBM, GARCH y bootstrapping
    8. 2.5.8 Checklist de «backtest honesto» antes de pasar a paper trading
  6. 2.6 · Métricas de rendimiento

    1. 2.6.1 CAGR, Sharpe, Sortino, Calmar, Omega, drawdown y Ulcer Index
    2. 2.6.2 Win rate, payoff, expectativa, alpha, beta, information ratio, turnover y capacidad
    3. 2.6.3 Informes con QuantStats o pyfolio-reloaded y tearsheets propios

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

un ladolos dosPrecio medioCotizar bid y ask¿Ejecución?InventarioHorquilla cobradaRiesgo de precio
Fuente 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:

ComponenteSignoDe 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

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.

IN [01]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)
OUT [01]Órdenes a mercado y precio medio de MMQ
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

λ(δ)=A e−kδ.\lambda(\delta) = A\, e^{-k\delta}.

AA es la intensidad de una cotización en el propio medio y kk 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-k.

IN [02]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")
OUT [02]Estimación de A y k por lado
                      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: AA entre 0,82 y 0,84 por segundo (verdad 0,81) y kk entre 40,0 y 40,2 por USD (verdad 40). Con todo el flujo, que es lo único que ve un creador real, AA sube a 0,87 y kk 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/k1/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

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 hh segundos siguientes: signo×(mt+h−mt)\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.

IN [03]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)
OUT [03]Marcado del medio tras las órdenes informadas y las no informadas
    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

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, ∑tqt (mt+1−mt)\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.

IN [04]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/k1/k que maximiza el ingreso de cada lado, sin mirar el inventario:

IN [05]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)
OUT [05]Cotizaciones simétricas a 1/k del medio
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 qq y un horizonte TT 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:

rt=st−qt γ σ2 (T−t).r_t = s_t - q_t\,\gamma\,\sigma^2\,(T - t).

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

δa+δb=γ σ2 (T−t)+2γln⁡(1+γk).\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 (kk) y, con γ\gamma pequeño, tiende a 2/k2/k, la horquilla simétrica de antes. La diferencia entre las dos estrategias está, sobre todo, en el desplazamiento por inventario.

Inventario qPrecio de reservaMedio sσ, γ, T - tBid = r - δAsk = r + δk, γSemihorquilla δ
Fuente 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 qq en acciones, γ\gamma tiene unidades de 1/USD y valores pequeños: con γ=10−4\gamma = 10^{-4} y 100 acciones, el desplazamiento al abrir la sesión es de un céntimo.

IN [06]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)
OUT [06]Avellaneda-Stoikov con γ = 1e-4
                 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
IN [07]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)
OUT [07]Sensibilidad a γ
         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 γ=2×10−5\gamma = 2 \times 10^{-5} el control es más flojo (desviación de 563 acciones) y el resultado parecido; con 5×10−45 \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×10−32 \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−λ(δ)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.

IN [08]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)
OUT [08]200 sesiones simuladas: resultado y dispersión
             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 γ=10−4\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 γ=5×10−4\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 γ=10−4\gamma = 10^{-4}). La fórmula multiplica el inventario por T−tT - 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.

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
IN [09]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)
OUT [09]Barrido de la semihorquilla simétrica
        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/k1/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.

  1. 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
IN [10]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}")
OUT [10]Horquilla + inventario = resultado
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.

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

Respuesta plegada en cada pregunta

Autoevaluación

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

    • AUn tick (1 céntimo)
    • Bδ = 1/k ≈ 2,8 céntimos
    • CCuanto más ancha, mejor
    • DLa mitad de la horquilla del mercado
    Ver respuesta

    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?

    • ASube, para comprar más
    • BNo cambia: solo cambia la horquilla
    • CBaja 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
    • DSe iguala al último precio negociado
    Ver respuesta

    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?

    • AQue 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
    • BNada: es ruido
    • CQue debe operar solo con las informadas
    • DQue el medio tiene tendencia
    Ver respuesta

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

    • APorque la fórmula está mal
    • BPorque se ejecuta menos
    • CPorque los costes son mayores
    • DPorque 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
    Ver respuesta

    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?

    • APorque los datos son de un solo día
    • BPorque 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
    • CPorque usa el precio medio
    • DPorque no hay órdenes informadas
    Ver respuesta

    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