Compare commits

..

9 commits

Author SHA1 Message Date
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
8 changed files with 144 additions and 16 deletions

View file

@ -34,6 +34,8 @@ the standard output. For example
kraken < ./data/inputs/balanced-book-1.in.csv
```
You can use `--enable-trade` to enable trade matching.
## Architecture
### Libraries
@ -79,6 +81,16 @@ 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 in the code.
Related to that point, the matching functionality should be tested further,
rather than just using the two provided examples.
#### Cancelling orders
I do not like the way I have done the `cancel_reverse_info_` mapping: to

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,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

View file

@ -87,8 +87,9 @@ private:
TopInfo top_info_{};
};
Engine::Engine(std::shared_ptr<EngineListener> listener)
: listener_(listener) {}
Engine::Engine(std::shared_ptr<EngineListener> listener,
CrossBehaviour cross_behaviour)
: listener_(listener), cross_behaviour_(cross_behaviour) {}
void Engine::process_orders(std::vector<Order> const& orders) {
for (auto const& order : orders) {
@ -101,6 +102,23 @@ 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:
@ -109,8 +127,16 @@ void Engine::operator()(TradeOrder const& trade_order) {
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);
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;
}
}
@ -124,8 +150,15 @@ void Engine::operator()(TradeOrder const& trade_order) {
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);
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;
}
}

View file

@ -9,12 +9,21 @@
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);
Engine(std::shared_ptr<EngineListener> listener,
CrossBehaviour cross_behaviour = CrossBehaviour::REJECT);
/// Process orders, triggerring the listener on each event.
void process_orders(std::vector<Order> const& orders);
@ -25,6 +34,7 @@ private:
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 {

View file

@ -5,12 +5,21 @@
#include "engine/engine.hh"
#include "parse/parse.hh"
int main() {
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;
}
}
auto const orders = kraken::parse::parse_orders(std::cin);
auto listener = std::make_shared<kraken::engine::CsvEngineListener>();
auto engine = kraken::engine::Engine(listener);
auto engine = kraken::engine::Engine(listener, cross_behaviour);
engine.process_orders(orders);

View file

@ -31,6 +31,19 @@ test_file() {
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}"
@ -38,6 +51,13 @@ for test_name in "$DATA_DIR"/inputs/*.in.csv; do
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

@ -16,20 +16,36 @@ 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 listener = std::make_shared<CsvEngineListener>();
auto engine = Engine{listener};
engine.process_orders(parse_orders(input));
auto expected_output_file = std::ifstream{
CMAKE_SOURCE_DIR + "/data/outputs/"s + test_name + ".out.csv"};
auto expected = read_csv(expected_output_file, CsvHeader::KEEP);
ASSERT_EQ(listener->output(), expected);
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
@ -97,3 +113,11 @@ TEST(engine, 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");
}