Controles de risco que rodam depois da execução apenas medem o estrago. Em sistemas de negociação, os controles que realmente protegem a operação ficam antes da ordem sair: no caminho entre a estratégia (ou o operador) e a bolsa. Reguladores reconhecem isso. Nos Estados Unidos, por exemplo, a Market Access Rule da SEC exige que corretoras com acesso ao mercado mantenham controles pré-negociação. E, independentemente da regulação, é a última linha de defesa contra erros de código, dados ruins e dedos rápidos.
Este artigo trata de como projetar esse portão para que ele seja correto, previsível e auditável.
Quais checagens incluir
O conjunto varia por mercado e instituição, mas um núcleo aparece em quase todos os sistemas:
- Kill switch: bloqueio imediato de novas ordens por conta, estratégia ou sistema inteiro.
- Tamanho máximo da ordem: em quantidade e em valor financeiro.
- Banda de preço: rejeita ordens com preço muito distante de uma referência recente, o que pega erros de digitação e de unidade.
- Limite de posição e exposição: a posição projetada, se a ordem for executada, precisa ficar dentro do limite.
- Limite de frequência: número máximo de ordens por intervalo, para conter loops descontrolados.
- Detecção de duplicatas: a mesma ordem enviada duas vezes por retentativa não pode ser executada duas vezes.
- Lista restrita: instrumentos que a conta não pode negociar.
Ordem das checagens
Execute as checagens da mais barata para a mais cara e pare na primeira falha. O kill switch é um booleano; o cálculo de exposição pode envolver vários instrumentos. Como a maioria das rejeições acontece nas regras baratas, o caminho comum fica rápido e a latência fica previsível.
Cada checagem deve ser uma função pura: recebe a ordem, o estado da conta e a visão de mercado, e devolve aprovação ou um motivo de rejeição. Funções puras são fáceis de testar exaustivamente e de reproduzir.
fn within_price_band(price: Decimal, reference: Decimal, band_bps: u32) -> bool {
// |price - reference| / reference <= band
let deviation = (price - reference).abs() / reference;
deviation <= Decimal::from(band_bps) / Decimal::from(10_000)
}
fn is_duplicate(order: &Order, recent: &RecentOrders) -> bool {
// client_order_id is unique per session; a retry reuses it.
recent.contains(&(order.session, order.client_order_id))
}Note o uso de tipos decimais. Preços e valores financeiros em ponto flutuante binário produzem erros de arredondamento que, em uma comparação de limite, viram aprovações ou rejeições erradas.
Estado sem locks: um escritor por conta
Limites de posição dependem de estado: posições abertas, ordens pendentes, limites consumidos. Se várias threads atualizam esse estado com locks, a latência passa a depender da disputa entre elas, e a ordem de processamento deixa de ser determinística.
Uma alternativa robusta é particionar as contas em shards e ter um único escritor por shard. Todas as ordens de uma conta passam pela mesma thread, em sequência. Não há locks, a latência fica estável e, para os mesmos eventos de entrada, as decisões são sempre as mesmas.
Um detalhe importante: o limite deve considerar ordens pendentes, não apenas posições executadas. Caso contrário, várias ordens enviadas em sequência rápida passam individualmente e, juntas, estouram o limite.
Falhar fechado
O que fazer quando o dado de referência está desatualizado, a conexão com o serviço de posições caiu ou o estado é inconsistente? A resposta segura é rejeitar. Uma banda de preço calculada sobre um preço de cinco minutos atrás aprova exatamente as ordens que deveria barrar.
Isso exige que cada dado de entrada carregue seu horário e que o sistema conheça a idade máxima aceitável para cada um. Falhar fechado tem custo operacional, então os motivos de rejeição precisam ser claros e monitorados: uma onda de rejeições por "dado velho" é um alerta de infraestrutura, não um problema de risco.
Auditoria e replay
Toda decisão precisa ser explicável depois: "por que esta ordem foi aceita às 10:31:07?". A forma mais confiável de responder é registrar cada evento de entrada (ordens, execuções, preços de referência, mudanças de limite) em um log append-only e derivar o estado de risco a partir dele.
Com processamento determinístico, reproduzir o log recria exatamente as mesmas decisões. Isso serve para auditoria, para investigação de incidentes e para testes: um teste de regressão pode reprocessar um dia inteiro de eventos e verificar que nenhuma decisão mudou.
Como testar
Além de testes unitários por regra, três tipos de teste dão confiança real:
- Testes de propriedade: para qualquer sequência de ordens gerada aleatoriamente, a posição projetada nunca excede o limite.
- Replay determinístico: o mesmo log, processado duas vezes, produz decisões idênticas.
- Simulador de contraparte: um simulador de bolsa permite testar rejeições, execuções parciais e desconexões antes de qualquer conexão real.
Resumo
Um portão de risco pré-negociação bem projetado é simples por fora e rigoroso por dentro: checagens puras ordenadas por custo, um escritor por conta, tipos decimais, falha fechada diante de incerteza e um log que permite reconstruir qualquer decisão. Ele não decide o que negociar. Garante que o que for negociado esteja dentro das regras.
Leituras relacionadas
- Fintech · 3 min de leitura
- IA aplicada · 3 min de leitura
- IA aplicada · 4 min de leitura