Compare commits
4 commits
c62f79cbb2
...
b163776e69
Author | SHA1 | Date | |
---|---|---|---|
Bruno BELANYI | b163776e69 | ||
Bruno BELANYI | 8534a74c87 | ||
Bruno BELANYI | fae4a9d5c7 | ||
Bruno BELANYI | acbae579b3 |
163
README.md
Normal file
163
README.md
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
# Kraken technical assessment: matching engine
|
||||||
|
|
||||||
|
## How to
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
This project was written on a Linux (NixOS) machine, using the `CMake` build
|
||||||
|
system.
|
||||||
|
|
||||||
|
To build the project you should run the following commands:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir build
|
||||||
|
cmake -B build
|
||||||
|
cmake --build build
|
||||||
|
```
|
||||||
|
|
||||||
|
To run unit and integration tests you can use:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake --build build --target test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
The `kraken` binary should be built in `build/src/kraken`. You can see example
|
||||||
|
inputs and their matching outputs in the `data/` directory at the root of the
|
||||||
|
repository.
|
||||||
|
|
||||||
|
`kraken` reads its input from standard input, and displays its results on
|
||||||
|
the standard output. For example
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kraken < ./data/inputs/balanced-book-1.in.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Libraries
|
||||||
|
|
||||||
|
The project is divided into small libraries in separate directories, each for
|
||||||
|
a specific purpose:
|
||||||
|
|
||||||
|
* `book`: defines the vocabulary types to quantify orders.
|
||||||
|
* `csv`: reading and writing CSV files, in a very naive way.
|
||||||
|
* `engine`: the matching engine proper, and a listener interface used to create
|
||||||
|
the expected output.
|
||||||
|
* `parse`: parsing the raw CSV data into a list of orders.
|
||||||
|
* `utils`: utility types, specifically a `StrongType` wrapper to ensure a `User`
|
||||||
|
is not mistaken for a `Quantity`.
|
||||||
|
|
||||||
|
### A KISS architecture
|
||||||
|
|
||||||
|
In each step of the project, the code was kept to its simplest, trying to solve
|
||||||
|
the problem at hand in the simplest way possible, while keeping to the single
|
||||||
|
responsibility principle. This is why for example:
|
||||||
|
|
||||||
|
* The input is parsed at once, and processed in a single step, by different
|
||||||
|
components.
|
||||||
|
* Almost no efforts were made to avoid superfluous copies, or such
|
||||||
|
optimizations.
|
||||||
|
* The engine and the expected output are separated from each other through a
|
||||||
|
listener interface, instead of entangling both in the same class.
|
||||||
|
|
||||||
|
### A test harness
|
||||||
|
|
||||||
|
To allow for refactoring without fear, each library has a test-suite to ensure
|
||||||
|
it is in working order.
|
||||||
|
|
||||||
|
This allowed me to simplify cancelling orders from having to potentially look
|
||||||
|
at *all currently active orders* to just a few orders on a given price level.
|
||||||
|
|
||||||
|
### Reasonably extensible
|
||||||
|
|
||||||
|
Given the focus on "events" (the engine processes each order separately, calling
|
||||||
|
the listener at opportune times), it should be fairly simple to extend the core
|
||||||
|
of this code to allow for online processing (i.e: the engine reads its input and
|
||||||
|
displays its output as it comes along), etc...
|
||||||
|
|
||||||
|
### What I would improve
|
||||||
|
|
||||||
|
#### Cancelling orders
|
||||||
|
|
||||||
|
I do not like the way I have done the `cancel_reverse_info_` mapping: to
|
||||||
|
simplify I use a `CancelOrder` value as a key instead of creating an
|
||||||
|
`Engine`-specific type.
|
||||||
|
|
||||||
|
#### Repetition
|
||||||
|
|
||||||
|
I do not like the repetition that happens due to `asks_` and `bids_` being
|
||||||
|
"mirror" of each other in the way they should be handled. This is mitigated
|
||||||
|
somewhat by making use of helper lambda functions when the code is identical.
|
||||||
|
|
||||||
|
#### Top of the book handling
|
||||||
|
|
||||||
|
I feel like the `CallbackOnTopOfBookChange` is kind of a hack, even though it
|
||||||
|
was very effective to solve the problem I had of:
|
||||||
|
|
||||||
|
* I want to memorize the current top of the book
|
||||||
|
* At the very end of my current operation, calculate the top of the book again
|
||||||
|
* Then call the listener back to notify of a change if it happened.
|
||||||
|
|
||||||
|
I think there might be a smarter way to go about this. To (mis-)quote Blaise
|
||||||
|
Pascal:
|
||||||
|
|
||||||
|
> If I had more time, I would have made it shorter.
|
||||||
|
|
||||||
|
### Complexity analysis
|
||||||
|
|
||||||
|
This will focus on the matching engine code, let's discard the complexity of
|
||||||
|
the input pre-processing from this discussion.
|
||||||
|
|
||||||
|
Given the use of `std::{multi_,}map` types, the space analysis is pretty simple:
|
||||||
|
linear with respect to the number of active orders. Empty books are not removed
|
||||||
|
and would therefore also consume a small amount of space: I am not accounting
|
||||||
|
for this in this analysis.
|
||||||
|
|
||||||
|
Let's focus on the time complexity.
|
||||||
|
|
||||||
|
#### Flush orders
|
||||||
|
|
||||||
|
The simplest to process, we just empty all the book information, in a time
|
||||||
|
complexity linear in the number of active orders in the book.
|
||||||
|
|
||||||
|
#### Cancel order
|
||||||
|
|
||||||
|
The first version of this code would have a worst-case cost linear in the
|
||||||
|
number of active orders in the book, simply iterating through each one in turn.
|
||||||
|
|
||||||
|
Thanks to a reverse-mapping, the cancel cost is now the following:
|
||||||
|
|
||||||
|
* Lookup in `cancel_reverse_info_`: logarithmic with respect to the number of
|
||||||
|
active orders across all instruments.
|
||||||
|
* Lookup in `bids_`/`asks_` for the book on a given `Symbol`: logarithmic with
|
||||||
|
respect to the number of symbols.
|
||||||
|
* Finding the bounds on the price range: logarithmic with respect to the number
|
||||||
|
of orders in the given book.
|
||||||
|
* Iterating through that range: linear with respect to the number of orders
|
||||||
|
at the given price range.
|
||||||
|
|
||||||
|
#### Trade order
|
||||||
|
|
||||||
|
* Lookup on `bids_` and `asks_` for the given symbol: logarithmic to the number
|
||||||
|
of symbols.
|
||||||
|
* Look for a cross of the book, ensure the book is not empty (constant time),
|
||||||
|
and look at the first value in the book: logarithmic to the number of orders
|
||||||
|
in the book
|
||||||
|
* Inserting the order in the book: logarithmic to the number of orders in the
|
||||||
|
book.
|
||||||
|
* Inserting into `cancel_reverse_info_` (for faster cancelling): logarithmic
|
||||||
|
to the number of orders across all instruments.
|
||||||
|
|
||||||
|
#### Top-of-book handling
|
||||||
|
|
||||||
|
For both trade orders and cancel orders, the `CallbackOnTopOfBookChange` does
|
||||||
|
the following:
|
||||||
|
|
||||||
|
* Lookup on `bids_` and `asks_` for the given symbol: logarithmic to the number
|
||||||
|
of symbols.
|
||||||
|
* Check the size of the book (constant time) and look at the first order's
|
||||||
|
price: logarithmic to the number of orders in the book.
|
||||||
|
* Find the price range: logarithmic yet again.
|
||||||
|
* Iterating on the range: linear to the number of orders at the given price.
|
|
@ -41,46 +41,42 @@ struct CallbackOnTopOfBookChange {
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
using info_type = std::optional<std::pair<Price, Quantity>>;
|
||||||
|
|
||||||
struct TopInfo {
|
struct TopInfo {
|
||||||
std::optional<std::pair<Price, Quantity>> asks;
|
info_type asks;
|
||||||
std::optional<std::pair<Price, Quantity>> bids;
|
info_type bids;
|
||||||
};
|
};
|
||||||
|
|
||||||
TopInfo calculate_top() const {
|
TopInfo calculate_top() const {
|
||||||
auto info = TopInfo{};
|
auto info = TopInfo{};
|
||||||
|
|
||||||
if (auto const bid_it = engine_.bids_.find(symbol_);
|
auto const make_top_for = [](auto const& order_map) -> info_type {
|
||||||
bid_it != engine_.bids_.end()) {
|
if (order_map.size() > 0) {
|
||||||
auto const& [_, bid_map] = *bid_it;
|
auto const price = order_map.begin()->first;
|
||||||
|
|
||||||
if (bid_map.size() > 0) {
|
|
||||||
auto const price = bid_map.begin()->first;
|
|
||||||
int quantity = 0;
|
int quantity = 0;
|
||||||
auto const [begin, end] = bid_map.equal_range(price);
|
auto const [begin, end] = order_map.equal_range(price);
|
||||||
|
|
||||||
for (auto it = begin; it != end; ++it) {
|
for (auto it = begin; it != end; ++it) {
|
||||||
quantity += int(it->second.quantity);
|
quantity += int(it->second.quantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
info.bids = {price, Quantity(quantity)};
|
return std::make_pair(price, Quantity(quantity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (auto const bid_it = engine_.bids_.find(symbol_);
|
||||||
|
bid_it != engine_.bids_.end()) {
|
||||||
|
auto const& [_, bid_map] = *bid_it;
|
||||||
|
info.bids = make_top_for(bid_map);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auto const ask_it = engine_.asks_.find(symbol_);
|
if (auto const ask_it = engine_.asks_.find(symbol_);
|
||||||
ask_it != engine_.asks_.end()) {
|
ask_it != engine_.asks_.end()) {
|
||||||
auto const& [_, ask_map] = *ask_it;
|
auto const& [_, ask_map] = *ask_it;
|
||||||
|
info.asks = make_top_for(ask_map);
|
||||||
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;
|
return info;
|
||||||
|
@ -139,47 +135,68 @@ void Engine::operator()(TradeOrder const& trade_order) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record reverse-lookup info for faster cancelling
|
||||||
|
cancel_reverse_info_.insert({
|
||||||
|
{
|
||||||
|
trade_order.user,
|
||||||
|
trade_order.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trade_order.side,
|
||||||
|
trade_order.symbol,
|
||||||
|
trade_order.price,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
listener_->on_acknowledgement(trade_order.user, trade_order.id);
|
listener_->on_acknowledgement(trade_order.user, trade_order.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Engine::operator()(CancelOrder const& cancel_order) {
|
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) {
|
auto const matches_order = [&](auto const& data) {
|
||||||
return data.second.user == cancel_order.user
|
return data.second.user == cancel_order.user
|
||||||
&& data.second.id == cancel_order.id;
|
&& data.second.id == cancel_order.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (auto& [symbol, bid_map] : bids_) {
|
if (auto it = cancel_reverse_info_.find(cancel_order);
|
||||||
auto const it = std::ranges::find_if(bid_map, matches_order);
|
it != cancel_reverse_info_.end()) {
|
||||||
if (it != bid_map.end()) {
|
auto const& [_, info] = *it;
|
||||||
// Set-up automatic call-back in case top-of-book changes
|
|
||||||
auto _ = CallbackOnTopOfBookChange(symbol, *this);
|
|
||||||
|
|
||||||
bid_map.erase(it);
|
// Set-up automatic call-back in case top-of-book changes
|
||||||
listener_->on_acknowledgement(cancel_order.user, cancel_order.id);
|
auto __ = CallbackOnTopOfBookChange(info.symbol, *this);
|
||||||
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);
|
|
||||||
|
|
||||||
|
switch (info.side) {
|
||||||
|
case Side::ASK: {
|
||||||
|
auto& ask_map = asks_[info.symbol];
|
||||||
|
auto const [begin, end] = ask_map.equal_range(info.price);
|
||||||
|
auto it = std::find_if(begin, end, matches_order);
|
||||||
|
if (it == end) {
|
||||||
|
listener_->on_bad_order(cancel_order.user, cancel_order.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
ask_map.erase(it);
|
ask_map.erase(it);
|
||||||
listener_->on_acknowledgement(cancel_order.user, cancel_order.id);
|
break;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
case Side::BID: {
|
||||||
|
auto& bid_map = bids_[info.symbol];
|
||||||
|
auto const [begin, end] = bid_map.equal_range(info.price);
|
||||||
|
auto it = std::find_if(begin, end, matches_order);
|
||||||
|
if (it == end) {
|
||||||
|
listener_->on_bad_order(cancel_order.user, cancel_order.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bid_map.erase(it);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cancel_reverse_info_.erase(it);
|
||||||
|
listener_->on_acknowledgement(cancel_order.user, cancel_order.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
listener_->on_bad_order(cancel_order.user, cancel_order.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Engine::operator()(FlushOrder const&) {
|
void Engine::operator()(FlushOrder const&) {
|
||||||
bids_.clear();
|
bids_.clear();
|
||||||
asks_.clear();
|
asks_.clear();
|
||||||
|
cancel_reverse_info_.clear();
|
||||||
|
|
||||||
listener_->on_flush();
|
listener_->on_flush();
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,15 @@ private:
|
||||||
|
|
||||||
std::map<Symbol, instrument_side_data<std::greater<void>>> bids_;
|
std::map<Symbol, instrument_side_data<std::greater<void>>> bids_;
|
||||||
std::map<Symbol, instrument_side_data<std::less<void>>> asks_;
|
std::map<Symbol, instrument_side_data<std::less<void>>> asks_;
|
||||||
|
|
||||||
|
// Map cancel information to allow faster lookups
|
||||||
|
struct CancelReverseInfo {
|
||||||
|
Side side;
|
||||||
|
Symbol symbol;
|
||||||
|
Price price;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::map<CancelOrder, CancelReverseInfo> cancel_reverse_info_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace kraken::engine
|
} // namespace kraken::engine
|
||||||
|
|
|
@ -7,6 +7,9 @@ template <typename T, typename Tag>
|
||||||
struct StrongType {
|
struct StrongType {
|
||||||
explicit StrongType(T value) : value_(value) {}
|
explicit StrongType(T value) : value_(value) {}
|
||||||
|
|
||||||
|
StrongType(StrongType const&) = default;
|
||||||
|
StrongType& operator=(StrongType const&) = default;
|
||||||
|
|
||||||
explicit operator T() const {
|
explicit operator T() const {
|
||||||
return value_;
|
return value_;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue