Found while running cpp_orderbook through an open-source matching-engine benchmark, the Matching Engine Performance Challenge — it cross-checks engines against the byte-identical consensus of Liquibook, QuantCup and Exchange-core.
A sell order that crosses a higher resting bid executes at the seller's own (lower) limit instead of the maker's price. In OrderBook::matchOrders the fill price comes from MIN(bid->_price, ask->_price) (orderbook.cpp:24). Since matching only happens when bid->_price >= ask->_price (orderbook.cpp:22), that MIN always returns ask->_price — fine when a buyer lifts a resting ask, wrong when a seller hits a resting bid.
// orderbook.cpp:22-32
if (bid->_price >= ask->_price) {
int qty = MIN(bid->remaining, ask->remaining);
F price = MIN(bid->_price, ask->_price); // line 24
Order* aggressor = aggressorSide == Order::BUY ? bid : ask;
Order* opposite = aggressorSide == Order::BUY ? ask : bid;
bid->fill(qty,price);
ask->fill(qty,price);
const Trade trade(price, qty, *aggressor, *opposite);
Cause. The price is derived from MIN of the two limits rather than from the maker. matchOrders already identifies the resting side as opposite (orderbook.cpp:27), but uses it only for the Trade's counterparty identity, never for the price. So on a sell-initiated cross the maker is named correctly while the price is pulled from the wrong side. Worse, both bid->fill(qty,price) and ask->fill(qty,price) get the same bad price, and fill() (order.h:58-62) folds it into _avgPrice, so both orders also report a wrong averagePrice() (order.h:97). Quantity, remaining, and book state stay consistent — the error is purely in price.
Repro Any sell whose limit is below a resting bid (resting_bid_price > incoming_ask_price): the cross is valid and the trade prints at the seller's limit. Against the benchmark's three baseline engines, the first divergence on the normal scenario is a sell crossing a resting bid — same qty, same maker (232140), same taker (932154), different price:
baselines: 1,76,33857,87,232140,932154
robaho: 1,76,33559,87,232140,932154
Trade counts match exactly (62,474 on normal), so it's a pricing difference, not over/under-matching. The static scenario passes byte-for-byte because its sparse fills meet at the mid where bid->_price == ask->_price and MIN coincides with the maker — consistent with the mechanism.
Fix. Price the fill from the maker, which matchOrders already holds as opposite. Compute opposite before the price:
Order* aggressor = aggressorSide == Order::BUY ? bid : ask;
Order* opposite = aggressorSide == Order::BUY ? ask : bid;
int qty = MIN(bid->remaining, ask->remaining);
F price = opposite->_price; // execute at the resting (maker) price
The buy-aggressor case is unchanged (opposite is the ask, so opposite->_price == ask->_price == MIN(...)); the sell-aggressor case is fixed (opposite is the bid). Happy to share the failing workload.
Found while running cpp_orderbook through an open-source matching-engine benchmark, the Matching Engine Performance Challenge — it cross-checks engines against the byte-identical consensus of Liquibook, QuantCup and Exchange-core.
A sell order that crosses a higher resting bid executes at the seller's own (lower) limit instead of the maker's price. In
OrderBook::matchOrdersthe fill price comes fromMIN(bid->_price, ask->_price)(orderbook.cpp:24). Since matching only happens whenbid->_price >= ask->_price(orderbook.cpp:22), thatMINalways returnsask->_price— fine when a buyer lifts a resting ask, wrong when a seller hits a resting bid.Cause. The price is derived from
MINof the two limits rather than from the maker.matchOrdersalready identifies the resting side asopposite(orderbook.cpp:27), but uses it only for theTrade's counterparty identity, never for the price. So on a sell-initiated cross the maker is named correctly while the price is pulled from the wrong side. Worse, bothbid->fill(qty,price)andask->fill(qty,price)get the same badprice, andfill()(order.h:58-62) folds it into_avgPrice, so both orders also report a wrongaveragePrice()(order.h:97). Quantity, remaining, and book state stay consistent — the error is purely in price.Repro Any sell whose limit is below a resting bid (
resting_bid_price > incoming_ask_price): the cross is valid and the trade prints at the seller's limit. Against the benchmark's three baseline engines, the first divergence on thenormalscenario is a sell crossing a resting bid — same qty, same maker (232140), same taker (932154), different price:Trade counts match exactly (62,474 on
normal), so it's a pricing difference, not over/under-matching. Thestaticscenario passes byte-for-byte because its sparse fills meet at the mid wherebid->_price == ask->_priceandMINcoincides with the maker — consistent with the mechanism.Fix. Price the fill from the maker, which
matchOrdersalready holds asopposite. Computeoppositebefore the price:The buy-aggressor case is unchanged (
oppositeis the ask, soopposite->_price == ask->_price == MIN(...)); the sell-aggressor case is fixed (oppositeis the bid). Happy to share the failing workload.