Implementation shortfall e TCA caseiro: medindo o que a execução realmente custa
Toda mesa institucional tem um pipeline de análise de custos de transação. Quase ninguém que roda um bot de cripto tem. A configuração típica registra execuções, soma taxas e chama a diferença entre o PnL do backtest e o PnL ao vivo de "slippage" — um único resíduo inexplicado que absorve latência, spread, impacto, seleção adversa e toda ordem não preenchida que fugiu. Você não consegue corrigir um custo que mede como um único número. O ferramental para medir isso corretamente existe desde 1988, não é proprietário, e sobre seus próprios registros de execuções são aproximadamente 200 linhas de Python. Este artigo constrói esse ferramental.
O ganho não é um dashboard mais bonito. Seu backtest contém um modelo de custos — uma constante de slippage, uma probabilidade de preenchimento, um coeficiente de impacto — e cada parâmetro nele é, atualmente, um palpite. TCA sobre execuções ao vivo é a única verdade fundamental contra a qual esses parâmetros podem ser calibrados. Construímos o lado de simulação desse ciclo em Simulação de preenchimento: a escada da fantasia do preço de fechamento à realidade consciente da fila; este artigo constrói o lado da medição.
Papel versus realidade: o que Perold realmente mediu
O truque fundamental vem de André Perold (1988, "The Implementation Shortfall: Paper Versus Reality," Journal of Portfolio Management 14(3), 4–9). Execute dois portfólios em paralelo. O portfólio de papel executa cada decisão instantaneamente, em tamanho ilimitado, a custo zero, ao preço vigente no momento da decisão. O portfólio real é o que seu bot realmente fez: preenchimentos parciais, cotações perseguidas, remanescentes cancelados, taxas. O implementation shortfall é a diferença entre seus retornos.
A definição importa por causa daquilo que ela se recusa a esconder. Um extrato de taxas mostra comissões. Um relatório de execução versus preço-limite não mostra absolutamente nada (você nunca é preenchido pior que seu limite — por construção). O portfólio de papel cobra por tudo: deriva entre decisão e chegada, o spread que você cruzou, o impacto que você causou e — crucialmente — as ordens que nunca foram preenchidas enquanto o preço fugia. Wagner e Edwards (1993, "Best Execution," Financial Analysts Journal 49(1), 65–71) chamaram as taxas visíveis de ponta do iceberg; para qualquer coisa com giro, a parte submersa domina.
Fixando a notação. Uma ordem-mãe: lado (compra/venda), tamanho . O preço de decisão é o mid que sua estratégia viu quando o sinal disparou. As execuções chegam como com quantidade total preenchida . No horizonte (ordem-mãe completada, cancelada ou expirada), o mid é . As taxas explícitas são . O implementation shortfall em termos monetários:
normalizado em basis points ao dividir pelo nocional de papel . Positivo significa que você pagou. O primeiro termo é o que suas execuções custaram em relação ao portfólio de papel; o segundo é o custo de oportunidade de Perold — o remanescente não preenchido marcado ao preço para o qual ele fugiu; o terceiro é a única parte que seu extrato de exchange admite.
Uma nota de convenção: o mercado diz "preço de chegada" (arrival price), e na maior parte do TCA de equities chegada significa o mid quando a ordem alcançou o mercado. Para um bot, o momento da decisão e o momento de chegada diferem pela sua própria latência interna somada à fila de rate-limit — um custo real e mensurável. Por isso mantemos ambos os timestamps e ambos os preços, e deixamos a decomposição separá-los.
A decomposição: delay, impacto, timing, oportunidade, taxas
Um único número de IS diz que a execução é cara. Não diz por quê, e as correções para componentes diferentes são completamente diferentes — você não resolve custo de delay e custo de impacto com a mesma mudança. O implementation shortfall expandido de Robert Kissell (Kissell, 2006, "The Expanded Implementation Shortfall: Understanding Transaction Cost Components," Journal of Trading 1(3), 6–16; desenvolvido em profundidade em The Science of Algorithmic Trading and Portfolio Management, Academic Press, 2013) desmembra o total em componentes, cada um atribuível a um estágio distinto do ciclo de vida da ordem. A versão do praticante, usando o mid de chegada (mid no primeiro reconhecimento pela exchange):
A identidade se reduz exatamente à definição de Perold — expanda os termos e se cancela. Cada parte tem um dono distinto:
Custo de delay : deriva de preço entre o disparo do sinal e sua primeira ordem-filha ficar ativa na exchange. Isso é sua infraestrutura — serialização, rede, filas de rate-limit, checagens de risco. Se seu sinal tem alfa genuíno de curto horizonte, o custo de delay é onde ele vaza primeiro; uma média consistentemente positiva indica que o mercado se move a seu favor antes de você chegar — alfa de momentum em decaimento, ou alguém mais rápido negociando o mesmo sinal.
Custo de negociação : o que suas execuções pagaram em relação à chegada — spread cruzado mais impacto de mercado mais deriva intra-cronograma. Este é o boletim do algoritmo de execução, e a grandeza que a pesquisa de execução de fato modela. Almgren, Thum, Hauptmann e Li (2005, "Direct Estimation of Equity Market Impact," Risk 18(7), 58–62) mediram isso sobre aproximadamente 700.000 ordens de equities americanas das mesas do Citigroup (dezembro de 2001 a junho de 2003) e descobriram que o custo de negociação escala com a volatilidade diária e a taxa de participação — impacto temporário seguindo uma lei de potência na taxa de negociação com expoente próximo de 3/5, impacto permanente próximo de linear no tamanho. Reutilizaremos essa forma funcional quando calibrarmos.
Risco de timing: não é um termo na decomposição da média, mas a variância em torno dela. Distribuir uma ordem-mãe ao longo do tempo reduz o impacto esperado e expõe você à volatilidade; para um cronograma com posição remanescente , o desvio-padrão do custo escala como . Este é precisamente o trade-off que o framework de Almgren–Chriss otimiza. No seu relatório de TCA, isso aparece como a dispersão do IS entre ordens-mãe — reporte o desvio-padrão ao lado de cada média, ou a média receberá toda a atenção e as caudas ficarão com todo o seu dinheiro.
Custo de oportunidade : a quantidade não preenchida marcada ao preço terminal. Para estratégias passivas, este é rotineiramente o maior e menos examinado componente, e é o termo que torna todo o framework honesto — mais sobre isso na seção de armadilhas, porque omiti-lo é a forma mais comum de as pessoas enganarem a si mesmas com TCA.
Taxas : a parte explícita. Em cripto, registre as taxas após descontos (níveis VIP, rebates em token convertidos ao preço no momento da execução) e mantenha os rebates de maker com sinal — uma taxa negativa é dado, não ruído.
Um exemplo resolvido
Compra de BTC. Sinal dispara com ; nocional de papel de $600.000. Primeira ordem-filha reconhecida com mid . Nos dois minutos seguintes, 8 BTC são preenchidos a VWAP 60.072; o preço está se afastando em tendência, o algoritmo respeita seu limite, e os 2 BTC remanescentes são cancelados com mid em . Taxas combinadas de 2,5 bps sobre o nocional preenchido.
| Componente | Fórmula | USD | bps do papel |
|---|---|---|---|
| Delay | 120 | 2,0 | |
| Custo de negociação | 480 | 8,0 | |
| Oportunidade | 456 | 7,6 | |
| Taxas | 120 | 2,0 | |
| IS Total | 1.176 | 19,6 |
O extrato da exchange mostra $120. O custo real de implementar a decisão foi $1.176 — um fator de dez, com os dois maiores componentes invisíveis à contabilidade baseada em taxas. Aproximadamente 40% dele veio de quantidade que nunca foi negociada. Um relatório de TCA que analisasse apenas as execuções pontuaria essa ordem-mãe em 8 bps e a consideraria boa.

Markouts: precificando a seleção adversa
O implementation shortfall avalia a ordem-mãe. Ele não diz nada sobre a qualidade das execuções individuais — especificamente, se você sistematicamente negocia com contrapartes que sabem algo que você não sabe. Isso é medido por markouts: marcar cada execução ao mid em um horizonte fixo após ela ter ocorrido.
onde é o preço de execução no momento e é o mid no momento . Este é o PnL por unidade a mercado da execução no horizonte , e tem uma anatomia limpa em : uma execução de maker começa em meio-spread (você comprou no bid, o mid está acima de você); uma execução de taker começa em meio-spread. O que acontece à medida que cresce é o conteúdo informacional da negociação:
- s isola sniping e captura de cotação obsoleta. Se suas execuções de maker já estão no vermelho um segundo depois de preenchidas, participantes mais rápidos estão acertando suas cotações no momento em que ficam mal precificadas — seu loop de atualização de cotação é mais lento que o loop de gatilho deles. Este markout é um diagnóstico de latência, não um diagnóstico de estratégia.
- s mede a seleção adversa clássica: execuções seguidas de movimento continuado através do seu preço. Para um market maker, este é o custo que a captura de spread precisa superar; a microestrutura de equities chama a grandeza relacionada de realized spread, institucionalizada no reporte da SEC Rule 605 em um horizonte de 5 minutos. Cripto se move mais rápido; 10–60s é a faixa equivalente.
- s diz se as execuções carregam momentum contra você além do horizonte de microestrutura — para estratégias de taker, se o alfa do seu sinal em 60s supera o spread mais impacto que você pagou para entrar. Uma curva de markout de taker que começa em bps e nunca cruza zero é uma estratégia que paga por entradas que seu alfa não consegue financiar.
A assimetria maker/taker é toda a economia da negociação passiva resumida em duas curvas. Uma curva de maker que começa em bps (meio-spread) e decai para bps aos 60s diz: você captura o spread e devolve mais, e nenhum nível de rebate corrige isso. A mesma curva se estabilizando em bps diz que o motor de cotação está se sustentando. Isso é também precisamente o que backtests ingênuos de touch-fill ignoram — um simulador que preenche você sempre que o preço toca seu limite ignora que ser preenchido está correlacionado com estar errado, motivo pelo qual o degrau consciente de fila da escada de simulação de preenchimento precisa de markouts medidos como entrada, e não como suposição.

