Skip to content

[Bug Report] Trades print at the aggressor's price, not the resting maker's price #1

Description

@OPTIONPOOL

Found while running the ordermatch matcher through an open-source matching-engine benchmark, the Matching Engine Performance Challenge — it cross-checks engines against the byte-identical consensus of other open source engines. The built target here is ordermatch (CMakeLists.txt:30), the QuickFIX-style sample matcher in src/ordermatch/ — a price/time-priority limit order book over two std::multimaps. This is the self-contained ordermatch book that actually compiles and runs.

When an aggressive order crosses a resting order priced better than its own limit, the fill prints at the aggressor's price instead of the resting maker's price. Under price-time priority the maker — the side already in the book — sets the execution price. Here the print follows the aggressor instead, so the trade executes away from the price the book actually offered.

Mechanism. Market::match(bid, ask) picks the trade price unconditionally from the ask (src/ordermatch/Market.cpp:109):

void Market::match( Order& bid, Order& ask )
{
  double price = ask.getPrice();   // <-- always the ask's price
  long quantity = 0;
  ...
  bid.execute( price, quantity );
  ask.execute( price, quantity );  // both sides filled at the ask price
}

The crossing test that gates this is iBid->second.getPrice() >= iAsk->second.getPrice() (Market.cpp:73), so the best bid can sit strictly above the best ask. When that happens because a resting BUY is sitting above an incoming SELL, ask.getPrice() is the aggressor's (lower) price, and both execute() calls stamp m_lastExecutedPrice with it (Order.h:77). The trade then prints below the resting bid — at the aggressor's price rather than the maker's — so both sides report an execution price that is not the one price-time priority dictates. The mirror case — an aggressive BUY lifting a lower resting ask — happens to print correctly, but only by accident: there the ask is the resting maker, so ask.getPrice() is already the right price.

This surfaces to any consumer of the engine's output. Application::processOrder inserts each order then runs match() (Application.cpp:186-191) and reports every executed order via fillOrderApplication::updateOrder, which sends order.getLastExecutedPrice() as FIX LastPx (Application.cpp:150). So a FIX client that rests a bid and gets hit by a marketable sell receives the wrong fill price.

Repro. Rest a BUY of qty 100 @ 105, then send an aggressor SELL of qty 100 @ 100. They cross (105 >= 100), match() sets price = ask.getPrice() = 100, and both orders end up with lastExecutedPrice = 100. The correct print is the maker's 105 — the resting bid set the price first, so under price-time priority the trade should execute at 105, not at the aggressor's 100. The benchmark catches this as a per-fill price mismatch against the consensus on every scenario where an aggressive sell hits a higher standing bid.

Classification. Convention deviation, not a crash or a lost order: quantity is conserved and the same two orders match — only the printed execution price is wrong (price-time-priority violation).

Fix. Price each fill at the resting/maker order's price rather than unconditionally at the ask. The maker is whichever side was already in the book when the aggressor arrived; in this matcher's flow that's the order already resting in the multimap before the just-inserted aggressor crosses into it. Passing the resting side's price into execute() for both fills makes the print follow the maker — and on the existing (accidentally-correct) aggressive-buy path it's a no-op, since there the ask already is the maker.

Happy to share the failing workload.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions