MatchEngine is a simple C++20 limit-order matching engine. It keeps an
in-memory bid book and ask book, matches incoming orders by price-time priority,
and supports adding, cancelling, and modifying active orders.
The current code under src/ exposes a C++ API. It does not currently include
a stdin command parser or built-in print function, so the examples below show
the intended order flow and the corresponding matching result.
An order has this shape:
Order{
.orderId = 1,
.side = Side::Bid,
.priceType = PriceType::Limit,
.orderType = OrderType::GTC,
.price = 100,
.qty = 10,
}Main fields:
Side::Bid: buy order.Side::Ask: sell order.PriceType::Limit: only matches at a compatible price.PriceType::Market: matches available opposite-side liquidity immediately.OrderType::GTC: remaining limit quantity can stay on the book.OrderType::IOC: remaining quantity is cancelled after matching.
The engine uses price-time priority.
- Highest bid price matches first.
- Lowest ask price matches first.
- At the same price, the oldest order matches first.
A bid can match an ask when:
bid price >= ask price
An ask can match a bid when:
ask price <= bid price
Market orders skip the limit-price check and match against the best available opposite-side orders until filled or until the opposite book is empty.
Input:
MatchEngine engine;
engine.addOrder(Order{
.orderId = 1,
.side = Side::Bid,
.priceType = PriceType::Limit,
.orderType = OrderType::GTC,
.price = 100,
.qty = 10,
});Result:
No match.
Book:
BUY:
100 10
SELL:
empty
The bid has no matching ask, so it rests on the book.
Input:
MatchEngine engine;
engine.addOrder(Order{
.orderId = 1,
.side = Side::Bid,
.priceType = PriceType::Limit,
.orderType = OrderType::GTC,
.price = 100,
.qty = 10,
});
engine.addOrder(Order{
.orderId = 2,
.side = Side::Ask,
.priceType = PriceType::Limit,
.orderType = OrderType::GTC,
.price = 95,
.qty = 4,
});Result:
Matched:
order 2 sells 4 against order 1 at resting bid price 100
Book:
BUY:
100 6
SELL:
empty
The ask crosses the best bid because 95 <= 100. Quantity 4 is executed, and
the remaining bid quantity is 6.
Input:
MatchEngine engine;
engine.addOrder(Order{
.orderId = 1,
.side = Side::Ask,
.priceType = PriceType::Limit,
.orderType = OrderType::GTC,
.price = 105,
.qty = 3,
});
engine.addOrder(Order{
.orderId = 2,
.side = Side::Bid,
.priceType = PriceType::Limit,
.orderType = OrderType::GTC,
.price = 110,
.qty = 8,
});Result:
Matched:
order 2 buys 3 from order 1 at resting ask price 105
Book:
BUY:
110 5
SELL:
empty
The incoming bid fills the resting ask. Since order 2 is a limit GTC order,
its remaining quantity 5 rests on the bid book.
Input:
MatchEngine engine;
engine.addOrder(Order{
.orderId = 1,
.side = Side::Ask,
.priceType = PriceType::Limit,
.orderType = OrderType::GTC,
.price = 105,
.qty = 3,
});
engine.addOrder(Order{
.orderId = 2,
.side = Side::Bid,
.priceType = PriceType::Limit,
.orderType = OrderType::IOC,
.price = 110,
.qty = 8,
});Result:
Matched:
order 2 buys 3 from order 1 at resting ask price 105
Cancelled:
remaining quantity 5 from order 2
Book:
BUY:
empty
SELL:
empty
The IOC order can execute immediately, but it never rests on the book.
Input:
MatchEngine engine;
engine.addOrder(Order{
.orderId = 1,
.side = Side::Ask,
.priceType = PriceType::Limit,
.orderType = OrderType::GTC,
.price = 100,
.qty = 5,
});
engine.addOrder(Order{
.orderId = 2,
.side = Side::Ask,
.priceType = PriceType::Limit,
.orderType = OrderType::GTC,
.price = 100,
.qty = 7,
});
engine.addOrder(Order{
.orderId = 3,
.side = Side::Bid,
.priceType = PriceType::Limit,
.orderType = OrderType::GTC,
.price = 100,
.qty = 8,
});Result:
Matched:
order 3 buys 5 from order 1 at resting ask price 100
order 3 buys 3 from order 2 at resting ask price 100
Book:
BUY:
empty
SELL:
100 4
Orders 1 and 2 have the same price, so order 1 is matched first because
it arrived earlier.
Cancel input:
engine.cancelOrder(1);Result:
If order 1 is active, it is removed from the book.
If order 1 is unknown, nothing changes.
Modify input:
engine.modifyOrder(1, 99, 6);Result:
If order 1 is active:
- its price becomes 99
- its quantity becomes 6
- it is reinserted as a new resting order
- it loses its old time priority
If the modified order crosses the opposite side, it matches immediately.
The source uses C++20.
cmake -S . -B build
cmake --build buildThe current CMake setup expects a root-level main.cpp. If your local checkout
does not have one, add a small driver program that creates a MatchEngine and
submits orders like the examples above.
The included main.cpp reads commands from standard input and submits them to
MatchEngine.
Command format:
BUY <LIMIT|MARKET> <GTC|IOC> <price> <qty> <order_id>
SELL <LIMIT|MARKET> <GTC|IOC> <price> <qty> <order_id>
CANCEL <order_id>
MODIFY <order_id> <new_price> <new_qty>
Example input:
SELL LIMIT GTC 105 3 1
SELL LIMIT GTC 106 4 2
BUY LIMIT GTC 110 8 3
BUY LIMIT IOC 110 5 4
MODIFY 3 109 2
CANCEL 2
Corresponding program output:
SUBMITTED SELL 1
SUBMITTED SELL 2
SUBMITTED BUY 3
SUBMITTED BUY 4
MODIFIED 3
CANCELLED 2
Run it with the included test input:
./build/src/main < input.txtMatchEngine does the actual matching internally. The current public API does
not expose trade reports or a book snapshot, so the driver only prints whether
each command was submitted to the engine.