Um matching engine spot é uma pequena máquina de estados cercada por restrições duras: price-time priority, execuções parciais, uma cauda de latência que não se move sob rajadas de carga. É assim que o order book, o laço de casamento, o journal e o harness de replay são construídos - e onde a linguagem deixa de ajudar.
Um matching engine de exchange spot tem um trabalho estreito. Ele recebe um fluxo ordenado de comandos - nova ordem, cancelamento, substituição -, aplica-os a um order book sob price-time priority e emite um fluxo ordenado de eventos: negócios, atualizações do book, rejeições, confirmações.
Tudo que é difícil nisso vem de três restrições empilhadas sobre esse trabalho: o resultado precisa ser idêntico em cada replay da mesma entrada, o restante de uma ordem parcialmente executada precisa manter seu lugar na fila, e a cauda de latência não pode se mover quando chega uma rajada.
O book são dois lados, cada um uma coleção de níveis ordenada por preço. Um nível não é um número - é uma fila de ordens em repouso naquele preço, em ordem de chegada. O casamento toca o melhor nível constantemente e os níveis profundos raramente, então a estrutura é escolhida para esse padrão de acesso, e não pela elegância.
A consequência das filas intrusivas e dos handles de índice é que uma ordem em repouso nunca se move na memória enquanto vive. Sua posição na fila é uma propriedade dos seus elos, e não de onde ela por acaso está - e é isso que torna as execuções parciais baratas mais adiante.
Uma ordem agressiva que chega percorre o lado oposto a partir do melhor preço para dentro. Em cada nível ela percorre a fila FIFO a partir da frente. Ela para quando o preço do nível deixa de ser aceitável para a ordem que chegou ou quando a quantidade que chegou zera.
Uma ordem em repouso parcialmente executada mantém seu lugar. O restante continua na frente da sua fila com a sequência de chegada original, porque uma execução muda uma quantidade e nada mais. Uma ordem agressiva parcialmente executada que seja um limit simples vira uma ordem em repouso no fim do seu próprio nível de preço, com uma nova sequência de chegada - ela chegou agora, não antes.
A semântica dos tipos de ordem é decidida na fronteira desse laço, e não dentro dele. Immediate-or-cancel descarta o restante em vez de deixá-lo em repouso. Fill-or-kill faz uma passada seca primeiro e ou executa por inteiro ou rejeita. Post-only rejeita se a ordem cruzaria na chegada. Manter isso fora do laço faz do laço o único lugar onde o estado do book muda.
A prevenção de self-trade, as quantidades mínimas e a validação de tick e de lote também ficam antes do laço. Uma ordem que chega ao casamento já foi provada bem formada, então o laço não tem ramos de erro para atrasá-lo ou para discordar.
A latência média raramente é o problema. O problema é a pior observação durante uma rajada, que é quando o engine mais importa e quando uma pausa stop-the-world tem mais chance de cair. Sob um runtime gerenciado com garbage collector, essa pausa é agendada pelo coletor e não por você, e ela cai no meio da rajada que produziu o lixo.
A alocação manual é uma versão menor do mesmo problema. Um alocador de uso geral pode percorrer free lists, tomar um lock ou pedir mais memória ao kernel, e a chamada que faz isso é a chamada que aparece na cauda. A correção é a mesma nos dois casos: não alocar no hot path, ponto.
Um engine é determinístico quando a mesma sequência de entrada produz a mesma sequência de saída, byte a byte, em outra máquina e um ano depois. Qualquer coisa que leia o relógio de parede, o escalonamento de threads ou a ordem de iteração de hash dentro do caminho de casamento quebra essa propriedade.
Portanto, os timestamps são uma entrada, e não algo que o engine lê por conta própria. O sequenciador carimba um comando quando o aceita, e o laço de casamento trata o carimbo como dado. A aleatoriedade, se for necessária, vem de um gerador com semente, e a semente faz parte do journal.
Recuperação não é um recurso aparafusado depois que o casamento funciona. O engine escreve um journal append-only dos comandos aceitos em ordem de sequência, e o book em memória nada mais é do que o resultado de dobrar esse journal. Reconstruir depois de uma queda significa reproduzi-lo.
O engine escreve o journal, mas durabilidade é uma propriedade do caminho de armazenamento e de quantas máquinas têm o registro antes de a confirmação sair. Isso é uma decisão de replicação e de hardware, e é aí que o tempo de recuperação é de fato ganho ou perdido.
Determinismo é o que torna o engine testável. Como a mesma entrada dá a mesma saída, uma sessão capturada é um teste de regressão, e uma falha encontrada uma vez pode ser reproduzida exatamente em vez de caçada.
Um modelo de referência vale mais do que parece. Duas implementações escritas a partir da mesma especificação divergem exatamente onde a especificação era ambígua, e regras de casamento são cheias de ambiguidade nas bordas - limites cruzados, restos zerados, cancelamentos disputando com execuções.
Rust remove uma categoria de problema em vez de deixar o laço mais rápido por si só. Não há garbage collector, então nenhuma pausa é agendada pelas suas costas. O ownership faz da disciplina de escritor único algo que o compilador impõe, em vez de algo que a revisão de código precisa notar. Handles de slab e elos intrusivos, propensos a erro em uma linguagem sem lifetimes, aqui são verificáveis. Panics em overflow de inteiros nos builds de debug pegam uma classe de bug que corrompe um book silenciosamente.
O limite honesto é que a maior parte do que determina a latência de cauda não é a linguagem:
Rust também cobra um preço. O borrow checker atrasa as primeiras semanas de um design que ainda está se movendo, o ecossistema para protocolos específicos de exchange é mais raso do que em linguagens mais antigas, e blocos unsafe em torno de estruturas lock-free exigem a mesma disciplina de revisão que o código equivalente em qualquer outro lugar. Escolhê-la é uma decisão sobre a cauda de latência e sobre segurança de memória em um núcleo de escritor único, não uma decisão sobre conforto do desenvolvedor.
A amBrain constrói infraestrutura de trading em Yerevan, na Armênia, desde 2019, com hot paths em Rust - dados de mercado entregues em menos de 5 ms, checagens de risco pré-negociação em menos de 1 ms. Se você está desenhando um matching engine e quer discutir a estrutura do book, o formato do journal ou o harness de replay, vale ter essa conversa antes da primeira linha do hot path.
Nosso time de engenharia é especializado em soluções de FinTech. Vamos conversar sobre como dar vida ao seu projeto.