Skip to content

Adigain/Corex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Corex Matching Engine

Corex is a production-quality, low-latency matching engine written in modern C++17. It is designed for correctness, performance, and maintainability, making it suitable for high-frequency trading simulations or crypto-exchange backends.

The project strictly focuses on zero-allocation fast paths, cache-friendly data structures, and predictable performance.

Core Features & Architecture

Corex relies on several advanced C++ paradigms and system design principles to achieve sub-microsecond latency and uncompromising correctness:

  • Zero-Allocation Hot Path via Intrusive Containers Instead of utilizing std::list or vectors for the orderbook's price levels (which would cause continuous heap allocations and cache misses), Corex embeds boost::intrusive::list_base_hook directly inside the Order structures. This ensures that inserting, moving, or canceling an order is reduced to mere pointer manipulation without invoking the system memory allocator, avoiding OS-level latency spikes during peak market volatility.

  • O(1) Symbol Lookup via robin_hood Hashing To route millions of orders per second to the correct orderbook, Corex replaces the standard std::unordered_map with robin_hood::unordered_map. Standard library unordered maps use separate chaining (linked lists for buckets), causing severe cache thrashing due to pointer-chasing. The robin_hood flat hash map utilizes open addressing with linear probing and backward shift deletion, ensuring that symbol routing is executed almost entirely out of the CPU's L1/L2 cache for ultra-fast, predictable O(1) lookups.

  • Dual-Mode Execution Architecture

    • Market (Deterministic Single-Threaded): Designed for extreme low-latency environments where locking overhead is unacceptable. It operates entirely lock-free from the caller's thread, processing sequences in deterministic order for reproducible backtesting and high-frequency trading.
    • ConcurrentMarket (Asynchronous Thread-Pool): Utilizes a dedicated thread pool and condition-variable backed lock queues for offloading orderbook management. Designed for scenarios where multiple worker threads process disconnected symbols concurrently, allowing frontend producers to submit orders asynchronously without blocking.
  • Comprehensive Order Types with O(1) Execution

    • Limit & Market Orders: Matches aggressively against the MapOrderBook liquidity.
    • Fill-Or-Kill (FOK): Atomic execution ensuring either full quantity is instantly filled or the order is entirely canceled, crucial for zero-slippage strategies.
    • Stop & Trailing Stop: Maintained in separate internal triggers and activated efficiently into the main book. Trailing stops dynamically adjust to market prices using efficient C++17 std::map::extract() node manipulation, avoiding full container rebuilds.
  • Event-Driven Zero-Copy Snapshots To integrate with external gateways or risk-management systems, Corex emits events (OrderAdded, ExecutedOrder, OrderDeleted). Instead of blindly copying the 100+ byte internal Order objects (which contain intrusive list hooks), it constructs lightweight, stripped-down OrderSnapshot objects by value. This decouples internal book state from external event handlers and significantly reduces memory bandwidth requirements.

  • Robust Boundary Validation & Modern C++17 The engine leverages move semantics, constexpr, and strictly-defined exceptions (std::invalid_argument) at the API boundary, guaranteeing that internal algorithms only execute on validated, sanitized data. This eliminates undefined behavior (like unsigned underflows or infinite loops) when processing malformed market inputs.

Build Instructions

Corex uses CMake for its build system. Dependencies like Google Test, Google Benchmark, and spdlog are fetched automatically.

# Clone the repository
git clone https://github.com/yourusername/corex.git
cd corex

# Create a build directory
mkdir build && cd build

# Configure and compile with optimizations enabled
cmake -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)

Running the Project

After building, you can run the examples provided in the bin/ directory:

# Run the single-threaded market example
./bin/corex_market_example

# Run the concurrent market example
./bin/corex_concurrent_market_example

Running Tests

The test suite validates the correctness of the matching algorithms, orderbook updates, and error handling.

# Run all unit tests
./bin/corex_tests

Benchmarks

Corex includes comprehensive microbenchmarks for both the single-threaded (Market) and multi-threaded (ConcurrentMarket) engines. Benchmarks measure the time taken to process millions of orders across varying numbers of symbols (orderbooks).

Test Environment Specs:

  • CPU: 12th Gen Intel(R) Core(TM) i7-12700H (14 Cores / 20 Threads, up to 4.7 GHz)
  • Caches: L1: 1.25 MiB, L2: 11.5 MiB, L3: 24 MiB
  • OS: Linux (x86_64)

To reproduce these benchmarks on your machine:

./bin/corex_benchmark
./bin/concurrent_corex_benchmark

Single-Threaded (Market) Benchmark Results

