Skip to content
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
build/
_codeql_build_dir/
_codeql_detected_source_root
*.o
*.a
*.out
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[submodule "googletest"]
path = googletest
url = git@github.com:google/googletest.git
url = https://github.com/google/googletest.git
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 12 additions & 8 deletions include/exchange.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <unordered_map>
#include <map>
#include <deque>
#include <thread>
#include <utility>
Expand All @@ -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
Expand All @@ -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;
}
};

Expand All @@ -55,11 +60,10 @@ struct Event

struct Book
{
Book() : actualSide(None), outstandingQty(0), openedOrdersQty(0) {}
Book() {}

uint32_t outstandingQty, openedOrdersQty;
Side actualSide;
deque<InternalOrder> orders;
map<uint32_t, deque<InternalOrder>, greater<uint32_t>> buyOrders; // descending price for buys
map<uint32_t, deque<InternalOrder>> sellOrders; // ascending price for sells
};

struct Notifier : public threadable
Expand All @@ -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();

Expand Down
180 changes: 114 additions & 66 deletions src/exchange.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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});
}
}

Expand Down
Loading