diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1f86e40 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: C++ CI + +on: + push: + branches: + - main + - polishes + pull_request: + branches: + - main + +jobs: + build-test-sanitize: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Build release executable + run: make + + - name: Run automated tests + run: make test + + - name: Run AddressSanitizer and UndefinedBehaviorSanitizer + run: make sanitize diff --git a/Makefile b/Makefile index 53fd7ce..0b74e39 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,39 @@ -CXX = g++ -CXXFLAGS = -std=c++17 -O2 -Wall -Wextra -Iinclude +CXX ?= g++ +CPPFLAGS := -Iinclude +COMMON_FLAGS := -std=c++17 -Wall -Wextra -Wpedantic -Wconversion -Wshadow +RELEASE_FLAGS := -O2 -DNDEBUG +DEBUG_FLAGS := -O0 -g3 +SANITIZER_FLAGS := -fsanitize=address,undefined -fno-omit-frame-pointer -SRC = src/main.cpp src/OrderBook.cpp -OUT = matching_engine +BUILD_DIR := build +ENGINE := $(BUILD_DIR)/matching_engine +TEST_BIN := $(BUILD_DIR)/order_book_tests +SANITIZER_TEST_BIN := $(BUILD_DIR)/order_book_tests_sanitize -all: - $(CXX) $(CXXFLAGS) $(SRC) -o $(OUT) +ENGINE_SOURCES := src/main.cpp src/OrderBook.cpp +TEST_SOURCES := tests/OrderBookTests.cpp src/OrderBook.cpp + +.PHONY: all test sanitize clean + +all: $(ENGINE) + +$(BUILD_DIR): + mkdir -p $(BUILD_DIR) + +$(ENGINE): $(ENGINE_SOURCES) | $(BUILD_DIR) + $(CXX) $(CPPFLAGS) $(COMMON_FLAGS) $(RELEASE_FLAGS) $(ENGINE_SOURCES) -o $(ENGINE) + +$(TEST_BIN): $(TEST_SOURCES) | $(BUILD_DIR) + $(CXX) $(CPPFLAGS) $(COMMON_FLAGS) $(DEBUG_FLAGS) $(TEST_SOURCES) -o $(TEST_BIN) + +test: $(TEST_BIN) + ./$(TEST_BIN) + +$(SANITIZER_TEST_BIN): $(TEST_SOURCES) | $(BUILD_DIR) + $(CXX) $(CPPFLAGS) $(COMMON_FLAGS) $(DEBUG_FLAGS) $(SANITIZER_FLAGS) $(TEST_SOURCES) -o $(SANITIZER_TEST_BIN) + +sanitize: $(SANITIZER_TEST_BIN) + ASAN_OPTIONS=detect_leaks=1 ./$(SANITIZER_TEST_BIN) clean: - rm -f $(OUT) matching_engine.exe \ No newline at end of file + rm -rf $(BUILD_DIR) diff --git a/include/OrderBook.hpp b/include/OrderBook.hpp index a490285..c86d09f 100644 --- a/include/OrderBook.hpp +++ b/include/OrderBook.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -17,13 +18,9 @@ class OrderBook { using BuyBook = std::map>; using SellBook = std::map; - // Buy book: highest price first BuyBook buyBook; - - // Sell book: lowest price first SellBook sellBook; - // Stores where an order currently exists in the book struct OrderLocation { Side side; Price price; @@ -36,31 +33,32 @@ class OrderBook { std::size_t tradesGenerated; }; - // Fast lookup by order ID for cancel/modify std::unordered_map orderMap; - - // Stores all executed trades std::vector trades; - - // Used to preserve time priority long long timestampCounter; bool verboseOutput; - // Internal helper functions void matchBuyOrder(Order& incomingOrder); void matchSellOrder(Order& incomingOrder); void addToBook(const Order& order); BenchmarkResult runBenchmarkOnce(int numberOfOrders, unsigned int seed); public: - OrderBook(); + explicit OrderBook(bool verbose = true); - void addOrder(int orderId, Side side, int price, int quantity); - void cancelOrder(int orderId); - void modifyOrder(int orderId, int newPrice, int newQuantity); + bool addOrder(int orderId, Side side, int price, int quantity); + bool cancelOrder(int orderId); + bool modifyOrder(int orderId, int newPrice, int newQuantity); void printBook() const; void printTrades() const; void runBenchmark(int numberOfOrders); void runBenchmarkMultiple(int numberOfOrders, int runs); + + [[nodiscard]] bool hasOrder(int orderId) const noexcept; + [[nodiscard]] std::optional getOrder(int orderId) const; + [[nodiscard]] const std::vector& getTrades() const noexcept; + [[nodiscard]] std::size_t activeOrderCount() const noexcept; + [[nodiscard]] std::optional bestBid() const noexcept; + [[nodiscard]] std::optional bestAsk() const noexcept; }; diff --git a/src/OrderBook.cpp b/src/OrderBook.cpp index 3ee6385..6a0899a 100644 --- a/src/OrderBook.cpp +++ b/src/OrderBook.cpp @@ -4,24 +4,39 @@ #include #include #include +#include #include #include +#include -OrderBook::OrderBook() : timestampCounter(0), verboseOutput(true) {} +namespace { +struct BenchmarkOrder { + int orderId; + Side side; + int price; + int quantity; +}; +} // namespace -void OrderBook::addOrder(int orderId, Side side, int price, int quantity) { +OrderBook::OrderBook(bool verbose) + : timestampCounter(0), verboseOutput(verbose) {} + +bool OrderBook::addOrder(int orderId, Side side, int price, int quantity) { if (quantity <= 0 || price <= 0) { - std::cout << "Invalid order: price and quantity must be positive.\n"; - return; + if (verboseOutput) { + std::cout << "Invalid order: price and quantity must be positive.\n"; + } + return false; } if (orderMap.find(orderId) != orderMap.end()) { - std::cout << "Order ID already exists: " << orderId << "\n"; - return; + if (verboseOutput) { + std::cout << "Order ID already exists: " << orderId << "\n"; + } + return false; } Order incomingOrder(orderId, side, price, quantity, ++timestampCounter); - if (side == Side::BUY) { matchBuyOrder(incomingOrder); } else { @@ -31,37 +46,39 @@ void OrderBook::addOrder(int orderId, Side side, int price, int quantity) { if (incomingOrder.quantity > 0) { addToBook(incomingOrder); } + + return true; } void OrderBook::matchBuyOrder(Order& incomingOrder) { while (incomingOrder.quantity > 0 && !sellBook.empty()) { auto bestSellLevel = sellBook.begin(); - int bestSellPrice = bestSellLevel->first; + const int bestSellPrice = bestSellLevel->first; if (incomingOrder.price < bestSellPrice) { break; } auto& sellQueue = bestSellLevel->second; - while (incomingOrder.quantity > 0 && !sellQueue.empty()) { Order& restingSellOrder = sellQueue.front(); - - int tradedQuantity = std::min(incomingOrder.quantity, restingSellOrder.quantity); + const int tradedQuantity = + std::min(incomingOrder.quantity, restingSellOrder.quantity); trades.emplace_back( incomingOrder.orderId, restingSellOrder.orderId, restingSellOrder.price, - tradedQuantity - ); + tradedQuantity); incomingOrder.quantity -= tradedQuantity; restingSellOrder.quantity -= tradedQuantity; - if(verboseOutput){ - std::cout << "TRADE: BuyOrder=" << incomingOrder.orderId << " SellOrder=" << restingSellOrder.orderId - << " Price=" << restingSellOrder.price << " Quantity=" << tradedQuantity << "\n"; + if (verboseOutput) { + std::cout << "TRADE: BuyOrder=" << incomingOrder.orderId + << " SellOrder=" << restingSellOrder.orderId + << " Price=" << restingSellOrder.price + << " Quantity=" << tradedQuantity << "\n"; } if (restingSellOrder.quantity == 0) { @@ -79,34 +96,34 @@ void OrderBook::matchBuyOrder(Order& incomingOrder) { void OrderBook::matchSellOrder(Order& incomingOrder) { while (incomingOrder.quantity > 0 && !buyBook.empty()) { auto bestBuyLevel = buyBook.begin(); - int bestBuyPrice = bestBuyLevel->first; + const int bestBuyPrice = bestBuyLevel->first; if (incomingOrder.price > bestBuyPrice) { break; } auto& buyQueue = bestBuyLevel->second; - while (incomingOrder.quantity > 0 && !buyQueue.empty()) { Order& restingBuyOrder = buyQueue.front(); - - int tradedQuantity = std::min(incomingOrder.quantity, restingBuyOrder.quantity); + const int tradedQuantity = + std::min(incomingOrder.quantity, restingBuyOrder.quantity); trades.emplace_back( restingBuyOrder.orderId, incomingOrder.orderId, restingBuyOrder.price, - tradedQuantity - ); + tradedQuantity); incomingOrder.quantity -= tradedQuantity; restingBuyOrder.quantity -= tradedQuantity; - if(verboseOutput){ - std::cout << "TRADE: BuyOrder=" << restingBuyOrder.orderId << " SellOrder=" << incomingOrder.orderId - << " Price=" << restingBuyOrder.price << " Quantity=" << tradedQuantity << "\n"; + if (verboseOutput) { + std::cout << "TRADE: BuyOrder=" << restingBuyOrder.orderId + << " SellOrder=" << incomingOrder.orderId + << " Price=" << restingBuyOrder.price + << " Quantity=" << tradedQuantity << "\n"; } - + if (restingBuyOrder.quantity == 0) { orderMap.erase(restingBuyOrder.orderId); buyQueue.pop_front(); @@ -123,38 +140,24 @@ void OrderBook::addToBook(const Order& order) { if (order.side == Side::BUY) { auto& orderQueue = buyBook[order.price]; orderQueue.push_back(order); - auto iteratorToOrder = std::prev(orderQueue.end()); - - orderMap[order.orderId] = OrderLocation{ - order.side, - order.price, - iteratorToOrder - }; + orderMap[order.orderId] = + OrderLocation{order.side, order.price, iteratorToOrder}; } else { auto& orderQueue = sellBook[order.price]; orderQueue.push_back(order); - auto iteratorToOrder = std::prev(orderQueue.end()); - - orderMap[order.orderId] = OrderLocation{ - order.side, - order.price, - iteratorToOrder - }; + orderMap[order.orderId] = + OrderLocation{order.side, order.price, iteratorToOrder}; } } void OrderBook::printBook() const { std::cout << "\n===== ORDER BOOK =====\n"; - std::cout << "SELL ORDERS:\n"; for (const auto& priceLevel : sellBook) { - int price = priceLevel.first; - const auto& orderQueue = priceLevel.second; - - std::cout << "Price " << price << ": "; - for (const auto& order : orderQueue) { + std::cout << "Price " << priceLevel.first << ": "; + for (const auto& order : priceLevel.second) { std::cout << "[ID=" << order.orderId << ", Qty=" << order.quantity << "] "; } @@ -163,65 +166,54 @@ void OrderBook::printBook() const { std::cout << "\nBUY ORDERS:\n"; for (const auto& priceLevel : buyBook) { - int price = priceLevel.first; - const auto& orderQueue = priceLevel.second; - - std::cout << "Price " << price << ": "; - for (const auto& order : orderQueue) { + std::cout << "Price " << priceLevel.first << ": "; + for (const auto& order : priceLevel.second) { std::cout << "[ID=" << order.orderId << ", Qty=" << order.quantity << "] "; } std::cout << "\n"; } - std::cout << "======================\n"; } void OrderBook::printTrades() const { std::cout << "\n===== TRADES =====\n"; - if (trades.empty()) { std::cout << "No trades executed.\n"; } - for (const auto& trade : trades) { std::cout << "BuyOrder=" << trade.buyOrderId << " SellOrder=" << trade.sellOrderId << " Price=" << trade.price << " Quantity=" << trade.quantity << "\n"; } - std::cout << "==================\n"; } -void OrderBook::cancelOrder(int orderId) { +bool OrderBook::cancelOrder(int orderId) { auto orderLocationIterator = orderMap.find(orderId); - if (orderLocationIterator == orderMap.end()) { - std::cout << "Cancel failed: Order ID not found: " << orderId << "\n"; - return; + if (verboseOutput) { + std::cout << "Cancel failed: Order ID not found: " << orderId << "\n"; + } + return false; } - OrderLocation location = orderLocationIterator->second; - + const OrderLocation location = orderLocationIterator->second; if (location.side == Side::BUY) { auto priceLevelIterator = buyBook.find(location.price); - if (priceLevelIterator != buyBook.end()) { auto& orderQueue = priceLevelIterator->second; orderQueue.erase(location.iterator); - if (orderQueue.empty()) { buyBook.erase(priceLevelIterator); } } } else { auto priceLevelIterator = sellBook.find(location.price); - if (priceLevelIterator != sellBook.end()) { auto& orderQueue = priceLevelIterator->second; orderQueue.erase(location.iterator); - if (orderQueue.empty()) { sellBook.erase(priceLevelIterator); } @@ -229,65 +221,88 @@ void OrderBook::cancelOrder(int orderId) { } orderMap.erase(orderId); - - std::cout << "Cancelled order: " << orderId << "\n"; + if (verboseOutput) { + std::cout << "Cancelled order: " << orderId << "\n"; + } + return true; } -void OrderBook::modifyOrder(int orderId, int newPrice, int newQuantity) { +bool OrderBook::modifyOrder(int orderId, int newPrice, int newQuantity) { auto orderLocationIterator = orderMap.find(orderId); - if (orderLocationIterator == orderMap.end()) { - std::cout << "Modify failed: Order ID not found: " << orderId << "\n"; - return; + if (verboseOutput) { + std::cout << "Modify failed: Order ID not found: " << orderId << "\n"; + } + return false; } if (newPrice <= 0 || newQuantity <= 0) { - std::cout << "Modify failed: price and quantity must be positive.\n"; - return; + if (verboseOutput) { + std::cout << "Modify failed: price and quantity must be positive.\n"; + } + return false; } - Side existingSide = orderLocationIterator->second.side; + const Side existingSide = orderLocationIterator->second.side; + const bool previousVerbose = verboseOutput; + verboseOutput = false; + const bool cancelled = cancelOrder(orderId); + const bool added = cancelled && addOrder(orderId, existingSide, newPrice, newQuantity); + verboseOutput = previousVerbose; - cancelOrder(orderId); - addOrder(orderId, existingSide, newPrice, newQuantity); + if (!added) { + return false; + } - std::cout << "Modified order: " << orderId - << " NewPrice=" << newPrice - << " NewQuantity=" << newQuantity << "\n"; + if (verboseOutput) { + std::cout << "Modified order: " << orderId + << " NewPrice=" << newPrice + << " NewQuantity=" << newQuantity << "\n"; + } + return true; } -OrderBook::BenchmarkResult OrderBook::runBenchmarkOnce(int numberOfOrders, unsigned int seed) { - OrderBook benchmarkBook; - benchmarkBook.verboseOutput = false; - +OrderBook::BenchmarkResult OrderBook::runBenchmarkOnce( + int numberOfOrders, + unsigned int seed) { std::mt19937 randomGenerator(seed); std::uniform_int_distribution sideDistribution(0, 1); std::uniform_int_distribution priceDistribution(90, 110); std::uniform_int_distribution quantityDistribution(1, 100); - auto startTime = std::chrono::high_resolution_clock::now(); + std::vector generatedOrders; + generatedOrders.reserve(static_cast(numberOfOrders)); for (int orderId = 1; orderId <= numberOfOrders; ++orderId) { - Side side = sideDistribution(randomGenerator) == 0 ? Side::BUY : Side::SELL; - int price = priceDistribution(randomGenerator); - int quantity = quantityDistribution(randomGenerator); - - benchmarkBook.addOrder(orderId, side, price, quantity); + generatedOrders.push_back(BenchmarkOrder{ + orderId, + sideDistribution(randomGenerator) == 0 ? Side::BUY : Side::SELL, + priceDistribution(randomGenerator), + quantityDistribution(randomGenerator)}); } - auto endTime = std::chrono::high_resolution_clock::now(); + OrderBook benchmarkBook(false); + const auto startTime = std::chrono::steady_clock::now(); - auto totalMicroseconds = - std::chrono::duration_cast(endTime - startTime).count(); + for (const auto& order : generatedOrders) { + benchmarkBook.addOrder( + order.orderId, order.side, order.price, order.quantity); + } - double averageMicroseconds = - static_cast(totalMicroseconds) / static_cast(numberOfOrders); + const auto endTime = std::chrono::steady_clock::now(); + const auto totalMicroseconds = + std::chrono::duration_cast( + endTime - startTime) + .count(); + + const double averageMicroseconds = + static_cast(totalMicroseconds) / + static_cast(numberOfOrders); return BenchmarkResult{ totalMicroseconds, averageMicroseconds, - benchmarkBook.trades.size() - }; + benchmarkBook.trades.size()}; } void OrderBook::runBenchmark(int numberOfOrders) { @@ -296,12 +311,13 @@ void OrderBook::runBenchmark(int numberOfOrders) { return; } - BenchmarkResult result = runBenchmarkOnce(numberOfOrders, 42); - + const BenchmarkResult result = runBenchmarkOnce(numberOfOrders, 42U); std::cout << "\n===== BENCHMARK =====\n"; std::cout << "Processed orders: " << numberOfOrders << "\n"; std::cout << "Total time: " << result.totalMicroseconds << " microseconds\n"; - std::cout << "Average processing time: " << std::fixed << std::setprecision(4) << result.averageMicroseconds << " microseconds/order\n"; + std::cout << "Average processing time: " << std::fixed + << std::setprecision(4) << result.averageMicroseconds + << " microseconds/order\n"; std::cout << "Trades generated: " << result.tradesGenerated << "\n"; std::cout << "=====================\n"; } @@ -317,27 +333,66 @@ void OrderBook::runBenchmarkMultiple(int numberOfOrders, int runs) { long long totalTime = 0; std::size_t totalTrades = 0; - for (int run = 1; run <= runs; ++run) { - unsigned int seed = 42 + run; - BenchmarkResult result = runBenchmarkOnce(numberOfOrders, seed); - + for (int run = 0; run < runs; ++run) { + const BenchmarkResult result = runBenchmarkOnce(numberOfOrders, 42U); bestTime = std::min(bestTime, result.totalMicroseconds); worstTime = std::max(worstTime, result.totalMicroseconds); totalTime += result.totalMicroseconds; totalTrades += result.tradesGenerated; } - double averageTotalTime = static_cast(totalTime) / static_cast(runs); - double averagePerOrder = averageTotalTime / static_cast(numberOfOrders); - double averageTrades = static_cast(totalTrades) / static_cast(runs); + const double averageTotalTime = + static_cast(totalTime) / static_cast(runs); + const double averagePerOrder = + averageTotalTime / static_cast(numberOfOrders); + const double averageTrades = + static_cast(totalTrades) / static_cast(runs); std::cout << "\n===== MULTI-RUN BENCHMARK =====\n"; std::cout << "Runs: " << runs << "\n"; std::cout << "Orders per run: " << numberOfOrders << "\n"; std::cout << "Best time: " << bestTime << " microseconds\n"; - std::cout << "Average time: " << std::fixed << std::setprecision(2) << averageTotalTime << " microseconds\n"; + std::cout << "Average time: " << std::fixed << std::setprecision(2) + << averageTotalTime << " microseconds\n"; std::cout << "Worst time: " << worstTime << " microseconds\n"; - std::cout << "Average processing time: " << std::fixed << std::setprecision(4) << averagePerOrder << " microseconds/order\n"; - std::cout << "Average trades generated: " << std::fixed << std::setprecision(2) << averageTrades << "\n"; + std::cout << "Average processing time: " << std::fixed + << std::setprecision(4) << averagePerOrder + << " microseconds/order\n"; + std::cout << "Average trades generated: " << std::fixed + << std::setprecision(2) << averageTrades << "\n"; std::cout << "===============================\n"; } + +bool OrderBook::hasOrder(int orderId) const noexcept { + return orderMap.find(orderId) != orderMap.end(); +} + +std::optional OrderBook::getOrder(int orderId) const { + const auto location = orderMap.find(orderId); + if (location == orderMap.end()) { + return std::nullopt; + } + return *(location->second.iterator); +} + +const std::vector& OrderBook::getTrades() const noexcept { + return trades; +} + +std::size_t OrderBook::activeOrderCount() const noexcept { + return orderMap.size(); +} + +std::optional OrderBook::bestBid() const noexcept { + if (buyBook.empty()) { + return std::nullopt; + } + return buyBook.begin()->first; +} + +std::optional OrderBook::bestAsk() const noexcept { + if (sellBook.empty()) { + return std::nullopt; + } + return sellBook.begin()->first; +} diff --git a/tests/OrderBookTests.cpp b/tests/OrderBookTests.cpp new file mode 100644 index 0000000..242c625 --- /dev/null +++ b/tests/OrderBookTests.cpp @@ -0,0 +1,168 @@ +#include "OrderBook.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { +void expect(bool condition, const std::string& message) { + if (!condition) { + throw std::runtime_error(message); + } +} + +void testBetterPriceExecutesFirst() { + OrderBook book(false); + book.addOrder(1, Side::SELL, 101, 5); + book.addOrder(2, Side::SELL, 100, 5); + book.addOrder(3, Side::BUY, 101, 5); + + const auto& trades = book.getTrades(); + expect(trades.size() == 1, "Expected exactly one trade"); + expect(trades[0].sellOrderId == 2, "Best ask should execute first"); + expect(trades[0].price == 100, "Trade should occur at resting price"); +} + +void testFifoWithinPriceLevel() { + OrderBook book(false); + book.addOrder(1, Side::SELL, 100, 5); + book.addOrder(2, Side::SELL, 100, 5); + book.addOrder(3, Side::BUY, 100, 7); + + const auto& trades = book.getTrades(); + expect(trades.size() == 2, "Expected two fills"); + expect(trades[0].sellOrderId == 1, "Older order must execute first"); + expect(trades[0].quantity == 5, "First order should fill completely"); + expect(trades[1].sellOrderId == 2, "Second order should execute next"); + expect(trades[1].quantity == 2, "Second order should be partially filled"); + + const auto remaining = book.getOrder(2); + expect(remaining.has_value(), "Second sell order should remain active"); + expect(remaining->quantity == 3, "Remaining quantity should be three"); +} + +void testPartialFillOfRestingOrder() { + OrderBook book(false); + book.addOrder(1, Side::BUY, 100, 10); + book.addOrder(2, Side::SELL, 100, 4); + + const auto remaining = book.getOrder(1); + expect(remaining.has_value(), "Resting buy order should remain"); + expect(remaining->quantity == 6, "Resting quantity should reduce to six"); + expect(!book.hasOrder(2), "Fully filled incoming order must not rest"); +} + +void testIncomingOrderSweepsMultipleLevels() { + OrderBook book(false); + book.addOrder(1, Side::SELL, 100, 3); + book.addOrder(2, Side::SELL, 101, 4); + book.addOrder(3, Side::BUY, 102, 10); + + const auto& trades = book.getTrades(); + expect(trades.size() == 2, "Expected fills at two price levels"); + expect(trades[0].price == 100, "Lowest ask should execute first"); + expect(trades[1].price == 101, "Next ask should execute second"); + + const auto remaining = book.getOrder(3); + expect(remaining.has_value(), "Incoming buy remainder should rest"); + expect(remaining->quantity == 3, "Incoming remainder should equal three"); + expect(book.bestBid() == 102, "Remaining order should become best bid"); +} + +void testCancellationRemovesOrder() { + OrderBook book(false); + book.addOrder(1, Side::BUY, 100, 5); + + expect(book.cancelOrder(1), "Cancellation should succeed"); + expect(!book.hasOrder(1), "Cancelled order should be absent"); + expect(book.activeOrderCount() == 0, "Book should contain no active orders"); + expect(!book.cancelOrder(1), "Repeated cancellation should fail"); +} + +void testModificationLosesTimePriority() { + OrderBook book(false); + book.addOrder(1, Side::BUY, 100, 5); + book.addOrder(2, Side::BUY, 100, 5); + book.modifyOrder(1, 100, 5); + book.addOrder(3, Side::SELL, 100, 5); + + const auto& trades = book.getTrades(); + expect(trades.size() == 1, "Expected one trade"); + expect(trades[0].buyOrderId == 2, + "Modified order should lose its earlier FIFO priority"); +} + +void testDuplicateActiveOrderIdRejected() { + OrderBook book(false); + expect(book.addOrder(1, Side::BUY, 100, 5), "First add should succeed"); + expect(!book.addOrder(1, Side::SELL, 99, 5), + "Duplicate active order ID should be rejected"); + + const auto order = book.getOrder(1); + expect(order.has_value(), "Original order should remain"); + expect(order->side == Side::BUY, "Original side should remain unchanged"); + expect(order->price == 100, "Original price should remain unchanged"); +} + +void testInvalidPriceAndQuantityRejected() { + OrderBook book(false); + expect(!book.addOrder(1, Side::BUY, 0, 5), "Zero price must be rejected"); + expect(!book.addOrder(2, Side::BUY, 100, 0), "Zero quantity must be rejected"); + expect(!book.addOrder(3, Side::SELL, -1, 5), "Negative price must be rejected"); + expect(!book.addOrder(4, Side::SELL, 100, -1), + "Negative quantity must be rejected"); + expect(book.activeOrderCount() == 0, "No invalid order should enter the book"); +} + +void testUncrossedOrdersDoNotTrade() { + OrderBook book(false); + book.addOrder(1, Side::BUY, 100, 5); + book.addOrder(2, Side::SELL, 101, 5); + + expect(book.getTrades().empty(), "Uncrossed orders must not trade"); + expect(book.bestBid() == 100, "Best bid should be 100"); + expect(book.bestAsk() == 101, "Best ask should be 101"); +} + +void testTradeUsesRestingOrderPrice() { + OrderBook book(false); + book.addOrder(1, Side::SELL, 100, 5); + book.addOrder(2, Side::BUY, 105, 5); + + const auto& trades = book.getTrades(); + expect(trades.size() == 1, "Expected one trade"); + expect(trades[0].price == 100, "Trade price should be the resting order price"); +} +} // namespace + +int main() { + const std::vector>> tests = { + {"better price executes first", testBetterPriceExecutesFirst}, + {"FIFO within price level", testFifoWithinPriceLevel}, + {"partial fill of resting order", testPartialFillOfRestingOrder}, + {"incoming order sweeps multiple levels", testIncomingOrderSweepsMultipleLevels}, + {"cancellation removes order", testCancellationRemovesOrder}, + {"modification loses time priority", testModificationLosesTimePriority}, + {"duplicate active ID rejected", testDuplicateActiveOrderIdRejected}, + {"invalid price and quantity rejected", testInvalidPriceAndQuantityRejected}, + {"uncrossed orders do not trade", testUncrossedOrdersDoNotTrade}, + {"trade uses resting order price", testTradeUsesRestingOrderPrice}, + }; + + std::size_t passed = 0; + for (const auto& [name, test] : tests) { + try { + test(); + ++passed; + std::cout << "[PASS] " << name << "\n"; + } catch (const std::exception& exception) { + std::cerr << "[FAIL] " << name << ": " << exception.what() << "\n"; + } + } + + std::cout << "\n" << passed << "/" << tests.size() << " tests passed.\n"; + return passed == tests.size() ? 0 : 1; +} diff --git a/tests/sample_test.txt b/tests/sample_test.txt deleted file mode 100644 index b19300c..0000000 --- a/tests/sample_test.txt +++ /dev/null @@ -1,24 +0,0 @@ -ADD 1 BUY 100 10 -ADD 2 BUY 100 5 -ADD 3 SELL 100 12 -PRINT -TRADES - -ADD 4 SELL 105 10 -ADD 5 BUY 104 10 -PRINT - -CANCEL 4 -PRINT - -MODIFY 5 106 10 -PRINT -TRADES - -ADD 6 SELL 106 5 -ADD 7 SELL 106 8 -ADD 8 BUY 106 10 -PRINT -TRADES - -EXIT