Lección 2.4.8 · 65 min

Modelado de fills: órdenes limitadas, cola, fills parciales, huecos y stops

Un backtest decide en cada barra si una orden se ha ejecutado y a qué precio. Con órdenes a mercado el error es pequeño; con limitadas y stops, la regla de ejecución puede convertir una estrategia perdedora en ganadora. Esta lección compara los modelos de fill con el libro orden a orden de NOVA y con las barras de 5 minutos de KAPA, y mide cuánto infla el resultado cada supuesto.

Fig. — Cuatro modelos de fill para 2.251 compras limitadas en el mejor bid de NOVA (2 de junio de 2025): proporción ejecutada en 60 s y resultado de comprar en el bid y vender en el ask 60 s después
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 fill también es un supuesto

Un backtest no ejecuta nada: decide, con los datos que tiene, si una orden se habría ejecutado y a qué precio. Esa decisión es el modelo de fill. Con órdenes a mercado pequeñas el modelo importa poco: se ejecutan seguro, y el precio es el de la barra más el deslizamiento y los costes de la lección 2.4.7. Con órdenes limitadas y stops, el modelo decide la estrategia: una limitada que se da por ejecutada cuando no lo habría estado, o al precio de un stop que el mercado saltó, mejora el resultado en cada operación, siempre en el mismo sentido.

ModeloRegla para una compra limitada a LLDatos que necesitaSesgo
Por toqueSe ejecuta si el precio negocia a LL o menosBarras u operacionesOptimista: supone ser el primero de la cola
Por cruceSolo si el precio negocia por debajo de LLBarras u operacionesPesimista en cantidad; capta la selección adversa
Por colaCuando se agota la cantidad que había delante a LLL3, o L2 más operacionesEl más realista; exige datos de libro
ProbabilísticoPor toque, con probabilidad ppBarrasAjusta cuántas, no cuáles
ParticipaciónComo mucho una fracción del volumen de cada barraVolumenFills parciales; no mide el impacto

Los datos de esta lección son sintéticos: la sesión del 2 de junio de 2025 de NOVA (nivel 1), simulada orden a orden, con su libro L2, sus mensajes L3 y sus operaciones, y las barras de 5 minutos del ETF ficticio KAPA de 2022 a 2025 (nivel 2). Ninguno es de un mercado real.

Una orden en la cola: NOVA a las 10:00

Los mensajes L3 de NOVA cubren de 10:00 a 10:05 de Nueva York (14:00 a 14:05 UTC) y empiezan con una foto de todas las órdenes vivas en orden de prioridad (lección 1.1.3). Con ellos se puede seguir exactamente qué habría pasado con una orden propia que llegara a las 10:00 al final de la cola del mejor precio:

  • delante tiene las órdenes que ya estaban a ese precio;
  • la cola avanza cuando una de ellas se ejecuta, se cancela o reduce su tamaño;
  • cuando no queda nadie delante, la siguiente ejecución a ese precio habría sido contra la orden propia.
IN [01]python
import numpy as np
import pandas as pd

l3 = pd.read_parquet("nova_libro_l3.parquet")
foto, mensajes = l3.iloc[:358], l3.iloc[358:]


def simular_cola(lado, precio, tamano=500):
    delante = foto[(foto["lado"] == lado) & (foto["precio"] == precio)]
    vivas = dict(zip(delante["id_orden"], delante["tamano"]))
    inicio = sum(vivas.values())
    toque = fill = None
    lleno = 0
    m = mensajes[(mensajes["lado"] == lado)
                 & (mensajes["precio"] == precio)]
    for msg in m.itertuples():
        if msg.evento == "ejecucion" and toque is None:
            toque = msg.ts                  # modelo por toque
        if msg.id_orden in vivas:           # avanza la cola
            if msg.evento == "cancelacion":
                vivas.pop(msg.id_orden)
            elif msg.evento == "modificacion":
                vivas[msg.id_orden] = msg.tamano
            elif msg.evento == "ejecucion":
                vivas[msg.id_orden] -= msg.tamano
                if vivas[msg.id_orden] <= 0:
                    vivas.pop(msg.id_orden)
        elif msg.evento == "ejecucion" and not vivas:
            lleno += msg.tamano             # contra la propia
            if lleno >= tamano:
                fill = msg.ts
                break
    return inicio, toque, fill


for lado, precio in [("bid", 53.08), ("ask", 53.09)]:
    inicio, toque, fill = simular_cola(lado, precio)
    seg = lambda t: None if t is None else round(
        (t - pd.Timestamp("2025-06-02 14:00", tz="UTC"))
        .total_seconds(), 1)
    print(f"{lado} {precio}: {inicio} acciones delante · toque a",
          f"{seg(toque)} s · fill por cola a {seg(fill)} s")
OUT [01]Cola exacta de una limitada con mensajes L3
bid 53.08: 5966 acciones delante · toque a 0.5 s · fill por cola a None s
ask 53.09: 2488 acciones delante · toque a 2.4 s · fill por cola a 27.0 s

Las dos órdenes cuentan la historia entera de las limitadas.

  • La compra a 53,08 se habría dado por ejecutada en el modelo por toque a los 0,5 segundos: una venta agresiva negoció a 53,08 casi enseguida. Pero delante había 5.966 acciones en ocho órdenes. La cola llegó a vaciarse (por ejecuciones y cancelaciones) y, justo entonces, el precio subió y nadie más vendió a 53,08: la orden propia no se ejecuta nunca en la ventana. El precio se fue al alza, que es justo cuando el comprador quería estar dentro.
  • La venta a 53,09 se toca a los 2,4 segundos y se ejecuta de verdad a los 27 segundos, cuando se agotan las 2.488 acciones de delante. Para entonces, el precio ya subía.
IN [02]python
cot = pd.read_parquet("nova_cotizaciones.parquet")
cot["medio"] = (cot["bid"] + cot["ask"]) / 2
ventana = cot[(cot["ts"] >= "2025-06-02 14:00")
              & (cot["ts"] <= "2025-06-02 14:05")]
ventana.set_index("ts")["medio"].resample("1min").last()
OUT [02]Qué hizo el precio después
ts
2025-06-02 14:00:00+00:00    53.085
2025-06-02 14:01:00+00:00    53.140
2025-06-02 14:02:00+00:00    53.155
2025-06-02 14:03:00+00:00    53.155
2025-06-02 14:04:00+00:00    53.175
Freq: min, Name: medio, dtype: float64

El punto medio pasa de 53,085 a 53,175 en cuatro minutos. El modelo por toque compra a 53,08 en el primer medio segundo y gana con la subida; el modelo por cola no compra (y se pierde la subida) y vende a 53,09 justo antes de que el precio suba casi nueve céntimos. Es la selección adversa de los creadores de mercado de la lección 2.3.6, vista desde una sola orden: a una limitada la ejecutan sobre todo cuando no conviene.

avanza la colacola vacíallega un agresorel precio se alejael precio se alejaEN_COLAPRIMERAEJECUTADASIN_FILL
Fuente Mermaid
stateDiagram-v2
  [*] --> EN_COLA
  EN_COLA --> EN_COLA: avanza la cola
  EN_COLA --> PRIMERA: cola vacía
  PRIMERA --> EJECUTADA: llega un agresor
  PRIMERA --> SIN_FILL: el precio se aleja
  EN_COLA --> SIN_FILL: el precio se aleja
  EJECUTADA --> [*]
  SIN_FILL --> [*]

Toque, cruce y cola en toda la sesión

Una orden no es una muestra. Los mensajes L3 solo cubren cinco minutos, pero las fotos L2 de cada 10 segundos y las operaciones de toda la sesión permiten aproximar la cola en cualquier momento: la orden propia llega al final del mejor bid de la foto, con la cantidad del nivel delante, y la cola avanza con el volumen negociado a ese precio en los centros con libro. Es una aproximación conservadora (no ve las cancelaciones de delante, que adelantarían la orden) y es la que se puede hacer con datos L2 y de operaciones; NautilusTrader hace algo parecido con queue_position=True.

Para cada una de las 2.251 fotos entre las 9:35 y las 15:50 se simula una compra limitada de 200 acciones en el mejor bid, válida 60 segundos, con tres modelos: toque (cualquier operación de la cinta consolidada a ese precio o menos), cruce (una operación por debajo) y cola (el volumen negociado a ese precio en los libros visibles supera la cantidad de delante más la propia, o el precio cruza).

