Curvas de slippage, no constantes de slippage: modelos de costos que sobreviven al contacto con el trading en vivo
La línea más costosa en la mayoría de los backtests es slippage_bps = 5. No porque 5 sea el número equivocado —promediado sobre un año de fills incluso podría ser correcto— sino porque una constante está equivocada condicionalmente, y tu proceso de selección de estrategias es una máquina para encontrar y explotar errores condicionales. El slippage es una función del tamaño de la orden, el spread, la volatilidad y la liquidez disponible. Una constante es esa función evaluada en un punto y luego extrapolada a todas partes, incluida la cascada de liquidaciones de las 3 de la madrugada donde tu señal de reversión a la media se dispara con más fuerza y el libro tiene una décima parte de su profundidad habitual. El backtest cobra 5 bps ahí también. El trading en vivo no lo hará.
Este artículo trata sobre el modelo de costos dentro del bucle del backtest: la función cost(size, market_state) que se resta de cada operación simulada. Deliberadamente no trata sobre predecir el impacto ex ante para programar una ejecución —ese es el problema complementario, cubierto desde el lado del modelo en modelado de impacto de precio y desde el lado de la programación óptima en Almgren-Chriss. Aquí la pregunta es más acotada y, para cualquiera que seleccione estrategias mediante backtest, más urgente: qué función debería ocupar el lugar donde actualmente está tu constante, cómo la ajustas a partir de los datos que realmente tienes, y cuánto cambia la respuesta la estrategia que terminas ejecutando. Esta última parte tiene una respuesta empírica contundente: reordena 20 variantes de una misma familia de estrategias bajo cuatro modelos de costos y observa cómo se invierte el ranking.
La escalera de modelos de costos

Al igual que la escalera de simulación de fills, los modelos de costos forman una escalera de fidelidad donde cada peldaño requiere más datos y elimina un sesgo sistemático específico. Los peldaños son acumulativos: cada uno conserva los términos del anterior y agrega uno.
M0: bps constantes
Un solo número, aplicado a cada operación. Los tres modos de fallo, en orden creciente de daño:
- Ciego al tamaño. Un clip de 5M pagan lo mismo. Cualquier comparación de estrategias entre distintos niveles de capacidad carece de sentido.
- Ciego al régimen. Un martes tranquilo por la tarde y el colapso de FTX pagan lo mismo. El error no es ruido; está anticorrelacionado precisamente con los estados en los que las estrategias impulsadas por eventos concentran su operativa.
- Tóxico para la selección. Cuando barres 200 combinaciones de parámetros, el optimizador descubre que bajo un costo plano, el turnover es barato en relación con la realidad, y desplaza toda la búsqueda hacia variantes de alta frecuencia. El error de costo se convierte en una fuerza direccional sobre la selección del modelo, no en una fuerza simétrica sobre el PnL.
M1: proporcional al spread
Cobra el semi-spread medido en el timestamp de la decisión (más la comisión, que es determinista y siempre debería ser su propia línea). Este es el primer modelo que responde al mercado de alguna forma: los spreads se ensanchan con las noticias, en las aperturas de sesión, en altcoins ilíquidas, así que el costo ahora se mueve junto con al menos una variable de liquidez. Lo que aún omite: el spread cotiza el fill de un solo clip en el tope del libro. Cualquier cosa más grande recorre el libro, y ese recorrido es invisible para .
M2: escalado por volatilidad
Agrega un término proporcional a la volatilidad de corto horizonte. La justificación es tanto empírica como estructural: los spreads y la profundidad los fijan market makers cuyo riesgo de selección adversa e inventario escala con (la lógica de cotización de Avellaneda-Stoikov ejecutada a la inversa), así que el costo de demandar liquidez está naturalmente denominado en unidades de volatilidad. Cuando no tienes datos de libro en absoluto —barras diarias, historial largo— con alrededor de 0.05–0.1 de la volatilidad diaria ya es un modelo dramáticamente mejor que cualquier constante, porque al menos respira con el régimen.
M3: raíz cuadrada en la participación
El término de tamaño. es el tamaño de tu orden, el volumen de mercado en la ventana relevante, la volatilidad en esa misma ventana. Esta forma funcional no es una conveniencia; es uno de los resultados empíricos más replicados en la microestructura de mercado, y vale la pena precisar de dónde proviene.
El punto de partida teórico es Kyle (1985), "Continuous auctions and insider trading" (Econometrica 53(6), 1315–1335). En el modelo de Kyle, un market maker fija el precio como una función lineal del flujo neto de órdenes, , con
— impacto por unidad de flujo proporcional a la incertidumbre de valor sobre el volumen de trading no informado. La lambda de Kyle le dio al campo su unidad de liquidez (la profundidad es ) y su primera afirmación contrastable: el costo por dólar operado depende de la relación entre información y liquidez, no de una constante. Pero el modelo es lineal en el tamaño, y los datos dicen lo contrario.
Los datos dicen cóncavo. Almgren, Thum, Hauptmann y Li (2005), "Direct estimation of equity market impact" (Risk, julio de 2005), ajustaron aproximadamente 700.000 órdenes de acciones estadounidenses de las mesas de Citigroup y encontraron que el impacto temporal crece como la participación elevada a la potencia —probaron explícitamente y rechazaron tanto el modelo lineal como la raíz cuadrada pura en favor de — mientras que el impacto permanente resultó en , estadísticamente indistinguible de lineal (lo cual es tranquilizador, ya que Huberman y Stanzl (2004) demostraron que un impacto permanente no lineal admite manipulación). Tóth et al. (2011), "Anomalous price impact and the critical nature of liquidity in financial markets" (Physical Review X 1, 021006), usando casi 500.000 metaórdenes de Capital Fund Management en mercados de futuros, encontraron con de orden uno —y aportaron la explicación hoy estándar: el libro visible es una fracción evanescente de la liquidez latente, que es localmente lineal en precio alrededor del mid, lo que hace que el costo de consumirla escale como raíz cuadrada. Trabajos posteriores refinan los bordes: Zarinelli, Treccani, Farmer y Lillo (2015), "Beyond the square root" (Market Microstructure and Liquidity 1(2)), mostraron sobre siete millones de metaórdenes institucionales de ANcerno que la raíz cuadrada ajusta bien en cerca de dos décadas de tamaño de orden, pero una forma logarítmica se extiende a lo largo de cinco; Bucci et al. (2019, Physical Review Letters 122, 108302) documentaron la transición de un impacto lineal (al estilo Kyle) en participaciones muy pequeñas a raíz cuadrada por encima de ese umbral. Y Frazzini, Israel y Moskowitz (2018), "Trading Costs" (SSRN 3229719), usando 1.7 billones de dólares en ejecuciones reales de AQR a lo largo de 19 años, encontraron costos realizados un orden de magnitud menores que las estimaciones académicas previas —una advertencia de que los coeficientes ajustados sobre el flujo de otros, con distinta urgencia y distinto contenido informativo, no se trasladan al tuyo.
Para el bucle del backtest, el resumen práctico son tres afirmaciones. El impacto escala con la volatilidad. El impacto es cóncavo en el tamaño, con un exponente que deberías tratar como hasta que tus propios datos argumenten por . El prefactor es de orden uno pero varía según el venue, el activo y —de manera crítica— tu propio estilo de trading, razón por la cual la siguiente sección trata sobre ajustarlo en lugar de copiarlo. La calibración profunda (kernels de decaimiento, descomposición transitorio vs. permanente, impacto cruzado) pertenece al artículo de modelado de impacto; el backtest solo necesita la curva.
Un número trabajado, para fijar magnitudes. BTC perp, volatilidad diaria bps, volumen diario del venue V_d = \6Y = 0.75\delta = 0.5$:
| Tamaño de orden | Término de impacto | + semi-spread (0.5 bp) | ||
|---|---|---|---|---|
| $50k | 0.0008% | 0.0029 | 0.5 bp | 1.0 bp |
| $500k | 0.008% | 0.0091 | 1.7 bp | 2.2 bp |
| $5M | 0.083% | 0.0289 | 5.4 bp | 5.9 bp |
| $50M | 0.83% | 0.0913 | 17.1 bp | 17.6 bp |
Tres órdenes de magnitud en tamaño abarcan un factor de ~18 en costo. Ninguna constante cubre esa tabla, y cada comparación de estrategias que varía el tamaño de posición mientras mantiene fijo slippage_bps está comparando en silencio manzanas a 5M.
Ajustando la curva: tus propios fills, o los de nadie
A partir de tus propios fills
El dato correcto es tu propio registro de TCA: una fila por orden madre con el mid de llegada, el VWAP de fill realizado, el tamaño de la orden y el estado del mercado en el momento de la decisión —exactamente el dataset que la medición de implementation shortfall produce como subproducto. El shortfall en bps contra la llegada, normalizado por la volatilidad contemporánea, regresado sobre la participación, te da y directamente.
La trampa es el ruido. El shortfall de una sola orden es impacto más ruido de precio durante la ventana de ejecución, y el ruido domina: para una ejecución de 10 minutos con volatilidad diaria de 250 bps, el término de ruido es bps frente a una señal de impacto de quizás 3 bps. Una relación señal-ruido de 0.15 por observación es la razón por la que Almgren et al. necesitaron 700 mil órdenes y por la que debes ajustar sobre medianas por bucket, nunca sobre puntos crudos:
import numpy as np
import pandas as pd
def fit_sqrt_curve(orders: pd.DataFrame) -> tuple[float, float]:
"""orders: one row per parent order.
is_bps -- implementation shortfall vs arrival mid, bps, cost-positive
sigma_bps -- volatility at arrival (same window used at simulation time)
q_over_v -- parent size / market volume over the execution window
Fits I = Y * sigma * (Q/V)^delta on participation-bucket medians."""
df = orders.query("q_over_v > 0 and sigma_bps > 0").copy()
df["i_norm"] = df.is_bps / df.sigma_bps # impact in vol units
df["bucket"] = pd.qcut(np.log10(df.q_over_v), 12, duplicates="drop")
b = (df.groupby("bucket", observed=True)
.agg(i=("i_norm", "median"), qv=("q_over_v", "median")))
b = b[b.i > 0] # noisy buckets can go negative; drop, don't clip
delta, log_y = np.polyfit(np.log(b.qv), np.log(b.i), 1)
return float(np.exp(log_y)), float(delta)
Sobre unas pocas miles de órdenes madre de cripto, este ajuste ya es estable dentro de aproximadamente ±30% en y ±0.1 en —tosco, pero la diferencia entre una curva ajustada y una constante adivinada no es del 30%, es la forma entera. Reajusta trimestralmente; la deriva en es en sí misma un diagnóstico (un creciente con participación constante significa que el régimen de liquidez del venue cambió o que tu flujo de órdenes se volvió más informado/más detectable).
Arranque en frío: aún sin fills
Antes de la primera orden en vivo solo tienes datos públicos, y dos opciones honestas.
Recorrer el libro por profundidad. Promedia el costo de recorrer instantáneas L2 en cada tamaño, exactamente como hace el simulador de fills por operación, pero agregado en una curva. Esto es una cota inferior con un signo conocido: la instantánea muestra la liquidez que sobrevivió al flujo de todos los demás, la dinámica de reposición es invisible, y tus propias órdenes hijas anteriores habrán movido el libro antes de que lleguen las posteriores.
def curve_from_book(snapshots, sizes_usd, safety=1.75):
"""snapshots: iterable of (mid, asks), asks = [(price, qty), ...] best-first.
Depth-walk cost is a LOWER bound on realized cost -- scale it."""
rows = []
for q_usd in sizes_usd:
costs = []
for mid, asks in snapshots:
rem, paid, got = q_usd / mid, 0.0, 0.0
for p, q in asks:
x = min(q, rem)
paid += x * (p - mid); got += x; rem -= x
if rem <= 0:
break
if rem <= 0:
costs.append(paid / got / mid * 1e4)
rows.append((q_usd, safety * float(np.median(costs))))
return pd.DataFrame(rows, columns=["q_usd", "cost_bps"])
Rastrear la huella de operaciones públicas. Agrupa las operaciones agresoras por tamaño, mide el movimiento del mid unos segundos después de cada una, y extrae de ahí una curva de impacto empírica. Esto recupera la forma cóncava y, aproximadamente, el escalado correcto en , pero el nivel está sesgado en una dirección que no puedes determinar a priori: los prints públicos son decisiones de otros traders, así que los prints grandes están condicionalmente informados (sesgando el impacto medido hacia arriba en relación con tu rebalanceo no informado) mientras que los traders más sensibles a la liquidez dividen sus órdenes precisamente para esconderse (sesgándolo hacia abajo). Úsala para la forma, no para el nivel.
De cualquier forma, el protocolo de arranque en frío es: toma la curva de datos públicos, aplica un factor de seguridad de 1.5–2, opera en tamaños pequeños, y empieza a llenar la tabla de TCA que la reemplazará. La curva pública es andamiaje, no una pared de carga.
Dependencia del régimen: la misma orden, cinco veces el costo