Benchmark Time (ms) CPU (ms) Iterations
BM_Market/symbols:1/orders:1000000 199 199 3
BM_Market/symbols:1/orders:2000000 419 419 2
BM_Market/symbols:1/orders:3000000 619 619 1
BM_Market/symbols:1/orders:4000000 909 909 1
BM_Market/symbols:100/orders:1000000 210 210 3
BM_Market/symbols:100/orders:2000000 492 492 2
BM_Market/symbols:100/orders:3000000 781 780 1
BM_Market/symbols:100/orders:4000000 1126 1126 1
BM_Market/symbols:1000/orders:1000000 315 315 2
BM_Market/symbols:1000/orders:2000000 981 981 1
BM_Market/symbols:1000/orders:3000000 1146 1145 1
BM_Market/symbols:1000/orders:4000000 1638 1632 1
BM_Market/symbols:2000/orders:1000000 446 444 2
BM_Market/symbols:2000/orders:2000000 849 849 1
BM_Market/symbols:2000/orders:3000000 2170 2169 1
BM_Market/symbols:2000/orders:4000000 2421 2420 1

Concurrent (ConcurrentMarket) Benchmark Results

Benchmark Time (ms) CPU (ms) Iterations
BM_ConcurrentMarket/symbols:1/orders:1000000 492 329 2
BM_ConcurrentMarket/symbols:1/orders:2000000 1102 750 1
BM_ConcurrentMarket/symbols:1/orders:3000000 1639 1097 1
BM_ConcurrentMarket/symbols:1/orders:4000000 2225 1452 1
BM_ConcurrentMarket/symbols:100/orders:1000000 562 527 2
BM_ConcurrentMarket/symbols:100/orders:2000000 1255 1164 1
BM_ConcurrentMarket/symbols:100/orders:3000000 1666 1552 1
BM_ConcurrentMarket/symbols:100/orders:4000000 2338 2197 1
BM_ConcurrentMarket/symbols:1000/orders:1000000 503 476 2
BM_ConcurrentMarket/symbols:1000/orders:2000000 976 803 1
BM_ConcurrentMarket/symbols:1000/orders:3000000 1305 1185 1
BM_ConcurrentMarket/symbols:1000/orders:4000000 1777 1619 1
BM_ConcurrentMarket/symbols:2000/orders:1000000 471 440 2
BM_ConcurrentMarket/symbols:2000/orders:2000000 946 883 1
BM_ConcurrentMarket/symbols:2000/orders:3000000 1670 1297 1
BM_ConcurrentMarket/symbols:2000/orders:4000000 1940 1525 1

Performance Analysis: Single-Threaded vs. Concurrent

You may notice from the benchmark results that the ConcurrentMarket is generally slower in real execution time than the single-threaded Market, particularly for workloads concentrated on fewer symbols (e.g., processing 1M orders for a single symbol takes ~199ms sequentially vs. ~492ms concurrently).

This is a classic phenomenon in low-latency systems.

Why is concurrent slower here?

  1. Synchronization Overhead: Every order submitted to the concurrent market is packaged into a task, pushed onto a queue protected by a std::mutex, and signals a std::condition_variable. A worker thread must then wake up, lock the mutex, pop the task, and execute it. Because internal matching operations (like finding a node in std::map) take mere nanoseconds, the OS-level locking and thread wake-up times vastly overshadow the actual algorithmic work.
  2. Single-Symbol Contention: When all orders target the same symbol (symbols:1), parallelization offers zero benefit because the matching logic for a single orderbook must be strictly serialized. Threads end up contending for the same cache lines or waiting on each other.
  3. Cache Line Bouncing: Passing order data from a producer thread to a consumer worker thread forces CPU caches to be invalidated and synchronized across cores, introducing significant memory latency compared to a single-threaded loop operating out of L1 cache.

Future Work

To enable the concurrent engine to outperform the single-threaded engine under high load, the architecture will be evolved to address these bottlenecks:

  1. Sharding by Symbol: Instead of a generic global task queue, orderbooks (symbols) can be pinned to specific worker threads (sharded architecture). This completely eliminates mutexes during order execution—producer threads route orders to the correct thread-local queue, and workers operate lock-free on their assigned symbols.
  2. Lock-Free Queues: Replacing std::mutex queues with Single-Producer Single-Consumer (SPSC) or Multi-Producer Single-Consumer (MPSC) lock-free ring buffers (similar to the LMAX Disruptor pattern) will remove system calls and context-switching overhead from the hot path.
  3. Task Batching: Grouping incoming orders into batches before dispatching them to worker threads can significantly amortize synchronization costs.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages