| #include "common/Book.hpp" |
| #include <algorithm> |
| #include <cmath> |
|
|
| namespace eunex { |
|
|
| Book::Book(SymbolIndex_t symbolIdx) |
| : symbolIdx_(symbolIdx) {} |
|
|
| |
|
|
| void Book::setPhase(TradingPhase phase) { |
| phase_ = phase; |
| } |
|
|
| |
| |
| |
|
|
| Price_t Book::getIOP() const { |
| if (bids_.empty() || asks_.empty()) return 0; |
|
|
| |
| std::vector<Price_t> prices; |
| for (auto& [p, _] : bids_) prices.push_back(p); |
| for (auto& [p, _] : asks_) prices.push_back(p); |
| std::sort(prices.begin(), prices.end()); |
| prices.erase(std::unique(prices.begin(), prices.end()), prices.end()); |
|
|
| Price_t bestPrice = 0; |
| Quantity_t bestVolume = 0; |
| Quantity_t bestSurplus = UINT64_MAX; |
| bool bestBuySurplus = false; |
|
|
| for (Price_t p : prices) { |
| |
| Quantity_t cumBuy = 0; |
| for (auto& [bidPrice, orders] : bids_) { |
| if (bidPrice >= p) { |
| for (auto& o : orders) cumBuy += o.remainingQty; |
| } |
| } |
| |
| Quantity_t cumSell = 0; |
| for (auto& [askPrice, orders] : asks_) { |
| if (askPrice <= p) { |
| for (auto& o : orders) cumSell += o.remainingQty; |
| } |
| } |
|
|
| Quantity_t volume = std::min(cumBuy, cumSell); |
| Quantity_t surplus = (cumBuy > cumSell) ? (cumBuy - cumSell) : (cumSell - cumBuy); |
| bool buySurplus = cumBuy > cumSell; |
|
|
| if (volume > bestVolume || |
| (volume == bestVolume && surplus < bestSurplus) || |
| (volume == bestVolume && surplus == bestSurplus && buySurplus && p > bestPrice)) { |
| bestPrice = p; |
| bestVolume = volume; |
| bestSurplus = surplus; |
| bestBuySurplus = buySurplus; |
| } |
| } |
|
|
| return bestVolume > 0 ? bestPrice : 0; |
| } |
|
|
| |
|
|
| void Book::uncross(const TradeCallback& onTrade, const ExecCallback& onExec) { |
| Price_t iop = getIOP(); |
| if (iop == 0) return; |
|
|
| Quantity_t remainingVolume = UINT64_MAX; |
|
|
| |
| |
| auto askIt = asks_.begin(); |
| while (askIt != asks_.end() && askIt->first <= iop && remainingVolume > 0) { |
| auto& level = askIt->second; |
| auto orderIt = level.begin(); |
| while (orderIt != level.end() && remainingVolume > 0) { |
| |
| auto bidIt = bids_.begin(); |
| while (bidIt != bids_.end() && bidIt->first >= iop && orderIt->remainingQty > 0) { |
| auto& bidLevel = bidIt->second; |
| auto bidOrderIt = bidLevel.begin(); |
| while (bidOrderIt != bidLevel.end() && orderIt->remainingQty > 0) { |
| Quantity_t traded = std::min(orderIt->remainingQty, bidOrderIt->remainingQty); |
|
|
| Trade trade{}; |
| trade.tradeId = allocateTradeId(); |
| trade.symbolIdx = symbolIdx_; |
| trade.price = iop; |
| trade.quantity = traded; |
| trade.buyOrderId = bidOrderIt->orderId; |
| trade.sellOrderId = orderIt->orderId; |
| trade.buyClOrdId = bidOrderIt->clOrdId; |
| trade.sellClOrdId = orderIt->clOrdId; |
| trade.buySessionId = bidOrderIt->sessionId; |
| trade.sellSessionId = orderIt->sessionId; |
| trade.matchTime = nowNs(); |
| onTrade(trade); |
| lastTradePrice_ = iop; |
|
|
| orderIt->remainingQty -= traded; |
| bidOrderIt->remainingQty -= traded; |
|
|
| ExecutionReport bidRpt{bidOrderIt->orderId, bidOrderIt->clOrdId, |
| bidOrderIt->remainingQty == 0 ? OrderStatus::Filled : OrderStatus::PartiallyFilled, |
| bidOrderIt->quantity - bidOrderIt->remainingQty, |
| bidOrderIt->remainingQty, iop, traded, trade.tradeId}; |
| onExec(bidRpt); |
|
|
| ExecutionReport askRpt{orderIt->orderId, orderIt->clOrdId, |
| orderIt->remainingQty == 0 ? OrderStatus::Filled : OrderStatus::PartiallyFilled, |
| orderIt->quantity - orderIt->remainingQty, |
| orderIt->remainingQty, iop, traded, trade.tradeId}; |
| onExec(askRpt); |
|
|
| if (bidOrderIt->remainingQty == 0) { |
| orderIndex_.erase(bidOrderIt->orderId); |
| bidOrderIt = bidLevel.erase(bidOrderIt); |
| } else { |
| ++bidOrderIt; |
| } |
| } |
| if (bidLevel.empty()) { |
| bidIt = bids_.erase(bidIt); |
| } else { |
| ++bidIt; |
| } |
| } |
|
|
| if (orderIt->remainingQty == 0) { |
| orderIndex_.erase(orderIt->orderId); |
| orderIt = level.erase(orderIt); |
| } else { |
| ++orderIt; |
| } |
| } |
| if (level.empty()) { |
| askIt = asks_.erase(askIt); |
| } else { |
| ++askIt; |
| } |
| } |
| } |
|
|
| |
|
|
| void Book::newOrder(Order& order, const TradeCallback& onTrade, |
| const ExecCallback& onExec) { |
| order.orderId = allocateOrderId(); |
| order.remainingQty = order.quantity; |
| order.status = OrderStatus::New; |
| order.entryTime = nowNs(); |
|
|
| |
| if (order.ordType == OrderType::StopMarket || order.ordType == OrderType::StopLimit) { |
| stopOrders_.push_back(order); |
| ExecutionReport rpt{order.orderId, order.clOrdId, OrderStatus::New, |
| 0, order.quantity, 0, 0, 0}; |
| onExec(rpt); |
| return; |
| } |
|
|
| |
| if (phase_ == TradingPhase::PreOpen || phase_ == TradingPhase::Opening) { |
| insertResting(order); |
| ExecutionReport rpt{order.orderId, order.clOrdId, OrderStatus::New, |
| 0, order.quantity, 0, 0, 0}; |
| onExec(rpt); |
| return; |
| } |
|
|
| |
| if (phase_ == TradingPhase::Closed) { |
| order.status = OrderStatus::Rejected; |
| ExecutionReport rpt{order.orderId, order.clOrdId, OrderStatus::Rejected, |
| 0, order.quantity, 0, 0, 0}; |
| onExec(rpt); |
| return; |
| } |
|
|
| |
| if (order.tif == TimeInForce::FOK) { |
| Quantity_t available = 0; |
| if (order.side == Side::Buy) { |
| for (auto& [price, orders] : asks_) { |
| if (order.ordType != OrderType::Market && price > order.price) |
| break; |
| for (auto& resting : orders) |
| available += resting.remainingQty; |
| if (available >= order.quantity) break; |
| } |
| } else { |
| for (auto& [price, orders] : bids_) { |
| if (order.ordType != OrderType::Market && price < order.price) |
| break; |
| for (auto& resting : orders) |
| available += resting.remainingQty; |
| if (available >= order.quantity) break; |
| } |
| } |
| if (available < order.quantity) { |
| order.status = OrderStatus::Rejected; |
| ExecutionReport rpt{order.orderId, order.clOrdId, OrderStatus::Rejected, |
| 0, order.quantity, 0, 0, 0}; |
| onExec(rpt); |
| return; |
| } |
| } |
|
|
| if (order.side == Side::Buy) { |
| matchBuy(order, onTrade, onExec); |
| } else { |
| matchSell(order, onTrade, onExec); |
| } |
|
|
| |
| if (order.remainingQty > 0) { |
| if (order.ordType == OrderType::Market || order.tif == TimeInForce::IOC) { |
| order.status = OrderStatus::Cancelled; |
| ExecutionReport rpt{order.orderId, order.clOrdId, OrderStatus::Cancelled, |
| order.quantity - order.remainingQty, 0, 0, 0, 0}; |
| onExec(rpt); |
| } else { |
| order.status = (order.remainingQty < order.quantity) |
| ? OrderStatus::PartiallyFilled : OrderStatus::New; |
| insertResting(order); |
| ExecutionReport rpt{order.orderId, order.clOrdId, order.status, |
| order.quantity - order.remainingQty, |
| order.remainingQty, 0, 0, 0}; |
| onExec(rpt); |
| } |
| } else { |
| order.status = OrderStatus::Filled; |
| ExecutionReport rpt{order.orderId, order.clOrdId, OrderStatus::Filled, |
| order.quantity, 0, 0, 0, 0}; |
| onExec(rpt); |
| } |
| } |
|
|
| |
| |
| |
|
|
| void Book::triggerStopOrders(Price_t tradePrice, const TradeCallback& onTrade, |
| const ExecCallback& onExec) { |
| std::vector<Order> remaining; |
| std::vector<Order> triggered; |
|
|
| for (auto& stop : stopOrders_) { |
| bool trigger = false; |
| if (stop.side == Side::Buy && tradePrice >= stop.stopPrice) |
| trigger = true; |
| else if (stop.side == Side::Sell && tradePrice <= stop.stopPrice) |
| trigger = true; |
|
|
| if (trigger) { |
| triggered.push_back(stop); |
| } else { |
| remaining.push_back(stop); |
| } |
| } |
|
|
| stopOrders_ = std::move(remaining); |
|
|
| for (auto& stop : triggered) { |
| Order converted{}; |
| converted.clOrdId = stop.clOrdId; |
| converted.symbolIdx = stop.symbolIdx; |
| converted.side = stop.side; |
| converted.tif = stop.tif; |
| converted.quantity = stop.remainingQty; |
| converted.sessionId = stop.sessionId; |
| converted.stopPrice = 0; |
|
|
| if (stop.ordType == OrderType::StopMarket) { |
| converted.ordType = OrderType::Market; |
| converted.price = 0; |
| } else { |
| converted.ordType = OrderType::Limit; |
| converted.price = stop.price; |
| } |
|
|
| newOrder(converted, onTrade, onExec); |
| } |
| } |
|
|
| |
|
|
| bool Book::cancelStopOrder(OrderId_t orderId, ExecutionReport& report) { |
| for (auto it = stopOrders_.begin(); it != stopOrders_.end(); ++it) { |
| if (it->orderId == orderId) { |
| report = {orderId, it->clOrdId, OrderStatus::Cancelled, |
| 0, 0, 0, 0, 0}; |
| stopOrders_.erase(it); |
| return true; |
| } |
| } |
| return false; |
| } |
|
|
| |
|
|
| void Book::matchBuy(Order& incoming, const TradeCallback& onTrade, |
| const ExecCallback& onExec) { |
| auto it = asks_.begin(); |
| while (incoming.remainingQty > 0 && it != asks_.end()) { |
| Price_t askPrice = it->first; |
|
|
| if (incoming.ordType != OrderType::Market && askPrice > incoming.price) |
| break; |
|
|
| auto& level = it->second; |
| auto orderIt = level.begin(); |
| while (incoming.remainingQty > 0 && orderIt != level.end()) { |
| Quantity_t traded = std::min(incoming.remainingQty, orderIt->remainingQty); |
| Price_t tradePrice = askPrice; |
|
|
| Trade trade{}; |
| trade.tradeId = allocateTradeId(); |
| trade.symbolIdx = symbolIdx_; |
| trade.price = tradePrice; |
| trade.quantity = traded; |
| trade.buyOrderId = incoming.orderId; |
| trade.sellOrderId = orderIt->orderId; |
| trade.buyClOrdId = incoming.clOrdId; |
| trade.sellClOrdId = orderIt->clOrdId; |
| trade.buySessionId = incoming.sessionId; |
| trade.sellSessionId = orderIt->sessionId; |
| trade.matchTime = nowNs(); |
| onTrade(trade); |
| lastTradePrice_ = tradePrice; |
|
|
| incoming.remainingQty -= traded; |
| orderIt->remainingQty -= traded; |
|
|
| ExecutionReport restingRpt{orderIt->orderId, orderIt->clOrdId, |
| orderIt->remainingQty == 0 ? OrderStatus::Filled : OrderStatus::PartiallyFilled, |
| orderIt->quantity - orderIt->remainingQty, |
| orderIt->remainingQty, tradePrice, traded, trade.tradeId}; |
| onExec(restingRpt); |
|
|
| if (orderIt->remainingQty == 0) { |
| orderIndex_.erase(orderIt->orderId); |
| orderIt = level.erase(orderIt); |
| } else { |
| ++orderIt; |
| } |
| } |
|
|
| if (level.empty()) { |
| it = asks_.erase(it); |
| } else { |
| ++it; |
| } |
| } |
| } |
|
|
| |
|
|
| void Book::matchSell(Order& incoming, const TradeCallback& onTrade, |
| const ExecCallback& onExec) { |
| auto it = bids_.begin(); |
| while (incoming.remainingQty > 0 && it != bids_.end()) { |
| Price_t bidPrice = it->first; |
|
|
| if (incoming.ordType != OrderType::Market && bidPrice < incoming.price) |
| break; |
|
|
| auto& level = it->second; |
| auto orderIt = level.begin(); |
| while (incoming.remainingQty > 0 && orderIt != level.end()) { |
| Quantity_t traded = std::min(incoming.remainingQty, orderIt->remainingQty); |
| Price_t tradePrice = bidPrice; |
|
|
| Trade trade{}; |
| trade.tradeId = allocateTradeId(); |
| trade.symbolIdx = symbolIdx_; |
| trade.price = tradePrice; |
| trade.quantity = traded; |
| trade.buyOrderId = orderIt->orderId; |
| trade.sellOrderId = incoming.orderId; |
| trade.buyClOrdId = orderIt->clOrdId; |
| trade.sellClOrdId = incoming.clOrdId; |
| trade.buySessionId = orderIt->sessionId; |
| trade.sellSessionId = incoming.sessionId; |
| trade.matchTime = nowNs(); |
| onTrade(trade); |
| lastTradePrice_ = tradePrice; |
|
|
| incoming.remainingQty -= traded; |
| orderIt->remainingQty -= traded; |
|
|
| ExecutionReport restingRpt{orderIt->orderId, orderIt->clOrdId, |
| orderIt->remainingQty == 0 ? OrderStatus::Filled : OrderStatus::PartiallyFilled, |
| orderIt->quantity - orderIt->remainingQty, |
| orderIt->remainingQty, tradePrice, traded, trade.tradeId}; |
| onExec(restingRpt); |
|
|
| if (orderIt->remainingQty == 0) { |
| orderIndex_.erase(orderIt->orderId); |
| orderIt = level.erase(orderIt); |
| } else { |
| ++orderIt; |
| } |
| } |
|
|
| if (level.empty()) { |
| it = bids_.erase(it); |
| } else { |
| ++it; |
| } |
| } |
| } |
|
|
| |
|
|
| bool Book::cancelOrder(OrderId_t orderId, ExecutionReport& report) { |
| |
| if (cancelStopOrder(orderId, report)) |
| return true; |
|
|
| auto it = orderIndex_.find(orderId); |
| if (it == orderIndex_.end()) |
| return false; |
|
|
| auto [side, price] = it->second; |
|
|
| if (side == Side::Buy) { |
| auto levelIt = bids_.find(price); |
| if (levelIt != bids_.end()) { |
| auto& vec = levelIt->second; |
| for (auto oit = vec.begin(); oit != vec.end(); ++oit) { |
| if (oit->orderId == orderId) { |
| report = {orderId, oit->clOrdId, OrderStatus::Cancelled, |
| oit->quantity - oit->remainingQty, 0, 0, 0, 0}; |
| vec.erase(oit); |
| if (vec.empty()) bids_.erase(levelIt); |
| orderIndex_.erase(it); |
| return true; |
| } |
| } |
| } |
| } else { |
| auto levelIt = asks_.find(price); |
| if (levelIt != asks_.end()) { |
| auto& vec = levelIt->second; |
| for (auto oit = vec.begin(); oit != vec.end(); ++oit) { |
| if (oit->orderId == orderId) { |
| report = {orderId, oit->clOrdId, OrderStatus::Cancelled, |
| oit->quantity - oit->remainingQty, 0, 0, 0, 0}; |
| vec.erase(oit); |
| if (vec.empty()) asks_.erase(levelIt); |
| orderIndex_.erase(it); |
| return true; |
| } |
| } |
| } |
| } |
| return false; |
| } |
|
|
| |
|
|
| bool Book::modifyOrder(OrderId_t orderId, Price_t newPrice, Quantity_t newQty, |
| ExecutionReport& report) { |
| ExecutionReport cancelRpt{}; |
| if (!cancelOrder(orderId, cancelRpt)) |
| return false; |
|
|
| Order replacement{}; |
| replacement.clOrdId = cancelRpt.clOrdId; |
| replacement.symbolIdx = symbolIdx_; |
| replacement.side = (bids_.count(cancelRpt.lastPrice)) ? Side::Buy : Side::Sell; |
| replacement.ordType = OrderType::Limit; |
| replacement.tif = TimeInForce::Day; |
| replacement.price = newPrice; |
| replacement.quantity = newQty; |
| replacement.remainingQty = newQty; |
| replacement.orderId = allocateOrderId(); |
| replacement.entryTime = nowNs(); |
| replacement.status = OrderStatus::New; |
| replacement.stopPrice = 0; |
|
|
| insertResting(replacement); |
| report = {replacement.orderId, replacement.clOrdId, OrderStatus::New, |
| 0, newQty, 0, 0, 0}; |
| return true; |
| } |
|
|
| |
|
|
| void Book::insertResting(Order& order) { |
| orderIndex_[order.orderId] = {order.side, order.price}; |
| if (order.side == Side::Buy) { |
| bids_[order.price].push_back(order); |
| } else { |
| asks_[order.price].push_back(order); |
| } |
| } |
|
|
| void Book::removeOrder(OrderId_t orderId, Side side, Price_t price) { |
| orderIndex_.erase(orderId); |
| if (side == Side::Buy) { |
| auto it = bids_.find(price); |
| if (it != bids_.end()) { |
| auto& vec = it->second; |
| vec.erase(std::remove_if(vec.begin(), vec.end(), |
| [orderId](const Order& o) { return o.orderId == orderId; }), vec.end()); |
| if (vec.empty()) bids_.erase(it); |
| } |
| } else { |
| auto it = asks_.find(price); |
| if (it != asks_.end()) { |
| auto& vec = it->second; |
| vec.erase(std::remove_if(vec.begin(), vec.end(), |
| [orderId](const Order& o) { return o.orderId == orderId; }), vec.end()); |
| if (vec.empty()) asks_.erase(it); |
| } |
| } |
| } |
|
|
| |
|
|
| Price_t Book::bestBid() const { |
| return bids_.empty() ? 0 : bids_.begin()->first; |
| } |
|
|
| Price_t Book::bestAsk() const { |
| return asks_.empty() ? 0 : asks_.begin()->first; |
| } |
|
|
| template<typename MapT> |
| std::vector<Book::Level> Book::getLevels(const MapT& map, int depth) const { |
| std::vector<Level> result; |
| result.reserve(depth); |
| int count = 0; |
| for (auto& [price, orders] : map) { |
| if (count >= depth) break; |
| Quantity_t totalQty = 0; |
| for (auto& o : orders) totalQty += o.remainingQty; |
| result.push_back({price, totalQty, static_cast<int>(orders.size())}); |
| ++count; |
| } |
| return result; |
| } |
|
|
| std::vector<Book::Level> Book::getBids(int depth) const { |
| return getLevels(bids_, depth); |
| } |
|
|
| std::vector<Book::Level> Book::getAsks(int depth) const { |
| return getLevels(asks_, depth); |
| } |
|
|
| size_t Book::bidCount() const { |
| size_t count = 0; |
| for (auto& [_, orders] : bids_) count += orders.size(); |
| return count; |
| } |
|
|
| size_t Book::askCount() const { |
| size_t count = 0; |
| for (auto& [_, orders] : asks_) count += orders.size(); |
| return count; |
| } |
|
|
| } |
|
|