From 912a8a68174a2a1aad04560167293956f34e3bee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 02:44:25 +0000 Subject: [PATCH 1/4] feat: add UserID to orders and implement self-trade prevention Agent-Logs-Url: https://github.com/geseq/cpp-orderbook/sessions/1375c9ee-a4fa-4c9c-8c02-e7cb3b2eb6a6 Co-authored-by: geseq <5458743+geseq@users.noreply.github.com> --- include/order.hpp | 3 +- include/orderbook.hpp | 51 ++++++++++++++--------- include/orderqueue.hpp | 4 +- include/pricelevel.hpp | 4 +- include/types.hpp | 5 +++ main.cpp | 4 +- src/orderqueue.cpp | 11 +++-- src/pricelevel.cpp | 44 ++++++++++++-------- src/types.cpp | 2 + test/determinism_test.cpp | 11 ++--- test/orderbook_test.cpp | 85 ++++++++++++++++++++++++++++++++++++++- test/orderqueue_test.cpp | 45 +++++++++++---------- test/pricelevel_test.cpp | 20 ++++----- test/util.cpp | 1 + 14 files changed, 206 insertions(+), 84 deletions(-) diff --git a/include/order.hpp b/include/order.hpp index a97e8b9..2957071 100644 --- a/include/order.hpp +++ b/include/order.hpp @@ -15,6 +15,7 @@ using namespace boost::intrusive; struct Order : public set_base_hook>, list_base_hook> { OrderID id; + UserID user_id; Decimal qty; Decimal original_qty; Decimal price; @@ -24,7 +25,7 @@ struct Order : public set_base_hook>, list_base_hook(const Order &a, const Order &b) { return a.id > b.id; } diff --git a/include/orderbook.hpp b/include/orderbook.hpp index 1788d34..7a25bd1 100644 --- a/include/orderbook.hpp +++ b/include/orderbook.hpp @@ -24,8 +24,8 @@ class OrderBook { asks_(PriceLevel(price_level_pool_size)), orders_(OrderMap()){}; - void addOrder(OrderID id, Type type, Side side, Decimal qty, Decimal price, Flag flag); - void putTradeNotification(OrderID mOrderID, OrderID tOrderID, OrderStatus mStatus, OrderStatus tStatus, Decimal qty, Decimal price); + void addOrder(OrderID id, UserID user_id, Type type, Side side, Decimal qty, Decimal price, Flag flag); + void putTradeNotification(OrderID mOrderID, OrderID tOrderID, UserID mUserID, UserID tUserID, OrderStatus mStatus, OrderStatus tStatus, Decimal qty, Decimal price); void cancelOrder(OrderID id); bool hasOrder(OrderID id); void setMatching(bool matching) { matching_ = matching; } @@ -46,17 +46,18 @@ class OrderBook { bool matching_ = true; - std::pair eraseOrder(OrderID id); - void processOrder(OrderID id, Type type, Side side, Decimal qty, Decimal price, Flag flag); + std::tuple eraseOrder(OrderID id); + void processOrder(OrderID id, UserID user_id, Type type, Side side, Decimal qty, Decimal price, Flag flag); }; template -void OrderBook::addOrder(OrderID id, Type type, Side side, Decimal qty, Decimal price, Flag flag) { +void OrderBook::addOrder(OrderID id, UserID user_id, Type type, Side side, Decimal qty, Decimal price, Flag flag) { if (qty.is_zero()) [[unlikely]] { notification_.onExecutionReport(ExecutionReport{ .exec_type = ExecType::Rejected, .msg_type = MsgType::CreateOrder, .order_id = id, + .user_id = user_id, .status = OrderStatus::Rejected, .qty = qty, .original_qty = qty, @@ -71,6 +72,7 @@ void OrderBook::addOrder(OrderID id, Type type, Side side, Decimal .exec_type = ExecType::Rejected, .msg_type = MsgType::CreateOrder, .order_id = id, + .user_id = user_id, .status = OrderStatus::Rejected, .qty = qty, .original_qty = qty, @@ -86,6 +88,7 @@ void OrderBook::addOrder(OrderID id, Type type, Side side, Decimal .exec_type = ExecType::Rejected, .msg_type = MsgType::CreateOrder, .order_id = id, + .user_id = user_id, .status = OrderStatus::Rejected, .qty = qty, .original_qty = qty, @@ -100,6 +103,7 @@ void OrderBook::addOrder(OrderID id, Type type, Side side, Decimal .exec_type = ExecType::Rejected, .msg_type = MsgType::CreateOrder, .order_id = id, + .user_id = user_id, .status = OrderStatus::Rejected, .qty = qty, .original_qty = qty, @@ -116,6 +120,7 @@ void OrderBook::addOrder(OrderID id, Type type, Side side, Decimal .exec_type = ExecType::Rejected, .msg_type = MsgType::CreateOrder, .order_id = id, + .user_id = user_id, .status = OrderStatus::Rejected, .qty = uint64_t(0), .original_qty = qty, @@ -129,6 +134,7 @@ void OrderBook::addOrder(OrderID id, Type type, Side side, Decimal .exec_type = ExecType::Rejected, .msg_type = MsgType::CreateOrder, .order_id = id, + .user_id = user_id, .status = OrderStatus::Rejected, .qty = uint64_t(0), .original_qty = qty, @@ -142,26 +148,27 @@ void OrderBook::addOrder(OrderID id, Type type, Side side, Decimal .exec_type = ExecType::New, .msg_type = MsgType::CreateOrder, .order_id = id, + .user_id = user_id, .status = OrderStatus::Accepted, .qty = qty, .original_qty = qty, }); - processOrder(id, type, side, qty, price, flag); + processOrder(id, user_id, type, side, qty, price, flag); } template -void OrderBook::processOrder(OrderID id, Type type, Side side, Decimal qty, Decimal price, Flag flag) { - const auto tradeNotification = [this](OrderID mOrderID, OrderID tOrderID, OrderStatus mOrderStatus, OrderStatus tOrderStatus, Decimal qty, Decimal price) { - this->putTradeNotification(mOrderID, tOrderID, mOrderStatus, tOrderStatus, qty, price); +void OrderBook::processOrder(OrderID id, UserID user_id, Type type, Side side, Decimal qty, Decimal price, Flag flag) { + const auto tradeNotification = [this](OrderID mOrderID, OrderID tOrderID, UserID mUserID, UserID tUserID, OrderStatus mOrderStatus, OrderStatus tOrderStatus, Decimal qty, Decimal price) { + this->putTradeNotification(mOrderID, tOrderID, mUserID, tUserID, mOrderStatus, tOrderStatus, qty, price); this->last_price = price; }; const auto postOrderFill = [this](OrderID id) { this->eraseOrder(id); }; if (type == Type::Market) { if (side == Side::Buy) { - asks_.processMarketOrder(tradeNotification, postOrderFill, id, qty, flag); + asks_.processMarketOrder(tradeNotification, postOrderFill, id, user_id, qty, flag); } else { - bids_.processMarketOrder(tradeNotification, postOrderFill, id, qty, flag); + bids_.processMarketOrder(tradeNotification, postOrderFill, id, user_id, qty, flag); } return; @@ -169,9 +176,9 @@ void OrderBook::processOrder(OrderID id, Type type, Side side, Dec Decimal qtyProcessed; if (side == Side::Buy) { - qtyProcessed = asks_.processLimitOrder(tradeNotification, postOrderFill, id, price, qty, flag); + qtyProcessed = asks_.processLimitOrder(tradeNotification, postOrderFill, id, user_id, price, qty, flag); } else { - qtyProcessed = bids_.processLimitOrder(tradeNotification, postOrderFill, id, price, qty, flag); + qtyProcessed = bids_.processLimitOrder(tradeNotification, postOrderFill, id, user_id, price, qty, flag); } if ((flag & (IoC | FoK)) != 0) { @@ -180,7 +187,7 @@ void OrderBook::processOrder(OrderID id, Type type, Side side, Dec auto qtyLeft = qty - qtyProcessed; if (qtyLeft > uint64_t(0)) { - auto* o = order_pool_.acquire(id, type, side, qtyLeft, price, flag); + auto* o = order_pool_.acquire(id, user_id, type, side, qtyLeft, price, flag); o->original_qty = qty; if (side == Side::Buy) { bids_.append(o); @@ -195,11 +202,13 @@ void OrderBook::processOrder(OrderID id, Type type, Side side, Dec } template -void OrderBook::putTradeNotification(OrderID mOrderID, OrderID tOrderID, OrderStatus mStatus, OrderStatus tStatus, Decimal qty, Decimal price) { +void OrderBook::putTradeNotification(OrderID mOrderID, OrderID tOrderID, UserID mUserID, UserID tUserID, OrderStatus mStatus, OrderStatus tStatus, Decimal qty, Decimal price) { notification_.onExecutionReport(ExecutionReport{ .exec_type = ExecType::Trade, .maker_order_id = mOrderID, .taker_order_id = tOrderID, + .maker_user_id = mUserID, + .taker_user_id = tUserID, .maker_status = mStatus, .taker_status = tStatus, .last_qty = qty, @@ -209,7 +218,7 @@ void OrderBook::putTradeNotification(OrderID mOrderID, OrderID tOr template void OrderBook::cancelOrder(OrderID id) { - auto [qty, original_qty] = eraseOrder(id); + auto [qty, original_qty, user_id] = eraseOrder(id); if (qty.is_zero()) { notification_.onExecutionReport(ExecutionReport{ .exec_type = ExecType::Rejected, @@ -227,6 +236,7 @@ void OrderBook::cancelOrder(OrderID id) { .exec_type = ExecType::Canceled, .msg_type = MsgType::CancelOrder, .order_id = id, + .user_id = user_id, .status = OrderStatus::Canceled, .qty = qty, .original_qty = original_qty, @@ -234,10 +244,10 @@ void OrderBook::cancelOrder(OrderID id) { } template -std::pair OrderBook::eraseOrder(OrderID id) { +std::tuple OrderBook::eraseOrder(OrderID id) { auto it = orders_.find(id, OrderIDCompare()); if (it == orders_.end()) { - return {uint64_t(0), uint64_t(0)}; + return {uint64_t(0), uint64_t(0), uint64_t(0)}; } auto& pool = order_pool_; @@ -245,13 +255,14 @@ std::pair OrderBook::eraseOrder(OrderID id) { scope_exit defer([&pool, &order]() { pool.release(order); }); orders_.erase(*it); + const UserID user_id = order->user_id; if (order->side == Side::Buy) { bids_.remove(order); - return {order->qty, order->original_qty}; + return {order->qty, order->original_qty, user_id}; } asks_.remove(order); - return {order->qty, order->original_qty}; + return {order->qty, order->original_qty, user_id}; } template diff --git a/include/orderqueue.hpp b/include/orderqueue.hpp index 20cf7a0..bb7c526 100644 --- a/include/orderqueue.hpp +++ b/include/orderqueue.hpp @@ -10,7 +10,7 @@ namespace orderbook { using TradeNotification = - std::function; + std::function; using PostOrderFill = std::function; using OrderList = boost::intrusive::list; @@ -28,7 +28,7 @@ class OrderQueue : public boost::intrusive::set_base_hook - Decimal processMarketOrder(const TradeNotification& tn, const PostOrderFill& pf, OrderID takerOrderID, Decimal qty, Flag flag); + Decimal processMarketOrder(const TradeNotification& tn, const PostOrderFill& pf, OrderID takerOrderID, UserID takerUserID, Decimal qty, Flag flag); template - Decimal processLimitOrder(const TradeNotification& tn, const PostOrderFill& pf, OrderID takerOrderID, Decimal price, Decimal qty, Flag flag); + Decimal processLimitOrder(const TradeNotification& tn, const PostOrderFill& pf, OrderID takerOrderID, UserID takerUserID, Decimal price, Decimal qty, Flag flag); const PriceTree& price_tree() const { return price_tree_; }; }; diff --git a/include/types.hpp b/include/types.hpp index b42a50c..1ab18e7 100644 --- a/include/types.hpp +++ b/include/types.hpp @@ -11,6 +11,7 @@ namespace orderbook { using Decimal = decimal::U8; using OrderID = uint64_t; +using UserID = uint64_t; enum class Type : uint8_t { Limit, @@ -58,6 +59,7 @@ enum class Error : uint16_t { OrderNotExists, InsufficientQty, NoMatching, + SelfTrade, }; std::ostream& operator<<(std::ostream& os, const Error& error); @@ -87,6 +89,7 @@ struct ExecutionReport { // Order event fields (New, Rejected, Canceled) MsgType msg_type{}; OrderID order_id{}; + UserID user_id{}; OrderStatus status{}; Decimal qty{}; Decimal original_qty{}; @@ -94,6 +97,8 @@ struct ExecutionReport { // Trade event fields OrderID maker_order_id{}; OrderID taker_order_id{}; + UserID maker_user_id{}; + UserID taker_user_id{}; OrderStatus maker_status{}; OrderStatus taker_status{}; Decimal last_qty{}; diff --git a/main.cpp b/main.cpp index 898722f..15eebcf 100644 --- a/main.cpp +++ b/main.cpp @@ -90,8 +90,8 @@ void throughput(int64_t seed, int duration, orderbook::Decimal lowerBound, order ob->cancelOrder(sellID); buyID = ++nextID; sellID = ++nextID; - ob->addOrder(buyID, Type::Limit, Side::Buy, bidQty, bid, Flag::None); - ob->addOrder(sellID, Type::Limit, Side::Sell, askQty, ask, Flag::None); + ob->addOrder(buyID, 0, Type::Limit, Side::Buy, bidQty, bid, Flag::None); + ob->addOrder(sellID, 0, Type::Limit, Side::Sell, askQty, ask, Flag::None); operations += 4; // 2 cancels + 2 adds } diff --git a/src/orderqueue.cpp b/src/orderqueue.cpp index d806c67..f733acb 100644 --- a/src/orderqueue.cpp +++ b/src/orderqueue.cpp @@ -29,7 +29,7 @@ void OrderQueue::remove(Order* o) { } } -Decimal OrderQueue::process(const TradeNotification& tradeNotification, const PostOrderFill& postFill, OrderID takerOrderID, Decimal qty) { +Decimal OrderQueue::process(const TradeNotification& tradeNotification, const PostOrderFill& postFill, OrderID takerOrderID, UserID takerUserID, Decimal qty) { Decimal qtyProcessed = {}; BOOST_ASSERT(orders_.begin() != orders_.end()); for (auto it = orders_.begin(); it != orders_.end() && qty > uint64_t(0);) { @@ -37,14 +37,19 @@ Decimal OrderQueue::process(const TradeNotification& tradeNotification, const Po BOOST_ASSERT(orders_.begin() != orders_.end()); auto* ho = &*it; BOOST_ASSERT(ho != nullptr); + if (takerUserID != 0 && ho->user_id == takerUserID) { + ++it; + continue; + } if (qty < ho->qty) { qtyProcessed += qty; ho->qty -= qty; total_qty_ -= qty; - tradeNotification(ho->id, takerOrderID, OrderStatus::FilledPartial, OrderStatus::FilledComplete, qty, ho->price); + tradeNotification(ho->id, takerOrderID, ho->user_id, takerUserID, OrderStatus::FilledPartial, OrderStatus::FilledComplete, qty, ho->price); break; } else { const auto makerOrderID = ho->id; + const auto makerUserID = ho->user_id; const auto makerPrice = ho->price; auto matchedQty = ho->qty; qtyProcessed += matchedQty; @@ -56,7 +61,7 @@ Decimal OrderQueue::process(const TradeNotification& tradeNotification, const Po postFill(makerOrderID); // qty has already been decremented by matchedQty, so zero means taker is fully filled. const auto takerStatus = qty.is_zero() ? OrderStatus::FilledComplete : OrderStatus::FilledPartial; - tradeNotification(makerOrderID, takerOrderID, OrderStatus::FilledComplete, takerStatus, matchedQty, makerPrice); + tradeNotification(makerOrderID, takerOrderID, makerUserID, takerUserID, OrderStatus::FilledComplete, takerStatus, matchedQty, makerPrice); } } diff --git a/src/pricelevel.cpp b/src/pricelevel.cpp index 60e9701..9a3ae49 100644 --- a/src/pricelevel.cpp +++ b/src/pricelevel.cpp @@ -73,7 +73,7 @@ OrderQueue* PriceLevel

