Compare commits
9 commits
b163776e69
...
afe78c4d8c
Author | SHA1 | Date | |
---|---|---|---|
Bruno BELANYI | afe78c4d8c | ||
Bruno BELANYI | 4b06f591c6 | ||
Bruno BELANYI | 832abfb224 | ||
Bruno BELANYI | ef027c77f6 | ||
Bruno BELANYI | 1cf45d9125 | ||
Bruno BELANYI | bc20b1ee9a | ||
Bruno BELANYI | ce9457fabd | ||
Bruno BELANYI | 68a7e55238 | ||
Bruno BELANYI | 803452bfa2 |
12
README.md
12
README.md
|
@ -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
|
||||
|
|
10
data/outputs/balanced-book-3.trades.out.csv
Normal file
10
data/outputs/balanced-book-3.trades.out.csv
Normal 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
|
|
10
data/outputs/shallow-ask.trades.out.csv
Normal file
10
data/outputs/shallow-ask.trades.out.csv
Normal 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
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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))"
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue