From 2275b3b545f73a0f59dc3885760bd152aa7ae578 Mon Sep 17 00:00:00 2001 From: Matthew Date: Thu, 23 Apr 2026 19:26:24 -0500 Subject: [PATCH] Create position manager --- benchmarks/bench_matching_engine.cpp | 10 +- include/velox/matching/matching_engine.hpp | 7 +- include/velox/risk/position_manager.hpp | 36 ++++--- src/matching/matching_engine.cpp | 15 ++- src/risk/position_manager.cpp | 111 ++++++++++++--------- tests/CMakeLists.txt | 1 + tests/test_matching_engine.cpp | 2 +- tests/test_position_manager.cpp | 87 ++++++++++++++++ 8 files changed, 190 insertions(+), 79 deletions(-) create mode 100644 tests/test_position_manager.cpp diff --git a/benchmarks/bench_matching_engine.cpp b/benchmarks/bench_matching_engine.cpp index cc11ffa..42724c7 100644 --- a/benchmarks/bench_matching_engine.cpp +++ b/benchmarks/bench_matching_engine.cpp @@ -111,7 +111,7 @@ static void BM_MatchingEngine_NoRisk_NoGateway(benchmark::State& state) { Pool pool; std::vector owned; NullGateway gateway(g_gateway_pool); - MatchingEngine engine("AAPL", nullptr, &gateway); + MatchingEngine engine("AAPL", nullptr, &gateway, nullptr); const int N = state.range(0); std::vector orders; @@ -142,7 +142,7 @@ static void BM_MatchingEngine_WithRisk_NoGateway(benchmark::State& state) { std::vector owned; RiskManager risk; NullGateway gateway(g_gateway_pool); - MatchingEngine engine("AAPL", &risk, &gateway); + MatchingEngine engine("AAPL", &risk, &gateway, nullptr); const int N = state.range(0); std::vector orders; @@ -172,7 +172,7 @@ static void BM_MatchingEngine_NoRisk_RealGateway(benchmark::State& state) { Pool pool; std::vector owned; RealGateway gateway(g_gateway_pool); - MatchingEngine engine("AAPL", nullptr, &gateway); + MatchingEngine engine("AAPL", nullptr, &gateway, nullptr); const int N = state.range(0); std::vector orders; @@ -203,7 +203,7 @@ static void BM_MatchingEngine_FullPipeline(benchmark::State& state) { std::vector owned; RiskManager risk; RealGateway gateway(g_gateway_pool); - MatchingEngine engine("AAPL", &risk, &gateway); + MatchingEngine engine("AAPL", &risk, &gateway, nullptr); const int N = state.range(0); std::vector orders; @@ -234,7 +234,7 @@ static void BM_MatchingEngine_Throughput(benchmark::State& state) { std::vector owned; RiskManager risk; RealGateway gateway(g_gateway_pool); - MatchingEngine engine("AAPL", &risk, &gateway); + MatchingEngine engine("AAPL", &risk, &gateway, nullptr); const int N = 1000; std::vector orders; diff --git a/include/velox/matching/matching_engine.hpp b/include/velox/matching/matching_engine.hpp index 70d4a31..7ac9dd3 100644 --- a/include/velox/matching/matching_engine.hpp +++ b/include/velox/matching/matching_engine.hpp @@ -1,11 +1,12 @@ #pragma once #include #include "velox/book/order_book.hpp" -#include "velox/matching/order.hpp" #include "lockfree/spsc_queue.hpp" #include "lockfree/pool.hpp" #include "velox/risk/risk_manager.hpp" #include "velox/gateway/execution_gateway.hpp" +#include "velox/risk/position_manager.hpp" +#include "velox/matching/order.hpp" namespace velox { @@ -15,7 +16,8 @@ class MatchingEngine { MatchingEngine(const char* symbol, RiskManager* risk_manager, - ExecutionGateway* gateway); + ExecutionGateway* gateway, + PositionManager* position_manager); ~MatchingEngine(); @@ -40,6 +42,7 @@ class MatchingEngine { OrderBook m_order_book; RiskManager* m_risk_manager; ExecutionGateway* m_gateway; + PositionManager* m_position_manager; OrderQueue m_incoming_orders; diff --git a/include/velox/risk/position_manager.hpp b/include/velox/risk/position_manager.hpp index cdcb976..1de3a87 100644 --- a/include/velox/risk/position_manager.hpp +++ b/include/velox/risk/position_manager.hpp @@ -1,44 +1,42 @@ #pragma once #include #include +#include +#include #include "velox/matching/order.hpp" namespace velox { +struct Position { + std::atomic net_position{0}; // Total bought - total sold + std::atomic realized_pnl{0}; // Locked-in P&L + std::atomic avg_entry_price{0}; // Average entry price + std::atomic total_bought{0}; + std::atomic total_sold{0}; +}; + class PositionManager { public: PositionManager(); ~PositionManager(); - // Update position after fill - void update_position(const Order* order, uint32_t fill_quantity); + // Update position after fill (called by Execution Gateway) + void update_position(const Order* order, uint32_t fill_quantity, int64_t fill_price); // Get current position for symbol int64_t get_position(const char* symbol) const; // P&L tracking - void record_fill(const Order* order, uint32_t fill_quantity, int64_t fill_price); - int64_t get_realized_pnl() const; - int64_t get_unrealized_pnl(int64_t current_price) const; + int64_t get_realized_pnl(const char* symbol) const; + int64_t get_unrealized_pnl(const char* symbol, int64_t current_price) const; + int64_t get_total_pnl(const char* symbol, int64_t current_price) const; // Reset for testing void reset(); - - // Hash symbol - uint32_t hash_symbol(const char* symbol) const; private: - struct Position { - std::atomic net_position{0}; - std::atomic realized_pnl{0}; - std::atomic avg_entry_price{0}; - std::atomic total_bought{0}; - std::atomic total_sold{0}; - }; - - Position m_positions[256]; - std::atomic m_total_realized_pnl{0}; - void update_average_price(Position& pos, uint32_t quantity, int64_t price); + std::unordered_map m_positions; + void update_realized_pnl(Position& pos, int64_t price); }; } \ No newline at end of file diff --git a/src/matching/matching_engine.cpp b/src/matching/matching_engine.cpp index 3d65be4..05027ac 100644 --- a/src/matching/matching_engine.cpp +++ b/src/matching/matching_engine.cpp @@ -5,10 +5,12 @@ namespace velox { MatchingEngine::MatchingEngine(const char* symbol, RiskManager* risk_manager, - ExecutionGateway* gateway) + ExecutionGateway* gateway, + PositionManager* positiion_manager) : m_order_book(symbol), m_risk_manager(risk_manager), - m_gateway(gateway) {} + m_gateway(gateway), + m_position_manager(positiion_manager) {} MatchingEngine::~MatchingEngine() = default; @@ -73,7 +75,14 @@ void MatchingEngine::send_fill(Order* order, uint32_t fill_quantity, int64_t fil (void)fill_quantity; (void)fill_price; - m_gateway->send_order(order); + if (m_gateway) { + m_gateway->send_order(order); + } + + // Update position manager + if (m_position_manager) { + m_position_manager->update_position(order, fill_quantity, fill_price); + } } bool MatchingEngine::check_risk(const Order* order) const { diff --git a/src/risk/position_manager.cpp b/src/risk/position_manager.cpp index 23a60b8..6d7e893 100644 --- a/src/risk/position_manager.cpp +++ b/src/risk/position_manager.cpp @@ -1,77 +1,90 @@ #include "velox/risk/position_manager.hpp" #include +#include namespace velox { PositionManager::PositionManager() = default; PositionManager::~PositionManager() = default; -void PositionManager::update_position(const Order* order, uint32_t fill_quantity) { - if (!order) return; - - uint32_t idx = hash_symbol(order->symbol); - auto& pos = m_positions[idx]; +void PositionManager::update_position(const Order* order, uint32_t fill_quantity, int64_t fill_price) { + if (!order || fill_quantity == 0) return; + std::string symbol(order->symbol); + + // Create default position if symbol doesn't exist in system + auto& pos = m_positions[symbol]; + if (order->is_buy()) { - int64_t new_pos = pos.net_position.load() + static_cast(fill_quantity); - pos.net_position.store(new_pos, std::memory_order_release); - pos.total_bought.fetch_add(fill_quantity, std::memory_order_release); - update_average_price(pos, fill_quantity, order->price); - } else { - int64_t new_pos = pos.net_position.load() - static_cast(fill_quantity); - pos.net_position.store(new_pos, std::memory_order_release); + // 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); + uint64_t old_bought = pos.total_bought.load(std::memory_order_acquire); + + 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; + + pos.net_position.store(new_position, std::memory_order_release); + pos.avg_entry_price.store(new_avg, std::memory_order_release); + pos.total_bought.store(new_bought, std::memory_order_release); + } + else { + // Sell: decrease position, realize P&L + 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); + + // Realized P&L = (sell_price - avg_price) * quantity + int64_t pnl = (fill_price - old_avg) * fill_quantity; + update_realized_pnl(pos, pnl); + + int64_t new_position = old_position - fill_quantity; + pos.net_position.store(new_position, std::memory_order_release); pos.total_sold.fetch_add(fill_quantity, std::memory_order_release); } } -int64_t PositionManager::get_position(const char* symbol) const { - uint32_t idx = hash_symbol(symbol); - return m_positions[idx].net_position.load(std::memory_order_acquire); +void PositionManager::update_realized_pnl(Position& pos, int64_t pnl) { + pos.realized_pnl.fetch_add(pnl, std::memory_order_release); } -void PositionManager::record_fill(const Order* order, uint32_t fill_quantity, int64_t fill_price) { - update_position(order, fill_quantity); - // TODO: Calculate P&L -} - -int64_t PositionManager::get_realized_pnl() const { - return m_total_realized_pnl.load(std::memory_order_acquire); +int64_t PositionManager::get_position(const char* symbol) const { + 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_unrealized_pnl(int64_t current_price) const { - // TODO: Calculate unrealized P&L based on positions - return 0; +int64_t PositionManager::get_realized_pnl(const char* symbol) const { + auto it = m_positions.find(symbol); + if (it == m_positions.end()) return 0; + return it->second.realized_pnl.load(std::memory_order_acquire); } -void PositionManager::reset() { - for (auto& pos : m_positions) { - pos.net_position.store(0, std::memory_order_release); - pos.realized_pnl.store(0, std::memory_order_release); - pos.avg_entry_price.store(0, std::memory_order_release); - pos.total_bought.store(0, std::memory_order_release); - pos.total_sold.store(0, std::memory_order_release); +int64_t PositionManager::get_unrealized_pnl(const char* symbol, int64_t current_price) const { + auto it = m_positions.find(symbol); + if (it == m_positions.end()) return 0; + + int64_t position = it->second.net_position.load(std::memory_order_acquire); + if (position == 0) return 0; + + int64_t avg_price = it->second.avg_entry_price.load(std::memory_order_acquire); + if (position > 0) { + // Long position: (current - avg) * position + return (current_price - avg_price) * position; + } else { + // Short position: (avg - current) * (-position) + return (avg_price - current_price) * (-position); } - m_total_realized_pnl.store(0, std::memory_order_release); } -uint32_t PositionManager::hash_symbol(const char* symbol) const { - uint32_t hash = 0; - while (*symbol) { - hash = hash * 31 + static_cast(*symbol++); - } - return hash % 256; +int64_t PositionManager::get_total_pnl(const char* symbol, int64_t current_price) const { + return get_realized_pnl(symbol) + get_unrealized_pnl(symbol, current_price); } -void PositionManager::update_average_price(Position& pos, uint32_t quantity, int64_t price) { - int64_t total_bought = pos.total_bought.load(); - int64_t old_avg = pos.avg_entry_price.load(); - - if (total_bought == 0) { - pos.avg_entry_price.store(price, std::memory_order_release); - } else { - int64_t new_avg = (old_avg * total_bought + price * quantity) / (total_bought + quantity); - pos.avg_entry_price.store(new_avg, std::memory_order_release); - } +void PositionManager::reset() { + m_positions.clear(); } } \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3049986..ad99760 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(velox_tests test_execution_gateway.cpp test_feed_handler.cpp test_book_snapshot.cpp + test_position_manager.cpp ) target_link_libraries(velox_tests diff --git a/tests/test_matching_engine.cpp b/tests/test_matching_engine.cpp index d884045..44d3f6d 100644 --- a/tests/test_matching_engine.cpp +++ b/tests/test_matching_engine.cpp @@ -12,7 +12,7 @@ class MatchingEngineTest : public ::testing::Test { pool = std::make_unique>(); risk = std::make_unique(); gateway = std::make_unique(); - engine = std::make_unique("AAPL", risk.get(), gateway.get()); + engine = std::make_unique("AAPL", risk.get(), gateway.get(), nullptr); } Order* create_order(uint64_t id, OrderSide side, int64_t price, uint32_t qty) { diff --git a/tests/test_position_manager.cpp b/tests/test_position_manager.cpp new file mode 100644 index 0000000..8e93af3 --- /dev/null +++ b/tests/test_position_manager.cpp @@ -0,0 +1,87 @@ +#include +#include "velox/risk/position_manager.hpp" +#include "lockfree/pool.hpp" +#include + +using namespace velox; + +class PositionManagerTest : public ::testing::Test { +protected: + void SetUp() override { + pool = std::make_unique>(); + mgr = std::make_unique(); + } + + Order* create_order(uint64_t id, OrderSide side, const char* sym, int64_t price, uint32_t qty) { + auto o = pool->acquire(); + o->order_id = id; + o->side = side; + o->price = price; + o->quantity = qty; + o->remaining_quantity = qty; + std::strncpy(o->symbol, sym, 7); + Order* raw = o.get(); + owned.push_back(std::move(o)); + return raw; + } + + std::unique_ptr> pool; + std::unique_ptr mgr; + std::vector> owned; +}; + +TEST_F(PositionManagerTest, BuyCreatesLongPosition) { + auto order = create_order(1, OrderSide::BUY, "AAPL", 10000, 100); + mgr->update_position(order, 100, 10000); + + EXPECT_EQ(mgr->get_position("AAPL"), 100); + EXPECT_EQ(mgr->get_realized_pnl("AAPL"), 0); + EXPECT_DOUBLE_EQ(mgr->get_unrealized_pnl("AAPL", 10100), 100 * 100); // 100 * 100 = 10000 +} + +TEST_F(PositionManagerTest, SellClosesPosition) { + auto buy = create_order(1, OrderSide::BUY, "AAPL", 10000, 100); + mgr->update_position(buy, 100, 10000); + + auto sell = create_order(2, OrderSide::SELL, "AAPL", 10100, 100); + mgr->update_position(sell, 100, 10100); + + EXPECT_EQ(mgr->get_position("AAPL"), 0); + EXPECT_EQ(mgr->get_realized_pnl("AAPL"), 100 * 100); // profit = (10100-10000)*100 = 10000 + EXPECT_EQ(mgr->get_unrealized_pnl("AAPL", 10100), 0); +} + +TEST_F(PositionManagerTest, PartialFillLeavesRemaining) { + auto buy = create_order(1, OrderSide::BUY, "AAPL", 10000, 100); + mgr->update_position(buy, 60, 10000); + + EXPECT_EQ(mgr->get_position("AAPL"), 60); + EXPECT_EQ(mgr->get_unrealized_pnl("AAPL", 10100), 60 * 100); +} + +TEST_F(PositionManagerTest, MultipleBuysWeightedAverage) { + mgr->update_position(create_order(1, OrderSide::BUY, "AAPL", 10000, 100), 100, 10000); + mgr->update_position(create_order(2, OrderSide::BUY, "AAPL", 10200, 100), 100, 10200); + + EXPECT_EQ(mgr->get_position("AAPL"), 200); + // Avg price = (10000*100 + 10200*100)/200 = 10100 + EXPECT_EQ(mgr->get_unrealized_pnl("AAPL", 10100), 0); + EXPECT_EQ(mgr->get_unrealized_pnl("AAPL", 10200), 20000); // 200 * (10200-10100) = 20000 +} + +TEST_F(PositionManagerTest, DifferentSymbolsIndependent) { + mgr->update_position(create_order(1, OrderSide::BUY, "AAPL", 10000, 100), 100, 10000); + mgr->update_position(create_order(2, OrderSide::BUY, "MSFT", 20000, 50), 50, 20000); + + EXPECT_EQ(mgr->get_position("AAPL"), 100); + EXPECT_EQ(mgr->get_position("MSFT"), 50); + EXPECT_EQ(mgr->get_unrealized_pnl("AAPL", 10100), 10000); + EXPECT_EQ(mgr->get_unrealized_pnl("MSFT", 20100), 50 * 100); +} + +TEST_F(PositionManagerTest, ResetClearsAll) { + mgr->update_position(create_order(1, OrderSide::BUY, "AAPL", 10000, 100), 100, 10000); + mgr->reset(); + EXPECT_EQ(mgr->get_position("AAPL"), 0); + EXPECT_EQ(mgr->get_realized_pnl("AAPL"), 0); +} \ No newline at end of file