El precio que no obtienes
Un backtest opera al precio de sus datos; una orden real se ejecuta en otro. El deslizamiento (slippage) es esa diferencia: compras algo más alto que el precio visto y vendes algo más bajo. Tiene dos fuentes. El diferencial: una orden de mercado compra al ask y vende al bid, así que pagas la mitad del diferencial en cada sentido respecto al precio medio. Y el impacto de mercado: una orden grande consume las mejores cotizaciones y empuja el precio en su contra.
def fill_price(mid, side, slippage_bps):
if side == "buy":
return mid * (1 + slippage_bps / 10000)
if side == "sell":
return mid * (1 - slippage_bps / 10000)
raise ValueError("side must be 'buy' or 'sell'")
print(round(fill_price(50.0, "buy", 4), 4)) # 50.02 pays more
print(round(fill_price(50.0, "sell", 4), 4)) # 49.98 receives less# Crossing the spread: a market buy pays the ask, a market sell receives the bid
bid, ask = 49.98, 50.02
mid = (bid + ask) / 2
half_spread_bps = (ask - bid) / 2 / mid * 10000
print(mid, round(half_spread_bps, 2)) # 50.0 4.0 each way, relative to the mid price
print(round((ask - mid) / mid * 10000, 2), round((mid - bid) / mid * 10000, 2)) # 4.0 4.0Medirlo frente a una referencia
Para medir el deslizamiento hace falta una referencia: el precio de llegada, el precio medio cuando se decidió la orden. El déficit lleva signo para que positivo signifique siempre coste: en una compra, fill - arrival; en una venta, arrival - fill, en puntos básicos del precio de llegada. La misma ejecución mala para un comprador es buena para un vendedor, por eso el signo depende del lado. Las órdenes grandes se ejecutan por partes, así que la cifra a informar es el precio medio de ejecución ponderado por volumen.
import numpy as np
def slippage_bps(fill, arrival, side):
if side == "buy":
return (fill - arrival) / arrival * 10000
if side == "sell":
return (arrival - fill) / arrival * 10000
raise ValueError("side must be 'buy' or 'sell'")
def average_fill(prices, quantities):
prices, quantities = np.asarray(prices, dtype=float), np.asarray(quantities, dtype=float)
return float(np.sum(prices * quantities) / np.sum(quantities))
# Buying 600 shares in three pieces; the price at arrival was 50.00
avg = average_fill([50.0, 50.1, 50.3], [100, 200, 300])
print(round(avg, 4)) # 50.1833
print(round(slippage_bps(avg, 50.0, "buy"), 2)) # 36.67 bps: it cost us
print(round(slippage_bps(avg, 50.0, "sell"), 2)) # -36.67: the same fills would have helped a sellerEl impacto crece con el tamaño
El diferencial es un cargo fijo; el impacto depende de cuánto operas. Una regla práctica bien contrastada es la ley de la raíz cuadrada: el impacto esperado como fracción del precio es aproximadamente k * volatility * sqrt(quantity / ADV), donde ADV es el volumen diario medio y k una constante cercana a 1. Cuadruplicar la orden solo duplica el impacto, así que dividir una orden grande en el tiempo ayuda, pero menos de lo que se espera.
import math
def sqrt_impact_bps(volatility, quantity, adv, k=1.0):
return k * volatility * math.sqrt(quantity / adv) * 10000
# A stock with 2% daily volatility and 1,000,000 shares of average daily volume
for shares in (10_000, 50_000, 200_000, 800_000):
print(shares, round(sqrt_impact_bps(0.02, shares, 1_000_000), 1), "bps")
# 10,000: 20.0, 50,000: 44.7, 200,000: 89.4, 800,000: 178.9- Una compra se ejecuta por encima del precio medio y una venta por debajo:
mid * (1 +/- bps / 10000); el diferencial solo cuesta la mitad de su ancho en cada sentido. - Mida frente al precio de llegada, con signo para que positivo signifique coste; las ejecuciones parciales se combinan con un promedio ponderado por volumen.
- El impacto sigue la ley de la raíz cuadrada
k * volatility * sqrt(quantity / ADV): cuatro veces el tamaño, el doble de impacto.
A continuación: ejecuciones de órdenes realistas, simular cómo se ejecutan realmente las órdenes contra un libro y barras de datos.
Lab: qué se califica
- Poner precio a la ejecución con
fill_price25 pts - Medir el déficit con
slippage_bps25 pts - Combinar ejecuciones parciales con
average_fill25 pts - Pronosticar el impacto con
sqrt_impact_bps25 pts