Zig em Fintech e Trading — Case de Sucesso

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étricaC++ (anterior)Zig (novo)
Latência p501.2 μs0.8 μs
Latência p994.5 μs2.1 μs
Latência p99.915 μs4.8 μs
Throughput2M ordens/s3.5M ordens/s
Jitter12 μs3 μ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

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.