Implementation shortfall y TCA casero: midiendo lo que la ejecución realmente te cuesta
Todo desk institucional tiene un pipeline de análisis de costos de transacción. Casi nadie que opera un bot cripto lo tiene. La configuración típica registra los fills, suma las comisiones y llama "slippage" a la diferencia entre el PnL del backtest y el PnL en vivo — un único residuo sin explicar que absorbe latencia, spread, impacto, selección adversa y cada orden sin ejecutar que se escapó. No puedes corregir un costo que mides como un solo número. La maquinaria para medirlo correctamente existe desde 1988, no es propietaria, y sobre tus propios registros de fills son apenas unas 200 líneas de Python. Este artículo la construye.
El resultado no es un dashboard más bonito. Tu backtest contiene un modelo de costos — una constante de slippage, una probabilidad de fill, un coeficiente de impacto — y cada parámetro ahí es actualmente una suposición. El TCA sobre fills en vivo es la única verdad de referencia contra la cual esos parámetros pueden calibrarse. Construimos el lado de la simulación de ese ciclo en Simulación de fills: la escalera desde la fantasía del precio de cierre hasta la realidad consciente de la cola; este artículo construye el lado de la medición.
Papel versus realidad: qué midió Perold en realidad
El truco fundacional se debe a André Perold (1988, "The Implementation Shortfall: Paper Versus Reality," Journal of Portfolio Management 14(3), 4–9). Se corren dos carteras en paralelo. La cartera en papel ejecuta cada decisión instantáneamente, en tamaño ilimitado, a costo cero, al precio vigente en el momento de la decisión. La cartera real es lo que tu bot efectivamente hizo: fills parciales, cotizaciones perseguidas, remanentes cancelados, comisiones. El implementation shortfall es la diferencia entre sus retornos.
La definición importa por lo que se niega a ocultar. Un estado de comisiones muestra las comisiones. Un reporte de fill versus precio límite no muestra nada en absoluto (nunca ejecutas peor que tu límite — por construcción). La cartera en papel te cobra por todo: la deriva entre la decisión y la llegada, el spread que cruzaste, el impacto que causaste y — de forma crucial — las órdenes que nunca se ejecutaron mientras el precio se escapaba. Wagner y Edwards (1993, "Best Execution," Financial Analysts Journal 49(1), 65–71) llamaron a las comisiones visibles la punta del iceberg; para cualquier cosa con rotación, la parte sumergida domina.
Fijemos la notación. Una orden padre: lado (compra/venta), tamaño . El precio de decisión es el mid que vio tu estrategia cuando se disparó la señal. Los fills llegan como con cantidad total ejecutada . En el horizonte (padre completado, cancelado o vencido), el mid es . Las comisiones explícitas son . El implementation shortfall en términos monetarios:
normalizado a puntos básicos dividiendo por el nocional en papel . Positivo significa que pagaste. El primer término es lo que costaron tus fills respecto a la cartera en papel; el segundo es el costo de oportunidad de Perold — el remanente sin ejecutar marcado al precio al que se escapó; el tercero es la única parte que tu exchange admite en su estado de cuenta.
Una nota de convención: la industria dice "precio de llegada" (arrival price), y en la mayoría del TCA de renta variable llegada significa el mid cuando la orden alcanzó el mercado. Para un bot, el momento de decisión y el momento de llegada difieren por tu propia latencia interna más la cola de rate-limit — un costo real y medible. Así que mantenemos ambas marcas de tiempo y ambos precios, y dejamos que la descomposición los separe.
La descomposición: delay, impacto, timing, oportunidad, comisiones
Un único número de IS te dice que la ejecución es cara. No te dice por qué, y las soluciones para los distintos componentes son completamente diferentes — no resuelves el costo de delay y el costo de impacto con el mismo cambio. El implementation shortfall expandido de Robert Kissell (Kissell, 2006, "The Expanded Implementation Shortfall: Understanding Transaction Cost Components," Journal of Trading 1(3), 6–16; desarrollado con extensión de libro en The Science of Algorithmic Trading and Portfolio Management, Academic Press, 2013) desagrega el total en componentes, cada uno atribuible a una etapa distinta del ciclo de vida de la orden. La versión del practicante, usando el mid de llegada (mid en el primer acuse de recibo del exchange):
La identidad se reduce exactamente a la definición de Perold — se expanden los términos y se cancela. Cada pieza tiene un dueño distinto:
Costo de delay : la deriva de precio entre el disparo de la señal y que tu primera orden hija esté activa en el exchange. Esto es tu infraestructura — serialización, red, colas de rate-limit, controles de riesgo. Si tu señal tiene alfa genuino de corto horizonte, el costo de delay es donde se filtra primero; una media consistentemente positiva dice que el mercado se mueve a tu favor antes de que llegues — alfa de momentum en descomposición, o alguien más rápido operando la misma señal.
Costo de trading : lo que pagaron tus fills respecto a la llegada — spread cruzado más impacto de mercado más deriva intra-schedule. Esta es la libreta de calificaciones del algoritmo de ejecución, y la cantidad que la investigación en ejecución realmente modela. Almgren, Thum, Hauptmann y Li (2005, "Direct Estimation of Equity Market Impact," Risk 18(7), 58–62) la midieron sobre aproximadamente 700.000 órdenes de renta variable estadounidense de los desks de Citigroup (diciembre 2001–junio 2003) y encontraron que el costo de trading escala con la volatilidad diaria y la tasa de participación — impacto temporal siguiendo una ley de potencia en la tasa de trading con exponente cercano a 3/5, impacto permanente cercano a lineal en el tamaño. Reutilizaremos su forma funcional cuando calibremos.
Riesgo de timing: no es un término en la descomposición de la media sino la varianza alrededor de ella. Repartir una orden padre en el tiempo reduce el impacto esperado y te expone a la volatilidad; para un schedule con posición remanente la desviación estándar del costo escala como . Este es precisamente el trade-off que optimiza el marco de Almgren–Chriss. En tu reporte de TCA aparece como la dispersión del IS entre órdenes padre — reporta la desviación estándar junto a cada media, o la media se llevará toda la atención y las colas se llevarán todo tu dinero.
Costo de oportunidad : la cantidad sin ejecutar marcada al precio terminal. Para estrategias pasivas este es rutinariamente el componente más grande y menos examinado, y es el término que hace honesto a todo el marco — más sobre esto en la sección de trampas, porque omitirlo es la forma más común en que la gente se engaña a sí misma con el TCA.
Comisiones : la parte explícita. En cripto, registra las comisiones después de descuentos (niveles VIP, rebates en token convertidos al precio en el momento del fill) y mantén los rebates de maker con su signo — una comisión negativa es dato, no ruido.
Un ejemplo resuelto
Compra BTC. La señal se dispara con ; nocional en papel $600.000. La primera orden hija se confirma con mid . Durante los siguientes dos minutos, 8 BTC se ejecutan a un VWAP de 60.072; el precio tiende a alejarse, el algoritmo respeta su límite, y los 2 BTC restantes se cancelan con el mid en . Comisión combinada 2,5 pb sobre el nocional ejecutado.
| Componente | Fórmula | USD | pb del nocional en papel |
|---|---|---|---|
| Delay | 120 | 2,0 | |
| Costo de trading | 480 | 8,0 | |
| Oportunidad | 456 | 7,6 | |
| Comisiones | 120 | 2,0 | |
| IS total | 1.176 | 19,6 |
El estado de cuenta del exchange muestra $120. El costo real de implementar la decisión fue $1.176 — un factor de diez, con los dos componentes más grandes invisibles para una contabilidad basada en comisiones. Aproximadamente el 40% provino de cantidad que nunca se operó. Un reporte de TCA que solo analizara los fills calificaría esta orden padre con 8 pb y la daría por buena.

Markouts: poniendo precio a la selección adversa
El implementation shortfall califica la orden padre. No dice nada sobre la calidad de los fills individuales — específicamente, si sistemáticamente operas con contrapartes que saben algo que tú no sabes. Eso se mide con markouts: marcar cada fill al mid en un horizonte fijo después de que ocurrió.
donde es el precio del fill en el momento y es el mid en el momento . Este es el PnL a mercado por unidad del fill en el horizonte , y tiene una anatomía limpia en : un fill de maker empieza en medio spread (compraste al bid, el mid está por encima tuyo); un fill de taker empieza en medio spread. Lo que ocurre a medida que crece es el contenido informativo del trade:
- s aísla el sniping y la captura de cotizaciones obsoletas. Si tus fills de maker ya están en pérdida un segundo después de ejecutarse, participantes más rápidos están golpeando tus cotizaciones en el momento en que quedan mal cotizadas — tu ciclo de actualización de cotizaciones es más lento que su ciclo de disparo. Este markout es un diagnóstico de latencia, no un diagnóstico de estrategia.
- s mide la selección adversa clásica: fills seguidos de movimiento continuado a través de tu precio. Para un market maker este es el costo que la captura de spread debe superar; la microestructura de renta variable llama a la cantidad relacionada realized spread, institucionalizada en el reporte de la Regla 605 de la SEC a un horizonte de 5 minutos. Cripto se mueve más rápido; 10–60s es la banda equivalente.
- s te dice si los fills cargan momentum en tu contra más allá del horizonte de microestructura — para estrategias de taker, si el alfa de tu señal a 60s supera el spread-más-impacto que pagaste por entrar. Una curva de markout de taker que empieza en pb y nunca cruza cero es una estrategia que paga por entradas que su alfa no puede financiar.
La asimetría maker/taker es toda la economía del trading pasivo condensada en dos curvas. Una curva de maker que empieza en pb (medio spread) y decae a pb hacia los 60s dice: capturas el spread y devuelves más, y ningún nivel de rebate arregla eso. La misma curva estabilizándose en pb dice que el motor de cotización se gana el sustento. Esto es también precisamente lo que los backtests ingenuos de fill-al-toque dan por sentado — un simulador que te ejecuta cada vez que el precio toca tu límite ignora que ser ejecutado está correlacionado con estar equivocado, razón por la cual el escalón consciente de la cola de la escalera de simulación de fills necesita markouts medidos como insumo en lugar de como supuesto.

El cálculo es un merge_asof sobre tu propio 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)
Segmenta el reporte aún más por símbolo, hora del día y distancia de la cotización respecto al mid. El corte más accionable para una estrategia de maker es markout por posición en la cola al momento del fill: los fills al frente de una cola fresca tienen un precio muy diferente de los fills donde el nivel fue arrasado a través tuyo.
El pipeline: qué registrar
El TCA muere en la capa de logging, no en la capa matemática. Las matemáticas de arriba necesitan números que la mayoría de los bots descartan, y ninguno de ellos puede reconstruirse después a partir del historial del exchange. Los no negociables:
- Mid de decisión, de tu propio feed, en el momento de la señal. No el cierre de la vela del exchange, no una reconstrucción posterior. El benchmark es "el precio que mi estrategia creía cuando decidió" — solo tu proceso en ese momento lo sabe.
- Ambas marcas de tiempo: momento de decisión y momento del primer acuse de recibo, o el costo de delay es inmedible y se fusiona silenciosamente con el costo de trading.
- Cada fill con la marca de tiempo del exchange, la comisión y el flag maker/taker — tu hora local de recepción está contaminada por tu propia latencia de entrada.
- Órdenes padre canceladas y vencidas, registradas como todo lo demás. Las órdenes padre con cero fills son las filas más caras de la tabla.
- Un stream de mid persistente (o stream L1) con granularidad de 100–250ms, retenido el tiempo suficiente para calcular markouts. Si ya registras libros de órdenes para el simulador de fills, esto es gratis.
Dos tablas planas son 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",
}
La función de atribución es una transcripción directa de la descomposición:
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")
Y las consultas de atribución, que es donde el TCA deja de ser contabilidad y pasa a ser investigación:
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)}))
La consulta 4 es la tabla de entrada que un smart order router optimiza — enrutar sin TCA por venue es enrutar por tabla de comisiones, es decir, por el componente de costo más pequeño (Smart order routing en cripto toma esta tabla como su punto de partida). Junto con el módulo de markouts, esto es lo prometido: ~200 líneas.
Cerrando el ciclo: calibrando el modelo de costos del backtest
Aquí es donde el pipeline se paga solo. Tu backtest afirma números: slippage_bps = 5, una curva de probabilidad de fill, un coeficiente de impacto. Cada uno de ellos es una afirmación sobre la ejecución en vivo, y la tabla de TCA es la ejecución en vivo. El ciclo: medir con TCA, ajustar el modelo de costos, correr el backtest con el modelo ajustado, operar, volver a medir.

Para el costo de taker, toma prestada la forma funcional de Almgren et al. (2005) en lugar de inventar una. Su resultado — costo proporcional a la volatilidad diaria multiplicada por una potencia de la tasa de participación — da un modelo de dos parámetros:
con la estimación de renta variable como prior. Ajústalo sobre medias agrupadas en bins, no sobre órdenes padre crudas — los costos de órdenes padre individuales están dominados por el ruido (ese es el término de riesgo de timing), y una regresión log-log sobre datos crudos ajustará alegremente el ruido:
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
Después sé honesto respecto a los márgenes de error. El equipo de Almgren tenía 700.000 órdenes y aun así reportó el exponente de impacto temporal como ; un bot con 2.000 órdenes padre no puede permitirse estimar libremente por símbolo. El régimen práctico: agrupa entre símbolos tras normalizar por , contrae fuertemente hacia 0,6 (o simplemente fíjalo y ajusta solo ), y reajusta mensualmente. Si tu ajustado va a la deriva hacia arriba, tu huella creció o el mercado se adelgazó — de cualquier forma el backtest necesitaba saberlo.
Para estrategias de maker los objetivos de calibración son distintos y provienen de los otros dos módulos:
- Probabilidad de fill: el modelo de cola del simulador de fills predice por celda (distancia-al-spread, posición-en-cola); tu registro de órdenes padre provee las tasas de fill realizadas por celda. El desacuerdo es un bug del simulador con una referencia de grilla adjunta.
- Selección adversa: reemplaza el supuesto implícito del simulador de que "los fills son intercambiables" con la tabla de markout medida — un fill de maker simulado a distancia en la hora carga el medido como un descuento inmediato a mercado. Este único cambio es la diferencia entre un backtest de maker que alucina y uno que rastrea la realidad; fue el insumo de calibración faltante señalado en el artículo de simulación de fills.
- Dispersión de costos: alimenta al backtest con la distribución del IS, no su media. Un modelo de costos que solo desplaza la media no puede reproducir los drawdowns que crea el riesgo de timing; incluso una lognormal ajustada al IS por orden padre supera a una constante.
El tratamiento completo de las curvas de slippage — formas funcionales, condicionamiento por régimen, cuándo se rompe la ley de potencia — es su propio artículo: Curvas de slippage y modelos de costos para backtests. El punto aquí es arquitectónico: los modelos de ese artículo son imposibles de ajustar sin las tablas de este artículo.
Trampas
La literatura de TCA tiene décadas de antigüedad, y también las formas de manipularla. Tres modos de falla explican la mayor parte del autoengaño.
Manipulación de benchmark. Cualquier benchmark que no sea el precio de llegada puede seguirse de cerca artificialmente. El clásico es el VWAP: un algoritmo calificado contra el VWAP del intervalo puede seguirlo dentro de un punto básico mientras la posición sangra veinte respecto a la llegada, porque el benchmark deriva junto con el precio que tú mismo estás empujando — y a una participación relevante tus propios prints son el VWAP, así que seguirlo es corregirse la propia tarea. Diseccionamos la política de benchmarks en TWAP vs VWAP vs POV; la regla del lado del TCA es más simple: los benchmarks se eligen antes de operar, y el IS-versus-llegada siempre se calcula incluso cuando un scheduler se califica contra su propio benchmark de scheduling. La variante más sutil es la manipulación de la llegada: si el componente que fija decision_ts puede ver momentum de corto plazo, puede cronometrar las "decisiones" para favorecer el término de delay. Las marcas de tiempo de decisión pertenecen a la capa de señal, registradas antes de que corra cualquier lógica de ejecución.
Sesgo de supervivencia en el análisis de solo-fills. Condiciona tu análisis de costos a los fills y el trading pasivo parecerá gratis. Concretamente: 100 órdenes padre de compra pasivas, un tick por debajo del mid. Sesenta se ejecutan y — al ser pasivas — se ejecutan a precios en promedio 3 pb mejores que la llegada: "costo" medido pb, un reporte del cual estar orgulloso. Las cuarenta que nunca se ejecutaron fueron exactamente aquellas donde el precio se elevó lejos; márcalas con 25 pb adversos al cancelar y el número honesto es pb. El reporte de solo-fills y el reporte verdadero difieren por 11 pb y por el signo. Esto no es un caso de esquina — es el mecanismo mismo del trading pasivo: ser ejecutado está correlacionado con que el precio pase a través tuyo, que es el mismo condicionamiento que convierte a los backtests de fill-al-toque en fantasía. El costo de oportunidad no es un refinamiento opcional del IS; es el término que defiende toda la medición contra el sesgo de selección. El mismo sesgo tiene una variante de taker: órdenes IOC que fallaron, órdenes rechazadas por rate limits o controles de riesgo — si los fallos no se registran, el costo de fallar queda sin medir, y es mayor precisamente en los mercados rápidos donde tu estrategia más quería el trade.
Errores diversos, brevemente: mids de llegada reconstruidos a partir de velas del exchange (tu feed y la vela discrepan justo cuando importa); promediar pb por orden padre sin ponderar por nocional (mil fills de polvo superando en votos a una orden real); comisiones registradas antes del descuento; mids de markout tomados de un venue distinto al del fill (la base cross-venue disfrazándose de selección adversa); y en perpetuos, dejar que el devengo de funding se filtre dentro de la ventana de ejecución — el funding es un costo, pero no un costo de ejecución, y mezclarlos envenena ambos análisis.
Qué hacer esta semana
Agrega las dos tablas de log a tu bot — el esquema de arriba es una lista de columnas, no un proyecto. No reconstruyas nada retroactivamente; dos semanas de logs honestos superan a un año de reconstrucciones. Corre la descomposición y los tres horizontes de markout. Aprenderás qué componente domina (casi nunca las comisiones), si tus fills pasivos sufren selección adversa más allá de lo que compensa la captura de spread, y qué tan lejos está la constante de costo de tu backtest de la curva medida. Después conecta la curva ajustada de vuelta al simulador y vuelve a correr el backtest que te dijo, en primer lugar, que operaras esta estrategia. La cartera en papel de Perold, treinta y ocho años después, sigue siendo el único oponente honesto que tiene tu cartera real.
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.