Found while running apex-core 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.
Every fill that crosses the spread executes at the incoming order's limit price instead of the resting order's price. That inverts price-time priority: the order already on the book should set the trade price (price improvement accrues to the aggressor), but apex gives it to whoever arrived last.
Cause. apex is a deferred matcher — create_order only inserts, even when the order crosses (matching.rs:225 → book.rs:136), and matching runs later on an explicit match_orders() sweep (matching.rs:243). Inside that sweep, walking_cross_taker picks which of the two best crossing orders is the taker by priority, not by which one is incoming (book.rs:367):
let taker = match (buy_maker_only, sell_maker_only) {
(true, false) => sell_order,
(false, true) => buy_order,
_ => {
if buy_key.priority < sell_key.priority {
buy_order // <-- the EARLIER/older order is labeled the taker
} else {
sell_order
}
}
};
Priority is timestamp-derived, and lower means older: priority() = updated_at * 100 + id % 100, documented as "The Earlier the order, the higher the priority" (types.rs:457, and the BookKey ordering doc at types.rs:180). Because the book defers matching, the order resting on one side was inserted earlier than the crossing order on the other side, so the resting order always has the strictly-lower priority — and this branch hands the taker role to the resting order. The genuinely incoming aggressor therefore becomes the maker.
That matters because Trade::matched stamps both legs at maker.price (types.rs:624 and :631):
Trade { role: TradeRole::Maker, order_id: maker.id, price: maker.price, /* ... */ },
Trade { role: TradeRole::Taker, order_id: taker.id, price: maker.price, /* ... */ },
The selected taker flows through walk(taker) → match_limit_order(taker) → process_order_pair(taker, maker, ...) → Trade::matched(now, taker, maker) (matching.rs:247, :179, :196, :40), so the order apex labels the maker is the one whose price the fill takes. With the roles swapped, the fill prints at the incoming aggressor's limit — the exact opposite of correct behaviour.
Repro (engine-level). Rest a BUY 10 @ 100, then send a crossing SELL 4 @ 95. walking_cross_taker sees buy.priority < sell.priority (the BUY is older) and labels the resting BUY the taker; the incoming SELL becomes the maker, so the trade prints at 95. Correct is 100 — the resting bid's price. Symmetric: rest a SELL 10 @ 100, send a BUY 4 @ 105, and apex fills at 105 instead of 100.
In the benchmark the report-stream hash matched the consensus on the static scenario (a fixed mid, almost no spread-crossing) and diverged on all four scenarios with active crossing. The divergence was fully deterministic, and the 192-point state audit passed on all five scenarios — book state, residual quantities, and counterparties are all correct. The only thing wrong is the trade price.
Fix. Price each fill at the resting (passive) order's price, and decide the aggressor by which order is crossing an already-resting level rather than by timestamp. Concretely, in the both-sides-cross branch of walking_cross_taker, the incoming order is the maker-side of the pair as currently wired, so the trade should be stamped at the taker-labeled (resting) order's price; equivalently, swap the taker/maker roles so the order that arrived against standing liquidity is the taker and Trade::matched keeps pricing at the resting maker.price. After that, the engine reproduces the consensus trade-report hash byte-for-byte on every scenario.
Happy to share the failing workload.
Found while running apex-core 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.
Every fill that crosses the spread executes at the incoming order's limit price instead of the resting order's price. That inverts price-time priority: the order already on the book should set the trade price (price improvement accrues to the aggressor), but apex gives it to whoever arrived last.
Cause. apex is a deferred matcher —
create_orderonly inserts, even when the order crosses (matching.rs:225→book.rs:136), and matching runs later on an explicitmatch_orders()sweep (matching.rs:243). Inside that sweep,walking_cross_takerpicks which of the two best crossing orders is the taker by priority, not by which one is incoming (book.rs:367):Priority is timestamp-derived, and lower means older:
priority() = updated_at * 100 + id % 100, documented as "The Earlier the order, the higher the priority" (types.rs:457, and theBookKeyordering doc attypes.rs:180). Because the book defers matching, the order resting on one side was inserted earlier than the crossing order on the other side, so the resting order always has the strictly-lower priority — and this branch hands the taker role to the resting order. The genuinely incoming aggressor therefore becomes the maker.That matters because
Trade::matchedstamps both legs atmaker.price(types.rs:624and:631):The selected
takerflows throughwalk(taker)→match_limit_order(taker)→process_order_pair(taker, maker, ...)→Trade::matched(now, taker, maker)(matching.rs:247,:179,:196,:40), so the order apex labels the maker is the one whose price the fill takes. With the roles swapped, the fill prints at the incoming aggressor's limit — the exact opposite of correct behaviour.Repro (engine-level). Rest a BUY 10 @ 100, then send a crossing SELL 4 @ 95.
walking_cross_takerseesbuy.priority < sell.priority(the BUY is older) and labels the resting BUY the taker; the incoming SELL becomes the maker, so the trade prints at 95. Correct is 100 — the resting bid's price. Symmetric: rest a SELL 10 @ 100, send a BUY 4 @ 105, and apex fills at 105 instead of 100.In the benchmark the report-stream hash matched the consensus on the
staticscenario (a fixed mid, almost no spread-crossing) and diverged on all four scenarios with active crossing. The divergence was fully deterministic, and the 192-point state audit passed on all five scenarios — book state, residual quantities, and counterparties are all correct. The only thing wrong is the trade price.Fix. Price each fill at the resting (passive) order's price, and decide the aggressor by which order is crossing an already-resting level rather than by timestamp. Concretely, in the both-sides-cross branch of
walking_cross_taker, the incoming order is the maker-side of the pair as currently wired, so the trade should be stamped at the taker-labeled (resting) order's price; equivalently, swap the taker/maker roles so the order that arrived against standing liquidity is the taker andTrade::matchedkeeps pricing at the restingmaker.price. After that, the engine reproduces the consensus trade-report hash byte-for-byte on every scenario.Happy to share the failing workload.