NOT-OMEGA commited on
Commit
e65039f
·
verified ·
1 Parent(s): 6fe98de

Update src/Orderbook.cpp

Browse files
Files changed (1) hide show
  1. src/Orderbook.cpp +72 -70
src/Orderbook.cpp CHANGED
@@ -19,13 +19,13 @@ Orderbook::~Orderbook() {
19
  }
20
 
21
  // ─────────────────────────────────────────────────────────────────────────────
22
- // GFD pruning thread: runs at market close (16:00 local)
23
  // ─────────────────────────────────────────────────────────────────────────────
24
  void Orderbook::PruneGoodForDayOrders() {
25
  using namespace std::chrono;
26
  while (true) {
27
- const auto now = system_clock::now();
28
- const auto now_t = system_clock::to_time_t(now);
29
  std::tm parts{};
30
  #ifdef _WIN32
31
  localtime_s(&parts, &now_t);
@@ -56,7 +56,7 @@ void Orderbook::PruneGoodForDayOrders() {
56
  }
57
 
58
  // ─────────────────────────────────────────────────────────────────────────────
59
- // AddOrder - main entry point
60
  // ─────────────────────────────────────────────────────────────────────────────
61
  Trades Orderbook::AddOrder(OrderPointer order) {
62
  std::scoped_lock lk{ordersMutex_};
@@ -64,7 +64,7 @@ Trades Orderbook::AddOrder(OrderPointer order) {
64
  }
65
 
66
  // ─────────────────────────────────────────────────────────────────────────────
67
- // AddOrderInternal - called with mutex already held
68
  // ─────────────────────────────────────────────────────────────────────────────
69
  Trades Orderbook::AddOrderInternal(OrderPointer order) {
70
  stats_.totalOrders.fetch_add(1, std::memory_order_relaxed);
@@ -73,7 +73,7 @@ Trades Orderbook::AddOrderInternal(OrderPointer order) {
73
  if (orders_.contains(order->GetOrderId()))
74
  return {};
75
 
76
- // ── PostOnly: reject if it would cross the spread ─────────────────────────
77
  if (order->GetOrderType() == OrderType::PostOnly) {
78
  if (CanMatch(order->GetSide(), order->GetPrice())) {
79
  order->SetStatus(OrderStatus::Rejected);
@@ -81,7 +81,7 @@ Trades Orderbook::AddOrderInternal(OrderPointer order) {
81
  }
82
  }
83
 
84
- // ── StopLimit: park in stop order book if stop not triggered yet ──────────
85
  if (order->GetOrderType() == OrderType::StopLimit) {
86
  Price lastTrade = stats_.lastTradePrice.load(std::memory_order_relaxed);
87
  bool triggered = false;
@@ -94,14 +94,13 @@ Trades Orderbook::AddOrderInternal(OrderPointer order) {
94
  stopSells_.emplace(order->GetStopPrice(), order);
95
  return {};
96
  }
97
- // Already triggered: convert to GTC limit and fall through
98
  order = std::make_shared<Order>(
99
  OrderType::GoodTillCancel,
100
  order->GetOrderId(), order->GetSide(), order->GetPrice(),
101
  order->GetRemainingQuantity());
102
  }
103
 
104
- // ── Market order: use worst available price to ensure full sweep ──────────
105
  if (order->GetOrderType() == OrderType::Market) {
106
  if (order->GetSide() == Side::Buy && !asks_.empty()) {
107
  const auto& [worstAsk, _] = *asks_.rbegin();
@@ -110,24 +109,24 @@ Trades Orderbook::AddOrderInternal(OrderPointer order) {
110
  const auto& [worstBid, _] = *bids_.rbegin();
111
  order->ToGoodTillCancel(worstBid);
112
  } else {
113
- return {}; // no liquidity
114
  }
115
  }
116
 
117
- // ── FAK / IOC: reject immediately if no match ─────────────────────────────
118
  if (order->GetOrderType() == OrderType::FillAndKill ||
119
  order->GetOrderType() == OrderType::ImmediateOrCancel) {
120
  if (!CanMatch(order->GetSide(), order->GetPrice()))
121
  return {};
122
  }
123
 
124
- // ── FOK: reject if cannot fully fill ─────────────────────────────────────
125
  if (order->GetOrderType() == OrderType::FillOrKill) {
126
  if (!CanFullyFill(order->GetSide(), order->GetPrice(), order->GetInitialQuantity()))
127
  return {};
128
  }
129
 
130
- // ── Insert into price level ───────────────────────────────────────────────
131
  OrderPointers::iterator it;
132
  if (order->GetSide() == Side::Buy) {
133
  auto& level = bids_[order->GetPrice()];
@@ -140,7 +139,6 @@ Trades Orderbook::AddOrderInternal(OrderPointer order) {
140
  }
141
  orders_[order->GetOrderId()] = {order, it};
142
  OnOrderAdded(order);
143
-
144
  if (onAdded_) onAdded_(*order);
145
 
146
  return MatchOrders();
@@ -159,6 +157,9 @@ void Orderbook::CancelOrdersInternal(const OrderIds& ids) {
159
  for (auto id : ids) CancelOrderInternal(id);
160
  }
161
 
 
 
 
162
  void Orderbook::CancelOrderInternal(OrderId orderId) {
163
  if (!orders_.contains(orderId)) return;
164
 
@@ -166,16 +167,21 @@ void Orderbook::CancelOrderInternal(OrderId orderId) {
166
  orders_.erase(orderId);
167
 
168
  if (order->GetSide() == Side::Sell) {
169
- auto price = order->GetPrice();
170
- auto& level = asks_.at(price);
171
- level.erase(it);
172
- if (level.empty()) asks_.erase(price);
 
 
173
  } else {
174
- auto price = order->GetPrice();
175
- auto& level = bids_.at(price);
176
- level.erase(it);
177
- if (level.empty()) bids_.erase(price);
 
 
178
  }
 
179
  order->Cancel();
180
  OnOrderCancelled(order);
181
  if (onCancelled_) onCancelled_(*order);
@@ -183,10 +189,7 @@ void Orderbook::CancelOrderInternal(OrderId orderId) {
183
  }
184
 
185
  // ─────────────────────────────────────────────────────────────────────────────
186
- // BUG FIX #1: ModifyOrder - original had a race condition between CancelOrder
187
- // (which released the lock) and AddOrder (which re-acquired it). Another thread
188
- // could insert an order with the same ID in between. Fixed by using internal
189
- // helpers that assume the lock is already held.
190
  // ─────────────────────────────────────────────────────────────────────────────
191
  Trades Orderbook::ModifyOrder(OrderModify mod) {
192
  std::scoped_lock lk{ordersMutex_};
@@ -197,17 +200,32 @@ Trades Orderbook::ModifyOrder(OrderModify mod) {
197
  }
198
 
199
  // ─────────────────────────────────────────────────────────────────────────────
200
- // Matching Engine - price-time priority
 
 
 
 
 
 
 
 
 
 
201
  // ─────────────────────────────────────────────────────────────────────────────
202
  Trades Orderbook::MatchOrders() {
203
  Trades trades;
204
  trades.reserve(32);
205
 
206
  while (!bids_.empty() && !asks_.empty()) {
207
- auto& [bidPrice, bidLevel] = *bids_.begin();
208
- auto& [askPrice, askLevel] = *asks_.begin();
 
 
 
 
 
209
 
210
- if (bidPrice < askPrice) break; // no cross
211
 
212
  while (!bidLevel.empty() && !askLevel.empty()) {
213
  auto bid = bidLevel.front();
@@ -215,7 +233,6 @@ Trades Orderbook::MatchOrders() {
215
 
216
  Quantity qty = std::min(bid->GetRemainingQuantity(),
217
  ask->GetRemainingQuantity());
218
-
219
  bid->Fill(qty);
220
  ask->Fill(qty);
221
 
@@ -246,62 +263,57 @@ Trades Orderbook::MatchOrders() {
246
 
247
  if (onTrade_) onTrade_(trade);
248
 
 
249
  CheckAndTriggerStops(ask->GetPrice());
250
  }
251
 
252
- // BUG FIX #2: levelData_ entries are cleaned inside OnOrderMatched/
253
- // OnOrderCancelled. Erasing them again here caused a double-erase.
254
- // Only erase the price-level containers (bids_/asks_), not levelData_.
255
  if (bidLevel.empty()) bids_.erase(bidPrice);
256
  if (askLevel.empty()) asks_.erase(askPrice);
257
  }
258
 
259
- // BUG FIX #3: Original cancelFAK only checked the *front* of the top-of-book
260
- // level AFTER matching was complete. If the FAK was partially filled and moved
261
- // off the front, it would never be cancelled. Fix: track FAK orders explicitly
262
- // and cancel them by ID after matching, regardless of their position.
263
  std::vector<OrderId> fakToCancel;
264
  for (auto& [id, entry] : orders_) {
265
  auto t = entry.order->GetOrderType();
266
- if (t == OrderType::FillAndKill || t == OrderType::ImmediateOrCancel) {
267
  fakToCancel.push_back(id);
268
- }
269
  }
270
  for (auto id : fakToCancel) CancelOrderInternal(id);
271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  return trades;
273
  }
274
 
275
  // ─────────────────────────────────────────────────────────────────────────────
276
- // BUG FIX #4: CheckAndTriggerStops - original called AddOrder() which tried to
277
- // acquire ordersMutex_ but this function is called from MatchOrders() which
278
- // is called from AddOrder() which already holds the lock → deadlock.
279
- // Fixed by calling AddOrderInternal() (lock-free internal path) directly.
280
  // ─────────────────────────────────────────────────────────────────────────────
281
  void Orderbook::CheckAndTriggerStops(Price lastTrade) {
282
- std::vector<OrderPointer> toInject;
283
-
284
  for (auto it = stopBuys_.begin(); it != stopBuys_.end(); ) {
285
  if (lastTrade >= it->first) {
286
- toInject.push_back(it->second);
287
  it = stopBuys_.erase(it);
288
  } else ++it;
289
  }
290
  for (auto it = stopSells_.begin(); it != stopSells_.end(); ) {
291
  if (lastTrade <= it->first) {
292
- toInject.push_back(it->second);
293
  it = stopSells_.erase(it);
294
  } else ++it;
295
  }
296
-
297
- for (auto& o : toInject) {
298
- auto limitOrder = std::make_shared<Order>(
299
- OrderType::GoodTillCancel,
300
- o->GetOrderId(), o->GetSide(), o->GetPrice(),
301
- o->GetRemainingQuantity());
302
- // Use internal path — mutex is already held by the caller
303
- AddOrderInternal(limitOrder);
304
- }
305
  }
306
 
307
  // ─────────────────────────────────────────────────────────────────────────────
@@ -319,7 +331,6 @@ bool Orderbook::CanMatch(Side side, Price price) const {
319
 
320
  bool Orderbook::CanFullyFill(Side side, Price price, Quantity qty) const {
321
  if (!CanMatch(side, price)) return false;
322
-
323
  Quantity avail = 0;
324
  if (side == Side::Buy) {
325
  for (const auto& [lvlPrice, level] : asks_) {
@@ -338,14 +349,7 @@ bool Orderbook::CanFullyFill(Side side, Price price, Quantity qty) const {
338
  }
339
 
340
  // ─────────────────────────────────────────────────────────────────────────────
341
- // BUG FIX #5: UpdateLevelData - "Remove" action decremented count, but matched
342
- // orders that are NOT fully filled should only reduce quantity, not count.
343
- // "Match" action handled quantity only (correct), but "Remove" was also being
344
- // called for fully-filled matched orders via OnOrderMatched — however those
345
- // orders were already removed from the orders_ map, so their level entries
346
- // got double-decremented. Fixed by using Match for partial fills and not
347
- // calling UpdateLevelData at all for fully-filled matched orders (they are
348
- // cleaned up when erased from the level list above).
349
  // ─────────────────────────────────────────────────────────────────────────────
350
  void Orderbook::UpdateLevelData(Price price, Quantity qty, LevelData::Action action) {
351
  auto& d = levelData_[price];
@@ -370,8 +374,6 @@ void Orderbook::OnOrderCancelled(OrderPointer order) {
370
  UpdateLevelData(order->GetPrice(), order->GetRemainingQuantity(), LevelData::Action::Remove);
371
  }
372
  void Orderbook::OnOrderMatched(Price price, Quantity qty, bool fullyFilled) {
373
- // For fully filled: use Remove (decrements count).
374
- // For partial fill: use Match (quantity only, order still lives in book).
375
  UpdateLevelData(price, qty,
376
  fullyFilled ? LevelData::Action::Remove : LevelData::Action::Match);
377
  }
@@ -418,8 +420,8 @@ Price Orderbook::Spread() const {
418
  OrderbookSnapshot Orderbook::GetSnapshot() const {
419
  std::scoped_lock lk{ordersMutex_};
420
  OrderbookSnapshot snap;
421
- snap.seqNum = stats_.seqNum.load();
422
- snap.timestampNs = std::chrono::steady_clock::now().time_since_epoch().count();
423
  snap.lastTradePrice = stats_.lastTradePrice.load();
424
  snap.lastTradeQty = stats_.lastTradeQty.load();
425
  snap.totalVolume = stats_.totalVolume.load();
 
19
  }
20
 
21
  // ─────────────────────────────────────────────────────────────────────────────
22
+ // GFD pruning thread
23
  // ─────────────────────────────────────────────────────────────────────────────
24
  void Orderbook::PruneGoodForDayOrders() {
25
  using namespace std::chrono;
26
  while (true) {
27
+ const auto now = system_clock::now();
28
+ const auto now_t = system_clock::to_time_t(now);
29
  std::tm parts{};
30
  #ifdef _WIN32
31
  localtime_s(&parts, &now_t);
 
56
  }
57
 
58
  // ─────────────────────────────────────────────────────────────────────────────
59
+ // AddOrder
60
  // ─────────────────────────────────────────────────────────────────────────────
61
  Trades Orderbook::AddOrder(OrderPointer order) {
62
  std::scoped_lock lk{ordersMutex_};
 
64
  }
65
 
66
  // ─────────────────────────────────────────────────────────────────────────────
67
+ // AddOrderInternal called with mutex already held
68
  // ─────────────────────────────────────────────────────────────────────────────
69
  Trades Orderbook::AddOrderInternal(OrderPointer order) {
70
  stats_.totalOrders.fetch_add(1, std::memory_order_relaxed);
 
73
  if (orders_.contains(order->GetOrderId()))
74
  return {};
75
 
76
+ // PostOnly: reject if crosses spread
77
  if (order->GetOrderType() == OrderType::PostOnly) {
78
  if (CanMatch(order->GetSide(), order->GetPrice())) {
79
  order->SetStatus(OrderStatus::Rejected);
 
81
  }
82
  }
83
 
84
+ // StopLimit: park if not yet triggered
85
  if (order->GetOrderType() == OrderType::StopLimit) {
86
  Price lastTrade = stats_.lastTradePrice.load(std::memory_order_relaxed);
87
  bool triggered = false;
 
94
  stopSells_.emplace(order->GetStopPrice(), order);
95
  return {};
96
  }
 
97
  order = std::make_shared<Order>(
98
  OrderType::GoodTillCancel,
99
  order->GetOrderId(), order->GetSide(), order->GetPrice(),
100
  order->GetRemainingQuantity());
101
  }
102
 
103
+ // Market order: use worst available price
104
  if (order->GetOrderType() == OrderType::Market) {
105
  if (order->GetSide() == Side::Buy && !asks_.empty()) {
106
  const auto& [worstAsk, _] = *asks_.rbegin();
 
109
  const auto& [worstBid, _] = *bids_.rbegin();
110
  order->ToGoodTillCancel(worstBid);
111
  } else {
112
+ return {};
113
  }
114
  }
115
 
116
+ // FAK / IOC: reject if no match available
117
  if (order->GetOrderType() == OrderType::FillAndKill ||
118
  order->GetOrderType() == OrderType::ImmediateOrCancel) {
119
  if (!CanMatch(order->GetSide(), order->GetPrice()))
120
  return {};
121
  }
122
 
123
+ // FOK: reject if cannot fully fill
124
  if (order->GetOrderType() == OrderType::FillOrKill) {
125
  if (!CanFullyFill(order->GetSide(), order->GetPrice(), order->GetInitialQuantity()))
126
  return {};
127
  }
128
 
129
+ // Insert into price level
130
  OrderPointers::iterator it;
131
  if (order->GetSide() == Side::Buy) {
132
  auto& level = bids_[order->GetPrice()];
 
139
  }
140
  orders_[order->GetOrderId()] = {order, it};
141
  OnOrderAdded(order);
 
142
  if (onAdded_) onAdded_(*order);
143
 
144
  return MatchOrders();
 
157
  for (auto id : ids) CancelOrderInternal(id);
158
  }
159
 
160
+ // BUG FIX #4: Replaced .at() with .find() to avoid std::out_of_range crash
161
+ // when orders_ and bids_/asks_ are in inconsistent state. .at() would throw
162
+ // and leave mutex locked. Now we safely skip missing price levels.
163
  void Orderbook::CancelOrderInternal(OrderId orderId) {
164
  if (!orders_.contains(orderId)) return;
165
 
 
167
  orders_.erase(orderId);
168
 
169
  if (order->GetSide() == Side::Sell) {
170
+ auto price = order->GetPrice();
171
+ auto levelIt = asks_.find(price);
172
+ if (levelIt != asks_.end()) {
173
+ levelIt->second.erase(it);
174
+ if (levelIt->second.empty()) asks_.erase(levelIt);
175
+ }
176
  } else {
177
+ auto price = order->GetPrice();
178
+ auto levelIt = bids_.find(price);
179
+ if (levelIt != bids_.end()) {
180
+ levelIt->second.erase(it);
181
+ if (levelIt->second.empty()) bids_.erase(levelIt);
182
+ }
183
  }
184
+
185
  order->Cancel();
186
  OnOrderCancelled(order);
187
  if (onCancelled_) onCancelled_(*order);
 
189
  }
190
 
191
  // ─────────────────────────────────────────────────────────────────────────────
192
+ // ModifyOrder
 
 
 
193
  // ─────────────────────────────────────────────────────────────────────────────
194
  Trades Orderbook::ModifyOrder(OrderModify mod) {
195
  std::scoped_lock lk{ordersMutex_};
 
200
  }
201
 
202
  // ─────────────────────────────────────────────────────────────────────────────
203
+ // MatchOrders
204
+ //
205
+ // BUG FIX #2: Iterator invalidation fixed — structured bindings (auto&) on
206
+ // map iterators dangled when bids_.erase(bidPrice) was called inside the loop.
207
+ // Now we copy Price values and use find() after erasure is safe.
208
+ //
209
+ // BUG FIX #3: Stop injection deferred — CheckAndTriggerStops now only pushes
210
+ // to pendingStops_. After the main matching loop exits, we drain pendingStops_
211
+ // and inject via AddOrderInternal. This breaks the recursive call chain:
212
+ // AddOrderInternal → MatchOrders → CheckAndTriggerStops → AddOrderInternal
213
+ // which could stack-overflow on stop-order chains.
214
  // ─────────────────────────────────────────────────────────────────────────────
215
  Trades Orderbook::MatchOrders() {
216
  Trades trades;
217
  trades.reserve(32);
218
 
219
  while (!bids_.empty() && !asks_.empty()) {
220
+ // BUG FIX #2: Copy price values — do NOT hold references across erasure
221
+ auto bidIt = bids_.begin();
222
+ auto askIt = asks_.begin();
223
+ Price bidPrice = bidIt->first;
224
+ Price askPrice = askIt->first;
225
+ auto& bidLevel = bidIt->second;
226
+ auto& askLevel = askIt->second;
227
 
228
+ if (bidPrice < askPrice) break;
229
 
230
  while (!bidLevel.empty() && !askLevel.empty()) {
231
  auto bid = bidLevel.front();
 
233
 
234
  Quantity qty = std::min(bid->GetRemainingQuantity(),
235
  ask->GetRemainingQuantity());
 
236
  bid->Fill(qty);
237
  ask->Fill(qty);
238
 
 
263
 
264
  if (onTrade_) onTrade_(trade);
265
 
266
+ // BUG FIX #3: Only COLLECTS stops into pendingStops_, does NOT inject yet
267
  CheckAndTriggerStops(ask->GetPrice());
268
  }
269
 
 
 
 
270
  if (bidLevel.empty()) bids_.erase(bidPrice);
271
  if (askLevel.empty()) asks_.erase(askPrice);
272
  }
273
 
274
+ // Cancel remaining FAK/IOC orders
 
 
 
275
  std::vector<OrderId> fakToCancel;
276
  for (auto& [id, entry] : orders_) {
277
  auto t = entry.order->GetOrderType();
278
+ if (t == OrderType::FillAndKill || t == OrderType::ImmediateOrCancel)
279
  fakToCancel.push_back(id);
 
280
  }
281
  for (auto id : fakToCancel) CancelOrderInternal(id);
282
 
283
+ // BUG FIX #3: Drain pendingStops_ AFTER matching is fully complete.
284
+ // Swap first so that any stops triggered by injected orders go into
285
+ // a fresh pendingStops_ and are processed in next AddOrderInternal call,
286
+ // not recursively here.
287
+ std::vector<OrderPointer> stops;
288
+ std::swap(stops, pendingStops_);
289
+ for (auto& o : stops) {
290
+ auto limitOrder = std::make_shared<Order>(
291
+ OrderType::GoodTillCancel,
292
+ o->GetOrderId(), o->GetSide(), o->GetPrice(),
293
+ o->GetRemainingQuantity());
294
+ AddOrderInternal(limitOrder);
295
+ }
296
+
297
  return trades;
298
  }
299
 
300
  // ─────────────────────────────────────────────────────────────────────────────
301
+ // BUG FIX #3: CheckAndTriggerStops only collects into pendingStops_.
302
+ // No longer calls AddOrderInternal directly, breaking the recursion chain.
 
 
303
  // ─────────────────────────────────────────────────────────────────────────────
304
  void Orderbook::CheckAndTriggerStops(Price lastTrade) {
 
 
305
  for (auto it = stopBuys_.begin(); it != stopBuys_.end(); ) {
306
  if (lastTrade >= it->first) {
307
+ pendingStops_.push_back(it->second);
308
  it = stopBuys_.erase(it);
309
  } else ++it;
310
  }
311
  for (auto it = stopSells_.begin(); it != stopSells_.end(); ) {
312
  if (lastTrade <= it->first) {
313
+ pendingStops_.push_back(it->second);
314
  it = stopSells_.erase(it);
315
  } else ++it;
316
  }
 
 
 
 
 
 
 
 
 
317
  }
318
 
319
  // ─────────────────────────────────────────────────────────────────────────────
 
331
 
332
  bool Orderbook::CanFullyFill(Side side, Price price, Quantity qty) const {
333
  if (!CanMatch(side, price)) return false;
 
334
  Quantity avail = 0;
335
  if (side == Side::Buy) {
336
  for (const auto& [lvlPrice, level] : asks_) {
 
349
  }
350
 
351
  // ─────────────────────────────────────────────────────────────────────────────
352
+ // Level data tracking
 
 
 
 
 
 
 
353
  // ─────────────────────────────────────────────────────────────────────────────
354
  void Orderbook::UpdateLevelData(Price price, Quantity qty, LevelData::Action action) {
355
  auto& d = levelData_[price];
 
374
  UpdateLevelData(order->GetPrice(), order->GetRemainingQuantity(), LevelData::Action::Remove);
375
  }
376
  void Orderbook::OnOrderMatched(Price price, Quantity qty, bool fullyFilled) {
 
 
377
  UpdateLevelData(price, qty,
378
  fullyFilled ? LevelData::Action::Remove : LevelData::Action::Match);
379
  }
 
420
  OrderbookSnapshot Orderbook::GetSnapshot() const {
421
  std::scoped_lock lk{ordersMutex_};
422
  OrderbookSnapshot snap;
423
+ snap.seqNum = stats_.seqNum.load();
424
+ snap.timestampNs = std::chrono::steady_clock::now().time_since_epoch().count();
425
  snap.lastTradePrice = stats_.lastTradePrice.load();
426
  snap.lastTradeQty = stats_.lastTradeQty.load();
427
  snap.totalVolume = stats_.totalVolume.load();