O preço que você não consegue
Um backtest negocia ao preço de seus dados; uma ordem real é executada em outro. O deslizamento (slippage) é essa diferença: você compra um pouco acima do preço visto e vende um pouco abaixo. Tem duas fontes. O spread: uma ordem a mercado compra no ask e vende no bid, então você paga metade do spread em cada sentido em relação ao preço médio. E o impacto de mercado: uma ordem grande consome as melhores cotações e empurra o preço contra si.
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.0Medi-lo contra uma referência
Para medir o deslizamento é preciso uma referência: o preço de chegada, o preço médio quando a ordem foi decidida. O prejuízo tem sinal para que positivo signifique sempre custo: numa compra, fill - arrival; numa venda, arrival - fill, em pontos-base do preço de chegada. A mesma execução ruim para um comprador é boa para um vendedor, por isso o sinal depende do lado. Ordens grandes são executadas em partes, então o número a reportar é o preço médio de execução ponderado pelo volume.
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 sellerO impacto cresce com o tamanho
O spread é uma cobrança fixa; o impacto depende de quanto você negocia. Uma regra prática bem testada é a lei da raiz quadrada: o impacto esperado como fração do preço é cerca de k * volatility * sqrt(quantity / ADV), em que ADV é o volume diário médio e k uma constante perto de 1. Quadruplicar a ordem só dobra o impacto, então dividir uma ordem grande ao longo do tempo ajuda, mas menos do 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- Uma compra é executada acima do preço médio e uma venda abaixo:
mid * (1 +/- bps / 10000); o spread sozinho custa metade de sua largura em cada sentido. - Meça contra o preço de chegada, com sinal para que positivo signifique custo; execuções parciais combinam-se com média ponderada pelo volume.
- O impacto segue a lei da raiz quadrada
k * volatility * sqrt(quantity / ADV): quatro vezes o tamanho, o dobro do impacto.
A seguir: execuções de ordens realistas, simular como as ordens são realmente executadas contra um livro e barras de dados.
Lab: o que é avaliado
- Precificar a execução com
fill_price25 pts - Medir o prejuízo com
slippage_bps25 pts - Combinar execuções parciais com
average_fill25 pts - Prever o impacto com
sqrt_impact_bps25 pts