O cálculo é um merge_asof sobre seu próprio stream de mid:
import pandas as pd
def markouts(fills: pd.DataFrame, mids: pd.DataFrame,
horizons=("1s", "10s", "60s")) -> pd.DataFrame:
"""fills: [ts, price, qty, side, liquidity]; mids: [ts, mid].
Both UTC-indexed and sorted. Mid stream must be from YOUR captured
feed, not candles reconstructed later."""
fills = fills.sort_values("ts").reset_index(drop=True)
mids = mids.sort_values("ts")
out = fills.copy()
for h in horizons:
probe = fills[["ts"]].copy()
probe["ts"] = probe["ts"] + pd.Timedelta(h)
m = pd.merge_asof(probe, mids, on="ts", direction="backward")
out[f"mo_{h}"] = (fills["side"] * (m["mid"].values - fills["price"])
/ fills["price"] * 1e4)
return out
def markout_report(mo: pd.DataFrame) -> pd.DataFrame:
"""Qty-weighted markouts by liquidity flag. Weighting matters:
a 0.001 BTC fill and a 2 BTC fill are not equal evidence."""
cols = [c for c in mo.columns if c.startswith("mo_")]
def agg(g):
w = g["qty"] / g["qty"].sum()
return pd.Series({c: (g[c] * w).sum() for c in cols}
| {"n": len(g), "qty": g["qty"].sum()})
return mo.groupby("liquidity").apply(agg)
Fatie o relatório ainda mais por símbolo, hora do dia e distância da cotação em relação ao mid. O corte mais acionável para uma estratégia de maker é markout por posição-na-fila-no-momento-da-execução: execuções na frente de uma fila fresca precificam de forma muito diferente de execuções em que o nível foi varrido através de você.
O pipeline: o que registrar
TCA morre na camada de logging, não na camada matemática. A matemática acima precisa de números que a maioria dos bots descarta, e nenhum deles pode ser reconstruído depois a partir do histórico da exchange. Os itens inegociáveis:
- Mid de decisão, do seu próprio feed, no momento do sinal. Não o fechamento de candle da exchange, não uma reconstrução posterior. O benchmark é "o preço que minha estratégia acreditava quando decidiu" — apenas seu processo naquele momento sabe disso.
- Ambos os timestamps: momento da decisão e momento do primeiro reconhecimento (ack), ou o custo de delay é imensurável e se mistura silenciosamente ao custo de negociação.
- Toda execução com o timestamp da exchange, taxa e flag maker/taker — seu horário local de recebimento é poluído pela sua própria latência de entrada.
- Ordens-mãe canceladas e expiradas, registradas como tudo o mais. As ordens-mãe com zero preenchimento são as linhas mais caras da tabela.
- Um stream de mid persistente (ou stream de L1) com granularidade de 100–250ms, retido por tempo suficiente para calcular markouts. Se você já registra os books para o simulador de preenchimento, isso é gratuito.
Duas tabelas simples são suficientes:
PARENTS = {
"parent_id": "str",
"strategy": "str",
"symbol": "str",
"venue": "str",
"algo": "str", # twap | pov | sniper | quote | ...
"side": "int8", # +1 buy, -1 sell
"qty": "float64", # parent size, base units
"limit_px": "float64", # NaN for unconstrained
"decision_ts": "datetime64[ns, UTC]",
"decision_mid": "float64", # mid your feed showed at decision_ts
"arrival_ts": "datetime64[ns, UTC]", # first exchange ack
"arrival_mid": "float64",
"end_ts": "datetime64[ns, UTC]", # filled / cancelled / expired
"terminal_mid": "float64",
"mkt_volume": "float64", # market volume over [arrival_ts, end_ts]
"sigma_bps": "float64", # realized vol estimate at decision time
}
FILLS = {
"parent_id": "str",
"ts": "datetime64[ns, UTC]", # exchange timestamp
"price": "float64",
"qty": "float64",
"fee": "float64", # quote ccy, post-discount, signed
"liquidity": "str", # maker | taker
"venue": "str",
}
A função de atribuição é uma transcrição direta da decomposição:
import numpy as np
def is_decomposition(p: pd.Series, fills: pd.DataFrame) -> dict:
"""Expanded implementation shortfall for one parent, bps of paper
notional. Sign convention: positive = cost."""
s, X = p["side"], p["qty"]
paper = X * p["decision_mid"]
x = fills["qty"].sum()
delay = s * X * (p["arrival_mid"] - p["decision_mid"])
trade = s * ((fills["price"] - p["arrival_mid"]) * fills["qty"]).sum()
oppty = s * (X - x) * (p["terminal_mid"] - p["arrival_mid"])
fees = fills["fee"].sum()
bps = lambda v: 1e4 * v / paper
return {
"parent_id": p["parent_id"],
"delay_bps": bps(delay), "trade_bps": bps(trade),
"oppty_bps": bps(oppty), "fees_bps": bps(fees),
"is_bps": bps(delay + trade + oppty + fees),
"fill_ratio": x / X,
"participation": x / max(p["mkt_volume"], x),
}
def tca_table(parents: pd.DataFrame, fills: pd.DataFrame) -> pd.DataFrame:
fg = dict(tuple(fills.groupby("parent_id")))
empty = fills.iloc[0:0]
rows = [is_decomposition(p, fg.get(p["parent_id"], empty))
for _, p in parents.iterrows()]
return parents.merge(pd.DataFrame(rows), on="parent_id")
E as consultas de atribuição, que é onde o TCA deixa de ser contabilidade e passa a ser pesquisa:
tca = tca_table(parents, fills)
def wavg(g: pd.DataFrame, col: str) -> float:
w = g["qty"] * g["decision_mid"] # notional weights
return (g[col] * w).sum() / w.sum()
comp = ["delay_bps", "trade_bps", "oppty_bps", "fees_bps", "is_bps"]
by_strat = tca.groupby("strategy").apply(
lambda g: pd.Series({c: wavg(g, c) for c in comp}
| {"is_std": g["is_bps"].std(), "n": len(g)}))
done = tca[tca["fill_ratio"] > 0.99]
done["pov_bin"] = pd.qcut(done["participation"], 6)
impact_curve = done.groupby("pov_bin").apply(lambda g: wavg(g, "trade_bps"))
tca["hour"] = tca["arrival_ts"].dt.hour
by_hour = tca.groupby("hour").apply(lambda g: wavg(g, "is_bps"))
by_venue = tca.groupby("venue").apply(
lambda g: pd.Series({"is_bps": wavg(g, "is_bps"),
"is_std": g["is_bps"].std(),
"fill_ratio": g["fill_ratio"].mean(), "n": len(g)}))
A consulta 4 é a tabela de entrada sobre a qual um smart order router otimiza — roteamento sem TCA por venue é roteamento pela tabela de taxas, ou seja, pelo menor componente de custo (Smart order routing em cripto parte dessa tabela como ponto de partida). Junto com o módulo de markout, isso é o prometido ~200 linhas.
Fechando o ciclo: calibrando o modelo de custos do backtest
Aqui é onde o pipeline se paga. Seu backtest afirma números: slippage_bps = 5, uma curva de probabilidade de preenchimento, um coeficiente de impacto. Cada um deles é uma afirmação sobre execução ao vivo, e a tabela de TCA é execução ao vivo. O ciclo: medir com TCA, ajustar o modelo de custos, rodar o backtest com o modelo ajustado, negociar, remedir.

Para o custo de taker, tome emprestada a forma funcional de Almgren et al. (2005) em vez de inventar uma. O resultado deles — custo proporcional à volatilidade diária multiplicada por uma potência da taxa de participação — dá um modelo de dois parâmetros:
com a estimativa de equities como prior. Ajuste sobre médias agrupadas (bins), não sobre ordens-mãe brutas — custos de ordens-mãe individuais são dominados por ruído (esse é o termo de risco de timing), e uma regressão log-log sobre dados brutos vai ajustar o ruído com prazer:
done = tca[(tca["fill_ratio"] > 0.99) & (tca["participation"] > 0)]
done["norm_cost"] = done["trade_bps"] / done["sigma_bps"]
bins = done.groupby(pd.qcut(done["participation"], 8)).agg(
pov=("participation", "mean"), cost=("norm_cost", "mean"))
bins = bins[bins["cost"] > 0] # can't log a negative bin
b, log_a = np.polyfit(np.log(bins["pov"]), np.log(bins["cost"]), 1)
def taker_cost_bps(participation, sigma_bps, a=np.exp(log_a), b=b):
return a * sigma_bps * participation ** b
Depois, seja honesto quanto às margens de erro. A equipe de Almgren teve 700.000 ordens e ainda assim reportou o expoente de impacto temporário como ; um bot com 2.000 ordens-mãe não tem margem para estimar livremente por símbolo. O regime prático: agrupe entre símbolos após normalizar por , encolha fortemente em direção a 0,6 (ou simplesmente fixe-o e ajuste apenas ), e reajuste mensalmente. Se seu ajustado deriva para cima, sua pegada cresceu ou o mercado ficou mais raso — de qualquer forma, o backtest precisava saber disso.
Para estratégias de maker, os alvos de calibração são diferentes e vêm dos outros dois módulos:
- Probabilidade de preenchimento: o modelo de fila do simulador de preenchimento prevê por célula (distância-do-spread, posição-na-fila); seu log de ordens-mãe fornece as taxas de preenchimento realizadas por célula. Discordância é um bug do simulador com uma referência de grade anexada.
- Seleção adversa: substitua a suposição implícita do simulador de que "execuções são intercambiáveis" pela tabela de markout medida — uma execução de maker simulada à distância na hora carrega o medido como um desconto imediato a mercado. Essa única mudança é a diferença entre um backtest de maker que alucina e um que rastreia a realidade; foi a entrada de calibração faltante apontada no artigo de simulação de preenchimento.
- Dispersão de custo: alimente o backtest com a distribuição do IS, não sua média. Um modelo de custo que apenas desloca a média não consegue reproduzir os drawdowns que o risco de timing cria; mesmo uma lognormal ajustada ao IS por ordem-mãe supera uma constante.
O tratamento completo das curvas de slippage — formas funcionais, condicionamento por regime, quando a lei de potência se rompe — é um artigo à parte: Curvas de slippage e modelos de custo para backtests. O ponto aqui é arquitetural: os modelos daquele artigo são inajustáveis sem as tabelas deste artigo.
Armadilhas
A literatura de TCA tem décadas, e as formas de manipulá-la também. Três modos de falha respondem pela maior parte do autoengano.
Manipulação de benchmark. Qualquer benchmark que não seja o preço de chegada pode ser "abraçado" (seguido de perto artificialmente). O clássico é o VWAP: um algoritmo avaliado contra o VWAP do intervalo pode acompanhá-lo dentro de um basis point enquanto a posição sangra vinte contra a chegada, porque o benchmark deriva junto com o preço que você está empurrando — e em participação relevante, suas próprias execuções são o VWAP, então acompanhá-lo é corrigir a própria lição de casa. Dissecamos a política dos benchmarks em TWAP vs VWAP vs POV; a regra do lado do TCA é mais simples: benchmarks são escolhidos antes de negociar, e o IS versus chegada é sempre calculado mesmo quando um agendador é avaliado contra seu próprio benchmark de agendamento. A variante mais sutil é a manipulação de chegada: se o componente que define decision_ts consegue enxergar momentum de curto prazo, ele pode cronometrar "decisões" para embelezar o termo de delay. Timestamps de decisão pertencem à camada de sinal, registrados antes que qualquer lógica de execução seja executada.
Viés de sobrevivência na análise apenas-de-execuções. Condicione sua análise de custo às execuções e a execução passiva parece gratuita. Concretamente: 100 ordens-mãe de compra passivas, um tick abaixo do mid. Sessenta são preenchidas e — sendo passivas — preenchidas a preços em média 3 bps melhores que a chegada: "custo" medido de bps, um relatório do qual se orgulhar. As quarenta que nunca foram preenchidas foram exatamente aquelas em que o preço subiu e fugiu; marque-as em 25 bps adversos no cancelamento e o número honesto é bps. O relatório apenas-de-execuções e o relatório verdadeiro diferem por 11 bps e por sinal. Este não é um caso de borda — é o próprio mecanismo da negociação passiva: ser preenchido está correlacionado com o preço passar através de você, o mesmo condicionamento que torna os backtests de touch-fill uma fantasia. O custo de oportunidade não é um refinamento opcional do IS; é o termo que defende toda a medição contra o viés de seleção. O mesmo viés tem uma variante de taker: ordens IOC que falharam, ordens rejeitadas por rate limits ou checagens de risco — se as falhas não forem registradas, o custo de falhar é imensurável, e ele é maior precisamente nos mercados rápidos em que sua estratégia mais queria a negociação.
Armadilhas diversas, brevemente: mids de chegada reconstruídos a partir de candles da exchange (seu feed e o candle discordam exatamente quando importa); calcular a média de bps por ordem-mãe sem pesos nocionais (mil execuções residuais superando o voto de uma ordem real); taxas registradas antes do desconto; mids de markout tirados de um venue diferente do da execução (base cross-venue disfarçada de seleção adversa); e, em perpétuos, deixar o acúmulo de funding vazar para dentro da janela de execução — funding é um custo, mas não um custo de execução, e misturá-los envenena as duas análises.
O que fazer esta semana
Adicione as duas tabelas de log ao seu bot — o esquema acima é uma lista de colunas, não um projeto. Não reconstrua retroativamente nada; duas semanas de logs honestos superam um ano de reconstruções. Rode a decomposição e os três horizontes de markout. Você vai descobrir qual componente domina (quase nunca as taxas), se suas execuções passivas são adversamente selecionadas além da captura de spread, e o quanto a constante de custo do seu backtest está distante da curva medida. Depois, conecte a curva ajustada de volta ao simulador e rode novamente o backtest que disse para você rodar essa estratégia em primeiro lugar. O portfólio de papel de Perold, trinta e oito anos depois, ainda é o único oponente honesto que o seu portfólio real tem.
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.