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

Order Book — Price-Time Priority

Aula 3 de 7

Estrutura do Order Book

O coração de uma Matching Engine é o Order Book — uma estrutura que mantém ordens de compra e venda organizadas por preço e tempo.

BIDS (COMPRAS)          ASKS (VENDAS)
Preço   Qtd   Hora     Preço   Qtd   Hora
─────   ───   ────     ─────   ───   ────
15.10   200   09:01     15.20   150   09:00
15.05   100   09:02     15.25   300   09:01
15.00   500   09:00     15.30   100   09:03
  • Bids (compras): ordenados por preço decrescente (maior oferta primeiro)
  • Asks (vendas): ordenados por preço crescente (menor oferta primeiro)
  • Price-Time Priority: mesma faixa de preço, ordem mais antiga primeiro (FIFO)

Implementação

public class OrderBook
{
    private readonly string _simbolo;

    // Bids: preço decrescente → usar comparador reverso
    private readonly SortedDictionary<decimal, SortedSet<OrdemEntry>> _bids = new(
        Comparer<decimal>.Create((a, b) => b.CompareTo(a))
    );

    // Asks: preço crescente (padrão)
    private readonly SortedDictionary<decimal, SortedSet<OrdemEntry>> _asks = new();

    private readonly object _lock = new();

    public OrderBook(string simbolo)
    {
        _simbolo = simbolo;
    }

    public void AdicionarOrdem(Ordem ordem)
    {
        var book = ordem.Lado == OrdemLado.COMPRA ? _bids : _asks;
        var entry = new OrdemEntry(ordem.Id, ordem.QuantidadeRestante, ordem.CriadaEm);

        lock (_lock)
        {
            if (!book.TryGetValue(ordem.Preco!.Value, out var levels))
            {
                levels = new SortedSet<OrdemEntry>();
                book[ordem.Preco.Value] = levels;
            }
            levels.Add(entry);
        }
    }

    public bool RemoverOrdem(Guid ordemId, OrdemLado lado, decimal preco)
    {
        var book = lado == OrdemLado.COMPRA ? _bids : _asks;

        lock (_lock)
        {
            if (!book.TryGetValue(preco, out var levels))
                return false;

            var entry = levels.FirstOrDefault(e => e.OrdemId == ordemId);
            if (entry == null) return false;

            levels.Remove(entry);
            if (levels.Count == 0)
                book.Remove(preco);

            return true;
        }
    }
}

internal record OrdemEntry(Guid OrdemId, int QuantidadeRestante, DateTime CriadaEm)
    : IComparable<OrdemEntry>
{
    public int CompareTo(OrdemEntry? other)
    {
        if (other == null) return 1;
        // FIFO: mais antiga primeiro
        int cmp = CriadaEm.CompareTo(other.CriadaEm);
        return cmp != 0 ? cmp : OrdemId.CompareTo(other.OrdemId);
    }
}

Matching Logic

public List<Trade> ExecutarMatches()
{
    var trades = new List<Trade>();

    lock (_lock)
    {
        while (_bids.Count > 0 && _asks.Count > 0)
        {
            // Melhor bid (maior preço) e melhor ask (menor preço)
            var bestBid = _bids.First();
            var bestAsk = _asks.First();

            if (bestBid.Key < bestAsk.Key)
                break;  // spread positivo, sem match

            var buyEntry = bestBid.Value.First();
            var sellEntry = bestAsk.Value.First();

            var matchPreco = bestAsk.Key;   // preço do vendedor (ou midpoint)
            var matchQtd = Math.Min(buyEntry.QuantidadeRestante, sellEntry.QuantidadeRestante);

            var trade = new Trade(
                Guid.NewGuid(),
                buyEntry.OrdemId, sellEntry.OrdemId,
                _simbolo, matchQtd, matchPreco,
                DateTime.UtcNow
            );

            trades.Add(trade);
            AtualizarOrdem(bestBid, buyEntry, matchQtd);
            AtualizarOrdem(bestAsk, sellEntry, matchQtd);
        }
    }

    return trades;
}

O Order Book com price-time priority é o padrão Nasdaq e a maioria das bolsas globais. Prioriza o melhor preço; em caso de empate, quem chegou primeiro.