---
title: "2.4.8 Modelado de fills: limitadas, cola, huecos y stops"
description: "Cuándo se ejecuta de verdad una orden en un backtest: limitadas por toque, por cruce y por cola, selección adversa, fills parciales y stops en huecos."
url: "https://pythonparatrading.com/curso-python-trading/nivel-2/modelado-de-fills/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-2/modelado-de-fills.png"
---

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

- Curso: [Nivel 2: Estrategias sistemáticas y backtesting riguroso](https://pythonparatrading.com/curso-python-trading/nivel-2/)
- Módulo: 2.4 · Backtesting: motores y paradigmas
- Duración: 65 min
- Requisitos: [1.1.3 · Libro de órdenes: bid, ask, spread y profundidad](https://pythonparatrading.com/curso-python-trading/nivel-1/libro-de-ordenes-bid-ask-y-profundidad/), [1.1.4 · Tipos de órdenes](https://pythonparatrading.com/curso-python-trading/nivel-1/tipos-de-ordenes/), [1.1.5 · Costes, liquidación, apalancamiento y margen](https://pythonparatrading.com/curso-python-trading/nivel-1/costes-liquidacion-apalancamiento-y-margen/)

## Al terminar esta lección

- Distinguir los modelos de ejecución de una orden limitada (por toque, por cruce, por posición en la cola, probabilístico) y saber qué datos exige cada uno.
- Simular la posición en la cola de una orden limitada con mensajes L3 y, de forma aproximada, con fotos L2 y operaciones.
- Medir la selección adversa con el markout y cuantificar cuánto infla el modelo por toque el resultado de una estrategia con limitadas.
- Modelar fills parciales con un límite de participación en el volumen y explicar por qué convierten un coste en un riesgo.
- Ejecutar stops de forma honesta en los huecos de apertura y medir el deslizamiento que esconde el modelo optimista.

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

| Modelo | Regla para una compra limitada a $L$ | Datos que necesita | Sesgo |
|---|---|---|---|
| Por toque | Se ejecuta si el precio negocia a $L$ o menos | Barras u operaciones | Optimista: supone ser el primero de la cola |
| Por cruce | Solo si el precio negocia por debajo de $L$ | Barras u operaciones | Pesimista en cantidad; capta la selección adversa |
| Por cola | Cuando se agota la cantidad que había delante a $L$ | L3, o L2 más operaciones | El más realista; exige datos de libro |
| Probabilístico | Por toque, con probabilidad $p$ | Barras | Ajusta cuántas, no cuáles |
| Participación | Como mucho una fracción del volumen de cada barra | Volumen | Fills 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.

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

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

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

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

```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)
```
```output
         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.* Tres líneas en escala logarítmica de tiempo. Las tres bajan con el horizonte: las compras pasivas se llenan cuando el precio cae. Por toque (destacado) va de +0,80 pb a 5 s a −2,74 a 300 s; por cola, de −0,94 a −12,27; por cruce, de −1,14 a −13,20.

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

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

```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)
```
```output
            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):

```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)
```
```output
          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.* Barras verticales por fecha, de enero de 2022 a mayo de 2025. La mayoría están entre 0,5 y 30 pb; cinco superan los 40: 41,6 (enero de 2022), 88,5 (18 de mayo de 2022, destacada), 67,3 (diciembre de 2023), 64,0 (febrero de 2024) y 58,3 (marzo de 2025). Media: 27,9 pb.

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

```python
huecos_1pct.sort_values("desl_pb", ascending=False)[
    ["cierre_previo", "stop", "apertura", "desl_pb"]].head(4).round(2)
```
```output
            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

```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ón | Modelo razonable | En NautilusTrader 1.231 |
|---|---|---|
| Órdenes a mercado pequeñas | Precio de la barra (o la apertura siguiente) más deslizamiento y costes | `FillModel(prob_slippage=…)`, `FeeModel` |
| Órdenes a mercado grandes | Participación en el volumen más impacto | Libros sintéticos (`ThreeTierFillModel`…) o un modelo propio |
| Limitadas con datos L3 | Cola exacta | `book_type=BookType.L3_MBO` y datos de libro |
| Limitadas con L2 y operaciones | Cola aproximada | `trade_execution=True`, `queue_position=True` |
| Limitadas con solo barras | Por cruce (y aceptar que es pesimista en cantidad) | Sin equivalente directo: `prob_fill_on_limit` ajusta la cantidad, no la selección |
| Stops | Apertura si hay hueco; el stop primero si hay duda | `order_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.**

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

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

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

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

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

   - A. Que la orden era la primera de la cola en ese precio
   - B. Que el precio atraviesa la orden
   - C. Que hay impacto de mercado
   - D. Nada: es el modelo exacto

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

   - A. Es el mismo en los dos modelos
   - 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
   - C. Es mejor en el modelo por cola
   - D. Depende de la comisión

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

   - A. Porque 44 % es poco
   - 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
   - C. Porque NautilusTrader no admite ese parámetro
   - D. Sí basta

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

   - A. A 368,98, el precio del stop
   - B. A la apertura, 366,62, porque el precio saltó por encima del stop sin negociarse en él
   - C. Al mínimo del día
   - D. Al cierre del día

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

   - A. Nada
   - 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
   - C. Se garantiza el precio de llegada
   - D. Aumenta la comisión

   *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

### Research

- [Liquidez Order Book - Trading Liquidez Agregada](https://pythonparatrading.com/limit-order-book-y-liquidez-de-mercado/) · Research-0008 · 11 min

### Documentación

- [NautilusTrader: modelos de ejecución (prob_fill_on_limit, prob_slippage, libros sintéticos)](https://nautilustrader.io/docs/latest/concepts/backtesting/fill-models/) · nautilustrader.io
- [NautilusTrader: ejecución con operaciones y seguimiento de la posición en la cola](https://nautilustrader.io/docs/latest/concepts/backtesting/trade-execution/) · nautilustrader.io

### Libros

- [Bouchaud, Bonart, Donier y Gould (2018): Trades, Quotes and Prices. Cambridge University Press](https://doi.org/10.1017/9781316659335) · doi.org

### Datos

- [Datos del nivel 1: mensajes L3 del libro de NOVA (Parquet)](https://pythonparatrading.com/curso/datos/nivel-1/nova_libro_l3.parquet) · Descarga · nova_libro_l3.parquet
- [Datos del nivel 2: barras de 5 minutos del ETF KAPA (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/indice2_5min.parquet) · Descarga · indice2_5min.parquet

Lección anterior: [2.4.7 · Modelado de costes: comisiones, horquilla, impacto, financiación y préstamo de títulos](https://pythonparatrading.com/curso-python-trading/nivel-2/modelado-de-costes/)

Lección siguiente: [2.5.1 · Sesgos de backtest: anticipación, supervivencia, selección y narrativa](https://pythonparatrading.com/curso-python-trading/nivel-2/sesgos-de-backtest/)