IN [03]python
l2 = pd.read_parquet("nova_libro_l2.parquet")
op = pd.read_parquet("nova_operaciones.parquet")
con_libro = op[op["centro"].isin(["PRINCIPAL", "MTF_A", "MTF_B"])
               & (op["condicion"] == "regular")].reset_index(drop=True)
mejor_bid = l2[(l2["nivel"] == 1) & (l2["lado"] == "bid")]
mejor_bid = mejor_bid[mejor_bid["ts"].between(
    "2025-06-02 13:35", "2025-06-02 19:50")]
H, TAM = pd.Timedelta(seconds=60), 200


def tramo(df, desde, hasta):
    i = df["ts"].searchsorted(desde, side="right")
    j = df["ts"].searchsorted(hasta, side="right")
    return df.iloc[i:j]


filas = []
for foto_l2 in mejor_bid.itertuples():
    p, delante = foto_l2.precio, foto_l2.tamano
    cinta = tramo(op, foto_l2.ts, foto_l2.ts + H)
    libro = tramo(con_libro, foto_l2.ts, foto_l2.ts + H)
    toque = cinta.loc[cinta["precio"] <= p, "ts"].min()
    cruce = libro.loc[libro["precio"] < p, "ts"].min()
    en_p = libro["tamano"].where(libro["precio"] == p, 0).cumsum()
    cola = libro.loc[en_p >= delante + TAM, "ts"].min()
    cola = min(t for t in [cola, cruce] if pd.notna(t)) if (
        pd.notna(cola) or pd.notna(cruce)) else pd.NaT
    filas.append({"ts": foto_l2.ts, "precio": p, "delante": delante,
                  "toque": toque, "cruce": cruce, "cola": cola})
fills = pd.DataFrame(filas)
print(len(fills), "órdenes · cantidad delante (mediana):",
      int(fills["delante"].median()), "acciones")
fills[["toque", "cruce", "cola"]].notna().mean().round(3)
OUT [03]Tres modelos de fill en 2.251 órdenes
2251 órdenes · cantidad delante (mediana): 6900 acciones
toque    0.868
cruce    0.413
cola     0.444
dtype: float64

Con el modelo por toque, el 87 % de las órdenes se ejecuta en un minuto; con el de cola, el 44 %, y con el de cruce, el 41 %. La diferencia es casi entera la cola: la mediana de lo que hay delante en el mejor bid son 6.900 acciones, treinta y tantas veces la orden propia, y en un minuto no siempre se negocian.

La selección adversa: a quién y cuándo ejecutan

El porcentaje de ejecuciones es la mitad del problema. La otra mitad es cuáles se ejecutan. El markout mide la calidad de una ejecución: dónde está el punto medio un tiempo después, comparado con el precio pagado. Para una compra, un markout negativo significa que el precio siguió cayendo después de comprar.

IN [04]python
medio = cot.set_index("ts")["medio"]
ask = cot.set_index("ts")["ask"]


def a_los(serie, ts, segundos):
    i = serie.index.searchsorted(
        ts + pd.Timedelta(seconds=segundos)) - 1
    return serie.iloc[i].to_numpy()


markout = {}
for modelo in ["toque", "cruce", "cola"]:
    f = fills[fills[modelo].notna()]
    fila = {}
    for s in [5, 30, 60, 300]:
        m = a_los(medio, f[modelo], s)
        fila[f"{s}s"] = (1e4 * (m - f["precio"]) / f["precio"]).mean()
    a = a_los(ask, f[modelo], 60)
    fila["ida_vuelta_60s"] = (1e4 * (a - f["precio"])
                              / f["precio"]).mean()
    fila["p_fill"] = len(f) / len(fills)
    markout[modelo] = fila
pd.DataFrame(markout).T.round(2)
OUT [04]Markout de las ejecuciones por modelo
         5s   30s   60s   300s  ida_vuelta_60s  p_fill
toque  0.80  0.11 -0.57  -2.74            0.39    0.87
cruce -1.14 -3.32 -5.95 -13.20           -4.98    0.41
cola  -0.94 -2.98 -5.38 -12.27           -4.40    0.44
Fig. — Markout de las compras limitadas de NOVA según el modelo de fill: distancia del punto medio al precio de compra 5, 30, 60 y 300 segundos después

En los tres modelos el markout empeora con el tiempo: las compras pasivas se llenan cuando el flujo es vendedor, y el flujo vendedor sigue. Pero el modelo por toque está mucho más arriba en todos los horizontes: incluye las ejecuciones «gratis» de los toques en que el precio rebota, que en la cola real no llegan a ejecutarse.

La columna ida_vuelta_60s es una estrategia de creador de mercado mínima: comprar en el bid y vender en el ask 60 segundos después, suponiendo (con generosidad, en los tres casos por igual) que la salida se ejecuta. Con el modelo por toque gana 0,39 pb por operación; con el de cola, pierde 4,40. El mismo código, los mismos datos y la misma salida: solo cambia la regla de entrada, y la estrategia pasa de ganar a perder. La figura de la cabecera lo resume en dos ejes, la probabilidad de ejecución y el resultado de cada ejecución.

¿Arregla el sesgo una probabilidad de fill, como el prob_fill_on_limit de NautilusTrader (lección 2.4.5)? Se puede comprobar: ejecutar al azar el 44 % de los toques, la misma proporción que da el modelo por cola (sobre todas las órdenes, el 38,9 %).

IN [05]python
rng = np.random.default_rng(2028)
p_cola = fills["cola"].notna().mean()
azar = fills[fills["toque"].notna()
             & (rng.random(len(fills)) < p_cola)]
a = a_los(ask, azar["toque"], 60)
m = a_los(medio, azar["toque"], 60)
print(f"fills: {len(azar) / len(fills):.1%}")
print(f"markout 60 s: {(1e4 * (m / azar['precio'] - 1)).mean():.2f} pb")
print(f"ida y vuelta: {(1e4 * (a / azar['precio'] - 1)).mean():.2f} pb")
OUT [05]Toques al azar con la probabilidad del modelo por cola
fills: 38.9%
markout 60 s: -0.50 pb
ida y vuelta: 0.47 pb

No lo arregla: el ida y vuelta sigue en +0,47 pb, frente a los −4,40 del modelo por cola. La probabilidad ajusta cuántas órdenes se ejecutan, pero las elige al azar entre los toques, que son las buenas y las malas mezcladas. Las reales son las malas. Para una estrategia que vive de limitadas, la probabilidad de fill aleatoria es mejor que nada para el número de operaciones y casi nada para el resultado.

Fills parciales: participar en el volumen

Una orden grande no se ejecuta en una barra. La forma habitual de modelarlo es un límite de participación: en cada barra se ejecuta como mucho una fracción del volumen negociado (aquí, el 10 %) y el resto espera a la siguiente. Con las barras de KAPA, para cada una de las 1.003 sesiones, una compra que empieza a las 10:00 y se ejecuta al cierre de cada barra:

IN [06]python
barras = pd.read_parquet("indice2_5min.parquet")
ny = barras["ts"].dt.tz_convert("America/New_York")
barras["dia"], barras["hora"] = ny.dt.date, ny.dt.strftime("%H:%M")


def participar(sesion, total, part=0.10, inicio="10:00"):
    s = sesion[sesion["hora"] >= inicio]
    llegada = s["apertura"].iloc[0]          # precio de decisión
    resto, gasto, n = total, 0.0, 0
    for b in s.itertuples():
        q = min(resto, int(part * b.volumen))
        gasto, resto, n = gasto + q * b.cierre, resto - q, n + 1
        if resto == 0:
            break
    medio = gasto / (total - resto)
    return n, resto > 0, 1e4 * (medio / llegada - 1)


tabla = {}
for total in [50_000, 500_000, 2_000_000, 10_000_000]:
    r = pd.DataFrame([participar(g, total)
                      for _, g in barras.groupby("dia")],
                     columns=["barras", "incompleta", "desl_pb"])
    tabla[f"{total:,}"] = {
        "barras_mediana": r["barras"].median(),
        "incompleta_pct": 100 * r["incompleta"].mean(),
        "desl_medio_pb": r["desl_pb"].mean(),
        "desl_desv_pb": r["desl_pb"].std()}
