kb.erickguedes.com
C#: Matching Engine — Projeto Mercado Financeiro

Projeto Final — Matching Engine Completa

Aula 7 de 7

Arquitetura Final

┌─────────────┐     ┌──────────────┐     ┌──────────────┐
│   Cliente   │────▶│  Gateway     │────▶│  Exchange    │
│  (TCP/WS)   │     │  (Channel)   │     │  (Core)      │
└─────────────┘     └──────────────┘     └──────┬───────┘
                                                │
                    ┌───────────────────────────┤
                    │                           │
                    ▼                           ▼
            ┌──────────────┐           ┌──────────────┐
            │  OrderBook   │           │  Risk Mgr    │
            │  (AAPL)      │           │  (Validação) │
            └──────────────┘           └──────────────┘

Classe Exchange — Core do Sistema

public class Exchange
{
    private readonly ConcurrentDictionary<string, IOrderBook> _books = new();
    private readonly IRiskManager _riskManager;
    private readonly ITradePublisher _publisher;
    private readonly ILogger _logger;
    private readonly IdGenerator _idGen = new();

    public Exchange(IRiskManager riskManager, ITradePublisher publisher,
                    ILogger logger)
    {
        _riskManager = riskManager;
        _publisher = publisher;
        _logger = logger;
    }

    public async Task<EnviarOrdemResultado> EnviarOrdemAsync(Ordem ordem)
    {
        var stopwatch = Stopwatch.StartNew();

        // 1. Validação de risco
        if (!_riskManager.Autorizar(ordem))
        {
            _logger.Warning($"Ordem {ordem.Id} rejeitada pelo risk manager");
            return EnviarOrdemResultado.Rejeitada("Risk check falhou");
        }

        // 2. Obter ou criar OrderBook
        var book = _books.GetOrAdd(ordem.Simbolo, _ => new OrderBook());

        // 3. Adicionar e executar matches
        book.AdicionarOrdem(ordem);
        var trades = book.ExecutarMatches();

        // 4. Publicar trades
        foreach (var trade in trades)
        {
            await _publisher.PublicarTradeAsync(trade);
            _logger.Info($"Trade executado: {trade}");
        }

        stopwatch.Stop();
        _logger.Info($"Ordem {ordem.Id} processada em {stopwatch.ElapsedMicroseconds}µs");

        return EnviarOrdemResultado.Sucesso(ordem.Id, trades);
    }

    public BookSnapshot ObterSnapshot(string simbolo)
        => _books.TryGetValue(simbolo, out var book)
            ? book.ObterSnapshot()
            : BookSnapshot.Vazio(simbolo);
}

Risk Manager

public class RiskManager : IRiskManager
{
    private readonly ConcurrentDictionary<string, LimitesCliente> _limites = new();

    public bool Autorizar(Ordem ordem)
    {
        var limites = _limites.GetOrAdd(
            ordem.Simbolo, _ => new LimitesCliente());

        // Check 1: Quantidade máxima por ordem
        if (ordem.Quantidade > 100_000)
            return false;

        // Check 2: Preço dentro do range (circuit breaker)
        if (ordem.Preco.HasValue)
        {
            var precoAtual = limites.UltimoPreco;
            if (precoAtual > 0)
            {
                var variacao = Math.Abs(ordem.Preco.Value - precoAtual) / precoAtual;
                if (variacao > 0.10m)  // 10% max
                    return false;
            }
        }

        // Check 3: Saldo suficiente (modo cash)
        if (ordem.Lado == OrdemLado.COMPRA && ordem.Preco.HasValue)
        {
            var valorTotal = ordem.Quantidade * ordem.Preco.Value;
            if (valorTotal > limites.SaldoDisponivel)
                return false;
        }

        return true;
    }
}

Market Data Publisher

public interface ITradePublisher
{
    Task PublicarTradeAsync(Trade trade);
    IAsyncEnumerable<Trade> ObterStream(string simbolo,
        CancellationToken ct);
}

public class InMemoryTradePublisher : ITradePublisher
{
    private readonly ConcurrentDictionary<string, Channel<Trade>> _channels = new();

    public async Task PublicarTradeAsync(Trade trade)
    {
        if (_channels.TryGetValue(trade.Simbolo, out var channel))
            await channel.Writer.WriteAsync(trade);
    }

    public IAsyncEnumerable<Trade> ObterStream(string simbolo,
        CancellationToken ct)
    {
        var channel = _channels.GetOrAdd(simbolo, _ =>
            Channel.CreateUnbounded<Trade>());
        return channel.Reader.ReadAllAsync(ct);
    }
}

Program.cs — Ponto de Entrada

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddSingleton<IRiskManager, RiskManager>();
builder.Services.AddSingleton<ITradePublisher, InMemoryTradePublisher>();
builder.Services.AddSingleton<ILogger, ConsoleLogger>();
builder.Services.AddSingleton<Exchange>();
builder.Services.AddHostedService<WebSocketServer>();

var app = builder.Build();
app.Run();

Teste de Integração

public class ExchangeIntegrationTests
{
    [Fact]
    public async Task CompraVenda_DeveExecutarTradeCompleto()
    {
        var exchange = new Exchange(
            new RiskManager(),
            new InMemoryTradePublisher(),
            new ConsoleLogger());

        var compra = new Ordem("AAPL", OrdemLado.COMPRA, 100,
                               OrdemTipo.LIMITE, 150m);
        var venda = new Ordem("AAPL", OrdemLado.VENDA, 100,
                              OrdemTipo.LIMITE, 150m);

        var r1 = await exchange.EnviarOrdemAsync(compra);
        var r2 = await exchange.EnviarOrdemAsync(venda);

        r1.Trades.Should().HaveCount(1);
        r1.Trades[0].Quantidade.Should().Be(100);
        r1.Trades[0].Preco.Should().Be(150m);
    }
}

Esta Matching Engine implementa price-time priority, risk management, market data streaming e é extensível para WebSocket/TCP. O projeto está pronto para deploy real!