Low-Latency Order Book in C++20

Build a matching engine with lock-free concurrency and algorithmic trading.

Introduction

30 Second Summary

Every time you buy a stock, crypto, or ETF, your order hits a matching engine that pairs you with a seller in microseconds. These systems process millions of orders per second with zero tolerance for delay.

In this project, you will build a complete low-latency order book and matching engine from scratch in C++20. You will then layer lock-free concurrency and an algorithmic trading strategy on top.

What You'll Build

A production-style matching engine that supports multiple order types, custom memory allocation, multi-threaded processing via lock-free queues, and a mean-reversion trading strategy with performance metrics.

By the end of this project, you'll have:

  • A price-time priority order book with a custom memory pool allocator that eliminates heap allocation on the hot path, processing millions of orders per second.
  • A lock-free SPSC ring buffer connecting a feed handler thread to the matching engine thread using atomic acquire/release memory ordering.
  • A mean-reversion trading strategy that monitors mid-price deviation from a moving average, submits orders to your engine, and reports PnL with Sharpe ratio and max drawdown.
  • Secret Mission: Add a competing momentum strategy and run both strategies head-to-head on the same order book to see which market regime favors which approach.

Are there any prerequisites?

You need intermediate C++ knowledge: classes, templates, smart pointers, and STL containers. No prior quantitative finance or concurrency knowledge is required.

Before We Start

You're about to build the core infrastructure that powers every stock exchange in the world: a matching engine. This is the system that pairs buyers with sellers at the best available price, processing millions of orders per second with nanosecond-level performance.

Before writing any code, you need two tools ready on your Mac: a C++20 compiler and a build system. Your Mac already ships with Apple Clang via Xcode Command Line Tools. You also need CMake to manage the build process across multiple source files, libraries, and test targets.

In this step, get ready to:
  • Verify your C++ compiler supports C++20.
  • Install and verify CMake 3.20+.
  • Open VS Code and a terminal.
Verify your C++ compiler

Apple Clang is the C++ compiler bundled with Xcode Command Line Tools. It supports C++20 features like concepts, designated initializers, and std::optional monadic operations that this project uses.

  • Check your Clang version by running this command in any terminal:
clang --version

✔️ I see a version number

Your compiler is ready. You should see output like Apple clang version 16.x.x (or similar). Any recent Apple Clang version supports the C++20 features this project needs.

ⓧ Command not found

This means Xcode Command Line Tools are not installed. Install them by running:

xcode-select --install

A dialog will appear asking you to confirm the installation. Click Install and wait for the download to complete. Once finished, run clang --version again to confirm it works.

Install and verify CMake

CMake is a build system generator. It reads a CMakeLists.txt file and produces the actual Makefiles that compile your C++ code. This project requires CMake version 3.20 or higher for full C++20 support and the FetchContent module.

  • Check if CMake is already installed by running:
cmake --version

✔️ I see version 3.20 or higher

You're all set. The version number appears on the first line of output (e.g. cmake version 4.3.1). Any version at 3.20 or above works for this project.

ⓧ I see an older version

You have CMake installed, but it's too old for this project. Upgrade it using Homebrew by running:

brew upgrade cmake

After the upgrade finishes, run cmake --version again to confirm the version is 3.20 or higher.

Don't have Homebrew?

If brew is not recognized, you need to install Homebrew first. Visit brew.sh and follow the one-line install instructions on the homepage. Then come back and run the upgrade command.

ⓧ Command not found

CMake is not installed. Install it using Homebrew by running:

brew install cmake

After the install finishes, verify it by running:

cmake --version

You should see a version number of 3.20 or higher on the first line of output.

Don't have Homebrew?

If brew is not recognized, you need to install Homebrew first. Visit brew.sh and follow the one-line install instructions on the homepage. Then come back and run the install command.

CMake install failing?

Make sure Xcode Command Line Tools are installed first (the previous substep). Homebrew depends on them.

help me troubleshoot my CMake installation on macOS

Open VS Code and a terminal
  • Press Cmd+Space to open Spotlight search.
  • Type Visual Studio Code and press Enter to open it.
  • Open the integrated terminal by pressing Ctrl+` (Control and the backtick key).

What is the integrated terminal?

VS Code has a built-in terminal panel at the bottom of the window. You'll use this terminal for all commands throughout the project. It works exactly like the macOS Terminal app, but keeps everything in one window.

Your tools are verified and ready. Next up, you'll create the project directory structure and write the CMake build configuration that ties everything together.

Set Up the Project & Build System

Every C++ project needs a build system to compile source files into executables and libraries. For this project, you'll use CMake to manage compilation of your matching engine, benchmarks, and tests from a single configuration file.

In this step, you'll create the project skeleton and configure CMake to build three separate targets: the main engine executable, a benchmark binary, and a test suite powered by Google Test.

In this step, get ready to:
  • Create the project directory with source, header, test, and benchmark folders.
  • Write a CMakeLists.txt that configures C++20, Google Test, and three build targets.
  • Compile the project and verify the empty executable runs.
Create the project directory structure

You need a clean folder layout that separates headers, source files, tests, and benchmarks. This is the standard structure for production C++ projects.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir order_engine
cd order_engine
  • Create the four subdirectories by running this command:
mkdir include src tests bench

The CMake configuration you'll write next references source files that don't exist yet. CMake requires these files to be present at configure time, so you'll create empty placeholders now and fill them in during later steps.

  • Create placeholder source files by running these commands:
# Main entry point and benchmark entry point (need a main function)
echo 'int main() { return 0; }' > src/main.cpp
echo 'int main() { return 0; }' > bench/bench_main.cpp

# Empty library source stubs
touch src/order_book.cpp src/matching_engine.cpp src/order_generator.cpp
touch src/feed_handler.cpp src/strategy.cpp src/metrics.cpp

# Empty test file stubs
touch tests/test_memory_pool.cpp tests/test_order_book.cpp
touch tests/test_matching_engine.cpp tests/test_spsc_queue.cpp

Why create empty files?

CMake reads your file list at configure time and fails if any listed source file is missing. Empty .cpp files compile into empty object files, which is enough for the build to succeed. You'll fill these with real code starting in the next step.

  • Confirm the directory structure by running:
ls

You should see four folders listed: bench, include, src, and tests.

  • Confirm the source stubs exist by running:
ls src/ tests/ bench/

You should see main.cpp and six library stubs under src/, four test stubs under tests/, and bench_main.cpp under bench/.

Not seeing the expected files?

Make sure you ran the touch and echo commands from inside the order_engine folder. Run pwd to verify your current directory ends in order_engine.

help me debug my directory structure

Now open this project in VS Code so you can write the build configuration.

  • Switch to VS Code.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the order_engine folder, and click Open.

You should see the include, src, tests, and bench folders in the Explorer sidebar on the left.

Write the CMake build configuration

CMake uses a CMakeLists.txt file at the project root to define how your code compiles, what libraries to link, and where to find dependencies.

  • In VS Code's Explorer sidebar, right-click in the empty space below the folders and select New File.
  • Name the file CMakeLists.txt and press Enter.
  • Paste the following build configuration into the file:
cmake_minimum_required(VERSION 3.20)
project(order_engine LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Main library
add_library(engine_lib
    src/order_book.cpp
    src/matching_engine.cpp
    src/order_generator.cpp
    src/feed_handler.cpp
    src/strategy.cpp
    src/metrics.cpp
)
target_include_directories(engine_lib PUBLIC include)
target_compile_options(engine_lib PRIVATE -Wall -Wextra -Wpedantic)

# Main executable
add_executable(order_engine src/main.cpp)
target_link_libraries(order_engine PRIVATE engine_lib)

# Benchmark executable
add_executable(bench_engine bench/bench_main.cpp)
target_link_libraries(bench_engine PRIVATE engine_lib)
target_compile_options(bench_engine PRIVATE -O3 -march=native)

# Testing
include(FetchContent)
FetchContent_Declare(
    googletest
    URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.zip
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)

enable_testing()

add_executable(engine_tests
    tests/test_memory_pool.cpp
    tests/test_order_book.cpp
    tests/test_matching_engine.cpp
    tests/test_spsc_queue.cpp
)
target_link_libraries(engine_tests PRIVATE engine_lib GTest::gtest_main)

include(GoogleTest)
gtest_discover_tests(engine_tests)

What does this configuration do?

  • cmake_minimum_required(VERSION 3.20) ensures anyone building this project has a CMake version that supports C++20.
  • engine_lib compiles all your source files into a shared library with strict warnings enabled.
  • order_engine is the main executable that links against the library.
  • bench_engine is the benchmark binary, compiled with -O3 -march=native for maximum optimization.
  • The FetchContent block downloads Google Test v1.17.0 at configure time so you don't need to install it manually.
  • engine_tests links against both your library and GTest::gtest_main, which provides the test runner's entry point.
  • Save CMakeLists.txt by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Configure and compile the project

CMake works in two phases: configure (generate build files) and build (compile everything). The first configure will also download Google Test, so it takes a bit longer.

  • In your terminal, make sure you are inside the order_engine folder, then generate the build files by running:
cmake -S . -B build

What does this command do?

-S . tells CMake the source directory is the current folder (where CMakeLists.txt lives). -B build tells it to put all generated build files in a build/ subdirectory, keeping your source tree clean.

You should see output ending with lines like Configuring done and Generating done. CMake will also print progress as it downloads Google Test.

Seeing a configure error?

If CMake reports a missing source file, double-check that you ran the touch commands from the earlier substep. Every .cpp file listed in CMakeLists.txt must exist on disk.

If you see a network error during the Google Test download, check your internet connection and re-run the same command.

help me fix my CMake configure error

  • Compile all targets by running:
cmake --build build

You should see compilation output for each target with no errors.

  • Verify the main executable runs by running:
./build/order_engine

The command produces no output and returns immediately. This confirms your empty main() compiled and linked correctly against the engine library.

Build failed with linker errors?

If you see undefined reference errors, make sure bench/bench_main.cpp contains int main() { return 0; } and not just an empty file. The benchmark target needs its own entry point.

If compilation reports syntax errors in your CMakeLists.txt, check for missing parentheses or quotes around the FetchContent URL.

help me fix my build errors

✔️ Awesome, I've got everything!

Great. Double check you've saved CMakeLists.txt and that ./build/order_engine runs without output.

ⓧ I'd like to double check the full code

Here is the complete CMakeLists.txt and src/main.cpp for comparison.

cmake_minimum_required(VERSION 3.20)
project(order_engine LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Main library
add_library(engine_lib
    src/order_book.cpp
    src/matching_engine.cpp
    src/order_generator.cpp
    src/feed_handler.cpp
    src/strategy.cpp
    src/metrics.cpp
)
target_include_directories(engine_lib PUBLIC include)
target_compile_options(engine_lib PRIVATE -Wall -Wextra -Wpedantic)

# Main executable
add_executable(order_engine src/main.cpp)
target_link_libraries(order_engine PRIVATE engine_lib)

# Benchmark executable
add_executable(bench_engine bench/bench_main.cpp)
target_link_libraries(bench_engine PRIVATE engine_lib)
target_compile_options(bench_engine PRIVATE -O3 -march=native)

# Testing
include(FetchContent)
FetchContent_Declare(
    googletest
    URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.zip
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)

enable_testing()

add_executable(engine_tests
    tests/test_memory_pool.cpp
    tests/test_order_book.cpp
    tests/test_matching_engine.cpp
    tests/test_spsc_queue.cpp
)
target_link_libraries(engine_tests PRIVATE engine_lib GTest::gtest_main)

include(GoogleTest)
gtest_discover_tests(engine_tests)
int main() { return 0; }

Your build system is ready and all three targets compile cleanly. Next up, you'll define the core data types and build a custom memory pool allocator that eliminates heap allocation on the hot path.

Define Core Types & Build the Memory Pool

Every matching engine processes the same fundamental objects: orders, prices, and quantities. Before writing any matching logic, you need a rock-solid set of types that every other file in the project will depend on.

In this step you will define those shared types, build a custom memory pool allocator that eliminates heap allocation on the hot path, and write unit tests to prove it works correctly.

In this step, get ready to:
  • Define shared types, enums, and the Order struct.
  • Build a fixed-size memory pool with O(1) allocate and deallocate.
  • Write and pass unit tests validating pool correctness.
Define the core types and Order struct

The matching engine needs precise, lightweight representations for prices, quantities, and order identifiers. You will define these as type aliases over fixed-width integers so every component agrees on sizes and there are no implicit conversion surprises.

  • Create a new file called types.hpp inside your include/ directory and paste the following code:
#pragma once

#include <cstdint>
#include <chrono>

namespace engine {

// Fixed-width type aliases for prices, quantities, and order IDs
using Price    = int64_t;
using Quantity = uint32_t;
using OrderId  = uint64_t;

// Sentinel values used throughout the engine
constexpr Price INVALID_PRICE = 0;
constexpr Price MAX_PRICE = INT64_MAX;
constexpr Price MIN_PRICE = 1;
constexpr uint32_t NULL_INDEX = UINT32_MAX;

// Buy or Sell
enum class Side : uint8_t {
    Buy,
    Sell
};

// Supported order types
enum class OrderType : uint8_t {
    Limit,
    Market,
    ImmediateOrCancel,
    FillOrKill
};

// Emitted when two orders match
struct Trade {
    OrderId buy_order_id;
    OrderId sell_order_id;
    Price price;
    Quantity quantity;
    uint64_t timestamp_ns;
};

// Used by the order book to locate an order in the pool
struct OrderLocation {
    Price price;
    Side side;
    uint32_t pool_index;
};

// Result codes returned after submitting or cancelling an order
enum class OrderResult : uint8_t {
    Accepted,
    Filled,
    PartialFill,
    Cancelled,
    Rejected
};

// Returns the current time in nanoseconds (steady clock)
inline uint64_t now_ns() {
    return static_cast<uint64_t>(
        std::chrono::steady_clock::now().time_since_epoch().count()
    );
}

} // namespace engine

What does this code do?

This header defines the vocabulary of your entire engine. Every other file will include it.

  • Price is a signed 64-bit integer representing cents (or ticks). Signed lets you detect underflow in arithmetic.
  • NULL_INDEX is the sentinel for "no link" in the intrusive linked lists you will build later.
  • The Side and OrderType enums use uint8_t backing to keep Order structs compact and cache-friendly.
  • now_ns() provides nanosecond timestamps via std::chrono::steady_clock. This is the portable choice for Apple Silicon (no RDTSC on ARM).

Next, define the Order struct that holds everything a single order needs. Each order will live inside the memory pool and participate in an intrusive doubly-linked list.

  • Create a new file called order.hpp inside your include/ directory and paste the following code:
#pragma once

#include "types.hpp"

namespace engine {

struct Order {
    OrderId id;
    Price price;
    Quantity quantity;
    Quantity filled_quantity;
    Side side;
    OrderType type;
    uint64_t timestamp_ns;

    // Intrusive doubly-linked list indices (into the memory pool)
    uint32_t prev_index;
    uint32_t next_index;

    // Pool management
    uint32_t pool_index;

    [[nodiscard]] Quantity remaining() const noexcept {
        return quantity - filled_quantity;
    }

    [[nodiscard]] bool is_filled() const noexcept {
        return filled_quantity >= quantity;
    }

    void reset() noexcept {
        id = 0;
        price = 0;
        quantity = 0;
        filled_quantity = 0;
        side = Side::Buy;
        type = OrderType::Limit;
        timestamp_ns = 0;
        prev_index = NULL_INDEX;
        next_index = NULL_INDEX;
    }
};

} // namespace engine

Why intrusive linked list fields?

Most linked lists allocate separate node objects on the heap. Intrusive lists embed the prev/next pointers directly inside the data struct. This means zero extra allocations when inserting or removing an order from a price level.

The prev_index and next_index fields store indices into the memory pool array rather than raw pointers. Index-based linking keeps the data structure relocatable and avoids pointer-chasing cache misses.

  • Save both include/types.hpp and include/order.hpp.
  • Confirm the files exist by running:
ls include/

You should see types.hpp and order.hpp listed in the output.

Don't see the files?

Make sure you created the files inside the include/ subdirectory of your order_engine project folder, not in the project root.

help me find where my header files should go

Implement the fixed-size memory pool allocator

On a real exchange, every nanosecond matters. Calling new or delete on the hot path triggers the system allocator, which can stall for hundreds of nanoseconds. A fixed-size memory pool pre-allocates a contiguous block of Order objects at startup and hands them out in O(1) using a free list.

  • Create a new file called memory_pool.hpp inside your include/ directory and paste the following code:
#pragma once

#include "types.hpp"
#include <array>
#include <cstdint>
#include <optional>

namespace engine {

template <typename T, uint32_t Capacity>
class FixedPool {
public:
    FixedPool() noexcept {
        // Initialize free list: each slot points to the next
        for (uint32_t i = 0; i < Capacity - 1; ++i) {
            free_list_[i] = i + 1;
        }
        free_list_[Capacity - 1] = NULL_INDEX;
        free_head_ = 0;
        size_ = 0;
    }

    [[nodiscard]] std::optional<uint32_t> allocate() noexcept {
        if (free_head_ == NULL_INDEX) {
            return std::nullopt;
        }
        uint32_t index = free_head_;
        free_head_ = free_list_[index];
        ++size_;
        storage_[index].pool_index = index;
        return index;
    }

    void deallocate(uint32_t index) noexcept {
        storage_[index].reset();
        free_list_[index] = free_head_;
        free_head_ = index;
        --size_;
    }

    [[nodiscard]] T& get(uint32_t index) noexcept {
        return storage_[index];
    }

    [[nodiscard]] const T& get(uint32_t index) const noexcept {
        return storage_[index];
    }

    [[nodiscard]] uint32_t size() const noexcept { return size_; }
    [[nodiscard]] uint32_t capacity() const noexcept { return Capacity; }
    [[nodiscard]] bool full() const noexcept { return size_ == Capacity; }
    [[nodiscard]] bool empty() const noexcept { return size_ == 0; }

private:
    std::array<T, Capacity> storage_{};
    std::array<uint32_t, Capacity> free_list_{};
    uint32_t free_head_{0};
    uint32_t size_{0};
};

} // namespace engine

How does the free list work?

The pool maintains two parallel arrays. storage_ holds the actual Order objects. free_list_ is a singly-linked list threaded through the same indices.

On construction, slot 0 points to slot 1, slot 1 points to slot 2, and so on. free_head_ starts at 0. Allocating pops the head in O(1). Deallocating pushes the returned index back to the front in O(1). No heap calls ever happen.

The allocate() function returns std::optional<uint32_t>. If the pool is full, it returns std::nullopt instead of throwing or returning a magic value. This makes the "pool exhausted" case explicit at every call site.

Why not use std::pmr?

C++17 introduced std::pmr::monotonic_buffer_resource for arena-style allocation. It is opaque and general-purpose. A hand-rolled fixed pool teaches you exactly how free-list allocation works, gives you full control over the deallocation pattern, and keeps the implementation visible for profiling.

  • Save include/memory_pool.hpp.

You won't see a visible change yet. The next substep adds tests that exercise the pool.

✔️ Awesome, I've got everything!

Great. Make sure all three header files are saved in include/.

ⓧ I'd like to double check the full code

Here is include/types.hpp in full:

#pragma once

#include <cstdint>
#include <chrono>

namespace engine {

using Price    = int64_t;
using Quantity = uint32_t;
using OrderId  = uint64_t;

constexpr Price INVALID_PRICE = 0;
constexpr Price MAX_PRICE = INT64_MAX;
constexpr Price MIN_PRICE = 1;
constexpr uint32_t NULL_INDEX = UINT32_MAX;

enum class Side : uint8_t {
    Buy,
    Sell
};

enum class OrderType : uint8_t {
    Limit,
    Market,
    ImmediateOrCancel,
    FillOrKill
};

struct Trade {
    OrderId buy_order_id;
    OrderId sell_order_id;
    Price price;
    Quantity quantity;
    uint64_t timestamp_ns;
};

struct OrderLocation {
    Price price;
    Side side;
    uint32_t pool_index;
};

enum class OrderResult : uint8_t {
    Accepted,
    Filled,
    PartialFill,
    Cancelled,
    Rejected
};

inline uint64_t now_ns() {
    return static_cast<uint64_t>(
        std::chrono::steady_clock::now().time_since_epoch().count()
    );
}

} // namespace engine

Here is include/order.hpp in full:

#pragma once

#include "types.hpp"

namespace engine {

struct Order {
    OrderId id;
    Price price;
    Quantity quantity;
    Quantity filled_quantity;
    Side side;
    OrderType type;
    uint64_t timestamp_ns;

    // Intrusive doubly-linked list indices (into the memory pool)
    uint32_t prev_index;
    uint32_t next_index;

    // Pool management
    uint32_t pool_index;

    [[nodiscard]] Quantity remaining() const noexcept {
        return quantity - filled_quantity;
    }

    [[nodiscard]] bool is_filled() const noexcept {
        return filled_quantity >= quantity;
    }

    void reset() noexcept {
        id = 0;
        price = 0;
        quantity = 0;
        filled_quantity = 0;
        side = Side::Buy;
        type = OrderType::Limit;
        timestamp_ns = 0;
        prev_index = NULL_INDEX;
        next_index = NULL_INDEX;
    }
};

} // namespace engine

Here is include/memory_pool.hpp in full:

#pragma once

#include "types.hpp"
#include <array>
#include <cstdint>
#include <optional>

namespace engine {

template <typename T, uint32_t Capacity>
class FixedPool {
public:
    FixedPool() noexcept {
        for (uint32_t i = 0; i < Capacity - 1; ++i) {
            free_list_[i] = i + 1;
        }
        free_list_[Capacity - 1] = NULL_INDEX;
        free_head_ = 0;
        size_ = 0;
    }

    [[nodiscard]] std::optional<uint32_t> allocate() noexcept {
        if (free_head_ == NULL_INDEX) {
            return std::nullopt;
        }
        uint32_t index = free_head_;
        free_head_ = free_list_[index];
        ++size_;
        storage_[index].pool_index = index;
        return index;
    }

    void deallocate(uint32_t index) noexcept {
        storage_[index].reset();
        free_list_[index] = free_head_;
        free_head_ = index;
        --size_;
    }

    [[nodiscard]] T& get(uint32_t index) noexcept {
        return storage_[index];
    }

    [[nodiscard]] const T& get(uint32_t index) const noexcept {
        return storage_[index];
    }

    [[nodiscard]] uint32_t size() const noexcept { return size_; }
    [[nodiscard]] uint32_t capacity() const noexcept { return Capacity; }
    [[nodiscard]] bool full() const noexcept { return size_ == Capacity; }
    [[nodiscard]] bool empty() const noexcept { return size_ == 0; }

private:
    std::array<T, Capacity> storage_{};
    std::array<uint32_t, Capacity> free_list_{};
    uint32_t free_head_{0};
    uint32_t size_{0};
};

} // namespace engine
Write unit tests for the memory pool

You need to verify three things: basic allocate/deallocate works, the pool correctly rejects when exhausted, and data stored in allocated slots survives reads intact.

  • Open tests/test_memory_pool.cpp and replace any existing content with the following test code:
#include <gtest/gtest.h>
#include "memory_pool.hpp"
#include "order.hpp"

using namespace engine;

TEST(MemoryPoolTest, AllocateAndDeallocate) {
    FixedPool<Order, 100> pool;
    EXPECT_EQ(pool.size(), 0u);

    auto idx = pool.allocate();
    ASSERT_TRUE(idx.has_value());
    EXPECT_EQ(pool.size(), 1u);

    pool.deallocate(idx.value());
    EXPECT_EQ(pool.size(), 0u);
}

TEST(MemoryPoolTest, PoolExhaustion) {
    FixedPool<Order, 3> pool;

    auto a = pool.allocate();
    auto b = pool.allocate();
    auto c = pool.allocate();
    ASSERT_TRUE(a.has_value());
    ASSERT_TRUE(b.has_value());
    ASSERT_TRUE(c.has_value());

    auto d = pool.allocate();
    EXPECT_FALSE(d.has_value()); // Pool full

    pool.deallocate(b.value());
    auto e = pool.allocate();
    EXPECT_TRUE(e.has_value()); // Reuse freed slot
}

TEST(MemoryPoolTest, OrderDataIntegrity) {
    FixedPool<Order, 10> pool;
    auto idx = pool.allocate();
    ASSERT_TRUE(idx.has_value());

    Order& order = pool.get(idx.value());
    order.id = 42;
    order.price = 10000;
    order.quantity = 50;

    EXPECT_EQ(pool.get(idx.value()).id, 42u);
    EXPECT_EQ(pool.get(idx.value()).price, 10000);
    EXPECT_EQ(pool.get(idx.value()).quantity, 50u);
}

What do these tests verify?

  • AllocateAndDeallocate confirms the pool starts empty, grows by one on allocate, and shrinks back to zero on deallocate.
  • PoolExhaustion fills a tiny 3-slot pool, confirms a 4th allocation returns std::nullopt, then deallocates one slot and confirms reuse.
  • OrderDataIntegrity writes data into an allocated Order and reads it back, proving the pool does not corrupt stored values.

Before building, the CMakeLists.txt references other test and source files that do not have real implementations yet. You need stub files so the build succeeds.

  • Create empty stub files for the remaining source and test files by running:
# Stub source files (empty .cpp files satisfy the linker)
touch src/order_book.cpp src/matching_engine.cpp src/order_generator.cpp src/feed_handler.cpp src/strategy.cpp src/metrics.cpp

# Stub test files
touch tests/test_order_book.cpp tests/test_matching_engine.cpp tests/test_spsc_queue.cpp

# Stub benchmark file
touch bench/bench_main.cpp

Why create stub files?

Your CMakeLists.txt already lists all source files for the library, test, and benchmark targets. CMake will fail if any listed file is missing. Empty .cpp files satisfy the compiler. You will replace them with real implementations in later steps.

  • Save tests/test_memory_pool.cpp.
  • Rebuild the project and run your tests by running:
cmake --build build
ctest --test-dir build --output-on-failure

You should see output showing 3 tests passed with no failures.

Seeing build errors?

  • If you see "file not found" for a header include, confirm your three header files are in include/ and not in src/ or the project root.
  • If you see linker errors about missing source files, make sure you ran the touch commands above to create the stubs.
  • If ctest reports 0 tests found, try running cmake -S . -B build again to re-configure before building.

help me fix my memory pool build errors

✔️ Awesome, I've got everything!

Great. Make sure all tests pass before moving on.

ⓧ I'd like to double check the full code

Here is tests/test_memory_pool.cpp in full:

#include <gtest/gtest.h>
#include "memory_pool.hpp"
#include "order.hpp"

using namespace engine;

TEST(MemoryPoolTest, AllocateAndDeallocate) {
    FixedPool<Order, 100> pool;
    EXPECT_EQ(pool.size(), 0u);

    auto idx = pool.allocate();
    ASSERT_TRUE(idx.has_value());
    EXPECT_EQ(pool.size(), 1u);

    pool.deallocate(idx.value());
    EXPECT_EQ(pool.size(), 0u);
}

TEST(MemoryPoolTest, PoolExhaustion) {
    FixedPool<Order, 3> pool;

    auto a = pool.allocate();
    auto b = pool.allocate();
    auto c = pool.allocate();
    ASSERT_TRUE(a.has_value());
    ASSERT_TRUE(b.has_value());
    ASSERT_TRUE(c.has_value());

    auto d = pool.allocate();
    EXPECT_FALSE(d.has_value());

    pool.deallocate(b.value());
    auto e = pool.allocate();
    EXPECT_TRUE(e.has_value());
}

TEST(MemoryPoolTest, OrderDataIntegrity) {
    FixedPool<Order, 10> pool;
    auto idx = pool.allocate();
    ASSERT_TRUE(idx.has_value());

    Order& order = pool.get(idx.value());
    order.id = 42;
    order.price = 10000;
    order.quantity = 50;

    EXPECT_EQ(pool.get(idx.value()).id, 42u);
    EXPECT_EQ(pool.get(idx.value()).price, 10000);
    EXPECT_EQ(pool.get(idx.value()).quantity, 50u);
}

Your types are defined and your memory pool is proven correct. Next up, you will use this pool to build a price-level linked list and the full order book.

Build the Order Book

Your memory pool handles the raw storage of orders. Now you need the data structure that organizes those orders by price, so the matching engine can always find the best available price instantly.

An order book groups orders into price levels, with each level maintaining a FIFO queue of orders at that price. You will build two layers: a PriceLevel struct that manages the linked list of orders at a single price, and an OrderBook class that maps prices to levels and provides O(1) cancel via a hash map.

In this step, get ready to:
  • Implement PriceLevel as an intrusive doubly-linked list with O(1) append and remove.
  • Build the OrderBook class with sorted bid/ask sides and O(1) cancel lookup.
  • Write unit tests validating price ordering, cancellation, and mid-price computation.
Implement the PriceLevel struct

A price level represents all orders resting at a single price. Orders within a level follow time priority: the first order placed gets filled first. You will implement this as an intrusive doubly-linked list using pool indices instead of pointers.

Why intrusive linked lists?

A normal linked list allocates a separate node for each element. An intrusive list stores the prev/next links inside the element itself (the Order struct). This eliminates an extra allocation per order and keeps the data cache-friendly.

Since your orders already live in the memory pool and have prev_index and next_index fields, you can link them without any heap allocation.

  • Create a new file include/price_level.hpp and add the following code:
#pragma once

#include "types.hpp"
#include "order.hpp"
#include "memory_pool.hpp"

namespace engine {

struct PriceLevel {
    Price price{0};
    Quantity total_quantity{0};
    uint32_t order_count{0};
    uint32_t head_index{NULL_INDEX};
    uint32_t tail_index{NULL_INDEX};

    // Append an order to the back of this level's queue
    template <uint32_t Capacity>
    void append(uint32_t order_index, FixedPool<Order, Capacity>& pool) noexcept {
        Order& order = pool.get(order_index);
        order.prev_index = tail_index;
        order.next_index = NULL_INDEX;

        if (tail_index != NULL_INDEX) {
            pool.get(tail_index).next_index = order_index;
        } else {
            head_index = order_index;
        }
        tail_index = order_index;
        total_quantity += order.remaining();
        ++order_count;
    }

    // Remove an order from anywhere in this level's queue
    template <uint32_t Capacity>
    void remove(uint32_t order_index, FixedPool<Order, Capacity>& pool) noexcept {
        Order& order = pool.get(order_index);
        total_quantity -= order.remaining();
        --order_count;

        if (order.prev_index != NULL_INDEX) {
            pool.get(order.prev_index).next_index = order.next_index;
        } else {
            head_index = order.next_index;
        }

        if (order.next_index != NULL_INDEX) {
            pool.get(order.next_index).prev_index = order.prev_index;
        } else {
            tail_index = order.prev_index;
        }

        order.prev_index = NULL_INDEX;
        order.next_index = NULL_INDEX;
    }

    [[nodiscard]] bool is_empty() const noexcept {
        return order_count == 0;
    }
};

} // namespace engine

What does this code do?

  • PriceLevel tracks the total quantity and count of orders at one price, plus head/tail indices into the memory pool.
  • append() links a new order to the tail. If the list is empty, the new order becomes both head and tail.
  • remove() unlinks any order from the list by patching its neighbors' pointers. This gives you O(1) removal from any position.
  • The template parameter lets the same PriceLevel work with any pool capacity, avoiding hardcoding the pool size.
  • Save include/price_level.hpp.

Seeing a compile error about incomplete types?

Make sure your includes are in the correct order: types.hpp first, then order.hpp, then memory_pool.hpp. The PriceLevel needs to see the full definition of Order and FixedPool.

Help me debug my price_level.hpp include order

Build the OrderBook class

The OrderBook organizes price levels into two sorted sides: bids (highest price first) and asks (lowest price first). It also maintains a hash map from order ID to location, enabling O(1) cancel lookup regardless of how many orders are on the book.

  • Open include/order_book.hpp and replace its contents with the following class declaration:
#pragma once

#include "types.hpp"
#include "order.hpp"
#include "memory_pool.hpp"
#include "price_level.hpp"
#include <map>
#include <unordered_map>
#include <functional>
#include <optional>
#include <vector>

namespace engine {

constexpr uint32_t MAX_ORDERS = 1'000'000;

class OrderBook {
public:
    OrderBook() = default;

    // Add an order to the book, returns pool index on success
    [[nodiscard]] std::optional<uint32_t> add_order(OrderId id, Price price,
                                                      Quantity qty, Side side,
                                                      OrderType type);

    // Cancel an order by ID, returns true if found and removed
    bool cancel_order(OrderId id);

    // Access best prices
    [[nodiscard]] std::optional<Price> best_bid() const noexcept;
    [[nodiscard]] std::optional<Price> best_ask() const noexcept;
    [[nodiscard]] std::optional<Price> mid_price() const noexcept;

    // Access order by ID
    [[nodiscard]] const Order* find_order(OrderId id) const noexcept;

    // Get top of book (first order at best price)
    [[nodiscard]] Order* front_bid() noexcept;
    [[nodiscard]] Order* front_ask() noexcept;

    // Remove filled order from book
    void remove_order(uint32_t pool_index, Side side, Price price);

    // Check available quantity at or better than price
    [[nodiscard]] Quantity available_quantity(Side aggressor_side, Price limit_price) const noexcept;

    // Accessors
    [[nodiscard]] uint32_t order_count() const noexcept { return pool_.size(); }
    [[nodiscard]] const auto& bids() const noexcept { return bids_; }
    [[nodiscard]] const auto& asks() const noexcept { return asks_; }
    [[nodiscard]] FixedPool<Order, MAX_ORDERS>& pool() noexcept { return pool_; }

private:
    std::map<Price, PriceLevel, std::greater<>> bids_; // highest first
    std::map<Price, PriceLevel> asks_;                  // lowest first
    std::unordered_map<OrderId, OrderLocation> order_index_;
    FixedPool<Order, MAX_ORDERS> pool_;
};

} // namespace engine

What does this code do?

  • bids_ uses std::greater<> so the map sorts highest price first. The best bid is always at bids_.begin().
  • asks_ uses the default ascending comparator. The best ask (lowest price) is always at asks_.begin().
  • order_index_ maps every order ID to its location (price, side, pool index). This enables O(1) cancel without scanning the book.
  • MAX_ORDERS sets the pool capacity to 1 million orders. This pre-allocates all memory upfront so the hot path never calls malloc.
  • Save include/order_book.hpp.

Now implement the order management methods. These handle adding orders to the book and removing them via cancel.

  • Open src/order_book.cpp and replace its contents with the order lifecycle methods:
#include "order_book.hpp"

namespace engine {

std::optional<uint32_t> OrderBook::add_order(OrderId id, Price price,
                                              Quantity qty, Side side,
                                              OrderType type) {
    // Allocate a slot from the pool
    auto slot = pool_.allocate();
    if (!slot.has_value()) return std::nullopt;

    // Initialize the order fields
    uint32_t idx = slot.value();
    Order& order = pool_.get(idx);
    order.id = id;
    order.price = price;
    order.quantity = qty;
    order.filled_quantity = 0;
    order.side = side;
    order.type = type;
    order.timestamp_ns = now_ns();
    order.prev_index = NULL_INDEX;
    order.next_index = NULL_INDEX;

    // Place on the correct side of the book
    if (side == Side::Buy) {
        bids_[price].price = price;
        bids_[price].append(idx, pool_);
    } else {
        asks_[price].price = price;
        asks_[price].append(idx, pool_);
    }

    // Record location for O(1) cancel lookup
    order_index_[id] = OrderLocation{price, side, idx};
    return idx;
}

bool OrderBook::cancel_order(OrderId id) {
    // Look up the order's location
    auto it = order_index_.find(id);
    if (it == order_index_.end()) return false;

    auto [price, side, pool_index] = it->second;
    remove_order(pool_index, side, price);
    order_index_.erase(it);
    return true;
}

void OrderBook::remove_order(uint32_t pool_index, Side side, Price price) {
    // Unlink from the appropriate price level
    if (side == Side::Buy) {
        auto it = bids_.find(price);
        if (it != bids_.end()) {
            it->second.remove(pool_index, pool_);
            if (it->second.is_empty()) {
                bids_.erase(it);
            }
        }
    } else {
        auto it = asks_.find(price);
        if (it != asks_.end()) {
            it->second.remove(pool_index, pool_);
            if (it->second.is_empty()) {
                asks_.erase(it);
            }
        }
    }

    // Remove from order index and return slot to pool
    Order& order = pool_.get(pool_index);
    order_index_.erase(order.id);
    pool_.deallocate(pool_index);
}

What does this code do?

  • add_order() allocates a pool slot, initializes the order, appends it to the correct price level, and records its location in the hash map.
  • cancel_order() looks up the order in the hash map (O(1)), then delegates to remove_order() for the actual unlinking.
  • remove_order() unlinks the order from its price level, cleans up empty levels (erasing from the map), and returns the pool slot for reuse.

Next, add the query methods below the remove_order function, still inside the namespace engine block.

  • Add the following query methods to the bottom of src/order_book.cpp (before the closing } // namespace engine):
std::optional<Price> OrderBook::best_bid() const noexcept {
    if (bids_.empty()) return std::nullopt;
    return bids_.begin()->first;
}

std::optional<Price> OrderBook::best_ask() const noexcept {
    if (asks_.empty()) return std::nullopt;
    return asks_.begin()->first;
}

std::optional<Price> OrderBook::mid_price() const noexcept {
    auto bb = best_bid();
    auto ba = best_ask();
    if (!bb.has_value() || !ba.has_value()) return std::nullopt;
    return (bb.value() + ba.value()) / 2;
}

const Order* OrderBook::find_order(OrderId id) const noexcept {
    auto it = order_index_.find(id);
    if (it == order_index_.end()) return nullptr;
    return &pool_.get(it->second.pool_index);
}

Order* OrderBook::front_bid() noexcept {
    if (bids_.empty()) return nullptr;
    auto& level = bids_.begin()->second;
    if (level.head_index == NULL_INDEX) return nullptr;
    return &pool_.get(level.head_index);
}

Order* OrderBook::front_ask() noexcept {
    if (asks_.empty()) return nullptr;
    auto& level = asks_.begin()->second;
    if (level.head_index == NULL_INDEX) return nullptr;
    return &pool_.get(level.head_index);
}

Quantity OrderBook::available_quantity(Side aggressor_side, Price limit_price) const noexcept {
    Quantity total = 0;
    if (aggressor_side == Side::Buy) {
        // Check asks at or below limit_price
        for (auto& [price, level] : asks_) {
            if (price > limit_price) break;
            total += level.total_quantity;
        }
    } else {
        // Check bids at or above limit_price
        for (auto& [price, level] : bids_) {
            if (price < limit_price) break;
            total += level.total_quantity;
        }
    }
    return total;
}

What does this code do?

  • best_bid() and best_ask() return the top-of-book price from each side. Because the maps are sorted, the best price is always at begin().
  • mid_price() computes the average of best bid and best ask. This is what trading strategies use to gauge fair value.
  • front_bid() and front_ask() return the actual first Order at the best price. The matching engine uses these to fill incoming aggressive orders.
  • available_quantity() sums liquidity at or better than a given price. The Fill-or-Kill order type will use this to pre-check if enough volume exists before attempting to fill.
  • Save src/order_book.cpp.

Seeing errors about pool_.get() in a const method?

The find_order() method is marked const, so it can only call const member functions. Make sure your FixedPool has a const overload of get() (your pool from Step 2 already has this).

Help me fix const-correctness in my OrderBook

Write and run unit tests

You need tests that verify the book correctly sorts bids and asks, supports cancellation, and computes the mid-price.

  • Open tests/test_order_book.cpp and replace its contents with these tests:
#include <gtest/gtest.h>
#include "order_book.hpp"

using namespace engine;

TEST(OrderBookTest, AddAndBestPrices) {
    OrderBook book;

    book.add_order(1, 100, 10, Side::Buy, OrderType::Limit);
    book.add_order(2, 105, 10, Side::Buy, OrderType::Limit);
    book.add_order(3, 110, 10, Side::Sell, OrderType::Limit);
    book.add_order(4, 115, 10, Side::Sell, OrderType::Limit);

    // Best bid is highest buy, best ask is lowest sell
    EXPECT_EQ(book.best_bid().value(), 105);
    EXPECT_EQ(book.best_ask().value(), 110);
}

TEST(OrderBookTest, CancelOrder) {
    OrderBook book;

    book.add_order(1, 100, 10, Side::Buy, OrderType::Limit);
    book.add_order(2, 100, 20, Side::Buy, OrderType::Limit);

    // Cancel first order, one remains
    EXPECT_TRUE(book.cancel_order(1));
    EXPECT_EQ(book.order_count(), 1u);
    EXPECT_FALSE(book.cancel_order(99)); // Non-existent
}

TEST(OrderBookTest, MidPrice) {
    OrderBook book;

    book.add_order(1, 100, 10, Side::Buy, OrderType::Limit);
    book.add_order(2, 110, 10, Side::Sell, OrderType::Limit);

    // Mid-price is average of best bid and best ask
    EXPECT_EQ(book.mid_price().value(), 105);
}

TEST(OrderBookTest, EmptyBookReturnsNullopt) {
    OrderBook book;
    EXPECT_FALSE(book.best_bid().has_value());
    EXPECT_FALSE(book.best_ask().has_value());
    EXPECT_FALSE(book.mid_price().has_value());
}

What does this code do?

  • AddAndBestPrices verifies that bids sort highest-first and asks sort lowest-first.
  • CancelOrder confirms that cancelling reduces order count and that cancelling a non-existent ID returns false.
  • MidPrice checks the average of best bid and best ask.
  • EmptyBookReturnsNullopt ensures the book handles the empty state gracefully without crashing.
  • Save tests/test_order_book.cpp.
  • Build and run your tests by running these commands:
cmake --build build
ctest --test-dir build --output-on-failure

You should see all OrderBook tests pass alongside your existing memory pool tests.

Tests failing or not compiling?

If you see linker errors about undefined references to OrderBook methods, make sure your src/order_book.cpp is saved and includes both the order management and query methods inside the namespace engine {} block.

If the CancelOrder test fails, verify that cancel_order() both removes the order from its price level AND calls pool_.deallocate() so that order_count() (which reads pool size) decreases.

Help me debug my order book test failures

✔️ Awesome, I've got everything!

Great. Double check you have saved all three new files before moving on.

ⓧ I'd like to double check the full code

Here is the complete code for each file at this point in the project.

#pragma once

#include "types.hpp"
#include "order.hpp"
#include "memory_pool.hpp"

namespace engine {

struct PriceLevel {
    Price price{0};
    Quantity total_quantity{0};
    uint32_t order_count{0};
    uint32_t head_index{NULL_INDEX};
    uint32_t tail_index{NULL_INDEX};

    template <uint32_t Capacity>
    void append(uint32_t order_index, FixedPool<Order, Capacity>& pool) noexcept {
        Order& order = pool.get(order_index);
        order.prev_index = tail_index;
        order.next_index = NULL_INDEX;

        if (tail_index != NULL_INDEX) {
            pool.get(tail_index).next_index = order_index;
        } else {
            head_index = order_index;
        }
        tail_index = order_index;
        total_quantity += order.remaining();
        ++order_count;
    }

    template <uint32_t Capacity>
    void remove(uint32_t order_index, FixedPool<Order, Capacity>& pool) noexcept {
        Order& order = pool.get(order_index);
        total_quantity -= order.remaining();
        --order_count;

        if (order.prev_index != NULL_INDEX) {
            pool.get(order.prev_index).next_index = order.next_index;
        } else {
            head_index = order.next_index;
        }

        if (order.next_index != NULL_INDEX) {
            pool.get(order.next_index).prev_index = order.prev_index;
        } else {
            tail_index = order.prev_index;
        }

        order.prev_index = NULL_INDEX;
        order.next_index = NULL_INDEX;
    }

    [[nodiscard]] bool is_empty() const noexcept {
        return order_count == 0;
    }
};

} // namespace engine
#pragma once

#include "types.hpp"
#include "order.hpp"
#include "memory_pool.hpp"
#include "price_level.hpp"
#include <map>
#include <unordered_map>
#include <functional>
#include <optional>
#include <vector>

namespace engine {

constexpr uint32_t MAX_ORDERS = 1'000'000;

class OrderBook {
public:
    OrderBook() = default;

    [[nodiscard]] std::optional<uint32_t> add_order(OrderId id, Price price,
                                                      Quantity qty, Side side,
                                                      OrderType type);

    bool cancel_order(OrderId id);

    [[nodiscard]] std::optional<Price> best_bid() const noexcept;
    [[nodiscard]] std::optional<Price> best_ask() const noexcept;
    [[nodiscard]] std::optional<Price> mid_price() const noexcept;

    [[nodiscard]] const Order* find_order(OrderId id) const noexcept;

    [[nodiscard]] Order* front_bid() noexcept;
    [[nodiscard]] Order* front_ask() noexcept;

    void remove_order(uint32_t pool_index, Side side, Price price);

    [[nodiscard]] Quantity available_quantity(Side aggressor_side, Price limit_price) const noexcept;

    [[nodiscard]] uint32_t order_count() const noexcept { return pool_.size(); }
    [[nodiscard]] const auto& bids() const noexcept { return bids_; }
    [[nodiscard]] const auto& asks() const noexcept { return asks_; }
    [[nodiscard]] FixedPool<Order, MAX_ORDERS>& pool() noexcept { return pool_; }

private:
    std::map<Price, PriceLevel, std::greater<>> bids_;
    std::map<Price, PriceLevel> asks_;
    std::unordered_map<OrderId, OrderLocation> order_index_;
    FixedPool<Order, MAX_ORDERS> pool_;
};

} // namespace engine
#include "order_book.hpp"

namespace engine {

std::optional<uint32_t> OrderBook::add_order(OrderId id, Price price,
                                              Quantity qty, Side side,
                                              OrderType type) {
    auto slot = pool_.allocate();
    if (!slot.has_value()) return std::nullopt;

    uint32_t idx = slot.value();
    Order& order = pool_.get(idx);
    order.id = id;
    order.price = price;
    order.quantity = qty;
    order.filled_quantity = 0;
    order.side = side;
    order.type = type;
    order.timestamp_ns = now_ns();
    order.prev_index = NULL_INDEX;
    order.next_index = NULL_INDEX;

    if (side == Side::Buy) {
        bids_[price].price = price;
        bids_[price].append(idx, pool_);
    } else {
        asks_[price].price = price;
        asks_[price].append(idx, pool_);
    }

    order_index_[id] = OrderLocation{price, side, idx};
    return idx;
}

bool OrderBook::cancel_order(OrderId id) {
    auto it = order_index_.find(id);
    if (it == order_index_.end()) return false;

    auto [price, side, pool_index] = it->second;
    remove_order(pool_index, side, price);
    order_index_.erase(it);
    return true;
}

void OrderBook::remove_order(uint32_t pool_index, Side side, Price price) {
    if (side == Side::Buy) {
        auto it = bids_.find(price);
        if (it != bids_.end()) {
            it->second.remove(pool_index, pool_);
            if (it->second.is_empty()) {
                bids_.erase(it);
            }
        }
    } else {
        auto it = asks_.find(price);
        if (it != asks_.end()) {
            it->second.remove(pool_index, pool_);
            if (it->second.is_empty()) {
                asks_.erase(it);
            }
        }
    }

    Order& order = pool_.get(pool_index);
    order_index_.erase(order.id);
    pool_.deallocate(pool_index);
}

std::optional<Price> OrderBook::best_bid() const noexcept {
    if (bids_.empty()) return std::nullopt;
    return bids_.begin()->first;
}

std::optional<Price> OrderBook::best_ask() const noexcept {
    if (asks_.empty()) return std::nullopt;
    return asks_.begin()->first;
}

std::optional<Price> OrderBook::mid_price() const noexcept {
    auto bb = best_bid();
    auto ba = best_ask();
    if (!bb.has_value() || !ba.has_value()) return std::nullopt;
    return (bb.value() + ba.value()) / 2;
}

const Order* OrderBook::find_order(OrderId id) const noexcept {
    auto it = order_index_.find(id);
    if (it == order_index_.end()) return nullptr;
    return &pool_.get(it->second.pool_index);
}

Order* OrderBook::front_bid() noexcept {
    if (bids_.empty()) return nullptr;
    auto& level = bids_.begin()->second;
    if (level.head_index == NULL_INDEX) return nullptr;
    return &pool_.get(level.head_index);
}

Order* OrderBook::front_ask() noexcept {
    if (asks_.empty()) return nullptr;
    auto& level = asks_.begin()->second;
    if (level.head_index == NULL_INDEX) return nullptr;
    return &pool_.get(level.head_index);
}

Quantity OrderBook::available_quantity(Side aggressor_side, Price limit_price) const noexcept {
    Quantity total = 0;
    if (aggressor_side == Side::Buy) {
        for (auto& [price, level] : asks_) {
            if (price > limit_price) break;
            total += level.total_quantity;
        }
    } else {
        for (auto& [price, level] : bids_) {
            if (price < limit_price) break;
            total += level.total_quantity;
        }
    }
    return total;
}

} // namespace engine
#include <gtest/gtest.h>
#include "order_book.hpp"

using namespace engine;

TEST(OrderBookTest, AddAndBestPrices) {
    OrderBook book;

    book.add_order(1, 100, 10, Side::Buy, OrderType::Limit);
    book.add_order(2, 105, 10, Side::Buy, OrderType::Limit);
    book.add_order(3, 110, 10, Side::Sell, OrderType::Limit);
    book.add_order(4, 115, 10, Side::Sell, OrderType::Limit);

    EXPECT_EQ(book.best_bid().value(), 105);
    EXPECT_EQ(book.best_ask().value(), 110);
}

TEST(OrderBookTest, CancelOrder) {
    OrderBook book;

    book.add_order(1, 100, 10, Side::Buy, OrderType::Limit);
    book.add_order(2, 100, 20, Side::Buy, OrderType::Limit);

    EXPECT_TRUE(book.cancel_order(1));
    EXPECT_EQ(book.order_count(), 1u);
    EXPECT_FALSE(book.cancel_order(99));
}

TEST(OrderBookTest, MidPrice) {
    OrderBook book;

    book.add_order(1, 100, 10, Side::Buy, OrderType::Limit);
    book.add_order(2, 110, 10, Side::Sell, OrderType::Limit);

    EXPECT_EQ(book.mid_price().value(), 105);
}

TEST(OrderBookTest, EmptyBookReturnsNullopt) {
    OrderBook book;
    EXPECT_FALSE(book.best_bid().has_value());
    EXPECT_FALSE(book.best_ask().has_value());
    EXPECT_FALSE(book.mid_price().has_value());
}

Your order book is built and tested. Next up, you will implement the matching engine that sweeps through these price levels to fill incoming orders and emit trades.

Implement the Matching Engine

Your order book can store and cancel orders. Now it needs a brain: the matching engine that pairs buyers with sellers at the best available price.

The matching engine is the core algorithm that runs every exchange in the world. When a buy order arrives, it sweeps the ask side from the lowest price upward. When a sell order arrives, it sweeps the bid side from the highest price downward. Each time quantities overlap, a trade is emitted.

In this step, get ready to:
  • Define the matching engine interface with trade callbacks and order request types.
  • Implement the price-time priority matching loop supporting Limit, Market, IOC, and FOK orders.
  • Write unit tests covering full fills, partial fills, multi-level sweeps, and order rejection.
Define the matching engine interface

The matching engine wraps the order book and adds the matching logic. It accepts incoming orders via a simple struct and notifies listeners of trades via a callback.

  • Create a new file matching_engine.hpp inside your include/ directory.
  • Paste the following code into include/matching_engine.hpp:
#pragma once

#include "order_book.hpp"
#include "types.hpp"
#include <vector>
#include <functional>

namespace engine {

struct OrderRequest {
    OrderId id;
    Price price;
    Quantity quantity;
    Side side;
    OrderType type;
};

class MatchingEngine {
public:
    using TradeCallback = std::function<void(const Trade&)>;

    MatchingEngine() = default;

    void set_trade_callback(TradeCallback cb) { trade_callback_ = std::move(cb); }

    OrderResult submit(const OrderRequest& request);
    bool cancel(OrderId id);

    [[nodiscard]] const OrderBook& book() const noexcept { return book_; }
    [[nodiscard]] OrderBook& book() noexcept { return book_; }
    [[nodiscard]] uint64_t trade_count() const noexcept { return trade_count_; }

private:
    OrderResult match_order(OrderId id, Price price, Quantity quantity,
                            Side side, OrderType type);
    void emit_trade(OrderId buy_id, OrderId sell_id, Price price, Quantity qty);

    OrderBook book_;
    TradeCallback trade_callback_;
    uint64_t trade_count_{0};
};

} // namespace engine

What does this header define?

OrderRequest is a lightweight struct holding everything needed to submit an order: ID, price, quantity, side, and type.

TradeCallback is a std::function that fires every time a trade executes. External code (benchmarks, strategies, loggers) registers a callback via set_trade_callback() without modifying the engine itself.

The private method match_order() contains the core sweep logic. emit_trade() increments the trade counter and fires the callback.

  • Save include/matching_engine.hpp.
Implement the price-time priority matching loop

The matching algorithm supports four order types. Limit orders sweep what they can and rest the residual. Market orders use an extreme price to guarantee aggressive matching. Immediate-or-Cancel fills what it can and discards the rest. Fill-or-Kill pre-checks liquidity and rejects the entire order if there is not enough.

  • Create a new file matching_engine.cpp inside your src/ directory.
  • Paste the following implementation into src/matching_engine.cpp:
#include "matching_engine.hpp"

namespace engine {

OrderResult MatchingEngine::submit(const OrderRequest& request) {
    return match_order(request.id, request.price, request.quantity,
                       request.side, request.type);
}

bool MatchingEngine::cancel(OrderId id) {
    return book_.cancel_order(id);
}

OrderResult MatchingEngine::match_order(OrderId id, Price price, Quantity quantity,
                                         Side side, OrderType type) {
    // For Market orders, use extreme price to guarantee matching
    Price effective_price = price;
    if (type == OrderType::Market) {
        effective_price = (side == Side::Buy) ? MAX_PRICE : MIN_PRICE;
    }

    // Fill-or-Kill: pre-check liquidity
    if (type == OrderType::FillOrKill) {
        Quantity available = book_.available_quantity(side, effective_price);
        if (available < quantity) {
            return OrderResult::Rejected;
        }
    }

    Quantity remaining = quantity;

    if (side == Side::Buy) {
        // Match against asks (lowest first)
        while (remaining > 0) {
            Order* best = book_.front_ask();
            if (!best || best->price > effective_price) break;

            Quantity fill_qty = std::min(remaining, best->remaining());
            best->filled_quantity += fill_qty;
            remaining -= fill_qty;

            emit_trade(id, best->id, best->price, fill_qty);

            if (best->is_filled()) {
                Price p = best->price;
                uint32_t idx = best->pool_index;
                book_.remove_order(idx, Side::Sell, p);
            }
        }
    } else {
        // Match against bids (highest first)
        while (remaining > 0) {
            Order* best = book_.front_bid();
            if (!best || best->price < effective_price) break;

            Quantity fill_qty = std::min(remaining, best->remaining());
            best->filled_quantity += fill_qty;
            remaining -= fill_qty;

            emit_trade(best->id, id, best->price, fill_qty);

            if (best->is_filled()) {
                Price p = best->price;
                uint32_t idx = best->pool_index;
                book_.remove_order(idx, Side::Buy, p);
            }
        }
    }

    // Handle residual
    Quantity filled = quantity - remaining;

    if (remaining > 0) {
        if (type == OrderType::ImmediateOrCancel || type == OrderType::Market) {
            // Cancel residual
            return (filled > 0) ? OrderResult::PartialFill : OrderResult::Rejected;
        }

        if (type == OrderType::Limit) {
            // Rest on book
            book_.add_order(id, price, remaining, side, type);
            return (filled > 0) ? OrderResult::PartialFill : OrderResult::Accepted;
        }
    }

    return OrderResult::Filled;
}

void MatchingEngine::emit_trade(OrderId buy_id, OrderId sell_id,
                                 Price price, Quantity qty) {
    ++trade_count_;
    if (trade_callback_) {
        Trade trade{buy_id, sell_id, price, qty, now_ns()};
        trade_callback_(trade);
    }
}

} // namespace engine

What does match_order do?

  • Market orders set effective_price to MAX_PRICE (buy) or MIN_PRICE (sell) so the sweep loop never stops on price.
  • Fill-or-Kill calls available_quantity() upfront. If the book cannot satisfy the full order, it returns Rejected without touching any resting orders.
  • The sweep loop grabs the best resting order (front_ask() or front_bid()), fills the minimum of remaining quantities, emits a trade, and removes fully filled orders from the book.
  • After the loop, IOC and Market discard any unfilled remainder. Limit orders rest the residual on the book for future matching.

Why does the buy loop emit trade(id, best->id) but the sell loop emits trade(best->id, id)?

The Trade struct stores buy_order_id first and sell_order_id second. When the aggressor is a buyer, id is the buy side and best->id is the resting sell. When the aggressor is a seller, the roles flip.

  • Save src/matching_engine.cpp.

Seeing a compile error about std::min?

Make sure matching_engine.hpp includes order_book.hpp. That header chain pulls in <algorithm> transitively through the STL containers.

help me fix a compile error in matching_engine.cpp

✔️ Awesome, I've got everything!

Great. Double check you've saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the full include/matching_engine.hpp:

#pragma once

#include "order_book.hpp"
#include "types.hpp"
#include <vector>
#include <functional>

namespace engine {

struct OrderRequest {
    OrderId id;
    Price price;
    Quantity quantity;
    Side side;
    OrderType type;
};

class MatchingEngine {
public:
    using TradeCallback = std::function<void(const Trade&)>;

    MatchingEngine() = default;

    void set_trade_callback(TradeCallback cb) { trade_callback_ = std::move(cb); }

    OrderResult submit(const OrderRequest& request);
    bool cancel(OrderId id);

    [[nodiscard]] const OrderBook& book() const noexcept { return book_; }
    [[nodiscard]] OrderBook& book() noexcept { return book_; }
    [[nodiscard]] uint64_t trade_count() const noexcept { return trade_count_; }

private:
    OrderResult match_order(OrderId id, Price price, Quantity quantity,
                            Side side, OrderType type);
    void emit_trade(OrderId buy_id, OrderId sell_id, Price price, Quantity qty);

    OrderBook book_;
    TradeCallback trade_callback_;
    uint64_t trade_count_{0};
};

} // namespace engine

And the full src/matching_engine.cpp:

#include "matching_engine.hpp"

namespace engine {

OrderResult MatchingEngine::submit(const OrderRequest& request) {
    return match_order(request.id, request.price, request.quantity,
                       request.side, request.type);
}

bool MatchingEngine::cancel(OrderId id) {
    return book_.cancel_order(id);
}

OrderResult MatchingEngine::match_order(OrderId id, Price price, Quantity quantity,
                                         Side side, OrderType type) {
    // For Market orders, use extreme price to guarantee matching
    Price effective_price = price;
    if (type == OrderType::Market) {
        effective_price = (side == Side::Buy) ? MAX_PRICE : MIN_PRICE;
    }

    // Fill-or-Kill: pre-check liquidity
    if (type == OrderType::FillOrKill) {
        Quantity available = book_.available_quantity(side, effective_price);
        if (available < quantity) {
            return OrderResult::Rejected;
        }
    }

    Quantity remaining = quantity;

    if (side == Side::Buy) {
        // Match against asks (lowest first)
        while (remaining > 0) {
            Order* best = book_.front_ask();
            if (!best || best->price > effective_price) break;

            Quantity fill_qty = std::min(remaining, best->remaining());
            best->filled_quantity += fill_qty;
            remaining -= fill_qty;

            emit_trade(id, best->id, best->price, fill_qty);

            if (best->is_filled()) {
                Price p = best->price;
                uint32_t idx = best->pool_index;
                book_.remove_order(idx, Side::Sell, p);
            }
        }
    } else {
        // Match against bids (highest first)
        while (remaining > 0) {
            Order* best = book_.front_bid();
            if (!best || best->price < effective_price) break;

            Quantity fill_qty = std::min(remaining, best->remaining());
            best->filled_quantity += fill_qty;
            remaining -= fill_qty;

            emit_trade(best->id, id, best->price, fill_qty);

            if (best->is_filled()) {
                Price p = best->price;
                uint32_t idx = best->pool_index;
                book_.remove_order(idx, Side::Buy, p);
            }
        }
    }

    // Handle residual
    Quantity filled = quantity - remaining;

    if (remaining > 0) {
        if (type == OrderType::ImmediateOrCancel || type == OrderType::Market) {
            // Cancel residual
            return (filled > 0) ? OrderResult::PartialFill : OrderResult::Rejected;
        }

        if (type == OrderType::Limit) {
            // Rest on book
            book_.add_order(id, price, remaining, side, type);
            return (filled > 0) ? OrderResult::PartialFill : OrderResult::Accepted;
        }
    }

    return OrderResult::Filled;
}

void MatchingEngine::emit_trade(OrderId buy_id, OrderId sell_id,
                                 Price price, Quantity qty) {
    ++trade_count_;
    if (trade_callback_) {
        Trade trade{buy_id, sell_id, price, qty, now_ns()};
        trade_callback_(trade);
    }
}

} // namespace engine
Write unit tests for matching scenarios

A matching engine has many edge cases: what if the incoming order is larger than the resting order? What if it sweeps through multiple price levels? The tests below cover five critical scenarios.

  • Create a new file test_matching_engine.cpp inside your tests/ directory.
  • Paste the following test code into tests/test_matching_engine.cpp:
#include <gtest/gtest.h>
#include "matching_engine.hpp"

using namespace engine;

TEST(MatchingEngineTest, LimitOrderFullFill) {
    MatchingEngine engine;
    std::vector<Trade> trades;
    engine.set_trade_callback([&](const Trade& t) { trades.push_back(t); });

    // Resting sell at 100
    engine.submit({1, 100, 10, Side::Sell, OrderType::Limit});
    // Aggressive buy at 100
    auto result = engine.submit({2, 100, 10, Side::Buy, OrderType::Limit});

    EXPECT_EQ(result, OrderResult::Filled);
    ASSERT_EQ(trades.size(), 1u);
    EXPECT_EQ(trades[0].price, 100);
    EXPECT_EQ(trades[0].quantity, 10u);
}

TEST(MatchingEngineTest, PartialFill) {
    MatchingEngine engine;
    std::vector<Trade> trades;
    engine.set_trade_callback([&](const Trade& t) { trades.push_back(t); });

    engine.submit({1, 100, 5, Side::Sell, OrderType::Limit});
    auto result = engine.submit({2, 100, 10, Side::Buy, OrderType::Limit});

    EXPECT_EQ(result, OrderResult::PartialFill);
    ASSERT_EQ(trades.size(), 1u);
    EXPECT_EQ(trades[0].quantity, 5u);
    // Remaining 5 should rest on book
    EXPECT_EQ(engine.book().best_bid().value(), 100);
}

TEST(MatchingEngineTest, FillOrKillRejection) {
    MatchingEngine engine;

    engine.submit({1, 100, 5, Side::Sell, OrderType::Limit});
    // FOK for 10 but only 5 available
    auto result = engine.submit({2, 100, 10, Side::Buy, OrderType::FillOrKill});

    EXPECT_EQ(result, OrderResult::Rejected);
    EXPECT_EQ(engine.trade_count(), 0u);
}

TEST(MatchingEngineTest, ImmediateOrCancel) {
    MatchingEngine engine;
    std::vector<Trade> trades;
    engine.set_trade_callback([&](const Trade& t) { trades.push_back(t); });

    engine.submit({1, 100, 5, Side::Sell, OrderType::Limit});
    auto result = engine.submit({2, 100, 10, Side::Buy, OrderType::ImmediateOrCancel});

    EXPECT_EQ(result, OrderResult::PartialFill);
    ASSERT_EQ(trades.size(), 1u);
    EXPECT_EQ(trades[0].quantity, 5u);
    // No residual on book
    EXPECT_FALSE(engine.book().best_bid().has_value());
}

TEST(MatchingEngineTest, MultiLevelSweep) {
    MatchingEngine engine;
    std::vector<Trade> trades;
    engine.set_trade_callback([&](const Trade& t) { trades.push_back(t); });

    engine.submit({1, 100, 5, Side::Sell, OrderType::Limit});
    engine.submit({2, 101, 5, Side::Sell, OrderType::Limit});
    engine.submit({3, 102, 5, Side::Sell, OrderType::Limit});

    // Buy sweeps all three levels
    auto result = engine.submit({4, 102, 15, Side::Buy, OrderType::Limit});

    EXPECT_EQ(result, OrderResult::Filled);
    EXPECT_EQ(trades.size(), 3u);
    EXPECT_EQ(trades[0].price, 100); // Best ask first
    EXPECT_EQ(trades[1].price, 101);
    EXPECT_EQ(trades[2].price, 102);
}

What does each test verify?

  • LimitOrderFullFill confirms that two matching orders of equal quantity produce exactly one trade with the correct price and quantity.
  • PartialFill checks that when a buy is larger than the resting sell, the residual rests on the bid side.
  • FillOrKillRejection verifies that an FOK order is rejected outright when liquidity is insufficient. Zero trades should fire.
  • ImmediateOrCancel confirms partial execution with no residual resting on the book.
  • MultiLevelSweep ensures the engine sweeps price levels in order, filling at 100, then 101, then 102.
  • Save tests/test_matching_engine.cpp.
  • Rebuild and run the tests by running these commands from your build/ directory:
cmake --build build
ctest --test-dir build --output-on-failure

You should see all matching engine tests pass alongside the memory pool and order book tests from earlier.

Tests failing with linker errors?

Your CMakeLists.txt already lists src/matching_engine.cpp in the engine_lib sources and tests/test_matching_engine.cpp in the engine_tests target. If you see undefined reference errors, confirm those filenames match exactly in your CMakeLists.txt.

help me fix linker errors in my matching engine build

✔️ Awesome, I've got everything!

Great. Make sure you see all tests passing before moving on.

ⓧ I'd like to double check the full code

Here is the full tests/test_matching_engine.cpp:

#include <gtest/gtest.h>
#include "matching_engine.hpp"

using namespace engine;

TEST(MatchingEngineTest, LimitOrderFullFill) {
    MatchingEngine engine;
    std::vector<Trade> trades;
    engine.set_trade_callback([&](const Trade& t) { trades.push_back(t); });

    // Resting sell at 100
    engine.submit({1, 100, 10, Side::Sell, OrderType::Limit});
    // Aggressive buy at 100
    auto result = engine.submit({2, 100, 10, Side::Buy, OrderType::Limit});

    EXPECT_EQ(result, OrderResult::Filled);
    ASSERT_EQ(trades.size(), 1u);
    EXPECT_EQ(trades[0].price, 100);
    EXPECT_EQ(trades[0].quantity, 10u);
}

TEST(MatchingEngineTest, PartialFill) {
    MatchingEngine engine;
    std::vector<Trade> trades;
    engine.set_trade_callback([&](const Trade& t) { trades.push_back(t); });

    engine.submit({1, 100, 5, Side::Sell, OrderType::Limit});
    auto result = engine.submit({2, 100, 10, Side::Buy, OrderType::Limit});

    EXPECT_EQ(result, OrderResult::PartialFill);
    ASSERT_EQ(trades.size(), 1u);
    EXPECT_EQ(trades[0].quantity, 5u);
    // Remaining 5 should rest on book
    EXPECT_EQ(engine.book().best_bid().value(), 100);
}

TEST(MatchingEngineTest, FillOrKillRejection) {
    MatchingEngine engine;

    engine.submit({1, 100, 5, Side::Sell, OrderType::Limit});
    // FOK for 10 but only 5 available
    auto result = engine.submit({2, 100, 10, Side::Buy, OrderType::FillOrKill});

    EXPECT_EQ(result, OrderResult::Rejected);
    EXPECT_EQ(engine.trade_count(), 0u);
}

TEST(MatchingEngineTest, ImmediateOrCancel) {
    MatchingEngine engine;
    std::vector<Trade> trades;
    engine.set_trade_callback([&](const Trade& t) { trades.push_back(t); });

    engine.submit({1, 100, 5, Side::Sell, OrderType::Limit});
    auto result = engine.submit({2, 100, 10, Side::Buy, OrderType::ImmediateOrCancel});

    EXPECT_EQ(result, OrderResult::PartialFill);
    ASSERT_EQ(trades.size(), 1u);
    EXPECT_EQ(trades[0].quantity, 5u);
    // No residual on book
    EXPECT_FALSE(engine.book().best_bid().has_value());
}

TEST(MatchingEngineTest, MultiLevelSweep) {
    MatchingEngine engine;
    std::vector<Trade> trades;
    engine.set_trade_callback([&](const Trade& t) { trades.push_back(t); });

    engine.submit({1, 100, 5, Side::Sell, OrderType::Limit});
    engine.submit({2, 101, 5, Side::Sell, OrderType::Limit});
    engine.submit({3, 102, 5, Side::Sell, OrderType::Limit});

    // Buy sweeps all three levels
    auto result = engine.submit({4, 102, 15, Side::Buy, OrderType::Limit});

    EXPECT_EQ(result, OrderResult::Filled);
    EXPECT_EQ(trades.size(), 3u);
    EXPECT_EQ(trades[0].price, 100); // Best ask first
    EXPECT_EQ(trades[1].price, 101);
    EXPECT_EQ(trades[2].price, 102);
}

Your matching engine now handles all four order types with price-time priority. Next up, you'll build a synthetic order generator that produces realistic market data to stress-test this engine at scale.

Build the Order Generator & Benchmarking Harness

Your matching engine can pair buyers with sellers across multiple price levels. Now you need a way to stress-test it with realistic order flows at scale.

In this step, you will build a synthetic order generator that produces randomized limit and market orders around a configurable mid-price. You will also create a nanosecond-precision benchmarking harness that measures per-operation latency percentiles and throughput.

In this step, get ready to:
  • Build an OrderGenerator class that produces realistic order flows with configurable spread, market order ratio, and cancel targets.
  • Create a BenchmarkTimer that captures nanosecond latency samples and reports p50, p99, and p99.9 percentiles.
  • Replay one million orders through your engine and measure throughput.
Build the synthetic order generator

A matching engine is only as good as the data you feed it. Real exchanges see a mix of limit orders clustered around the mid-price, aggressive market orders that cross the spread, and cancel messages that remove resting liquidity.

Your OrderGenerator class will replicate this pattern using a seeded pseudo-random number generator for deterministic, reproducible order flows.

  • Create the order generator header by adding a new file include/order_generator.hpp with the following code:
#pragma once

#include "types.hpp"
#include "matching_engine.hpp"
#include <vector>
#include <random>
#include <string>

namespace engine {

struct GeneratorConfig {
    Price base_price{10000};       // Base mid-price (in cents)
    Price spread{10};              // Half-spread around mid
    double market_order_ratio{0.1}; // 10% market orders
    double cancel_ratio{0.2};      // 20% cancels
    uint32_t max_quantity{100};
    uint64_t seed{42};
};

class OrderGenerator {
public:
    explicit OrderGenerator(GeneratorConfig config);

    std::vector<OrderRequest> generate(uint32_t count);
    std::vector<OrderId> get_cancel_targets(uint32_t count,
                                             const std::vector<OrderId>& live_orders);

    // CSV I/O
    static void write_csv(const std::string& path,
                          const std::vector<OrderRequest>& orders);
    static std::vector<OrderRequest> read_csv(const std::string& path);

private:
    GeneratorConfig config_;
    std::mt19937_64 rng_;
    OrderId next_id_{1};
};

} // namespace engine

What does this header define?

GeneratorConfig holds all the knobs for controlling order flow: the center price, how wide the spread is, what fraction of orders are aggressive market orders, and a seed for reproducibility.

The OrderGenerator class uses std::mt19937_64 (a 64-bit Mersenne Twister) for fast, deterministic pseudo-random number generation. The same seed always produces the same order sequence, which is critical for benchmarking.

  • Now implement the generator by creating src/order_generator.cpp with this code:
#include "order_generator.hpp"
#include <fstream>
#include <sstream>

namespace engine {

OrderGenerator::OrderGenerator(GeneratorConfig config)
    : config_(config), rng_(config.seed) {}

std::vector<OrderRequest> OrderGenerator::generate(uint32_t count) {
    std::vector<OrderRequest> orders;
    orders.reserve(count);

    std::uniform_int_distribution<int64_t> price_dist(
        config_.base_price - config_.spread * 5,
        config_.base_price + config_.spread * 5
    );
    std::uniform_int_distribution<uint32_t> qty_dist(1, config_.max_quantity);
    std::uniform_real_distribution<double> type_dist(0.0, 1.0);
    std::uniform_int_distribution<int> side_dist(0, 1);

    for (uint32_t i = 0; i < count; ++i) {
        OrderRequest req{};
        req.id = next_id_++;
        req.quantity = qty_dist(rng_);
        req.side = (side_dist(rng_) == 0) ? Side::Buy : Side::Sell;

        double type_roll = type_dist(rng_);
        if (type_roll < config_.market_order_ratio) {
            req.type = OrderType::Market;
            req.price = (req.side == Side::Buy) ? MAX_PRICE : MIN_PRICE;
        } else {
            req.type = OrderType::Limit;
            req.price = price_dist(rng_);
        }

        orders.push_back(req);
    }

    return orders;
}

std::vector<OrderId> OrderGenerator::get_cancel_targets(
    uint32_t count, const std::vector<OrderId>& live_orders) {
    std::vector<OrderId> cancels;
    if (live_orders.empty()) return cancels;

    std::uniform_int_distribution<size_t> idx_dist(0, live_orders.size() - 1);
    for (uint32_t i = 0; i < count && i < live_orders.size(); ++i) {
        cancels.push_back(live_orders[idx_dist(rng_)]);
    }
    return cancels;
}

void OrderGenerator::write_csv(const std::string& path,
                                const std::vector<OrderRequest>& orders) {
    std::ofstream file(path);
    file << "id,price,quantity,side,type\n";
    for (const auto& o : orders) {
        file << o.id << ","
             << o.price << ","
             << o.quantity << ","
             << static_cast<int>(o.side) << ","
             << static_cast<int>(o.type) << "\n";
    }
}

std::vector<OrderRequest> OrderGenerator::read_csv(const std::string& path) {
    std::vector<OrderRequest> orders;
    std::ifstream file(path);
    std::string line;
    std::getline(file, line); // skip header

    while (std::getline(file, line)) {
        std::istringstream ss(line);
        std::string token;
        OrderRequest req{};

        std::getline(ss, token, ','); req.id = std::stoull(token);
        std::getline(ss, token, ','); req.price = std::stoll(token);
        std::getline(ss, token, ','); req.quantity = static_cast<uint32_t>(std::stoul(token));
        std::getline(ss, token, ','); req.side = static_cast<Side>(std::stoi(token));
        std::getline(ss, token, ','); req.type = static_cast<OrderType>(std::stoi(token));

        orders.push_back(req);
    }
    return orders;
}

} // namespace engine

What does this implementation do?

  • generate() creates orders with prices uniformly distributed within 5x the spread on each side of the mid-price. A configurable fraction become market orders (set to extreme prices to guarantee matching).
  • get_cancel_targets() randomly selects order IDs from a live-orders vector to simulate real cancel traffic.
  • write_csv() and read_csv() enable deterministic replay. You can generate an order sequence once, save it, and replay it against different engine configurations.
  • Save both include/order_generator.hpp and src/order_generator.cpp.

Seeing compile errors about OrderRequest?

The order_generator.hpp header includes matching_engine.hpp because it uses the OrderRequest struct defined there. Make sure your include/matching_engine.hpp from the previous step is saved and correct.

Help me debug compile errors in my order generator

Create the benchmarking harness

To prove your engine is fast, you need precise measurements. The benchmarking harness wraps each operation in nanosecond-resolution timing, collects all samples, then computes percentile latencies that reveal tail behavior.

  • Create the benchmark timer by adding include/benchmark.hpp with this code:
#pragma once

#include <chrono>
#include <vector>
#include <algorithm>
#include <cstdint>
#include <string>
#include <iostream>
#include <iomanip>

namespace engine {

class BenchmarkTimer {
public:
    void start() noexcept {
        start_ = std::chrono::steady_clock::now();
    }

    void stop() noexcept {
        auto end = std::chrono::steady_clock::now();
        auto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start_).count();
        samples_.push_back(static_cast<uint64_t>(ns));
    }

    void reset() noexcept { samples_.clear(); }

    void report(const std::string& name) const {
        if (samples_.empty()) return;

        auto sorted = samples_;
        std::sort(sorted.begin(), sorted.end());

        uint64_t total = 0;
        for (auto s : sorted) total += s;

        auto p = [&](double pct) -> uint64_t {
            size_t idx = static_cast<size_t>(pct * static_cast<double>(sorted.size() - 1));
            return sorted[idx];
        };

        std::cout << std::left << std::setw(25) << name
                  << " | n=" << std::setw(10) << sorted.size()
                  << " | avg=" << std::setw(8) << (total / sorted.size()) << "ns"
                  << " | p50=" << std::setw(8) << p(0.50) << "ns"
                  << " | p99=" << std::setw(8) << p(0.99) << "ns"
                  << " | p99.9=" << std::setw(8) << p(0.999) << "ns"
                  << " | throughput=" << std::setw(10)
                  << (sorted.size() * 1'000'000'000ULL / total) << " ops/s"
                  << "\n";
    }

private:
    std::chrono::steady_clock::time_point start_;
    std::vector<uint64_t> samples_;
};

} // namespace engine

Why percentiles instead of just averages?

Average latency hides spikes. A system with 100ns average might have a p99.9 of 10,000ns, meaning 1 in 1,000 operations is 100x slower. In trading systems, tail latency determines whether you get filled or someone else does.

The report() method sorts all samples and indexes directly into the sorted array to extract p50, p99, and p99.9 values. This is the same approach production latency monitoring systems use.

  • Create the benchmark executable by adding bench/bench_main.cpp with this code:
#include "matching_engine.hpp"
#include "order_generator.hpp"
#include "benchmark.hpp"
#include <iostream>

using namespace engine;

int main() {
    std::cout << "=== Matching Engine Benchmarks ===\n";
    std::cout << "Build: Release (-O3 -march=native)\n\n";

    MatchingEngine engine;
    engine.set_trade_callback([](const Trade&) {}); // No-op callback

    GeneratorConfig gen_config{};
    gen_config.base_price = 10000;
    gen_config.spread = 50; // Wide spread = fewer matches = more resting orders
    OrderGenerator generator(gen_config);

    // Benchmark: Order Add (no match)
    {
        BenchmarkTimer timer;
        auto orders = generator.generate(500'000);
        // Use wide prices to avoid matching
        for (auto& o : orders) {
            if (o.side == Side::Buy) o.price = gen_config.base_price - 100;
            else o.price = gen_config.base_price + 100;
            o.type = OrderType::Limit;
        }

        for (const auto& order : orders) {
            timer.start();
            engine.submit(order);
            timer.stop();
        }
        timer.report("Add order (no match)");
    }

    // Benchmark: Cancel
    {
        BenchmarkTimer timer;
        // Cancel the orders we just added
        for (OrderId id = 1; id <= 100'000; ++id) {
            timer.start();
            engine.cancel(id);
            timer.stop();
        }
        timer.report("Cancel order");
    }

    // Benchmark: Order Add (with match)
    {
        MatchingEngine fresh_engine;
        fresh_engine.set_trade_callback([](const Trade&) {});
        BenchmarkTimer timer;

        GeneratorConfig match_config{};
        match_config.base_price = 10000;
        match_config.spread = 5; // Tight spread = lots of matches
        match_config.market_order_ratio = 0.0;
        OrderGenerator match_gen(match_config);

        auto orders = match_gen.generate(500'000);
        for (const auto& order : orders) {
            timer.start();
            fresh_engine.submit(order);
            timer.stop();
        }
        timer.report("Add order (with match)");
    }

    std::cout << "\nDone.\n";
    return 0;
}

What does each benchmark measure?

  • Add order (no match) isolates the cost of inserting into the std::map tree plus allocating from the memory pool. Wide prices prevent any matching.
  • Cancel order measures O(1) hash map lookup plus linked-list unlink plus pool deallocation.
  • Add order (with match) uses a tight spread so many orders cross and trigger the matching loop. This measures the full critical path.
  • Save include/benchmark.hpp and bench/bench_main.cpp.

Build error about missing bench directory?

Make sure your bench/ directory exists at the same level as src/ and include/. If it does not exist yet, create it with mkdir bench from your project root.

Help me fix build errors in my benchmark setup

Replay one million orders through the engine

The benchmark harness measures individual operations in isolation. Now you will wire up a full end-to-end replay that generates one million orders, feeds them through the engine, and reports aggregate statistics.

  • Replace the contents of src/main.cpp with the following code:
#include "matching_engine.hpp"
#include "order_generator.hpp"
#include <iostream>
#include <chrono>

using namespace engine;

int main() {
    std::cout << "=== Low-Latency Order Book & Matching Engine ===\n\n";

    // Phase 1: Single-threaded benchmark
    std::cout << "[Phase 1] Single-threaded matching engine\n";
    {
        MatchingEngine engine;
        uint64_t trade_count = 0;
        engine.set_trade_callback([&](const Trade&) { ++trade_count; });

        GeneratorConfig gen_config{};
        gen_config.base_price = 10000;
        gen_config.spread = 10;
        OrderGenerator generator(gen_config);

        auto orders = generator.generate(1'000'000);

        auto start = std::chrono::steady_clock::now();
        for (const auto& order : orders) {
            engine.submit(order);
        }
        auto end = std::chrono::steady_clock::now();

        auto duration_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
        double ops_per_sec = 1'000'000.0 / (static_cast<double>(duration_ns) / 1e9);

        std::cout << "  Orders processed: 1,000,000\n";
        std::cout << "  Trades generated: " << trade_count << "\n";
        std::cout << "  Total time:       " << (duration_ns / 1'000'000) << " ms\n";
        std::cout << "  Throughput:       " << static_cast<uint64_t>(ops_per_sec) << " ops/s\n";
        std::cout << "  Avg latency:      " << (duration_ns / 1'000'000) << " ns/op\n\n";
    }

    std::cout << "\nDone.\n";
    return 0;
}

What does this main() do?

It generates one million randomized orders, then submits them to the matching engine inside a timed block. The trade callback simply increments a counter. After replay, it computes throughput (operations per second) and average latency.

You will add more phases to this main() in later steps as you build the multi-threaded pipeline and trading strategy.

  • Save src/main.cpp.
  • Rebuild and run the main executable by running these commands from your build directory:
cmake --build build
./build/order_engine

You should see output showing 1,000,000 orders processed, a trade count, total time in milliseconds, and throughput in ops/sec.

  • Run the optimized benchmark by building and executing the bench target:
cmake --build build --target bench_engine
./build/bench_engine

You should see a formatted table with p50, p99, and p99.9 latencies for each operation type, plus throughput in ops/sec. The benchmark binary is compiled with -O3 -march=native so it represents peak performance on your hardware.

What should I expect to see?

Exact numbers vary by machine. On a modern Apple Silicon Mac, expect "Add order (no match)" in the 200-500ns range for p50, with cancel slightly faster. "Add order (with match)" will be slower because the matching loop does more work per operation.

If throughput for no-match adds exceeds 2 million ops/sec, your memory pool and book structure are performing well.

Benchmark numbers look suspiciously slow?

Make sure you are running ./build/bench_engine (the optimized binary), not ./build/order_engine (which is compiled without optimization flags). The benchmark target has -O3 -march=native set in the CMakeLists.txt.

Also close other CPU-intensive applications while benchmarking to reduce noise in your measurements.

Help me understand my benchmark results

✔️ Awesome, I've got everything!

Great. Double check you have saved all four files before moving on.

ⓧ I'd like to double check the full code

Here is the complete code for each file. Compare yours against these:

#pragma once

#include "types.hpp"
#include "matching_engine.hpp"
#include <vector>
#include <random>
#include <string>

namespace engine {

struct GeneratorConfig {
    Price base_price{10000};       // Base mid-price (in cents)
    Price spread{10};              // Half-spread around mid
    double market_order_ratio{0.1}; // 10% market orders
    double cancel_ratio{0.2};      // 20% cancels
    uint32_t max_quantity{100};
    uint64_t seed{42};
};

class OrderGenerator {
public:
    explicit OrderGenerator(GeneratorConfig config);

    std::vector<OrderRequest> generate(uint32_t count);
    std::vector<OrderId> get_cancel_targets(uint32_t count,
                                             const std::vector<OrderId>& live_orders);

    // CSV I/O
    static void write_csv(const std::string& path,
                          const std::vector<OrderRequest>& orders);
    static std::vector<OrderRequest> read_csv(const std::string& path);

private:
    GeneratorConfig config_;
    std::mt19937_64 rng_;
    OrderId next_id_{1};
};

} // namespace engine
#include "order_generator.hpp"
#include <fstream>
#include <sstream>

namespace engine {

OrderGenerator::OrderGenerator(GeneratorConfig config)
    : config_(config), rng_(config.seed) {}

std::vector<OrderRequest> OrderGenerator::generate(uint32_t count) {
    std::vector<OrderRequest> orders;
    orders.reserve(count);

    std::uniform_int_distribution<int64_t> price_dist(
        config_.base_price - config_.spread * 5,
        config_.base_price + config_.spread * 5
    );
    std::uniform_int_distribution<uint32_t> qty_dist(1, config_.max_quantity);
    std::uniform_real_distribution<double> type_dist(0.0, 1.0);
    std::uniform_int_distribution<int> side_dist(0, 1);

    for (uint32_t i = 0; i < count; ++i) {
        OrderRequest req{};
        req.id = next_id_++;
        req.quantity = qty_dist(rng_);
        req.side = (side_dist(rng_) == 0) ? Side::Buy : Side::Sell;

        double type_roll = type_dist(rng_);
        if (type_roll < config_.market_order_ratio) {
            req.type = OrderType::Market;
            req.price = (req.side == Side::Buy) ? MAX_PRICE : MIN_PRICE;
        } else {
            req.type = OrderType::Limit;
            req.price = price_dist(rng_);
        }

        orders.push_back(req);
    }

    return orders;
}

std::vector<OrderId> OrderGenerator::get_cancel_targets(
    uint32_t count, const std::vector<OrderId>& live_orders) {
    std::vector<OrderId> cancels;
    if (live_orders.empty()) return cancels;

    std::uniform_int_distribution<size_t> idx_dist(0, live_orders.size() - 1);
    for (uint32_t i = 0; i < count && i < live_orders.size(); ++i) {
        cancels.push_back(live_orders[idx_dist(rng_)]);
    }
    return cancels;
}

void OrderGenerator::write_csv(const std::string& path,
                                const std::vector<OrderRequest>& orders) {
    std::ofstream file(path);
    file << "id,price,quantity,side,type\n";
    for (const auto& o : orders) {
        file << o.id << ","
             << o.price << ","
             << o.quantity << ","
             << static_cast<int>(o.side) << ","
             << static_cast<int>(o.type) << "\n";
    }
}

std::vector<OrderRequest> OrderGenerator::read_csv(const std::string& path) {
    std::vector<OrderRequest> orders;
    std::ifstream file(path);
    std::string line;
    std::getline(file, line); // skip header

    while (std::getline(file, line)) {
        std::istringstream ss(line);
        std::string token;
        OrderRequest req{};

        std::getline(ss, token, ','); req.id = std::stoull(token);
        std::getline(ss, token, ','); req.price = std::stoll(token);
        std::getline(ss, token, ','); req.quantity = static_cast<uint32_t>(std::stoul(token));
        std::getline(ss, token, ','); req.side = static_cast<Side>(std::stoi(token));
        std::getline(ss, token, ','); req.type = static_cast<OrderType>(std::stoi(token));

        orders.push_back(req);
    }
    return orders;
}

} // namespace engine
#pragma once

#include <chrono>
#include <vector>
#include <algorithm>
#include <cstdint>
#include <string>
#include <iostream>
#include <iomanip>

namespace engine {

class BenchmarkTimer {
public:
    void start() noexcept {
        start_ = std::chrono::steady_clock::now();
    }

    void stop() noexcept {
        auto end = std::chrono::steady_clock::now();
        auto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start_).count();
        samples_.push_back(static_cast<uint64_t>(ns));
    }

    void reset() noexcept { samples_.clear(); }

    void report(const std::string& name) const {
        if (samples_.empty()) return;

        auto sorted = samples_;
        std::sort(sorted.begin(), sorted.end());

        uint64_t total = 0;
        for (auto s : sorted) total += s;

        auto p = [&](double pct) -> uint64_t {
            size_t idx = static_cast<size_t>(pct * static_cast<double>(sorted.size() - 1));
            return sorted[idx];
        };

        std::cout << std::left << std::setw(25) << name
                  << " | n=" << std::setw(10) << sorted.size()
                  << " | avg=" << std::setw(8) << (total / sorted.size()) << "ns"
                  << " | p50=" << std::setw(8) << p(0.50) << "ns"
                  << " | p99=" << std::setw(8) << p(0.99) << "ns"
                  << " | p99.9=" << std::setw(8) << p(0.999) << "ns"
                  << " | throughput=" << std::setw(10)
                  << (sorted.size() * 1'000'000'000ULL / total) << " ops/s"
                  << "\n";
    }

private:
    std::chrono::steady_clock::time_point start_;
    std::vector<uint64_t> samples_;
};

} // namespace engine
#include "matching_engine.hpp"
#include "order_generator.hpp"
#include "benchmark.hpp"
#include <iostream>

using namespace engine;

int main() {
    std::cout << "=== Matching Engine Benchmarks ===\n";
    std::cout << "Build: Release (-O3 -march=native)\n\n";

    MatchingEngine engine;
    engine.set_trade_callback([](const Trade&) {}); // No-op callback

    GeneratorConfig gen_config{};
    gen_config.base_price = 10000;
    gen_config.spread = 50; // Wide spread = fewer matches = more resting orders
    OrderGenerator generator(gen_config);

    // Benchmark: Order Add (no match)
    {
        BenchmarkTimer timer;
        auto orders = generator.generate(500'000);
        // Use wide prices to avoid matching
        for (auto& o : orders) {
            if (o.side == Side::Buy) o.price = gen_config.base_price - 100;
            else o.price = gen_config.base_price + 100;
            o.type = OrderType::Limit;
        }

        for (const auto& order : orders) {
            timer.start();
            engine.submit(order);
            timer.stop();
        }
        timer.report("Add order (no match)");
    }

    // Benchmark: Cancel
    {
        BenchmarkTimer timer;
        // Cancel the orders we just added
        for (OrderId id = 1; id <= 100'000; ++id) {
            timer.start();
            engine.cancel(id);
            timer.stop();
        }
        timer.report("Cancel order");
    }

    // Benchmark: Order Add (with match)
    {
        MatchingEngine fresh_engine;
        fresh_engine.set_trade_callback([](const Trade&) {});
        BenchmarkTimer timer;

        GeneratorConfig match_config{};
        match_config.base_price = 10000;
        match_config.spread = 5; // Tight spread = lots of matches
        match_config.market_order_ratio = 0.0;
        OrderGenerator match_gen(match_config);

        auto orders = match_gen.generate(500'000);
        for (const auto& order : orders) {
            timer.start();
            fresh_engine.submit(order);
            timer.stop();
        }
        timer.report("Add order (with match)");
    }

    std::cout << "\nDone.\n";
    return 0;
}

You now have hard numbers proving your engine's performance. The memory pool gives you zero-allocation order insertion, and the O(1) cancel path shows up clearly in the benchmark results. Next, you will build a lock-free SPSC ring buffer that lets a separate feed handler thread push orders into the engine without any mutex contention.

Build the Lock-Free SPSC Queue

Your matching engine tears through orders at nanosecond speeds on a single thread. Now you need a way for a separate thread to feed it messages without any locking overhead.

A lock-free Single-Producer Single-Consumer (SPSC) ring buffer is the answer. One thread writes messages in, another reads them out, and the only synchronization is a pair of atomic integers. This is the exact pattern production exchanges use between their network layer and matching engine.

In this step, get ready to:
  • Implement a lock-free ring buffer with atomic head and tail pointers.
  • Add cache-line alignment to prevent false sharing between producer and consumer threads.
  • Write unit tests including a multi-threaded stress test that verifies zero data loss.
Implement the SPSCQueue template class

A ring buffer uses a fixed-size array where the producer writes at the head position and the consumer reads from the tail position. Both indices wrap around using a bitmask instead of modulo division.

  • Create a new file include/spsc_queue.hpp and add the following code:
#pragma once

#include <atomic>
#include <array>
#include <cstdint>
#include <optional>
#include <new>

namespace engine {

template <typename T, uint32_t Capacity>
class SPSCQueue {
    static_assert((Capacity & (Capacity - 1)) == 0,
                  "Capacity must be a power of 2");

public:
    SPSCQueue() noexcept = default;

    // Producer: returns true if push succeeded
    [[nodiscard]] bool try_push(const T& item) noexcept {
        const uint32_t head = head_.load(std::memory_order_relaxed);
        const uint32_t next_head = (head + 1) & mask_;

        if (next_head == tail_.load(std::memory_order_acquire)) {
            return false; // Queue full
        }

        buffer_[head] = item;
        head_.store(next_head, std::memory_order_release);
        return true;
    }

    // Consumer: returns item if pop succeeded
    [[nodiscard]] std::optional<T> try_pop() noexcept {
        const uint32_t tail = tail_.load(std::memory_order_relaxed);

        if (tail == head_.load(std::memory_order_acquire)) {
            return std::nullopt; // Queue empty
        }

        T item = buffer_[tail];
        tail_.store((tail + 1) & mask_, std::memory_order_release);
        return item;
    }

    [[nodiscard]] bool empty() const noexcept {
        return head_.load(std::memory_order_acquire) ==
               tail_.load(std::memory_order_acquire);
    }

    [[nodiscard]] uint32_t size() const noexcept {
        uint32_t h = head_.load(std::memory_order_acquire);
        uint32_t t = tail_.load(std::memory_order_acquire);
        return (h - t) & mask_;
    }

private:
    static constexpr uint32_t mask_ = Capacity - 1;

    alignas(64) std::atomic<uint32_t> head_{0};
    alignas(64) std::atomic<uint32_t> tail_{0};
    std::array<T, Capacity> buffer_{};
};

} // namespace engine

What does this code do?

The static_assert enforces a power-of-2 capacity so you can use a bitmask (& mask_) instead of expensive modulo division to wrap indices around.

In try_push(), the producer reads its own head_ with memory_order_relaxed (no ordering needed for your own variable), then checks tail_ with memory_order_acquire to see the consumer's latest progress. It stores with memory_order_release to guarantee the buffer write is visible before the head advances.

The alignas(64) on head_ and tail_ places them on separate cache lines to prevent false sharing. Without this, writing to head_ would invalidate the consumer's cached copy of tail_ (and vice versa), destroying performance.

Why 64-byte alignment?

The x86 standard cache line is 64 bytes. Apple M-series chips actually use 128-byte cache lines, but alignas(64) is the portable standard and still prevents most false sharing. It is the convention used across open-source SPSC implementations.

  • Save include/spsc_queue.hpp.

Seeing a compile error about static_assert?

The static_assert fires at compile time if the capacity template argument is not a power of 2. Make sure any instantiation uses 4, 8, 16, 64, 1024, 65536, etc.

Help me understand the SPSC queue static_assert error.

Write single-threaded unit tests

Before testing concurrency, you need to verify the basic API works correctly on a single thread. These tests confirm push, pop, empty detection, and full-queue rejection.

  • Create a new file tests/test_spsc_queue.cpp and add the includes and basic tests:
#include <gtest/gtest.h>
#include "spsc_queue.hpp"
#include <thread>
#include <vector>

using namespace engine;

TEST(SPSCQueueTest, PushAndPop) {
    SPSCQueue<int, 8> queue;

    EXPECT_TRUE(queue.try_push(42));
    auto val = queue.try_pop();
    ASSERT_TRUE(val.has_value());
    EXPECT_EQ(val.value(), 42);
}

TEST(SPSCQueueTest, EmptyPop) {
    SPSCQueue<int, 8> queue;
    EXPECT_FALSE(queue.try_pop().has_value());
}

TEST(SPSCQueueTest, FullQueue) {
    SPSCQueue<int, 4> queue; // Capacity 4, usable slots = 3

    EXPECT_TRUE(queue.try_push(1));
    EXPECT_TRUE(queue.try_push(2));
    EXPECT_TRUE(queue.try_push(3));
    EXPECT_FALSE(queue.try_push(4)); // Full (one slot wasted for full detection)
}

What does this code do?

PushAndPop verifies the basic contract: push an item, pop it back, confirm the value is unchanged.

EmptyPop confirms that popping an empty queue returns std::nullopt instead of crashing or returning garbage.

FullQueue tests that a capacity-4 ring buffer only holds 3 items. One slot is always wasted to distinguish "full" from "empty" (both would have head == tail otherwise).

Add the multi-threaded stress test

The real proof of a lock-free queue is correctness under concurrency. This test spawns a producer and consumer on separate threads, sends 100,000 items, and verifies every single one arrives in order.

  • In tests/test_spsc_queue.cpp, add the following test below the FullQueue test:
TEST(SPSCQueueTest, MultithreadedStress) {
    SPSCQueue<uint64_t, 1024> queue;
    constexpr uint64_t NUM_ITEMS = 100'000;

    std::vector<uint64_t> received;
    received.reserve(NUM_ITEMS);

    std::thread producer([&]() {
        for (uint64_t i = 0; i < NUM_ITEMS; ++i) {
            while (!queue.try_push(i)) {
                std::this_thread::yield();
            }
        }
    });

    std::thread consumer([&]() {
        uint64_t count = 0;
        while (count < NUM_ITEMS) {
            auto val = queue.try_pop();
            if (val.has_value()) {
                received.push_back(val.value());
                ++count;
            } else {
                std::this_thread::yield();
            }
        }
    });

    producer.join();
    consumer.join();

    // Verify all items received in order
    ASSERT_EQ(received.size(), NUM_ITEMS);
    for (uint64_t i = 0; i < NUM_ITEMS; ++i) {
        EXPECT_EQ(received[i], i);
    }
}

What does this code do?

The producer thread pushes integers 0 through 99,999 into the queue. When the queue is full, it yields the CPU and retries. The consumer pops items and stores them in a vector.

After both threads finish, the test checks that every item arrived and that they arrived in order. If the memory ordering were wrong, items could appear out of order or be corrupted. This is the definitive proof that your acquire/release pairs are correct.

  • Save tests/test_spsc_queue.cpp.
  • Build and run the tests by running these commands:
cmake --build build
ctest --test-dir build --output-on-failure

You should see all SPSC queue tests passing, including the multi-threaded stress test with 100,000 items delivered in order.

MultithreadedStress test hanging or failing?

If the test hangs, the consumer may be stuck in an infinite loop because items are lost. Double-check that head_.store uses std::memory_order_release and the consumer's head_.load uses std::memory_order_acquire. Without this pair, the buffer write might not be visible to the reader.

If items arrive out of order, confirm the bitmask wrapping is correct: (head + 1) & mask_ with mask_ = Capacity - 1.

Help me debug my SPSC queue multi-threaded test.

✔️ Awesome, I've got everything!

Great. Double check you have saved both files.

ⓧ I'd like to double check the full code

Here is include/spsc_queue.hpp:

#pragma once

#include <atomic>
#include <array>
#include <cstdint>
#include <optional>
#include <new>

namespace engine {

template <typename T, uint32_t Capacity>
class SPSCQueue {
    static_assert((Capacity & (Capacity - 1)) == 0,
                  "Capacity must be a power of 2");

public:
    SPSCQueue() noexcept = default;

    // Producer: returns true if push succeeded
    [[nodiscard]] bool try_push(const T& item) noexcept {
        const uint32_t head = head_.load(std::memory_order_relaxed);
        const uint32_t next_head = (head + 1) & mask_;

        if (next_head == tail_.load(std::memory_order_acquire)) {
            return false; // Queue full
        }

        buffer_[head] = item;
        head_.store(next_head, std::memory_order_release);
        return true;
    }

    // Consumer: returns item if pop succeeded
    [[nodiscard]] std::optional<T> try_pop() noexcept {
        const uint32_t tail = tail_.load(std::memory_order_relaxed);

        if (tail == head_.load(std::memory_order_acquire)) {
            return std::nullopt; // Queue empty
        }

        T item = buffer_[tail];
        tail_.store((tail + 1) & mask_, std::memory_order_release);
        return item;
    }

    [[nodiscard]] bool empty() const noexcept {
        return head_.load(std::memory_order_acquire) ==
               tail_.load(std::memory_order_acquire);
    }

    [[nodiscard]] uint32_t size() const noexcept {
        uint32_t h = head_.load(std::memory_order_acquire);
        uint32_t t = tail_.load(std::memory_order_acquire);
        return (h - t) & mask_;
    }

private:
    static constexpr uint32_t mask_ = Capacity - 1;

    alignas(64) std::atomic<uint32_t> head_{0};
    alignas(64) std::atomic<uint32_t> tail_{0};
    std::array<T, Capacity> buffer_{};
};

} // namespace engine

Here is tests/test_spsc_queue.cpp:

#include <gtest/gtest.h>
#include "spsc_queue.hpp"
#include <thread>
#include <vector>

using namespace engine;

TEST(SPSCQueueTest, PushAndPop) {
    SPSCQueue<int, 8> queue;

    EXPECT_TRUE(queue.try_push(42));
    auto val = queue.try_pop();
    ASSERT_TRUE(val.has_value());
    EXPECT_EQ(val.value(), 42);
}

TEST(SPSCQueueTest, EmptyPop) {
    SPSCQueue<int, 8> queue;
    EXPECT_FALSE(queue.try_pop().has_value());
}

TEST(SPSCQueueTest, FullQueue) {
    SPSCQueue<int, 4> queue; // Capacity 4, usable slots = 3

    EXPECT_TRUE(queue.try_push(1));
    EXPECT_TRUE(queue.try_push(2));
    EXPECT_TRUE(queue.try_push(3));
    EXPECT_FALSE(queue.try_push(4)); // Full (one slot wasted for full detection)
}

TEST(SPSCQueueTest, MultithreadedStress) {
    SPSCQueue<uint64_t, 1024> queue;
    constexpr uint64_t NUM_ITEMS = 100'000;

    std::vector<uint64_t> received;
    received.reserve(NUM_ITEMS);

    std::thread producer([&]() {
        for (uint64_t i = 0; i < NUM_ITEMS; ++i) {
            while (!queue.try_push(i)) {
                std::this_thread::yield();
            }
        }
    });

    std::thread consumer([&]() {
        uint64_t count = 0;
        while (count < NUM_ITEMS) {
            auto val = queue.try_pop();
            if (val.has_value()) {
                received.push_back(val.value());
                ++count;
            } else {
                std::this_thread::yield();
            }
        }
    });

    producer.join();
    consumer.join();

    // Verify all items received in order
    ASSERT_EQ(received.size(), NUM_ITEMS);
    for (uint64_t i = 0; i < NUM_ITEMS; ++i) {
        EXPECT_EQ(received[i], i);
    }
}

Why a SPSC queue instead of a mutex-protected std::queue?

A mutex lock/unlock pair costs hundreds of nanoseconds, even when uncontended. On the hot path of a matching engine processing millions of messages per second, that overhead is unacceptable.

The SPSC queue uses only atomic loads and stores with acquire/release semantics. These compile down to a single fence instruction (or even nothing on x86). The result is single-digit nanosecond push/pop latency with zero system calls.

Your lock-free queue is proven correct under concurrency. Next, you will wire it into a multi-threaded pipeline where a feed handler thread pushes order messages through this queue to the matching engine thread.

Build the Multi-Threaded Pipeline

Your SPSC queue can move data between threads without locks. Now it's time to use it.

In this step, you'll build the two-thread architecture that real exchanges use. A FeedHandler thread pushes order messages into the SPSC queue, and an EngineThread spins on the other end, popping messages and feeding them to the matching engine.

In this step, get ready to:
  • Define a message protocol for inter-thread communication.
  • Implement a feed handler producer and matching engine consumer with spin-wait loops.
  • Benchmark the multi-threaded pipeline against the single-threaded engine.
Define the message types and thread classes

The two threads need a shared vocabulary. You'll define an OrderMessage struct that wraps either a new order, a cancel request, or a shutdown signal.

  • Create a new file include/feed_handler.hpp and add the following code:
#pragma once

#include "types.hpp"
#include "matching_engine.hpp"
#include "spsc_queue.hpp"
#include <vector>
#include <atomic>
#include <thread>

namespace engine {

// Message types for inter-thread communication
enum class MessageType : uint8_t {
    NewOrder,
    CancelOrder,
    Shutdown
};

// Fixed-size message struct pushed through the SPSC queue
struct OrderMessage {
    MessageType msg_type;
    OrderRequest request;  // Used for NewOrder
    OrderId cancel_id;     // Used for CancelOrder
};

constexpr uint32_t QUEUE_CAPACITY = 65536; // 64K slots, power of 2

// Producer thread: reads orders and pushes them into the queue
class FeedHandler {
public:
    FeedHandler(SPSCQueue<OrderMessage, QUEUE_CAPACITY>& queue);

    void start(const std::vector<OrderRequest>& orders,
               const std::vector<OrderId>& cancels);
    void stop();
    void join();

    [[nodiscard]] uint64_t messages_sent() const noexcept { return messages_sent_; }

private:
    void run(std::vector<OrderRequest> orders, std::vector<OrderId> cancels);

    SPSCQueue<OrderMessage, QUEUE_CAPACITY>& queue_;
    std::thread thread_;
    std::atomic<bool> running_{false};
    uint64_t messages_sent_{0};
};

// Consumer thread: pops messages and dispatches to the matching engine
class EngineThread {
public:
    EngineThread(SPSCQueue<OrderMessage, QUEUE_CAPACITY>& queue,
                 MatchingEngine& engine);

    void start();
    void stop();
    void join();

    [[nodiscard]] uint64_t messages_processed() const noexcept {
        return messages_processed_;
    }

private:
    void run();

    SPSCQueue<OrderMessage, QUEUE_CAPACITY>& queue_;
    MatchingEngine& engine_;
    std::thread thread_;
    std::atomic<bool> running_{false};
    uint64_t messages_processed_{0};
};

} // namespace engine

What does this code do?

MessageType is a simple enum with three values. NewOrder tells the consumer to submit an order. CancelOrder tells it to cancel. Shutdown signals the consumer to exit its spin loop.

QUEUE_CAPACITY is 65536 (a power of 2). This satisfies the SPSCQueue static assert and gives the producer plenty of runway before it stalls waiting for the consumer.

FeedHandler owns the producer thread. EngineThread owns the consumer thread. Both store an std::atomic<bool> to coordinate graceful shutdown.

  • Save include/feed_handler.hpp.
Implement the feed handler with spin-wait loops

The producer spins on try_push() until the queue has room, yielding the thread between attempts. The consumer does the same with try_pop(). This is a spin-wait pattern. It burns CPU but delivers the lowest possible latency.

  • Create a new file src/feed_handler.cpp and add the FeedHandler implementation:
#include "feed_handler.hpp"
#include <thread>

namespace engine {

FeedHandler::FeedHandler(SPSCQueue<OrderMessage, QUEUE_CAPACITY>& queue)
    : queue_(queue) {}

void FeedHandler::start(const std::vector<OrderRequest>& orders,
                         const std::vector<OrderId>& cancels) {
    running_.store(true, std::memory_order_release);
    thread_ = std::thread(&FeedHandler::run, this, orders, cancels);
}

void FeedHandler::stop() {
    running_.store(false, std::memory_order_release);
}

void FeedHandler::join() {
    if (thread_.joinable()) thread_.join();
}

void FeedHandler::run(std::vector<OrderRequest> orders,
                       std::vector<OrderId> cancels) {
    // Send all new orders
    for (const auto& order : orders) {
        if (!running_.load(std::memory_order_acquire)) break;

        OrderMessage msg{};
        msg.msg_type = MessageType::NewOrder;
        msg.request = order;

        while (!queue_.try_push(msg)) {
            std::this_thread::yield();
        }
        ++messages_sent_;
    }

    // Send cancels
    for (const auto& cancel_id : cancels) {
        if (!running_.load(std::memory_order_acquire)) break;

        OrderMessage msg{};
        msg.msg_type = MessageType::CancelOrder;
        msg.cancel_id = cancel_id;

        while (!queue_.try_push(msg)) {
            std::this_thread::yield();
        }
        ++messages_sent_;
    }

    // Send shutdown signal
    OrderMessage shutdown{};
    shutdown.msg_type = MessageType::Shutdown;
    while (!queue_.try_push(shutdown)) {
        std::this_thread::yield();
    }
}

What does FeedHandler::run() do?

The run() method takes orders and cancels by value (copied into the thread). It iterates through all orders first, then cancels, spinning on try_push() with std::this_thread::yield() when the queue is full.

After all messages are sent, it pushes a Shutdown message. This tells the consumer thread to stop cleanly without needing a shared atomic flag or condition variable.

  • Now add the EngineThread implementation below the FeedHandler code in the same file:
// EngineThread implementation

EngineThread::EngineThread(SPSCQueue<OrderMessage, QUEUE_CAPACITY>& queue,
                           MatchingEngine& engine)
    : queue_(queue), engine_(engine) {}

void EngineThread::start() {
    running_.store(true, std::memory_order_release);
    thread_ = std::thread(&EngineThread::run, this);
}

void EngineThread::stop() {
    running_.store(false, std::memory_order_release);
}

void EngineThread::join() {
    if (thread_.joinable()) thread_.join();
}

void EngineThread::run() {
    while (running_.load(std::memory_order_acquire)) {
        auto msg = queue_.try_pop();
        if (!msg.has_value()) {
            std::this_thread::yield();
            continue;
        }

        switch (msg->msg_type) {
            case MessageType::NewOrder:
                engine_.submit(msg->request);
                break;
            case MessageType::CancelOrder:
                engine_.cancel(msg->cancel_id);
                break;
            case MessageType::Shutdown:
                running_.store(false, std::memory_order_release);
                return;
        }
        ++messages_processed_;
    }
}

} // namespace engine

What does EngineThread::run() do?

This is the consumer spin loop. It calls try_pop() in a tight loop. When the queue is empty, it yields and retries. When a message arrives, a switch dispatches it to engine_.submit() or engine_.cancel().

The Shutdown case sets running_ to false and returns immediately. This guarantees the consumer exits after processing every message the producer sent.

  • Save src/feed_handler.cpp.
  • In CMakeLists.txt, find the add_library(engine_lib ...) block and add src/feed_handler.cpp to the source list:
add_library(engine_lib
    src/order_book.cpp
    src/matching_engine.cpp
    src/order_generator.cpp
    src/feed_handler.cpp
)
  • Save CMakeLists.txt.

Build errors about undefined references?

If you see linker errors mentioning FeedHandler or EngineThread, double-check that src/feed_handler.cpp is listed in the add_library call. CMake only compiles files it knows about.

help me fix linker errors for FeedHandler and EngineThread

✔️ Awesome, I've got everything!

Great. Double check you've saved both include/feed_handler.hpp and src/feed_handler.cpp.

ⓧ I'd like to double check the full code

Here is the complete src/feed_handler.cpp:

#include "feed_handler.hpp"
#include <thread>

namespace engine {

FeedHandler::FeedHandler(SPSCQueue<OrderMessage, QUEUE_CAPACITY>& queue)
    : queue_(queue) {}

void FeedHandler::start(const std::vector<OrderRequest>& orders,
                         const std::vector<OrderId>& cancels) {
    running_.store(true, std::memory_order_release);
    thread_ = std::thread(&FeedHandler::run, this, orders, cancels);
}

void FeedHandler::stop() {
    running_.store(false, std::memory_order_release);
}

void FeedHandler::join() {
    if (thread_.joinable()) thread_.join();
}

void FeedHandler::run(std::vector<OrderRequest> orders,
                       std::vector<OrderId> cancels) {
    // Send all new orders
    for (const auto& order : orders) {
        if (!running_.load(std::memory_order_acquire)) break;

        OrderMessage msg{};
        msg.msg_type = MessageType::NewOrder;
        msg.request = order;

        while (!queue_.try_push(msg)) {
            std::this_thread::yield();
        }
        ++messages_sent_;
    }

    // Send cancels
    for (const auto& cancel_id : cancels) {
        if (!running_.load(std::memory_order_acquire)) break;

        OrderMessage msg{};
        msg.msg_type = MessageType::CancelOrder;
        msg.cancel_id = cancel_id;

        while (!queue_.try_push(msg)) {
            std::this_thread::yield();
        }
        ++messages_sent_;
    }

    // Send shutdown signal
    OrderMessage shutdown{};
    shutdown.msg_type = MessageType::Shutdown;
    while (!queue_.try_push(shutdown)) {
        std::this_thread::yield();
    }
}

// EngineThread implementation

EngineThread::EngineThread(SPSCQueue<OrderMessage, QUEUE_CAPACITY>& queue,
                           MatchingEngine& engine)
    : queue_(queue), engine_(engine) {}

void EngineThread::start() {
    running_.store(true, std::memory_order_release);
    thread_ = std::thread(&EngineThread::run, this);
}

void EngineThread::stop() {
    running_.store(false, std::memory_order_release);
}

void EngineThread::join() {
    if (thread_.joinable()) thread_.join();
}

void EngineThread::run() {
    while (running_.load(std::memory_order_acquire)) {
        auto msg = queue_.try_pop();
        if (!msg.has_value()) {
            std::this_thread::yield();
            continue;
        }

        switch (msg->msg_type) {
            case MessageType::NewOrder:
                engine_.submit(msg->request);
                break;
            case MessageType::CancelOrder:
                engine_.cancel(msg->cancel_id);
                break;
            case MessageType::Shutdown:
                running_.store(false, std::memory_order_release);
                return;
        }
        ++messages_processed_;
    }
}

} // namespace engine
Benchmark the multi-threaded pipeline

Time to see how the two-thread architecture compares to the single-threaded loop. You'll run 1 million orders through both paths and compare throughput.

  • Replace the entire contents of src/main.cpp with the following code:
#include "matching_engine.hpp"
#include "order_generator.hpp"
#include "feed_handler.hpp"
#include <iostream>
#include <chrono>

using namespace engine;

int main() {
    std::cout << "=== Low-Latency Order Book & Matching Engine ===\n\n";

    // Phase 1: Single-threaded benchmark
    std::cout << "[Phase 1] Single-threaded matching engine\n";
    {
        MatchingEngine engine;
        uint64_t trade_count = 0;
        engine.set_trade_callback([&](const Trade&) { ++trade_count; });

        GeneratorConfig gen_config{};
        gen_config.base_price = 10000;
        gen_config.spread = 10;
        OrderGenerator generator(gen_config);

        auto orders = generator.generate(1'000'000);

        auto start = std::chrono::steady_clock::now();
        for (const auto& order : orders) {
            engine.submit(order);
        }
        auto end = std::chrono::steady_clock::now();

        auto duration_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
        double ops_per_sec = 1'000'000.0 / (static_cast<double>(duration_ns) / 1e9);

        std::cout << "  Orders processed: 1,000,000\n";
        std::cout << "  Trades generated: " << trade_count << "\n";
        std::cout << "  Total time:       " << (duration_ns / 1'000'000) << " ms\n";
        std::cout << "  Throughput:       " << static_cast<uint64_t>(ops_per_sec) << " ops/s\n";
        std::cout << "  Avg latency:      " << (duration_ns / 1'000'000) << " ns/op\n\n";
    }

    // Phase 2: Multi-threaded pipeline
    std::cout << "[Phase 2] Multi-threaded pipeline (Feed \u2192 SPSC \u2192 Engine)\n";
    {
        SPSCQueue<OrderMessage, QUEUE_CAPACITY> queue;
        MatchingEngine engine;
        uint64_t trade_count = 0;
        engine.set_trade_callback([&](const Trade&) { ++trade_count; });

        GeneratorConfig gen_config{};
        gen_config.base_price = 10000;
        gen_config.spread = 10;
        OrderGenerator generator(gen_config);

        auto orders = generator.generate(1'000'000);
        std::vector<OrderId> cancels; // No cancels for this test

        FeedHandler feed(queue);
        EngineThread engine_thread(queue, engine);

        auto start = std::chrono::steady_clock::now();
        engine_thread.start();
        feed.start(orders, cancels);

        feed.join();
        engine_thread.join();
        auto end = std::chrono::steady_clock::now();

        auto duration_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
        double ops_per_sec = 1'000'000.0 / (static_cast<double>(duration_ns) / 1e9);

        std::cout << "  Messages sent:     " << feed.messages_sent() << "\n";
        std::cout << "  Messages processed:" << engine_thread.messages_processed() << "\n";
        std::cout << "  Trades generated:  " << trade_count << "\n";
        std::cout << "  Total time:        " << (duration_ns / 1'000'000) << " ms\n";
        std::cout << "  Throughput:        " << static_cast<uint64_t>(ops_per_sec) << " ops/s\n\n";
    }

    std::cout << "\nDone.\n";
    return 0;
}

What does this benchmark measure?

Phase 1 runs all 1 million orders through the matching engine in a single loop on one thread. This is the baseline.

Phase 2 splits the work across two threads. The FeedHandler pushes messages into the SPSC queue while the EngineThread pops and processes them concurrently. You'll see the overhead of cross-thread communication.

The multi-threaded version may show slightly lower throughput because of the queue hop. The real benefit appears when the feed handler does I/O (network reads, CSV parsing) that would otherwise block the engine's hot path.

  • Save src/main.cpp.
  • Rebuild and run the project by running these commands:
cmake --build build
./build/order_engine

You should see output for both Phase 1 and Phase 2 showing order counts, trade counts, total time in milliseconds, and throughput in ops/sec.

Seeing a segfault or hang?

If the program hangs, check that EngineThread::start() is called before FeedHandler::start(). The consumer must be spinning before the producer fills the queue.

If you see a segfault, verify that the SPSCQueue is declared in the same scope as both thread objects. If the queue goes out of scope while threads are running, you get undefined behavior.

help me debug my multi-threaded matching engine pipeline

✔️ Awesome, I've got everything!

Great. Double check you've saved src/main.cpp and confirmed both phases print results.

ⓧ I'd like to double check the full code

Here is the complete src/main.cpp at this point:

#include "matching_engine.hpp"
#include "order_generator.hpp"
#include "feed_handler.hpp"
#include <iostream>
#include <chrono>

using namespace engine;

int main() {
    std::cout << "=== Low-Latency Order Book & Matching Engine ===\n\n";

    // Phase 1: Single-threaded benchmark
    std::cout << "[Phase 1] Single-threaded matching engine\n";
    {
        MatchingEngine engine;
        uint64_t trade_count = 0;
        engine.set_trade_callback([&](const Trade&) { ++trade_count; });

        GeneratorConfig gen_config{};
        gen_config.base_price = 10000;
        gen_config.spread = 10;
        OrderGenerator generator(gen_config);

        auto orders = generator.generate(1'000'000);

        auto start = std::chrono::steady_clock::now();
        for (const auto& order : orders) {
            engine.submit(order);
        }
        auto end = std::chrono::steady_clock::now();

        auto duration_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
        double ops_per_sec = 1'000'000.0 / (static_cast<double>(duration_ns) / 1e9);

        std::cout << "  Orders processed: 1,000,000\n";
        std::cout << "  Trades generated: " << trade_count << "\n";
        std::cout << "  Total time:       " << (duration_ns / 1'000'000) << " ms\n";
        std::cout << "  Throughput:       " << static_cast<uint64_t>(ops_per_sec) << " ops/s\n";
        std::cout << "  Avg latency:      " << (duration_ns / 1'000'000) << " ns/op\n\n";
    }

    // Phase 2: Multi-threaded pipeline
    std::cout << "[Phase 2] Multi-threaded pipeline (Feed \u2192 SPSC \u2192 Engine)\n";
    {
        SPSCQueue<OrderMessage, QUEUE_CAPACITY> queue;
        MatchingEngine engine;
        uint64_t trade_count = 0;
        engine.set_trade_callback([&](const Trade&) { ++trade_count; });

        GeneratorConfig gen_config{};
        gen_config.base_price = 10000;
        gen_config.spread = 10;
        OrderGenerator generator(gen_config);

        auto orders = generator.generate(1'000'000);
        std::vector<OrderId> cancels; // No cancels for this test

        FeedHandler feed(queue);
        EngineThread engine_thread(queue, engine);

        auto start = std::chrono::steady_clock::now();
        engine_thread.start();
        feed.start(orders, cancels);

        feed.join();
        engine_thread.join();
        auto end = std::chrono::steady_clock::now();

        auto duration_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
        double ops_per_sec = 1'000'000.0 / (static_cast<double>(duration_ns) / 1e9);

        std::cout << "  Messages sent:     " << feed.messages_sent() << "\n";
        std::cout << "  Messages processed:" << engine_thread.messages_processed() << "\n";
        std::cout << "  Trades generated:  " << trade_count << "\n";
        std::cout << "  Total time:        " << (duration_ns / 1'000'000) << " ms\n";
        std::cout << "  Throughput:        " << static_cast<uint64_t>(ops_per_sec) << " ops/s\n\n";
    }

    std::cout << "\nDone.\n";
    return 0;
}

Your matching engine now runs on a dedicated thread, decoupled from the feed source. In the next step, you'll build a mean-reversion trading strategy that monitors prices and submits orders back into this pipeline.

Implement the Trading Strategy & Performance Metrics

Your matching engine processes orders in nanoseconds, and the multi-threaded pipeline feeds them through a lock-free queue. But an exchange without traders is just an empty building. Now you'll add an algorithmic participant that watches prices, detects opportunities, and submits orders automatically.

In this step, you'll implement a mean-reversion trading strategy. The core idea is simple: when the price drops too far below its recent average, it tends to bounce back up (so you buy). When it rises too far above, it tends to fall back (so you sell). You'll also build a performance metrics system to measure how profitable the strategy is using industry-standard measures like Sharpe ratio and maximum drawdown.

In this step, get ready to:
  • Build a mean-reversion strategy that generates buy/sell signals from price deviations.
  • Implement performance metrics including PnL, Sharpe ratio, and max drawdown.
  • Connect the strategy to the matching engine and run it against 100K orders.
Create the mean-reversion strategy header

The strategy needs a configuration struct to hold tunable parameters and a class that tracks price history, computes a simple moving average (SMA), and generates order signals when the current price deviates enough from the average.

  • Create the file include/strategy.hpp and add the following code:
#pragma once

#include "types.hpp"
#include "matching_engine.hpp"
#include <vector>
#include <deque>
#include <cstdint>

namespace engine {

struct StrategyConfig {
    uint32_t sma_window{50};       // Number of mid-price samples for SMA
    double entry_threshold{0.002}; // 0.2% deviation to trigger signal
    Quantity order_size{10};
    int32_t max_position{100};     // Maximum absolute position
    OrderId id_start{10'000'000};  // Starting order ID for strategy orders
};

class MeanReversionStrategy {
public:
    explicit MeanReversionStrategy(StrategyConfig config);

    // Feed a new mid-price observation
    void on_mid_price(Price mid_price);

    // Get pending orders to submit (call after on_mid_price)
    [[nodiscard]] std::vector<OrderRequest> get_pending_orders();

    // Notify of a fill
    void on_fill(Side side, Quantity qty, Price price);

    // Position and PnL
    [[nodiscard]] int32_t position() const noexcept { return position_; }
    [[nodiscard]] double realized_pnl() const noexcept { return realized_pnl_; }
    [[nodiscard]] double unrealized_pnl(Price current_mid) const noexcept;
    [[nodiscard]] const std::string& name() const noexcept { return name_; }

private:
    [[nodiscard]] double compute_sma() const noexcept;

    StrategyConfig config_;
    std::string name_{"MeanReversion"};
    std::deque<Price> price_history_;
    std::vector<OrderRequest> pending_orders_;

    int32_t position_{0};
    double realized_pnl_{0.0};
    double avg_entry_price_{0.0};
    OrderId next_order_id_;
};

} // namespace engine

What does this header define?

StrategyConfig holds all tunable parameters. The sma_window of 50 means the strategy looks at the last 50 mid-price observations to compute its moving average. The entry_threshold of 0.002 (0.2%) is how far the price must deviate before a signal fires.

MeanReversionStrategy uses a std::deque for price history (efficient push_back and pop_front for a sliding window). The id_start at 10 million ensures strategy order IDs never collide with generator-produced orders.

  • Save include/strategy.hpp.
Implement the strategy logic

The implementation file contains the signal generation logic and position management. When the mid-price drops below the SMA by more than the threshold, the strategy generates a buy order. When it rises above, it generates a sell. Position tracking ensures realized PnL is computed correctly when trades close.

  • Create the file src/strategy.cpp and add the following code:
#include "strategy.hpp"
#include <numeric>

namespace engine {

MeanReversionStrategy::MeanReversionStrategy(StrategyConfig config)
    : config_(config), next_order_id_(config.id_start) {}

void MeanReversionStrategy::on_mid_price(Price mid_price) {
    price_history_.push_back(mid_price);
    if (price_history_.size() > config_.sma_window) {
        price_history_.pop_front();
    }

    if (price_history_.size() < config_.sma_window) return;

    double sma = compute_sma();
    double current = static_cast<double>(mid_price);
    double deviation = (current - sma) / sma;

    pending_orders_.clear();

    // Mean reversion: buy when below SMA, sell when above
    if (deviation < -config_.entry_threshold &&
        position_ < config_.max_position) {
        OrderRequest req{};
        req.id = next_order_id_++;
        req.price = mid_price + 1; // Slightly aggressive
        req.quantity = config_.order_size;
        req.side = Side::Buy;
        req.type = OrderType::Limit;
        pending_orders_.push_back(req);
    } else if (deviation > config_.entry_threshold &&
               position_ > -config_.max_position) {
        OrderRequest req{};
        req.id = next_order_id_++;
        req.price = mid_price - 1; // Slightly aggressive
        req.quantity = config_.order_size;
        req.side = Side::Sell;
        req.type = OrderType::Limit;
        pending_orders_.push_back(req);
    }
}

std::vector<OrderRequest> MeanReversionStrategy::get_pending_orders() {
    return std::move(pending_orders_);
}

void MeanReversionStrategy::on_fill(Side side, Quantity qty, Price price) {
    double fill_value = static_cast<double>(price) * static_cast<double>(qty);

    if (side == Side::Buy) {
        // Buying: if we're short, this closes some short position
        if (position_ < 0) {
            Quantity closing = std::min(static_cast<Quantity>(-position_), qty);
            realized_pnl_ += (avg_entry_price_ - static_cast<double>(price))
                              * static_cast<double>(closing);
            qty -= closing;
        }
        position_ += static_cast<int32_t>(qty + (position_ < 0 ?
            std::min(static_cast<Quantity>(-position_), qty) : 0));
        // Simplified: update avg entry for new position
        if (position_ > 0) {
            avg_entry_price_ = static_cast<double>(price);
        }
    } else {
        // Selling: if we're long, this closes some long position
        if (position_ > 0) {
            Quantity closing = std::min(static_cast<Quantity>(position_), qty);
            realized_pnl_ += (static_cast<double>(price) - avg_entry_price_)
                              * static_cast<double>(closing);
            qty -= closing;
        }
        position_ -= static_cast<int32_t>(qty + (position_ > 0 ?
            std::min(static_cast<Quantity>(position_), qty) : 0));
        if (position_ < 0) {
            avg_entry_price_ = static_cast<double>(price);
        }
    }
}

double MeanReversionStrategy::unrealized_pnl(Price current_mid) const noexcept {
    if (position_ == 0) return 0.0;
    double current = static_cast<double>(current_mid);
    if (position_ > 0) {
        return (current - avg_entry_price_) * static_cast<double>(position_);
    } else {
        return (avg_entry_price_ - current) * static_cast<double>(-position_);
    }
}

double MeanReversionStrategy::compute_sma() const noexcept {
    if (price_history_.empty()) return 0.0;
    double sum = 0.0;
    for (Price p : price_history_) {
        sum += static_cast<double>(p);
    }
    return sum / static_cast<double>(price_history_.size());
}

} // namespace engine

What does this code do?

on_mid_price() maintains a rolling window of prices using a deque. Once the window is full (50 samples), it computes the SMA and checks if the current price deviates by more than 0.2%. A negative deviation triggers a buy signal. A positive deviation triggers a sell signal.

on_fill() handles the PnL math. When closing a position (buying to cover a short, or selling a long), it computes the realized profit. The max_position check in on_mid_price() acts as a basic risk check to prevent runaway accumulation.

  • Save src/strategy.cpp.

Why mean reversion instead of trend-following?

Mean reversion is the bread-and-butter strategy at high frequencies. When order flow creates a temporary imbalance, the price tends to snap back toward its fair value. This works best in range-bound markets with tight spreads.

Trend-following (momentum) strategies work better in directional markets. In the Secret Mission, you'll implement a momentum strategy and compare the two head-to-head.

Seeing a compile error about deque?

Make sure #include <deque> is in your header file. Also verify that src/strategy.cpp is listed in your CMakeLists.txt under engine_lib sources.

Help me fix my strategy.hpp compilation error

Build the performance metrics tracker

A strategy is only as good as the numbers prove. You need standard quantitative metrics to evaluate performance: total PnL, Sharpe ratio (risk-adjusted return), maximum drawdown (worst peak-to-trough loss), and win rate.

  • Create the file include/metrics.hpp and add the following code:
#pragma once

#include "types.hpp"
#include <vector>
#include <string>
#include <cmath>

namespace engine {

class PerformanceMetrics {
public:
    void record_trade(double pnl_change);
    void record_equity(double equity);

    [[nodiscard]] double total_pnl() const noexcept;
    [[nodiscard]] double sharpe_ratio() const noexcept;
    [[nodiscard]] double max_drawdown() const noexcept;
    [[nodiscard]] double win_rate() const noexcept;
    [[nodiscard]] uint64_t total_trades() const noexcept { return trade_pnls_.size(); }

    void print_report(const std::string& strategy_name) const;

private:
    std::vector<double> trade_pnls_;
    std::vector<double> equity_curve_;
};

} // namespace engine

What does this header define?

PerformanceMetrics collects two data series. The trade_pnls_ vector stores the PnL change from each trade (used for Sharpe and win rate). The equity_curve_ vector stores snapshots of total equity over time (used for max drawdown).

  • Save include/metrics.hpp.
  • Now create src/metrics.cpp and add the full implementation:
#include "metrics.hpp"
#include <iostream>
#include <iomanip>
#include <numeric>
#include <algorithm>

namespace engine {

void PerformanceMetrics::record_trade(double pnl_change) {
    trade_pnls_.push_back(pnl_change);
}

void PerformanceMetrics::record_equity(double equity) {
    equity_curve_.push_back(equity);
}

double PerformanceMetrics::total_pnl() const noexcept {
    return std::accumulate(trade_pnls_.begin(), trade_pnls_.end(), 0.0);
}

double PerformanceMetrics::sharpe_ratio() const noexcept {
    if (trade_pnls_.size() < 2) return 0.0;

    double mean = total_pnl() / static_cast<double>(trade_pnls_.size());
    double variance = 0.0;
    for (double pnl : trade_pnls_) {
        variance += (pnl - mean) * (pnl - mean);
    }
    variance /= static_cast<double>(trade_pnls_.size() - 1);
    double stddev = std::sqrt(variance);

    if (stddev == 0.0) return 0.0;
    // Annualize assuming ~252 trading days, ~1000 observations per day
    return (mean / stddev) * std::sqrt(252'000.0);
}

double PerformanceMetrics::max_drawdown() const noexcept {
    if (equity_curve_.empty()) return 0.0;

    double peak = equity_curve_[0];
    double max_dd = 0.0;

    for (double equity : equity_curve_) {
        if (equity > peak) peak = equity;
        double dd = (peak - equity) / peak;
        if (dd > max_dd) max_dd = dd;
    }
    return max_dd;
}

double PerformanceMetrics::win_rate() const noexcept {
    if (trade_pnls_.empty()) return 0.0;
    uint64_t wins = 0;
    for (double pnl : trade_pnls_) {
        if (pnl > 0.0) ++wins;
    }
    return static_cast<double>(wins) / static_cast<double>(trade_pnls_.size());
}

void PerformanceMetrics::print_report(const std::string& strategy_name) const {
    std::cout << "\n===== " << strategy_name << " Performance Report =====\n";
    std::cout << std::fixed << std::setprecision(2);
    std::cout << "  Total Trades:    " << total_trades() << "\n";
    std::cout << "  Total PnL:       " << total_pnl() << "\n";
    std::cout << "  Sharpe Ratio:    " << sharpe_ratio() << "\n";
    std::cout << "  Max Drawdown:    " << (max_drawdown() * 100.0) << "%\n";
    std::cout << "  Win Rate:        " << (win_rate() * 100.0) << "%\n";
    std::cout << "==========================================\n";
}

} // namespace engine

What does this code do?

sharpe_ratio() computes the annualized Sharpe ratio using the standard formula: mean return divided by standard deviation, then scaled by the square root of the number of observations per year. The scaling factor sqrt(252'000.0) assumes 252 trading days with about 1,000 observations each.

max_drawdown() walks the equity curve, tracking the peak. Whenever equity drops below the peak, it computes the percentage decline. The worst decline across the entire run is the maximum drawdown. This tells you the worst loss a strategy has experienced.

  • Save src/metrics.cpp.

Seeing linker errors about std::accumulate?

Make sure #include <numeric> is at the top of src/metrics.cpp. Also confirm src/metrics.cpp is listed in CMakeLists.txt under the engine_lib target.

Help me fix my metrics build error

Connect the strategy to the engine and run Phase 3

Now you'll wire everything together. The strategy receives trade callbacks from the engine, computes signals after each order, and submits its own orders back into the book. This creates a feedback loop where the strategy is an active participant in the market.

  • In src/main.cpp, add the strategy and metrics includes at the top of the file alongside the existing includes:
#include "matching_engine.hpp"
#include "order_generator.hpp"
#include "feed_handler.hpp"
#include "strategy.hpp"
#include "metrics.hpp"
#include "benchmark.hpp"
#include <iostream>
#include <chrono>
  • Now add Phase 3 at the bottom of main(), after the Phase 2 closing brace and before return 0;:
    // Phase 3: Algorithmic Trading Strategy
    std::cout << "[Phase 3] Mean-Reversion Trading Strategy\n";
    {
        MatchingEngine engine;
        PerformanceMetrics metrics;

        StrategyConfig strat_config{};
        strat_config.sma_window = 50;
        strat_config.entry_threshold = 0.002;
        strat_config.order_size = 10;
        MeanReversionStrategy strategy(strat_config);

        // Generate market data and run strategy
        GeneratorConfig gen_config{};
        gen_config.base_price = 10000;
        gen_config.spread = 5;
        gen_config.market_order_ratio = 0.3; // More aggressive for price movement
        OrderGenerator generator(gen_config);

        auto orders = generator.generate(100'000);
        double equity = 0.0;

        engine.set_trade_callback([&](const Trade& trade) {
            // Check if this is a strategy fill
            if (trade.buy_order_id >= strat_config.id_start ||
                trade.sell_order_id >= strat_config.id_start) {
                Side fill_side = (trade.buy_order_id >= strat_config.id_start)
                                 ? Side::Buy : Side::Sell;
                strategy.on_fill(fill_side, trade.quantity, trade.price);
            }
        });

        for (const auto& order : orders) {
            engine.submit(order);

            // Update strategy with current mid-price
            auto mid = engine.book().mid_price();
            if (mid.has_value()) {
                strategy.on_mid_price(mid.value());

                auto strat_orders = strategy.get_pending_orders();
                for (const auto& so : strat_orders) {
                    engine.submit(so);
                }

                // Record equity
                equity = strategy.realized_pnl() + strategy.unrealized_pnl(mid.value());
                metrics.record_equity(equity);
            }
        }

        std::cout << "  Final position:   " << strategy.position() << "\n";
        std::cout << "  Realized PnL:     " << strategy.realized_pnl() << "\n";

        auto mid = engine.book().mid_price();
        if (mid.has_value()) {
            std::cout << "  Unrealized PnL:   " << strategy.unrealized_pnl(mid.value()) << "\n";
            std::cout << "  Total PnL:        "
                      << (strategy.realized_pnl() + strategy.unrealized_pnl(mid.value())) << "\n";
        }

        metrics.print_report("MeanReversion");
    }

    std::cout << "\nDone.\n";

What does this code do?

The trade callback checks if a fill belongs to the strategy (by comparing order IDs against id_start). After each market order is processed, the strategy receives the current mid-price, decides whether to trade, and submits orders back into the engine.

The generator uses a tighter spread (5 vs 10) and a higher market order ratio (0.3 vs 0.1) to create more price movement. This gives the mean-reversion strategy more opportunities to detect deviations.

  • Save src/main.cpp.
  • Rebuild and run the project by running these commands:
cmake --build build
./build/order_engine

You should see all three phases run. Phase 3 prints the strategy's final position, realized PnL, unrealized PnL, total PnL, and a full performance report with Sharpe ratio, max drawdown, and win rate.

Build fails with undefined reference errors?

Make sure both src/strategy.cpp and src/metrics.cpp are listed in your CMakeLists.txt under the engine_lib sources. Also verify you included #include "strategy.hpp" and #include "metrics.hpp" at the top of src/main.cpp.

Help me fix my Phase 3 build error

✔️ Awesome, I've got everything!

Great. Double check you've saved all four files: include/strategy.hpp, src/strategy.cpp, include/metrics.hpp, and src/metrics.cpp.

ⓧ I'd like to double check the full code

Here is the full code for each file you created or modified in this step.

#pragma once

#include "types.hpp"
#include "matching_engine.hpp"
#include <vector>
#include <deque>
#include <cstdint>

namespace engine {

struct StrategyConfig {
    uint32_t sma_window{50};       // Number of mid-price samples for SMA
    double entry_threshold{0.002}; // 0.2% deviation to trigger signal
    Quantity order_size{10};
    int32_t max_position{100};     // Maximum absolute position
    OrderId id_start{10'000'000};  // Starting order ID for strategy orders
};

class MeanReversionStrategy {
public:
    explicit MeanReversionStrategy(StrategyConfig config);

    // Feed a new mid-price observation
    void on_mid_price(Price mid_price);

    // Get pending orders to submit (call after on_mid_price)
    [[nodiscard]] std::vector<OrderRequest> get_pending_orders();

    // Notify of a fill
    void on_fill(Side side, Quantity qty, Price price);

    // Position and PnL
    [[nodiscard]] int32_t position() const noexcept { return position_; }
    [[nodiscard]] double realized_pnl() const noexcept { return realized_pnl_; }
    [[nodiscard]] double unrealized_pnl(Price current_mid) const noexcept;
    [[nodiscard]] const std::string& name() const noexcept { return name_; }

private:
    [[nodiscard]] double compute_sma() const noexcept;

    StrategyConfig config_;
    std::string name_{"MeanReversion"};
    std::deque<Price> price_history_;
    std::vector<OrderRequest> pending_orders_;

    int32_t position_{0};
    double realized_pnl_{0.0};
    double avg_entry_price_{0.0};
    OrderId next_order_id_;
};

} // namespace engine
#include "strategy.hpp"
#include <numeric>

namespace engine {

MeanReversionStrategy::MeanReversionStrategy(StrategyConfig config)
    : config_(config), next_order_id_(config.id_start) {}

void MeanReversionStrategy::on_mid_price(Price mid_price) {
    price_history_.push_back(mid_price);
    if (price_history_.size() > config_.sma_window) {
        price_history_.pop_front();
    }

    if (price_history_.size() < config_.sma_window) return;

    double sma = compute_sma();
    double current = static_cast<double>(mid_price);
    double deviation = (current - sma) / sma;

    pending_orders_.clear();

    // Mean reversion: buy when below SMA, sell when above
    if (deviation < -config_.entry_threshold &&
        position_ < config_.max_position) {
        OrderRequest req{};
        req.id = next_order_id_++;
        req.price = mid_price + 1; // Slightly aggressive
        req.quantity = config_.order_size;
        req.side = Side::Buy;
        req.type = OrderType::Limit;
        pending_orders_.push_back(req);
    } else if (deviation > config_.entry_threshold &&
               position_ > -config_.max_position) {
        OrderRequest req{};
        req.id = next_order_id_++;
        req.price = mid_price - 1; // Slightly aggressive
        req.quantity = config_.order_size;
        req.side = Side::Sell;
        req.type = OrderType::Limit;
        pending_orders_.push_back(req);
    }
}

std::vector<OrderRequest> MeanReversionStrategy::get_pending_orders() {
    return std::move(pending_orders_);
}

void MeanReversionStrategy::on_fill(Side side, Quantity qty, Price price) {
    double fill_value = static_cast<double>(price) * static_cast<double>(qty);

    if (side == Side::Buy) {
        // Buying: if we're short, this closes some short position
        if (position_ < 0) {
            Quantity closing = std::min(static_cast<Quantity>(-position_), qty);
            realized_pnl_ += (avg_entry_price_ - static_cast<double>(price))
                              * static_cast<double>(closing);
            qty -= closing;
        }
        position_ += static_cast<int32_t>(qty + (position_ < 0 ?
            std::min(static_cast<Quantity>(-position_), qty) : 0));
        // Simplified: update avg entry for new position
        if (position_ > 0) {
            avg_entry_price_ = static_cast<double>(price);
        }
    } else {
        // Selling: if we're long, this closes some long position
        if (position_ > 0) {
            Quantity closing = std::min(static_cast<Quantity>(position_), qty);
            realized_pnl_ += (static_cast<double>(price) - avg_entry_price_)
                              * static_cast<double>(closing);
            qty -= closing;
        }
        position_ -= static_cast<int32_t>(qty + (position_ > 0 ?
            std::min(static_cast<Quantity>(position_), qty) : 0));
        if (position_ < 0) {
            avg_entry_price_ = static_cast<double>(price);
        }
    }
}

double MeanReversionStrategy::unrealized_pnl(Price current_mid) const noexcept {
    if (position_ == 0) return 0.0;
    double current = static_cast<double>(current_mid);
    if (position_ > 0) {
        return (current - avg_entry_price_) * static_cast<double>(position_);
    } else {
        return (avg_entry_price_ - current) * static_cast<double>(-position_);
    }
}

double MeanReversionStrategy::compute_sma() const noexcept {
    if (price_history_.empty()) return 0.0;
    double sum = 0.0;
    for (Price p : price_history_) {
        sum += static_cast<double>(p);
    }
    return sum / static_cast<double>(price_history_.size());
}

} // namespace engine
#pragma once

#include "types.hpp"
#include <vector>
#include <string>
#include <cmath>

namespace engine {

class PerformanceMetrics {
public:
    void record_trade(double pnl_change);
    void record_equity(double equity);

    [[nodiscard]] double total_pnl() const noexcept;
    [[nodiscard]] double sharpe_ratio() const noexcept;
    [[nodiscard]] double max_drawdown() const noexcept;
    [[nodiscard]] double win_rate() const noexcept;
    [[nodiscard]] uint64_t total_trades() const noexcept { return trade_pnls_.size(); }

    void print_report(const std::string& strategy_name) const;

private:
    std::vector<double> trade_pnls_;
    std::vector<double> equity_curve_;
};

} // namespace engine
#include "metrics.hpp"
#include <iostream>
#include <iomanip>
#include <numeric>
#include <algorithm>

namespace engine {

void PerformanceMetrics::record_trade(double pnl_change) {
    trade_pnls_.push_back(pnl_change);
}

void PerformanceMetrics::record_equity(double equity) {
    equity_curve_.push_back(equity);
}

double PerformanceMetrics::total_pnl() const noexcept {
    return std::accumulate(trade_pnls_.begin(), trade_pnls_.end(), 0.0);
}

double PerformanceMetrics::sharpe_ratio() const noexcept {
    if (trade_pnls_.size() < 2) return 0.0;

    double mean = total_pnl() / static_cast<double>(trade_pnls_.size());
    double variance = 0.0;
    for (double pnl : trade_pnls_) {
        variance += (pnl - mean) * (pnl - mean);
    }
    variance /= static_cast<double>(trade_pnls_.size() - 1);
    double stddev = std::sqrt(variance);

    if (stddev == 0.0) return 0.0;
    // Annualize assuming ~252 trading days, ~1000 observations per day
    return (mean / stddev) * std::sqrt(252'000.0);
}

double PerformanceMetrics::max_drawdown() const noexcept {
    if (equity_curve_.empty()) return 0.0;

    double peak = equity_curve_[0];
    double max_dd = 0.0;

    for (double equity : equity_curve_) {
        if (equity > peak) peak = equity;
        double dd = (peak - equity) / peak;
        if (dd > max_dd) max_dd = dd;
    }
    return max_dd;
}

double PerformanceMetrics::win_rate() const noexcept {
    if (trade_pnls_.empty()) return 0.0;
    uint64_t wins = 0;
    for (double pnl : trade_pnls_) {
        if (pnl > 0.0) ++wins;
    }
    return static_cast<double>(wins) / static_cast<double>(trade_pnls_.size());
}

void PerformanceMetrics::print_report(const std::string& strategy_name) const {
    std::cout << "\n===== " << strategy_name << " Performance Report =====\n";
    std::cout << std::fixed << std::setprecision(2);
    std::cout << "  Total Trades:    " << total_trades() << "\n";
    std::cout << "  Total PnL:       " << total_pnl() << "\n";
    std::cout << "  Sharpe Ratio:    " << sharpe_ratio() << "\n";
    std::cout << "  Max Drawdown:    " << (max_drawdown() * 100.0) << "%\n";
    std::cout << "  Win Rate:        " << (win_rate() * 100.0) << "%\n";
    std::cout << "==========================================\n";
}

} // namespace engine
#include "matching_engine.hpp"
#include "order_generator.hpp"
#include "feed_handler.hpp"
#include "strategy.hpp"
#include "metrics.hpp"
#include "benchmark.hpp"
#include <iostream>
#include <chrono>

using namespace engine;

int main() {
    std::cout << "=== Low-Latency Order Book & Matching Engine ===\n\n";

    // Phase 1: Single-threaded benchmark
    std::cout << "[Phase 1] Single-threaded matching engine\n";
    {
        MatchingEngine engine;
        uint64_t trade_count = 0;
        engine.set_trade_callback([&](const Trade&) { ++trade_count; });

        GeneratorConfig gen_config{};
        gen_config.base_price = 10000;
        gen_config.spread = 10;
        OrderGenerator generator(gen_config);

        auto orders = generator.generate(1'000'000);

        auto start = std::chrono::steady_clock::now();
        for (const auto& order : orders) {
            engine.submit(order);
        }
        auto end = std::chrono::steady_clock::now();

        auto duration_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
        double ops_per_sec = 1'000'000.0 / (static_cast<double>(duration_ns) / 1e9);

        std::cout << "  Orders processed: 1,000,000\n";
        std::cout << "  Trades generated: " << trade_count << "\n";
        std::cout << "  Total time:       " << (duration_ns / 1'000'000) << " ms\n";
        std::cout << "  Throughput:       " << static_cast<uint64_t>(ops_per_sec) << " ops/s\n";
        std::cout << "  Avg latency:      " << (duration_ns / 1'000'000) << " ns/op\n\n";
    }

    // Phase 2: Multi-threaded pipeline
    std::cout << "[Phase 2] Multi-threaded pipeline (Feed → SPSC → Engine)\n";
    {
        SPSCQueue<OrderMessage, QUEUE_CAPACITY> queue;
        MatchingEngine engine;
        uint64_t trade_count = 0;
        engine.set_trade_callback([&](const Trade&) { ++trade_count; });

        GeneratorConfig gen_config{};
        gen_config.base_price = 10000;
        gen_config.spread = 10;
        OrderGenerator generator(gen_config);

        auto orders = generator.generate(1'000'000);
        std::vector<OrderId> cancels; // No cancels for this test

        FeedHandler feed(queue);
        EngineThread engine_thread(queue, engine);

        auto start = std::chrono::steady_clock::now();
        engine_thread.start();
        feed.start(orders, cancels);

        feed.join();
        engine_thread.join();
        auto end = std::chrono::steady_clock::now();

        auto duration_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
        double ops_per_sec = 1'000'000.0 / (static_cast<double>(duration_ns) / 1e9);

        std::cout << "  Messages sent:     " << feed.messages_sent() << "\n";
        std::cout << "  Messages processed:" << engine_thread.messages_processed() << "\n";
        std::cout << "  Trades generated:  " << trade_count << "\n";
        std::cout << "  Total time:        " << (duration_ns / 1'000'000) << " ms\n";
        std::cout << "  Throughput:        " << static_cast<uint64_t>(ops_per_sec) << " ops/s\n\n";
    }

    // Phase 3: Algorithmic Trading Strategy
    std::cout << "[Phase 3] Mean-Reversion Trading Strategy\n";
    {
        MatchingEngine engine;
        PerformanceMetrics metrics;

        StrategyConfig strat_config{};
        strat_config.sma_window = 50;
        strat_config.entry_threshold = 0.002;
        strat_config.order_size = 10;
        MeanReversionStrategy strategy(strat_config);

        // Generate market data and run strategy
        GeneratorConfig gen_config{};
        gen_config.base_price = 10000;
        gen_config.spread = 5;
        gen_config.market_order_ratio = 0.3; // More aggressive for price movement
        OrderGenerator generator(gen_config);

        auto orders = generator.generate(100'000);
        double equity = 0.0;

        engine.set_trade_callback([&](const Trade& trade) {
            // Check if this is a strategy fill
            if (trade.buy_order_id >= strat_config.id_start ||
                trade.sell_order_id >= strat_config.id_start) {
                Side fill_side = (trade.buy_order_id >= strat_config.id_start)
                                 ? Side::Buy : Side::Sell;
                strategy.on_fill(fill_side, trade.quantity, trade.price);
            }
        });

        for (const auto& order : orders) {
            engine.submit(order);

            // Update strategy with current mid-price
            auto mid = engine.book().mid_price();
            if (mid.has_value()) {
                strategy.on_mid_price(mid.value());

                auto strat_orders = strategy.get_pending_orders();
                for (const auto& so : strat_orders) {
                    engine.submit(so);
                }

                // Record equity
                equity = strategy.realized_pnl() + strategy.unrealized_pnl(mid.value());
                metrics.record_equity(equity);
            }
        }

        std::cout << "  Final position:   " << strategy.position() << "\n";
        std::cout << "  Realized PnL:     " << strategy.realized_pnl() << "\n";

        auto mid = engine.book().mid_price();
        if (mid.has_value()) {
            std::cout << "  Unrealized PnL:   " << strategy.unrealized_pnl(mid.value()) << "\n";
            std::cout << "  Total PnL:        "
                      << (strategy.realized_pnl() + strategy.unrealized_pnl(mid.value())) << "\n";
        }

        metrics.print_report("MeanReversion");
    }

    std::cout << "\nDone.\n";
    return 0;
}

Your matching engine is now a complete trading system. It processes orders, matches them, and runs an algorithmic strategy that makes autonomous trading decisions with risk controls and performance tracking. In the Secret Mission, you'll add a competing momentum strategy and see which approach wins.

Secret mission

Competing Strategies

Your mean-reversion strategy profits when prices snap back to the mean. But what about trending markets? Build a momentum strategy that takes the opposite bet, run both head-to-head on the same order book, and discover which market regime favors which approach.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and creates no cloud resources. There are no ongoing costs or subscriptions to worry about.

Decide whether to keep your resources as they are, free up disk space from build artifacts, or delete everything entirely.

Resources you used:

  • Project directory (order_engine on your Desktop) containing all source code, headers, tests, and benchmarks.
  • CMake build artifacts (the build/ directory inside your project).
  • Generated output file (strategy_comparison.csv inside your project directory).

Keep everything running

No action needed. Choose this if you plan to keep experimenting with your matching engine, adding new strategies, or tuning parameters.

  • Your project directory stays on your Desktop with all source code, build artifacts, and CSV output intact.
  • You can rebuild anytime by running cmake --build build from your project directory.

Pause - I'll come back to this later

Free up disk space from compiled binaries while keeping your source code intact. Build artifacts can consume hundreds of megabytes.

  • Delete the build directory to reclaim disk space by running:
cd ~/Desktop/order_engine
rm -rf build
  • Your source code, headers, tests, and CMakeLists.txt remain untouched.
  • When you return, rebuild by running cmake -S . -B build followed by cmake --build build from your project directory.

Delete - I don't want to use this again

Remove all project files from your machine. This permanently deletes your source code, build artifacts, and generated output.

  • Delete the entire project directory by running this command (macOS/Linux):
rm -rf ~/Desktop/order_engine
  • On Windows, run this instead:
rmdir /s /q %USERPROFILE%\Desktop\order_engine
  • Confirm the directory is gone by running:
ls ~/Desktop | grep order_engine
  • You should see no output, confirming the project has been removed.

Nice Work!

Nice Work!

You just built a production-style low-latency order book and matching engine from scratch in C++20. This is the exact infrastructure that powers real financial exchanges around the world.

Here's what you accomplished:

  • Built a price-time priority matching engine with a custom memory pool allocator, O(1) cancel via hash map lookup, and nanosecond-level benchmarking.
  • Implemented a lock-free SPSC ring buffer using std::atomic with acquire/release memory ordering, and wired it into a multi-threaded feed handler pipeline.
  • Created a mean-reversion trading strategy with position tracking, risk limits, and performance metrics including Sharpe ratio and max drawdown computation.
  • Secret Mission: Ran a momentum strategy head-to-head against mean-reversion on the same order book, then swept parameters to identify which strategy wins in which market regime.

Ready to quiz yourself?