From c2fa72c7fcd0714a3ea973206c465f3258bf5f99 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 15:19:50 +0000 Subject: [PATCH 1/6] Add determinism replay coverage and fix callback capture Agent-Logs-Url: https://github.com/geseq/cpp-orderbook/sessions/39161e46-dfa5-4aa5-bd63-791f5f7f951c Co-authored-by: geseq <5458743+geseq@users.noreply.github.com> --- include/orderbook.hpp | 5 +-- test/orderbook_test.cpp | 90 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/include/orderbook.hpp b/include/orderbook.hpp index 92f1714..1788d34 100644 --- a/include/orderbook.hpp +++ b/include/orderbook.hpp @@ -151,12 +151,11 @@ void OrderBook::addOrder(OrderID id, Type type, Side side, Decimal template void OrderBook::processOrder(OrderID id, Type type, Side side, Decimal qty, Decimal price, Flag flag) { - static const auto tradeNotification = [this](OrderID mOrderID, OrderID tOrderID, OrderStatus mOrderStatus, OrderStatus tOrderStatus, Decimal qty, - Decimal price) { + const auto tradeNotification = [this](OrderID mOrderID, OrderID tOrderID, OrderStatus mOrderStatus, OrderStatus tOrderStatus, Decimal qty, Decimal price) { this->putTradeNotification(mOrderID, tOrderID, mOrderStatus, tOrderStatus, qty, price); this->last_price = price; }; - static const auto postOrderFill = [this](OrderID id) { this->eraseOrder(id); }; + const auto postOrderFill = [this](OrderID id) { this->eraseOrder(id); }; if (type == Type::Market) { if (side == Side::Buy) { diff --git a/test/orderbook_test.cpp b/test/orderbook_test.cpp index d4a8ba7..0a68606 100644 --- a/test/orderbook_test.cpp +++ b/test/orderbook_test.cpp @@ -1,6 +1,8 @@ #include +#include #include +#include #include #include "util.cpp" @@ -752,6 +754,94 @@ TEST_F(LimitOrderTest, TestFoK_MarketBuy_CannotFill) { n.Verify({"CreateOrder Accepted 811 11 11"}); } +TEST_F(LimitOrderTest, TestDeterminism_ReplayProducesSameExecutionTraceAndBookState) { + struct Action { + enum class Kind : uint8_t { Add, Cancel, SetMatching }; + Kind kind; + OrderID id = 0; + Type type = Type::Limit; + Side side = Side::Buy; + Decimal qty = Decimal(0, 0); + Decimal price = Decimal(0, 0); + Flag flag = Flag::None; + bool matching = true; + }; + + const auto replay = [](const std::vector& actions) { + Notification notification; + auto localOb = std::make_shared>(notification); + + for (const auto& action : actions) { + if (action.kind == Action::Kind::SetMatching) { + localOb->setMatching(action.matching); + } 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); + } + } + + return std::tuple{notification.Strings(), localOb->toString(), localOb->last_price}; + }; + + const std::vector actions = { + // Rejections and no-matching mode branches. + {Action::Kind::Add, 1, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(100, 0), Flag::None}, + {Action::Kind::Add, 2, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(90, 0), Flag::None}, + {Action::Kind::Cancel, 2}, + {Action::Kind::Cancel, 2}, + {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, false}, + {Action::Kind::Add, 3, Type::Market, Side::Buy, Decimal(1, 0), Decimal(0, 0), Flag::None}, + {Action::Kind::Add, 4, Type::Limit, Side::Sell, Decimal(1, 0), Decimal(80, 0), Flag::None}, + {Action::Kind::Add, 5, Type::Limit, Side::Buy, Decimal(1, 0), Decimal(70, 0), Flag::None}, + {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, true}, + // Build depth and duplicate/invalid limit branches. + {Action::Kind::Add, 6, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(90, 0), Flag::None}, + {Action::Kind::Add, 6, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(90, 0), Flag::None}, + {Action::Kind::Add, 7, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(100, 0), Flag::None}, + {Action::Kind::Add, 8, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(110, 0), Flag::None}, + {Action::Kind::Add, 9, Type::Limit, Side::Buy, Decimal(1, 0), Decimal(0, 0), Flag::None}, + // Matching paths for all flags and both types. + {Action::Kind::Add, 10, Type::Limit, Side::Buy, Decimal(3, 0), Decimal(100, 0), Flag::None}, + {Action::Kind::Add, 11, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(90, 0), Flag::IoC}, + {Action::Kind::Add, 12, Type::Limit, Side::Buy, Decimal(3, 0), Decimal(100, 0), Flag::AoN}, + {Action::Kind::Add, 13, Type::Limit, Side::Buy, Decimal(4, 0), Decimal(120, 0), Flag::FoK}, + {Action::Kind::Add, 14, Type::Market, Side::Sell, Decimal(3, 0), Decimal(0, 0), Flag::None}, + {Action::Kind::Add, 15, Type::Market, Side::Buy, Decimal(5, 0), Decimal(0, 0), Flag::AoN}, + {Action::Kind::Add, 16, Type::Market, Side::Buy, Decimal(2, 0), Decimal(0, 0), Flag::FoK}, + {Action::Kind::Cancel, 8}, + {Action::Kind::Cancel, 999}, + }; + + const auto [reportsA, bookA, lastPriceA] = replay(actions); + const auto [reportsB, bookB, lastPriceB] = replay(actions); + const auto [reportsC, bookC, lastPriceC] = replay(actions); + + ASSERT_EQ(reportsA, reportsB); + ASSERT_EQ(reportsA, reportsC); + ASSERT_EQ(bookA, bookB); + ASSERT_EQ(bookA, bookC); + ASSERT_EQ(lastPriceA, lastPriceB); + ASSERT_EQ(lastPriceA, lastPriceC); +} + +TEST_F(LimitOrderTest, TestDeterminism_IndependentBooksStayIsolated) { + Notification n1; + Notification n2; + 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); + + 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); + + 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"}); + ASSERT_EQ(ob1->toString(), ob2->toString()); +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); From 049faae97f571696be46ad57b1d3266534fdb6b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 15:23:42 +0000 Subject: [PATCH 2/6] Strengthen determinism assertions in replay test Agent-Logs-Url: https://github.com/geseq/cpp-orderbook/sessions/39161e46-dfa5-4aa5-bd63-791f5f7f951c Co-authored-by: geseq <5458743+geseq@users.noreply.github.com> --- test/orderbook_test.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/orderbook_test.cpp b/test/orderbook_test.cpp index 0a68606..73392c3 100644 --- a/test/orderbook_test.cpp +++ b/test/orderbook_test.cpp @@ -817,8 +817,20 @@ TEST_F(LimitOrderTest, TestDeterminism_ReplayProducesSameExecutionTraceAndBookSt const auto [reportsB, bookB, lastPriceB] = replay(actions); const auto [reportsC, bookC, lastPriceC] = replay(actions); + const auto containsReport = [](const std::vector& reports, const std::string& expected) { + for (const auto& report : reports) { + if (report == expected) { + return true; + } + } + return false; + }; + ASSERT_EQ(reportsA, reportsB); ASSERT_EQ(reportsA, reportsC); + ASSERT_TRUE(containsReport(reportsA, "CreateOrder Rejected 1 0 0 ErrInvalidQty")); + ASSERT_TRUE(containsReport(reportsA, "CancelOrder Canceled 2 2 2")); + ASSERT_TRUE(containsReport(reportsA, "CancelOrder Rejected 2 0 0 ErrOrderNotExists")); ASSERT_EQ(bookA, bookB); ASSERT_EQ(bookA, bookC); ASSERT_EQ(lastPriceA, lastPriceB); From c84157ae8a5252521691dbc25858f9689b4db177 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 15:46:27 +0000 Subject: [PATCH 3/6] Fix cross-instance callback binding and expand determinism coverage for interleaved/midpoint replay paths Agent-Logs-Url: https://github.com/geseq/cpp-orderbook/sessions/6250214d-2e8b-4074-a54f-6f5e322cfefe Co-authored-by: geseq <5458743+geseq@users.noreply.github.com> --- test/orderbook_test.cpp | 175 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/test/orderbook_test.cpp b/test/orderbook_test.cpp index 73392c3..f47fc49 100644 --- a/test/orderbook_test.cpp +++ b/test/orderbook_test.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -854,6 +855,180 @@ TEST_F(LimitOrderTest, TestDeterminism_IndependentBooksStayIsolated) { ASSERT_EQ(ob1->toString(), ob2->toString()); } +TEST_F(LimitOrderTest, TestDeterminism_TwoBooksInterleavedMatchStandaloneRuns) { + struct Action { + enum class Kind : uint8_t { Add, Cancel, SetMatching }; + Kind kind; + OrderID id = 0; + Type type = Type::Limit; + Side side = Side::Buy; + Decimal qty = Decimal(0, 0); + Decimal price = Decimal(0, 0); + Flag flag = Flag::None; + bool matching = true; + }; + + const auto applyAction = [](const Action& action, const std::shared_ptr>& localOb) { + if (action.kind == Action::Kind::SetMatching) { + localOb->setMatching(action.matching); + } 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); + } + }; + + const std::vector actionsA = { + {Action::Kind::Add, 1, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(100, 0), Flag::None}, + {Action::Kind::Add, 2, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(90, 0), Flag::None}, + {Action::Kind::Cancel, 2}, + {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, false}, + {Action::Kind::Add, 3, Type::Market, Side::Buy, Decimal(1, 0), Decimal(0, 0), Flag::None}, + {Action::Kind::Add, 4, Type::Limit, Side::Sell, Decimal(1, 0), Decimal(80, 0), Flag::None}, + {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, true}, + {Action::Kind::Add, 5, Type::Limit, Side::Buy, Decimal(3, 0), Decimal(100, 0), Flag::None}, + {Action::Kind::Add, 6, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(90, 0), Flag::IoC}, + {Action::Kind::Add, 7, Type::Limit, Side::Buy, Decimal(4, 0), Decimal(120, 0), Flag::FoK}, + {Action::Kind::Add, 8, Type::Market, Side::Sell, Decimal(3, 0), Decimal(0, 0), Flag::AoN}, + {Action::Kind::Cancel, 999}, + }; + + const std::vector actionsB = { + {Action::Kind::Add, 101, Type::Limit, Side::Sell, Decimal(0, 0), Decimal(100, 0), Flag::None}, + {Action::Kind::Add, 102, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(110, 0), Flag::None}, + {Action::Kind::Cancel, 102}, + {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, false}, + {Action::Kind::Add, 103, Type::Market, Side::Sell, Decimal(1, 0), Decimal(0, 0), Flag::None}, + {Action::Kind::Add, 104, Type::Limit, Side::Buy, Decimal(1, 0), Decimal(120, 0), Flag::None}, + {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, true}, + {Action::Kind::Add, 105, Type::Limit, Side::Sell, Decimal(3, 0), Decimal(100, 0), Flag::None}, + {Action::Kind::Add, 106, Type::Limit, Side::Buy, Decimal(5, 0), Decimal(110, 0), Flag::IoC}, + {Action::Kind::Add, 107, Type::Limit, Side::Sell, Decimal(4, 0), Decimal(80, 0), Flag::FoK}, + {Action::Kind::Add, 108, Type::Market, Side::Buy, Decimal(3, 0), Decimal(0, 0), Flag::AoN}, + {Action::Kind::Cancel, 1999}, + }; + + const auto runSequence = [&applyAction](const std::vector& actions) { + Notification notification; + auto localOb = std::make_shared>(notification); + for (const auto& action : actions) { + applyAction(action, localOb); + } + return std::tuple{notification.Strings(), localOb->toString(), localOb->last_price}; + }; + + const auto [standaloneReportsA, standaloneBookA, standaloneLastPriceA] = runSequence(actionsA); + const auto [standaloneReportsB, standaloneBookB, standaloneLastPriceB] = runSequence(actionsB); + + Notification interleavedN1; + Notification interleavedN2; + auto interleavedOb1 = std::make_shared>(interleavedN1); + auto interleavedOb2 = std::make_shared>(interleavedN2); + + const size_t maxActions = std::max(actionsA.size(), actionsB.size()); + for (size_t i = 0; i < maxActions; ++i) { + if (i < actionsA.size()) { + applyAction(actionsA[i], interleavedOb1); + } + if (i < actionsB.size()) { + applyAction(actionsB[i], interleavedOb2); + } + } + + ASSERT_EQ(interleavedN1.Strings(), standaloneReportsA); + ASSERT_EQ(interleavedN2.Strings(), standaloneReportsB); + ASSERT_EQ(interleavedOb1->toString(), standaloneBookA); + ASSERT_EQ(interleavedOb2->toString(), standaloneBookB); + ASSERT_EQ(interleavedOb1->last_price, standaloneLastPriceA); + ASSERT_EQ(interleavedOb2->last_price, standaloneLastPriceB); +} + +TEST_F(LimitOrderTest, TestDeterminism_RecoverAtEveryMidpointThenReplaySuffix) { + struct Action { + enum class Kind : uint8_t { Add, Cancel, SetMatching }; + Kind kind; + OrderID id = 0; + Type type = Type::Limit; + Side side = Side::Buy; + Decimal qty = Decimal(0, 0); + Decimal price = Decimal(0, 0); + Flag flag = Flag::None; + bool matching = true; + }; + + const auto applyAction = [](const Action& action, const std::shared_ptr>& localOb) { + if (action.kind == Action::Kind::SetMatching) { + localOb->setMatching(action.matching); + } 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); + } + }; + + const std::vector actions = { + {Action::Kind::Add, 1, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(100, 0), Flag::None}, + {Action::Kind::Add, 2, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(90, 0), Flag::None}, + {Action::Kind::Cancel, 2}, + {Action::Kind::Cancel, 2}, + {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, false}, + {Action::Kind::Add, 3, Type::Market, Side::Buy, Decimal(1, 0), Decimal(0, 0), Flag::None}, + {Action::Kind::Add, 4, Type::Limit, Side::Sell, Decimal(1, 0), Decimal(80, 0), Flag::None}, + {Action::Kind::Add, 5, Type::Limit, Side::Buy, Decimal(1, 0), Decimal(70, 0), Flag::None}, + {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, true}, + {Action::Kind::Add, 6, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(90, 0), Flag::None}, + {Action::Kind::Add, 6, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(90, 0), Flag::None}, + {Action::Kind::Add, 7, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(100, 0), Flag::None}, + {Action::Kind::Add, 8, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(110, 0), Flag::None}, + {Action::Kind::Add, 9, Type::Limit, Side::Buy, Decimal(1, 0), Decimal(0, 0), Flag::None}, + {Action::Kind::Add, 10, Type::Limit, Side::Buy, Decimal(3, 0), Decimal(100, 0), Flag::None}, + {Action::Kind::Add, 11, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(90, 0), Flag::IoC}, + {Action::Kind::Add, 12, Type::Limit, Side::Buy, Decimal(3, 0), Decimal(100, 0), Flag::AoN}, + {Action::Kind::Add, 13, Type::Limit, Side::Buy, Decimal(4, 0), Decimal(120, 0), Flag::FoK}, + {Action::Kind::Add, 14, Type::Market, Side::Sell, Decimal(3, 0), Decimal(0, 0), Flag::None}, + {Action::Kind::Add, 15, Type::Market, Side::Buy, Decimal(5, 0), Decimal(0, 0), Flag::AoN}, + {Action::Kind::Add, 16, Type::Market, Side::Buy, Decimal(2, 0), Decimal(0, 0), Flag::FoK}, + {Action::Kind::Cancel, 8}, + {Action::Kind::Cancel, 999}, + }; + + for (size_t split = 0; split <= actions.size(); ++split) { + Notification baselineN; + auto baselineOb = std::make_shared>(baselineN); + for (size_t i = 0; i < split; ++i) { + applyAction(actions[i], baselineOb); + } + const auto snapshotBookState = baselineOb->toString(); + const auto snapshotLastPrice = baselineOb->last_price; + + baselineN.Reset(); + for (size_t i = split; i < actions.size(); ++i) { + applyAction(actions[i], baselineOb); + } + const auto baselineSuffixReports = baselineN.Strings(); + const auto baselineFinalBook = baselineOb->toString(); + const auto baselineFinalLastPrice = baselineOb->last_price; + + Notification recoveredN; + auto recoveredOb = std::make_shared>(recoveredN); + for (size_t i = 0; i < split; ++i) { + applyAction(actions[i], recoveredOb); + } + + ASSERT_EQ(recoveredOb->toString(), snapshotBookState) << "split=" << split; + ASSERT_EQ(recoveredOb->last_price, snapshotLastPrice) << "split=" << split; + + recoveredN.Reset(); + for (size_t i = split; i < actions.size(); ++i) { + applyAction(actions[i], recoveredOb); + } + + ASSERT_EQ(recoveredN.Strings(), baselineSuffixReports) << "split=" << split; + ASSERT_EQ(recoveredOb->toString(), baselineFinalBook) << "split=" << split; + ASSERT_EQ(recoveredOb->last_price, baselineFinalLastPrice) << "split=" << split; + } +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); From f1b98d57b3f1c33c6e114e4daf68ce0363383908 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 15:50:57 +0000 Subject: [PATCH 4/6] Add separate determinism test suite with four-book market-pair coverage Agent-Logs-Url: https://github.com/geseq/cpp-orderbook/sessions/e32f7d1c-6fe3-42f9-b4b8-fae6bcce69e3 Co-authored-by: geseq <5458743+geseq@users.noreply.github.com> --- test/determinism_test.cpp | 263 ++++++++++++++++++++++++++++++++++++++ test/orderbook_test.cpp | 258 ------------------------------------- 2 files changed, 263 insertions(+), 258 deletions(-) create mode 100644 test/determinism_test.cpp diff --git a/test/determinism_test.cpp b/test/determinism_test.cpp new file mode 100644 index 0000000..627f202 --- /dev/null +++ b/test/determinism_test.cpp @@ -0,0 +1,263 @@ +#include + +#include +#include +#include +#include + +#include "util.cpp" + +class DeterminismTest : public ::testing::Test { + protected: + struct Action { + enum class Kind : uint8_t { Add, Cancel, SetMatching }; + Kind kind; + OrderID id = 0; + Type type = Type::Limit; + Side side = Side::Buy; + Decimal qty = Decimal(0, 0); + Decimal price = Decimal(0, 0); + Flag flag = Flag::None; + bool matching = true; + }; + + static void applyAction(const Action& action, const std::shared_ptr>& localOb) { + if (action.kind == Action::Kind::SetMatching) { + localOb->setMatching(action.matching); + } 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); + } + } + + static std::tuple, std::string, Decimal> runSequence(const std::vector& actions) { + Notification notification; + auto localOb = std::make_shared>(notification); + for (const auto& action : actions) { + applyAction(action, localOb); + } + return std::tuple{notification.Strings(), localOb->toString(), localOb->last_price}; + } + + static bool hasExactReport(const std::vector& reports, const std::string& expected) { + return std::find(reports.begin(), reports.end(), expected) != reports.end(); + } + + static bool hasReportContaining(const std::vector& reports, const std::string& token) { + for (const auto& report : reports) { + if (report.find(token) != std::string::npos) { + return true; + } + } + return false; + } + + static bool hasActionKind(const std::vector& actions, Action::Kind expected) { + return std::any_of(actions.begin(), actions.end(), [expected](const Action& action) { return action.kind == expected; }); + } + + static bool hasType(const std::vector& actions, Type expected) { + return std::any_of(actions.begin(), actions.end(), [expected](const Action& action) { return action.kind == Action::Kind::Add && action.type == expected; }); + } + + static bool hasSide(const std::vector& actions, Side expected) { + return std::any_of(actions.begin(), actions.end(), [expected](const Action& action) { return action.kind == Action::Kind::Add && action.side == expected; }); + } + + static bool hasFlag(const std::vector& actions, Flag expected) { + return std::any_of(actions.begin(), actions.end(), [expected](const Action& action) { return action.kind == Action::Kind::Add && action.flag == expected; }); + } + + static std::vector marketAActions() { + return { + {Action::Kind::Add, 1, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(100, 0), Flag::None}, + {Action::Kind::Add, 2, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(90, 0), Flag::None}, + {Action::Kind::Cancel, 2}, + {Action::Kind::Cancel, 2}, + {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, false}, + {Action::Kind::Add, 3, Type::Market, Side::Buy, Decimal(1, 0), Decimal(0, 0), Flag::None}, + {Action::Kind::Add, 4, Type::Limit, Side::Sell, Decimal(1, 0), Decimal(80, 0), Flag::None}, + {Action::Kind::Add, 5, Type::Limit, Side::Buy, Decimal(1, 0), Decimal(70, 0), Flag::None}, + {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, true}, + {Action::Kind::Add, 6, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(90, 0), Flag::None}, + {Action::Kind::Add, 6, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(90, 0), Flag::None}, + {Action::Kind::Add, 7, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(100, 0), Flag::None}, + {Action::Kind::Add, 8, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(110, 0), Flag::None}, + {Action::Kind::Add, 9, Type::Limit, Side::Buy, Decimal(1, 0), Decimal(0, 0), Flag::None}, + {Action::Kind::Add, 10, Type::Limit, Side::Buy, Decimal(3, 0), Decimal(100, 0), Flag::None}, + {Action::Kind::Add, 11, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(90, 0), Flag::IoC}, + {Action::Kind::Add, 12, Type::Limit, Side::Buy, Decimal(3, 0), Decimal(100, 0), Flag::AoN}, + {Action::Kind::Add, 13, Type::Limit, Side::Buy, Decimal(4, 0), Decimal(120, 0), Flag::FoK}, + {Action::Kind::Add, 14, Type::Market, Side::Sell, Decimal(3, 0), Decimal(0, 0), Flag::None}, + {Action::Kind::Add, 15, Type::Market, Side::Buy, Decimal(5, 0), Decimal(0, 0), Flag::AoN}, + {Action::Kind::Add, 16, Type::Market, Side::Buy, Decimal(2, 0), Decimal(0, 0), Flag::FoK}, + {Action::Kind::Cancel, 8}, + {Action::Kind::Cancel, 999}, + }; + } + + static std::vector marketBActions() { + return { + {Action::Kind::Add, 1001, Type::Limit, Side::Sell, Decimal(0, 0), Decimal(100, 0), Flag::None}, + {Action::Kind::Add, 1002, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(110, 0), Flag::None}, + {Action::Kind::Cancel, 1002}, + {Action::Kind::Cancel, 1002}, + {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, false}, + {Action::Kind::Add, 1003, Type::Market, Side::Sell, Decimal(1, 0), Decimal(0, 0), Flag::None}, + {Action::Kind::Add, 1004, Type::Limit, Side::Buy, Decimal(1, 0), Decimal(120, 0), Flag::None}, + {Action::Kind::Add, 1005, Type::Limit, Side::Sell, Decimal(1, 0), Decimal(130, 0), Flag::None}, + {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, true}, + {Action::Kind::Add, 1006, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(110, 0), Flag::None}, + {Action::Kind::Add, 1006, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(110, 0), Flag::None}, + {Action::Kind::Add, 1007, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(100, 0), Flag::None}, + {Action::Kind::Add, 1008, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(90, 0), Flag::None}, + {Action::Kind::Add, 1009, Type::Limit, Side::Sell, Decimal(1, 0), Decimal(0, 0), Flag::None}, + {Action::Kind::Add, 1010, Type::Limit, Side::Sell, Decimal(3, 0), Decimal(100, 0), Flag::None}, + {Action::Kind::Add, 1011, Type::Limit, Side::Buy, Decimal(5, 0), Decimal(110, 0), Flag::IoC}, + {Action::Kind::Add, 1012, Type::Limit, Side::Sell, Decimal(3, 0), Decimal(100, 0), Flag::AoN}, + {Action::Kind::Add, 1013, Type::Limit, Side::Sell, Decimal(4, 0), Decimal(80, 0), Flag::FoK}, + {Action::Kind::Add, 1014, Type::Market, Side::Buy, Decimal(3, 0), Decimal(0, 0), Flag::None}, + {Action::Kind::Add, 1015, Type::Market, Side::Sell, Decimal(5, 0), Decimal(0, 0), Flag::AoN}, + {Action::Kind::Add, 1016, Type::Market, Side::Sell, Decimal(2, 0), Decimal(0, 0), Flag::FoK}, + {Action::Kind::Cancel, 1008}, + {Action::Kind::Cancel, 1999}, + }; + } +}; + +TEST_F(DeterminismTest, ReplayProducesSameExecutionTraceAndBookState) { + const auto actions = marketAActions(); + const auto [reportsA, bookA, lastPriceA] = runSequence(actions); + const auto [reportsB, bookB, lastPriceB] = runSequence(actions); + const auto [reportsC, bookC, lastPriceC] = runSequence(actions); + + ASSERT_EQ(reportsA, reportsB); + ASSERT_EQ(reportsA, reportsC); + ASSERT_TRUE(hasExactReport(reportsA, "CreateOrder Rejected 1 0 0 ErrInvalidQty")); + ASSERT_TRUE(hasExactReport(reportsA, "CancelOrder Canceled 2 2 2")); + ASSERT_TRUE(hasExactReport(reportsA, "CancelOrder Rejected 2 0 0 ErrOrderNotExists")); + ASSERT_EQ(bookA, bookB); + ASSERT_EQ(bookA, bookC); + ASSERT_EQ(lastPriceA, lastPriceB); + ASSERT_EQ(lastPriceA, lastPriceC); +} + +TEST_F(DeterminismTest, RecoverAtEveryMidpointThenReplaySuffix) { + const auto actions = marketAActions(); + + for (size_t split = 0; split <= actions.size(); ++split) { + Notification baselineN; + auto baselineOb = std::make_shared>(baselineN); + for (size_t i = 0; i < split; ++i) { + applyAction(actions[i], baselineOb); + } + const auto snapshotBookState = baselineOb->toString(); + const auto snapshotLastPrice = baselineOb->last_price; + + baselineN.Reset(); + for (size_t i = split; i < actions.size(); ++i) { + applyAction(actions[i], baselineOb); + } + const auto baselineSuffixReports = baselineN.Strings(); + const auto baselineFinalBook = baselineOb->toString(); + const auto baselineFinalLastPrice = baselineOb->last_price; + + Notification recoveredN; + auto recoveredOb = std::make_shared>(recoveredN); + for (size_t i = 0; i < split; ++i) { + applyAction(actions[i], recoveredOb); + } + + ASSERT_EQ(recoveredOb->toString(), snapshotBookState) << "split=" << split; + ASSERT_EQ(recoveredOb->last_price, snapshotLastPrice) << "split=" << split; + + recoveredN.Reset(); + for (size_t i = split; i < actions.size(); ++i) { + applyAction(actions[i], recoveredOb); + } + + ASSERT_EQ(recoveredN.Strings(), baselineSuffixReports) << "split=" << split; + ASSERT_EQ(recoveredOb->toString(), baselineFinalBook) << "split=" << split; + ASSERT_EQ(recoveredOb->last_price, baselineFinalLastPrice) << "split=" << split; + } +} + +TEST_F(DeterminismTest, TwoCopiesPerMarketRemainDeterministicWithDifferentInterleavedMarkets) { + const auto marketA = marketAActions(); + const auto marketB = marketBActions(); + + ASSERT_TRUE(hasActionKind(marketA, Action::Kind::Cancel)); + ASSERT_TRUE(hasActionKind(marketA, Action::Kind::SetMatching)); + ASSERT_TRUE(hasActionKind(marketB, Action::Kind::Cancel)); + ASSERT_TRUE(hasActionKind(marketB, Action::Kind::SetMatching)); + ASSERT_TRUE(hasType(marketA, Type::Limit)); + ASSERT_TRUE(hasType(marketA, Type::Market)); + ASSERT_TRUE(hasType(marketB, Type::Limit)); + ASSERT_TRUE(hasType(marketB, Type::Market)); + ASSERT_TRUE(hasSide(marketA, Side::Buy)); + ASSERT_TRUE(hasSide(marketA, Side::Sell)); + ASSERT_TRUE(hasSide(marketB, Side::Buy)); + ASSERT_TRUE(hasSide(marketB, Side::Sell)); + ASSERT_TRUE(hasFlag(marketA, Flag::None)); + ASSERT_TRUE(hasFlag(marketA, Flag::IoC)); + ASSERT_TRUE(hasFlag(marketA, Flag::AoN)); + ASSERT_TRUE(hasFlag(marketA, Flag::FoK)); + ASSERT_TRUE(hasFlag(marketB, Flag::None)); + ASSERT_TRUE(hasFlag(marketB, Flag::IoC)); + ASSERT_TRUE(hasFlag(marketB, Flag::AoN)); + ASSERT_TRUE(hasFlag(marketB, Flag::FoK)); + + const auto [standaloneReportsA, standaloneBookA, standaloneLastPriceA] = runSequence(marketA); + const auto [standaloneReportsB, standaloneBookB, standaloneLastPriceB] = runSequence(marketB); + + Notification nA1; + Notification nA2; + Notification nB1; + Notification nB2; + auto obA1 = std::make_shared>(nA1); + auto obA2 = std::make_shared>(nA2); + auto obB1 = std::make_shared>(nB1); + auto obB2 = std::make_shared>(nB2); + + const size_t maxActions = std::max(marketA.size(), marketB.size()); + for (size_t i = 0; i < maxActions; ++i) { + if (i < marketA.size()) { + applyAction(marketA[i], obA1); + applyAction(marketA[i], obA2); + } + if (i < marketB.size()) { + applyAction(marketB[i], obB1); + applyAction(marketB[i], obB2); + } + } + + ASSERT_EQ(nA1.Strings(), nA2.Strings()); + ASSERT_EQ(obA1->toString(), obA2->toString()); + ASSERT_EQ(obA1->last_price, obA2->last_price); + + ASSERT_EQ(nB1.Strings(), nB2.Strings()); + ASSERT_EQ(obB1->toString(), obB2->toString()); + ASSERT_EQ(obB1->last_price, obB2->last_price); + + ASSERT_EQ(nA1.Strings(), standaloneReportsA); + ASSERT_EQ(obA1->toString(), standaloneBookA); + ASSERT_EQ(obA1->last_price, standaloneLastPriceA); + ASSERT_EQ(nB1.Strings(), standaloneReportsB); + ASSERT_EQ(obB1->toString(), standaloneBookB); + ASSERT_EQ(obB1->last_price, standaloneLastPriceB); + + ASSERT_TRUE(hasReportContaining(nA1.Strings(), "Filled")); + ASSERT_TRUE(hasReportContaining(nA1.Strings(), "CreateOrder Rejected")); + ASSERT_TRUE(hasReportContaining(nA1.Strings(), "CancelOrder Canceled")); + ASSERT_TRUE(hasReportContaining(nA1.Strings(), "CancelOrder Rejected")); + ASSERT_TRUE(hasReportContaining(nB1.Strings(), "Filled")); + ASSERT_TRUE(hasReportContaining(nB1.Strings(), "CreateOrder Rejected")); + ASSERT_TRUE(hasReportContaining(nB1.Strings(), "CancelOrder Canceled")); + ASSERT_TRUE(hasReportContaining(nB1.Strings(), "CancelOrder Rejected")); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/orderbook_test.cpp b/test/orderbook_test.cpp index f47fc49..eac397e 100644 --- a/test/orderbook_test.cpp +++ b/test/orderbook_test.cpp @@ -1,7 +1,6 @@ #include #include -#include #include #include #include @@ -755,89 +754,6 @@ TEST_F(LimitOrderTest, TestFoK_MarketBuy_CannotFill) { n.Verify({"CreateOrder Accepted 811 11 11"}); } -TEST_F(LimitOrderTest, TestDeterminism_ReplayProducesSameExecutionTraceAndBookState) { - struct Action { - enum class Kind : uint8_t { Add, Cancel, SetMatching }; - Kind kind; - OrderID id = 0; - Type type = Type::Limit; - Side side = Side::Buy; - Decimal qty = Decimal(0, 0); - Decimal price = Decimal(0, 0); - Flag flag = Flag::None; - bool matching = true; - }; - - const auto replay = [](const std::vector& actions) { - Notification notification; - auto localOb = std::make_shared>(notification); - - for (const auto& action : actions) { - if (action.kind == Action::Kind::SetMatching) { - localOb->setMatching(action.matching); - } 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); - } - } - - return std::tuple{notification.Strings(), localOb->toString(), localOb->last_price}; - }; - - const std::vector actions = { - // Rejections and no-matching mode branches. - {Action::Kind::Add, 1, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(100, 0), Flag::None}, - {Action::Kind::Add, 2, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(90, 0), Flag::None}, - {Action::Kind::Cancel, 2}, - {Action::Kind::Cancel, 2}, - {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, false}, - {Action::Kind::Add, 3, Type::Market, Side::Buy, Decimal(1, 0), Decimal(0, 0), Flag::None}, - {Action::Kind::Add, 4, Type::Limit, Side::Sell, Decimal(1, 0), Decimal(80, 0), Flag::None}, - {Action::Kind::Add, 5, Type::Limit, Side::Buy, Decimal(1, 0), Decimal(70, 0), Flag::None}, - {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, true}, - // Build depth and duplicate/invalid limit branches. - {Action::Kind::Add, 6, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(90, 0), Flag::None}, - {Action::Kind::Add, 6, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(90, 0), Flag::None}, - {Action::Kind::Add, 7, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(100, 0), Flag::None}, - {Action::Kind::Add, 8, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(110, 0), Flag::None}, - {Action::Kind::Add, 9, Type::Limit, Side::Buy, Decimal(1, 0), Decimal(0, 0), Flag::None}, - // Matching paths for all flags and both types. - {Action::Kind::Add, 10, Type::Limit, Side::Buy, Decimal(3, 0), Decimal(100, 0), Flag::None}, - {Action::Kind::Add, 11, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(90, 0), Flag::IoC}, - {Action::Kind::Add, 12, Type::Limit, Side::Buy, Decimal(3, 0), Decimal(100, 0), Flag::AoN}, - {Action::Kind::Add, 13, Type::Limit, Side::Buy, Decimal(4, 0), Decimal(120, 0), Flag::FoK}, - {Action::Kind::Add, 14, Type::Market, Side::Sell, Decimal(3, 0), Decimal(0, 0), Flag::None}, - {Action::Kind::Add, 15, Type::Market, Side::Buy, Decimal(5, 0), Decimal(0, 0), Flag::AoN}, - {Action::Kind::Add, 16, Type::Market, Side::Buy, Decimal(2, 0), Decimal(0, 0), Flag::FoK}, - {Action::Kind::Cancel, 8}, - {Action::Kind::Cancel, 999}, - }; - - const auto [reportsA, bookA, lastPriceA] = replay(actions); - const auto [reportsB, bookB, lastPriceB] = replay(actions); - const auto [reportsC, bookC, lastPriceC] = replay(actions); - - const auto containsReport = [](const std::vector& reports, const std::string& expected) { - for (const auto& report : reports) { - if (report == expected) { - return true; - } - } - return false; - }; - - ASSERT_EQ(reportsA, reportsB); - ASSERT_EQ(reportsA, reportsC); - ASSERT_TRUE(containsReport(reportsA, "CreateOrder Rejected 1 0 0 ErrInvalidQty")); - ASSERT_TRUE(containsReport(reportsA, "CancelOrder Canceled 2 2 2")); - ASSERT_TRUE(containsReport(reportsA, "CancelOrder Rejected 2 0 0 ErrOrderNotExists")); - ASSERT_EQ(bookA, bookB); - ASSERT_EQ(bookA, bookC); - ASSERT_EQ(lastPriceA, lastPriceB); - ASSERT_EQ(lastPriceA, lastPriceC); -} - TEST_F(LimitOrderTest, TestDeterminism_IndependentBooksStayIsolated) { Notification n1; Notification n2; @@ -855,180 +771,6 @@ TEST_F(LimitOrderTest, TestDeterminism_IndependentBooksStayIsolated) { ASSERT_EQ(ob1->toString(), ob2->toString()); } -TEST_F(LimitOrderTest, TestDeterminism_TwoBooksInterleavedMatchStandaloneRuns) { - struct Action { - enum class Kind : uint8_t { Add, Cancel, SetMatching }; - Kind kind; - OrderID id = 0; - Type type = Type::Limit; - Side side = Side::Buy; - Decimal qty = Decimal(0, 0); - Decimal price = Decimal(0, 0); - Flag flag = Flag::None; - bool matching = true; - }; - - const auto applyAction = [](const Action& action, const std::shared_ptr>& localOb) { - if (action.kind == Action::Kind::SetMatching) { - localOb->setMatching(action.matching); - } 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); - } - }; - - const std::vector actionsA = { - {Action::Kind::Add, 1, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(100, 0), Flag::None}, - {Action::Kind::Add, 2, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(90, 0), Flag::None}, - {Action::Kind::Cancel, 2}, - {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, false}, - {Action::Kind::Add, 3, Type::Market, Side::Buy, Decimal(1, 0), Decimal(0, 0), Flag::None}, - {Action::Kind::Add, 4, Type::Limit, Side::Sell, Decimal(1, 0), Decimal(80, 0), Flag::None}, - {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, true}, - {Action::Kind::Add, 5, Type::Limit, Side::Buy, Decimal(3, 0), Decimal(100, 0), Flag::None}, - {Action::Kind::Add, 6, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(90, 0), Flag::IoC}, - {Action::Kind::Add, 7, Type::Limit, Side::Buy, Decimal(4, 0), Decimal(120, 0), Flag::FoK}, - {Action::Kind::Add, 8, Type::Market, Side::Sell, Decimal(3, 0), Decimal(0, 0), Flag::AoN}, - {Action::Kind::Cancel, 999}, - }; - - const std::vector actionsB = { - {Action::Kind::Add, 101, Type::Limit, Side::Sell, Decimal(0, 0), Decimal(100, 0), Flag::None}, - {Action::Kind::Add, 102, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(110, 0), Flag::None}, - {Action::Kind::Cancel, 102}, - {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, false}, - {Action::Kind::Add, 103, Type::Market, Side::Sell, Decimal(1, 0), Decimal(0, 0), Flag::None}, - {Action::Kind::Add, 104, Type::Limit, Side::Buy, Decimal(1, 0), Decimal(120, 0), Flag::None}, - {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, true}, - {Action::Kind::Add, 105, Type::Limit, Side::Sell, Decimal(3, 0), Decimal(100, 0), Flag::None}, - {Action::Kind::Add, 106, Type::Limit, Side::Buy, Decimal(5, 0), Decimal(110, 0), Flag::IoC}, - {Action::Kind::Add, 107, Type::Limit, Side::Sell, Decimal(4, 0), Decimal(80, 0), Flag::FoK}, - {Action::Kind::Add, 108, Type::Market, Side::Buy, Decimal(3, 0), Decimal(0, 0), Flag::AoN}, - {Action::Kind::Cancel, 1999}, - }; - - const auto runSequence = [&applyAction](const std::vector& actions) { - Notification notification; - auto localOb = std::make_shared>(notification); - for (const auto& action : actions) { - applyAction(action, localOb); - } - return std::tuple{notification.Strings(), localOb->toString(), localOb->last_price}; - }; - - const auto [standaloneReportsA, standaloneBookA, standaloneLastPriceA] = runSequence(actionsA); - const auto [standaloneReportsB, standaloneBookB, standaloneLastPriceB] = runSequence(actionsB); - - Notification interleavedN1; - Notification interleavedN2; - auto interleavedOb1 = std::make_shared>(interleavedN1); - auto interleavedOb2 = std::make_shared>(interleavedN2); - - const size_t maxActions = std::max(actionsA.size(), actionsB.size()); - for (size_t i = 0; i < maxActions; ++i) { - if (i < actionsA.size()) { - applyAction(actionsA[i], interleavedOb1); - } - if (i < actionsB.size()) { - applyAction(actionsB[i], interleavedOb2); - } - } - - ASSERT_EQ(interleavedN1.Strings(), standaloneReportsA); - ASSERT_EQ(interleavedN2.Strings(), standaloneReportsB); - ASSERT_EQ(interleavedOb1->toString(), standaloneBookA); - ASSERT_EQ(interleavedOb2->toString(), standaloneBookB); - ASSERT_EQ(interleavedOb1->last_price, standaloneLastPriceA); - ASSERT_EQ(interleavedOb2->last_price, standaloneLastPriceB); -} - -TEST_F(LimitOrderTest, TestDeterminism_RecoverAtEveryMidpointThenReplaySuffix) { - struct Action { - enum class Kind : uint8_t { Add, Cancel, SetMatching }; - Kind kind; - OrderID id = 0; - Type type = Type::Limit; - Side side = Side::Buy; - Decimal qty = Decimal(0, 0); - Decimal price = Decimal(0, 0); - Flag flag = Flag::None; - bool matching = true; - }; - - const auto applyAction = [](const Action& action, const std::shared_ptr>& localOb) { - if (action.kind == Action::Kind::SetMatching) { - localOb->setMatching(action.matching); - } 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); - } - }; - - const std::vector actions = { - {Action::Kind::Add, 1, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(100, 0), Flag::None}, - {Action::Kind::Add, 2, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(90, 0), Flag::None}, - {Action::Kind::Cancel, 2}, - {Action::Kind::Cancel, 2}, - {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, false}, - {Action::Kind::Add, 3, Type::Market, Side::Buy, Decimal(1, 0), Decimal(0, 0), Flag::None}, - {Action::Kind::Add, 4, Type::Limit, Side::Sell, Decimal(1, 0), Decimal(80, 0), Flag::None}, - {Action::Kind::Add, 5, Type::Limit, Side::Buy, Decimal(1, 0), Decimal(70, 0), Flag::None}, - {Action::Kind::SetMatching, 0, Type::Limit, Side::Buy, Decimal(0, 0), Decimal(0, 0), Flag::None, true}, - {Action::Kind::Add, 6, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(90, 0), Flag::None}, - {Action::Kind::Add, 6, Type::Limit, Side::Buy, Decimal(2, 0), Decimal(90, 0), Flag::None}, - {Action::Kind::Add, 7, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(100, 0), Flag::None}, - {Action::Kind::Add, 8, Type::Limit, Side::Sell, Decimal(2, 0), Decimal(110, 0), Flag::None}, - {Action::Kind::Add, 9, Type::Limit, Side::Buy, Decimal(1, 0), Decimal(0, 0), Flag::None}, - {Action::Kind::Add, 10, Type::Limit, Side::Buy, Decimal(3, 0), Decimal(100, 0), Flag::None}, - {Action::Kind::Add, 11, Type::Limit, Side::Sell, Decimal(5, 0), Decimal(90, 0), Flag::IoC}, - {Action::Kind::Add, 12, Type::Limit, Side::Buy, Decimal(3, 0), Decimal(100, 0), Flag::AoN}, - {Action::Kind::Add, 13, Type::Limit, Side::Buy, Decimal(4, 0), Decimal(120, 0), Flag::FoK}, - {Action::Kind::Add, 14, Type::Market, Side::Sell, Decimal(3, 0), Decimal(0, 0), Flag::None}, - {Action::Kind::Add, 15, Type::Market, Side::Buy, Decimal(5, 0), Decimal(0, 0), Flag::AoN}, - {Action::Kind::Add, 16, Type::Market, Side::Buy, Decimal(2, 0), Decimal(0, 0), Flag::FoK}, - {Action::Kind::Cancel, 8}, - {Action::Kind::Cancel, 999}, - }; - - for (size_t split = 0; split <= actions.size(); ++split) { - Notification baselineN; - auto baselineOb = std::make_shared>(baselineN); - for (size_t i = 0; i < split; ++i) { - applyAction(actions[i], baselineOb); - } - const auto snapshotBookState = baselineOb->toString(); - const auto snapshotLastPrice = baselineOb->last_price; - - baselineN.Reset(); - for (size_t i = split; i < actions.size(); ++i) { - applyAction(actions[i], baselineOb); - } - const auto baselineSuffixReports = baselineN.Strings(); - const auto baselineFinalBook = baselineOb->toString(); - const auto baselineFinalLastPrice = baselineOb->last_price; - - Notification recoveredN; - auto recoveredOb = std::make_shared>(recoveredN); - for (size_t i = 0; i < split; ++i) { - applyAction(actions[i], recoveredOb); - } - - ASSERT_EQ(recoveredOb->toString(), snapshotBookState) << "split=" << split; - ASSERT_EQ(recoveredOb->last_price, snapshotLastPrice) << "split=" << split; - - recoveredN.Reset(); - for (size_t i = split; i < actions.size(); ++i) { - applyAction(actions[i], recoveredOb); - } - - ASSERT_EQ(recoveredN.Strings(), baselineSuffixReports) << "split=" << split; - ASSERT_EQ(recoveredOb->toString(), baselineFinalBook) << "split=" << split; - ASSERT_EQ(recoveredOb->last_price, baselineFinalLastPrice) << "split=" << split; - } -} - int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); From d12bc462921397bcc68ee7efa94c286a03319bd7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 16:02:41 +0000 Subject: [PATCH 5/6] Move remaining determinism isolation test into dedicated suite Agent-Logs-Url: https://github.com/geseq/cpp-orderbook/sessions/e32f7d1c-6fe3-42f9-b4b8-fae6bcce69e3 Co-authored-by: geseq <5458743+geseq@users.noreply.github.com> --- test/determinism_test.cpp | 17 +++++++++++++++++ test/orderbook_test.cpp | 17 ----------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/test/determinism_test.cpp b/test/determinism_test.cpp index 627f202..8362386 100644 --- a/test/determinism_test.cpp +++ b/test/determinism_test.cpp @@ -143,6 +143,23 @@ TEST_F(DeterminismTest, ReplayProducesSameExecutionTraceAndBookState) { ASSERT_EQ(lastPriceA, lastPriceC); } +TEST_F(DeterminismTest, IndependentBooksStayIsolated) { + Notification n1; + Notification n2; + 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); + + 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); + + 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"}); + ASSERT_EQ(ob1->toString(), ob2->toString()); +} + TEST_F(DeterminismTest, RecoverAtEveryMidpointThenReplaySuffix) { const auto actions = marketAActions(); diff --git a/test/orderbook_test.cpp b/test/orderbook_test.cpp index eac397e..7eaf785 100644 --- a/test/orderbook_test.cpp +++ b/test/orderbook_test.cpp @@ -754,23 +754,6 @@ TEST_F(LimitOrderTest, TestFoK_MarketBuy_CannotFill) { n.Verify({"CreateOrder Accepted 811 11 11"}); } -TEST_F(LimitOrderTest, TestDeterminism_IndependentBooksStayIsolated) { - Notification n1; - Notification n2; - 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); - - 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); - - 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"}); - ASSERT_EQ(ob1->toString(), ob2->toString()); -} - int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); From df3533b1fd6728a0e7cf45113f1a3f964de7b7a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 16:36:56 +0000 Subject: [PATCH 6/6] Address PR review comments in tests Agent-Logs-Url: https://github.com/geseq/cpp-orderbook/sessions/5c7b4556-fcf4-4c84-88c9-bf03728728a9 Co-authored-by: geseq <5458743+geseq@users.noreply.github.com> --- test/determinism_test.cpp | 7 ++----- test/orderbook_test.cpp | 1 + 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/test/determinism_test.cpp b/test/determinism_test.cpp index 8362386..83350b2 100644 --- a/test/determinism_test.cpp +++ b/test/determinism_test.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include @@ -273,8 +275,3 @@ TEST_F(DeterminismTest, TwoCopiesPerMarketRemainDeterministicWithDifferentInterl ASSERT_TRUE(hasReportContaining(nB1.Strings(), "CancelOrder Canceled")); ASSERT_TRUE(hasReportContaining(nB1.Strings(), "CancelOrder Rejected")); } - -int main(int argc, char** argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/orderbook_test.cpp b/test/orderbook_test.cpp index 7eaf785..b7180cb 100644 --- a/test/orderbook_test.cpp +++ b/test/orderbook_test.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include