diff --git a/CMakeLists.txt b/CMakeLists.txt index a5d7978..55716d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,6 +88,7 @@ set(BUILD_BENCHMARKS ${_SAVED_BUILD_BENCHMARKS}) # Subdirectories add_subdirectory(src) +add_subdirectory(tools) if(BUILD_TESTS) add_subdirectory(tests) diff --git a/benchmarks/bench_matching_engine.cpp b/benchmarks/bench_matching_engine.cpp index 42724c7..8e6cb8d 100644 --- a/benchmarks/bench_matching_engine.cpp +++ b/benchmarks/bench_matching_engine.cpp @@ -83,6 +83,9 @@ static void BM_OrderBook_MatchOnly(benchmark::State& state) { std::vector sells; sells.reserve(100); + std::vector fills; + fills.reserve(100); + for (int i = 0; i < 100; ++i) { sells.push_back(make_order(pool, owned, i, OrderSide::SELL, 10000 + i, 100)); } @@ -99,7 +102,8 @@ static void BM_OrderBook_MatchOnly(benchmark::State& state) { reset_order(buy, 10000); state.ResumeTiming(); - book.match(buy); + fills.clear(); + book.match(buy, fills); benchmark::DoNotOptimize(book); } } diff --git a/benchmarks/bench_order_book.cpp b/benchmarks/bench_order_book.cpp index 8564340..271c197 100644 --- a/benchmarks/bench_order_book.cpp +++ b/benchmarks/bench_order_book.cpp @@ -87,6 +87,10 @@ static void BM_OrderBook_MatchBuy(benchmark::State& state) { // Resting sell orders — created once, reset each iteration std::vector sells; sells.reserve(100); + + std::vector fills; + fills.reserve(100); + for (int i = 0; i < 100; ++i) sells.push_back(make_order(pool, owned, i, OrderSide::SELL, 10000 - i, 100)); @@ -102,9 +106,10 @@ static void BM_OrderBook_MatchBuy(benchmark::State& state) { book.add_order(s); } reset_order(buy, 10000); - state.ResumeTiming(); + state.ResumeTiming(); - auto remaining = book.match(buy); + fills.clear(); + auto remaining = book.match(buy, fills); benchmark::DoNotOptimize(remaining); } } @@ -116,6 +121,10 @@ static void BM_OrderBook_MatchSell(benchmark::State& state) { std::vector buys; buys.reserve(100); + + std::vector fills; + fills.reserve(100); + for (int i = 0; i < 100; ++i) buys.push_back(make_order(pool, owned, i, OrderSide::BUY, 10000 + i, 100)); @@ -128,10 +137,12 @@ static void BM_OrderBook_MatchSell(benchmark::State& state) { reset_order(b, 100); book.add_order(b); } + + fills.clear(); reset_order(sell, 10000); state.ResumeTiming(); - auto remaining = book.match(sell); + auto remaining = book.match(sell, fills); benchmark::DoNotOptimize(remaining); } } @@ -143,6 +154,10 @@ static void BM_OrderBook_MarketOrder(benchmark::State& state) { std::vector sells; sells.reserve(100); + + std::vector fills; + fills.reserve(100); + for (int i = 0; i < 100; ++i) sells.push_back(make_order(pool, owned, i, OrderSide::SELL, 10000 + i * 10, 100)); @@ -158,7 +173,8 @@ static void BM_OrderBook_MarketOrder(benchmark::State& state) { reset_order(buy, 10000); state.ResumeTiming(); - auto remaining = book.match(buy); + fills.clear(); + auto remaining = book.match(buy, fills); benchmark::DoNotOptimize(remaining); } } diff --git a/benchmarks/bench_price_level.cpp b/benchmarks/bench_price_level.cpp index 6931e87..e78eeb9 100644 --- a/benchmarks/bench_price_level.cpp +++ b/benchmarks/bench_price_level.cpp @@ -40,6 +40,9 @@ BENCHMARK(BM_PriceLevel_RemoveOrder); static void BM_PriceLevel_MatchOrder(State& state) { ObjectPool pool; PriceLevel level(10000); + + std::vector fills; + fills.reserve(100); // Setup: add a buy order auto buy = pool.acquire(); @@ -56,7 +59,8 @@ static void BM_PriceLevel_MatchOrder(State& state) { sell->quantity = 60; sell->remaining_quantity = 60; - auto remaining = level.match_order(sell.get()); + fills.clear(); + auto remaining = level.match_order(sell.get(), fills); DoNotOptimize(remaining); // Restore the buy order for next iteration diff --git a/include/velox/book/fill.hpp b/include/velox/book/fill.hpp new file mode 100644 index 0000000..d520aac --- /dev/null +++ b/include/velox/book/fill.hpp @@ -0,0 +1,13 @@ +#pragma once +#include +#include "velox/matching/order.hpp" + +namespace velox { + +struct Fill { + Order* order; + uint32_t quantity; + int64_t price; +}; + +} \ No newline at end of file diff --git a/include/velox/book/order_book.hpp b/include/velox/book/order_book.hpp index d82583f..4d72393 100644 --- a/include/velox/book/order_book.hpp +++ b/include/velox/book/order_book.hpp @@ -5,6 +5,7 @@ #include #include "velox/matching/order.hpp" #include "velox/book/price_level.hpp" +#include namespace velox { @@ -16,7 +17,7 @@ class OrderBook { // Core operations bool add_order(Order* order); bool cancel_order(uint64_t order_id); - Order* match(Order* incoming_order); + Order* match(Order* incoming_order, std::vector& fills); const std::vector& get_bid_levels() const { return m_bid_levels; } const std::vector& get_ask_levels() const { return m_ask_levels; } @@ -35,7 +36,6 @@ class OrderBook { const char* symbol() const { return m_symbol; } private: - // Stock ticker size char m_symbol[8]; @@ -46,7 +46,7 @@ class OrderBook { alignas(64) std::atomic m_bid_depth{0}; alignas(64) std::atomic m_ask_depth{0}; - // Price levels (using simple vectors for now - can be optimized later) + // Price levels std::vector m_bid_levels; // Sorted high to low std::vector m_ask_levels; // Sorted low to high diff --git a/include/velox/book/price_level.hpp b/include/velox/book/price_level.hpp index 91ca069..1d5873e 100644 --- a/include/velox/book/price_level.hpp +++ b/include/velox/book/price_level.hpp @@ -1,7 +1,9 @@ #pragma once #include #include +#include #include "velox/matching/order.hpp" +#include "velox/book/fill.hpp" namespace velox { @@ -16,7 +18,7 @@ class PriceLevel { // Core operations void add_order(Order* order); void remove_order(Order* order); - Order* match_order(Order* incoming); + Order* match_order(Order* incoming, std::vector& fills); // Getters int64_t price() const { return m_price; } diff --git a/include/velox/core/symbol_engine.hpp b/include/velox/core/symbol_engine.hpp new file mode 100644 index 0000000..feffbfe --- /dev/null +++ b/include/velox/core/symbol_engine.hpp @@ -0,0 +1,50 @@ +#pragma once +#include "velox/book/order_book.hpp" +#include "velox/matching/matching_engine.hpp" +#include "velox/feed/feed_handler.hpp" +#include "lockfree/spsc_queue.hpp" + +namespace velox { + +class SymbolEngine { +public: + SymbolEngine(const char* symbol, + RiskManager* risk_manager, + ExecutionGateway* gateway, + PositionManager* position_manager) + : m_book(symbol), + m_engine(symbol, risk_manager, gateway, position_manager) {} + + // Called by Feed Handler thread to push an order from ITCH + void on_market_order(Order* order) { + m_engine.submit_order(order); + } + + // Called by Matching Engine thread to run matching + void run_match_cycle() { + m_engine.run_match_cycle(); + } + + // Cancel order (via Order Book) + bool cancel_order(uint64_t order_id) { + return m_engine.cancel_order(order_id); + } + + // Access to Order Book for snapshots + const OrderBook& order_book() const { + return m_book; + } + + // Get stats from Matching Engine thread + MatchingEngine::Stats get_stats() { return m_engine.get_stats(); } + + // Get symbol + const char* symbol() { return m_book.symbol(); } + +private: + OrderBook m_book; + MatchingEngine m_engine; + +}; + +} \ No newline at end of file diff --git a/include/velox/matching/matching_engine.hpp b/include/velox/matching/matching_engine.hpp index 7ac9dd3..da1872f 100644 --- a/include/velox/matching/matching_engine.hpp +++ b/include/velox/matching/matching_engine.hpp @@ -21,13 +21,18 @@ class MatchingEngine { ~MatchingEngine(); - // Submit order (called by risk manager) + // Submit order (called by Risk Manager) bool submit_order(Order* order); + + // Cancel order (via Order Book) + bool cancel_order(uint64_t order_id) { + return m_order_book.cancel_order(order_id); + } - // Run matching cycle (called by matching thread) + // Run matching cycle (called by Matching Engine) void run_match_cycle(); - // Statistics + // Stats struct Stats { uint64_t orders_matched = 0; uint64_t orders_rejected = 0; @@ -36,9 +41,8 @@ class MatchingEngine { }; Stats get_stats() const; - -private: +private: OrderBook m_order_book; RiskManager* m_risk_manager; ExecutionGateway* m_gateway; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b011ef7..618b6be 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -9,7 +9,6 @@ add_library(velox_core STATIC # Matching matching/matching_engine.cpp - matching/order.cpp # Risk risk/risk_manager.cpp @@ -28,4 +27,15 @@ target_link_libraries(velox_core target_include_directories(velox_core PUBLIC ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/third_party/whirlpool/include +) + +add_executable(velox_trading_engine main.cpp) + +target_link_libraries(velox_trading_engine + velox_core +) + +target_include_directories(velox_trading_engine PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/third_party/whirlpool/include ) \ No newline at end of file diff --git a/src/book/order_book.cpp b/src/book/order_book.cpp index 787ec12..ffb6b97 100644 --- a/src/book/order_book.cpp +++ b/src/book/order_book.cpp @@ -16,6 +16,12 @@ OrderBook::~OrderBook() { bool OrderBook::add_order(Order* order) { if (!order) return false; + /* + std::cout << "[ADD_ORDER] ID=" << order->order_id + << " side=" << (order->is_buy() ? "BUY" : "SELL") + << " price=" << order->price << std::endl; + */ + bool is_bid = order->is_buy(); int64_t price = order->price; @@ -25,12 +31,19 @@ bool OrderBook::add_order(Order* order) { level = new PriceLevel(price); insert_level(level, is_bid); } - + + /* + std::cout << "[DEBUG] OrderBook::add_order side=" << (is_bid ? "BUY" : "SELL") + << " price=" << price << " qty=" << order->remaining_quantity << std::endl; + */ + // Add order to the level level->add_order(order); // Index order for O(1) cancellation m_order_index[order->order_id] = {price, is_bid, order}; + + // std::cout << "[ADD_ORDER] Inserted into index. Index size=" << m_order_index.size() << std::endl; // Update depth and sequence if (is_bid) { @@ -46,6 +59,16 @@ bool OrderBook::add_order(Order* order) { } bool OrderBook::cancel_order(uint64_t order_id) { + /* + std::cout << "[CANCEL] Cancel called for order_id=" << order_id << std::endl; + std::cout << "[CANCEL] Index size=" << m_order_index.size() << std::endl; + + + for (const auto& pair : m_order_index) { + std::cout << "[CANCEL] Index contains ID=" << pair.first << std::endl; + } + */ + // O(1) hashmap lookup auto it = m_order_index.find(order_id); if (it == m_order_index.end()) { @@ -88,25 +111,37 @@ bool OrderBook::cancel_order(uint64_t order_id) { return true; } -Order* OrderBook::match(Order* incoming_order) { - if (!incoming_order || incoming_order->remaining_quantity == 0) +Order* OrderBook::match(Order* incoming_order, std::vector& fills) { + if (!incoming_order || incoming_order->remaining_quantity == 0) { return incoming_order; + } bool is_bid = incoming_order->is_buy(); std::vector& levels = is_bid ? m_ask_levels : m_bid_levels; + /* + std::cout << "OrderBook::match: incoming " << (is_bid ? "BUY" : "SELL") << " price=" << incoming_order->price << " qty=" << incoming_order->remaining_quantity << std::endl; + std::cout << "Best bid=" << best_bid() << " best ask=" << best_ask() << std::endl; + */ + while (!levels.empty() && incoming_order->remaining_quantity > 0) { PriceLevel* level = levels.front(); - if ( is_bid && level->price() > incoming_order->price) break; + if (is_bid && level->price() > incoming_order->price) break; if (!is_bid && level->price() < incoming_order->price) break; uint32_t before = level->total_quantity(); - level->match_order(incoming_order); + level->match_order(incoming_order, fills); uint32_t after = level->total_quantity(); - if (is_bid) m_ask_depth.fetch_sub(before - after, std::memory_order_release); - else m_bid_depth.fetch_sub(before - after, std::memory_order_release); + uint32_t diff = before - after; + + if (is_bid) { + m_ask_depth.fetch_sub(diff, std::memory_order_release); + } + else { + m_bid_depth.fetch_sub(diff, std::memory_order_release); + } if (level->empty()) { m_price_to_level.erase(level->price()); @@ -117,14 +152,26 @@ Order* OrderBook::match(Order* incoming_order) { // Rest unfilled quantity into the book if (incoming_order->remaining_quantity > 0) { + // std::cout << "[MATCH] Adding resting order ID=" << incoming_order->order_id << std::endl; + PriceLevel* level = find_level(incoming_order->price, is_bid); + if (!level) { level = new PriceLevel(incoming_order->price); insert_level(level, is_bid); } + level->add_order(incoming_order); - if (is_bid) m_bid_depth.fetch_add(incoming_order->remaining_quantity, std::memory_order_release); - else m_ask_depth.fetch_add(incoming_order->remaining_quantity, std::memory_order_release); + + if (is_bid) { + m_bid_depth.fetch_add(incoming_order->remaining_quantity, std::memory_order_release); + } + else { + m_ask_depth.fetch_add(incoming_order->remaining_quantity, std::memory_order_release); + } + + // Add to index + m_order_index[incoming_order->order_id] = {incoming_order->price, is_bid, incoming_order}; } update_best_prices(); @@ -182,15 +229,19 @@ void OrderBook::update_best_prices() { void OrderBook::update_depth() { uint32_t bid_depth = 0; + for (auto* level : m_bid_levels) { bid_depth += level->total_quantity(); } + m_bid_depth.store(bid_depth, std::memory_order_release); uint32_t ask_depth = 0; + for (auto* level : m_ask_levels) { ask_depth += level->total_quantity(); } + m_ask_depth.store(ask_depth, std::memory_order_release); } diff --git a/src/book/price_level.cpp b/src/book/price_level.cpp index dc657b7..cadd907 100644 --- a/src/book/price_level.cpp +++ b/src/book/price_level.cpp @@ -1,5 +1,9 @@ #include "velox/book/price_level.hpp" #include +#include +#include +#include "velox/book/order_book.hpp" +#include "velox/book/fill.hpp" namespace velox { @@ -32,11 +36,10 @@ void PriceLevel::remove_order(Order* order) { } } -Order* PriceLevel::match_order(Order* incoming) { - if (!incoming) return nullptr; +Order* PriceLevel::match_order(Order* incoming, std::vector& fills) { + if (!incoming || incoming->remaining_quantity == 0) return nullptr; while (incoming->remaining_quantity > 0) { - advance(); if (m_head == m_tail) break; @@ -53,26 +56,37 @@ Order* PriceLevel::match_order(Order* incoming) { uint32_t fill = std::min(current->remaining_quantity, incoming->remaining_quantity); - // Apply fill - current->remaining_quantity -= fill; - incoming->remaining_quantity -= fill; + if (fill > 0) { - current->filled_quantity += fill; - incoming->filled_quantity += fill; + // Resting order == fill price + int64_t fill_price = incoming->price; - m_total_quantity -= fill; + // Record fill + fills.push_back(Fill{current, fill, fill_price}); - // If maker fully filled → pop from FIFO - if (current->remaining_quantity == 0) { - m_buffer[m_head] = nullptr; - m_head = next(m_head); - m_size--; + // Apply fill + current->remaining_quantity -= fill; + incoming->remaining_quantity -= fill; + + current->filled_quantity += fill; + incoming->filled_quantity += fill; + + m_total_quantity -= fill; + + // If maker fully filled, pop from FIFO + if (current->remaining_quantity == 0) { + m_buffer[m_head] = nullptr; + m_head = next(m_head); + m_size--; + } } if (incoming->remaining_quantity == 0) break; } + // std::cout << " Matching at price=" << m_price << " m_total_qty=" << m_total_quantity << std::endl; + return (incoming->remaining_quantity > 0) ? incoming : nullptr; } diff --git a/src/feed/feed_handler.cpp b/src/feed/feed_handler.cpp index 9b7d565..d8acef7 100644 --- a/src/feed/feed_handler.cpp +++ b/src/feed/feed_handler.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #ifdef _WIN32 #include @@ -96,7 +97,12 @@ void FeedHandler::parse_add_order(const uint8_t* data, size_t len) { for (const auto& cb : m_on_add_order) { if (cb) cb(order); } - + + /* + std::cout << "Parsed order: " << order.symbol << " side=" << (order.side == OrderSide::BUY ? "BUY" : "SELL") + << " price=" << order.price << " qty=" << order.quantity << std::endl; + */ + m_message_count++; } @@ -207,9 +213,13 @@ void FeedHandler::process(const char* data, size_t len) { void FeedHandler::process_file(const std::string& filename) { std::ifstream file(filename, std::ios::binary); + if (!file.is_open()) { + // std::cout << "[DEBUG] Error opening file: " << filename << std::endl; return; } + + // std::cout << "[DEBUG] Opening file: " << filename << std::endl; file.seekg(0, std::ios::end); size_t file_size = file.tellg(); diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..f0376d2 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,169 @@ +#include "velox/core/symbol_engine.hpp" +#include "velox/risk/risk_manager.hpp" +#include "velox/gateway/execution_gateway.hpp" +#include "velox/risk/position_manager.hpp" +#include "velox/feed/feed_handler.hpp" +#include "velox/matching/matching_engine.hpp" +#include +#include +#include +#include +#include +#include +#include + +using namespace velox; + +static std::atomic g_running{true}; + +void signal_handler(int) { g_running = false; } + +int main() { + // Signal handlers + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + + // Market data ingestion (entry point) + FeedHandler feed_handler; + + // Shared components + RiskManager risk_manager; + ExecutionGateway gateway; + PositionManager pos_manager; + + // Add workers to Execution Gateway (1 per CPU core, tunable) + const int num_workers = std::thread::hardware_concurrency(); + + for (int i = 0; i < num_workers; ++i) { + gateway.add_worker(); + } + + // Trading symbols + std::vector symbols = {"AAPL", "MSFT", "GOOGL", "AMZN", "META"}; + + // One Symbol Engine per symbol + std::vector> engines; + std::vector worker_threads; + + for (const auto& s : symbols) { + engines.push_back(std::make_unique( + s.c_str(), &risk_manager, &gateway, &pos_manager)); + } + + // Market data feed + feed_handler.on_add_order([&](const Order& order) { + + // Linear search for matching symbol + // TODO: use more efficient algorithm for larger symbol set + for (auto& e : engines) { + if (strcmp(e->order_book().symbol(), order.symbol) == 0) { + + // Each engine has its own pool (for now) + static lockfree::ObjectPool global_pool; + static std::vector> pending_orders; + + auto new_order = global_pool.acquire(); + *new_order = order; // Copy + // std::cout << "[DEBUG] Routing to engine for symbol: " << e->order_book().symbol() << std::endl; + e->on_market_order(new_order.get()); + pending_orders.push_back(std::move(new_order)); + break; + } + } + }); + + // Spawn a thread for each symbol's matching engine + for (size_t i = 0; i < engines.size(); ++i) { + worker_threads.emplace_back([&e = engines[i]]() { + while (g_running) { + e->run_match_cycle(); + + // Small sleep cycles if no orders + std::this_thread::sleep_for(std::chrono::microseconds(1)); + } + }); + } + + // Spawn a thread for stats reporting + std::thread stats_thread([&]() { + while (g_running) { + std::this_thread::sleep_for(std::chrono::seconds(5)); // N = 5 seconds (arbitrary) + + std::cout << "\n=== Trading Engine Stats [" + << std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()) + << "] ===" << std::endl; + + for (auto& e : engines) { + auto stats = e->get_stats(); + int64_t position = pos_manager.get_position(e->symbol()); + int64_t realized_pnl = pos_manager.get_realized_pnl(e->symbol()); + + std::cout << " " << e->symbol() + << ": matched=" << stats.orders_matched + << ", position=" << position + << ", realized_PnL=$" << (realized_pnl / 100.0) + << ", rejected=" << stats.orders_rejected + << ", partial=" << stats.orders_partially_filled + << std::endl; + } + + std::cout << " Gateway: total_reports=" << gateway.total_reports_sent() + << ", total_orders=" << gateway.total_orders_sent() << std::endl; + } + }); + + // Process market data by simulating read from ITCH file + // TODO: change to read from constant stream + std::thread feed_thread([&]() { + // std::cout << "[DEBUG] Parsing mock ITCH file in test_data/NASDAQ_ITCH50_sample.bin..." << std::endl; + feed_handler.process_file("test_data/NASDAQ_ITCH50_sample.bin"); + g_running = false; // Stop workers when feed ends + }); + + // Spawn a thread to publish snapshots periodically + // TODO: implement once current event loop works + /* + std::thread snapshot_thread([&]() { + while (g_running) { + for (auto& eng : engines) { + + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + }); + */ + + // Wait for shutdown + feed_thread.join(); + g_running = false; + + // Fully drain each engine's queue before shutting down workers + for (auto& e : engines) { + e->run_match_cycle(); + } + + // Let stats reporting thread print before yielding + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Join stats thread + if (stats_thread.joinable()) { + stats_thread.join(); + } + + // Join worker threads + for (auto& t : worker_threads) { + if (t.joinable()) t.join(); + } + + // Print final stats + std::cout << "\n=== FINAL STATISTICS ===" << std::endl; + for (auto& e : engines) { + auto stats = e->get_stats(); + std::cout << e->symbol() << ": matched=" << stats.orders_matched + << ", rejected=" << stats.orders_rejected + << ", partial=" << stats.orders_partially_filled + << std::endl; + } + + return 0; +} \ No newline at end of file diff --git a/src/matching/matching_engine.cpp b/src/matching/matching_engine.cpp index 05027ac..f812c84 100644 --- a/src/matching/matching_engine.cpp +++ b/src/matching/matching_engine.cpp @@ -1,5 +1,6 @@ #include "velox/matching/matching_engine.hpp" #include +#include namespace velox { @@ -17,16 +18,24 @@ MatchingEngine::~MatchingEngine() = default; bool MatchingEngine::submit_order(Order* order) { if (!order) return false; + // std::cout << "[DEBUG] MatchingEngine::submit_order, pushing to queue" << std::endl; return m_incoming_orders.push(order); } void MatchingEngine::run_match_cycle() { + // std::cout << "[DEBUG] MatchingEngine::run_match_cycle" << std::endl; + while (auto opt_order = m_incoming_orders.pop()) { process_order(opt_order.value()); } } void MatchingEngine::process_order(Order* order) { + /* + std::cout << "[MATCHING_ENGINE] process_order ID=" << order->order_id + << " side=" << (order->is_buy() ? "BUY" : "SELL") + << " price=" << order->price << std::endl; + */ // Run risk check if (!check_risk(order)) { @@ -44,18 +53,48 @@ void MatchingEngine::process_order(Order* order) { return; } + + // Convert market orders to aggressive limit orders + if (order->type == OrderType::MARKET) { + if (order->is_buy()) { + order->price = INT64_MAX; + } else { + order->price = 0; + } + + order->type = OrderType::LIMIT; + } + + std::vector fills; + fills.reserve(16); // Match against order book - Order* remaining = m_order_book.match(order); + Order* remaining = m_order_book.match(order, fills); + + // Update position for every resting order that was filled + for (const Fill& fill : fills) { + /* + std::cout << "[PROCESS_ORDER] Resting order filled. ID=" << fill.order->order_id + << " qty=" << fill.quantity + << " fill.price=" << fill.price + << " order.price=" << fill.order->price << std::endl; + */ + + if (m_position_manager) { + m_position_manager->update_position(fill.order, fill.quantity, fill.order->price); + } + } // Send fills for executed portion if (order->filled_quantity > 0) { send_fill(order, order->filled_quantity, order->price); - m_match_count++; + + // Count per fill + m_match_count += fills.empty() ? 1 : fills.size(); } - // If partially filled, remainder is added to the book - if (remaining && remaining->remaining_quantity > 0) { + // Partial count only incremented when a fill actually occurred + if (order->filled_quantity > 0 && order->remaining_quantity > 0) { m_partial_count++; } diff --git a/src/matching/order.cpp b/src/matching/order.cpp deleted file mode 100644 index dcba56c..0000000 --- a/src/matching/order.cpp +++ /dev/null @@ -1,8 +0,0 @@ -#include "velox/matching/order.hpp" - -namespace velox { - -// Order methods can be implemented inline in header for now -// This file can be empty or removed - -} \ No newline at end of file diff --git a/src/risk/position_manager.cpp b/src/risk/position_manager.cpp index 6d7e893..59247c2 100644 --- a/src/risk/position_manager.cpp +++ b/src/risk/position_manager.cpp @@ -1,6 +1,7 @@ #include "velox/risk/position_manager.hpp" #include #include +#include namespace velox { @@ -10,12 +11,24 @@ PositionManager::~PositionManager() = default; void PositionManager::update_position(const Order* order, uint32_t fill_quantity, int64_t fill_price) { if (!order || fill_quantity == 0) return; + // Trim trailing spaces from symbol std::string symbol(order->symbol); + while (!symbol.empty() && symbol.back() == ' ') { + symbol.pop_back(); + } + + // std::cout << "[POSITION] Symbol='" << symbol << "' (original='" << order->symbol << "')" << std::endl; // Create default position if symbol doesn't exist in system auto& pos = m_positions[symbol]; if (order->is_buy()) { + /* + std::cout << "[POSITION] BUY: order_id=" << order->order_id + << " fill_price=" << fill_price + << " order.price=" << order->price << std::endl; + */ + // Buy: increase position, update average entry price int64_t old_position = pos.net_position.load(std::memory_order_acquire); int64_t old_avg = pos.avg_entry_price.load(std::memory_order_acquire); @@ -24,8 +37,14 @@ void PositionManager::update_position(const Order* order, uint32_t fill_quantity int64_t new_position = old_position + fill_quantity; uint64_t new_bought = old_bought + fill_quantity; - // Weighted average price - int64_t new_avg = (old_avg * old_bought + fill_price * fill_quantity) / new_bought; + + int64_t new_avg; + if (old_bought == 0) { + // First fill: use the order's own price + new_avg = fill_price; + } else { + new_avg = (old_avg * old_bought + fill_price * fill_quantity) / new_bought; + } pos.net_position.store(new_position, std::memory_order_release); pos.avg_entry_price.store(new_avg, std::memory_order_release); @@ -36,8 +55,17 @@ void PositionManager::update_position(const Order* order, uint32_t fill_quantity int64_t old_position = pos.net_position.load(std::memory_order_acquire); int64_t old_avg = pos.avg_entry_price.load(std::memory_order_acquire); + /* + std::cout << "[POSITION] Sell: fill_price=" << fill_price + << " old_avg=" << old_avg + << " quantity=" << fill_quantity << std::endl; + */ + // Realized P&L = (sell_price - avg_price) * quantity int64_t pnl = (fill_price - old_avg) * fill_quantity; + + // std::cout << "[POSITION] P&L=" << pnl << std::endl; + update_realized_pnl(pos, pnl); int64_t new_position = old_position - fill_quantity; @@ -51,18 +79,27 @@ void PositionManager::update_realized_pnl(Position& pos, int64_t pnl) { } int64_t PositionManager::get_position(const char* symbol) const { + std::string sym(symbol); + while (!sym.empty() && sym.back() == ' ') sym.pop_back(); + auto it = m_positions.find(symbol); if (it == m_positions.end()) return 0; return it->second.net_position.load(std::memory_order_acquire); } int64_t PositionManager::get_realized_pnl(const char* symbol) const { + std::string sym(symbol); + while (!sym.empty() && sym.back() == ' ') sym.pop_back(); + auto it = m_positions.find(symbol); if (it == m_positions.end()) return 0; return it->second.realized_pnl.load(std::memory_order_acquire); } int64_t PositionManager::get_unrealized_pnl(const char* symbol, int64_t current_price) const { + std::string sym(symbol); + while (!sym.empty() && sym.back() == ' ') sym.pop_back(); + auto it = m_positions.find(symbol); if (it == m_positions.end()) return 0; @@ -80,6 +117,9 @@ int64_t PositionManager::get_unrealized_pnl(const char* symbol, int64_t current_ } int64_t PositionManager::get_total_pnl(const char* symbol, int64_t current_price) const { + std::string sym(symbol); + while (!sym.empty() && sym.back() == ' ') sym.pop_back(); + return get_realized_pnl(symbol) + get_unrealized_pnl(symbol, current_price); } diff --git a/test_data/NASDAQ_ITCH50_sample.bin b/test_data/NASDAQ_ITCH50_sample.bin index d662364..17aed07 100644 Binary files a/test_data/NASDAQ_ITCH50_sample.bin and b/test_data/NASDAQ_ITCH50_sample.bin differ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ad99760..a6e0e1b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,6 +9,7 @@ add_executable(velox_tests test_feed_handler.cpp test_book_snapshot.cpp test_position_manager.cpp + test_integration.cpp ) target_link_libraries(velox_tests diff --git a/tests/test_integration.cpp b/tests/test_integration.cpp new file mode 100644 index 0000000..2783d72 --- /dev/null +++ b/tests/test_integration.cpp @@ -0,0 +1,248 @@ +#include +#include "velox/core/symbol_engine.hpp" +#include "velox/risk/risk_manager.hpp" +#include "velox/gateway/execution_gateway.hpp" +#include "velox/risk/position_manager.hpp" +#include "lockfree/pool.hpp" + +using namespace velox; + +class IntegrationTest : public ::testing::Test { +protected: + void SetUp() override { + // Shared components + risk_mgr = std::make_unique(); + gateway = std::make_unique(); + pos_mgr = std::make_unique(); + + // Add workers to gateway + int num_workers = std::thread::hardware_concurrency(); + for (int i = 0; i < num_workers; ++i) { + gateway->add_worker(); + } + + // Create engines for several symbols + symbols = {"AAPL", "MSFT", "GOOGL"}; + for (const auto& sym : symbols) { + engines.push_back(std::make_unique( + sym.c_str(), risk_mgr.get(), gateway.get(), pos_mgr.get())); + } + + // Global order pool (can be per‑engine, but a single pool is fine for tests) + order_pool = std::make_unique>(); + } + + Order* create_order(uint64_t id, OrderSide side, const char* symbol, + int64_t price, uint32_t qty, OrderType type = OrderType::LIMIT) { + auto order = order_pool->acquire(); + order->order_id = id; + order->client_order_id = id; + order->side = side; + order->price = price; + order->quantity = qty; + order->remaining_quantity = qty; + order->filled_quantity = 0; + order->type = type; + order->status = OrderStatus::NEW; + std::strncpy(order->symbol, symbol, 7); + Order* raw = order.get(); + owned_orders.push_back(std::move(order)); + return raw; + } + + void submit_order(Order* order) { + std::cout << "[INTEGRATION_TEST] Submitting order ID=" << order->order_id + << " symbol=" << order->symbol << std::endl; + + // Find the correct engine for the symbol + for (auto& eng : engines) { + if (strcmp(eng->order_book().symbol(), order->symbol) == 0) { + eng->on_market_order(order); + return; + } + } + FAIL() << "No engine for symbol: " << order->symbol; + } + + void run_match_cycles() { + for (auto& eng : engines) { + eng->run_match_cycle(); + } + } + + void run_match_cycles(int times) { + for (int i = 0; i < times; ++i) { + run_match_cycles(); + } + } + + SymbolEngine* engine_for_symbol(const char* sym) { + for (auto& eng : engines) { + if (strcmp(eng->order_book().symbol(), sym) == 0) + return eng.get(); + } + return nullptr; + } + + std::unique_ptr risk_mgr; + std::unique_ptr gateway; + std::unique_ptr pos_mgr; + std::vector> engines; + std::vector symbols; + std::unique_ptr> order_pool; + std::vector> owned_orders; +}; + +TEST_F(IntegrationTest, SingleSymbolMatch) { + auto buy = create_order(1, OrderSide::BUY, "AAPL", 10000, 100); + auto sell = create_order(2, OrderSide::SELL, "AAPL", 9900, 100); + + submit_order(buy); + submit_order(sell); + run_match_cycles(); + + auto stats = engine_for_symbol("AAPL")->get_stats(); + EXPECT_EQ(stats.orders_matched, 1); + EXPECT_EQ(stats.orders_rejected, 0); + EXPECT_EQ(stats.orders_partially_filled, 0); + + // Check final positions + EXPECT_EQ(pos_mgr->get_position("AAPL"), 0); + EXPECT_EQ(pos_mgr->get_realized_pnl("AAPL"), (9900 - 10000) * 100); // loss = -10000 +} + +TEST_F(IntegrationTest, TwoSymbolsIndependent) { + // AAPL: buy and sell that cross + auto aapl_buy = create_order(1, OrderSide::BUY, "AAPL", 10000, 50); + auto aapl_sell = create_order(2, OrderSide::SELL, "AAPL", 9900, 50); + submit_order(aapl_buy); + submit_order(aapl_sell); + + // MSFT: buy and sell that cross + auto msft_buy = create_order(3, OrderSide::BUY, "MSFT", 20000, 30); + auto msft_sell = create_order(4, OrderSide::SELL, "MSFT", 19900, 30); + submit_order(msft_buy); + submit_order(msft_sell); + + run_match_cycles(); + + EXPECT_EQ(engine_for_symbol("AAPL")->get_stats().orders_matched, 1); + EXPECT_EQ(engine_for_symbol("MSFT")->get_stats().orders_matched, 1); + EXPECT_EQ(pos_mgr->get_position("AAPL"), 0); + EXPECT_EQ(pos_mgr->get_position("MSFT"), 0); +} + +TEST_F(IntegrationTest, PartialFillRemainderResting) { + auto sell = create_order(1, OrderSide::SELL, "AAPL", 10000, 50); + submit_order(sell); + run_match_cycles(); + + auto buy = create_order(2, OrderSide::BUY, "AAPL", 10100, 100); + submit_order(buy); + run_match_cycles(); + + auto stats = engine_for_symbol("AAPL")->get_stats(); + EXPECT_EQ(stats.orders_matched, 1); + EXPECT_EQ(stats.orders_partially_filled, 1); + EXPECT_EQ(sell->remaining_quantity, 0); + EXPECT_EQ(buy->remaining_quantity, 50); + + // Now add another sell to match the remaining 50 + auto sell2 = create_order(3, OrderSide::SELL, "AAPL", 10000, 50); + submit_order(sell2); + run_match_cycles(); + + EXPECT_EQ(buy->remaining_quantity, 0); + EXPECT_EQ(sell2->remaining_quantity, 0); + EXPECT_EQ(engine_for_symbol("AAPL")->get_stats().orders_matched, 2); +} + +TEST_F(IntegrationTest, RiskRejection) { + risk_mgr->set_position_limit("AAPL", 30); // max 30 shares net + auto buy = create_order(1, OrderSide::BUY, "AAPL", 10000, 100); + submit_order(buy); + run_match_cycles(); + + auto stats = engine_for_symbol("AAPL")->get_stats(); + EXPECT_EQ(stats.orders_rejected, 1); + EXPECT_EQ(buy->status, OrderStatus::REJECTED); +} + +TEST_F(IntegrationTest, CircuitBreakerHalt) { + risk_mgr->activate_circuit_breaker(); + + auto buy = create_order(1, OrderSide::BUY, "AAPL", 10000, 10); + submit_order(buy); + run_match_cycles(); + + auto stats = engine_for_symbol("AAPL")->get_stats(); + EXPECT_EQ(stats.orders_rejected, 1); + EXPECT_EQ(buy->status, OrderStatus::REJECTED); +} + +TEST_F(IntegrationTest, PositionTracking) { + // Add a buy order (resting, not filled yet) + auto buy = create_order(1, OrderSide::BUY, "AAPL", 10000, 100); + submit_order(buy); + run_match_cycles(); + + // Position is 0 until a trade occurs + EXPECT_EQ(pos_mgr->get_position("AAPL"), 0); + EXPECT_EQ(pos_mgr->get_realized_pnl("AAPL"), 0); + + // Add a sell order that matches + auto sell = create_order(2, OrderSide::SELL, "AAPL", 9900, 100); + submit_order(sell); + run_match_cycles(); + + // After trade, position should be 0 (buy 100, sell 100) + EXPECT_EQ(pos_mgr->get_position("AAPL"), 0); + + // Realized P&L: (9900 - 10000) * 100 = -10000 + EXPECT_EQ(pos_mgr->get_realized_pnl("AAPL"), (9900 - 10000) * 100); +} + +TEST_F(IntegrationTest, MarketOrderMatch) { + auto limit_sell = create_order(1, OrderSide::SELL, "AAPL", 10000, 100); + submit_order(limit_sell); + run_match_cycles(); + + auto market_buy = create_order(2, OrderSide::BUY, "AAPL", 0, 60); + market_buy->type = OrderType::MARKET; + submit_order(market_buy); + run_match_cycles(); + + EXPECT_EQ(market_buy->remaining_quantity, 0); + EXPECT_EQ(limit_sell->remaining_quantity, 40); + EXPECT_EQ(engine_for_symbol("AAPL")->get_stats().orders_matched, 1); +} + +TEST_F(IntegrationTest, CancelBeforeMatch) { + auto buy = create_order(1, OrderSide::BUY, "AAPL", 10000, 100); + submit_order(buy); + run_match_cycles(); + + bool cancelled = engine_for_symbol("AAPL")->cancel_order(1); + EXPECT_TRUE(cancelled); + EXPECT_EQ(buy->status, OrderStatus::CANCELLED); + + auto sell = create_order(2, OrderSide::SELL, "AAPL", 9900, 100); + submit_order(sell); + run_match_cycles(); + + EXPECT_EQ(engine_for_symbol("AAPL")->get_stats().orders_matched, 0); + EXPECT_EQ(sell->remaining_quantity, 100); // still resting +} + +TEST_F(IntegrationTest, ManyOrdersNoMatch) { + const int N = 1000; + for (int i = 0; i < N; ++i) { + auto buy = create_order(i, OrderSide::BUY, "AAPL", 10000 + i, 100); + submit_order(buy); + } + run_match_cycles(); + + auto stats = engine_for_symbol("AAPL")->get_stats(); + EXPECT_EQ(stats.orders_matched, 0); + EXPECT_EQ(stats.orders_rejected, 0); +} \ No newline at end of file diff --git a/tests/test_order_book.cpp b/tests/test_order_book.cpp index 868f1b5..901f9fd 100644 --- a/tests/test_order_book.cpp +++ b/tests/test_order_book.cpp @@ -1,6 +1,7 @@ #include #include "velox/book/order_book.hpp" #include "lockfree/pool.hpp" +#include "velox/book/fill.hpp" using namespace velox; @@ -49,6 +50,9 @@ TEST_F(OrderBookTest, AddAskOrder) { } TEST_F(OrderBookTest, MatchBuyWithExistingSell) { + std::vector fills; + fills.reserve(100); + // Add sell order at $100 auto sell = create_order(1, OrderSide::SELL, 10000, 100); book->add_order(sell); @@ -56,7 +60,7 @@ TEST_F(OrderBookTest, MatchBuyWithExistingSell) { // Incoming buy order at $101 (crosses) auto buy = create_order(2, OrderSide::BUY, 10100, 60); - auto remaining = book->match(buy); + auto remaining = book->match(buy, fills); // Buy should be fully filled EXPECT_EQ(buy->remaining_quantity, 0); @@ -67,6 +71,9 @@ TEST_F(OrderBookTest, MatchBuyWithExistingSell) { } TEST_F(OrderBookTest, MatchSellWithExistingBuy) { + std::vector fills; + fills.reserve(100); + // Add buy order at $100 auto buy = create_order(1, OrderSide::BUY, 10000, 100); book->add_order(buy); @@ -74,7 +81,7 @@ TEST_F(OrderBookTest, MatchSellWithExistingBuy) { // Incoming sell order at $99 (crosses) auto sell = create_order(2, OrderSide::SELL, 9900, 60); - auto remaining = book->match(sell); + auto remaining = book->match(sell, fills); // Sell should be fully filled EXPECT_EQ(sell->remaining_quantity, 0); @@ -85,13 +92,16 @@ TEST_F(OrderBookTest, MatchSellWithExistingBuy) { } TEST_F(OrderBookTest, PartialFillRemainingGoesToBook) { + std::vector fills; + fills.reserve(100); + // Add sell order at $100 auto sell = create_order(1, OrderSide::SELL, 10000, 50); book->add_order(sell); // Incoming buy order for 100 (only 50 available) auto buy = create_order(2, OrderSide::BUY, 10100, 100); - auto remaining = book->match(buy); + auto remaining = book->match(buy, fills); // Buy should have 50 left and be added to book EXPECT_EQ(buy->remaining_quantity, 50); @@ -103,6 +113,9 @@ TEST_F(OrderBookTest, PartialFillRemainingGoesToBook) { } TEST_F(OrderBookTest, MultiplePriceLevels) { + std::vector fills; + fills.reserve(100); + // Add buy orders at different prices auto buy1 = create_order(1, OrderSide::BUY, 10000, 50); auto buy2 = create_order(2, OrderSide::BUY, 9900, 30); @@ -118,7 +131,7 @@ TEST_F(OrderBookTest, MultiplePriceLevels) { // Match a sell order at 10050 (should fill 10100 level only) auto sell = create_order(4, OrderSide::SELL, 10050, 15); - auto remaining = book->match(sell); + auto remaining = book->match(sell, fills); EXPECT_EQ(buy3->remaining_quantity, 5); // 20 - 15 = 5 left EXPECT_EQ(sell->remaining_quantity, 0); diff --git a/tests/test_price_level.cpp b/tests/test_price_level.cpp index 48345c7..46e3237 100644 --- a/tests/test_price_level.cpp +++ b/tests/test_price_level.cpp @@ -1,6 +1,7 @@ #include #include "velox/book/price_level.hpp" #include "lockfree/pool.hpp" +#include "velox/book/fill.hpp" using namespace velox; @@ -46,6 +47,9 @@ TEST_F(PriceLevelTest, AddMultipleOrders_FIFO) { } TEST_F(PriceLevelTest, RemoveOrder_IsImplicit_FIFO) { + std::vector fills; + fills.reserve(100); + PriceLevel level(10000); auto o1 = pool->acquire(); @@ -60,12 +64,15 @@ TEST_F(PriceLevelTest, RemoveOrder_IsImplicit_FIFO) { // simulate full fill of o2 via match (not remove_order) o2->remaining_quantity = 0; - level.match_order(o2.get()); // safe no-op-ish + level.match_order(o2.get(), fills); // safe no-op-ish EXPECT_EQ(level.head(), o1.get()); } TEST_F(PriceLevelTest, MatchOrderFullFill) { + std::vector fills; + fills.reserve(100); + PriceLevel level(10000); auto buy = pool->acquire(); @@ -76,7 +83,7 @@ TEST_F(PriceLevelTest, MatchOrderFullFill) { auto sell = pool->acquire(); sell->remaining_quantity = 60; - auto remaining = level.match_order(sell.get()); + auto remaining = level.match_order(sell.get(), fills); EXPECT_EQ(buy->remaining_quantity, 40); EXPECT_EQ(sell->remaining_quantity, 0); @@ -85,6 +92,9 @@ TEST_F(PriceLevelTest, MatchOrderFullFill) { } TEST_F(PriceLevelTest, MatchOrderPartialFillAcrossQueue) { + std::vector fills; + fills.reserve(100); + PriceLevel level(10000); auto b1 = pool->acquire(); @@ -99,7 +109,7 @@ TEST_F(PriceLevelTest, MatchOrderPartialFillAcrossQueue) { auto sell = pool->acquire(); sell->remaining_quantity = 60; - level.match_order(sell.get()); + level.match_order(sell.get(), fills); EXPECT_EQ(b1->remaining_quantity, 0); EXPECT_EQ(b2->remaining_quantity, 20); @@ -108,12 +118,15 @@ TEST_F(PriceLevelTest, MatchOrderPartialFillAcrossQueue) { } TEST_F(PriceLevelTest, MatchOrderEmptyLevel) { + std::vector fills; + fills.reserve(100); + PriceLevel level(10000); auto sell = pool->acquire(); sell->remaining_quantity = 100; - auto remaining = level.match_order(sell.get()); + auto remaining = level.match_order(sell.get(), fills); EXPECT_EQ(remaining, sell.get()); EXPECT_EQ(level.total_quantity(), 0); diff --git a/tools/create_itch.cpp b/tools/create_itch.cpp index 5a2e95c..4f7fece 100644 --- a/tools/create_itch.cpp +++ b/tools/create_itch.cpp @@ -1,61 +1,237 @@ #include #include -#include +#include +#include -int main() { - std::ofstream file("test_data/mock_ITCH_sample.bin", std::ios::binary); - - // Create a simple Add Order message +static uint64_t g_order_id = 1000; +static uint64_t next_id() { return ++g_order_id; } + +void write_add_order(std::ofstream& file, uint64_t order_id, + const char* symbol, char side, int64_t price, uint32_t quantity) { std::vector msg(35, 0); msg[0] = 0x00; - msg[1] = 0x23; // Length 35 - msg[2] = 'A'; // Add Order - - // Timestamp (13:00:00.000000000) - msg[3] = 0x00; - msg[4] = 0x00; - msg[5] = 0x00; - msg[6] = 0x00; - msg[7] = 0x0B; - msg[8] = 0x8B; - msg[9] = 0x8B; - msg[10] = 0x00; - - // Order ID (12345678) - msg[11] = 0x12; - msg[12] = 0x34; - msg[13] = 0x56; - msg[14] = 0x78; - msg[15] = 0x00; - msg[16] = 0x00; - msg[17] = 0x00; - msg[18] = 0x00; - - // Symbol (AAPL) - msg[19] = 'A'; - msg[20] = 'A'; - msg[21] = 'P'; - msg[22] = 'L'; - msg[23] = ' '; - msg[24] = ' '; - - // Side (Buy) - msg[25] = 'B'; - - // Price ($100.00 = 1,000,000) - msg[26] = 0x00; - msg[27] = 0x0F; - msg[28] = 0x42; - msg[29] = 0x40; - - // Quantity (100) - msg[30] = 0x00; - msg[31] = 0x00; - msg[32] = 0x00; - msg[33] = 0x64; - + msg[1] = 0x23; + msg[2] = 'A'; + + msg[3]=0x00; msg[4]=0x00; msg[5]=0x00; msg[6]=0x00; + msg[7]=0x0B; msg[8]=0x8B; msg[9]=0x8B; msg[10]=0x00; + + for (int i = 0; i < 8; ++i) + msg[11 + i] = (order_id >> (56 - i * 8)) & 0xFF; + + for (int i = 0; i < 6 && symbol[i]; ++i) + msg[19 + i] = symbol[i]; + + msg[25] = side; + + uint32_t price_raw = static_cast(price); + msg[26] = (price_raw >> 24) & 0xFF; + msg[27] = (price_raw >> 16) & 0xFF; + msg[28] = (price_raw >> 8) & 0xFF; + msg[29] = price_raw & 0xFF; + + msg[30] = (quantity >> 24) & 0xFF; + msg[31] = (quantity >> 16) & 0xFF; + msg[32] = (quantity >> 8) & 0xFF; + msg[33] = quantity & 0xFF; + file.write(reinterpret_cast(msg.data()), msg.size()); +} + +void write_cancel_order(std::ofstream& file, uint64_t order_id) { + std::vector msg(23, 0); + msg[0] = 0x00; + msg[1] = 0x17; + msg[2] = 'X'; + + msg[3]=0x00; msg[4]=0x00; msg[5]=0x00; msg[6]=0x00; + msg[7]=0x0B; msg[8]=0x8B; msg[9]=0x8B; msg[10]=0x00; + + for (int i = 0; i < 8; ++i) + msg[11 + i] = (order_id >> (56 - i * 8)) & 0xFF; + + msg[19]=0x00; msg[20]=0x00; msg[21]=0x00; msg[22]=0x64; + + file.write(reinterpret_cast(msg.data()), msg.size()); +} + +int main() { + system("mkdir test_data 2>nul"); + + std::ofstream file("test_data/NASDAQ_ITCH50_sample.bin", std::ios::binary); + if (!file) { std::cerr << "Failed to create file\n"; return 1; } + + // AAPL + + // S1: Simple full match + write_add_order(file, 1001, "AAPL", 'B', 10000, 100); + write_add_order(file, 1002, "AAPL", 'S', 9900, 100); + + // S2: Partial fill — sell only fills 30 of 50 + write_add_order(file, 1003, "AAPL", 'B', 10100, 50); + write_add_order(file, 1004, "AAPL", 'S', 10000, 30); + + // S3: Sweep two bid levels + write_add_order(file, 1005, "AAPL", 'B', 10200, 100); + write_add_order(file, 1006, "AAPL", 'B', 10100, 50); + write_add_order(file, 1007, "AAPL", 'S', 10000, 120); + + // S4: Cancel before sell arrives (MSFT block, but AAPL cancel here for parity) + uint64_t aapl_cancel_id = next_id(); + write_add_order(file, aapl_cancel_id, "AAPL", 'B', 10050, 200); + write_cancel_order(file, aapl_cancel_id); + + // S5: Partial fill remainder rests in book + write_add_order(file, 2001, "AAPL", 'S', 10000, 100); + write_add_order(file, 2002, "AAPL", 'B', 10100, 150); + + // S6: Deep book sweep — 5 ask levels, one large buy + write_add_order(file, next_id(), "AAPL", 'S', 10000, 50); + write_add_order(file, next_id(), "AAPL", 'S', 10010, 50); + write_add_order(file, next_id(), "AAPL", 'S', 10020, 50); + write_add_order(file, next_id(), "AAPL", 'S', 10030, 50); + write_add_order(file, next_id(), "AAPL", 'S', 10040, 50); + write_add_order(file, next_id(), "AAPL", 'B', 10050, 300); // sweeps all 5 + + // S7: No match — buy only, rests in book + write_add_order(file, next_id(), "AAPL", 'B', 9800, 100); + + // MSFT + + // S1: Simple full match + write_add_order(file, 1008, "MSFT", 'B', 20000, 100); + write_cancel_order(file, 1008); // cancel before fill + write_add_order(file, 1009, "MSFT", 'B', 20000, 50); + write_add_order(file, 1010, "MSFT", 'S', 19900, 50); + + // S2: Partial fill — buy 80, only 60 available + write_add_order(file, next_id(), "MSFT", 'S', 20100, 60); + write_add_order(file, next_id(), "MSFT", 'B', 20200, 80); + + // S3: Multiple bids, one large sell sweeps + write_add_order(file, next_id(), "MSFT", 'B', 20300, 100); + write_add_order(file, next_id(), "MSFT", 'B', 20200, 75); + write_add_order(file, next_id(), "MSFT", 'B', 20100, 50); + write_add_order(file, next_id(), "MSFT", 'S', 20000, 200); // sweeps top 2, partial on 3rd + + // S4: Cancel mid-book + uint64_t msft_mid = next_id(); + write_add_order(file, next_id(), "MSFT", 'B', 20500, 100); // best bid + write_add_order(file, msft_mid, "MSFT", 'B', 20400, 80); // second level + write_add_order(file, next_id(), "MSFT", 'B', 20300, 60); // third level + write_cancel_order(file, msft_mid); // remove middle level + write_add_order(file, next_id(), "MSFT", 'S', 20200, 150); // fills top + third + + // S5: No match — ask only rests + write_add_order(file, next_id(), "MSFT", 'S', 21000, 200); + + // GOOGL + + // S1: Simple full match + write_add_order(file, 1011, "GOOGL", 'B', 15000, 100); + write_add_order(file, next_id(), "GOOGL", 'S', 14900, 100); + + // S2: Large buy sweeps multiple ask levels + write_add_order(file, next_id(), "GOOGL", 'S', 15000, 80); + write_add_order(file, next_id(), "GOOGL", 'S', 15050, 80); + write_add_order(file, next_id(), "GOOGL", 'S', 15100, 80); + write_add_order(file, next_id(), "GOOGL", 'B', 15200, 300); // sweeps all 3 asks + + // S3: Alternating adds and cancels stress test + uint64_t g1 = next_id(), g2 = next_id(), g3 = next_id(); + write_add_order(file, g1, "GOOGL", 'B', 15200, 100); + write_add_order(file, g2, "GOOGL", 'B', 15100, 80); + write_add_order(file, g3, "GOOGL", 'B', 15000, 60); + write_cancel_order(file, g1); // cancel best bid + write_cancel_order(file, g3); // cancel worst bid + write_add_order(file, next_id(), "GOOGL", 'S', 14900, 80); // matches g2 only + + // S4: Partial fill remainder rests + write_add_order(file, next_id(), "GOOGL", 'S', 15000, 120); + write_add_order(file, next_id(), "GOOGL", 'B', 15100, 200); // 120 fills, 80 rests + + // S5: No match — bid too low + write_add_order(file, next_id(), "GOOGL", 'B', 14000, 50); + + // AMZN + + // S1: Simple full match + write_add_order(file, next_id(), "AMZN", 'B', 18000, 100); + write_add_order(file, next_id(), "AMZN", 'S', 17900, 100); + + // S2: Two asks at same price (FIFO — first ask fills first) + write_add_order(file, next_id(), "AMZN", 'S', 18100, 60); // first in queue + write_add_order(file, next_id(), "AMZN", 'S', 18100, 60); // second in queue + write_add_order(file, next_id(), "AMZN", 'B', 18200, 90); // fills first, partial second + + // S3: Bid side deep book + write_add_order(file, next_id(), "AMZN", 'B', 18500, 100); + write_add_order(file, next_id(), "AMZN", 'B', 18400, 80); + write_add_order(file, next_id(), "AMZN", 'B', 18300, 60); + write_add_order(file, next_id(), "AMZN", 'B', 18200, 40); + write_add_order(file, next_id(), "AMZN", 'S', 18000, 250); // sweeps top 3, partial on 4th + + // S4: Cancel then refill same level + uint64_t amzn_c = next_id(); + write_add_order(file, amzn_c, "AMZN", 'B', 18000, 200); + write_cancel_order(file, amzn_c); + write_add_order(file, next_id(), "AMZN", 'B', 18000, 150); // new order at same price + write_add_order(file, next_id(), "AMZN", 'S', 17500, 150); // should match new order + + // S5: No match — spread too wide + write_add_order(file, next_id(), "AMZN", 'B', 17000, 50); + write_add_order(file, next_id(), "AMZN", 'S', 19000, 50); + + // META + + // S1: Simple full match + write_add_order(file, next_id(), "META", 'B', 35000, 100); + write_add_order(file, next_id(), "META", 'S', 34900, 100); + + // S2: Partial fill on buy side + write_add_order(file, next_id(), "META", 'B', 35100, 200); + write_add_order(file, next_id(), "META", 'S', 35000, 120); // 120 fills, 80 bid rests + + // S3: Aggressive sell sweeps deep bid book + write_add_order(file, next_id(), "META", 'B', 35500, 100); + write_add_order(file, next_id(), "META", 'B', 35400, 80); + write_add_order(file, next_id(), "META", 'B', 35300, 60); + write_add_order(file, next_id(), "META", 'B', 35200, 40); + write_add_order(file, next_id(), "META", 'B', 35100, 20); + write_add_order(file, next_id(), "META", 'S', 35000, 400); // sweeps all 5 levels, 100 rests + + // S4: Cancel stress — add 5, cancel 3, fill remaining 2 + uint64_t m1=next_id(), m2=next_id(), m3=next_id(), m4=next_id(), m5=next_id(); + write_add_order(file, m1, "META", 'B', 35600, 100); + write_add_order(file, m2, "META", 'B', 35500, 80); + write_add_order(file, m3, "META", 'B', 35400, 60); + write_add_order(file, m4, "META", 'B', 35300, 40); + write_add_order(file, m5, "META", 'B', 35200, 20); + write_cancel_order(file, m1); + write_cancel_order(file, m3); + write_cancel_order(file, m5); + // m2 (80 @ 35500) and m4 (40 @ 35300) remain + write_add_order(file, next_id(), "META", 'S', 35000, 120); // fills m2 fully, m4 fully + + // S5: No match — ask only + write_add_order(file, next_id(), "META", 'S', 36000, 300); + + // Cross-symbol stress — rapid interleaved orders across all symbols + for (int i = 0; i < 10; ++i) { + write_add_order(file, next_id(), "AAPL", 'B', 10000 + i * 5, 50); + write_add_order(file, next_id(), "MSFT", 'B', 20000 + i * 5, 50); + write_add_order(file, next_id(), "GOOGL", 'S', 15000 - i * 5, 50); + write_add_order(file, next_id(), "AMZN", 'S', 18000 - i * 5, 50); + write_add_order(file, next_id(), "META", 'B', 35000 + i * 5, 50); + } + // Matching sells/buys to close out the above + write_add_order(file, next_id(), "AAPL", 'S', 9900, 500); + write_add_order(file, next_id(), "MSFT", 'S', 19900, 500); + write_add_order(file, next_id(), "GOOGL", 'B', 15100, 500); + write_add_order(file, next_id(), "AMZN", 'B', 18100, 500); + write_add_order(file, next_id(), "META", 'S', 34900, 500); + file.close(); - + std::cout << "Written test_data/NASDAQ_ITCH50_sample.bin\n"; return 0; } \ No newline at end of file