pd.DataFrame(tabla).T.round(2)
OUT [06]Compras de cuatro tamaños con un 10 % de participación
            barras_mediana  incompleta_pct  desl_medio_pb  desl_desv_pb
50,000                 1.0             0.0           0.59         12.62
500,000                6.0             0.0           0.75         18.21
2,000,000             31.0             0.0           1.42         30.36
10,000,000            72.0            98.4           3.23         48.43

Con 50.000 acciones la orden cabe en una barra; con dos millones tarda una mediana de 31 barras, dos horas y media, y con diez millones no se completa en el día en el 98 % de las sesiones. El deslizamiento medio frente al precio de llegada es pequeño y positivo (KAPA sube en la muestra: comprar más tarde sale algo más caro), pero su desviación típica crece con el tiempo de ejecución, de 13 a 48 pb. Repartir una orden reduce el impacto, pero la expone al mercado mientras se ejecuta: el coste se convierte en riesgo.

Un aviso sobre estos datos: los precios de KAPA no reaccionan a la orden simulada, como no reacciona ningún histórico. El modelo de participación decide cuánto y cuándo se ejecuta; el impacto que esas compras habrían causado hay que sumarlo aparte, con la ley de la raíz cuadrada de la lección 2.4.7. Y si una estrategia solo funciona con órdenes que necesitan días para ejecutarse, el problema no es el modelo de fill: es su capacidad (lección 2.2.3).

Stops y huecos

Un stop es una orden de mercado dormida (lección 1.1.4): cuando el precio llega al disparo, se ejecuta al primer precio disponible. Dentro de una sesión continua ese precio está cerca del disparo. Entre sesiones no: si el precio abre por debajo de un stop de venta, la primera operación es la apertura, y la orden se ejecuta ahí. El modelo optimista, que ejecuta siempre al precio del stop, regala la diferencia.

Con KAPA: cada día se compra al cierre y se protege la posición con un stop de venta a un porcentaje del cierre. La ejecución honrada es la apertura si la sesión abre por debajo del stop, y el stop en otro caso (en estas barras la apertura de cada barra es el cierre de la anterior, así que dentro de la sesión no hay huecos):

IN [07]python
diario = barras.groupby("dia").agg(
    apertura=("apertura", "first"), minimo=("minimo", "min"),
    cierre=("cierre", "last"))
diario["cierre_previo"] = diario["cierre"].shift()
diario = diario.dropna()

filas, huecos_1pct = [], None
for pct in [0.005, 0.01, 0.015, 0.02]:
    stop = (diario["cierre_previo"] * (1 - pct)).round(2)
    dispara = diario["minimo"] <= stop
    hueco = diario["apertura"] <= stop
    ejecucion = diario["apertura"].where(hueco, stop)
    desl = 1e4 * (stop - ejecucion) / stop
    filas.append({"stop_pct": 100 * pct, "disparos": dispara.sum(),
                  "en_hueco": hueco.sum(),
                  "desl_medio_hueco_pb": desl[hueco].mean(),
                  "desl_max_pb": desl.max(),
                  "coste_total_pb": desl.sum()})
    if pct == 0.01:
        huecos_1pct = diario[hueco].assign(stop=stop, desl_pb=desl)
pd.DataFrame(filas).set_index("stop_pct").round(1)
OUT [07]Stops de venta a distintas distancias
          disparos  en_hueco  desl_medio_hueco_pb  desl_max_pb  coste_total_pb
stop_pct
0.5            565       122                 27.9        138.1          3405.5
1.0            277        18                 27.9         88.5           502.2
1.5            127         4                 19.1         38.0            76.6
2.0             52         0                  NaN          0.0             0.0
Fig. — Los 18 huecos de apertura de KAPA, 2022-2025, que saltan un stop de venta al 1 % bajo el cierre anterior: diferencia entre el precio del stop y la apertura a la que de verdad se ejecuta

Con un stop al 1 %, 18 de los 277 disparos llegan en un hueco, y en esos el modelo optimista se ahorra una media de 28 pb por operación; en el peor (18 de mayo de 2022) son 88 pb. Cuanto más cerca está el stop, más disparos hay en hueco. Un stop al 2 % no se salta nunca en estos datos, pero la cola de los huecos de un ETF sobre un índice es corta; en una acción individual, el día después de unos resultados, el hueco puede ser de varias veces el stop (lección 1.1.4, el hueco del 5,1 % de NOVA).

IN [08]python
huecos_1pct.sort_values("desl_pb", ascending=False)[
    ["cierre_previo", "stop", "apertura", "desl_pb"]].head(4).round(2)
OUT [08]Los huecos más grandes
            cierre_previo    stop  apertura  desl_pb
dia
2022-05-18         243.12  240.69    238.56    88.50
2023-12-22         366.15  362.49    360.05    67.31
2024-02-29         372.71  368.98    366.62    63.96
2025-03-27         365.66  362.00    359.89    58.29

Hay una segunda trampa con stops sobre barras: si en la misma barra caen el stop y el objetivo de beneficio, la barra no dice cuál se tocó primero. La regla prudente es suponer que el stop (la pérdida) fue primero. NautilusTrader recorre cada barra en el orden apertura, máximo, mínimo, cierre, o con bar_adaptive_high_low_ordering=True, primero el extremo más cercano a la apertura (lección 2.4.5): las dos son convenciones, no información.

Qué modelo usar

L3L2 y cintabarrasTipo de ordenMercadoLimitadaStopPrecio y costesDatos de libroCola exactaCola aproximadaPor cruceApertura en hueco
Fuente Mermaid
flowchart TB
  O{Tipo de orden} --> M[Mercado]
  O --> L[Limitada]
  O --> S[Stop]
  M --> MP[Precio y costes]
  L --> D{Datos de libro}
  D -- L3 --> CE[Cola exacta]
  D -- L2 y cinta --> CA[Cola aproximada]
  D -- barras --> CR[Por cruce]
  S --> HA[Apertura en hueco]
SituaciónModelo razonableEn NautilusTrader 1.231
Órdenes a mercado pequeñasPrecio de la barra (o la apertura siguiente) más deslizamiento y costesFillModel(prob_slippage=…), FeeModel
Órdenes a mercado grandesParticipación en el volumen más impactoLibros sintéticos (ThreeTierFillModel…) o un modelo propio
Limitadas con datos L3Cola exactabook_type=BookType.L3_MBO y datos de libro
Limitadas con L2 y operacionesCola aproximadatrade_execution=True, queue_position=True
Limitadas con solo barrasPor cruce (y aceptar que es pesimista en cantidad)Sin equivalente directo: prob_fill_on_limit ajusta la cantidad, no la selección
StopsApertura si hay hueco; el stop primero si hay dudaorder_factory.stop_market con barras, bar_adaptive_high_low_ordering

La regla general es elegir el modelo pesimista que los datos permitan y comprobar cuánto cambia el resultado con el optimista. Si la estrategia solo funciona con el optimista, no funciona. En el registro de experimentos (lección 2.2.4), el modelo de fill va junto a los costes: dos backtests con distinto modelo son dos experimentos distintos.

Ejercicios

  1. Repite el estudio de los tres modelos para ventas limitadas en el mejor ask (lado == "ask", ejecución con operaciones a ese precio o más). ¿Se mantiene la conclusión?
Solución
IN [09]python
mejor_ask = l2[(l2["nivel"] == 1) & (l2["lado"] == "ask")]
mejor_ask = mejor_ask[mejor_ask["ts"].between(
    "2025-06-02 13:35", "2025-06-02 19:50")]
bid = cot.set_index("ts")["bid"]
res = {"toque": [], "cola": []}
for foto_l2 in mejor_ask.itertuples():
    p, delante = foto_l2.precio, foto_l2.tamano
    cinta = tramo(op, foto_l2.ts, foto_l2.ts + H)
    libro = tramo(con_libro, foto_l2.ts, foto_l2.ts + H)
    toque = cinta.loc[cinta["precio"] >= p, "ts"].min()
    cruce = libro.loc[libro["precio"] > p, "ts"].min()
    en_p = libro["tamano"].where(libro["precio"] == p, 0).cumsum()
    cola = libro.loc[en_p >= delante + TAM, "ts"].min()
    cola = min([t for t in [cola, cruce] if pd.notna(t)],
               default=pd.NaT)
    for modelo, t in [("toque", toque), ("cola", cola)]:
        if pd.notna(t):
            b = a_los(bid, pd.Series([t]), 60)[0]
            # vende en el ask, recompra en el bid
            res[modelo].append(1e4 * (p - b) / p)
for modelo, v in res.items():
    print(f"{modelo:6} p_fill {len(v) / len(mejor_ask):.1%}",
          f"· ida y vuelta 60 s {np.mean(v):.2f} pb")
OUT [09]Ventas limitadas en el ask
toque  p_fill 84.9% · ida y vuelta 60 s 0.88 pb
cola   p_fill 43.1% · ida y vuelta 60 s -4.20 pb

La conclusión se mantiene en el otro lado del libro: el modelo por toque ejecuta casi todas las ventas y les da un resultado mucho mejor que el de cola, que solo llena las que el precio atraviesa o que agotan la cola, en los momentos de flujo comprador. La selección adversa es simétrica.

  1. En el estudio de los stops, calcula el coste total del modelo optimista en USD para una posición de 1.000 acciones con el stop al 1 %, y compáralo con lo que costaría una comisión de 1 USD por orden en los 277 disparos.
Solución
IN [10]python
desl_usd = 1000 * (huecos_1pct["stop"] - huecos_1pct["apertura"])
print(f"deslizamiento escondido: {desl_usd.sum():,.0f} USD",
      f"en {len(huecos_1pct)} huecos")
print(f"comisiones de 277 ventas a 1 USD: {277 * 1:,} USD")
OUT [10]Lo que esconde el modelo optimista
deslizamiento escondido: 15,940 USD en 18 huecos
comisiones de 277 ventas a 1 USD: 277 USD

El deslizamiento que esconde el modelo optimista en solo 18 huecos, 15.940 USD, es más de cincuenta veces lo que costarían las comisiones de todas las ventas (277 USD). Es la diferencia entre modelar un coste que se ve en la tarifa y uno que solo aparece cuando el mercado salta.

  1. Un compañero presenta una estrategia de reversión a la media en barras diarias que compra con limitadas un 1 % por debajo del cierre y vende al cierre siguiente, con ejecución «si el mínimo del día toca el precio». ¿Qué tres cambios le pedirías en el backtest antes de discutir el resultado?
Solución

Primero, el modelo de fill: ejecutar solo si el mínimo queda al menos un tick por debajo del límite (cruce), y comparar el resultado con el del toque; si la diferencia es grande, la estrategia vive de los toques que la cola real no habría ejecutado. Segundo, los huecos: si la sesión abre por debajo del límite, la compra se ejecuta a la apertura (mejor precio que el límite, pero en el peor momento), y eso hay que tratarlo igual que un stop. Tercero, el tamaño: con qué fracción del volumen opera y qué impacto tendría, porque una compra limitada grande no se llena entera al tocar el precio. Y, si es posible, contrastarlo con datos L2 o de operaciones en una muestra para medir la selección adversa, como en la sesión de NOVA.

Respuesta plegada en cada pregunta

Autoevaluación

  1. Un backtest con barras da por ejecutada una compra limitada a 53,08 en cuanto el mínimo de la barra toca 53,08. ¿Qué supone sin decirlo?

    • AQue la orden era la primera de la cola en ese precio
    • BQue el precio atraviesa la orden
    • CQue hay impacto de mercado
    • DNada: es el modelo exacto
    Ver respuesta

    Respuesta A. Que la orden era la primera de la cola en ese precio

    Que el precio llegue a tu nivel no basta: antes se ejecutan las órdenes que estaban delante en la cola. En la sesión de NOVA, la orden de compra a 53,08 de las 10:00 se habría «ejecutado» por toque en medio segundo; con la cola real, delante había 5.966 acciones y el precio se fue sin llegar a ejecutarla.

  2. En la sesión de NOVA, una compra limitada en el bid se ejecuta en 60 segundos el 87 % de las veces por toque y el 44 % por cola. ¿Qué pasa con el resultado medio de cada ejecución?

    • AEs el mismo en los dos modelos
    • BEs peor en el modelo por cola: las ejecuciones que solo ocurren cuando la cola se agota son las de los momentos en que el precio sigue cayendo
    • CEs mejor en el modelo por cola
    • DDepende de la comisión
    Ver respuesta

    Respuesta B. Es peor en el modelo por cola: las ejecuciones que solo ocurren cuando la cola se agota son las de los momentos en que el precio sigue cayendo

    Es la selección adversa: una compra pasiva se llena cuando muchos venden. A 60 segundos, el punto medio está 0,57 pb por debajo del precio de compra en el modelo por toque y 5,38 pb en el modelo por cola.

  3. ¿Por qué no basta con ejecutar al azar un 44 % de los toques (como hace prob_fill_on_limit=0.44 en NautilusTrader con barras) para corregir el sesgo del modelo por toque?

    • APorque 44 % es poco
    • BPorque reduce el número de ejecuciones, pero elige al azar cuáles: conserva la calidad optimista de las del toque, que no tienen la selección adversa de las reales
    • CPorque NautilusTrader no admite ese parámetro
    • DSí basta
    Ver respuesta

    Respuesta B. Porque reduce el número de ejecuciones, pero elige al azar cuáles: conserva la calidad optimista de las del toque, que no tienen la selección adversa de las reales

    El problema del toque no es solo cuántas órdenes se llenan, sino cuáles. Las que de verdad se ejecutan son las peores. En NOVA, el 44 % aleatorio de los toques conserva un resultado de ida y vuelta positivo, mientras que el modelo por cola lo da claramente negativo.

  4. Un stop de venta a 368,98 en KAPA se dispara el 29 de febrero de 2024 porque la sesión abre a 366,62. ¿A qué precio es honrado ejecutarlo en el backtest?

    • AA 368,98, el precio del stop
    • BA la apertura, 366,62, porque el precio saltó por encima del stop sin negociarse en él
    • CAl mínimo del día
    • DAl cierre del día
    Ver respuesta

    Respuesta B. A la apertura, 366,62, porque el precio saltó por encima del stop sin negociarse en él

    Un stop es una orden de mercado dormida: cuando se dispara se ejecuta al primer precio disponible. Si la sesión abre por debajo del stop, ese precio es la apertura. Suponerlo a 368,98 ahorra 64 pb que no existen.

  5. Una compra de 2 millones de acciones de KAPA con un límite de participación del 10 % del volumen tarda una mediana de 31 barras. ¿Qué cambia frente a ejecutarla de golpe?

    • ANada
    • BSe reduce el impacto, pero el precio medio queda expuesto a lo que haga el mercado durante dos horas y media: la desviación típica del deslizamiento frente al precio de llegada sube a 30 pb
    • CSe garantiza el precio de llegada
    • DAumenta la comisión
    Ver respuesta

    Respuesta B. Se reduce el impacto, pero el precio medio queda expuesto a lo que haga el mercado durante dos horas y media: la desviación típica del deslizamiento frente al precio de llegada sube a 30 pb

    Repartir la orden convierte una parte del coste (el impacto) en riesgo de ejecución. En estos datos sintéticos los precios no reaccionan a la orden, así que la simulación solo mide ese riesgo; el impacto se añade con el modelo de la lección 2.4.7.

Ideas clave

  • El modelo de fill es un supuesto del backtest, como el de costes, y hay que declararlo: mercado al precio de la barra, limitada por toque, por cruce, por cola o probabilística.
  • Una orden limitada se ejecuta cuando se agota la cola que tiene delante; sin datos L3 se aproxima con el tamaño del nivel y el volumen negociado en él.
  • Las limitadas sufren selección adversa: se llenan cuando el precio va en contra. En NOVA, el modelo por toque convierte un ida y vuelta pasivo de −4,4 pb en uno de +0,4 pb.
  • Una probabilidad de fill aleatoria reduce cuántas órdenes se llenan, pero no cuáles: no corrige la selección adversa.
  • Los fills parciales con límite de participación reducen el impacto y añaden riesgo de ejecución; los stops se ejecutan a la apertura cuando el precio salta por encima de ellos.

Recursos