A função chamava-se enter_pack. Isso deveria ter sido o aviso. Um agente de investigação tinha escrito um cash-and-carry de BTC: quando o perpétuo cotava mais de oito pontos base acima do spot, comprar spot, shortar o perp, ficar parado. O funding e a base fariam o resto. O backtest preenchia as duas pernas no mesmo milissegundo, a mid, mais um tick de cortesia. A curva de equity era uma escada. Ninguém lhe chamaria empolgante. Era esse o argumento de venda.
Passámo-lo para paper numa segunda-feira.
Segunda-feira: o pack que preenchia sempre
O primeiro sinal chegou às 11:17 UTC. A base no ecrã era 9.4 bps, perp caro. No backtest, enter_pack devolveu duas execuções e um residual de zero. Em paper, devolveu uma execução de spot de 0.40 BTC no ask, e um short em espera no perp que não transacionou.
O spot tinha pago uma comissão taker de 4 bp e cerca de 1 bp de spread. O preço do perp que queríamos ainda estava no livro, depois já não estava. Vinte segundos depois a base cotada era 5 bps, porque tínhamos acabado de levantar o spot. Quarenta segundos depois o perp executou, depois do movimento. Ficámos packed. Ficámos packed a uma base que já não pagava a entrada e a saída.
O backtest teria registado «+9.4 bps de carry, residual 0.» O blotter tinha 4 bps de custo taker no spot, uma long sem cobertura de 40 segundos, e um gap de entrada de cerca de 5. Tinha uma amostra de um e, ainda assim, demasiada confiança na escada.
Terça-feira: abrimos a função
enter_pack recebia uma lista de pernas e um mid alvo. Fazia um ciclo, preenchia cada perna a mid * (1 ± slip), e escrevia uma linha de portefólio. Não havia um segundo timestamp. Não havia um ramo em que a perna dois não existisse. O teste unitário garantia residual == 0.
É um teste que nos faz sentir adultos. Nunca falha.
Partimos o pack em duas ordens com estado próprio: submetida, parcial, preenchida, cancelada. O incompleto passou a objeto de primeira classe. A reprodução histórica ainda não tinha um livro real, por isso fizemos batota. Atrasar a segunda execução com um sorteio da espera de segunda-feira (média de 26 segundos, uma cauda de que não gostei), e desviar-lhe o preço pelo que o spot fez nessa janela. Tosco, mas a escada ganhou entalhes antes do almoço: packs que entravam a 3 bps, packs que nunca chegaram a ser packs.
Quarta-feira: três segundos, e deita-se fora
Se a segunda perna não estiver on dentro de três segundos, fechar a primeira. Devíamos ter começado por aí. Soa conservador até se precificar o aborto. Fechar significa que se cruzou o spot para entrar e cruza-se para sair. Duas comissões taker, dois spreads, sem carry, sem funding. Na reprodução reescrita esses abortos não eram raros. Aglomeravam-se nas barras em que a base parecia mais «real», que são normalmente as barras em que já está a fluir alguma coisa pelo spot.
Na noite de quarta-feira tínhamos dezassete sinais em paper com que discutir.
| Backtest, segunda-feira de manhã | Paper, até quarta-feira | |
|---|---|---|
| Entrada | As duas pernas a mid, um timestamp | Spot no ask; perp 5–40s depois, ou nunca |
| Residual | Sempre 0 | Sem cobertura em 10 de 17 sinais |
| Comissões numa operação em curso | Dois takers | Dois takers, ou quatro se abortarmos |
| Motor de PnL | Base + funding | O que o spot fez enquanto esperávamos |
Um colega ainda tem um notebook que trata o pack como um produto cotado, BTC_CARRY, com uma «taxa de execução» fixa de 2 bp. Gostava daquele notebook. Produzia a mesma escada que o agente. Uma taxa é uma média. Os órfãos são uma distribuição. As médias não são atropeladas numa hora ilíquida; 0.40 BTC de spot residual podem.
Quinta-feira: residual, depois café
Três segundos abortavam quase tudo quando também estávamos à espera numa fila. Alargámos o paper para quinze. Às 02:11 UTC de quinta-feira, o spot executou e o perp não. Ainda estava na fase de luto do «vai ficar em espera, vai levantar» quando fui fazer café.
02:11:08 preenchido BTCUSDT 0.40 buy
02:11:08 submetido BTCUSDT-PERP 0.40 sell, post-only
02:11:41 ainda a trabalhar
02:26:00 mark do residual −18.1 bps
O BTC desvalorizou 18 bps numa janela calma. O residual era a operação inteira. O funding, quando imprimiu, creditou um perp que não tínhamos. O spot ficou ali, simplesmente long. O percurso overnight do backtest original nunca continha uma posição de uma só perna, por isso nunca tinha feito a pergunta que fazemos a qualquer outro livro: qual é a pior barra que este residual pode imprimir, e onde está o stop para uma posição que não pretendíamos manter?
Os sete packs completos também não eram heróis. Quando a segunda perna imprimiu, o nosso próprio lift no spot já tinha pressionado o índice, e a base de entrada estava 3–5 bps pior do que o snapshot que tinha disparado o sinal. O limiar de oito bps era um número acerca de um ecrã.
O que evitaríamos da próxima vez
Não lançaríamos um multi-leg como uma única função de fill. A mesma regra para um carry perp-spot, um calendar, ou o call vertical que um agente submete como dois limits e depois celebra quando ambos «preenchem» a mid.
- Estado de ordem por perna, incluindo parciais. Tratar o pack como pai de dois tickets.
- Um timer de órfão máximo que realmente dispara, e um caminho de fecho que paga dois spreads à saída.
- Mark-to-book no residual, com um stop dimensionado para o que sobra. O líquido packed é o nocional errado.
- Sem carry creditado, sem funding acumulado, até as duas pernas estarem on. Incompleto é incompleto.
- Recusar mid-on-mid como predefinição para qualquer coisa que tenha dois tickets. Se for preciso adivinhar, adivinhar pior na perna ilíquida e mais tarde na segunda execução.
E não deixaríamos test_pack_is_atomic entrar no repositório. Atomic é uma propriedade de um matching engine, não de uma função auxiliar. O spot de BTC e o perpétuo de BTC são dois livros que às vezes rimam. O paper é onde deixam de rimar, o que é a única razão pela qual a escada alguma vez foi interessante.



