Simulação de fills: a escada da fantasia do preço de fechamento à realidade ciente da fila
Seu backtest contém dois modelos: um modelo do seu alpha e um modelo dos seus fills. A maioria das pessoas gasta 95% do esforço no primeiro e herda o segundo do framework que estiver usando. Isso está invertido. Um sinal medíocre com um modelo de fill honesto produz uma estimativa de PnL medíocre, mas real. Um ótimo sinal com fill_price = candle.close produz um número que não é a estimativa de nada — é o resultado de uma premissa que você nunca examinou.
Para estratégias taker em instrumentos líquidos, o modelo de fill é um termo de correção. Para tudo que envolve ordens limitadas passivas — market making, entradas passivas, captura de rebate post-only — o modelo de fill é a estratégia. Se você é preenchido, quando, em que quantidade, e condicional a qual movimento de preço subsequente determina o sinal do PnL, não apenas sua magnitude.
Em Backtest-live parity mapeamos a taxonomia completa das divergências backtest-live e classificamos a divergência de execução com severidade 5/5 — a pior classe. Aquele artigo deu à simulação de fills três níveis grosseiros de precisão e seguiu em frente. Este é o complemento em profundidade: a escada completa, degrau por degrau, com o ciclo de vida da ordem como máquina de estados, limites de probabilidade de fill que você realmente pode calcular, e um experimento medido mostrando exatamente em qual degrau da escada uma estratégia maker "lucrativa" morre.
A escada

