3 de noviembre de 2026 · ejecución

Hicimos paper trading de un basis trade. Solo se llenó una pata.

Hicimos paper trading de un basis trade. Solo se llenó una pata.

La función se llamaba enter_pack. Esa debería haber sido la advertencia. Un agente de research había escrito un cash-and-carry de BTC: cuando el perpetual cotizara más de ocho puntos básicos por encima del spot, comprar spot, short el perp, quedarse quietos. El funding y el basis harían el resto. El backtest llenaba las dos patas en el mismo milisegundo, a mid, más un tick de cortesía. La curva de equity era una escalera. Nadie la llamaría emocionante. Ese era el argumento de venta.

Lo pasamos a paper un lunes.

Lunes: el pack que siempre se llenaba

La primera señal llegó a las 11:17 UTC. El basis en pantalla era 9.4 bps, perp caro. En el backtest, enter_pack devolvía dos fills y un residual de cero. En paper devolvió un fill de spot de 0.40 BTC en el ask, y un short en reposo sobre el perp que no cruzó.

El spot había pagado una comisión taker de 4 bp y alrededor de 1 bp de spread. El precio del perp que queríamos seguía en el book, y luego ya no. Veinte segundos después el basis cotizado era 5 bps, porque acabábamos de levantar el spot. Cuarenta segundos después se llenó el perp, después del movimiento. Estábamos packed. Estábamos packed a un basis que ya no pagaba entrar y salir.

El backtest habría apuntado «+9.4 bps carry, residual 0.». El blotter tenía 4 bps de coste taker de spot, una larga sin cobertura de 40 segundos y un gap de entrada de unos 5. Tenía una muestra de uno y, aun así, demasiada confianza en la escalera.

Martes: abrimos la función

enter_pack recibía una lista de patas y un mid objetivo. Iteraba, llenaba cada pata a mid * (1 ± slip) y escribía una sola fila de portfolio. No había un segundo timestamp. No había una rama en la que la pata dos no existiera. El test unitario afirmaba residual == 0.

Ese es un test que te hace sentir un adulto. Nunca va a fallar.

Partimos el pack en dos órdenes con estado propio: submitted, partial, filled, canceled. Incomplete pasó a ser un objeto de primera clase. El replay histórico seguía sin un book real, así que hicimos trampa. Retrasar el segundo fill con un sorteo de la espera del lunes (media de 26 segundos, una cola que no me gustaba) y desplazar su precio según lo que hubiera hecho el spot en esa ventana. Tosco, pero la escalera empezó a tener muescas antes de comer: packs que entraban a 3 bps, packs que nunca llegaron a ser packs.

Miércoles: tres segundos y se tira

Si la segunda pata no está puesta en tres segundos, flatten la primera. Tendríamos que haber empezado por ahí. Suena conservador hasta que precificas el abort. Hacer flatten significa que cruzaste el spot para entrar y lo cruzas para salir. Dos comisiones taker, dos spreads, sin carry, sin funding. En el replay reescrito esos aborts no eran raros. Se agrupaban en las barras donde el basis parecía más «real», que suelen ser las barras por las que ya está fluyendo algo a través del spot.

El miércoles por la noche teníamos diecisiete señales de paper con las que discutir.

17señales de paper en cuatro días
7packs con las dos patas llenas
18 bpsmovimiento posterior contra una larga residual
Backtest, lunes por la mañanaPaper, para el miércoles
EntradaLas dos patas a mid, un solo timestampSpot en el ask; perp 5–40s después, o nunca
ResidualSiempre 0Sin cobertura en 10 de 17 señales
Comisiones en un trade que funcionaDos takersDos takers, o cuatro si abortamos
Motor del PnLBasis + fundingLo que hiciera el spot mientras esperábamos
Un pack está completo cuando las dos órdenes están llenas, o es una posición direccional para la que no dimensionaste. Marca el residual contra el book en vivo todo el tiempo que exista. Si no puedes marcarlo, no puedes correr la estrategia.

Un colega todavía tiene un notebook que trata el pack como un producto listado, BTC_CARRY, con un «impuesto de ejecución» fijo de 2 bp. Me gustaba ese notebook. Producía la misma escalera que el agente. Un impuesto es una media. Los huérfanos son una distribución. A las medias no las atropellan en una hora fina; 0.40 BTC de spot residual sí.

Jueves: residual, y luego café

Tres segundos abortaba casi todo en cuanto también esperábamos en cola. Aflojamos el paper a quince. El jueves a las 02:11 UTC se llenó el spot y el perp no. Yo seguía en la fase de duelo de «va a restar, lo van a lift» cuando fui a hacerme un café.

02:11:08 ejecutado BTCUSDT 0.40 buy
02:11:08 enviado BTCUSDT-PERP 0.40 sell, post-only
02:11:41 sigue activa
02:26:00 marca residual −18.1 bps

BTC se dejó caer 18 bps en una ventana tranquila. El residual era toda la operación. El funding, cuando imprimió, acreditó un perp que no teníamos. El spot se quedó ahí, siendo largo. El path overnight del backtest original nunca contenía una posición de una sola pata, así que nunca se había hecho la pregunta que hacemos a cualquier otro book: ¿cuál es la peor barra que puede imprimir este residual, y dónde está el stop de una posición que no queríamos tener?

Los siete packs completos tampoco eran héroes. Para cuando se imprimió la segunda pata, nuestro propio lift de spot ya había presionado el índice, y el basis de entrada estaba 3–5 bps peor que el snapshot que había disparado la señal. El umbral de ocho bps era un número acerca de una pantalla.

Lo que nos saltaríamos la próxima vez

No sacaríamos a producción un multi-leg como una sola función de fill. La misma regla para un carry perp-spot, un calendar o el call vertical que un agente manda como dos limits y luego celebra cuando ambos «fill» a mid.

  1. Estado de orden por pata, incluidos los partials. Trata el pack como padre de dos tickets.
  2. Un timer de max-orphan que de verdad dispara, y un path de flatten que paga dos spreads a la salida.
  3. Mark-to-book del residual, con un stop dimensionado para lo que queda. El neto packed es el nocional equivocado.
  4. Ni carry acreditado ni funding acumulado hasta que las dos patas estén puestas. Incomplete es incomplete.
  5. Rechazar mid-on-mid como valor por defecto para cualquier cosa con dos tickets. Si hay que adivinar, adivina peor en la pata fina y más tarde en el segundo fill.

Y no dejaríamos entrar test_pack_is_atomic al repo. Atomic es una propiedad de un matching engine, no de una función auxiliar. El spot de BTC y el perpetual de BTC son dos books que a veces riman. El paper es donde dejan de rimar, y esa es la única razón por la que la escalera alguna vez fue interesante.

backtestingcash-and-carryejecuciónpaper tradingfuturos crypto
← Todos los artículos