diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 282442d..19d6821 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,21 +15,25 @@ jobs: - uses: actions/checkout@v4 with: submodules: 'false' - fetch-depth: 0 - name: Install dependencies run: | sudo apt update - sudo apt install -y cmake build-essential + sudo apt install -y cmake build-essential g++-11 libgtest-dev libbenchmark-dev + + - name: Verify Installation (Linux) + run: | + cmake --version + g++-11 --version - name: Configure run: cmake -B build -DBUILD_TESTS=ON -DBUILD_BENCHMARKS=OFF - #- name: Build - # run: cmake --build build --parallel - # - #- name: Run tests - # run: ./build/tests/velox_tests + - name: Build + run: cmake --build build --parallel + + - name: Run tests + run: ./build/tests/velox_tests windows-build: name: Windows Build @@ -39,13 +43,23 @@ jobs: - uses: actions/checkout@v4 with: submodules: 'false' - fetch-depth: 0 + + - name: Install Chocalatey Dependencies + shell: pwsh + run: | + choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System' -y + refreshenv + + - name: Verify Installation (Windows) + shell: pwsh + run: | + cmake --version - name: Configure run: cmake -B build -DBUILD_TESTS=ON -DBUILD_BENCHMARKS=OFF - #- name: Build - # run: cmake --build build --config Release --parallel - # - #- name: Run tests - # run: ./build/tests/Release/velox_tests.exe \ No newline at end of file + - name: Build + run: cmake --build build --config Release --parallel + + - name: Run tests + run: ./build/tests/Release/velox_tests.exe \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7e8eef5..28dbfca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ build/ build_*/ +market_data/ cmake-build-*/ .vscode/ .idea/ diff --git a/README.md b/README.md index 3d4cffd..49f8c1a 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ A low-latency trading engine built on a foundation of lock-free data structures. Velox is a C++17 trading engine targeting sub-microsecond order-to-execution latency. Every component in the hot path is allocation-free, lock-free, and cache-aware. The architecture maps each data flow to the correct concurrent primitive rather than reaching for a general-purpose queue everywhere. ``` -Market Feed → Decoder → Order Book → Matching Engine → Risk → Execution Gateway - ↑ ↓ - Symbol Config (RCU) Position Manager +Market Feed → Feed Handler → Order Book → Matching Engine → Risk Manager → Execution Gateway → Position Manager + ↓ + P&L Tracking ``` All inter-stage communication uses SPSC queues — the fastest possible channel when producer and consumer are known at design time. @@ -20,14 +20,14 @@ All inter-stage communication uses SPSC queues — the fastest possible channel ### Pipeline Stages -| Stage | Thread | Structure Used | Latency Budget | +| Stage | Thread | Structure Used | Latency | |---|---|---|---| -| Feed Handler | Core 0 | — | ~20ns | -| Market Data Decoder | Core 1 | SPSC Queue (feed→decoder) | ~50ns | -| Order Book Updater | Core 2 | SPSC Queue + HashMap | ~100ns | -| Matching Engine | Core 3 | Treiber Stack (level pool) | ~150ns | -| Risk Manager | Core 4 | Atomic Counter + HashMap | ~20ns | -| Execution Gateway | Core 5 | SPSC Queue (per strategy) | ~50ns | +| Feed Handler | Core 0 | ITCH 5.0 Parser | ~15 ns per message | +| Order Book | Core 1 | Sorted Vector + HashMap | ~26 ns per order | +| Matching Engine | Core 2 | Price-Time Priority | ~8 μs per match | +| Risk Manager | Core 3 | Atomic Counter + HashMap | ~4 ns per check | +| Execution Gateway | Core 4 | SPSC Queue + Object Pool | ~33 ns per report | +| Position Manager | Core 5 | Weighted Average P&L | ~23 ns per update | ### Lock-Free Primitives @@ -35,13 +35,13 @@ Each structure is chosen for the specific access pattern of its stage; not as a | Structure | Role | Why This One | |---|---|---| -| **SPSC Queue** | Inter-stage message passing | Single producer/consumer → wait-free, ~3ns per op | -| **Ring Buffer** | Market data capture and replay logging | Fixed allocation, power-of-2 masking, zero GC pressure | -| **HashMap** | Order book symbol lookups, risk exposure | O(1) reads on the critical path, open addressing | -| **Object Pool** | Order and fill object reuse | Eliminates `new`/`delete` in the hot path entirely | -| **Atomic Counter** | Order ID generation | Wait-free monotonic sequence, no coordination | -| **RCU** | Symbol config and reference data | Readers never stall; config changes are rare writes | -| **Treiber Stack** | Price level free-list | LIFO reuse of level nodes, ABA-safe with hazard pointers | +| **SPSC Queue** | Inter-stage message passing | Single producer/consumer → wait-free | +| **Ring Buffer** | Price level order queue | Fixed allocation, FIFO, cache-friendly | +| **HashMap** | Price level lookup, order cancellation | O(1) access on critical path | +| **Object Pool** | Order and report reuse | Eliminates `new`/`delete` in hot path | +| **Atomic Counter** | Sequence numbers, statistics | Wait-free monotonic increments | +| **PooledPtr** | RAII memory management | Automatic return to pool | +| **Hazard Pointers** | Treiber stack reclamation | Safe memory reuse | ### Why No MPMC Queue @@ -53,60 +53,35 @@ Multiple-producer scenarios are handled by **partitioning**: each strategy threa ``` mini-trading-engine/ -├── include/ -│ └── velox/ -| ├── feed/ -│ │ ├── feed_handler.hpp -│ │ └── decoder.hpp -│ │ -│ ├── book/ -│ │ ├── order_book.hpp -│ │ ├── price_level.hpp -│ │ └── book_snapshot.hpp -│ │ -│ ├── matching/ -│ │ ├── matching_engine.hpp -│ │ └── order.hpp -│ │ -│ ├── risk/ -│ │ ├── risk_manager.hpp -│ │ └── position_manager.hpp -│ │ -│ └── gateway/ -│ ├── execution_gateway.hpp -│ └── fix_encoder.hpp -├── src/ -│ ├── feed/ -│ │ ├── feed_handler.cpp # Raw market data ingestion -│ │ └── decoder.cpp # ITCH/FIX message decoding -│ ├── book/ -│ │ ├── order_book.cpp # Price level management -│ │ ├── price_level.cpp # Per-level order queue -│ │ └── book_snapshot.cpp # RCU-protected read view -│ ├── matching/ -│ │ ├── matching_engine.cpp # Price-time priority matching -│ │ └── order.cpp # Order struct (pool-allocated) -│ ├── risk/ -│ │ ├── risk_manager.cpp # Position limits and circuit breakers -│ │ └── position_manager.cpp # Atomic P&L and exposure tracking -│ └── gateway/ -│ ├── execution_gateway.cpp # Outbound order routing -│ └── fix_encoder.cpp # FIX 4.2 message formatting +├── include/velox/ +│ ├── feed/ +│ │ └── feed_handler.hpp # ITCH 5.0 parser +│ ├── book/ +│ │ ├── order_book.hpp # Price level management +│ │ ├── price_level.hpp # Ring buffer per level +│ │ └── book_snapshot.hpp # RCU-protected snapshots +│ ├── matching/ +│ │ ├── matching_engine.hpp # Order matching +│ │ └── order.hpp # Order struct +│ ├── risk/ +│ │ ├── risk_manager.hpp # Position limits +│ │ └── position_manager.hpp # P&L tracking +│ ├── gateway/ +│ │ └── execution_gateway.hpp # Report routing +│ └── core/ +│ └── symbol_engine.hpp # Per-symbol engine +├── src/ (matching .cpp files) ├── benchmarks/ -│ ├── bench_primitives.cpp # Lock-free structure microbenchmarks -│ ├── bench_pipeline.cpp # End-to-end pipeline throughput -│ └── bench_book.cpp # Order book update latency +│ └── (10+ benchmarking routines, including one for overall pipeline) ├── tests/ -│ └── ... -├── third_party/ # Third-party directory containing submodules -│ ├── benchmark/ # Google Benchmark library for performance testing -| | └── ... -│ ├── googletest/ # Google Test framework for unit testing -| | └── ... -│ └── whirlpool/ # Lock-free data structure library for core engine -| └── ... +│ └── (74+ unit and integration tests) ├── tools/ -│ └── itch_replay.cpp # Replay recorded NASDAQ ITCH 5.0 data +│ ├── create_mock_itch.cpp # Mock ITCH generator +│ └── itch_parser.cpp # Real ITCH replay tool +├── third_party/ +│ ├── googletest/ # Unit testing +│ ├── benchmark/ # Performance benchmarking +│ └── whirlpool/ # Custom lock-free library (submodule) └── CMakeLists.txt ``` @@ -119,7 +94,6 @@ mini-trading-engine/ - C++17 compiler (GCC 9+, Clang 10+) - CMake 3.16+ - Google Benchmark (for benchmarks) -- Linux recommended — thread pinning and TSC measurement require it ### Build @@ -133,13 +107,15 @@ cmake --build build --config Release ### Run Tests ```bash +# Run all tests ./build/tests/Release/velox_tests.exe ``` ### Run Benchmarks ```bash -./build/benchmarks/Release/bench_order.exe +# Run benchmark routine for Order Book +./build/benchmarks/Release/bench_order_book.exe ``` ## Performance @@ -148,82 +124,48 @@ All measurements taken on a pinned core with frequency scaling disabled. Latency ### Lock-Free Primitives -| Structure | Operation | p50 | p99 | p99.9 | -|---|---|---|---|---| -| SPSC Queue | push + pop | — | — | — | -| Ring Buffer | push + pop | — | — | — | -| HashMap | lookup (hit) | — | — | — | -| Object Pool | acquire + release | — | — | — | -| Atomic Counter | increment | — | — | — | -| Treiber Stack | push + pop | — | — | — | - -*Results pending hardware benchmarking. Target: SPSC within 2x of LMAX Disruptor.* - -### Pipeline Latency - -| Metric | Target | Measured | +| Structure | Operation | Time |---|---|---| -| Feed decode latency | < 100ns | — | -| Order book update | < 200ns | — | -| Match + risk check | < 100ns | — | -| Order-to-execution (hot path) | < 1µs | — | -| Throughput at saturation | > 10M orders/sec | — | - -### Measurement Methodology - -Latency is measured using `rdtsc` directly — not `std::chrono`. TSC has ~1ns resolution with no syscall overhead. All measurements: - -- Threads pinned to isolated cores via `pthread_setaffinity_np` -- CPU frequency scaling disabled -- 100k iteration warmup before recording -- Reported as p50 / p99 / p99.9 over 1M samples -- Compared against LMAX Disruptor and Chronicle Queue published benchmarks as a sanity check - ---- +| SPSC Queue | push/pop | ~3ns | +| Object Pool | acquire/release | ~23ns | +| Order Book | add order | ~26ns | +| Order Book | match | ~8μs | +| Risk Manager | check order | ~4ns | +| Position Manager | update P&L | ~23ns | +| Execution Gateway | send report | ~33ns | -## Validation Against Real Market Data - -Validate by replaying real market data and verifying fill prices match expected outcomes. - -**NASDAQ ITCH 5.0** sample files are freely available at [https://emi.nasdaq.com/ITCH/Nasdaq%20ITCH/](https://emi.nasdaq.com/ITCH/Nasdaq%20ITCH/). The `tools/itch_replay` tool parses these files and drives the full pipeline: +*Results pending hardware benchmarking. Target: SPSC within 2x of LMAX Disruptor.* -```bash -./build/tools/itch_replay --file 01302020.NASDAQ_ITCH50 --symbol AAPL --latency-report -``` +### Pipeline Throughput -This produces: -- Fill prices vs. expected prices (correctness) -- Per-stage latency breakdown (performance) -- Order book state snapshots at configurable intervals (accuracy) +| Metric | Measured | +|---|---| +| Match latency | ~8μs | +| Order throughput | ~70,000 orders/s | +| Full pipeline | 1.9ms | +| ITCH parse rate | 15ns/message | -A correct matching engine replaying ITCH data should produce fills identical to the exchange's public trade tape. +### Key Design Considerations ---- +No dynamic allocation in the hot path. All orders and reports come from object pools. The matching engine processes an entire order lifecycle without calling new or delete. -## Design Decisions +Ring buffer for price levels. Unlike traditional doubly-linked lists, the ring buffer provides O(1) FIFO operations with excellent cache locality. Lazy deletion marks cancelled orders without immediate removal. -**No dynamic allocation in the hot path.** All order objects come from the pool. All price level nodes come from the Treiber stack free-list. The matching engine processes an entire order lifecycle without calling `new` or `delete`. +Partitioned concurrency over shared queues. Each symbol runs on its own thread with dedicated SPSC queues. Contention is eliminated by design, not managed at runtime. -**Partitioned concurrency over shared queues.** Rather than a shared MPMC queue for strategy → gateway communication, each strategy owns a dedicated SPSC channel. Contention is eliminated by design, not managed at runtime. +Price-time priority matching. Orders at the same price are executed in FIFO order using the ring buffer's natural ordering. -**RCU for reference data.** Symbol configurations change rarely but are read on every order. RCU lets the matching engine read config with zero synchronization overhead on the critical path. Updates are handled on a background thread with copy-on-write semantics. +Real-time ITCH replay. The `itch_parser` tool reads binary ITCH files message-by-message (streaming, not loading entire file) and respects original timestamps for realistic simulation. -**TSC-based timing throughout.** All internal timestamps use `rdtsc`. The feed handler stamps each message on arrival; the execution gateway stamps each fill on departure. The difference is the authoritative latency number. +## Validation Methods ---- +- 74+ unit and integration tests covering all components -## Roadmap +- Google Benchmark suite for performance regression detection -- [ ] Feed handler — ITCH 5.0 parser -- [ ] Order book — price level management with RCU snapshots -- [ ] Matching engine — price-time priority, partial fills, cancel/replace -- [ ] Risk manager — position limits, per-symbol circuit breakers -- [ ] Execution gateway — FIX 4.2 encoder, simulated wire -- [ ] ITCH replay tool -- [ ] Full pipeline benchmark with percentile reporting -- [ ] Comparison report vs. Disruptor / Chronicle Queue +- ITCH 5.0 sample files from NASDAQ ---- +- Mock ITCH generator for deterministic testing ## References diff --git a/tests/test_integration.cpp b/tests/test_integration.cpp index 69e3473..aac4a3d 100644 --- a/tests/test_integration.cpp +++ b/tests/test_integration.cpp @@ -1,4 +1,5 @@ #include +#include #include "velox/core/symbol_engine.hpp" #include "velox/risk/risk_manager.hpp" #include "velox/gateway/execution_gateway.hpp" diff --git a/tests/test_order.cpp b/tests/test_order.cpp index cb4b314..8487cea 100644 --- a/tests/test_order.cpp +++ b/tests/test_order.cpp @@ -1,10 +1,21 @@ #include #include "velox/matching/order.hpp" #include "velox/core/object_pool.hpp" +#include "velox/book/order_book.hpp" +#include using namespace velox; using namespace lockfree; +class PriceLevelTest : public ::testing::Test { +protected: + void SetUp() override { + pool = std::make_unique>(); + } + + std::unique_ptr> pool; +}; + TEST(OrderTest, BasicOrderLifecycle) { ObjectPool pool; @@ -83,63 +94,66 @@ TEST(OrderTest, ResetOrder) { TEST(OrderTest, ObjectPoolReuse) { ObjectPool pool; - - auto order1 = pool.acquire(); - ASSERT_NE(order1.get(), nullptr); - order1->order_id = 1; - order1->price = 10000; - - uintptr_t addr1 = reinterpret_cast(order1.get()); - - // Release back to pool - order1.release(); - - auto order2 = pool.acquire(); - ASSERT_NE(order2.get(), nullptr); - - uintptr_t addr2 = reinterpret_cast(order2.get()); - - // Should reuse the same memory - EXPECT_EQ(addr1, addr2); - - order2->order_id = 2; - EXPECT_EQ(order2->order_id, 2); + uintptr_t addr1 = 0; + + // Separate order1 & order2 into separate scopes + { + auto order1 = pool.acquire(); + ASSERT_NE(order1.get(), nullptr); + order1->order_id = 1; + order1->price = 10000; + addr1 = reinterpret_cast(order1.get()); + // PooledPtr of order1 will be released back to pool automatically + } + + { + auto order2 = pool.acquire(); + ASSERT_NE(order2.get(), nullptr); + + uintptr_t addr2 = reinterpret_cast(order2.get()); + + // Should reuse the same memory + EXPECT_EQ(addr1, addr2); + + order2->order_id = 2; + EXPECT_EQ(order2->order_id, 2); + } } -/* -TEST(PriceLevelTest, AddAndMatch) { +TEST_F(PriceLevelTest, AddAndMatch) { PriceLevel level(10000); // $100.00 - OrderPool pool; - auto buy1 = pool.acquire(); - buy1->side = OrderSide::BUY; - buy1->price = 10000; - buy1->quantity = 50; - buy1->remaining_quantity = 50; - - auto buy2 = pool.acquire(); - buy2->side = OrderSide::BUY; - buy2->price = 10000; - buy2->quantity = 30; - buy2->remaining_quantity = 30; - - level.add_order(buy1.get()); - level.add_order(buy2.get()); + lockfree::ObjectPool pool; + std::vector> owned; + + auto create_order = [&](uint64_t id, OrderSide side, int64_t price, uint32_t qty) -> Order* { + auto order = pool.acquire(); + order->order_id = id; + order->side = side; + order->price = price; + order->quantity = qty; + order->remaining_quantity = qty; + order->filled_quantity = 0; + order->status = OrderStatus::NEW; + Order* raw = order.get(); + owned.push_back(std::move(order)); + return raw; + }; + + auto buy1 = create_order(1, OrderSide::BUY, 10000, 50); + auto buy2 = create_order(2, OrderSide::BUY, 10000, 30); + + level.add_order(buy1); + level.add_order(buy2); EXPECT_EQ(level.total_quantity(), 80); - // Incoming sell order - auto sell = pool.acquire(); - sell->side = OrderSide::SELL; - sell->price = 10000; - sell->quantity = 60; - sell->remaining_quantity = 60; - - auto result = level.match_order(sell.get()); + auto sell = create_order(3, OrderSide::SELL, 10000, 60); - // Should partially fill + std::vector fills; + auto result = level.match_order(sell, fills); EXPECT_EQ(buy1->remaining_quantity, 0); EXPECT_EQ(buy2->remaining_quantity, 20); EXPECT_EQ(sell->remaining_quantity, 0); -} -*/ \ No newline at end of file + EXPECT_EQ(result, nullptr); +} \ No newline at end of file diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index b3757ed..f2b3970 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -1 +1,8 @@ -add_executable(create_itch create_itch.cpp) \ No newline at end of file +add_executable(create_itch create_itch.cpp) +add_executable(itch_parser itch_parser.cpp) + +target_link_libraries(create_itch velox_core) +target_link_libraries(itch_parser velox_core) + +target_include_directories(create_itch PRIVATE ${CMAKE_SOURCE_DIR}/include) +target_include_directories(itch_parser PRIVATE ${CMAKE_SOURCE_DIR}/include) diff --git a/tools/itch_parser.cpp b/tools/itch_parser.cpp new file mode 100644 index 0000000..73125f7 --- /dev/null +++ b/tools/itch_parser.cpp @@ -0,0 +1,109 @@ +#include +#include +#include +#include +#include +#include "velox/feed/feed_handler.hpp" +#include "velox/core/symbol_engine.hpp" +#include "velox/risk/risk_manager.hpp" +#include "velox/gateway/execution_gateway.hpp" +#include "velox/risk/position_manager.hpp" + +using namespace velox; + +int main(int argc, char* argv[]) { + if (argc < 2) { + std::cerr << "Usage: " << argv[0] << " [speed_factor]\n"; + return 1; + } + + const char* filename = argv[1]; + double speed = (argc >= 3) ? std::stod(argv[2]) : 1.0; + + std::ifstream file(filename, std::ios::binary); + if (!file) { + std::cerr << "Failed to open: " << filename << std::endl; + return 1; + } + + // Engine setup + RiskManager risk; + ExecutionGateway gateway; + PositionManager pos_mgr; + FeedHandler feed; + + const int num_workers = std::thread::hardware_concurrency(); + for (int i = 0; i < num_workers; ++i) { + gateway.add_worker(); + } + + std::vector> engines; + engines.push_back(std::make_unique("AAPL", &risk, &gateway, &pos_mgr)); + engines.push_back(std::make_unique("MSFT", &risk, &gateway, &pos_mgr)); + engines.push_back(std::make_unique("GOOGL", &risk, &gateway, &pos_mgr)); + engines.push_back(std::make_unique("AMZN", &risk, &gateway, &pos_mgr)); + engines.push_back(std::make_unique("META", &risk, &gateway, &pos_mgr)); + + feed.on_add_order([&](const Order& order) { + for (auto& e : engines) { + if (strcmp(e->order_book().symbol(), order.symbol) == 0) { + static lockfree::ObjectPool pool; + static std::vector> pending; + auto new_order = pool.acquire(); + *new_order = order; + e->on_market_order(new_order.get()); + pending.push_back(std::move(new_order)); + break; + } + } + }); + + std::cout << "Starting at offset: " << file.tellg() << std::endl; + std::cout << "Replaying " << filename << " at " << speed << "x speed\n"; + + // Read entire file in chunks and feed to FeedHandler + // FeedHandler handles all ITCH parsing internally + const size_t BUFFER_SIZE = 64 * 1024; // 64KB chunks + std::vector buffer(BUFFER_SIZE); + uint64_t total_bytes = 0; + + // Optional: track time for speed control + auto start_time = std::chrono::steady_clock::now(); + uint64_t bytes_processed = 0; + + while (file.read(buffer.data(), BUFFER_SIZE) || file.gcount() > 0) { + size_t bytes_read = file.gcount(); + total_bytes += bytes_read; + + // Optional: add speed control here if needed (requires timestamp extraction) + + // Feed the raw data to FeedHandler – it handles all ITCH parsing + feed.process(buffer.data(), bytes_read); + + // Progress indicator + if (total_bytes / (1024 * 1024) > bytes_processed / (1024 * 1024) + 100) { + bytes_processed = total_bytes; + std::cout << "\rProcessed " << total_bytes / (1024 * 1024) << " MB..." << std::flush; + } + } + + file.close(); + std::cout << "\nFile read complete. Draining orders...\n"; + + // Drain remaining orders + for (auto& e : engines) { + e->run_match_cycle(); + } + + // Print final stats + std::cout << "\n=== FINAL STATS ===\n"; + for (auto& e : engines) { + auto stats = e->get_stats(); + int64_t pnl = pos_mgr.get_realized_pnl(e->symbol()); + std::cout << e->symbol() << ": matched=" << stats.orders_matched + << ", partial=" << stats.orders_partially_filled + << ", P&L=$" << (pnl / 100.0) << std::endl; + } + + return 0; +} \ No newline at end of file