Spaces:
Sleeping
Sleeping
Update src/Orderbook.cpp
Browse files- src/Orderbook.cpp +72 -70
src/Orderbook.cpp
CHANGED
|
@@ -19,13 +19,13 @@ Orderbook::~Orderbook() {
|
|
| 19 |
}
|
| 20 |
|
| 21 |
// ─────────────────────────────────────────────────────────────────────────────
|
| 22 |
-
// GFD pruning thread
|
| 23 |
// ─────────────────────────────────────────────────────────────────────────────
|
| 24 |
void Orderbook::PruneGoodForDayOrders() {
|
| 25 |
using namespace std::chrono;
|
| 26 |
while (true) {
|
| 27 |
-
const auto now
|
| 28 |
-
const auto now_t
|
| 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
|
| 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
|
| 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 |
-
//
|
| 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 |
-
//
|
| 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 |
-
//
|
| 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 {};
|
| 114 |
}
|
| 115 |
}
|
| 116 |
|
| 117 |
-
//
|
| 118 |
if (order->GetOrderType() == OrderType::FillAndKill ||
|
| 119 |
order->GetOrderType() == OrderType::ImmediateOrCancel) {
|
| 120 |
if (!CanMatch(order->GetSide(), order->GetPrice()))
|
| 121 |
return {};
|
| 122 |
}
|
| 123 |
|
| 124 |
-
//
|
| 125 |
if (order->GetOrderType() == OrderType::FillOrKill) {
|
| 126 |
if (!CanFullyFill(order->GetSide(), order->GetPrice(), order->GetInitialQuantity()))
|
| 127 |
return {};
|
| 128 |
}
|
| 129 |
|
| 130 |
-
//
|
| 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
|
| 170 |
-
auto
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
| 173 |
} else {
|
| 174 |
-
auto price
|
| 175 |
-
auto
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
| 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 |
-
//
|
| 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 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
// ─────────────────────────────────────────────────────────────────────────────
|
| 202 |
Trades Orderbook::MatchOrders() {
|
| 203 |
Trades trades;
|
| 204 |
trades.reserve(32);
|
| 205 |
|
| 206 |
while (!bids_.empty() && !asks_.empty()) {
|
| 207 |
-
|
| 208 |
-
auto
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
|
| 210 |
-
if (bidPrice < askPrice) break;
|
| 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 |
-
//
|
| 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 #
|
| 277 |
-
//
|
| 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 |
-
|
| 287 |
it = stopBuys_.erase(it);
|
| 288 |
} else ++it;
|
| 289 |
}
|
| 290 |
for (auto it = stopSells_.begin(); it != stopSells_.end(); ) {
|
| 291 |
if (lastTrade <= it->first) {
|
| 292 |
-
|
| 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 |
-
//
|
| 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
|
| 422 |
-
snap.timestampNs
|
| 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();
|