From 0817e7ac7e336ac0c67233f28909ff8df5389836 Mon Sep 17 00:00:00 2001 From: Bruno BELANYI Date: Sat, 12 Mar 2022 05:47:34 +0100 Subject: [PATCH] kraken: engin: add 'Engine' This is the brains of the operation, the matching engine. --- src/engine/CMakeLists.txt | 2 + src/engine/engine.cc | 187 ++++++++++++++++++++++++++++++++++++++ src/engine/engine.hh | 46 ++++++++++ 3 files changed, 235 insertions(+) create mode 100644 src/engine/engine.cc create mode 100644 src/engine/engine.hh diff --git a/src/engine/CMakeLists.txt b/src/engine/CMakeLists.txt index 88ba087..cf24bd5 100644 --- a/src/engine/CMakeLists.txt +++ b/src/engine/CMakeLists.txt @@ -1,6 +1,8 @@ add_library(engine STATIC csv-engine-listener.cc csv-engine-listener.hh + engine.cc + engine.hh engine-listener.cc engine-listener.hh ) diff --git a/src/engine/engine.cc b/src/engine/engine.cc new file mode 100644 index 0000000..36bc323 --- /dev/null +++ b/src/engine/engine.cc @@ -0,0 +1,187 @@ +#include "engine.hh" + +#include +#include + +#include + +#include "engine-listener.hh" + +namespace kraken::engine { + +// A RAII wrapper to ensure the top-of-book callback is made +struct CallbackOnTopOfBookChange { + CallbackOnTopOfBookChange(Symbol symbol, Engine& engine) + : symbol_(symbol), engine_(engine) { + top_info_ = calculate_top(); + } + + ~CallbackOnTopOfBookChange() { + auto const new_top = calculate_top(); + + // Sanity check: both sides of the book are not changing at once + assert(!(new_top.asks != top_info_.asks + && new_top.bids != top_info_.bids)); + + if (top_info_.asks != new_top.asks) { + if (new_top.asks) { + engine_.listener_->on_top_of_book_change( + Side::ASK, new_top.asks->first, new_top.asks->second); + } else { + engine_.listener_->on_top_of_book_change(Side::ASK); + } + } else if (top_info_.bids != new_top.bids) { + if (new_top.bids) { + engine_.listener_->on_top_of_book_change( + Side::BID, new_top.bids->first, new_top.bids->second); + } else { + engine_.listener_->on_top_of_book_change(Side::BID); + } + } + } + +private: + struct TopInfo { + std::optional> asks; + std::optional> bids; + }; + + TopInfo calculate_top() const { + auto info = TopInfo{}; + + if (auto const bid_it = engine_.bids_.find(symbol_); + bid_it != engine_.bids_.end()) { + auto const& [_, bid_map] = *bid_it; + + if (bid_map.size() > 0) { + auto const price = bid_map.begin()->first; + int quantity = 0; + auto const [begin, end] = bid_map.equal_range(price); + + for (auto it = begin; it != end; ++it) { + quantity += int(it->second.quantity); + } + + info.bids = {price, Quantity(quantity)}; + } + } + + if (auto const ask_it = engine_.asks_.find(symbol_); + ask_it != engine_.asks_.end()) { + auto const& [_, ask_map] = *ask_it; + + if (ask_map.size() > 0) { + auto const price = ask_map.begin()->first; + int quantity = 0; + auto const [begin, end] = ask_map.equal_range(price); + + for (auto it = begin; it != end; ++it) { + quantity += int(it->second.quantity); + } + + info.asks = {price, Quantity(quantity)}; + } + } + + return info; + } + + Symbol symbol_; + Engine& engine_; + TopInfo top_info_{}; +}; + +Engine::Engine(std::shared_ptr listener) + : listener_(listener) {} + +void Engine::process_orders(std::vector const& orders) { + for (auto const& order : orders) { + std::visit([this](auto const& trade_order) { (*this)(trade_order); }, + order); + } +} + +void Engine::operator()(TradeOrder const& trade_order) { + // Set-up automatic call-back in case top-of-book changes + auto _ = CallbackOnTopOfBookChange(trade_order.symbol, *this); + + // NOTE: some amount of repetition/mirroring + switch (trade_order.side) { + case Side::ASK: + if (auto bid_map_it = bids_.find(trade_order.symbol); + bid_map_it != bids_.end()) { + auto& [_, bid_map] = *bid_map_it; + if (bid_map.size() > 0 + && bid_map.begin()->first <= trade_order.price) { + // FIXME: handle matching if enabled + listener_->on_rejection(trade_order.user, trade_order.id); + return; + } + } + asks_[trade_order.symbol].insert( + {trade_order.price, OrderMetaData{trade_order.user, trade_order.id, + trade_order.quantity}}); + break; + case Side::BID: + if (auto ask_map_it = asks_.find(trade_order.symbol); + ask_map_it != asks_.end()) { + auto& [_, ask_map] = *ask_map_it; + if (ask_map.size() > 0 + && ask_map.begin()->first >= trade_order.price) { + // FIXME: handle matching if enabled + listener_->on_rejection(trade_order.user, trade_order.id); + return; + } + } + bids_[trade_order.symbol].insert( + {trade_order.price, OrderMetaData{trade_order.user, trade_order.id, + trade_order.quantity}}); + break; + } + + listener_->on_acknowledgement(trade_order.user, trade_order.id); +} + +void Engine::operator()(CancelOrder const& cancel_order) { + // Assume that the input is well-behaved, + // no duplicate (userId, userOrderId) values. + auto const matches_order = [&](auto const& data) { + return data.second.user == cancel_order.user + && data.second.id == cancel_order.id; + }; + + for (auto& [symbol, bid_map] : bids_) { + auto const it = std::ranges::find_if(bid_map, matches_order); + if (it != bid_map.end()) { + // Set-up automatic call-back in case top-of-book changes + auto _ = CallbackOnTopOfBookChange(symbol, *this); + + bid_map.erase(it); + listener_->on_acknowledgement(cancel_order.user, cancel_order.id); + return; + } + } + + for (auto& [symbol, ask_map] : asks_) { + auto const it = std::ranges::find_if(ask_map, matches_order); + if (it != ask_map.end()) { + // Set-up automatic call-back in case top-of-book changes + auto _ = CallbackOnTopOfBookChange(symbol, *this); + + ask_map.erase(it); + listener_->on_acknowledgement(cancel_order.user, cancel_order.id); + return; + } + } + + listener_->on_bad_order(cancel_order.user, cancel_order.id); +} + +void Engine::operator()(FlushOrder const&) { + bids_.clear(); + asks_.clear(); + + listener_->on_flush(); +} + +} // namespace kraken::engine diff --git a/src/engine/engine.hh b/src/engine/engine.hh new file mode 100644 index 0000000..6c84a63 --- /dev/null +++ b/src/engine/engine.hh @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include + +#include "book/order.hh" + +namespace kraken::engine { + +struct CallbackOnTopOfBookChange; +struct EngineListener; + +/// Matching engine which processes orders and keeps the book up-to-date. +struct Engine { + Engine(std::shared_ptr listener); + + /// Process orders, triggerring the listener on each event. + void process_orders(std::vector const& orders); + +private: + void operator()(TradeOrder const& trade_order); + void operator()(CancelOrder const& cancel_order); + void operator()(FlushOrder const& flush_order); + + std::shared_ptr listener_; + + // Symbol, price, side are implicit given the way the book is represented + struct OrderMetaData { + User user; + UserOrderId id; + Quantity quantity; + }; + + friend struct CallbackOnTopOfBookChange; + + // Sorted by price, then by time + template + using instrument_side_data = std::multimap; + + std::map>> bids_; + std::map>> asks_; +}; + +} // namespace kraken::engine