Compare commits

...

33 commits

Author SHA1 Message Date
Bruno BELANYI f8558238bf kraken: don't use a thread for reading input
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-25 10:47:14 +01:00
Bruno BELANYI 7c2a25a6b0 kraken: make multi-threaded
Some checks reported errors
continuous-integration/drone/push Build was killed
2022-03-24 17:54:37 +01:00
Bruno BELANYI a95df7a00f kraken: book: make 'Order' default constructible 2022-03-24 17:50:36 +01:00
Bruno BELANYI 654be9fc70 kraken: link with 'boost::thread' 2022-03-24 17:50:20 +01:00
Bruno BELANYI 22fe2ad421 nix: add 'boost' dependency 2022-03-24 17:50:01 +01:00
Bruno BELANYI 3998b9839f kraken: refactor to interleave input and output 2022-03-24 17:19:42 +01:00
Bruno BELANYI 8bba52d3b9 kraken: engine: expose 'process_single_order' 2022-03-24 17:19:02 +01:00
Bruno BELANYI e601250e7b kraken: parse: expose 'parse_single_order' 2022-03-24 17:18:23 +01:00
Bruno BELANYI d159cfb877 kraken: engine: non-const 'output' on listener 2022-03-24 17:17:44 +01:00
Bruno BELANYI 8474ed0c69 kraken: csv: expose 'read_csv_line' 2022-03-24 17:17:09 +01:00
Bruno BELANYI 6fc5502b99 doc: talk about improvements to matching
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-13 18:04:18 +01:00
Bruno BELANYI a681671efa data: add invalid scenarios
Those are not supported by my engine, as they have remaining quantities
on a trade match.
2022-03-13 18:04:18 +01:00
Bruno BELANYI ea8c880cca data: add simple tests for matching
Matching is done with asking price, when there is a difference.
2022-03-13 18:04:18 +01:00
Bruno BELANYI 14645524e2 doc: add warning about trade matching
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-12 14:24:33 +01:00
Bruno BELANYI afe78c4d8c doc: mention '--enable-trade'
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-12 14:03:52 +01:00
Bruno BELANYI 4b06f591c6 tests: integration: check when trade enabled 2022-03-12 14:03:06 +01:00
Bruno BELANYI 832abfb224 kraken: add '--enable-trade' 2022-03-12 13:59:44 +01:00
Bruno BELANYI ef027c77f6 doc: adjust README w/ trade matching 2022-03-12 13:57:21 +01:00
Bruno BELANYI 1cf45d9125 kraken: engine: DRY in trade matching 2022-03-12 13:56:08 +01:00
Bruno BELANYI bc20b1ee9a tests: unit: engine: add 'engine_trade' suite 2022-03-12 13:49:20 +01:00
Bruno BELANYI ce9457fabd kraken: engine: add trading on cross behaviour
It is limited, and mostly untested, I would need more explicit semantics
for the border cases to make it more robust.
2022-03-12 13:48:57 +01:00
Bruno BELANYI 68a7e55238 data: add trading data 2022-03-12 13:47:56 +01:00
Bruno BELANYI 803452bfa2 doc: add README 2022-03-12 13:19:15 +01:00
Bruno BELANYI 8534a74c87 kraken: engine: DRY in top-of-book handling 2022-03-12 12:40:23 +01:00
Bruno BELANYI fae4a9d5c7 kraken: engine: refactor 'cancel' ordering
Now that we have a way to reverse-lookup for cancel orders, we are not
linear in the number of active orders.
2022-03-12 12:40:23 +01:00
Bruno BELANYI acbae579b3 kraken: utils: make 'StrongType' copyable 2022-03-12 12:08:27 +01:00
Bruno BELANYI c62f79cbb2 tests: add integration tests
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-12 11:39:39 +01:00
Bruno BELANYI fa563f2f59 kraken: make functional binary 2022-03-12 11:39:39 +01:00
Bruno BELANYI e5ddd16da2 tests: unit: add 'engine' suite 2022-03-12 11:39:39 +01:00
Bruno BELANYI 37b04a678a kraken: engine: fix comparisons 2022-03-12 11:39:39 +01:00
Bruno BELANYI 5f1fbac76c data: add additional test cases
I was too tired last night (this morning...) I inverted a couple comparisons.

Those simple tests helped isolate the issue.
2022-03-12 11:39:39 +01:00
Bruno BELANYI 4515fc1c36 kraken: engine: add 'CsvEngineListern.output'
Otherwise it would be difficult to actually get access to the output...
2022-03-12 11:39:39 +01:00
Bruno BELANYI 0817e7ac7e kraken: engin: add 'Engine'
This is the brains of the operation, the matching engine.
2022-03-12 11:39:39 +01:00
39 changed files with 874 additions and 20 deletions

184
README.md Normal file
View file

@ -0,0 +1,184 @@
# 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
```
You can use `--enable-trade` to enable trade matching. Be careful, it has not
been as carefully tested, a wild `assert` could stop your session short.
## 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
#### Matching trades
The logic used when trade matching is enabled is pretty limited: it wasn't clear
to me what to do when either of the orders have left-over quantities to be
fulfilled. More explicit instructions on this point would lead to the removal of
the final `FIXME`s and `assert`s in the code.
I have added explicit test files of those cases in `data/invalid`.
##### Picking prices
Another improvement that is specific to trade matching would be scripting the
behaviour when bid/ask prices are not exactly equal when matching trades: the
current behaviour is to always use the asking price. One could imagine wanting
to use bid price, or crossing order
price, or the average (weighted by quantity?) of both prices, etc...
#### 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.

View file

@ -0,0 +1,2 @@
N,1,IBM,8,100,S,101
N,2,IBM,10,100,B,102
1 N 1 IBM 8 100 S 101
2 N 2 IBM 10 100 B 102

View file

@ -0,0 +1,2 @@
N,1,IBM,10,100,B,101
N,2,IBM,8,100,S,102
1 N 1 IBM 10 100 B 101
2 N 2 IBM 8 100 S 102

View file

@ -0,0 +1,2 @@
N,1,IBM,10,100,B,101
N,2,IBM,10,100,S,102
1 N 1 IBM 10 100 B 101
2 N 2 IBM 10 100 S 102

View file

@ -0,0 +1,3 @@
N,1,VAL,10,100,B,1
C,1,1
F
1 N,1,VAL,10,100,B,1
2 C,1,1
3 F

View file

@ -0,0 +1,2 @@
N,1,VAL,10,100,B,1
F
1 N,1,VAL,10,100,B,1
2 F

2
data/inputs/two.in.csv Normal file
View file

@ -0,0 +1,2 @@
N,1,VAL,10,100,B,1
N,1,VAL,9,100,B,2
1 N 1 VAL 10 100 B 1
2 N 1 VAL 9 100 B 2

View file

@ -0,0 +1,2 @@
N,1,IBM,8,100,S,101
N,2,IBM,10,50,B,102
1 N 1 IBM 8 100 S 101
2 N 2 IBM 10 50 B 102

View file

@ -0,0 +1,2 @@
N,1,IBM,8,50,S,101
N,2,IBM,10,100,B,102
1 N 1 IBM 8 50 S 101
2 N 2 IBM 10 100 B 102

View file

@ -0,0 +1,10 @@
A,1,1
B,B,10,100
A,1,2
B,S,12,100
A,2,101
A,2,102
B,S,11,100
A,1,103
T,1,103,2,102,11,100
B,S,12,100
1 A,1,1
2 B,B,10,100
3 A,1,2
4 B,S,12,100
5 A,2,101
6 A,2,102
7 B,S,11,100
8 A,1,103
9 T,1,103,2,102,11,100
10 B,S,12,100

View file

@ -0,0 +1,3 @@
A,1,101
B,S,8,100
R,2,102
1 A,1,101
2 B,S,8,100
3 R,2,102

View file

@ -0,0 +1,5 @@
A,1,101
B,S,8,100
A,2,102
T,2,102,1,101,8,100
B,S,-,-
1 A,1,101
2 B,S,8,100
3 A,2,102
4 T,2,102,1,101,8,100
5 B,S,-,-

View file

@ -0,0 +1,3 @@
A,1,101
B,B,10,100
R,2,102
1 A,1,101
2 B,B,10,100
3 R,2,102

View file

@ -0,0 +1,5 @@
A,1,101
B,B,10,100
A,2,102
T,1,101,2,102,8,100
B,B,-,-
1 A,1,101
2 B,B,10,100
3 A,2,102
4 T,1,101,2,102,8,100
5 B,B,-,-

View file

@ -0,0 +1,3 @@
A,1,101
B,B,10,100
R,2,102
1 A,1,101
2 B,B,10,100
3 R,2,102

View file

@ -0,0 +1,5 @@
A,1,101
B,B,10,100
A,2,102
T,1,101,2,102,10,100
B,B,-,-
1 A,1,101
2 B,B,10,100
3 A,2,102
4 T,1,101,2,102,10,100
5 B,B,-,-

View file

@ -0,0 +1,4 @@
A,1,1
B,B,10,100
A,1,1
B,B,-,-
1 A,1,1
2 B,B,10,100
3 A,1,1
4 B,B,-,-

View file

@ -0,0 +1,2 @@
A,1,1
B,B,10,100
1 A,1,1
2 B,B,10,100

View file

@ -0,0 +1,10 @@
A,1,1
B,B,10,100
A,2,101
A,2,102
B,S,11,100
A,1,2
T,1,2,2,102,11,100
B,S,-,-
A,2,103
B,S,11,100
1 A,1,1
2 B,B,10,100
3 A,2,101
4 A,2,102
5 B,S,11,100
6 A,1,2
7 T,1,2,2,102,11,100
8 B,S,-,-
9 A,2,103
10 B,S,11,100

3
data/outputs/two.out.csv Normal file
View file

@ -0,0 +1,3 @@
A,1,1
B,B,10,100
A,1,2
1 A,1,1
2 B,B,10,100
3 A,1,2

View file

@ -43,6 +43,10 @@
pkg-config
];
buildInputs = with final; [
boost
];
checkInputs = with final; [
gtest
];

View file

@ -9,11 +9,13 @@ add_subdirectory(utils)
configure_file(config.h.in config.h)
find_package(Boost REQUIRED COMPONENTS thread)
target_link_libraries(kraken PRIVATE
book
csv
engine
parse
Boost::thread
)
install(TARGETS kraken)

View file

@ -66,6 +66,6 @@ struct FlushOrder {
auto operator<=>(FlushOrder const&) const = default;
};
using Order = std::variant<TradeOrder, CancelOrder, FlushOrder>;
using Order = std::variant<FlushOrder, TradeOrder, CancelOrder>;
} // namespace kraken

View file

@ -4,10 +4,7 @@
namespace kraken::csv {
namespace {
// for convenience, use a stringstream which does not accept string_view inputs
csv_line_type parse_line(std::string const& line) {
csv_line_type read_csv_line(std::string const& line) {
auto parsed = csv_line_type{};
auto input = std::istringstream(line);
@ -18,8 +15,6 @@ csv_line_type parse_line(std::string const& line) {
return parsed;
}
} // namespace
csv_type read_csv(std::istream& input, CsvHeader header) {
auto parsed = std::vector<csv_line_type>{};
@ -30,7 +25,7 @@ csv_type read_csv(std::istream& input, CsvHeader header) {
continue;
}
parsed.emplace_back(parse_line(std::move(line)));
parsed.emplace_back(read_csv_line(std::move(line)));
}
return parsed;

View file

@ -14,6 +14,9 @@ enum class CsvHeader {
KEEP,
};
/// Parse a single CSV line, no error checking.
csv_line_type read_csv_line(std::string const& input);
/// Parse a CSV file from an input-stream, return a vector of parsed lines.
csv_type read_csv(std::istream& input, CsvHeader header = CsvHeader::SKIP);

View file

@ -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
)

View file

@ -6,6 +6,14 @@ CsvEngineListener::CsvEngineListener() = default;
CsvEngineListener::~CsvEngineListener() = default;
csv::csv_type const& CsvEngineListener::output() const {
return output_;
}
csv::csv_type& CsvEngineListener::output() {
return output_;
}
void CsvEngineListener::on_acknowledgement(User user, UserOrderId id) {
output_.emplace_back(csv::csv_line_type{
"A",

View file

@ -13,6 +13,9 @@ struct CsvEngineListener : EngineListener {
virtual ~CsvEngineListener();
csv::csv_type const& output() const;
csv::csv_type& output();
/// Called when a new trade or cancel order has been acknowledged.
void on_acknowledgement(User user, UserOrderId id) override;

241
src/engine/engine.cc Normal file
View file

@ -0,0 +1,241 @@
#include "engine.hh"
#include <algorithm>
#include <utility>
#include <cassert>
#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:
using info_type = std::optional<std::pair<Price, Quantity>>;
struct TopInfo {
info_type asks;
info_type bids;
};
TopInfo calculate_top() const {
auto info = TopInfo{};
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] = order_map.equal_range(price);
for (auto it = begin; it != end; ++it) {
quantity += int(it->second.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;
info.asks = make_top_for(ask_map);
}
return info;
}
Symbol symbol_;
Engine& engine_;
TopInfo top_info_{};
};
Engine::Engine(std::shared_ptr<EngineListener> listener,
CrossBehaviour cross_behaviour)
: listener_(listener), cross_behaviour_(cross_behaviour) {}
void Engine::process_single_order(Order const& order) {
std::visit([this](auto const& trade_order) { (*this)(trade_order); },
order);
}
void Engine::process_orders(std::vector<Order> const& orders) {
for (auto const& order : orders) {
process_single_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);
// FIXME: assumes a single trade for the order
// FIXME: assumes no remaining orders on both sides
auto const do_cross = [this, &trade_order](auto const& bid_order,
auto const& ask_order,
Price matching_price) {
auto const matching_quantity
= std::min(bid_order.quantity, ask_order.quantity);
listener_->on_acknowledgement(trade_order.user, trade_order.id);
listener_->on_match(bid_order.user, bid_order.id, ask_order.user,
ask_order.id, matching_price, matching_quantity);
assert(matching_quantity == bid_order.quantity
&& "multiple matches not implemented");
assert(matching_quantity == ask_order.quantity
&& "multiple matches not implemented");
};
// 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) {
if (cross_behaviour_ == CrossBehaviour::REJECT) {
listener_->on_rejection(trade_order.user, trade_order.id);
} else {
auto matching = bid_map.begin();
auto const& bid_order = matching->second;
// NOTE: Pick the asking price for matching
auto const matching_price = trade_order.price;
do_cross(bid_order, trade_order, matching_price);
bid_map.erase(matching);
}
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) {
if (cross_behaviour_ == CrossBehaviour::REJECT) {
listener_->on_rejection(trade_order.user, trade_order.id);
} else {
auto matching = ask_map.begin();
// NOTE: Pick the asking price for matching
auto const& [matching_price, ask_order] = *matching;
do_cross(trade_order, ask_order, matching_price);
ask_map.erase(matching);
}
return;
}
}
bids_[trade_order.symbol].insert(
{trade_order.price, OrderMetaData{trade_order.user, trade_order.id,
trade_order.quantity}});
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) {
auto const matches_order = [&](auto const& data) {
return data.second.user == cancel_order.user
&& data.second.id == cancel_order.id;
};
if (auto it = cancel_reverse_info_.find(cancel_order);
it != cancel_reverse_info_.end()) {
auto const& [_, info] = *it;
// 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);
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);
}
}
void Engine::operator()(FlushOrder const&) {
bids_.clear();
asks_.clear();
cancel_reverse_info_.clear();
listener_->on_flush();
}
} // namespace kraken::engine

68
src/engine/engine.hh Normal file
View file

@ -0,0 +1,68 @@
#pragma once
#include <functional>
#include <map>
#include <memory>
#include <vector>
#include "book/order.hh"
namespace kraken::engine {
/// Which behaviour on cross orders.
enum class CrossBehaviour {
/// Reject the crossing order.
REJECT,
/// Make the trade with the matching order(s).
MATCH,
};
struct CallbackOnTopOfBookChange;
struct EngineListener;
/// Matching engine which processes orders and keeps the book up-to-date.
struct Engine {
Engine(std::shared_ptr<EngineListener> listener,
CrossBehaviour cross_behaviour = CrossBehaviour::REJECT);
/// Process a single order, triggerring the listener appropriately.
void process_single_order(Order const& order);
/// Process orders, triggerring the listener on each event.
void process_orders(std::vector<Order> const& orders);
private:
void operator()(TradeOrder const& trade_order);
void operator()(CancelOrder const& cancel_order);
void operator()(FlushOrder const& flush_order);
std::shared_ptr<EngineListener> listener_;
CrossBehaviour cross_behaviour_;
// 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 <typename Order>
using instrument_side_data = std::multimap<Price, OrderMetaData, Order>;
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

View file

@ -1,5 +1,55 @@
#include <iostream>
#include <thread>
int main() {
std::cout << "Hello World!\n";
#include <boost/lockfree/spsc_queue.hpp>
#include "csv/read-csv.hh"
#include "csv/write-csv.hh"
#include "engine/csv-engine-listener.hh"
#include "engine/engine.hh"
#include "parse/parse.hh"
int main(int argc, char** argv) {
auto cross_behaviour = kraken::engine::CrossBehaviour::REJECT;
if (argc > 1) {
using namespace std::literals;
if (argv[1] == "--enable-trade"sv) {
cross_behaviour = kraken::engine::CrossBehaviour::MATCH;
}
}
// Up to 512 pending orders.
auto pending_orders = boost::lockfree::spsc_queue<kraken::Order>(512);
auto writer = std::jthread([&](std::stop_token stop_token) {
auto listener = std::make_shared<kraken::engine::CsvEngineListener>();
auto engine = kraken::engine::Engine(listener, cross_behaviour);
while (true) {
auto order = kraken::Order();
while (!pending_orders.pop(order)) {
// FIXME: busy wait
// Check that we didn't miss an order between last 'pop' and
// stop request, just in case.
if (stop_token.stop_requested() && pending_orders.empty()) {
return;
}
}
engine.process_single_order(order);
auto& output = listener->output();
kraken::csv::write_csv(std::cout, output);
output.resize(0);
}
});
for (std::string line; std::getline(std::cin, line);) {
auto const order = kraken::parse::parse_single_order(
kraken::csv::read_csv_line(line));
while (!pending_orders.push(order)) {
// FIXME: busy wait
}
}
// EOF, process orders and bring down
writer.request_stop();
}

View file

@ -57,20 +57,24 @@ CancelOrder cancel_from_raw(csv::csv_line_type const& raw_order) {
} // namespace
Order parse_single_order(csv::csv_line_type const& order) {
if (order[0] == "N") {
return trade_from_raw(order);
} else if (order[0] == "C") {
return cancel_from_raw(order);
} else if (order[0] == "F") {
return FlushOrder{};
} else {
throw ParseError("Not a valid order");
}
}
std::vector<Order> parse_orders(std::istream& input) {
auto const raw_orders = read_csv(input, kraken::csv::CsvHeader::KEEP);
auto orders = std::vector<Order>{};
for (auto const& raw_order : raw_orders) {
if (raw_order[0] == "N") {
orders.emplace_back(trade_from_raw(raw_order));
} else if (raw_order[0] == "C") {
orders.emplace_back(cancel_from_raw(raw_order));
} else if (raw_order[0] == "F") {
orders.emplace_back(FlushOrder{});
} else {
throw ParseError("Not a valid order");
}
orders.emplace_back(parse_single_order(raw_order));
}
return orders;

View file

@ -5,6 +5,7 @@
#include <vector>
#include "book/order.hh"
#include "csv/csv.hh"
namespace kraken::parse {
@ -12,6 +13,10 @@ struct ParseError : public std::runtime_error {
using std::runtime_error::runtime_error;
};
/// Convert a single CSV input line to an `Order`.
Order parse_single_order(csv::csv_line_type const& order);
/// Parse orders from an input stream as CSV.
std::vector<Order> parse_orders(std::istream& input);
} // namespace kraken::parse

View file

@ -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_;
}

View file

@ -1 +1,2 @@
add_subdirectory(integration)
add_subdirectory(unit)

View file

@ -0,0 +1,13 @@
find_program(BASH_PROGRAM bash)
if (BASH_PROGRAM)
add_test(test_output
${BASH_PROGRAM}
${CMAKE_CURRENT_SOURCE_DIR}/test-output.sh
${CMAKE_SOURCE_DIR}/data/
# FIXME: can't get it working $<TARGET_FILE:kraken>
${CMAKE_BINARY_DIR}/src/kraken
)
endif (BASH_PROGRAM)

View file

@ -0,0 +1,63 @@
#!/usr/bin/env bash
if [ $# != 0 ]; then
DATA_DIR="$1"
KRAKEN="$2"
else
DATA_DIR=data/
KRAKEN=build/src/kraken
fi
if ! [ -x "$KRAKEN" ] || ! [ -d "$DATA_DIR" ]; then
printf 'KRAKEN ('\''%s'\'') or DATA_DIR ('\''%s'\'') incorrectly set\n' "$KRAKEN" "$DATA_DIR"
exit 1
fi
FAILURES=0
SUCCESSES=0
# $1: should be the name of the input/output files couple, stripped of
# the data path prefix, or the `{in,out}.csv` suffix.
test_file() {
if ! diff="$(diff <("$KRAKEN" < "$DATA_DIR/inputs/$1.in.csv") "$DATA_DIR/outputs/$1.out.csv")"; then
((FAILURES += 1))
echo "$1: FAIL"
if [ -n "$VERBOSE" ]; then
printf '%s\n' "$diff"
fi
else
((SUCCESSES += 1))
echo "$1: OK"
fi
}
test_file_trades() {
if ! diff="$(diff <("$KRAKEN" --enable-trade < "$DATA_DIR/inputs/$1.in.csv") "$DATA_DIR/outputs/$1.trades.out.csv")"; then
((FAILURES += 1))
echo "$1 (trades): FAIL"
if [ -n "$VERBOSE" ]; then
printf '%s\n' "$diff"
fi
else
((SUCCESSES += 1))
echo "$1 (trades): OK"
fi
}
for test_name in "$DATA_DIR"/inputs/*.in.csv; do
test_name="$(basename "$test_name")"
test_name="${test_name%%.in.csv}"
test_file "$test_name"
done
for test_name in "$DATA_DIR"/outputs/*.trades.out.csv; do
test_name="$(basename "$test_name")"
test_name="${test_name%%.trades.out.csv}"
test_file_trades "$test_name"
done
printf '\nSummary: %d successes, %d failures\n' "$SUCCESSES" "$FAILURES"
exit "$((FAILURES != 0))"

View file

@ -25,4 +25,16 @@ target_link_libraries(parse_test PRIVATE
gtest_discover_tests(parse_test)
add_executable(engine_test engine.cc)
target_link_libraries(engine_test PRIVATE common_options)
target_link_libraries(engine_test PRIVATE
engine
parse
GTest::gtest
GTest::gtest_main
)
gtest_discover_tests(engine_test)
endif (${GTest_FOUND})

123
tests/unit/engine.cc Normal file
View file

@ -0,0 +1,123 @@
#include <fstream>
#include <gtest/gtest.h>
#include "config.h"
#include "csv/read-csv.hh"
#include "engine/csv-engine-listener.hh"
#include "engine/engine.hh"
#include "parse/parse.hh"
// Allow namespace pollution in tests for convenience
using namespace kraken;
using namespace kraken::csv;
using namespace kraken::engine;
using namespace kraken::parse;
namespace {
void do_compare(std::istream& input, std::istream& expected_output,
CrossBehaviour behaviour) {
auto listener = std::make_shared<CsvEngineListener>();
auto engine = Engine{listener, behaviour};
engine.process_orders(parse_orders(input));
auto expected = read_csv(expected_output, CsvHeader::KEEP);
ASSERT_EQ(listener->output(), expected);
}
void compare(std::string const& test_name) {
using namespace std::literals;
auto input = std::ifstream{CMAKE_SOURCE_DIR + "/data/inputs/"s + test_name
+ ".in.csv"};
auto expected_output_file = std::ifstream{
CMAKE_SOURCE_DIR + "/data/outputs/"s + test_name + ".out.csv"};
do_compare(input, expected_output_file, CrossBehaviour::REJECT);
}
void compare_trades(std::string const& test_name) {
using namespace std::literals;
auto input = std::ifstream{CMAKE_SOURCE_DIR + "/data/inputs/"s + test_name
+ ".in.csv"};
auto expected_output_file = std::ifstream{
CMAKE_SOURCE_DIR + "/data/outputs/"s + test_name + ".trades.out.csv"};
do_compare(input, expected_output_file, CrossBehaviour::MATCH);
}
} // namespace
TEST(engine, empty) {
compare("empty");
}
TEST(engine, only_one) {
compare("only-one");
}
TEST(engine, only_one_cancelled) {
compare("only-one-cancelled");
}
TEST(engine, two) {
compare("two");
}
TEST(engine, balanced_book_1) {
compare("balanced-book-1");
}
TEST(engine, balanced_book_2) {
compare("balanced-book-2");
}
TEST(engine, balanced_book_3) {
compare("balanced-book-3");
}
TEST(engine, balanced_book_4) {
compare("balanced-book-4");
}
TEST(engine, balanced_book_5) {
compare("balanced-book-5");
}
TEST(engine, balanced_book_6) {
compare("balanced-book-6");
}
TEST(engine, balanced_book_7) {
compare("balanced-book-7");
}
TEST(engine, balanced_book_8) {
compare("balanced-book-8");
}
TEST(engine, balanced_book_9) {
compare("balanced-book-9");
}
TEST(engine, shallow_ask) {
compare("shallow-ask");
}
TEST(engine, shallow_bid) {
compare("shallow-bid");
}
TEST(engine, tighten_spread_through_new_limit_orders) {
compare("tighten-spread-through-new-limit-orders");
}
TEST(engine_trade, balanced_book_3) {
compare_trades("balanced-book-3");
}
TEST(engine_trade, shallow_ask) {
compare_trades("shallow-ask");
}