A função se chamava enter_pack. Isso já deveria ter sido o aviso. Um agente de pesquisa tinha escrito um cash-and-carry de BTC: quando o perpetual negociasse mais de oito basis points acima do spot, comprar o spot, vender o perp, ficar parado. O funding e o basis fariam o resto. O backtest preenchia as duas pernas no mesmo milissegundo, no mid, mais um tick de educação. A curva de equity era uma escada. Ninguém chamaria de empolgante. Esse era o argumento de venda.
Promovemos para paper numa segunda-feira.
Segunda-feira: o pacote que sempre preenchia
O primeiro sinal chegou às 11:17 UTC. O basis na tela era 9.4 bps, perp rico. No backtest, enter_pack retornava dois fills e um residual de zero. No paper, retornou um fill de spot de 0.40 BTC no ask, e um short em espera no perp que não negociou.
O spot tinha pago uma taxa taker de 4 bp e cerca de 1 bp de spread. O preço do perp que queríamos ainda estava no book, depois não estava. Vinte segundos depois, o basis cotado era 5 bps, porque tínhamos acabado de fazer o lift do spot. Quarenta segundos depois o perp preencheu, depois do movimento. Estávamos empacotados. Estávamos empacotados num basis que já não pagava entrar e sair.
O backtest teria registrado "+9.4 bps de carry, residual 0." O blotter tinha 4 bps de custo taker no spot, um long sem hedge de 40 segundos, e um gap de entrada de cerca de 5. Eu tinha uma amostra de um e, ainda assim, confiança demais na escada.
Terça-feira: abrimos a função
enter_pack recebia uma lista de pernas e um mid alvo. Fazia um loop, preenchia cada perna em mid * (1 ± slip) e gravava uma linha de portfólio. Não havia um segundo timestamp. Não havia um branch em que a perna dois não existisse. O teste unitário afirmava residual == 0.
Esse é o teste que te faz sentir adulto. Nunca vai falhar.
Quebramos o pacote em duas ordens com estado próprio: enviada, parcial, preenchida, cancelada. Incompleto virou um objeto de primeira classe. O replay histórico ainda não tinha um book de verdade, então trapaceamos. Atrasar o segundo fill com um sorteio da espera de segunda-feira (média de 26 segundos, uma cauda que eu não gostei) e deslocar o preço pelo que o spot fez naquela janela. Cru, mas a escada ganhou entalhes antes do almoço: pacotes que entravam a 3 bps, pacotes que nunca viravam pacotes.
Quarta-feira: três segundos, depois joga fora
Se a segunda perna não estiver montada dentro de três segundos, zera a primeira. Deveríamos ter começado por aí. Parece conservador até você precificar o aborto. Zerar significa que você cruzou o spot para entrar e cruza de novo para sair. Duas taxas taker, dois spreads, sem carry, sem funding. No replay reescrito, esses abortos não eram raros. Eles se agrupavam nas barras em que o basis parecia mais "real", que costumam ser as barras em que alguma coisa já está fluindo pelo spot.
Na noite de quarta-feira já tínhamos dezessete sinais de paper para discutir.
| Backtest, segunda de manhã | Paper, até quarta-feira | |
|---|---|---|
| Entrada | As duas pernas no mid, um timestamp | Spot no ask; perp 5–40s depois, ou nunca |
| Residual | Sempre 0 | Sem hedge em 10 de 17 sinais |
| Taxas num trade que funciona | Dois takers | Dois takers, ou quatro se abortarmos |
| Motor do PnL | Basis + funding | O que o spot fez enquanto esperávamos |
Um colega ainda tem um notebook que trata o pacote como um produto listado, BTC_CARRY, com uma "taxa de execução" fixa de 2 bp. Eu gostava daquele notebook. Produzia a mesma escada que o agente. Uma taxa é uma média. Órfãos são uma distribuição. Médias não são atropeladas numa hora fina; 0.40 BTC de spot sobrando, sim.
Quinta-feira: sobra, depois café
Três segundos abortavam quase tudo quando também estávamos esperando na fila. Afrouxamos o paper para quinze. Às 02:11 UTC de quinta-feira, o spot preencheu e o perp não. Eu ainda estava na fase de luto do "vai restar, vai levantar" quando fui fazer café.
02:11:08 preenchido BTCUSDT 0.40 compra
02:11:08 enviado BTCUSDT-PERP 0.40 venda, post-only
02:11:41 ainda ativa
02:26:00 mark do residual −18.1 bps
O BTC caiu 18 bps numa janela quieta. O residual era o trade inteiro. O funding, quando imprimiu, creditou um perp que não tínhamos. O spot só ficou ali sendo long. O caminho overnight do backtest original nunca continha uma posição de uma perna só, então nunca tinha feito a pergunta que fazemos a qualquer outro book: qual é a pior barra que essa sobra pode imprimir, e onde fica o stop de uma posição que não pretendíamos carregar?
Os sete pacotes completos também não foram heróis. Quando a segunda perna imprimiu, nosso próprio lift de spot já tinha pressionado o índice, e o basis de entrada estava 3–5 bps pior do que o snapshot que disparou o sinal. O limiar de oito bps era um número sobre uma tela.
O que pularíamos da próxima vez
Não entregaríamos um multi-leg como uma função única de fill. A mesma regra para um carry perp-spot, um calendar, ou a vertical de call que um agente envia como dois limits e depois comemora quando os dois "preenchem" no mid.
- Estado de ordem por perna, inclusive parciais. Trate o pacote como pai de dois tickets.
- Um timer máximo de órfão que de fato dispara, e um caminho de zeragem que paga dois spreads na saída.
- Mark-to-book no residual, com um stop dimensionado para a sobra. O net empacotado é o nocional errado.
- Nenhum carry creditado, nenhum funding acumulado, até as duas pernas estarem montadas. Incompleto é incompleto.
- Recuse mid-on-mid como padrão para qualquer coisa que tenha dois tickets. Se precisar chutar, chute pior na perna fina e mais tarde no segundo fill.
E não deixaríamos test_pack_is_atomic entrar no repo. Atômico é propriedade de um matching engine, não de uma função auxiliar. BTC spot e BTC perpetual são dois books que às vezes rimam. O paper é onde eles param de rimar, e é só por isso que a escada alguma vez foi interessante.



