Português · English
Casa ordens de compra e venda de um mercado e registra cada negócio — o motor por trás de uma bolsa ou corretora.
Como rodar · Garantias · Arquitetura
Um livro de ofertas mantém, a cada instante, todas as ordens de compra e de venda ainda não executadas, e as executa quando um comprador aceita o preço de um vendedor — ou o contrário. O Pricetime é esse motor: recebe uma sequência de ordens (limit, market, IOC, FOK, além de cancelamento e substituição) e produz os negócios, respeitando a prioridade preço-tempo — melhor preço primeiro; no mesmo preço, quem chegou antes.
O difícil não é casar duas ordens. É garantir que, sobre milhões delas, o resultado esteja sempre correto: que a soma de dinheiro e de ações do sistema nunca mude, que a fila de prioridade nunca seja furada, que nenhum negócio saia a um preço pior que o topo do livro. E que a mesma sequência de ordens produza sempre o mesmo estado final, bit a bit, de modo que um mercado inteiro possa ser reconstruído a partir de uma única semente (seed) ou do log de eventos.
Essas propriedades são o produto. Por isso o documento abre pelas garantias, não pela lista de recursos: num sistema que move dinheiro, a correção é a funcionalidade, e cada garantia abaixo vem com o comando que a prova.
Cada invariante é verificada por testes de propriedade (fast-check), que geram milhares de fluxos de ordens aleatórios e conferem a propriedade após cada comando. Todas rodam por um único comando: npm test.
| Invariante | Descrição | Prova |
|---|---|---|
| Conservação | A soma de caixa e a soma de ações entre todas as contas permanecem em zero — nada é criado nem destruído. | npm test |
| Livro não cruzado | A melhor compra fica sempre estritamente abaixo da melhor venda. | npm test |
| Prioridade preço-tempo | Um negócio só ocorre contra a melhor ordem elegível do lado oposto; no mesmo preço, contra a mais antiga. | npm test |
| Topo do livro | Nenhum negócio executa a um preço pior que o melhor disponível no momento. | npm test |
| Replay determinístico | A mesma sequência de comandos produz o mesmo log de eventos e o mesmo estado final, comparados por um digest estável. | npm test |
| Event sourcing | O estado reconstruído dobrando apenas o log de eventos é idêntico ao do motor vivo. | npm test |
As verificações estão em src/engine/invariants.ts (avaliadas também ao vivo na interface) e os testes de propriedade em src/engine/engine.test.ts.
Requer Node 20 ou superior.
npm install
npm run dev # abre em http://localhost:3000Demais comandos:
npm test # testes unitários e de propriedade
npm run bench # benchmark de throughput e latência
npm run typecheck # verificação de tipos
npm run build # build de produçãoFluxo de dados: um comando entra no motor, que emite eventos; qualquer visão derivada é uma dobra sobre esse log.
comando ──▶ motor ──▶ eventos ──▶ projeções
(place/cancel/replace) (livro, negócios, saldos — fold para reconstruir)
Módulos:
src/
engine/ motor determinístico — puro, só inteiros, sem dependências
book.ts dois lados; array de preços ordenado + níveis FIFO
engine.ts matching, tipos de ordem, cancel/replace, liquidação
replay.ts projeção do log de eventos + digest estável do estado
invariants.ts verificações de invariante (compartilhadas com os testes)
rng.ts PRNG sfc32 semeável (só no simulador, nunca no motor)
sim/ simulador de fluxo de ordens semeado
worker/ Web Worker que hospeda um motor + simulador ao vivo
server/ event store: em memória + adapter Postgres (schema.sql)
components/ livro, gráficos (canvas), tape, controles
app/ Next.js App Router; feed SSE em /api/feed
bench/ harness de throughput + latência
Decisões estruturais:
- O motor é uma máquina de estados de thread única:
submit(comando) → eventos. Não há relógio de parede nem aleatoriedade; a prioridade temporal é um contador monotônico. É o que torna o replay determinístico. - Toda aritmética é inteira — preços em ticks, tamanhos em lotes. Sem ponto flutuante no motor, não há erro de arredondamento acumulado.
- O mesmo código do motor roda no navegador (Web Worker) e no Node (testes e benchmark), por ser TypeScript puro sem dependências. O worker mantém o matching fora do thread principal.
Stack: Next.js 15 (App Router), React 19, TypeScript em modo strict, Tailwind CSS v4, Vitest e fast-check. Gráficos em canvas, sem biblioteca de charting.
- Motor em TypeScript, não Rust→WASM. O pipeline Rust→WASM foi descartado: acrescenta complexidade de build sem ganho nesta escala e impede que o mesmo motor rode no navegador e no CI. TypeScript puro roda nos dois lados.
- Inteiros (ticks/lotes), não ponto flutuante. Floats foram descartados porque o erro de arredondamento acumulado quebraria tanto a conservação quanto a igualdade bit a bit dos replays.
- Livro em array de preços ordenado, não árvore balanceada. Para a profundidade de um livro real — na ordem de centenas de níveis vivos — o
spliceO(n) age sobre um array pequeno e cache-friendly, e o acesso é estreito (melhor nível + nível por preço), então a estrutura é trocável sem tocar no matching. Uma árvore ou skip-list entraria no lugar para livros ilimitados. Documentado emsrc/engine/book.ts. - Motor client-side em Web Worker, não server-side. Rodar o motor no navegador elimina timeout de função serverless e custo de servidor e mantém o replay determinístico no cliente.
- Substituição = cancelar + reinserir, não amend com prioridade preservada. Qualquer mudança de preço ou aumento de tamanho recoloca a ordem no fim da fila. A regra simples mantém o log de eventos uma sequência limpa e dobrável, e a semântica sem ambiguidade.
Método: um fluxo semeado de 1.000.000 de ordens é gravado e depois reexecutado contra um motor frio, cronometrando apenas o laço de submit com hrtime por ordem; as latências viram percentis. O script está em bench/run.ts e é reproduzível com npm run bench.
Execução de referência — win32/x64, Node v24.18.0, núcleo único, aritmética inteira; gravada em bench/results.json:
| Métrica | Valor |
|---|---|
| Throughput | ~2,47M ordens/s |
| Latência p50 | ~0,3 µs/ordem |
| Latência p99 | ~1,3 µs/ordem |
| Latência p99.9 | ~14 µs/ordem |
| Ordens casadas | 1.000.000 |
| Negócios gerados | 714.897 |
Os valores absolutos variam com o hardware; o benchmark é commitado para poderem ser reproduzidos e comparados.
- Semântica de matching (unitários): cada tipo de ordem, execução ao preço do maker, prioridade FIFO dentro do nível, remanescente de market/IOC/FOK, cancel e replace.
- Invariantes (property-based, fast-check): conservação, livro não cruzado, prioridade, topo do livro, determinismo e reconstrução por event sourcing, sobre fluxos de ordens aleatórios.
- Event store (round-trip): dobrar o log persistido reproduz o digest do motor vivo.
Rodam por npm test. O mesmo comando roda no CI (.github/workflows/ci.yml), junto de typecheck, lint, build e benchmark.
- Não há prevenção de auto-negociação: uma conta pode casar com a própria ordem. A conservação continua válida (o efeito líquido é zero).
- A persistência não está ligada por padrão. O event store tem implementação em memória; o adapter Postgres (
src/server/eventstore.ts,src/server/schema.sql) assume um único escritor. - O livro usa array de preços ordenado, adequado a profundidade limitada, não a um livro ilimitado.
- O mercado é simulado a partir de uma seed; não há dados de mercado reais nem dinheiro real.
- A cadência da simulação ao vivo na interface é limitada para leitura e não corresponde ao throughput de pico do benchmark.
Sem licença definida — todos os direitos reservados.
Autoria: Igor Bahia · github.com/igorjba/pricetime
