Zig em Fintech e Trading — Case de Sucesso
O setor financeiro exige o que há de melhor em performance de software: latências medidas em microsegundos, throughput de milhões de transações por segundo, determinismo absoluto e zero tolerância a falhas. Nestas condições extremas, Zig está emergindo como uma alternativa ao C e C++ para sistemas de trading de alta frequência (HFT), matching engines, processamento de risco e infraestrutura de pagamentos.
O Contexto: Por Que Performance Importa em Finanças
No mercado financeiro, tempo é literalmente dinheiro:
- 1 microsegundo de vantagem em latência pode representar milhões em lucro anual
- Matching engines das bolsas processam milhões de ordens por segundo
- Sistemas de risco precisam calcular exposição em tempo real
- Gateways de pagamento processam picos de milhares de transações simultâneas
Limitações das Soluções Tradicionais
- C++: Complexidade extrema, footguns abundantes, tempo de compilação lento
- Java: GC pauses imprevisíveis, latência de tail percentiles alta
- C: Sem abstrações seguras, bugs de memória custosos
- Rust: Borrow checker complexo para estruturas financeiras com ciclos de vida compartilhados
Matching Engine em Zig
O coração de qualquer bolsa de valores é o matching engine — o sistema que casa ordens de compra e venda. Uma empresa de tecnologia financeira brasileira reimplementou seu matching engine em Zig:
const std = @import("std");
const Lado = enum { compra, venda };
const TipoOrdem = enum { limite, mercado, stop };
const Ordem = struct {
id: u64,
instrumento: u32,
lado: Lado,
tipo: TipoOrdem,
preco: i64, // Preço em centavos (evita floating point)
quantidade: u32,
quantidade_executada: u32 = 0,
timestamp: u64, // Nanosegundos desde epoch
pub fn quantidadeRestante(self: Ordem) u32 {
return self.quantidade - self.quantidade_executada;
}
pub fn estaCompleta(self: Ordem) bool {
return self.quantidade_executada >= self.quantidade;
}
};
const Execucao = struct {
id_compra: u64,
id_venda: u64,
preco: i64,
quantidade: u32,
timestamp: u64,
};
/// Order Book com price-time priority
const OrderBook = struct {
compras: std.PriorityQueue(Ordem, void, compararCompras),
vendas: std.PriorityQueue(Ordem, void, compararVendas),
execucoes: std.ArrayList(Execucao),
proximo_id_exec: u64 = 1,
fn compararCompras(_: void, a: Ordem, b: Ordem) std.math.Order {
// Maior preço primeiro, depois menor timestamp
if (a.preco != b.preco) return std.math.order(b.preco, a.preco);
return std.math.order(a.timestamp, b.timestamp);
}
fn compararVendas(_: void, a: Ordem, b: Ordem) std.math.Order {
// Menor preço primeiro, depois menor timestamp
if (a.preco != b.preco) return std.math.order(a.preco, b.preco);
return std.math.order(a.timestamp, b.timestamp);
}
pub fn init(allocator: std.mem.Allocator) OrderBook {
return .{
.compras = std.PriorityQueue(Ordem, void, compararCompras).init(allocator, {}),
.vendas = std.PriorityQueue(Ordem, void, compararVendas).init(allocator, {}),
.execucoes = std.ArrayList(Execucao).init(allocator),
};
}
pub fn inserirOrdem(self: *OrderBook, ordem: Ordem) ![]const Execucao {
const inicio_exec = self.execucoes.items.len;
switch (ordem.lado) {
.compra => try self.matchCompra(ordem),
.venda => try self.matchVenda(ordem),
}
return self.execucoes.items[inicio_exec..];
}
fn matchCompra(self: *OrderBook, ordem_compra: Ordem) !void {
var compra = ordem_compra;
while (!compra.estaCompleta()) {
if (self.vendas.peek()) |melhor_venda| {
// Ordem de mercado ou preço compatível
if (compra.tipo == .mercado or compra.preco >= melhor_venda.preco) {
var venda = self.vendas.remove();
const qtd = @min(compra.quantidadeRestante(), venda.quantidadeRestante());
compra.quantidade_executada += qtd;
venda.quantidade_executada += qtd;
try self.execucoes.append(.{
.id_compra = compra.id,
.id_venda = venda.id,
.preco = venda.preco, // Preço passivo
.quantidade = qtd,
.timestamp = std.time.nanoTimestamp(),
});
if (!venda.estaCompleta()) {
try self.vendas.add(venda);
}
} else break;
} else break;
}
// Se a ordem não foi totalmente executada, adicionar ao book
if (!compra.estaCompleta() and compra.tipo == .limite) {
try self.compras.add(compra);
}
}
fn matchVenda(self: *OrderBook, ordem_venda: Ordem) !void {
var venda = ordem_venda;
while (!venda.estaCompleta()) {
if (self.compras.peek()) |melhor_compra| {
if (venda.tipo == .mercado or venda.preco <= melhor_compra.preco) {
var compra = self.compras.remove();
const qtd = @min(venda.quantidadeRestante(), compra.quantidadeRestante());
venda.quantidade_executada += qtd;
compra.quantidade_executada += qtd;
try self.execucoes.append(.{
.id_compra = compra.id,
.id_venda = venda.id,
.preco = compra.preco,
.quantidade = qtd,
.timestamp = std.time.nanoTimestamp(),
});
if (!compra.estaCompleta()) {
try self.compras.add(compra);
}
} else break;
} else break;
}
if (!venda.estaCompleta() and venda.tipo == .limite) {
try self.vendas.add(venda);
}
}
};
Sistema de Cálculo de Risco em Tempo Real
Cálculo de exposição e risco precisa acontecer em microsegundos para cada nova ordem:
const CalculadorRisco = struct {
posicoes: std.AutoHashMap(u32, Posicao),
limites: std.AutoHashMap(u32, LimiteRisco),
const Posicao = struct {
instrumento: u32,
quantidade_liquida: i64,
preco_medio: i64,
valor_nocional: i64,
};
const LimiteRisco = struct {
max_posicao: i64,
max_nocional: i64,
max_perda: i64,
};
const ResultadoRisco = enum {
aprovado,
rejeitado_posicao,
rejeitado_nocional,
rejeitado_perda,
};
/// Pré-validação de risco — chamada ANTES do matching
/// Deve executar em < 5 microsegundos
pub fn validarOrdem(self: *CalculadorRisco, ordem: Ordem, preco_mercado: i64) ResultadoRisco {
const limite = self.limites.get(ordem.instrumento) orelse return .aprovado;
const posicao_atual = self.posicoes.get(ordem.instrumento);
// Calcular posição projetada
const delta: i64 = switch (ordem.lado) {
.compra => @intCast(ordem.quantidade),
.venda => -@as(i64, @intCast(ordem.quantidade)),
};
const posicao_projetada = if (posicao_atual) |pos|
pos.quantidade_liquida + delta
else
delta;
// Verificar limite de posição
if (@abs(posicao_projetada) > @as(u64, @intCast(limite.max_posicao))) {
return .rejeitado_posicao;
}
// Verificar limite nocional
const nocional_projetado = @abs(posicao_projetada) * @as(u64, @intCast(preco_mercado));
if (nocional_projetado > @as(u64, @intCast(limite.max_nocional))) {
return .rejeitado_nocional;
}
return .aprovado;
}
};
Parser de Protocolo FIX (Financial Information eXchange)
O protocolo FIX é o padrão da indústria financeira para comunicação eletrônica:
/// Parser zero-allocation para mensagens FIX
const FixParser = struct {
const SOH = 0x01; // Delimitador de campos FIX
const Campo = struct {
tag: u32,
valor: []const u8,
};
/// Parse de mensagem FIX sem alocação de memória
/// Retorna iterador sobre os campos
pub fn parse(mensagem: []const u8) FixIterator {
return .{ .dados = mensagem, .posicao = 0 };
}
const FixIterator = struct {
dados: []const u8,
posicao: usize,
pub fn next(self: *FixIterator) ?Campo {
if (self.posicao >= self.dados.len) return null;
// Encontrar '=' (separador tag=valor)
const inicio_tag = self.posicao;
while (self.posicao < self.dados.len and self.dados[self.posicao] != '=') {
self.posicao += 1;
}
if (self.posicao >= self.dados.len) return null;
const tag_str = self.dados[inicio_tag..self.posicao];
self.posicao += 1; // Pular '='
// Encontrar SOH (delimitador de campo)
const inicio_valor = self.posicao;
while (self.posicao < self.dados.len and self.dados[self.posicao] != SOH) {
self.posicao += 1;
}
const valor = self.dados[inicio_valor..self.posicao];
if (self.posicao < self.dados.len) self.posicao += 1; // Pular SOH
return .{
.tag = parseTag(tag_str),
.valor = valor,
};
}
fn parseTag(str: []const u8) u32 {
var resultado: u32 = 0;
for (str) |c| {
resultado = resultado * 10 + (c - '0');
}
return resultado;
}
};
};
test "parser FIX" {
const msg = "8=FIX.4.4\x0135=D\x0149=SENDER\x0156=TARGET\x0111=ORD001\x0155=PETR4\x0154=1\x0138=100\x0144=28.50\x01";
var parser = FixParser.parse(msg);
// Tag 8: BeginString
const campo1 = parser.next().?;
try std.testing.expectEqual(@as(u32, 8), campo1.tag);
try std.testing.expectEqualStrings("FIX.4.4", campo1.valor);
// Tag 35: MsgType
const campo2 = parser.next().?;
try std.testing.expectEqual(@as(u32, 35), campo2.tag);
try std.testing.expectEqualStrings("D", campo2.valor);
}
Métricas de Performance
Latência do Matching Engine
| Métrica | C++ (anterior) | Zig (novo) |
|---|---|---|
| Latência p50 | 1.2 μs | 0.8 μs |
| Latência p99 | 4.5 μs | 2.1 μs |
| Latência p99.9 | 15 μs | 4.8 μs |
| Throughput | 2M ordens/s | 3.5M ordens/s |
| Jitter | 12 μs | 3 μs |
Redução de Tail Latency
A ausência de garbage collector e alocações previsíveis em Zig eliminam os spikes de latência que são inaceitáveis em trading:
/// Pool de objetos pré-alocado — zero alocações durante trading
fn PoolPreAlocado(comptime T: type, comptime capacidade: usize) type {
return struct {
objetos: [capacidade]T = undefined,
disponíveis: [capacidade]usize = init_disponíveis(),
topo: usize = capacidade,
const Self = @This();
fn init_disponíveis() [capacidade]usize {
var arr: [capacidade]usize = undefined;
for (0..capacidade) |i| {
arr[i] = i;
}
return arr;
}
pub fn adquirir(self: *Self) ?*T {
if (self.topo == 0) return null;
self.topo -= 1;
return &self.objetos[self.disponíveis[self.topo]];
}
pub fn liberar(self: *Self, ptr: *T) void {
const indice = (@intFromPtr(ptr) - @intFromPtr(&self.objetos)) / @sizeOf(T);
self.disponíveis[self.topo] = indice;
self.topo += 1;
}
};
}
// Pool de 1 milhão de ordens pré-alocadas
var pool_ordens = PoolPreAlocado(Ordem, 1_000_000){};
Gateway de Pagamentos PIX
No Brasil, o PIX revolucionou os pagamentos instantâneos. Um gateway de pagamentos utiliza Zig para processar transações com baixa latência:
const TransacaoPix = struct {
id: [32]u8, // UUID como bytes
chave_origem: []const u8,
chave_destino: []const u8,
valor_centavos: u64,
descricao: []const u8,
timestamp: i64,
status: StatusPix,
const StatusPix = enum {
pendente,
processando,
confirmada,
rejeitada,
devolvida,
};
};
/// Validador de transação — regras do Banco Central
fn validarTransacao(tx: TransacaoPix) !void {
// Valor mínimo: R$ 0,01
if (tx.valor_centavos == 0) return error.ValorZero;
// Valor máximo noturno: R$ 1.000,00
if (ehHorarioNoturno(tx.timestamp) and tx.valor_centavos > 100_000) {
return error.LimiteNoturnoExcedido;
}
// Chave de origem e destino não podem ser iguais
if (std.mem.eql(u8, tx.chave_origem, tx.chave_destino)) {
return error.OrigemDestinoIguais;
}
}
fn ehHorarioNoturno(timestamp: i64) bool {
const segundos_no_dia: u64 = @intCast(@mod(timestamp, 86400));
const hora = segundos_no_dia / 3600;
return hora >= 20 or hora < 6;
}
Benefícios para o Setor Financeiro
Conformidade Regulatória
- Auditabilidade: Código Zig é explícito e previsível, facilitando auditorias
- Determinismo: Sem GC pauses, sem alocações implícitas
- Rastreabilidade: Cada operação de memória é visível no código
Redução de Custos
- Menos hardware: Performance superior permite consolidar servidores
- Menos incidentes: Segurança de memória reduz bugs em produção
- Compilação rápida: Ciclo de desenvolvimento mais ágil
Conclusão
O setor financeiro, com suas demandas extremas de latência, throughput e confiabilidade, é um terreno natural para Zig. A linguagem oferece o controle de baixo nível necessário para HFT, a segurança de memória que reduz riscos operacionais, e a simplicidade que permite equipes menores manterem sistemas complexos. Para fintechs brasileiras que buscam competir globalmente em performance, Zig representa uma vantagem tecnológica significativa.
Conteúdo Relacionado
- Zig em Telecomunicações — Case em telecom
- Case TigerBeetle — Banco de dados financeiro em Zig
- Estratégias de Alocação de Memória — Memória avançada
- Otimização de Performance em Zig — Série de performance
- SIMD e Vetorização em Zig — SIMD para performance