Leka
A low-latency C++20 price-time-priority limit order book and matching engine
▶ Replay viewer
Loading...
Searching...
No Matches
matching_engine.cpp
Go to the documentation of this file.
1#include "lob/matching/matching_engine.hpp"
2
3#include <algorithm>
4#include <stdexcept>
5#include <vector>
6
7namespace lob {
8
9/**
10 * @file
11 * @brief Implements event dispatch and price/time matching behavior.
12 */
13
15 : orderBook(orderBook) {}
16
17/** Convenience wrapper: identical allocation behavior to the pre-buffer API. */
18std::vector<Execution> MatchingEngine::processEvent(const OrderEvent& event) {
19 std::vector<Execution> out;
20 processEvent(event, out);
21 return out;
22}
23
24/** Dispatches by event type so only the active payload is interpreted. */
25std::size_t MatchingEngine::processEvent(const OrderEvent& event, std::vector<Execution>& out) {
26 out.clear();
27 switch (event.getEventType()) {
28 case OrderEventType::NEW: {
29 const NewOrder& order = event.getNewOrder();
30 matchOrder(order.orderId, order.price, order.quantity,
31 order.timestamp, order.orderSide, order.orderType, out);
32 return out.size();
33 }
34 case OrderEventType::CANCEL:
35 if (!event.getCancelOrder().orderId.isValid()) {
36 throw std::invalid_argument("Invalid cancel order ID");
37 }
38 orderBook.cancelOrder(event.getCancelOrder().orderId);
39 return 0;
40 case OrderEventType::REDUCE: {
41 const ReduceOrder& reduction = event.getReduceOrder();
42 if (!reduction.orderId.isValid()) {
43 throw std::invalid_argument("Invalid reduce order ID");
44 }
45 // A reduction never changes price and never forfeits priority, so
46 // there is no decision to make here and no sequence number to
47 // allocate. The book resolves the order once and mutates in place.
48 orderBook.reduceOrder(reduction.orderId, reduction.newQuantity);
49 return 0;
50 }
51 }
52 throw std::logic_error("Unknown order event type");
53}
54
55/** Forwards the event-oriented overload to processEvent(). */
56std::vector<Execution> MatchingEngine::processOrder(const OrderEvent& event) {
57 return processEvent(event);
58}
59
60/** Convenience wrapper: identical allocation behavior to the pre-buffer API. */
61std::vector<Execution> MatchingEngine::processOrder(
62 OrderId orderId, Price price, Quantity quantity, Timestamp timestamp,
63 OrderSide orderSide, OrderType orderType) {
64 std::vector<Execution> out;
65 processOrder(orderId, price, quantity, timestamp, orderSide, orderType, out);
66 return out;
67}
68
70 OrderId orderId, Price price, Quantity quantity, Timestamp timestamp,
71 OrderSide orderSide, OrderType orderType, std::vector<Execution>& out) {
72 out.clear();
73 matchOrder(orderId, price, quantity, timestamp, orderSide, orderType, out);
74 return out.size();
75}
76
77/**
78 * @details
79 * Matching is performed without allocating an incoming Order. The incoming
80 * quantity remains local until a partially filled limit order must rest in the
81 * book. Resting orders are always consumed from the best opposing level's FIFO
82 * head, and filled resting orders are removed through OrderBook so its index,
83 * price level, and pool remain synchronized.
84 *
85 * This is the single implementation of the matching algorithm; every public
86 * entry point in this file funnels into it. It never clears @c out, so a
87 * caller reusing one buffer across many calls controls exactly when that
88 * buffer's contents are discarded.
89 */
90void MatchingEngine::matchOrder(
91 OrderId orderId, Price price, Quantity quantity, Timestamp timestamp,
92 OrderSide orderSide, OrderType orderType, std::vector<Execution>& out) {
93 if (!orderId.isValid() || !quantity.isValid() || !timestamp.isValid()) {
94 throw std::invalid_argument("Invalid incoming order value");
95 }
96 if (orderSide != OrderSide::BUY && orderSide != OrderSide::SELL) {
97 throw std::invalid_argument("Invalid incoming order side");
98 }
99 if (orderType != OrderType::LIMIT && orderType != OrderType::MARKET) {
100 throw std::invalid_argument("Invalid incoming order type");
101 }
102 if (orderType == OrderType::LIMIT && !price.isValid()) {
103 throw std::invalid_argument("Limit order requires a valid price");
104 }
105 if (orderType == OrderType::MARKET && price.isValid()) {
106 throw std::invalid_argument("Market order must not specify a price");
107 }
108 if (orderBook.findOrder(orderId) != nullptr) {
109 throw std::logic_error("Order with the same OrderId already exists");
110 }
111
112 const SequenceNumber sequenceNumber = sequenceNumberGenerator.generate();
113 const Quantity originalQuantity = quantity;
114 std::uint64_t remaining = quantity.getQuantity();
115
116 // Always inspect the best opposing level first; its FIFO head determines
117 // both price priority and time priority for the next execution.
118 while (remaining > 0) {
119 PriceLevel* level = orderSide == OrderSide::BUY
120 ? orderBook.getBestAsk()
121 : orderBook.getBestBid();
122 if (level == nullptr) {
123 break;
124 }
125
126 const Price bestPrice = level->getPrice();
127 if (orderType == OrderType::LIMIT) {
128 if (orderSide == OrderSide::BUY && price < bestPrice) {
129 break;
130 }
131 if (orderSide == OrderSide::SELL && price > bestPrice) {
132 break;
133 }
134 }
135
136 Order* restingOrder = level->getHeadOrder();
137 if (restingOrder == nullptr) {
138 throw std::logic_error("Non-empty price level has no head order");
139 }
140
141 const std::uint64_t restingRemaining =
142 restingOrder->getRemainingQuantity().getQuantity();
143 const std::uint64_t executionValue =
144 std::min(remaining, restingRemaining);
145 const Quantity executionQuantity{executionValue};
146
147 out.emplace_back(orderId, restingOrder->getOrderId(),
148 restingOrder->getPrice(), executionQuantity);
149
150 remaining -= executionValue;
151 restingOrder->reduceRemainingQuantity(executionQuantity);
152
153 // Full fills are removed through OrderBook so every index stays in sync.
154 // Partial fills update the level aggregate separately because removal
155 // would otherwise subtract the order's entire remaining quantity again.
156 if (restingOrder->isFullyFilled()) {
157 orderBook.removeOrder(restingOrder);
158 } else {
159 level->reduceTotalQuantity(executionQuantity);
160 }
161 }
162
163 if (remaining > 0 && orderType == OrderType::LIMIT) {
164 orderBook.addRestingRemainder(
165 orderId, price, originalQuantity, Quantity{remaining}, timestamp,
166 orderSide, OrderType::LIMIT, sequenceNumber);
167 }
168}
169
170} // namespace lob
MatchingEngine(OrderBook &orderBook)
Creates an engine that operates on the supplied book.
std::vector< Execution > processOrder(OrderId orderId, Price price, Quantity quantity, Timestamp timestamp, OrderSide orderSide, OrderType orderType)
Processes an incoming order and returns generated executions.
std::vector< Execution > processEvent(const OrderEvent &event)
Dispatches an event before interpreting its payload.
Owns resting limit orders and maintains their book indexes.
Type-safe event-first command for the matching engine.
const CancelOrder & getCancelOrder() const
Returns the CANCEL payload or throws when this is not a CANCEL event.
OrderEventType getEventType() const
Returns the operation represented by the active payload.
Type-safe identifier for an order; zero is reserved as invalid.
Definition order_id.hpp:10
bool isValid() const
Returns whether the identifier is nonzero.
Definition order_id.hpp:30
Type-safe nonzero price value used for price ordering.
Definition price.hpp:9
bool isValid() const
Returns whether the price is nonzero.
Definition price.hpp:28
Type-safe unsigned order quantity; zero represents a filled state.
Definition quantity.hpp:8
bool isValid() const
Returns whether the quantity is valid for a submitted order.
Definition quantity.hpp:23
std::uint64_t getQuantity() const
Returns the underlying numeric quantity.
Definition quantity.hpp:20
Nanoseconds since the Unix epoch; zero is reserved as invalid.
Definition timestamp.hpp:10
bool isValid() const
Returns whether the timestamp is nonzero.
Definition timestamp.hpp:25
OrderId orderId
Identifier of the order to remove.
Payload for accepting a new order.
Quantity quantity
Quantity submitted by the caller.
OrderType orderType
Limit or market execution behavior.
OrderSide orderSide
Buy or sell direction.
Timestamp timestamp
Timestamp assigned to the order.
OrderId orderId
Unique identifier for the new order.
Price price
Limit price, or an invalid price for a market order.
Payload for shrinking a resting order without losing priority.
Quantity newQuantity
Replacement remaining quantity; nonzero and not above the current one.
OrderId orderId
Identifier of the order to shrink.