Cada degrau exige mais dados e mais código, e cada degrau remove um viés sistemático específico. Os degraus são ordenados pelo que fazem de errado, não apenas pelo custo.
Degrau 0: fill a preço de fechamento
fill_price = bar.close
A ordem é preenchida instantaneamente, integralmente, no fechamento da barra que gerou o sinal. O sinal foi calculado a partir desse mesmo fechamento, então você está operando com dados que não existiam quando o preço foi impresso: look-ahead bias vestido de fantasia de execução. Qualquer estratégia com giro parece boa aqui.
Degrau 1: abertura da próxima barra
fill_price = next_bar.open
O modelo mínimo honesto para lógica taker em barras. O sinal é calculado na barra , o fill acontece no primeiro preço observável da barra . Isso elimina o look-ahead, mas ainda assume spread zero, impacto zero, liquidez infinita no preço de abertura e 100% de certeza de fill. Para ordens limitadas, isso degenera em lógica de touch-fill (mais adiante sobre por que isso é veneno).
Degrau 2: spread + slippage fixo
half_spread = mid * spread_bps / 2e4
slip = mid * slippage_bps / 1e4
fill_price = mid + side * (half_spread + slip) # side: +1 buy, -1 sell
Agora todo trade taker paga metade do spread mais uma constante calibrada. Este é o primeiro degrau em que uma estratégia de alto giro pode morrer no backtest — e esse é o ponto. O erro residual: o slippage não é constante. Ele escala com o tamanho da ordem em relação à profundidade exibida e explode exatamente quando sua estratégia mais quer operar. Um valor fixo de 5 bps é uma média sobre regimes; sua estratégia não opera no regime médio.
Degrau 3: depth-walk em L2 para ordens a mercado
Com snapshots do order book você para de adivinhar o slippage e passa a calculá-lo. Uma compra a mercado de tamanho percorre o lado ask nível por nível; o preço de fill é a média ponderada por volume entre os níveis consumidos:
def depth_walk(levels: list[tuple[float, float]], qty: float) -> tuple[float, float]:
"""levels: [(price, size), ...] sorted best-first. Returns (vwap, filled_qty)."""
remaining, cost = qty, 0.0
for price, size in levels:
take = min(size, remaining)
cost += take * price
remaining -= take
if remaining <= 0:
break
filled = qty - remaining
return (cost / filled if filled > 0 else float("nan"), filled)
Duas correções tornam isso substancialmente mais honesto. Primeiro, latência: percorra o book como ele estava depois do timestamp da sua decisão, onde é sua latência medida de sinal até a exchange — o book que você viu não é o book que você atingiu. Segundo, fills parciais de ordens a mercado: se a profundidade exibida dentro do seu preço-limite for menor que , o modelo precisa retornar um fill parcial e deixar um remanescente passivo, o que passa o problema para a máquina de estados abaixo.
O erro residual no degrau 3 é impacto e reposição: você consome o book como um objeto estático, mas books reais se repõem parcialmente (e contrapartes reais reagem). Para clips abaixo de alguns por cento da profundidade no topo do book esse erro é pequeno; para clips maiores você precisa de um modelo de impacto (Almgren-Chriss 2001) sobreposto.
Degrau 4: fills probabilísticos por posição na fila para ordens limitadas
Os degraus 0-3 respondem "a que preço minha ordem agressiva é preenchida". O degrau 4 responde à pergunta mais difícil: minha ordem passiva é preenchida de fato — e é o único degrau capaz de precificar uma estratégia maker. Uma ordem limitada passiva no preço é preenchida quando o volume negociado acumulado em excede o volume da fila que estava à sua frente. Isso exige rastrear sua posição em uma fila FIFO que você não consegue observar diretamente.
O maquinário de estimativa de posição na fila — posição inicial, regras de atualização em trades vs. cancelamentos, a família de probabilidade para alocar cancelamentos não observados — é o primitivo que construímos em Queue inside the wall. Não vou rederivá-lo aqui; o simulador o consome. O que o degrau 4 acrescenta é a regra de decisão de fill sobre a estimativa, coberta na seção de probabilidade de fill abaixo.
Por que este degrau importa é quantificado na literatura: Moallemi e Yuan (2016), "A Model for Queue Position Valuation in a Limit Order Book", mostram que, para instrumentos de tick grande, o valor econômico de uma posição na frente da fila versus no fundo da fila é comparável ao meio-spread — a mesma ordem de grandeza de toda a vantagem teórica de uma estratégia de market making. Um modelo de fill que ignora a posição na fila não subestima ou superestima o PnL de um maker; ele estima o PnL de uma estratégia diferente.
Existe um degrau 5 — simulação completa baseada em agentes, em que o mercado reage às suas ordens (modelos queue-reactive no sentido de Huang, Lehalle e Rosenbaum 2015; frameworks multiagente como o ABIDES, Byrd et al. 2020). O replay histórico, mesmo ciente da fila, assume que sua ordem não muda nada no comportamento de todos os demais. Essa premissa é razoável em tamanho de varejo e cada vez mais errada à medida que suas cotações se tornam uma fração visível do nível. O degrau 5 está fora do escopo aqui; saiba que a escada não termina no 4.
Fills parciais como máquina de estados
Os degraus 0-2 podem fingir que uma ordem é uma chamada de função: envia, recebe o preço, pronto. A partir do degrau 3, uma ordem é um processo com um ciclo de vida, e o simulador precisa modelá-la como uma máquina de estados ou vai lidar mal, silenciosamente, com os casos que mais importam.

enum OrderState {
PendingNew, // sent, not yet acked (latency window)
Resting { remaining: f64, q_ahead: f64 }, // in book, queue position estimated
PartialFill { remaining: f64, q_ahead: f64 }, // some qty done, rest still queued
PendingAmend, // amend in flight
PendingCancel, // cancel in flight
Filled,
Canceled { filled_qty: f64 }, // may be partially filled at cancel time
Rejected,
}
As transições carregam a economia:
PendingNew→Resting: a ordem entra na fila atrás de tudo que já existia no momento do ack, não no momento da decisão. Sua posição na fila é semeada com o volume do nível no instante . Simuladores que semeiam no momento da decisão superestimam sistematicamente a prioridade na fila — exatamente pelo volume que chegou durante sua janela de latência, que é a maior parte do volume durante rajadas.Resting→PartialFill: um trade no seu nível maior que a fila à sua frente te preenche parcialmente. O remanescente mantém sua posição (agora na frente da fila). Fills parciais não são ruído — são informação: ser preenchido em 0,3 de 1,0 e ver o preço saltar para longe é um evento de PnL diferente de um fill total, e o processo de inventário de um maker é construído a partir desses fragmentos.Resting→PendingAmend→Resting: a armadilha. Praticamente em toda exchange cripto, um amend é um cancel/replace — ocancelReplaceda Binance é atômico contra dupla execução, mas retorna um novo order ID no fundo da fila. Mesmo exchanges com semântica nativa de modificação (CME Globex) preservam a prioridade temporal apenas para uma redução de quantidade; uma mudança de preço ou aumento de quantidade a perde. Logo, no simulador: qualquer amend de preço resetaq_aheadpara o volume total atual do nível. Um motor de cotação que se reposiciona a cada 500ms não está "mantendo uma cotação" — está reentrando perpetuamente no fundo da fila, e seu perfil realista de fill é quase puramente seleção adversa. Combinado com o resultado de Moallemi-Yuan: recotar tem um preço, e o preço é a sua posição na fila.PendingCancel→PartialFill→Canceled: cancelamentos também têm latência. Na janela entre decidir puxar uma cotação e o cancelamento chegar ao matching engine, você ainda pode ser preenchido — e esses são os piores fills que você vai receber, porque o motivo pelo qual você estava puxando a cotação é que o mercado estava prestes a te atropelar. Um simulador sem latência de cancelamento apaga exatamente os fills mais tóxicos do seu histórico.
A máquina de estados também é o que torna a contabilidade do simulador honesta: taxas incidem por evento de fill, o inventário é atualizado por evento de fill, e as estatísticas de tempo-em-estado (quanto tempo as ordens ficam passivas até serem preenchidas versus canceladas) tornam-se diretamente comparáveis com logs ao vivo — o que alimenta o loop de calibração no final.
Probabilidade de fill de ordem limitada: três modelos e uma faixa
Dada uma ordem passiva no preço (digamos, um bid), quando o simulador declara um fill? Três regras de decisão, em ordem crescente de honestidade:
1. Touch-fill (ingênua). Preenche se o preço tocar seu nível: . Esta é uma regra de first-passage-time, e sua falha foi medida há um quarto de século: Lo, MacKinlay e Zhang (2002), "Econometric Models of Limit-Order Executions" (Journal of Financial Economics 65), ajustaram modelos de sobrevivência a dados reais de ordens limitadas e concluem que execuções hipotéticas construídas a partir de first-passage times "são proxies muito pobres das execuções reais de ordens limitadas". O modo de falha é estrutural: quando o preço toca seu nível e recua, o toque consumiu a frente da fila — os traders que cotaram antes de você. O touch-fill concede a você os fills deles. Pior, concede exatamente os bons fills (touch-and-bounce é o cenário lucrativo para um maker), enquanto seu conjunto real de fills é enviesado para touch-and-run-through — os adversamente selecionados.
2. Trade-through (limite conservador). Preenche apenas se o preço negociar estritamente através do seu nível: , ou, em dados de tick, se o volume negociado acumulado em exceder o nível inteiro. Se o preço atravessou, toda a fila em foi consumida, então você foi preenchido independentemente da posição. Isso nunca concede um fill que você não teria recebido. Seu viés é a imagem espelhada do touch-fill: nega todo fill em que a fila se esgotou até sua posição sem penetração total, e os fills que concede são desproporcionalmente os de run-through (adversamente selecionados). Um backtest de maker sob trade-through é um teste de estresse, não uma estimativa.
3. Estimativa de esgotamento de fila. Rastreie o volume negociado acumulado no seu preço a partir do trade tape e o volume cancelado inferido a partir de deltas de L2. Sua fila estimada à frente:
onde é o volume do nível na entrada da ordem (momento do ack) e é a fração de cancelamentos assumida como vinda de posições à sua frente — o parâmetro cuja forma principiada (a família ) é derivada no artigo de posição na fila. O fill começa quando ; sua quantidade preenchida é o volume negociado em excesso a isso, o que gera fills parciais naturalmente:
Definir (todos os cancelamentos à sua frente) dá o extremo otimista deste modelo; , o extremo pessimista. Quando você tem apenas snapshots esparsos de L2 e nenhum trade tape no nível — comum em feeds cripto com throttle de 100ms — você pode recorrer a um prior baseado em modelo: Cont, Stoikov e Talreja (2010), "A Stochastic Model for Order Book Dynamics" (Operations Research 58), modelam cada nível de preço como uma fila de nascimento-morte e calculam, via transformadas de Laplace, a probabilidade de uma ordem no bid ser executada antes que o mid se mova, condicional aos tamanhos atuais da fila. É um oráculo analítico de probabilidade de fill: cru em relação ao replay do tape, muito melhor que o touch-fill, e barato o suficiente para avaliar dentro de um loop de backtest quente.
A disciplina da faixa
As três regras não são concorrentes — são uma ordenação:
o que induz uma faixa de PnL. Rode todo backtest de maker três vezes e reporte o intervalo:
(a desigualdade superior é aproximada — o touch-fill pode reclassificar estratégias, não apenas inflá-las, porque concede fills contrafactuais bons). A regra de decisão que se segue: uma estratégia maker só é implantável se sobreviver no extremo conservador da faixa, e a faixa precisa ser estreita o suficiente para que a estimativa pontual signifique algo. Uma estratégia mostrando +3k trade-through tem uma faixa de 2,1k / +$0,4k está te dizendo algo real.
O experimento: descendo uma estratégia maker pela escada

Tome um maker deliberadamente simples — cotações simétricas no melhor bid/ask, clips fixos de 0,05 BTC, inventário limitado a ±0,5 BTC com flattening taker no limite. Um mês de dados perpétuos de BTCUSDT: barras de 1m para os degraus baixos, diffs de L2 a 100ms mais o trade tape para os degraus altos; taxa maker de 1,0 bps, taker de 4,0 bps. Mesmo código de sinal em cada degrau (núcleo compartilhado, de modo que a única variável seja o modelo de fill). Os números abaixo são de uma rodada representativa nossa — suas magnitudes vão variar por exchange, mês e tamanho; a forma, não:
| Degrau | Modelo de fill | Taxa de fill das cotações | Fills | PnL do mês | Veredito |
|---|---|---|---|---|---|
| 0 | close-fill | 98% | 41.200 | +$14.800 | fantasia |
| 1 | next-bar-open / touch em barras de 1m | 89% | 37.400 | +$9.600 | fantasia com latência |
| 2 | touch + spread e slippage fixo nos flattens taker | 89% | 37.400 | +$7.100 | custos modelados, fills ainda fictícios |
| 3 | + depth-walk em L2 nos flattens taker | 89% | 37.400 | +$6.400 | saídas honestas, entradas ainda fictícias |
| 4a | trade-through (conservador) | 21% | 8.900 | -$3.900 | limite de estresse |
| 4b | esgotamento de fila, calibrado | 37% | 15.600 | -$700 | melhor estimativa |
| 4c | esgotamento de fila, (otimista) | 44% | 18.700 | +$1.900 | extremo superior |
| — | shadow run ao vivo, mesmo mês | 35% | 14.100 | -$1.150 | realidade |
Leia a tabela de cima para baixo e observe onde a estratégia morre. Não é no degrau 2 — taxas e slippage tiram 26%, e a estratégia ainda parece robustamente lucrativa. Ela morre entre o degrau 3 e o degrau 4, e morre por um motivo que nenhum modelo de custo consegue capturar: seleção de fills. O touch-fill concedeu 37.400 fills, dos quais a maioria era touch-and-bounce — captura pura de spread. O modelo ciente da fila eliminou 58% desses fills, e os fills eliminados eram desproporcionalmente os lucrativos: quando o nível é atingido levemente, a fila à frente de um maker que recota com latência de varejo absorve tudo. Os fills que sobrevivem até o degrau 4 são enviesados para varreduras que limpam o nível — os fills em que o preço já está atravessando você. A taxa de fill caiu 2,4x; o PnL trocou de sinal. Essa assimetria — perder os bons fills, manter os ruins — é a seleção adversa tornada mecanicamente explícita, e é invisível em todo degrau abaixo do 4.
Note também o que a faixa fez: [-1.900] engloba o zero, com a estimativa calibrada em -1.150. O simulador não acertou o PnL ao vivo com precisão de centavos — mas acertou o sinal, a magnitude, e a taxa de fill dentro de 2 pontos percentuais. É para isso que serve um modelo de fill. O backtest do degrau 1 errou o PnL ao vivo por $10.750 numa estratégia cuja vantagem bruta mensal total era de poucos milhares de dólares: o erro do modelo de fill foi cerca de 3x o tamanho do alpha. Daí a tese: seu modelo de fill é uma premissa maior que seu alpha.
Uma ressalva para os degraus baixos: se você precisa viver com dados de barra (degraus 0-2), no mínimo resolva a ambiguidade intrabar com drill-down adaptativo — descendo de 1m para 1s/100ms/trades onde SL, TP ou níveis de cotação caem dentro do range da barra. O drill-down corrige erros de sequenciamento (qual nível foi atingido primeiro), mas não corrige erros de fila; é o complemento de resolução de dados deste artigo, não um substituto do degrau 4.
O loop de calibração: fechando contra fills ao vivo
Um simulador de degrau 4 tem parâmetros livres — , latência , latência de cancelamento, a premissa de reposição de nível. Sem calibração, é apenas um palpite com formato diferente. O loop que o transforma em instrumento:
1. Registre tudo ao vivo. Cada evento de ordem com timestamps da exchange: envio, ack, cada fill parcial, acks de amend, acks de cancelamento. Mais o estado do L2 no momento do envio. Esta é a mesma disciplina de log que backtest-live parity exige para seu DivergenceMonitor — a calibração do modelo de fill é a camada mais profunda desse monitor.
2. Reproduza as mesmas ordens pelo simulador. Alimente os dados de mercado registrados e as instruções de ordem registradas (não os fills) no simulador. Agora você tem resultados pareados: para cada ordem ao vivo, um destino simulado.
3. Compare distribuições, não médias, em buckets. Uma única taxa de fill global pode esconder erros que se compensam (otimista demais em regimes calmos, pessimista demais em rajadas — anulando-se em uma média "calibrada"). Agrupe por drivers:
import numpy as np
from scipy.stats import ks_2samp
def calibration_report(pairs, bucket_key):
"""pairs: [{'bucket':…, 'live_filled':bool, 'sim_filled':bool,
'live_ttf':float|None, 'sim_ttf':float|None}, …]"""
out = {}
for b in sorted({p['bucket'] for p in pairs}):
grp = [p for p in pairs if p['bucket'] == b]
live_fr = np.mean([p['live_filled'] for p in grp])
sim_fr = np.mean([p['sim_filled'] for p in grp])
live_ttf = [p['live_ttf'] for p in grp if p['live_ttf'] is not None]
sim_ttf = [p['sim_ttf'] for p in grp if p['sim_ttf'] is not None]
ks = ks_2samp(live_ttf, sim_ttf) if len(live_ttf) > 20 and len(sim_ttf) > 20 else None
out[b] = {
'n': len(grp),
'fill_rate_live': live_fr,
'fill_rate_sim': sim_fr,
'fill_rate_gap': sim_fr - live_fr, # signed: + means sim optimistic
'ttf_ks_pvalue': ks.pvalue if ks else None,
}
return out
Duas estatísticas por bucket: o gap de taxa de fill com sinal (simulador menos ao vivo) e um teste KS nas distribuições de tempo-até-fill entre as ordens preenchidas. A comparação de tempo-até-fill é a mais afiada — um simulador pode acertar as taxas de fill enquanto preenche em momentos sistematicamente errados, o que corrompe toda estatística downstream de inventário e seleção adversa. Esta é precisamente a lição do enquadramento de análise de sobrevivência de Lo-MacKinlay-Zhang: execução é um problema de tempo-até-evento, então valide-a como tal.
4. Ajuste os parâmetros, em ordem de identificabilidade. Latência primeiro (medida diretamente a partir dos timestamps de ack — não ajustada). Depois , minimizando o gap de taxa de fill entre os buckets de profundidade de fila. Depois verifique os buckets de regime de volatilidade: um gap otimista persistente concentrado nos buckets de rajada geralmente significa que seu simulador subestima fills tóxicos por latência de cancelamento ou reposição de nível, não .
5. Rode a faixa novamente. Após a calibração, a estimativa de esgotamento de fila deve cair dentro da faixa, perto do ao vivo, e — o verdadeiro teste de aceitação — a classificação das variantes de estratégia sob o simulador deve corresponder à classificação delas em modo shadow. Então congele os parâmetros e recalibre em um cronograma; a dinâmica de fill deriva com as faixas de taxa da exchange, mudanças de tick size e a população de HFT, e um calibrado em março é uma hipótese em julho.
Expectativas de convergência de nossas rodadas: um simulador de degrau 4 sem calibração tipicamente cai dentro de ±10-15 pontos da taxa de fill ao vivo; após uma rodada de calibração, ±3-5 pontos, com p-values do teste KS de tempo-até-fill deixando de rejeitar uniformemente. Você não vai fazer melhor que isso com replay histórico — o resíduo é a reação do mercado a você, o que é problema do degrau 5.
O que levar
- Nomeie seu degrau. Todo backtest está nesta escada, quer você tenha escolhido o degrau, quer seu framework tenha escolhido por você. Se você não consegue nomear o degrau do seu modelo de fill e seus vieses conhecidos, seu número de PnL tem margens de erro que você não viu.
- Takers podem parar no degrau 3. Depth-walk mais latência medida precifica execução agressiva honestamente em tamanho de varejo. Gaste o esforço economizado em qualidade de dados.
- Makers começam no degrau 4. Abaixo dele, a lógica de fill de ordem limitada não é aproximada — ela seleciona um conjunto contrafactual de fills com viés de seleção adversa oposto. Backtests de touch-fill de estratégias maker são o gerador mais confiável de estratégias que morrem ao contato com produção.
- Modele o ciclo de vida, não o fill. Fills parciais, reset de fila por amend, latência de cancelamento — a máquina de estados é onde vivem os fills tóxicos, e os fills tóxicos são onde o PnL do maker vai morrer.
- Reporte a faixa. Limites conservador e otimista custam duas rodadas extras de backtest e convertem "meu backtest diz +3,9k, +$1,9k]" — o que é uma decisão diferente, e melhor.
- Calibre contra fills ao vivo em buckets. Um modelo de fill validado apenas em agregado é um modelo de fill com erros compensatórios ocultos. Gaps de taxa de fill em buckets mais testes KS de tempo-até-fill, recalibrados trimestralmente.
Os degraus da escada não são gradações acadêmicas — cada um é uma mentira específica que seu backtest para de contar. Suba até que as mentiras sejam menores que sua vantagem.
Links úteis
- Moallemi, C., Yuan, K. — A Model for Queue Position Valuation in a Limit Order Book (2016)
- Cont, R., Stoikov, S., Talreja, R. — A Stochastic Model for Order Book Dynamics, Operations Research 58(3), 549-563 (2010)
- Lo, A., MacKinlay, C., Zhang, J. — Econometric Models of Limit-Order Executions, Journal of Financial Economics 65(1), 31-71 (2002)
- Huang, W., Lehalle, C.-A., Rosenbaum, M. — Simulating and Analyzing Order Book Data: The Queue-Reactive Model, JASA 110(509), 107-122 (2015)
- Almgren, R., Chriss, N. — Optimal Execution of Portfolio Transactions (2001)
- Byrd, D., Hybinette, M., Balch, T. — ABIDES: Towards High-Fidelity Multi-Agent Market Simulation (2020)
- Binance API — Cancel-Replace order semantics
- CME Globex — Order modification and time priority rules
Citação
@article{soloviov2026fillsimulation,
author = {Soloviov, Eugen},
title = {Fill simulation: the ladder from close-price fantasy to queue-aware reality},
year = {2026},
url = {https://marketmaker.cc/blog/fill-simulation-partial-fills-backtest},
description = {Five rungs of fill simulation fidelity — from close-price fills to probabilistic queue-position models. Partial fills as a state machine, limit-fill probability bounds as a PnL bracket, and a calibration loop against live fills.}
}
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.