diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8f3e06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build/ +_codeql_build_dir/ +_codeql_detected_source_root +*.o +*.a +*.out diff --git a/.gitmodules b/.gitmodules index 09d09ff..8cf8b5e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "googletest"] path = googletest - url = git@github.com:google/googletest.git + url = https://github.com/google/googletest.git diff --git a/README.md b/README.md index 7a740a5..772ddaa 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,10 @@ struct InputOrder uint16_t trader; uint16_t qty; Side side; + uint32_t price; } ``` + The `price` field represents the limit price for the order. Buy orders will match with sell orders at or below the buy price, and sell orders will match with buy orders at or above the sell price, implementing price-time priority matching. Next, the order is taken by ```PlaceOrder``` method and matched against the appropriate order book. This process generates several types of Events: 1. ```OrderPlaced``` - indicates that order been placed into order book and is still opened. 2. ```Exec``` - means that order has been fully matched with some opposite order. diff --git a/include/exchange.h b/include/exchange.h index e09b8d8..4f89465 100644 --- a/include/exchange.h +++ b/include/exchange.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -15,9 +16,11 @@ enum EventType {OrderPlaced, Exec, Tick}; struct InternalOrder { - InternalOrder(uint16_t trd, uint16_t qt) : trader(trd), qty(qt) {} + InternalOrder(uint16_t trd, uint16_t qt, uint32_t pr) : trader(trd), origQty(qt), qty(qt), price(pr) {} uint16_t trader; - uint16_t qty; + uint16_t origQty; // original order quantity + uint16_t qty; // remaining quantity + uint32_t price; }; struct InputOrder @@ -26,12 +29,14 @@ struct InputOrder uint16_t trader; uint16_t qty; Side side; + uint32_t price; bool operator==(const InputOrder& rhs) { return instrument == rhs.instrument && trader == rhs.trader && qty == rhs.qty && - side == rhs.side; + side == rhs.side && + price == rhs.price; } }; @@ -55,11 +60,10 @@ struct Event struct Book { - Book() : actualSide(None), outstandingQty(0), openedOrdersQty(0) {} + Book() {} - uint32_t outstandingQty, openedOrdersQty; - Side actualSide; - deque orders; + map, greater> buyOrders; // descending price for buys + map> sellOrders; // ascending price for sells }; struct Notifier : public threadable @@ -78,7 +82,7 @@ struct Engine : public threadable { Engine(Notifier& notifier); - void placeOrder(char instrument, Side side, uint16_t trader, uint16_t qty); + void placeOrder(char instrument, Side side, uint16_t trader, uint16_t qty, uint32_t price); void stop(); diff --git a/src/exchange.cpp b/src/exchange.cpp index 0862358..c3f3dd2 100644 --- a/src/exchange.cpp +++ b/src/exchange.cpp @@ -18,7 +18,7 @@ void Notifier::run() { if (false == clients[event.trader]->push(event)) { - cout << "NOTIFIER WARNING: events ring is full!. Increse the clients event buffer size!.\n"; + cout << "NOTIFIER WARNING: events ring is full!. Increase the clients event buffer size!.\n"; clients[event.trader]->forcePush(event); } break; @@ -60,93 +60,141 @@ void Engine::run() InputOrder newOrder; if (true == q.pop(newOrder)) { - placeOrder(newOrder.instrument, newOrder.side, newOrder.trader, newOrder.qty); + placeOrder(newOrder.instrument, newOrder.side, newOrder.trader, newOrder.qty, newOrder.price); } } } -void Engine::placeOrder(char instrument, Side side, uint16_t trader, uint16_t qty) +void Engine::placeOrder(char instrument, Side side, uint16_t trader, uint16_t qty, uint32_t price) { if (0 == qty || None == side) return; Book& book = books[instrument]; uint16_t remainQty = qty; - - - if (true == book.orders.empty() || side == book.actualSide) { - book.actualSide = side; - book.orders.emplace_back(trader, qty); - book.outstandingQty += qty; - book.openedOrdersQty += qty; - - if (false == notify.events.push({OrderPlaced, instrument, trader, qty, side})) - { - cout << "ENGINE WARNING: events ring is full!. Increse the event buffer size!.\n"; - notify.events.forcePush({OrderPlaced, instrument, trader, qty, side}); + uint16_t executedQty = 0; + + // Try to match with opposite side orders + if (side == Buy) { + // Buy order: match with sell orders at or below our price + auto it = book.sellOrders.begin(); + while (it != book.sellOrders.end() && remainQty > 0 && it->first <= price) { + auto& orderQueue = it->second; + while (!orderQueue.empty() && remainQty > 0) { + InternalOrder& top = orderQueue.front(); + uint16_t matchQty = min(remainQty, top.qty); + + remainQty -= matchQty; + executedQty += matchQty; + top.qty -= matchQty; + + if (top.qty == 0) { + if (false == notify.events.push({Exec, instrument, top.trader, top.origQty, Sell})) { + cout << "ENGINE WARNING: events ring is full!. Increase the event buffer size!.\n"; + notify.events.forcePush({Exec, instrument, top.trader, top.origQty, Sell}); + } + orderQueue.pop_front(); + } + } + + if (orderQueue.empty()) { + it = book.sellOrders.erase(it); + } else { + ++it; + } } - } - else - { - while (false == book.orders.empty() && 0 != remainQty) { - InternalOrder& top = book.orders.front(); - uint32_t topRemainQty = (top.qty + book.outstandingQty) - book.openedOrdersQty; - if (topRemainQty > remainQty) - { - book.outstandingQty -= remainQty; - remainQty = 0; + + if (executedQty == qty) { + // Fully executed + if (false == notify.events.push({Exec, instrument, trader, qty, Buy})) { + cout << "ENGINE WARNING: events ring is full!. Increase the event buffer size!.\n"; + notify.events.forcePush({Exec, instrument, trader, qty, Buy}); } - else { - remainQty -= topRemainQty; - book.orders.pop_front(); - book.outstandingQty -= topRemainQty; - book.openedOrdersQty -= top.qty; - - if (false == notify.events.push({Exec, instrument, top.trader, top.qty, book.actualSide})) - { - cout << "ENGINE WARNING: events ring is full!. Increse the event buffer size!.\n"; - notify.events.forcePush({Exec, instrument, top.trader, top.qty, book.actualSide}); - } + } else if (remainQty > 0) { + // Partial or no execution - just place in book + // When placing, use remainQty for the current qty, but keep track of original order qty + book.buyOrders[price].emplace_back(trader, qty, price); + book.buyOrders[price].back().qty = remainQty; // Update qty to remaining amount + if (false == notify.events.push({OrderPlaced, instrument, trader, qty, Buy})) { + cout << "ENGINE WARNING: events ring is full!. Increase the event buffer size!.\n"; + notify.events.forcePush({OrderPlaced, instrument, trader, qty, Buy}); } } - - if (0 == remainQty) - { - if (false == notify.events.push({Exec, instrument, trader, qty, side})) - { - cout << "ENGINE WARNING: events ring is full!. Increse the event buffer size!.\n"; - notify.events.forcePush({Exec, instrument, trader, qty, side}); + } else { + // Sell order: match with buy orders at or above our price + auto it = book.buyOrders.begin(); + while (it != book.buyOrders.end() && remainQty > 0 && it->first >= price) { + auto& orderQueue = it->second; + while (!orderQueue.empty() && remainQty > 0) { + InternalOrder& top = orderQueue.front(); + uint16_t matchQty = min(remainQty, top.qty); + + remainQty -= matchQty; + executedQty += matchQty; + top.qty -= matchQty; + + if (top.qty == 0) { + if (false == notify.events.push({Exec, instrument, top.trader, top.origQty, Buy})) { + cout << "ENGINE WARNING: events ring is full!. Increase the event buffer size!.\n"; + notify.events.forcePush({Exec, instrument, top.trader, top.origQty, Buy}); + } + orderQueue.pop_front(); + } + } + + if (orderQueue.empty()) { + it = book.buyOrders.erase(it); + } else { + ++it; } } - else - { - book.actualSide = side; - book.orders.emplace_back(trader, qty); - book.outstandingQty += remainQty; - book.openedOrdersQty += qty; - if (false == notify.events.push({OrderPlaced, instrument, trader, qty, side})) - { - cout << "ENGINE WARNING: events ring is full!. Increse the event buffer size!.\n"; - notify.events.forcePush({OrderPlaced, instrument, trader, qty, side}); + + if (executedQty == qty) { + // Fully executed + if (false == notify.events.push({Exec, instrument, trader, qty, Sell})) { + cout << "ENGINE WARNING: events ring is full!. Increase the event buffer size!.\n"; + notify.events.forcePush({Exec, instrument, trader, qty, Sell}); + } + } else if (remainQty > 0) { + // Partial or no execution - just place in book + // When placing, use remainQty for the current qty, but keep track of original order qty + book.sellOrders[price].emplace_back(trader, qty, price); + book.sellOrders[price].back().qty = remainQty; // Update qty to remaining amount + if (false == notify.events.push({OrderPlaced, instrument, trader, qty, Sell})) { + cout << "ENGINE WARNING: events ring is full!. Increase the event buffer size!.\n"; + notify.events.forcePush({OrderPlaced, instrument, trader, qty, Sell}); } } } - // market data - if (false == book.orders.empty()) - { - if (false == notify.events.push({Tick, instrument, 0, book.outstandingQty, book.actualSide})) - { - cout << "ENGINE WARNING: events ring is full!. Increse the event buffer size!.\n"; - notify.events.forcePush({Tick, instrument, 0, book.outstandingQty, book.actualSide}); + // market data - calculate total outstanding qty across all price levels + uint32_t totalOutstanding = 0; + Side bestSide = None; + + if (!book.buyOrders.empty()) { + for (const auto& priceLevel : book.buyOrders) { + for (const auto& order : priceLevel.second) { + totalOutstanding += order.qty; + } } + bestSide = Buy; } - else - { - if (false == notify.events.push({Tick, instrument, 0, 0, None})) - { - cout << "ENGINE WARNING: events ring is full!. Increse the event buffer size!.\n"; - notify.events.forcePush({Tick, instrument, 0, 0, None}); + + if (!book.sellOrders.empty()) { + uint32_t sellQty = 0; + for (const auto& priceLevel : book.sellOrders) { + for (const auto& order : priceLevel.second) { + sellQty += order.qty; + } } + if (sellQty > 0) { + totalOutstanding += sellQty; + bestSide = (bestSide == Buy) ? None : Sell; + } + } + + if (false == notify.events.push({Tick, instrument, 0, totalOutstanding, bestSide})) { + cout << "ENGINE WARNING: events ring is full!. Increase the event buffer size!.\n"; + notify.events.forcePush({Tick, instrument, 0, totalOutstanding, bestSide}); } } diff --git a/testsuite.cpp b/testsuite.cpp index b78b28b..a5ab860 100644 --- a/testsuite.cpp +++ b/testsuite.cpp @@ -19,10 +19,10 @@ TEST(MatchingEngineTest, FourOrders) Engine& eng = ex.engine; Event event; - eng.placeOrder('A', Buy, 666, 100); - eng.placeOrder('A', Buy, 777, 200); - eng.placeOrder('A', Sell, 888, 200); - eng.placeOrder('A', Sell, 888, 100); + eng.placeOrder('A', Buy, 666, 100, 100); + eng.placeOrder('A', Buy, 777, 200, 100); + eng.placeOrder('A', Sell, 888, 200, 100); + eng.placeOrder('A', Sell, 888, 100, 100); ASSERT_TRUE (true == notif.events.pop(event) && (Event{OrderPlaced,'A',666,100,Buy}) == event); ASSERT_TRUE (true == notif.events.pop(event) && (Event{Tick,'A',0,100,Buy}) == event); @@ -44,8 +44,8 @@ TEST(MatchingEngineTest, CasesFromEmail) Engine& eng = ex.engine; Event event; - eng.placeOrder('S', Buy, 1, 200); - eng.placeOrder('S', Sell, 2, 200); + eng.placeOrder('S', Buy, 1, 200, 100); + eng.placeOrder('S', Sell, 2, 200, 100); ASSERT_TRUE (true == notif.events.pop(event) && (Event{OrderPlaced,'S',1,200,Buy}) == event); ASSERT_TRUE (true == notif.events.pop(event) && (Event{Tick,'S',0,200,Buy}) == event); @@ -53,9 +53,9 @@ TEST(MatchingEngineTest, CasesFromEmail) ASSERT_TRUE (true == notif.events.pop(event) && (Event{Exec,'S',2,200,Sell}) == event); ASSERT_TRUE (true == notif.events.pop(event) && (Event{Tick,'S',0,0,None}) == event); - eng.placeOrder('G', Sell, 3, 300); - eng.placeOrder('G', Buy, 4, 200); - eng.placeOrder('G', Buy, 5, 200); + eng.placeOrder('G', Sell, 3, 300, 100); + eng.placeOrder('G', Buy, 4, 200, 100); + eng.placeOrder('G', Buy, 5, 200, 100); ASSERT_TRUE (true == notif.events.pop(event) && (Event{OrderPlaced,'G',3,300,Sell}) == event); ASSERT_TRUE (true == notif.events.pop(event) && (Event{Tick,'G',0,300,Sell}) == event); @@ -65,10 +65,10 @@ TEST(MatchingEngineTest, CasesFromEmail) ASSERT_TRUE (true == notif.events.pop(event) && (Event{OrderPlaced,'G',5,200,Buy}) == event); ASSERT_TRUE (true == notif.events.pop(event) && (Event{Tick,'G',0,100,Buy}) == event); - eng.placeOrder('H', Sell, 6, 200); - eng.placeOrder('H', Sell, 7, 200); - eng.placeOrder('H', Sell, 8, 200); - eng.placeOrder('H', Buy, 9, 600); + eng.placeOrder('H', Sell, 6, 200, 100); + eng.placeOrder('H', Sell, 7, 200, 100); + eng.placeOrder('H', Sell, 8, 200, 100); + eng.placeOrder('H', Buy, 9, 600, 100); ASSERT_TRUE (true == notif.events.pop(event) && (Event{OrderPlaced,'H',6,200,Sell}) == event); ASSERT_TRUE (true == notif.events.pop(event) && (Event{Tick,'H',0,200,Sell}) == event); @@ -95,19 +95,19 @@ TEST(MatchingEngineTest, OverEatingOneSide) { Side otherSide = (Buy == side) ? Sell : Buy; - eng.placeOrder('S', side, 1, 100); - eng.placeOrder('S', side, 2, 200); - eng.placeOrder('S', side, 3, 300); - eng.placeOrder('S', side, 4, 400); - eng.placeOrder('S', side, 5, 500); - eng.placeOrder('S', otherSide, 6, 100); - eng.placeOrder('S', otherSide, 7, 100); - eng.placeOrder('S', otherSide, 8, 100); - eng.placeOrder('S', otherSide, 9, 150); - eng.placeOrder('S', otherSide, 10, 300); - eng.placeOrder('S', otherSide, 11, 100); - eng.placeOrder('S', otherSide, 12, 700); - eng.placeOrder('S', side, 13, 50); + eng.placeOrder('S', side, 1, 100, 100); + eng.placeOrder('S', side, 2, 200, 100); + eng.placeOrder('S', side, 3, 300, 100); + eng.placeOrder('S', side, 4, 400, 100); + eng.placeOrder('S', side, 5, 500, 100); + eng.placeOrder('S', otherSide, 6, 100, 100); + eng.placeOrder('S', otherSide, 7, 100, 100); + eng.placeOrder('S', otherSide, 8, 100, 100); + eng.placeOrder('S', otherSide, 9, 150, 100); + eng.placeOrder('S', otherSide, 10, 300, 100); + eng.placeOrder('S', otherSide, 11, 100, 100); + eng.placeOrder('S', otherSide, 12, 700, 100); + eng.placeOrder('S', side, 13, 50, 100); ASSERT_TRUE (true == notif.events.pop(event) && (Event{OrderPlaced,'S',1,100,side}) == event); ASSERT_TRUE (true == notif.events.pop(event) && (Event{Tick,'S',0,100,side}) == event); @@ -171,12 +171,12 @@ TEST(MatchingEngineTest, OrderSlicing) Engine& eng = ex.engine; Event event; - eng.placeOrder('H', Buy, 3, 5); - eng.placeOrder('H', Sell, 1, 11); - eng.placeOrder('H', Buy, 2, 7); - eng.placeOrder('H', Buy, 3, 5); - eng.placeOrder('H', Sell, 1, 11); - eng.placeOrder('H', Buy, 3, 5); + eng.placeOrder('H', Buy, 3, 5, 100); + eng.placeOrder('H', Sell, 1, 11, 100); + eng.placeOrder('H', Buy, 2, 7, 100); + eng.placeOrder('H', Buy, 3, 5, 100); + eng.placeOrder('H', Sell, 1, 11, 100); + eng.placeOrder('H', Buy, 3, 5, 100); ASSERT_TRUE (true == notif.events.pop(event) && (Event{OrderPlaced, 'H', 3, 5, Buy}) == event); @@ -215,13 +215,13 @@ TEST_P(MatchingEnginePerformance, EventsBurst) uint32_t qty = 0; for (int i = 0; i < GetParam(); i++) { - eng.placeOrder('H', Buy, 1, 1); + eng.placeOrder('H', Buy, 1, 1, 100); ASSERT_TRUE (true == notif.events.pop(event) && (Event{OrderPlaced, 'H', 1, 1, Buy}) == event); qty += 1; ASSERT_TRUE (true == notif.events.pop(event) && (Event{Tick,'H', 0, qty, Buy}) == event); } - eng.placeOrder('H', Sell, 2, GetParam()); + eng.placeOrder('H', Sell, 2, GetParam(), 100); for (int i = 0; i < GetParam(); i++) { ASSERT_TRUE (true == notif.events.pop(event) && (Event{Exec, 'H', 1, 1, Buy}) == event); @@ -244,12 +244,12 @@ TEST(MultiProducerMultiConsumerQueueTest, OneThread_perf) for (uint32_t i = 0; i < 1000000; i++) { uint16_t x = static_cast(i); - q.push(InputOrder{'A', x,x, (x % 2) ? Buy : Sell}); + q.push(InputOrder{'A', x,x, (x % 2) ? Buy : Sell, 100}); if (i >= 10000) { q.pop(order); - ASSERT_TRUE ((InputOrder{'A', static_cast(x-10000), static_cast(x-10000), (x % 2) ? Buy : Sell}) == order); + ASSERT_TRUE ((InputOrder{'A', static_cast(x-10000), static_cast(x-10000), (x % 2) ? Buy : Sell, 100}) == order); } } @@ -263,7 +263,7 @@ TEST(MultiProducerMultiConsumerQueueTest, TwoThreads_perf) for (uint32_t i = 0; i < 1000000; i++) { uint16_t x = static_cast(i); - q.push(InputOrder{'A', x, x, (x % 2) ? Buy : Sell}); + q.push(InputOrder{'A', x, x, (x % 2) ? Buy : Sell, 100}); } }); @@ -272,7 +272,7 @@ TEST(MultiProducerMultiConsumerQueueTest, TwoThreads_perf) uint16_t x = static_cast(i); InputOrder order; q.pop(order); - ASSERT_TRUE ((InputOrder{'A', x, x, (x % 2) ? Buy : Sell}) == order); + ASSERT_TRUE ((InputOrder{'A', x, x, (x % 2) ? Buy : Sell, 100}) == order); } t1.join(); @@ -360,7 +360,7 @@ TEST_F(IntegrationTest, OneTraderConnectedToExchange) bool orderAccepted = false; auto init = [](TradingTool* me){ - me->q->push(InputOrder{'H', me->id, 10, Sell}); + me->q->push(InputOrder{'H', me->id, 10, Sell, 100}); }; auto algo = [&](TradingTool* me, Event e){ @@ -401,7 +401,7 @@ TEST_F(IntegrationTest, TwoTraderConnectedToExchange) bool orderExec2 = false; auto init = [](TradingTool* me){ - me->q->push(InputOrder{'H', me->id, 10, (me->id % 2) ? Sell : Buy}); + me->q->push(InputOrder{'H', me->id, 10, (me->id % 2) ? Sell : Buy, 100}); }; auto algo = [&](TradingTool* me, Event e){ @@ -452,7 +452,7 @@ TEST_F(IntegrationTest, ThreeTraderConnectedToExchange_OrderSlicing_15250_orders auto init = [&](TradingTool* me){ //unique_lock lc(m); //cout << "AddOrder: trader= " << me->id << endl; - me->q->push(InputOrder{'H', me->id, traderNotebook[me->id].slice, traderNotebook[me->id].s}); + me->q->push(InputOrder{'H', me->id, traderNotebook[me->id].slice, traderNotebook[me->id].s, 100}); /* cout << "eng.placeOrder('" << 'H' @@ -481,7 +481,7 @@ TEST_F(IntegrationTest, ThreeTraderConnectedToExchange_OrderSlicing_15250_orders } if (toExec != 0) { - me->q->push(InputOrder{'H', me->id, traderNotebook[me->id].slice, traderNotebook[me->id].s}); + me->q->push(InputOrder{'H', me->id, traderNotebook[me->id].slice, traderNotebook[me->id].s, 100}); /* cout << "eng.placeOrder('" << e.instrument @@ -533,6 +533,31 @@ TEST_F(IntegrationTest, ThreeTraderConnectedToExchange_OrderSlicing_15250_orders ASSERT_TRUE (traderNotebook[3].done); } +TEST(MatchingEngineTest, OrderPriceParameter) +{ + Exchange ex; + Notifier& notif = ex.notif; + Engine& eng = ex.engine; + Event event; + + // Test price-time priority matching + eng.placeOrder('L', Buy, 1, 100, 50); + eng.placeOrder('L', Buy, 2, 200, 75); + eng.placeOrder('L', Sell, 3, 150, 50); + + // Verify events generated + ASSERT_TRUE (true == notif.events.pop(event) && (Event{OrderPlaced,'L',1,100,Buy}) == event); + ASSERT_TRUE (true == notif.events.pop(event) && (Event{Tick,'L',0,100,Buy}) == event); + ASSERT_TRUE (true == notif.events.pop(event) && (Event{OrderPlaced,'L',2,200,Buy}) == event); + ASSERT_TRUE (true == notif.events.pop(event) && (Event{Tick,'L',0,300,Buy}) == event); + // Sell @50 matches with best buy (trader 2 @75) for 150 qty + // Trader 2's order is partially filled (150 of 200), so only the sell generates Exec + ASSERT_TRUE (true == notif.events.pop(event) && (Event{Exec,'L',3,150,Sell}) == event); + // Remaining: trader 2 has 50 @75, trader 1 has 100 @50 = 150 total + ASSERT_TRUE (true == notif.events.pop(event) && (Event{Tick,'L',0,150,Buy}) == event); + ASSERT_FALSE (notif.events.pop(event)); +} + //======================== MAIN MAIN ============================== int main(int argc, char** argv) {