::getQueue() { template template -Decimal PriceLevel

::processMarketOrder(const TradeNotification& tn, const PostOrderFill& pf, OrderID takerOrderID, Decimal qty, Flag flag) { +Decimal PriceLevel

::processMarketOrder(const TradeNotification& tn, const PostOrderFill& pf, OrderID takerOrderID, UserID takerUserID, Decimal qty, Flag flag) { static_assert(Q == PriceType::Bid || Q == PriceType::Ask, "Unsupported PriceType"); if ((flag & (AoN | FoK)) != 0 && qty > volume_) { @@ -83,11 +83,17 @@ Decimal PriceLevel

::processMarketOrder(const TradeNotification& tn, const Pos auto qtyLeft = qty; Decimal qtyProcessed = uint64_t(0); - for (auto q = getQueue(); !qtyLeft.is_zero() && q != nullptr; q = getQueue()) { - auto pq = q->process(tn, pf, takerOrderID, qtyLeft); - qtyLeft -= pq; - qtyProcessed += pq; - volume_ -= pq; + for (auto q = getQueue(); !qtyLeft.is_zero() && q != nullptr;) { + auto pq = q->process(tn, pf, takerOrderID, takerUserID, qtyLeft); + if (!pq.is_zero()) { + qtyLeft -= pq; + qtyProcessed += pq; + volume_ -= pq; + q = getQueue(); + } else { + // No progress at this price level (all STP); advance to next queue. + q = getNextQueue(q->price()); + } } return qtyProcessed; @@ -95,7 +101,7 @@ Decimal PriceLevel

::processMarketOrder(const TradeNotification& tn, const Pos template template -Decimal PriceLevel

::processLimitOrder(const TradeNotification& tn, const PostOrderFill& pf, OrderID takerOrderID, Decimal price, Decimal qty, Flag flag) { +Decimal PriceLevel

::processLimitOrder(const TradeNotification& tn, const PostOrderFill& pf, OrderID takerOrderID, UserID takerUserID, Decimal price, Decimal qty, Flag flag) { static_assert(Q == PriceType::Bid || Q == PriceType::Ask, "Unsupported PriceType"); Decimal qtyProcessed = {}; auto orderQueue = getQueue(); @@ -151,17 +157,23 @@ Decimal PriceLevel

::processLimitOrder(const TradeNotification& tn, const Post orderQueue = getQueue(); Decimal qtyLeft = qty; - for (orderQueue = getQueue(); !qtyLeft.is_zero() && orderQueue != nullptr; orderQueue = getQueue()) { + for (orderQueue = getQueue(); !qtyLeft.is_zero() && orderQueue != nullptr;) { // Stop as soon as the best remaining queue price no longer satisfies the limit. if constexpr (std::is_same_v) { if (orderQueue->price() < price) break; // bid price fell below sell limit } else { if (orderQueue->price() > price) break; // ask price rose above buy limit } - Decimal result = orderQueue->process(tn, pf, takerOrderID, qtyLeft); - qtyLeft -= result; - qtyProcessed += result; - volume_ -= result; + Decimal result = orderQueue->process(tn, pf, takerOrderID, takerUserID, qtyLeft); + if (!result.is_zero()) { + qtyLeft -= result; + qtyProcessed += result; + volume_ -= result; + orderQueue = getQueue(); + } else { + // No progress at this price level (all STP); advance to next queue. + orderQueue = getNextQueue(orderQueue->price()); + } } return qtyProcessed; @@ -202,15 +214,15 @@ template class PriceLevel; template class PriceLevel; template Decimal PriceLevel::processMarketOrder(const TradeNotification& tn, const PostOrderFill& pf, OrderID takerOrderID, - Decimal qty, Flag flag); + UserID takerUserID, Decimal qty, Flag flag); template Decimal PriceLevel::processMarketOrder(const TradeNotification& tn, const PostOrderFill& pf, OrderID takerOrderID, - Decimal qty, Flag flag); + UserID takerUserID, Decimal qty, Flag flag); template Decimal PriceLevel::processLimitOrder(const TradeNotification& tn, const PostOrderFill& pf, OrderID takerOrderID, - Decimal price, Decimal qty, Flag flag); + UserID takerUserID, Decimal price, Decimal qty, Flag flag); template Decimal PriceLevel::processLimitOrder(const TradeNotification& tn, const PostOrderFill& pf, OrderID takerOrderID, - Decimal price, Decimal qty, Flag flag); + UserID takerUserID, Decimal price, Decimal qty, Flag flag); } // namespace orderbook diff --git a/src/types.cpp b/src/types.cpp index fc7a71f..1c924f5 100644 --- a/src/types.cpp +++ b/src/types.cpp @@ -74,6 +74,8 @@ std::ostream& operator<<(std::ostream& os, const Error& error) { return os << "InsufficientQty"; case Error::NoMatching: return os << "NoMatching"; + case Error::SelfTrade: + return os << "SelfTrade"; } return os << "Unknown"; } diff --git a/test/determinism_test.cpp b/test/determinism_test.cpp index 83350b2..7d01944 100644 --- a/test/determinism_test.cpp +++ b/test/determinism_test.cpp @@ -21,6 +21,7 @@ class DeterminismTest : public ::testing::Test { Decimal price = Decimal(0, 0); Flag flag = Flag::None; bool matching = true; + UserID user_id = 0; }; static void applyAction(const Action& action, const std::shared_ptr>& localOb) { @@ -29,7 +30,7 @@ class DeterminismTest : public ::testing::Test { } else if (action.kind == Action::Kind::Cancel) { localOb->cancelOrder(action.id); } else { - localOb->addOrder(action.id, action.type, action.side, action.qty, action.price, action.flag); + localOb->addOrder(action.id, action.user_id, action.type, action.side, action.qty, action.price, action.flag); } } @@ -151,11 +152,11 @@ TEST_F(DeterminismTest, IndependentBooksStayIsolated) { auto ob1 = std::make_shared>(n1); auto ob2 = std::make_shared>(n2); - ob1->addOrder(1000, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(100, 0), Flag::None); - ob1->addOrder(1001, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(100, 0), Flag::None); + ob1->addOrder(1000, 0, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(100, 0), Flag::None); + ob1->addOrder(1001, 0, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(100, 0), Flag::None); - ob2->addOrder(2000, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(100, 0), Flag::None); - ob2->addOrder(2001, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(100, 0), Flag::None); + ob2->addOrder(2000, 0, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(100, 0), Flag::None); + ob2->addOrder(2001, 0, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(100, 0), Flag::None); n1.Verify({"CreateOrder Accepted 1000 2 2", "CreateOrder Accepted 1001 2 2", "1000 1001 FilledComplete FilledComplete 2 100"}); n2.Verify({"CreateOrder Accepted 2000 2 2", "CreateOrder Accepted 2001 2 2", "2000 2001 FilledComplete FilledComplete 2 100"}); diff --git a/test/orderbook_test.cpp b/test/orderbook_test.cpp index b7180cb..6e5e76e 100644 --- a/test/orderbook_test.cpp +++ b/test/orderbook_test.cpp @@ -40,8 +40,9 @@ class LimitOrderTest : public ::testing::Test { } else if (parts[5] == "F") { flag = orderbook::FoK; } + UserID user_id = (parts.size() > 6) ? std::stoull(parts[6]) : 0; - ob->addOrder(oid, type, side, qty, price, flag); + ob->addOrder(oid, user_id, type, side, qty, price, flag); } void processOrders(std::shared_ptr>& ob, const std::string& input, int prefix) { @@ -755,6 +756,88 @@ TEST_F(LimitOrderTest, TestFoK_MarketBuy_CannotFill) { n.Verify({"CreateOrder Accepted 811 11 11"}); } +// ────────────────────────────────────────────────────────────────────────────── +// Self-Trade Prevention +// ────────────────────────────────────────────────────────────────────────────── + +// Limit buy STP: taker (user 1) would match maker (user 1) at same price – no trade. +TEST_F(LimitOrderTest, TestSTP_LimitBuy_SameUser_NoTrade) { + // User 1 places a sell limit at 100, qty 2. + processLine(ob, "900\tL\tS\t2\t100\tN\t1"); + n.Reset(); + + // User 1 places a buy limit at 100 – self-trade, should not execute. + processLine(ob, "901\tL\tB\t2\t100\tN\t1"); + // clang-format off + n.Verify({"CreateOrder Accepted 901 2 2"}); + // clang-format on + // Taker rests in the book (no fill occurred). + ASSERT_TRUE(ob->hasOrder(901)); +} + +// Limit sell STP: taker (user 2) would match maker (user 2) at same price – no trade. +TEST_F(LimitOrderTest, TestSTP_LimitSell_SameUser_NoTrade) { + // User 2 places a buy limit at 90, qty 2. + processLine(ob, "910\tL\tB\t2\t90\tN\t2"); + n.Reset(); + + // User 2 places a sell limit at 90 – self-trade, should not execute. + processLine(ob, "911\tL\tS\t2\t90\tN\t2"); + // clang-format off + n.Verify({"CreateOrder Accepted 911 2 2"}); + // clang-format on + ASSERT_TRUE(ob->hasOrder(911)); +} + +// Limit buy STP: taker (user 1) skips maker (user 1) and trades with maker (user 2). +TEST_F(LimitOrderTest, TestSTP_LimitBuy_SkipsSelfTradeMatchesOther) { + // User 1 places a sell at 100, qty 2 (self-trade target). + processLine(ob, "920\tL\tS\t2\t100\tN\t1"); + // User 2 places a sell at 100, qty 2 (valid match). + processLine(ob, "921\tL\tS\t2\t100\tN\t2"); + n.Reset(); + + // User 1 places a buy at 100, qty 2 – skips own order (920), trades with user 2 (921). + processLine(ob, "922\tL\tB\t2\t100\tN\t1"); + // clang-format off + n.Verify({"CreateOrder Accepted 922 2 2", + "921 922 FilledComplete FilledComplete 2 100"}); + // clang-format on + ASSERT_FALSE(ob->hasOrder(922)); + // User 1's own sell order still rests in the book. + ASSERT_TRUE(ob->hasOrder(920)); +} + +// Market buy STP: taker (user 1) skips self-trade maker and trades with different-user maker. +TEST_F(LimitOrderTest, TestSTP_MarketBuy_SkipsSelfTradeMatchesOther) { + // User 1 places a sell at 100, qty 2. + processLine(ob, "930\tL\tS\t2\t100\tN\t1"); + // User 2 places a sell at 110, qty 2. + processLine(ob, "931\tL\tS\t2\t110\tN\t2"); + n.Reset(); + + // User 1 market buy, qty 2 – skips own 930, trades with 931 (user 2). + processLine(ob, "932\tM\tB\t2\t0\tN\t1"); + // clang-format off + n.Verify({"CreateOrder Accepted 932 2 2", + "931 932 FilledComplete FilledComplete 2 110"}); + // clang-format on + ASSERT_TRUE(ob->hasOrder(930)); +} + +// Limit buy STP: all resting asks belong to the taker – taker rests, no trade. +TEST_F(LimitOrderTest, TestSTP_LimitBuy_AllSelfTrade_TakerRests) { + // User 3 places two sell orders. + processLine(ob, "940\tL\tS\t2\t100\tN\t3"); + processLine(ob, "941\tL\tS\t2\t110\tN\t3"); + n.Reset(); + + // User 3 buys – all asks are own orders, nothing trades, taker rests. + processLine(ob, "942\tL\tB\t4\t110\tN\t3"); + n.Verify({"CreateOrder Accepted 942 4 4"}); + ASSERT_TRUE(ob->hasOrder(942)); +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); diff --git a/test/orderqueue_test.cpp b/test/orderqueue_test.cpp index eb822c1..e05c867 100644 --- a/test/orderqueue_test.cpp +++ b/test/orderqueue_test.cpp @@ -17,8 +17,8 @@ TEST_F(OrderQueueTest, TestOrderQueue) { Decimal price(100, 0); auto oq = std::make_unique(price); - auto o1 = Order(1, Type::Limit, Side::Buy, Decimal(100, 0), Decimal(100, 0), Flag::None); - auto o2 = Order(2, Type::Limit, Side::Buy, Decimal(100, 0), Decimal(100, 0), Flag::None); + auto o1 = Order(1, 0, Type::Limit, Side::Buy, Decimal(100, 0), Decimal(100, 0), Flag::None); + auto o2 = Order(2, 0, Type::Limit, Side::Buy, Decimal(100, 0), Decimal(100, 0), Flag::None); oq->append(&o1); oq->append(&o2); @@ -48,14 +48,14 @@ TEST_F(OrderQueueTest, TestOrderQueue_ProcessUpdatesTotalQtyOnFullFill) { Decimal price(100, 0); auto oq = std::make_unique(price); - auto o1 = Order(1, Type::Limit, Side::Buy, Decimal(100, 0), price, Flag::None); - auto o2 = Order(2, Type::Limit, Side::Buy, Decimal(100, 0), price, Flag::None); + auto o1 = Order(1, 0, Type::Limit, Side::Buy, Decimal(100, 0), price, Flag::None); + auto o2 = Order(2, 0, Type::Limit, Side::Buy, Decimal(100, 0), price, Flag::None); oq->append(&o1); oq->append(&o2); - const TradeNotification tn = [](OrderID makerOrderID, OrderID takerOrderID, OrderStatus makerOrderStatus, OrderStatus takerOrderStatus, Decimal matchedQty, - Decimal matchedPrice) {}; + const TradeNotification tn = [](OrderID makerOrderID, OrderID takerOrderID, UserID makerUserID, UserID takerUserID, OrderStatus makerOrderStatus, + OrderStatus takerOrderStatus, Decimal matchedQty, Decimal matchedPrice) {}; const PostOrderFill pf = [&oq, &o1, &o2](OrderID id) { if (id == 1) { oq->remove(&o1); @@ -64,7 +64,7 @@ TEST_F(OrderQueueTest, TestOrderQueue_ProcessUpdatesTotalQtyOnFullFill) { } }; - auto qtyProcessed = oq->process(tn, pf, 900, Decimal(150, 0)); + auto qtyProcessed = oq->process(tn, pf, 900, 0, Decimal(150, 0)); EXPECT_EQ(qtyProcessed, Decimal(150, 0)); EXPECT_EQ(oq->len(), 1); @@ -78,14 +78,14 @@ TEST_F(OrderQueueTest, TestOrderQueue_ProcessUpdatesTotalQtyOnExactFill) { Decimal price(100, 0); auto oq = std::make_unique(price); - auto o1 = Order(1, Type::Limit, Side::Buy, Decimal(100, 0), price, Flag::None); - auto o2 = Order(2, Type::Limit, Side::Buy, Decimal(100, 0), price, Flag::None); + auto o1 = Order(1, 0, Type::Limit, Side::Buy, Decimal(100, 0), price, Flag::None); + auto o2 = Order(2, 0, Type::Limit, Side::Buy, Decimal(100, 0), price, Flag::None); oq->append(&o1); oq->append(&o2); - const TradeNotification tn = [](OrderID makerOrderID, OrderID takerOrderID, OrderStatus makerOrderStatus, OrderStatus takerOrderStatus, Decimal matchedQty, - Decimal matchedPrice) {}; + const TradeNotification tn = [](OrderID makerOrderID, OrderID takerOrderID, UserID makerUserID, UserID takerUserID, OrderStatus makerOrderStatus, + OrderStatus takerOrderStatus, Decimal matchedQty, Decimal matchedPrice) {}; const PostOrderFill pf = [&oq, &o1, &o2](OrderID id) { if (id == 1) { oq->remove(&o1); @@ -94,7 +94,7 @@ TEST_F(OrderQueueTest, TestOrderQueue_ProcessUpdatesTotalQtyOnExactFill) { } }; - auto qtyProcessed = oq->process(tn, pf, 901, Decimal(200, 0)); + auto qtyProcessed = oq->process(tn, pf, 901, 0, Decimal(200, 0)); EXPECT_EQ(qtyProcessed, Decimal(200, 0)); EXPECT_EQ(oq->len(), 0); @@ -105,15 +105,15 @@ TEST_F(OrderQueueTest, TestOrderQueue_ProcessZeroesFilledOrderBeforePostFill) { Decimal price(100, 0); auto oq = std::make_unique(price); - auto o1 = Order(1, Type::Limit, Side::Buy, Decimal(100, 0), price, Flag::None); - auto o2 = Order(2, Type::Limit, Side::Buy, Decimal(100, 0), price, Flag::None); + auto o1 = Order(1, 0, Type::Limit, Side::Buy, Decimal(100, 0), price, Flag::None); + auto o2 = Order(2, 0, Type::Limit, Side::Buy, Decimal(100, 0), price, Flag::None); oq->append(&o1); oq->append(&o2); bool sawZeroQtyOnPostFill = false; - const TradeNotification tn = [](OrderID makerOrderID, OrderID takerOrderID, OrderStatus makerOrderStatus, OrderStatus takerOrderStatus, Decimal matchedQty, - Decimal matchedPrice) {}; + const TradeNotification tn = [](OrderID makerOrderID, OrderID takerOrderID, UserID makerUserID, UserID takerUserID, OrderStatus makerOrderStatus, + OrderStatus takerOrderStatus, Decimal matchedQty, Decimal matchedPrice) {}; const PostOrderFill pf = [&oq, &o1, &o2, &sawZeroQtyOnPostFill](OrderID id) { if (id == 1) { sawZeroQtyOnPostFill = (o1.qty == Decimal(0, 0)); @@ -123,7 +123,7 @@ TEST_F(OrderQueueTest, TestOrderQueue_ProcessZeroesFilledOrderBeforePostFill) { } }; - auto qtyProcessed = oq->process(tn, pf, 902, Decimal(150, 0)); + auto qtyProcessed = oq->process(tn, pf, 902, 0, Decimal(150, 0)); EXPECT_EQ(qtyProcessed, Decimal(150, 0)); EXPECT_TRUE(sawZeroQtyOnPostFill); @@ -138,16 +138,17 @@ TEST_F(OrderQueueTest, TestOrderQueue_ProcessUsesSnapshotForTradeNotificationAft Decimal price(100, 0); auto oq = std::make_unique(price); - auto o1 = Order(1, Type::Limit, Side::Buy, Decimal(100, 0), price, Flag::None); - auto o2 = Order(2, Type::Limit, Side::Buy, Decimal(100, 0), price, Flag::None); + auto o1 = Order(1, 0, Type::Limit, Side::Buy, Decimal(100, 0), price, Flag::None); + auto o2 = Order(2, 0, Type::Limit, Side::Buy, Decimal(100, 0), price, Flag::None); oq->append(&o1); oq->append(&o2); OrderID makerOrderID = 0; Decimal matchedPrice(0, 0); - const TradeNotification tn = [&makerOrderID, &matchedPrice](OrderID makerID, OrderID takerOrderID, OrderStatus makerOrderStatus, - OrderStatus takerOrderStatus, Decimal matchedQty, Decimal priceValue) { + const TradeNotification tn = [&makerOrderID, &matchedPrice](OrderID makerID, OrderID takerOrderID, UserID makerUserID, UserID takerUserID, + OrderStatus makerOrderStatus, OrderStatus takerOrderStatus, Decimal matchedQty, + Decimal priceValue) { makerOrderID = makerID; matchedPrice = priceValue; }; @@ -159,7 +160,7 @@ TEST_F(OrderQueueTest, TestOrderQueue_ProcessUsesSnapshotForTradeNotificationAft } }; - auto qtyProcessed = oq->process(tn, pf, 903, Decimal(100, 0)); + auto qtyProcessed = oq->process(tn, pf, 903, 0, Decimal(100, 0)); EXPECT_EQ(qtyProcessed, Decimal(100, 0)); EXPECT_EQ(makerOrderID, 1); diff --git a/test/pricelevel_test.cpp b/test/pricelevel_test.cpp index 40c7e38..556b59c 100644 --- a/test/pricelevel_test.cpp +++ b/test/pricelevel_test.cpp @@ -18,8 +18,8 @@ class PriceLevelTest : public ::testing::Test { TEST_F(PriceLevelTest, TestPriceLevel) { PriceLevel bidLevel(10); - auto o1 = std::make_shared(1, Type::Limit, Side::Buy, Decimal(10, 0), Decimal(10, 0), Flag::None); - auto o2 = std::make_shared(2, Type::Limit, Side::Buy, Decimal(10, 0), Decimal(20, 0), Flag::None); + auto o1 = std::make_shared(1, 0, Type::Limit, Side::Buy, Decimal(10, 0), Decimal(10, 0), Flag::None); + auto o2 = std::make_shared(2, 0, Type::Limit, Side::Buy, Decimal(10, 0), Decimal(20, 0), Flag::None); auto& tree = bidLevel.price_tree(); @@ -68,14 +68,14 @@ TEST_F(PriceLevelTest, TestPriceLevel) { TEST_F(PriceLevelTest, TestPriceFinding) { PriceLevel askLevel(10); - askLevel.append(new Order(1, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(130, 0), Flag::None)); - askLevel.append(new Order(2, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(170, 0), Flag::None)); - askLevel.append(new Order(3, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(100, 0), Flag::None)); - askLevel.append(new Order(4, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(160, 0), Flag::None)); - askLevel.append(new Order(5, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(140, 0), Flag::None)); - askLevel.append(new Order(6, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(120, 0), Flag::None)); - askLevel.append(new Order(7, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(150, 0), Flag::None)); - askLevel.append(new Order(8, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(110, 0), Flag::None)); + askLevel.append(new Order(1, 0, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(130, 0), Flag::None)); + askLevel.append(new Order(2, 0, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(170, 0), Flag::None)); + askLevel.append(new Order(3, 0, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(100, 0), Flag::None)); + askLevel.append(new Order(4, 0, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(160, 0), Flag::None)); + askLevel.append(new Order(5, 0, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(140, 0), Flag::None)); + askLevel.append(new Order(6, 0, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(120, 0), Flag::None)); + askLevel.append(new Order(7, 0, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(150, 0), Flag::None)); + askLevel.append(new Order(8, 0, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(110, 0), Flag::None)); ASSERT_EQ(askLevel.volume(), Decimal(40, 0)); diff --git a/test/util.cpp b/test/util.cpp index 19ff668..4afabf1 100644 --- a/test/util.cpp +++ b/test/util.cpp @@ -16,6 +16,7 @@ using orderbook::OrderID; using orderbook::OrderStatus; using orderbook::Side; using orderbook::Type; +using orderbook::UserID; class Notification : public orderbook::NotificationInterface { public: From a0e70cde60eb6a4a920c3ea8828701421d2f5c46 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 03:46:48 +0000 Subject: [PATCH 2/4] fix: add missing #include to orderbook.hpp Agent-Logs-Url: https://github.com/geseq/cpp-orderbook/sessions/75a9278b-a5dd-4bd9-9aaa-cdb283e81ef8 Co-authored-by: geseq <5458743+geseq@users.noreply.github.com> --- include/orderbook.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/orderbook.hpp b/include/orderbook.hpp index 7a25bd1..e907210 100644 --- a/include/orderbook.hpp +++ b/include/orderbook.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "boost/intrusive/rbtree.hpp" From dc0b080735cf06ce4e3746141fd21e1656d26437 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 03:54:29 +0000 Subject: [PATCH 3/4] fix: replace raw new with unique_ptr in TestPriceFinding, remove before destruction Agent-Logs-Url: https://github.com/geseq/cpp-orderbook/sessions/a22b1f79-c919-4859-a1fe-6f0c63d80392 Co-authored-by: geseq <5458743+geseq@users.noreply.github.com> --- test/pricelevel_test.cpp | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/test/pricelevel_test.cpp b/test/pricelevel_test.cpp index 556b59c..628cab3 100644 --- a/test/pricelevel_test.cpp +++ b/test/pricelevel_test.cpp @@ -68,14 +68,20 @@ TEST_F(PriceLevelTest, TestPriceLevel) { TEST_F(PriceLevelTest, TestPriceFinding) { PriceLevel askLevel(10); - askLevel.append(new Order(1, 0, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(130, 0), Flag::None)); - askLevel.append(new Order(2, 0, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(170, 0), Flag::None)); - askLevel.append(new Order(3, 0, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(100, 0), Flag::None)); - askLevel.append(new Order(4, 0, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(160, 0), Flag::None)); - askLevel.append(new Order(5, 0, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(140, 0), Flag::None)); - askLevel.append(new Order(6, 0, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(120, 0), Flag::None)); - askLevel.append(new Order(7, 0, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(150, 0), Flag::None)); - askLevel.append(new Order(8, 0, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(110, 0), Flag::None)); + std::vector> orders; + auto addOrder = [&](OrderID id, Decimal qty, Decimal price) { + orders.push_back(std::make_unique(id, 0, Type::Limit, Side::Sell, qty, price, Flag::None)); + askLevel.append(orders.back().get()); + }; + + addOrder(1, Decimal(5, 0), Decimal(130, 0)); + addOrder(2, Decimal(5, 0), Decimal(170, 0)); + addOrder(3, Decimal(5, 0), Decimal(100, 0)); + addOrder(4, Decimal(5, 0), Decimal(160, 0)); + addOrder(5, Decimal(5, 0), Decimal(140, 0)); + addOrder(6, Decimal(5, 0), Decimal(120, 0)); + addOrder(7, Decimal(5, 0), Decimal(150, 0)); + addOrder(8, Decimal(5, 0), Decimal(110, 0)); ASSERT_EQ(askLevel.volume(), Decimal(40, 0)); @@ -86,6 +92,11 @@ TEST_F(PriceLevelTest, TestPriceFinding) { ASSERT_EQ(askLevel.smallestGreaterThan(Decimal(169, 0))->price(), Decimal(170, 0)); ASSERT_EQ(askLevel.smallestGreaterThan(Decimal(150, 0))->price(), Decimal(160, 0)); ASSERT_EQ(askLevel.smallestGreaterThan(Decimal(170, 0)), nullptr); + + // Remove orders from the PriceLevel before they are destroyed + for (auto& o : orders) { + askLevel.remove(o.get()); + } } } // namespace test From 842f5441842c3ee6bda3f45290b1a2a2cc73bac4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 04:17:12 +0000 Subject: [PATCH 4/4] Changes before error encountered Agent-Logs-Url: https://github.com/geseq/cpp-orderbook/sessions/5e5b3b79-7a18-4d70-a4e1-6b53b05b9084 Co-authored-by: geseq <5458743+geseq@users.noreply.github.com> --- include/orderqueue.hpp | 1 + src/orderqueue.cpp | 14 +++++++ src/pricelevel.cpp | 27 ++++++++---- test/orderbook_test.cpp | 93 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 9 deletions(-) diff --git a/include/orderqueue.hpp b/include/orderqueue.hpp index bb7c526..2121c6b 100644 --- a/include/orderqueue.hpp +++ b/include/orderqueue.hpp @@ -29,6 +29,7 @@ class OrderQueue : public boost::intrusive::set_base_hook Decimal PriceLevel

::processMarketOrder(const TradeNotification& tn, const PostOrderFill& pf, OrderID takerOrderID, UserID takerUserID, Decimal qty, Flag flag) { static_assert(Q == PriceType::Bid || Q == PriceType::Ask, "Unsupported PriceType"); - if ((flag & (AoN | FoK)) != 0 && qty > volume_) { - // AoN/FoK must match the full requested quantity; return zero processed when aggregate volume is insufficient. - return Decimal{}; + if ((flag & (AoN | FoK)) != 0) { + // AoN/FoK must match the full requested quantity; exclude same-user (STP-skipped) + // quantity so self-liquidity cannot cause the pre-check to falsely pass. + Decimal availableVolume{}; + for (auto it = price_tree_.begin(); it != price_tree_.end(); ++it) { + availableVolume += it->availableQty(takerUserID); + } + if (qty > availableVolume) { + return Decimal{}; + } } auto qtyLeft = qty; @@ -120,8 +127,8 @@ Decimal PriceLevel

::processLimitOrder(const TradeNotification& tn, const Post } } - // AoN/FoK pre-check: only continue when aggregate fillable volume exists at eligible price levels. - // Matched quantity is accounted for incrementally via volume_ -= result in the execution loop below. + // AoN/FoK pre-check: only continue when aggregate STP-aware fillable volume exists at eligible price levels. + // Use availableQty (excludes same-user orders) so that self-liquidity cannot cause a false pass. if (flag & (AoN | FoK)) { if (qty > volume()) { return Decimal{}; @@ -132,20 +139,22 @@ Decimal PriceLevel

::processLimitOrder(const TradeNotification& tn, const Post if constexpr (std::is_same_v) { // Bid tree is descending (best = highest). Accumulate from bids >= sell limit. for (auto it = price_tree_.begin(); it != price_tree_.end() && it->price() >= price; ++it) { - if (aQty <= it->totalQty()) { + auto avail = it->availableQty(takerUserID); + if (aQty <= avail) { canFill = true; break; } - aQty -= it->totalQty(); + aQty -= avail; } } else { // Ask tree is ascending (best = lowest). Accumulate from asks <= buy limit. for (auto it = price_tree_.begin(); it != price_tree_.end() && it->price() <= price; ++it) { - if (aQty <= it->totalQty()) { + auto avail = it->availableQty(takerUserID); + if (aQty <= avail) { canFill = true; break; } - aQty -= it->totalQty(); + aQty -= avail; } } diff --git a/test/orderbook_test.cpp b/test/orderbook_test.cpp index 6e5e76e..119e2ec 100644 --- a/test/orderbook_test.cpp +++ b/test/orderbook_test.cpp @@ -838,6 +838,99 @@ TEST_F(LimitOrderTest, TestSTP_LimitBuy_AllSelfTrade_TakerRests) { ASSERT_TRUE(ob->hasOrder(942)); } +// ────────────────────────────────────────────────────────────────────────────── +// STP + AoN/FoK: pre-check must be STP-aware (exclude same-user qty) +// ────────────────────────────────────────────────────────────────────────────── + +// AoN market sell: only self-liquidity exists – STP-aware pre-check must reject, +// no trades, maker order survives. +TEST_F(LimitOrderTest, TestSTP_AoN_MarketSell_SelfLiquidityOnly_NoFill) { + processLine(ob, "1000\tL\tB\t2\t90\tN\t1"); + n.Reset(); + + // User 1 market AoN sell qty=2; all bids belong to user 1 → must not fill. + processLine(ob, "1001\tM\tS\t2\t0\tA\t1"); + n.Verify({"CreateOrder Accepted 1001 2 2"}); + ASSERT_TRUE(ob->hasOrder(1000)); +} + +// AoN market buy: only self-liquidity exists – must not fill. +TEST_F(LimitOrderTest, TestSTP_AoN_MarketBuy_SelfLiquidityOnly_NoFill) { + processLine(ob, "1010\tL\tS\t2\t100\tN\t1"); + n.Reset(); + + processLine(ob, "1011\tM\tB\t2\t0\tA\t1"); + n.Verify({"CreateOrder Accepted 1011 2 2"}); + ASSERT_TRUE(ob->hasOrder(1010)); +} + +// AoN limit buy: only matching ask is own order – STP-aware pre-check must reject; +// taker rests (AoN limit rests when it cannot fill), maker not consumed. +TEST_F(LimitOrderTest, TestSTP_AoN_LimitBuy_SelfLiquidityOnly_TakerRests) { + processLine(ob, "1020\tL\tS\t2\t100\tN\t1"); + n.Reset(); + + processLine(ob, "1021\tL\tB\t2\t100\tA\t1"); + n.Verify({"CreateOrder Accepted 1021 2 2"}); + ASSERT_TRUE(ob->hasOrder(1021)); + ASSERT_TRUE(ob->hasOrder(1020)); +} + +// FoK limit buy: only self-liquidity at the limit – must be killed (FoK never rests). +TEST_F(LimitOrderTest, TestSTP_FoK_LimitBuy_SelfLiquidityOnly_Killed) { + processLine(ob, "1030\tL\tS\t2\t100\tN\t1"); + n.Reset(); + + processLine(ob, "1031\tL\tB\t2\t100\tF\t1"); + n.Verify({"CreateOrder Accepted 1031 2 2"}); + ASSERT_FALSE(ob->hasOrder(1031)); + ASSERT_TRUE(ob->hasOrder(1030)); +} + +// AoN market buy: non-self ask volume is insufficient (naive check passes: total=self+other >= qty, +// but non-self alone < qty). Without the fix this partially fills from the non-self side. +TEST_F(LimitOrderTest, TestSTP_AoN_MarketBuy_PartialSelfLiquidity_NoFill) { + processLine(ob, "1050\tL\tS\t1\t100\tN\t1"); // user 1, qty=1 at 100 (self) + processLine(ob, "1051\tL\tS\t1\t110\tN\t2"); // user 2, qty=1 at 110 (non-self) + n.Reset(); + + // User 1 AoN buy qty=2: total=2 passes naive check, but non-self=1 < 2 → must not fill. + processLine(ob, "1052\tM\tB\t2\t0\tA\t1"); + n.Verify({"CreateOrder Accepted 1052 2 2"}); + ASSERT_TRUE(ob->hasOrder(1050)); + ASSERT_TRUE(ob->hasOrder(1051)); +} + +// AoN limit buy: self-order and other-order sit at the same price level; non-self qty < requested. +// Without the fix the AoN pre-check uses totalQty (self+other) and passes, then partially fills. +TEST_F(LimitOrderTest, TestSTP_AoN_LimitBuy_PartialSelfLiquidity_NoFill) { + processLine(ob, "1060\tL\tS\t2\t100\tN\t1"); // user 1, qty=2 at 100 (self) + processLine(ob, "1061\tL\tS\t1\t100\tN\t2"); // user 2, qty=1 at 100 (non-self) + n.Reset(); + + // User 1 AoN buy qty=3 at 100: totalQty=3 satisfies pre-check, but non-self=1 < 3 → must not fill. + processLine(ob, "1062\tL\tB\t3\t100\tA\t1"); + n.Verify({"CreateOrder Accepted 1062 3 3"}); + ASSERT_TRUE(ob->hasOrder(1062)); // AoN rests when it cannot fill + ASSERT_TRUE(ob->hasOrder(1060)); + ASSERT_TRUE(ob->hasOrder(1061)); +} + +// FoK limit buy: same-price level has self + non-self; non-self qty < requested. +// Without the fix the FoK pre-check passes then partially executes before being killed. +TEST_F(LimitOrderTest, TestSTP_FoK_LimitBuy_PartialSelfLiquidity_Killed) { + processLine(ob, "1070\tL\tS\t2\t100\tN\t1"); // user 1, qty=2 at 100 (self) + processLine(ob, "1071\tL\tS\t1\t100\tN\t2"); // user 2, qty=1 at 100 (non-self) + n.Reset(); + + // User 1 FoK buy qty=3 at 100: non-self=1 < 3 → must be killed with no trades. + processLine(ob, "1072\tL\tB\t3\t100\tF\t1"); + n.Verify({"CreateOrder Accepted 1072 3 3"}); + ASSERT_FALSE(ob->hasOrder(1072)); // FoK killed + ASSERT_TRUE(ob->hasOrder(1070)); + ASSERT_TRUE(ob->hasOrder(1071)); +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS();