Curvas de slippage, não constantes de slippage: modelos de custo que sobrevivem ao contato com o trading ao vivo
A linha mais cara na maioria dos backtests é slippage_bps = 5. Não porque 5 seja o número errado — em média, ao longo de um ano de fills, pode até estar correto — mas porque uma constante está errada condicionalmente, e o seu processo de seleção de estratégias é uma máquina para encontrar e explorar erros condicionais. O slippage é uma função do tamanho da ordem, do spread, da volatilidade e da liquidez disponível. Uma constante é essa função avaliada em um ponto e depois extrapolada para todos os lugares, incluindo a cascata de liquidação das 3 da manhã, onde o seu sinal de reversão à média dispara com mais força e o book tem um décimo da sua profundidade habitual. O backtest cobra 5 bps ali também. O trading ao vivo não vai cobrar isso.
Este artigo trata do modelo de custo dentro do loop do backtest: a função cost(size, market_state) que é subtraída de cada trade simulado. Deliberadamente não trata de prever o impacto ex ante para agendar uma execução — esse é o problema companheiro, coberto do lado do modelo em modelagem de impacto de preço e do lado do agendamento ótimo em Almgren-Chriss. Aqui a pergunta é mais estreita e, para quem seleciona estratégias por backtest, mais urgente: que função deveria estar no lugar onde hoje está a sua constante, como ajustá-la a partir dos dados que você realmente tem, e o quanto a resposta muda a estratégia que você acaba executando. A última parte tem uma resposta empírica nítida: reclassifique 20 variantes de uma família de estratégias sob quatro modelos de custo e observe o leaderboard se inverter.
A escada dos modelos de custo

Assim como a escada de simulação de fills, os modelos de custo formam uma escada de fidelidade em que cada degrau exige mais dados e remove um viés sistemático específico. Os degraus são cumulativos: cada um mantém os termos do anterior e adiciona um.
M0: bps constante
Um único número, aplicado a todo trade. Os três modos de falha, em ordem crescente de dano:
- Cego ao tamanho. Um clip de 5 milhões pagam o mesmo. Qualquer comparação de estratégias entre níveis de capacidade fica sem sentido.
- Cego ao regime. Uma terça-feira calma à tarde e o colapso da FTX pagam o mesmo. O erro não é ruído; é anticorrelacionado exatamente com os estados em que estratégias orientadas a eventos concentram seu trading.
- Tóxico para a seleção. Ao varrer 200 combinações de parâmetros, o otimizador descobre que, sob um custo fixo, o turnover é barato em relação à realidade e desloca toda a busca para variantes de alta frequência. O erro de custo se torna uma força direcional sobre a seleção de modelos, não uma força simétrica sobre o PnL.
M1: proporcional ao spread
Cobre o meio-spread medido no timestamp da decisão (mais a taxa, que é determinística e deve sempre ser sua própria linha). Este é o primeiro modelo que responde ao mercado de alguma forma: spreads se alargam em notícias, em aberturas de sessão, em altcoins de baixa liquidez, então o custo passa a co-mover com pelo menos uma variável de liquidez. O que ainda falta: o spread precifica o fill de um único clip no topo do book. Qualquer coisa maior atravessa o book, e essa travessia é invisível para .
M2: escalado pela volatilidade
Adicione um termo proporcional à volatilidade de curto horizonte. A justificativa é tanto empírica quanto estrutural: spreads e profundidade são definidos por market makers cujo risco de seleção adversa e de inventário escala com (a lógica de cotação de Avellaneda-Stoikov rodada ao contrário), então o custo de demandar liquidez é naturalmente denominado em unidades de volatilidade. Quando você não tem nenhum dado de book — apenas barras diárias, histórico longo — com em torno de 0,05–0,1 da vol diária já é um modelo drasticamente melhor do que qualquer constante, porque pelo menos respira com o regime.
M3: raiz quadrada na participação
O termo de tamanho. é o tamanho da sua ordem, o volume de mercado na janela relevante, a volatilidade na mesma janela. Essa forma funcional não é uma conveniência; é um dos resultados empíricos mais replicados na microestrutura de mercado, e vale a pena ser preciso sobre sua origem.
O ponto de partida teórico é Kyle (1985), "Continuous auctions and insider trading" (Econometrica 53(6), 1315–1335). No modelo de Kyle, um market maker define o preço como função linear do fluxo líquido de ordens, , com
— impacto por unidade de fluxo proporcional à incerteza de valor sobre o volume de noise trading. O lambda de Kyle deu ao campo sua unidade de liquidez (profundidade é ) e sua primeira afirmação testável: o custo por dólar negociado depende da razão entre informação e liquidez, não de uma constante. Mas o modelo é linear no tamanho, e os dados dizem o contrário.
Os dados dizem côncavo. Almgren, Thum, Hauptmann e Li (2005), "Direct estimation of equity market impact" (Risk, julho de 2005), ajustaram cerca de 700.000 ordens de ações americanas das mesas do Citigroup e encontraram impacto temporário crescendo como a participação elevada à potência — testaram e rejeitaram explicitamente tanto a forma linear quanto a raiz quadrada pura em favor de — enquanto o impacto permanente saiu em , estatisticamente indistinguível de linear (o que é tranquilizador, já que Huberman e Stanzl (2004) mostraram que impacto permanente não linear admite manipulação). Tóth et al. (2011), "Anomalous price impact and the critical nature of liquidity in financial markets" (Physical Review X 1, 021006), usando quase 500.000 metaordens da Capital Fund Management em mercados futuros, encontraram com da ordem de um — e forneceram a explicação hoje padrão: o book visível é uma fração desprezível da liquidez latente, que é localmente linear em preço ao redor do mid, fazendo com que o custo de consumi-la escale como a raiz quadrada. Trabalhos posteriores refinam as bordas: Zarinelli, Treccani, Farmer e Lillo (2015), "Beyond the square root" (Market Microstructure and Liquidity 1(2)), mostraram em sete milhões de metaordens institucionais da ANcerno que a raiz quadrada se ajusta bem ao longo de cerca de duas décadas de tamanho de ordem, mas uma forma logarítmica se estende por cinco; Bucci et al. (2019, Physical Review Letters 122, 108302) documentaram a transição de impacto linear (à la Kyle) em participação muito pequena para raiz quadrada acima dela. E Frazzini, Israel e Moskowitz (2018), "Trading Costs" (SSRN 3229719), usando $1,7 trilhão em execuções reais da AQR ao longo de 19 anos, encontraram custos realizados uma ordem de magnitude menores do que as estimativas acadêmicas anteriores — um alerta de que coeficientes ajustados sobre o fluxo de outras pessoas, com urgência e conteúdo informacional diferentes, não se transferem para o seu.
Para o loop do backtest, o resumo prático são três afirmações. O impacto escala com a volatilidade. O impacto é côncavo no tamanho, com um expoente que você deve tratar como até que seus próprios dados justifiquem . O prefator é da ordem de um, mas varia por venue, ativo e — crucialmente — pelo seu próprio estilo de trading, motivo pelo qual a próxima seção trata de ajustá-lo em vez de copiá-lo. A calibração profunda (kernels de decaimento, decomposição transitório vs. permanente, cross-impact) pertence ao artigo de modelagem de impacto; o backtest precisa apenas da curva.
Um número trabalhado, para fixar as magnitudes. Perp de BTC, vol diária bps, volume diário do venue V_d = \6Y = 0.75\delta = 0.5$:
| Tamanho da ordem | Termo de impacto | + meio-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 |
Três ordens de magnitude em tamanho cobrem um fator de ~18 em custo. Nenhuma constante cobre essa tabela, e toda comparação de estratégias que varia o tamanho da posição mantendo slippage_bps fixo está silenciosamente comparando maçãs a 5 milhões.
Ajustando a curva: seus fills, ou os de ninguém
A partir dos seus próprios fills
O dado correto é o seu próprio registro de TCA: uma linha por ordem-mãe com mid de chegada, VWAP de fill realizado, tamanho da ordem e estado de mercado no momento da decisão — exatamente o dataset que a medição de implementation shortfall produz como subproduto. O shortfall em bps contra a chegada, normalizado pela volatilidade contemporânea, regredido sobre a participação, dá diretamente e .
A armadilha é o ruído. O shortfall de uma única ordem é impacto mais ruído de preço ao longo da janela de execução, e o ruído domina: para uma execução de 10 minutos a 250 bps de vol diária, o termo de ruído é bps contra um sinal de impacto de talvez 3 bps. Uma relação sinal-ruído de 0,15 por observação é o motivo pelo qual Almgren et al. precisaram de 700 mil ordens e pelo qual você deve ajustar sobre medianas de buckets, nunca sobre pontos brutos:
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)
Em algumas milhares de ordens-mãe de cripto, esse ajuste já é estável a aproximadamente ±30% em e ±0,1 em — grosseiro, mas a diferença entre uma curva ajustada e uma constante chutada não é de 30%, é a forma inteira. Reajuste trimestralmente; a deriva em é, ela mesma, um diagnóstico (aumento de com participação constante significa que o regime de liquidez do venue mudou, ou que o seu fluxo de ordens ficou mais informado/mais detectável).
Partida a frio: ainda sem fills
Antes da primeira ordem ao vivo você tem apenas dados públicos, e duas opções honestas.
Percorrer a profundidade do book (depth-walk). Calcule a média do custo de percorrer snapshots L2 em cada tamanho, exatamente como o simulador de fills faz por trade, mas agregado em uma curva. Isso é um limite inferior com sinal conhecido: o snapshot mostra a liquidez que sobreviveu ao fluxo de todos os outros, a dinâmica de reposição é invisível, e suas próprias ordens filhas anteriores terão movido o book antes de as posteriores chegarem.
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 a pegada de trades públicos. Agrupe trades agressores por tamanho, meça o movimento do mid alguns segundos depois de cada um, e extraia uma curva de impacto empírica. Isso recupera a forma côncava e, aproximadamente, o escalonamento correto por , mas o nível é enviesado em uma direção que você não consegue determinar a priori: os prints públicos são decisões de outros traders, então prints grandes são condicionalmente informados (enviesando o impacto medido para cima em relação ao seu rebalanceamento não informado), enquanto os traders mais sensíveis à liquidez fracionam ordens justamente para se esconder (enviesando para baixo). Use-o para a forma, não para o nível.
De qualquer forma, o protocolo de partida a frio é: pegue a curva de dados públicos, aplique um fator de segurança de 1,5–2, negocie pequeno, e comece a preencher a tabela de TCA que a substituirá. A curva pública é um andaime, não uma parede estrutural.
Dependência de regime: a mesma ordem, cinco vezes o custo

Todo termo em depende do estado, e em um pico de volatilidade todos se movem contra você ao mesmo tempo. Spreads são proporcionais à vol de curto horizonte, então se alarga de 5 a 10x. entra diretamente no termo de impacto. O volume também sobe — o que ingenuamente reduz a participação — mas sobe menos do que no numerador, e essa compensação esconde um efeito mais nocivo: a profundidade cotada perto do mid colapsa muito mais rápido do que o volume negociado, então a curva realizada se torna mais íngreme do que a fórmula ajustada prevê a partir apenas dos inputs estressados.
Concretamente, com a curva ajustada acima, uma ordem de $3M em BTC:
- Dia 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.
- Dia de crise (nível 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.
A fórmula diz 3,3x. O TCA realizado em dias de estresse consistentemente vem acima da fórmula — o colapso de profundidade que o termo de raiz quadrada não enxerga — e é aí que entra um multiplicador de estresse medido. Nos nossos fills, o resíduo do bucket de crise fica entre 1,4 e 1,8x acima da fórmula com inputs estressados, levando o custo total de estresse a 4,5–6x o custo calmo para a mesma ordem. Essa é a origem honesta de "a mesma ordem custa 5x mais em um pico de vol": aproximadamente 2x vindos de líquido de volume, 2x do spread, e o restante da profundidade evaporando mais rápido do que as estatísticas de volume admitem.
A implementação é um modelo de custo condicional: ajuste a curva por bucket de regime, ou de forma equivalente ajuste uma curva e uma tabela de multiplicadores indexada por um quantil de vol realizada calculado no momento da decisão (apenas janela trailing — um rótulo de regime calculado sobre a amostra completa é look-ahead contrabandeado através do modelo de custo):
| Regime (quantil de vol trailing 1h) | Frequência | Mult. spread | Mult. curva (medido) |
|---|---|---|---|
| Calmo (< p25) | 25% | 0.6x | 0.7x |
| Normal (p25–p75) | 50% | 1.0x | 1.0x |
| Elevado (p75–p95) | 20% | 1.8x | 1.6x |
| Estresse (> p95) | 5% | 5–10x | 3–6x |
Frazzini, Israel e Moskowitz (2018) documentam o mesmo fenômeno em seus dados institucionais: custos realizados co-movem fortemente com a volatilidade contemporânea, e um modelo de custo sem um termo de vol variável no tempo precifica errado exatamente as caudas. O motivo pelo qual essa tabela importa mais do que sua linha de 5% de estresse sugere é correlação: as estratégias não amostram essas linhas uniformemente. Um sistema de breakout de volatilidade faz a maior parte do seu trading nas duas linhas de baixo. Um market maker de mercado calmo ganha na linha de cima e é stopado através da de baixo. Ponderar os trades de cada estratégia pelo seu próprio histograma de regime — em vez de cobrar de todos a média incondicional — é a maior correção de PnL nesta página inteira para sistemas orientados a eventos, valendo mais do que a diferença entre quaisquer dois degraus adjacentes da escada.
O experimento de sensibilidade: quatro modelos de custo, um leaderboard reembaralhado
Se o modelo de custo mudasse o PnL por um deslocamento constante, nada disso importaria para a seleção — toda variante se deslocaria igualmente e o argmax sobreviveria. Não é o caso, porque a sensibilidade ao custo varia entre variantes por uma ordem de magnitude. O experimento que torna isso concreto:
Configuração. Uma família de estratégias — reversão à média por z-score e breakout de Donchian em perps de BTC e ETH, barras de 1 minuto, 18 meses — com uma grade sobre o horizonte de holding (15m / 1h / 4h / 24h) e limiar de entrada: 20 variantes, turnover unidirecional de 0,4x a 11x o book por dia, clips de $250 mil executados ao longo de 5 minutos. Cada variante é backtestada uma vez por modelo de custo:
- M0: apenas taxas;
- M1: taxas + 5 bps constante;
- M2: taxas + meio-spread + ;
- M3: taxas + curva ajustada , com a tabela de multiplicadores de regime acima.
Mesmos fills, mesmos sinais, mesmo caminho de código — o modelo de custo é passado ao backtester como argumento do construtor, da forma como qualquer parâmetro de primeira classe é:
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 (execução representativa; retornos líquidos anualizados):
| Variante | Turnover/dia | 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 |

Correlações de rank (tau de Kendall) entre leaderboards: — uma constante mal reordena algo, é um imposto, não um modelo. — seis dos dez primeiros de M1 saem do top dez sob M3, e o campeão de M1 vai de +61% para negativo. — o termo de vol sozinho já recupera a maior parte da reordenação; o termo de tamanho completa o trabalho para as variantes que negociam grande em relação ao volume da janela. O mecanismo é exatamente a toxicidade de seleção do degrau M0: um 5 bps fixo subsidia os trades que realmente custam 15 bps (grandes, rápidos, em estresse) e sobretaxa os que custam 2 (pequenos, pacientes, em calmaria), então classificar por M1 promove sistematicamente as variantes cujos custos verdadeiros são mais subestimados. O otimizador não encontrou alpha; encontrou o termo de erro do seu modelo de custo.
É por isso que o modelo de custo pertence à mesma categoria mental que o split de walk-forward ou a função objetivo: um parâmetro de backtest de primeira classe que você varia, não uma constante ambiental que você define uma vez. Se o rank de uma variante é estável entre M2 e M3, seu edge é real em relação aos custos. Se seu rank colapsa ao subir a escada, você aprendeu que era um artefato do modelo de custo — o mesmo evento epistêmico de uma estratégia morrer out-of-sample em otimização walk-forward, e merece a mesma resposta. E se você selecionou o argmax de 200 variantes sob M1, a lógica do Sharpe deflacionado se aplica com uma reviravolta: você rodou uma busca de teste múltiplo em que a própria estatística de teste era enviesada a favor de entradas de alto turnover.
Limites honestos: PnL como uma faixa
A disciplina final decorre de admitir que o modelo de custo é incerto mesmo depois de ajustado. tem um intervalo de confiança, os multiplicadores de estresse são estimados a partir de um punhado de dias de cauda, e o futuro traz seus próprios regimes. Então pare de reportar o PnL líquido como um número. Reporte-o como uma faixa através de cenários de custo nomeados:
- Otimista: limite inferior do depth-walk, sem multiplicadores de estresse. Nunca reporte sozinho; sua única função é delimitar o melhor caso.
- Realista: curva ajustada, multiplicadores de regime ajustados — sua estimativa central.
- Pessimista: , spread , linha de estresse sempre aplicada em pelo menos seu valor p75. Aproximadamente: a curva ajustada avaliada na borda desfavorável do intervalo de confiança de cada parâmetro.
Duas regras de decisão tornam a faixa operacional. Primeira: aloque com base no pessimista, reporte o realista. Uma estratégia que só sobrevive sob o modelo realista é uma aposta de que o seu ajuste de custo não tem erro na direção cara — uma aposta que você não fez conscientemente. Segunda: acompanhe a razão de fragilidade — largura da faixa sobre o PnL bruto. A partir do experimento acima: BO-4h a 0,9x de turnover mostra bruto +39%, faixa [+22%, +29%], fragilidade 0,44. MR-15m a 11,4x mostra bruto +187%, faixa [−31%, +34%] — fragilidade 2,2, e o sinal da estratégia se inverte dentro da faixa. Essa segunda estratégia não tem sinal determinável no seu estado atual de conhecimento de custo; as afirmações honestas são "não sabemos" e "reduza o turnover até sabermos". Um backtest que imprime +34% e omite a borda de −31% da faixa não é otimista, é infalseável.
A faixa não custa nada para produzir — mais três entradas na varredura de modelos de custo que você já está rodando de qualquer forma — e ela muda as conversas. "Esta estratégia rende 20%" convida à crença. "Esta estratégia rende de 11% a 19% nos nossos cenários de custo, e permanece positiva sob o pessimista" convida à alocação.
Onde isso se encaixa
No motor de backtest, o modelo de custo se encaixa exatamente onde o simulador de fills devolve uma quantidade executada: os fills determinam o que você obteve, a curva de custo determina quanto você pagou por isso, e degraus acima de L2 nessa escada (depth-walk, modelos de fila) absorvem progressivamente o modelo de custo no próprio modelo de fills — a curva é o substituto paramétrico que você usa quando não está simulando o book diretamente. Os parâmetros vêm do seu próprio registro de TCA quando você tem um, e de dados públicos escalados por segurança quando não tem. A forma funcional vem de quarenta anos de teoria e medição — Kyle (1985) para explicar por que o custo por dólar depende da liquidez, Almgren et al. (2005) e Tóth et al. (2011) para a própria lei côncava, com Almgren-Chriss como a contrapartida de agendamento e os modelos de impacto neurais como a fronteira de previsão. O que este artigo acrescenta é o fluxo de trabalho: ajuste a curva, condicione-a ao regime, varra-a como um parâmetro, e reporte a faixa. Nada disso é glamouroso. Tudo isso é mais barato do que descobrir, ao vivo, de que lado de slippage_bps = 5 a sua estratégia realmente estava.
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.