Cada término en depende del estado, y en un pico de volatilidad todos se mueven en tu contra al mismo tiempo. Los spreads son proporcionales a la volatilidad de corto horizonte, así que se ensancha 5–10x. entra directamente en el término de impacto. El volumen también sube —lo que ingenuamente reduciría la participación— pero sube menos que en el numerador, y esa compensación esconde un efecto peor: la profundidad cotizada cerca del mid colapsa mucho más rápido que el volumen operado, así que la curva realizada se empina más allá de lo que predice la fórmula ajustada a partir de los insumos estresados por sí solos.
Concretamente, con la curva ajustada anterior, una orden de $3M en BTC:
- Día calmo: bps, spread 1 bp, V_d = \6= 0.5 + 0.75 \times 150 \times \sqrt{0.0005} = 0.5 + 2.5 = 3.0$ bps.
- Día de crisis (nivel LUNA, FTX): bps, spread 6 bps, V_d = \18= 3.0 + 0.75 \times 700 \times \sqrt{0.000167} = 3.0 + 6.8 = 9.8$ bps.
La fórmula dice 3.3x. El TCA realizado en días de estrés consistentemente supera a la fórmula —el colapso de profundidad que el término de raíz cuadrada no ve— y ahí es donde corresponde un multiplicador de estrés medido. En nuestros fills, el residual del bucket de crisis corre entre 1.4 y 1.8x por encima de la fórmula con insumos estresados, situando el costo total de estrés en 4.5–6x el costo calmo para la misma orden. Ese es el origen honesto de "la misma orden cuesta 5x más en un pico de volatilidad": aproximadamente 2x de neto de volumen, 2x del spread, y el resto de la profundidad evaporándose más rápido de lo que admiten las estadísticas de volumen.
La implementación es un modelo de costos condicional: ajusta la curva por bucket de régimen, o equivalentemente ajusta una curva y una tabla de multiplicadores indexada por un cuantil de volatilidad realizada calculado en el momento de la decisión (solo ventana rezagada — una etiqueta de régimen calculada sobre la muestra completa es look-ahead infiltrado a través del modelo de costos):
| Régimen (cuantil de volatilidad de 1h rezagada) | Frecuencia | Mult. spread | Mult. curva (medido) |
|---|---|---|---|
| Tranquilo (< p25) | 25% | 0.6x | 0.7x |
| Normal (p25–p75) | 50% | 1.0x | 1.0x |
| Elevado (p75–p95) | 20% | 1.8x | 1.6x |
| Estrés (> p95) | 5% | 5–10x | 3–6x |
Frazzini, Israel y Moskowitz (2018) documentan el mismo fenómeno en sus datos institucionales: los costos realizados se mueven fuertemente junto con la volatilidad contemporánea, y un modelo de costos sin un término de volatilidad variable en el tiempo tasa mal precisamente las colas. La razón por la que esta tabla importa más de lo que sugiere su fila de estrés del 5% es la correlación: las estrategias no muestrean estas filas de manera uniforme. Un sistema de ruptura de volatilidad hace la mayor parte de su operativa en las dos filas inferiores. Un market maker de mercado calmo gana en la fila superior y se detiene atravesando la inferior. Ponderar las operaciones de cada estrategia por su propio histograma de régimen —en lugar de cobrarle a todos el promedio incondicional— es la corrección de PnL más grande de toda esta página para los sistemas impulsados por eventos, más valiosa que la diferencia entre dos peldaños adyacentes cualesquiera de la escalera.
El experimento de sensibilidad: cuatro modelos de costos, un ranking reordenado
Si el modelo de costos cambiara el PnL en un desplazamiento constante, nada de esto importaría para la selección —cada variante se movería por igual y el argmax sobreviviría. No es así, porque la sensibilidad al costo varía entre variantes en un orden de magnitud. El experimento que hace esto concreto:
Configuración. Una familia de estrategias —reversión a la media por z-score y ruptura de Donchian en perps de BTC y ETH, barras de 1 minuto, 18 meses— con una grilla sobre el horizonte de tenencia (15m / 1h / 4h / 24h) y el umbral de entrada: 20 variantes, con turnover unidireccional de 0.4x a 11x el book por día, clips de $250k ejecutados en 5 minutos. Cada variante se backtestea una vez por modelo de costos:
- M0: solo comisiones;
- M1: comisiones + 5 bps constantes;
- M2: comisiones + semi-spread + ;
- M3: comisiones + curva ajustada , con la tabla de multiplicadores de régimen de arriba.
Mismos fills, mismas señales, mismo camino de código —el modelo de costos se pasa al backtester como un argumento del constructor, tal como cualquier parámetro de primera clase:
from dataclasses import dataclass
@dataclass(frozen=True)
class SqrtCost:
y: float = 0.75 # fitted impact coefficient
delta: float = 0.5 # fitted exponent
fee_bps: float = 2.5 # taker fee, always separate
def cost_bps(self, q_usd: float, st) -> float:
part = q_usd / st.window_volume_usd
return (self.fee_bps + st.stress_mult *
(st.spread_bps / 2 + self.y * st.sigma_bps * part ** self.delta))
for model in [ZeroCost(), ConstCost(5.0), VolCost(0.08), SqrtCost()]:
for variant in grid:
results[(model, variant)] = backtest(variant, cost_model=model)
Resultados (corrida representativa; retornos netos anualizados):
| Variante | Turnover/día | M0 | M1 (5 bp) | M2 | M3 (curva) | Rank M1 → M3 |
|---|---|---|---|---|---|---|
| MR-15m, z=1.0 | 11.4x | +187% | +61% | +9% | −14% | 1 → 17 |
| MR-15m, z=1.5 | 7.9x | +141% | +52% | +15% | −2% | 2 → 13 |
| BO-1h, k=2 | 3.1x | +74% | +43% | +28% | +19% | 3 → 4 |
| MR-1h, z=2.0 | 2.4x | +58% | +38% | +25% | +21% | 4 → 2 |
| BO-4h, k=3 | 0.9x | +39% | +33% | +29% | +26% | 7 → 1 |

Correlaciones de rango (Kendall ) entre rankings: —una constante apenas reordena nada, es un impuesto, no un modelo. —seis de las diez mejores de M1 abandonan el top diez bajo M3, y la campeona de M1 pasa de +61% a negativo. —el término de volatilidad por sí solo recupera la mayor parte del reordenamiento; el término de tamaño termina el trabajo para las variantes que operan grande en relación con el volumen de la ventana. El mecanismo es exactamente la toxicidad de selección del peldaño M0: un 5 bps plano subsidia las operaciones que en realidad cuestan 15 bps (grandes, rápidas, en estrés) y sobrecarga las que cuestan 2 (pequeñas, pacientes, en calma), así que rankear por M1 promueve sistemáticamente a las variantes cuyos costos verdaderos están más subestimados. El optimizador no encontró alfa; encontró el término de error de tu modelo de costos.
Por eso el modelo de costos pertenece a la misma categoría mental que el split walk-forward o la función objetivo: un parámetro de backtest de primera clase que varías, no una constante ambiental que fijas una vez. Si el ranking de una variante es estable entre M2 y M3, su edge es real en relación con los costos. Si su ranking colapsa al subir por la escalera, aprendiste que era un artefacto del modelo de costos —el mismo evento epistémico que una estrategia muriendo fuera de muestra en la optimización walk-forward, y merece la misma respuesta. Y si seleccionaste el argmax de 200 variantes bajo M1, la lógica del Sharpe deflactado aplica con un giro: ejecutaste una búsqueda de testeo múltiple donde el propio estadístico de prueba estaba sesgado a favor de las entradas de alta frecuencia.
Cotas honestas: el PnL como banda
La disciplina final se deriva de admitir que el modelo de costos es incierto incluso después de ajustarlo. tiene un intervalo de confianza, los multiplicadores de estrés se estiman a partir de un puñado de días de cola, y el futuro trae sus propios regímenes. Así que deja de reportar el PnL neto como un número. Repórtalo como una banda a través de escenarios de costos nombrados:
- Optimista: cota inferior del recorrido de profundidad, sin multiplicadores de estrés. Nunca reportar sola; su único trabajo es acotar el mejor caso.
- Realista: curva ajustada, multiplicadores de régimen ajustados —tu estimación central.
- Pesimista: , spread , fila de estrés siempre aplicada en su valor p75. Aproximadamente: la curva ajustada evaluada en el borde desfavorable del intervalo de confianza de cada parámetro.
Dos reglas de decisión hacen operativa la banda. Primera: asignar capital según el pesimista, reportar el realista. Una estrategia que solo sobrevive bajo el modelo realista es una apuesta a que tu ajuste de costos no tiene error en la dirección costosa —una apuesta que no hiciste conscientemente. Segunda: seguir la relación de fragilidad —ancho de la banda sobre PnL bruto. Del experimento anterior: BO-4h con turnover de 0.9x muestra bruto +39%, banda [+22%, +29%], fragilidad 0.44. MR-15m con 11.4x muestra bruto +187%, banda [−31%, +34%] —fragilidad 2.2, y el signo de la estrategia se invierte dentro de la banda. Esa segunda estrategia no tiene signo determinable con tu estado actual de conocimiento de costos; las afirmaciones honestas son "no lo sabemos" y "reduce el turnover hasta que lo sepamos". Un backtest que imprime +34% y omite el borde de −31% de la banda no es optimista, es infalsable.
La banda no cuesta nada producir —tres entradas más en el barrido de modelos de costos que ya estás corriendo de todos modos— y cambia las conversaciones. "Esta estrategia hace 20%" invita a creer. "Esta estrategia hace entre 11% y 19% en nuestros escenarios de costos, y se mantiene positiva bajo el pesimista" invita a asignar capital.
Dónde encaja esto
En el motor de backtest, el modelo de costos se ubica exactamente donde el simulador de fills devuelve una cantidad ejecutada: los fills determinan qué obtuviste, la curva de costos determina qué pagaste por ello, y los peldaños por encima de L2 en esa escalera (recorrido de profundidad, modelos de cola) absorben progresivamente el modelo de costos dentro del propio modelo de fills —la curva es el sustituto paramétrico que usas cuando no estás simulando el libro directamente. Los parámetros provienen de tu propio registro de TCA cuando tienes uno, y de datos públicos escalados con seguridad cuando no. La forma funcional proviene de cuarenta años de teoría y medición —Kyle (1985) para explicar por qué el costo por dólar depende de la liquidez en absoluto, Almgren et al. (2005) y Tóth et al. (2011) para la ley cóncava en sí, con Almgren-Chriss como la contraparte de programación y los modelos de impacto neuronales como la frontera de predicción. Lo que este artículo agrega es el flujo de trabajo: ajustar la curva, condicionarla al régimen, barrerla como un parámetro, y reportar la banda. Nada de esto es glamoroso. Todo es más barato que descubrir, en vivo, de qué lado de slippage_bps = 5 estaba realmente tu estrategia.
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.