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:
|
||||
using info_type = std::optional<std::pair<Price, Quantity>>;
|
||||
|
||||
struct TopInfo {
|
||||
std::optional<std::pair<Price, Quantity>> asks;
|
||||
std::optional<std::pair<Price, Quantity>> bids;
|
||||
info_type asks;
|
||||
info_type 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;
|
||||
auto const make_top_for = [](auto const& order_map) -> info_type {
|
||||
if (order_map.size() > 0) {
|
||||
auto const price = order_map.begin()->first;
|
||||
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) {
|
||||
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_);
|
||||
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)};
|
||||
}
|
||||
info.asks = make_top_for(ask_map);
|
||||
}
|
||||
|
||||
return info;
|
||||
|
@ -139,47 +135,68 @@ void Engine::operator()(TradeOrder const& trade_order) {
|
|||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
if (auto it = cancel_reverse_info_.find(cancel_order);
|
||||
it != cancel_reverse_info_.end()) {
|
||||
auto const& [_, info] = *it;
|
||||
|
||||
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);
|
||||
// Set-up automatic call-back in case top-of-book changes
|
||||
auto __ = CallbackOnTopOfBookChange(info.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);
|
||||
listener_->on_acknowledgement(cancel_order.user, cancel_order.id);
|
||||
return;
|
||||
break;
|
||||
}
|
||||
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&) {
|
||||
bids_.clear();
|
||||
asks_.clear();
|
||||
cancel_reverse_info_.clear();
|
||||
|
||||
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::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
|
||||
|
|
|
@ -7,6 +7,9 @@ template <typename T, typename Tag>
|
|||
struct StrongType {
|
||||
explicit StrongType(T value) : value_(value) {}
|
||||
|
||||
StrongType(StrongType const&) = default;
|
||||
StrongType& operator=(StrongType const&) = default;
|
||||
|
||||
explicit operator T() const {
|
||||
return value_;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue