Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e08bad534 | ||
|
|
09573167b8 | ||
|
|
caa03dcfa7 | ||
|
|
d010ba7ae7 | ||
|
|
c60df8bcfa | ||
|
|
634e69f72b | ||
|
|
2397a15b23 | ||
|
|
dfacf73289 | ||
|
|
0339fe9749 | ||
|
|
ff1d71a36d | ||
|
|
5b9fc54ea6 | ||
|
|
24f0eef2eb | ||
|
|
01e90c2497 | ||
|
|
a62b4af667 | ||
|
|
3aebd25838 |
No files matched your search
@@ -35,6 +35,9 @@ PB_ENUM_TO_JSON(CustomerOrFirm)
|
||||
void to_json(nlohmann::json &j, const RoutingRecord &r);
|
||||
void from_json(const nlohmann::json &j, RoutingRecord &r);
|
||||
|
||||
PB_ENUM_FROM_JSON(NativeOrderStatus)
|
||||
PB_ENUM_TO_JSON(NativeOrderStatus)
|
||||
|
||||
PB_ENUM_FROM_JSON(NativeOrderState_SubError)
|
||||
PB_ENUM_TO_JSON(NativeOrderState_SubError)
|
||||
|
||||
@@ -67,6 +70,12 @@ FMT_IMPL(
|
||||
"{}",
|
||||
bts::api::orderman::SelfMatchPreventionInstruction_Name(s));
|
||||
|
||||
FMT_IMPL(
|
||||
bts::api::orderman::NativeOrderStatus,
|
||||
s,
|
||||
"{}",
|
||||
bts::api::orderman::NativeOrderStatus_Name(s));
|
||||
|
||||
FMT_IMPL(
|
||||
bts::api::orderman::NativeOrderState_SubError,
|
||||
s,
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/utils/LockBox.h"
|
||||
#include "lib/utils/bts_assert.h"
|
||||
#include "lib/utils/type_name.h"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace bts::collections {
|
||||
|
||||
template <class KeyT, class ValueT>
|
||||
class ConcurrentFactory {
|
||||
utils::LockBox<std::unordered_map<KeyT, std::shared_ptr<ValueT>>> store;
|
||||
|
||||
public:
|
||||
std::shared_ptr<ValueT> add(
|
||||
const KeyT &key,
|
||||
std::function<void(std::shared_ptr<ValueT> &)> callback) {
|
||||
return store.lock([&](auto &map) {
|
||||
auto [it, is_new] = map.try_emplace(key, std::make_shared<ValueT>());
|
||||
bts_assert_msgf(
|
||||
is_new, "{}::add. Duplicate key '{}'",
|
||||
utils::cached_type_name<ConcurrentFactory<KeyT, ValueT>>(), key);
|
||||
callback(it->second);
|
||||
return it->second;
|
||||
});
|
||||
}
|
||||
|
||||
std::size_t erase(const KeyT &key) {
|
||||
return store.lock([&](auto &map) {
|
||||
map.erase(key);
|
||||
return map.size();
|
||||
});
|
||||
}
|
||||
|
||||
std::size_t size() const {
|
||||
return store.lock([&](auto &map) { return map.size(); });
|
||||
}
|
||||
|
||||
template <
|
||||
typename CallbackT = std::function<void(std::shared_ptr<ValueT> &)>>
|
||||
std::shared_ptr<const ValueT> get(const KeyT &key, CallbackT callback) {
|
||||
return store.lock([&](auto &map) {
|
||||
auto [it, is_new] = map.try_emplace(key, std::make_shared<ValueT>());
|
||||
bts_assert_msgf(
|
||||
!is_new, "{}::get. Key not found '{}'",
|
||||
utils::cached_type_name<ConcurrentFactory<KeyT, ValueT>>(), key);
|
||||
callback(it->second);
|
||||
return it->second;
|
||||
});
|
||||
}
|
||||
|
||||
template <
|
||||
typename CallbackT =
|
||||
std::function<void(std::shared_ptr<ValueT> &, bool)>>
|
||||
std::shared_ptr<const ValueT> get_or_add(
|
||||
const KeyT &key, CallbackT callback) {
|
||||
return store.lock([&](auto &map) {
|
||||
auto [it, is_new] = map.try_emplace(key);
|
||||
if (is_new) {
|
||||
it->second = std::make_shared<ValueT>();
|
||||
}
|
||||
callback(it->second, is_new);
|
||||
return it->second;
|
||||
});
|
||||
}
|
||||
|
||||
template <
|
||||
typename CallbackT =
|
||||
std::function<void(std::shared_ptr<ValueT> &, bool)>>
|
||||
std::shared_ptr<const ValueT> replace(const KeyT &key, CallbackT callback) {
|
||||
return store.lock([&](auto &map) {
|
||||
auto [it, is_new] = map.try_emplace(key);
|
||||
bts_assert_msgf(
|
||||
!is_new, "{}::replace. Key not found '{}'",
|
||||
utils::cached_type_name<ConcurrentFactory<KeyT, ValueT>>(), key);
|
||||
it->second = std::make_shared<ValueT>();
|
||||
callback(it->second, is_new);
|
||||
return it->second;
|
||||
});
|
||||
}
|
||||
|
||||
template <
|
||||
typename CallbackT =
|
||||
std::function<void(std::shared_ptr<ValueT> &, bool)>>
|
||||
std::shared_ptr<const ValueT> replace_or_add(
|
||||
const KeyT &key, CallbackT callback) {
|
||||
return store.lock([&](auto &map) {
|
||||
auto [it, is_new] = map.try_emplace(key);
|
||||
it->second = std::make_shared<ValueT>();
|
||||
callback(it->second, is_new);
|
||||
return it->second;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace bts::collections
|
||||
@@ -120,7 +120,7 @@ void HttpClient::send(const HttpRequest &req, IHttpResponse *response) {
|
||||
// Disable signals to avoid strange stack corruption related to set/longjmp
|
||||
// https://stackoverflow.com/questions/9191668/error-longjmp-causes-uninitialized-stack-frame
|
||||
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_FAILONERROR, true);
|
||||
curl_easy_setopt(curl, CURLOPT_FAILONERROR, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_URL, req.url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, curl_headers.data());
|
||||
if (response != nullptr) {
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
#include "lib/utils/bts_assert.h"
|
||||
#include "lib/utils/formatters/exception_formatter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <chrono>
|
||||
#include <exception>
|
||||
#include <fmt/format.h>
|
||||
#include <mutex>
|
||||
@@ -19,6 +22,7 @@
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
#include <websocketpp/close.hpp>
|
||||
#include <websocketpp/common/connection_hdl.hpp>
|
||||
@@ -34,6 +38,7 @@ namespace bts::websocket {
|
||||
struct WebsocketClientSnapshot {
|
||||
int client_id;
|
||||
WebsocketClientState state;
|
||||
int n_reconnects;
|
||||
int version;
|
||||
};
|
||||
|
||||
@@ -42,9 +47,10 @@ struct WebsocketClientSnapshot {
|
||||
FMT_IMPL(
|
||||
bts::websocket::WebsocketClientSnapshot,
|
||||
t,
|
||||
"id: {}; state: {}; version: {}",
|
||||
"id: {}; state: {}; n_reconnects: {}; version: {}",
|
||||
t.client_id,
|
||||
t.state,
|
||||
t.n_reconnects,
|
||||
t.version);
|
||||
|
||||
namespace bts::websocket {
|
||||
@@ -62,6 +68,8 @@ using std::size_t;
|
||||
using std::string;
|
||||
using std::thread;
|
||||
using std::uint32_t;
|
||||
using std::chrono::nanoseconds;
|
||||
using std::this_thread::sleep_for;
|
||||
using websocketpp::config::asio_tls_client;
|
||||
|
||||
using WebsocketppClient =
|
||||
@@ -86,9 +94,16 @@ using State = WebsocketClientState;
|
||||
#define ERROR(...) LOG(Http, Error, __VA_ARGS__)
|
||||
#define ERROR_FN(...) LOG(Http, Error, "WebsocketClient::" __VA_ARGS__)
|
||||
|
||||
static bts::core::TimeSpan reconnect_interval(std::uint32_t n_attempts) {
|
||||
std::int64_t secs = 5 * n_attempts;
|
||||
secs = std::clamp(secs, 5L, 300L);
|
||||
return bts::core::TimeSpan::from_seconds(secs);
|
||||
}
|
||||
|
||||
struct WebsocketClient::PImpl {
|
||||
WebsocketClientConfig config;
|
||||
WebsocketppClient client{};
|
||||
boost::asio::io_context *io_context = nullptr;
|
||||
WebsocketppClient::connection_ptr con;
|
||||
optional<OpenHandler> open_handler;
|
||||
optional<MessageHandler> message_handler;
|
||||
@@ -98,8 +113,45 @@ struct WebsocketClient::PImpl {
|
||||
optional<thread> io_thread;
|
||||
int client_id{client_id_counter++};
|
||||
mutex instance_mx;
|
||||
State state;
|
||||
State state = State::IDLE;
|
||||
int version = 0;
|
||||
int n_reconnects = 0;
|
||||
|
||||
PImpl() {
|
||||
client.set_access_channels(websocketpp::log::alevel::all);
|
||||
// client.clear_access_channels(websocketpp::log::alevel::frame_payload);
|
||||
client.set_error_channels(websocketpp::log::elevel::all);
|
||||
if (io_context == nullptr) {
|
||||
client.init_asio();
|
||||
} else {
|
||||
client.init_asio(io_context);
|
||||
}
|
||||
client.set_message_handler(
|
||||
[this](auto hdl, auto msg) { on_message(hdl, msg); });
|
||||
client.set_open_handler([this](auto hdl) { on_open(hdl); });
|
||||
client.set_ping_handler([](auto /*hdl*/, auto /*msg*/) {
|
||||
LOG(Http, Info, "Received ping");
|
||||
return true;
|
||||
});
|
||||
client.set_pong_timeout(0);
|
||||
client.set_pong_handler([](auto /*hdl*/, auto /*msg*/) {
|
||||
LOG(Http, Info, "Received pong");
|
||||
return true;
|
||||
});
|
||||
client.set_tls_init_handler([&](auto hdl) { return on_tls_init(hdl); });
|
||||
client.set_close_handler([=, this](auto hdl) {
|
||||
on_close(hdl);
|
||||
if (config.reconnect_on_close) {
|
||||
sleep_for(nanoseconds{reconnect_interval(n_reconnects).nanos()});
|
||||
connect();
|
||||
}
|
||||
});
|
||||
client.set_fail_handler([this](auto hdl) {
|
||||
const auto ec = client.get_con_from_hdl(hdl)->get_ec();
|
||||
ERROR_FN("fail_handler. {}; err: {}", snapshot(), ec.message());
|
||||
on_fail(hdl);
|
||||
});
|
||||
}
|
||||
|
||||
~PImpl() {
|
||||
if (io_thread) {
|
||||
@@ -120,7 +172,7 @@ struct WebsocketClient::PImpl {
|
||||
}
|
||||
|
||||
constexpr WebsocketClientSnapshot snapshot() const {
|
||||
return {client_id, state, version};
|
||||
return {client_id, state, n_reconnects, version};
|
||||
}
|
||||
|
||||
void close(websocketpp::close::status::value code, std::string reason) {
|
||||
@@ -164,8 +216,7 @@ struct WebsocketClient::PImpl {
|
||||
/**
|
||||
* Connects to the websocket server
|
||||
*/
|
||||
void connect(
|
||||
const map<string, string> &headers, bool run_in_background = true) {
|
||||
void connect() {
|
||||
lock_guard<mutex> lock{instance_mx};
|
||||
INFO_FN("connect. {}", snapshot());
|
||||
switch (state.value) {
|
||||
@@ -177,9 +228,6 @@ struct WebsocketClient::PImpl {
|
||||
}
|
||||
set_state(State::OPENING);
|
||||
try {
|
||||
init_logging();
|
||||
init_handlers();
|
||||
// Get connection
|
||||
websocketpp::lib::error_code ec;
|
||||
con = client.get_connection(config.url, ec);
|
||||
if (ec) {
|
||||
@@ -187,20 +235,11 @@ struct WebsocketClient::PImpl {
|
||||
return;
|
||||
}
|
||||
// Add headers
|
||||
for (const auto &[k, v] : headers) {
|
||||
for (const auto &[k, v] : config.headers) {
|
||||
con->append_header(k, v);
|
||||
}
|
||||
// Connect
|
||||
client.connect(con);
|
||||
if (run_in_background) {
|
||||
bts_assert_msgf(
|
||||
!io_thread, "Websocket io_thread already instantiated. {}",
|
||||
snapshot());
|
||||
io_thread = std::thread{[this]() { run(); }};
|
||||
if (config.connect_timeout > core::TimeSpan::zero()) {
|
||||
wait_for_connect(config.connect_timeout);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception &ex) {
|
||||
std::string str = ex.what();
|
||||
ERROR_FN(
|
||||
@@ -212,22 +251,11 @@ struct WebsocketClient::PImpl {
|
||||
}
|
||||
}
|
||||
|
||||
void wait_for_connect(const TimeSpan timeout) {
|
||||
INFO_FN("wait_for_connect. {}; timeout: {}", snapshot(), timeout);
|
||||
const auto start = TimeStamp::now();
|
||||
while (state == State::OPENING) {
|
||||
const auto elapsed = TimeStamp::now() - start;
|
||||
const auto rem = timeout - elapsed;
|
||||
if (rem < TimeSpan::zero()) {
|
||||
throw std::runtime_error{fmt::format(
|
||||
"Timed out waiting for connection after {} secs. {}",
|
||||
elapsed.total_seconds(), snapshot())};
|
||||
}
|
||||
WARN_FN(
|
||||
"wait_for_connect. Still waiting. {}; timeout: {}; rem: {}",
|
||||
snapshot(), timeout, rem);
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
}
|
||||
void reconnect() {
|
||||
WARN_FN("reconnect. {}", snapshot());
|
||||
sleep_for(nanoseconds{reconnect_interval(n_reconnects).nanos()});
|
||||
++n_reconnects;
|
||||
connect();
|
||||
}
|
||||
|
||||
void run() {
|
||||
@@ -258,10 +286,33 @@ struct WebsocketClient::PImpl {
|
||||
INFO("Websocket 'run' thread end. {}", snapshot());
|
||||
}
|
||||
|
||||
void run_one() {
|
||||
try {
|
||||
client.run_one();
|
||||
} catch (const websocketpp::exception &ex) {
|
||||
ERROR_FN(
|
||||
"io_thread (run_one). Websocket io threw websocketpp::exception. "
|
||||
"{}; err: {}",
|
||||
snapshot(), ex);
|
||||
} catch (const std::exception &ex) {
|
||||
ERROR_FN(
|
||||
"io_thread (run_one). Websocket io threw std::exception. {}; err: "
|
||||
"{}",
|
||||
snapshot(), ex);
|
||||
} catch (...) {
|
||||
bts_assert_msgf(
|
||||
false,
|
||||
"io_thread (run_one). Websocket io threw unknown exception. Could "
|
||||
"this have been thrown by a callback delegate? {}",
|
||||
snapshot());
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static std::atomic<int> client_id_counter;
|
||||
|
||||
auto on_tls_init(ConnectionHdl /*hdl*/) {
|
||||
std::shared_ptr<boost::asio::ssl::context> on_tls_init(
|
||||
ConnectionHdl /*hdl*/) {
|
||||
INFO_FN("on_tls_init. state: {}; version: {}", state, version);
|
||||
auto result =
|
||||
websocketpp::lib::make_shared<SslContext>(SslContext::sslv23_client);
|
||||
@@ -269,38 +320,6 @@ struct WebsocketClient::PImpl {
|
||||
return result;
|
||||
}
|
||||
|
||||
void init_handlers() {
|
||||
INFO_FN("init_handlers. state: {}; version: {}", state, version);
|
||||
bts_assert(!io_thread);
|
||||
client.init_asio();
|
||||
client.set_message_handler(
|
||||
[this](auto hdl, auto msg) { on_message(hdl, msg); });
|
||||
client.set_open_handler([this](auto hdl) { on_open(hdl); });
|
||||
client.set_ping_handler([](auto /*hdl*/, auto /*msg*/) {
|
||||
LOG(Http, Info, "Received ping");
|
||||
return true;
|
||||
});
|
||||
client.set_pong_timeout(0);
|
||||
client.set_pong_handler([](auto /*hdl*/, auto /*msg*/) {
|
||||
LOG(Http, Info, "Received pong");
|
||||
return true;
|
||||
});
|
||||
client.set_tls_init_handler(
|
||||
[this](auto hdl) { return on_tls_init(hdl); });
|
||||
client.set_close_handler([this](auto hdl) { on_close(hdl); });
|
||||
client.set_fail_handler([this](auto hdl) {
|
||||
const auto ec = client.get_con_from_hdl(hdl)->get_ec();
|
||||
ERROR_FN("fail_handler. {}; err: {}", snapshot(), ec.message());
|
||||
});
|
||||
}
|
||||
|
||||
void init_logging() {
|
||||
INFO_FN("init_logging. {}", snapshot());
|
||||
client.set_access_channels(websocketpp::log::alevel::all);
|
||||
// client.clear_access_channels(websocketpp::log::alevel::frame_payload);
|
||||
client.set_error_channels(websocketpp::log::elevel::all);
|
||||
}
|
||||
|
||||
void on_open(ConnectionHdl hdl) {
|
||||
lock_guard<mutex> lock{instance_mx};
|
||||
INFO_FN("on_open. {}", snapshot());
|
||||
@@ -358,11 +377,37 @@ struct WebsocketClient::PImpl {
|
||||
if (close_handler) {
|
||||
(*close_handler)(ec);
|
||||
}
|
||||
if (config.reconnect_on_close) {
|
||||
reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void on_fail(ConnectionHdl hdl) {
|
||||
lock_guard<mutex> lock{instance_mx};
|
||||
ERROR_FN("on_fail. {}", snapshot());
|
||||
set_state(State::IDLE);
|
||||
const auto con = client.get_con_from_hdl(hdl);
|
||||
const auto ec = con->get_ec();
|
||||
if (ec) {
|
||||
ERROR_FN(
|
||||
"on_fail. {}; local_code: {}; "
|
||||
"local_reason: {}; remote_code: {}; remote_reason: {}; err: {}",
|
||||
snapshot(), con->get_local_close_code(),
|
||||
con->get_local_close_reason(), con->get_remote_close_code(),
|
||||
con->get_remote_close_reason(), ec.message());
|
||||
}
|
||||
if (config.reconnect_on_fail) {
|
||||
reconnect();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
WebsocketClient::WebsocketClient(WebsocketClientConfig config)
|
||||
: me_(std::make_unique<PImpl>(std::move(config))) {}
|
||||
WebsocketClient::WebsocketClient(
|
||||
WebsocketClientConfig config, boost::asio::io_context *io_context)
|
||||
: me_(std::make_unique<PImpl>()) {
|
||||
me_->config = std::move(config);
|
||||
me_->io_context = io_context;
|
||||
}
|
||||
|
||||
WebsocketClient::~WebsocketClient() {
|
||||
me_->close(websocketpp::close::status::going_away, "");
|
||||
@@ -391,13 +436,16 @@ WebsocketClient::CloseHandler &WebsocketClient::close_handler() {
|
||||
return *me_->close_handler;
|
||||
}
|
||||
|
||||
void WebsocketClient::connect(
|
||||
const std::map<std::string, std::string> &headers, bool run_in_background) {
|
||||
me_->connect(headers, run_in_background);
|
||||
}
|
||||
void WebsocketClient::connect() { me_->connect(); }
|
||||
|
||||
void WebsocketClient::run() { me_->run(); }
|
||||
|
||||
void WebsocketClient::run_one() { me_->run_one(); }
|
||||
|
||||
boost::asio::io_context *WebsocketClient::io_context() {
|
||||
return me_->io_context;
|
||||
}
|
||||
|
||||
websocketpp::lib::error_code WebsocketClient::send_text(const string &text) {
|
||||
return me_->send_text(text);
|
||||
}
|
||||
@@ -415,4 +463,6 @@ bool WebsocketClient::is_connected() const {
|
||||
|
||||
void WebsocketClient::ping(const string &payload) { me_->con->ping(payload); }
|
||||
|
||||
const string &WebsocketClient::url() const { return me_->config.url; }
|
||||
|
||||
} // namespace bts::websocket
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "lib/io/websocket/websocket_client_config.h"
|
||||
#include "lib/utils/Delegate.h"
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <websocketpp/config/core_client.hpp>
|
||||
@@ -12,28 +13,33 @@ namespace bts::websocket {
|
||||
|
||||
class WebsocketClient {
|
||||
public:
|
||||
explicit WebsocketClient(WebsocketClientConfig config);
|
||||
WebsocketClient(
|
||||
WebsocketClientConfig config,
|
||||
boost::asio::io_context *io_context = nullptr);
|
||||
~WebsocketClient();
|
||||
|
||||
using OpenHandler = bts::core::Delegate<void(const std::error_code &)>;
|
||||
using MessageHandler = bts::core::Delegate<void(const std::string &)>;
|
||||
using CloseHandler = bts::core::Delegate<void(const std::error_code &)>;
|
||||
|
||||
void connect(
|
||||
const std::map<std::string, std::string> &headers = {},
|
||||
bool run_in_background = true);
|
||||
void connect();
|
||||
void disconnect();
|
||||
bool is_connected() const;
|
||||
boost::asio::io_context *io_context();
|
||||
void run();
|
||||
void run_one();
|
||||
CloseHandler &open_handler();
|
||||
MessageHandler &message_handler();
|
||||
CloseHandler &close_handler();
|
||||
websocketpp::lib::error_code send_text(const std::string &text);
|
||||
void ping(const std::string &payload = "");
|
||||
|
||||
const std::string &url() const;
|
||||
|
||||
private:
|
||||
struct PImpl;
|
||||
std::unique_ptr<PImpl> me_;
|
||||
};
|
||||
|
||||
} // namespace bts::websocket
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
namespace bts::websocket {
|
||||
|
||||
struct WebsocketClientConfig {
|
||||
std::string url;
|
||||
core::TimeSpan connect_timeout;
|
||||
core::TimeSpan pong_timeout;
|
||||
std::string url{};
|
||||
core::TimeSpan connect_timeout = core::TimeSpan::zero();
|
||||
core::TimeSpan pong_timeout = core::TimeSpan::zero();
|
||||
bool reconnect_on_close = false;
|
||||
bool reconnect_on_fail = false;
|
||||
std::map<std::string, std::string> headers{};
|
||||
};
|
||||
|
||||
} // namespace bts::websocket
|
||||
@@ -182,7 +182,7 @@ void FillStore::on_execution_report(
|
||||
fill.set_fill_kind(api::orderman::Fill::FILL_KIND_STANDARD);
|
||||
fill.set_security_key(er.security->id());
|
||||
}
|
||||
fill.set_account(to_str(er.account_name));
|
||||
fill.set_account(to_strn(er.account_name));
|
||||
fill.set_last_price(er.last_price.value());
|
||||
fill.set_avg_price(er.avg_price.value());
|
||||
fill.set_side(er.side);
|
||||
|
||||
@@ -284,12 +284,24 @@ struct OkxExecutionGateway::PImpl {
|
||||
|
||||
void start_impl() {
|
||||
size_t attempt = 0;
|
||||
while (state != OkxExecutionState::OFF) {
|
||||
for (size_t version = 0; state != OkxExecutionState::OFF; version++) {
|
||||
try {
|
||||
LOG(OrderMan, Warn,
|
||||
"OkxExecutionAdapter::start Attempting login (Attempt: {})",
|
||||
attempt + 1);
|
||||
ws_client.connect({}, true);
|
||||
if (ws_run_thread) {
|
||||
if (ws_run_thread->joinable()) {
|
||||
ws_run_thread->join();
|
||||
}
|
||||
ws_run_thread.reset();
|
||||
}
|
||||
ws_client.connect();
|
||||
ws_run_thread = threading::make_thread(
|
||||
"okx_execution_ws", [&, orig_version = version]() {
|
||||
while (version == orig_version) {
|
||||
ws_client.run_one();
|
||||
}
|
||||
});
|
||||
sleep(2);
|
||||
if (ws_ping_thread.has_value()) {
|
||||
if (ws_ping_thread->joinable()) {
|
||||
@@ -300,7 +312,7 @@ struct OkxExecutionGateway::PImpl {
|
||||
.reset(); // Reset the optional before creating a new thread
|
||||
}
|
||||
|
||||
ws_ping_thread.emplace([&]() {
|
||||
ws_ping_thread.emplace([&, orig_version = version]() {
|
||||
LOG(OrderMan, Warn,
|
||||
"OkxExecutionAdapter::ws_ping_thread thread starting. url: "
|
||||
"{}",
|
||||
@@ -311,7 +323,7 @@ struct OkxExecutionGateway::PImpl {
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(ping_interval.millis()));
|
||||
|
||||
while (ws_client.is_connected()) {
|
||||
while (ws_client.is_connected() && version == orig_version) {
|
||||
const auto now = bts::core::TimeStamp::now();
|
||||
if ((now - last_ping_time) > ping_interval) {
|
||||
LOG(OrderMan, Warn,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <system_error>
|
||||
|
||||
template <>
|
||||
struct fmt::formatter<std::error_code> {
|
||||
constexpr auto parse(fmt::format_parse_context &ctx) -> decltype(ctx.end()) {
|
||||
return ctx.end();
|
||||
}
|
||||
|
||||
template <typename FormatContext>
|
||||
auto format(const std::error_code &e, FormatContext &ctx) const
|
||||
-> decltype(ctx.out()) {
|
||||
return fmt::format_to(ctx.out(), "{}", e.message());
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,479 @@
|
||||
#include "cross_margin_gateway.h"
|
||||
#include "binance_execution/cross_margin/messages/cancel_order_response.h"
|
||||
#include "binance_execution/cross_margin/messages/create_order_request.h"
|
||||
#include "binance_execution/cross_margin/rest_api_client.h"
|
||||
#include "binance_execution/cross_margin/side_effect_type.h"
|
||||
#include "binance_execution/cross_margin/trade_data_stream/trade_data_subscription.h"
|
||||
#include "binance_execution/execution_report_store.h"
|
||||
#include "binance_execution/gateway_config.h"
|
||||
#include "binance_execution/listen_key_requestor.h"
|
||||
#include "binance_execution/order_key_map.h"
|
||||
#include "binance_execution/rest_api_result.h"
|
||||
#include "binance_execution/type_validators.h"
|
||||
#include "lib/api/service_helpers/AdminServiceUtils.h"
|
||||
#include "lib/api/services/AdminService.pb.h"
|
||||
#include "lib/api/services/OrderService.h"
|
||||
#include "lib/config/protos/jetfix.pb.h"
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/domain_model/Security.pb.h"
|
||||
#include "lib/io/json_formatter.h"
|
||||
#include "lib/logging/logging.h"
|
||||
#include "lib/md/SecurityOracle.h"
|
||||
#include "lib/mercury/events/Update.h"
|
||||
#include "lib/orderman/CancelOrderMsg.h"
|
||||
#include "lib/orderman/CancelReplaceOrderMsg.h"
|
||||
#include "lib/orderman/CreateOrderMsg.h"
|
||||
#include "lib/orderman/RFQMsg.h"
|
||||
#include "lib/orderman/events/NewAdminDataEvent.h"
|
||||
#include "lib/orderman/events/NewSecuritiesEvent.h"
|
||||
#include "lib/orderman/orderman_utils/ExchangePosition.h"
|
||||
#include "lib/orderman/orderman_utils/ExecutionReport.h"
|
||||
#include "lib/orderman/orderman_utils/orderman_common.h"
|
||||
#include "lib/utils/overloaded.h"
|
||||
|
||||
#include <boost/lockfree/spsc_queue.hpp>
|
||||
#include <fmt/format.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
using nlohmann::json;
|
||||
|
||||
namespace binance_execution::cross_margin {
|
||||
|
||||
#define ASSERT_FN(test, fn, ...) \
|
||||
bts_assert_msgf(test, "BinanceCrossMargin::" fn ". " __VA_ARGS__)
|
||||
#define LOG_FN(level, fn, ...) \
|
||||
LOG(OrderMan, level, "BinanceCrossMargin::" fn ". " __VA_ARGS__)
|
||||
#define DIAG_FN(fn, ...) LOG_FN(bts::Diag, fn, __VA_ARGS__)
|
||||
#define INFO_FN(fn, ...) LOG_FN(bts::Info, fn, __VA_ARGS__)
|
||||
#define WARN_FN(fn, ...) LOG_FN(bts::Warn, fn, __VA_ARGS__)
|
||||
#define ERROR_FN(fn, ...) LOG_FN(bts::Error, fn, __VA_ARGS__)
|
||||
|
||||
#define VENUE bts::api::VENUE_BINANCE
|
||||
|
||||
using boost::lockfree::spsc_queue;
|
||||
using bts::core::Delegate;
|
||||
using bts::io::json_formatter;
|
||||
using bts::orderman::CancelOrderMsg;
|
||||
using bts::orderman::CancelReplaceOrderMsg;
|
||||
using bts::orderman::CreateOrderMsg;
|
||||
using bts::orderman::ExecutionReport;
|
||||
using bts::orderman::NewAdminDataEvent;
|
||||
using bts::orderman::NewSecuritiesEvent;
|
||||
using bts::orderman::OrderMsg;
|
||||
using bts::orderman::RFQMsg;
|
||||
using std::int64_t;
|
||||
using std::optional;
|
||||
using std::shared_ptr;
|
||||
using std::size_t;
|
||||
using std::string;
|
||||
using std::uint32_t;
|
||||
using std::vector;
|
||||
|
||||
struct ExecutionGatewayCallbacks {
|
||||
bts::orderman::OnExeReport exe_report;
|
||||
bts::orderman::OnGwStatusChange gw_status_change;
|
||||
bts::orderman::OnCancelReject cancel_reject;
|
||||
bts::orderman::OnExchangePositionState exchange_position_state;
|
||||
bts::orderman::OnExchangeOpenOrdersState exchange_open_orders;
|
||||
};
|
||||
|
||||
struct CrossMarginGateway::PImpl {
|
||||
explicit PImpl(
|
||||
bts::md::SecurityOracle sec_oracle,
|
||||
GatewayConfig config,
|
||||
bts::config::JetFIX_Connection jetfix_connection)
|
||||
: sec_oracle(std::move(sec_oracle)), config(std::move(config)),
|
||||
jetfix_connection(std::make_shared<bts::config::JetFIX_Connection>(
|
||||
std::move(jetfix_connection))),
|
||||
rest_api_client(
|
||||
this->config.rest_endpoint,
|
||||
this->config.api_key,
|
||||
this->config.secret_key),
|
||||
listen_key_requestor(
|
||||
[this](const auto &req) {
|
||||
return rest_api_client.get_listen_key(req);
|
||||
},
|
||||
[this](const auto &req) {
|
||||
return rest_api_client.refresh_listen_key(req);
|
||||
}),
|
||||
key_map(std::make_shared<OrderKeyMap>()),
|
||||
orders(
|
||||
[this](const std::string &exch_key) {
|
||||
return *key_map->get_bts_key(exch_key);
|
||||
},
|
||||
this->config.account,
|
||||
this->jetfix_connection) {
|
||||
bts_assert(this->jetfix_connection->venue() == VENUE);
|
||||
bts_assert(!this->jetfix_connection->name().empty());
|
||||
pos.exchange = VENUE;
|
||||
}
|
||||
|
||||
static constexpr const char *CANCEL_REPLACE_CANCEL_SUFFIX = "crc";
|
||||
|
||||
bts::md::SecurityOracle sec_oracle;
|
||||
GatewayConfig config;
|
||||
shared_ptr<bts::config::JetFIX_Connection> jetfix_connection;
|
||||
|
||||
RestApiClient rest_api_client;
|
||||
optional<trade_data_stream::TradeDataSubscription> trade_data_sub;
|
||||
Delegate<void(trade_data_stream::AccountUpdate)> account_update_callback;
|
||||
Delegate<void(trade_data_stream::OrderUpdate)> order_update_callback;
|
||||
ListenKeyRequestor listen_key_requestor;
|
||||
|
||||
spsc_queue<trade_data_stream::AccountUpdate> incoming_account_updates{1024};
|
||||
spsc_queue<trade_data_stream::OrderUpdate> incoming_order_updates{1024};
|
||||
spsc_queue<shared_ptr<const ExecutionReport>> internal_order_rejects{256};
|
||||
vector<trade_data_stream::AccountUpdate> account_updates_vec;
|
||||
|
||||
ExecutionGatewayCallbacks callbacks;
|
||||
bts::orderman::ExchangePosition pos;
|
||||
bts::api::admin::BTSAccount account;
|
||||
shared_ptr<OrderKeyMap> key_map;
|
||||
ExecutionReportStore orders;
|
||||
|
||||
void start() {
|
||||
INFO_FN("start");
|
||||
listen_key_requestor.callback().bind<&PImpl::on_listen_key_>(*this);
|
||||
listen_key_requestor.start();
|
||||
}
|
||||
|
||||
void stop() {
|
||||
INFO_FN("stop");
|
||||
trade_data_sub.reset();
|
||||
}
|
||||
|
||||
void on_account_update(trade_data_stream::AccountUpdate u) {
|
||||
INFO_FN("on_account_update", "\n{:2}", json_formatter(u));
|
||||
incoming_account_updates.push(std::move(u));
|
||||
}
|
||||
|
||||
void on_order_update(trade_data_stream::OrderUpdate u) {
|
||||
INFO_FN("on_order_update", "\n{:2}", json_formatter(u));
|
||||
if (u.orig_client_order_id.ends_with(CANCEL_REPLACE_CANCEL_SUFFIX)) {
|
||||
// This is the "canceled" order part of a cancel/replace op. This op
|
||||
// generates another update without such a suffix. That's the new
|
||||
// order; that's the one we want.
|
||||
DIAG_FN(
|
||||
"on_order_update",
|
||||
"Ignoring cancel/replace cancel order update. id: {}",
|
||||
u.client_order_id);
|
||||
return;
|
||||
}
|
||||
incoming_order_updates.push(std::move(u));
|
||||
}
|
||||
|
||||
void on_listen_key_(const string &listen_key) {
|
||||
INFO_FN("on_listen_key", "value: {}", listen_key);
|
||||
trade_data_sub.reset();
|
||||
if (listen_key.empty()) {
|
||||
LOG(OrderMan, bts::Error,
|
||||
"Unable to subscribe to user data because 'listen_key' "
|
||||
"is empty.");
|
||||
return;
|
||||
}
|
||||
auto &uds = trade_data_sub.emplace(
|
||||
config.ws_stream_endpoint + "/ws/" + listen_key);
|
||||
uds.account_update_callback().bind<&PImpl::on_account_update>(*this);
|
||||
uds.order_update_callback().bind<&PImpl::on_order_update>(*this);
|
||||
uds.start();
|
||||
}
|
||||
|
||||
void create_order(const CreateOrderMsg &m, bool is_replace = false) {
|
||||
CreateOrderRequest req;
|
||||
req.autoRepayAtCancel = true;
|
||||
req.sideEffectType = SideEffectType::AUTO_BORROW_REPAY;
|
||||
std::string err;
|
||||
if (!req.init(m, err)) {
|
||||
ERROR_FN(
|
||||
"create_order",
|
||||
"{} {} {} @ {} (id={}, type={}, tif={}). Rejected "
|
||||
"internally (request not sent). {}",
|
||||
m.side, m.quantity, req.symbol, m.price, m.key, m.order_type,
|
||||
m.time_in_force, err);
|
||||
internal_order_rejects.push(orders.reject(m, err));
|
||||
return;
|
||||
}
|
||||
if (is_replace) {
|
||||
key_map->increment_order_version(m.key, req.newClientOrderId);
|
||||
}
|
||||
INFO_FN("create_order", "Sending request", json_formatter(req));
|
||||
const auto res = rest_api_client.create_order(req);
|
||||
if (!res.ok()) {
|
||||
ERROR_FN(
|
||||
"create_order", "id={}. {}", req.newClientOrderId,
|
||||
json_formatter(res));
|
||||
internal_order_rejects.push(
|
||||
orders.reject({}, json_formatter(*res.error).to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
void update_order(const CancelReplaceOrderMsg &m) {
|
||||
CancelOrderMsg cxl;
|
||||
cxl.exchange_order_id = m.exchange_order_id;
|
||||
cxl.manual_order_indicator = m.create_fields.manual_order_indicator;
|
||||
cxl.key = m.create_fields.key;
|
||||
cxl.routing_record = m.create_fields.routing_record;
|
||||
cxl.security = m.create_fields.security;
|
||||
cxl.side = m.create_fields.side;
|
||||
INFO_FN("update_order", "Cancelling order");
|
||||
const auto *res = cancel_order(cxl);
|
||||
if (res != nullptr && res->ok()) {
|
||||
INFO_FN("update_order", "Sending replacement order");
|
||||
create_order(m.create_fields, true);
|
||||
}
|
||||
}
|
||||
|
||||
const RestApiResult<CancelOrderResponse> *cancel_order(
|
||||
const CancelOrderMsg &m) {
|
||||
CancelOrderRequest req;
|
||||
std::string err;
|
||||
if (!req.init(m, *key_map, err)) {
|
||||
ERROR_FN(
|
||||
"cancel_order", "{} {} (id={}, exch_id={}). {}", m.side,
|
||||
req.symbol, m.key, m.exchange_order_id, err);
|
||||
internal_order_rejects.push(orders.set_error(
|
||||
m.key, "Rejected internally (request not sent). " + err));
|
||||
return nullptr;
|
||||
}
|
||||
INFO_FN(
|
||||
"cancel_order", "{} {} (id={}, exch_id={})", m.side, req.symbol,
|
||||
m.key, m.exchange_order_id);
|
||||
const auto &res = rest_api_client.cancel_order(req);
|
||||
if (!res.ok()) {
|
||||
ERROR_FN(
|
||||
"cancel_order", "{} {} (id={}, exch_id={}). {}", m.side,
|
||||
req.symbol, m.key, m.exchange_order_id, json_formatter(res));
|
||||
internal_order_rejects.push(orders.set_error(
|
||||
m.key,
|
||||
fmt::format(
|
||||
"Cancel request failed. {}", json_formatter(*res.error))));
|
||||
}
|
||||
return &res;
|
||||
}
|
||||
|
||||
void dispatch_internal_rejects() {
|
||||
shared_ptr<const ExecutionReport> curr;
|
||||
while (internal_order_rejects.pop(curr)) {
|
||||
callbacks.exe_report(curr, {});
|
||||
}
|
||||
}
|
||||
|
||||
void dispatch_order_updates() {
|
||||
if (incoming_order_updates.empty()) {
|
||||
return;
|
||||
}
|
||||
trade_data_stream::OrderUpdate curr;
|
||||
auto &km = *key_map;
|
||||
while (incoming_order_updates.pop(curr)) {
|
||||
auto bts_key = km.get_bts_key(curr.client_order_id);
|
||||
if (!bts_key) {
|
||||
ERROR_FN(
|
||||
"dispatch_order_updates",
|
||||
"Failed to generate exe report. Did not resolve bts key from "
|
||||
"exch clientOrderId '{}'",
|
||||
curr.client_order_id);
|
||||
continue;
|
||||
}
|
||||
auto exch_key = km.get_exch_key(*bts_key);
|
||||
if (exch_key != curr.client_order_id) {
|
||||
INFO_FN(
|
||||
"dispatch_order_updates",
|
||||
"Ignoring update w/ old clientOrderId '{}' (expected '{}', "
|
||||
"exch_id={}). This is expected for CancelReplace ops.",
|
||||
curr.client_order_id, curr.order_id, exch_key);
|
||||
continue;
|
||||
}
|
||||
// orders
|
||||
shared_ptr<const ExecutionReport> er_p =
|
||||
orders.update(curr.client_order_id, [&](auto &er_p, bool) {
|
||||
auto &er = *er_p;
|
||||
std::string error;
|
||||
if (!curr.to_execution_report(er, error)) {
|
||||
WARN_FN(
|
||||
"update", "Failed to initialize exe report. {}. {}",
|
||||
error, json_formatter(curr));
|
||||
return;
|
||||
}
|
||||
er.security = sec_oracle.get_security(
|
||||
VENUE, bts::api::SECURITYTYPE_SPOT, curr.symbol);
|
||||
});
|
||||
const ExecutionReport &er = *er_p;
|
||||
string error;
|
||||
if (!validate(er, error)) {
|
||||
ERROR_FN(
|
||||
"dispatch_order_updates",
|
||||
"Generated invalid ExecutionReport (id={}, exch_id={}). {}",
|
||||
curr.client_order_id, curr.order_id, error);
|
||||
continue;
|
||||
}
|
||||
switch (curr.order_status.value) {
|
||||
case OrderStatus::FILLED:
|
||||
case OrderStatus::CANCELED:
|
||||
case OrderStatus::REJECTED: {
|
||||
const size_t n_active = orders.erase(er.order_key);
|
||||
INFO_FN(
|
||||
"dispatch_order_updates",
|
||||
"Order reached terminal state {} (id={}, exch_id={}). {} "
|
||||
"active orders.",
|
||||
curr.order_status, curr.client_order_id, curr.order_id,
|
||||
n_active);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
INFO_FN(
|
||||
"dispatch_order_updates",
|
||||
"Firing exe report callback. {} {} {} @ {} {} (filled {}/{}, "
|
||||
"id={}). {} active orders",
|
||||
er.side, er.security->binance().security_desc(), er.quantity,
|
||||
er.price, er.order_status, er.filled_quantity, er.quantity,
|
||||
er.order_key, orders.size());
|
||||
callbacks.exe_report(std::move(er_p), {});
|
||||
}
|
||||
}
|
||||
|
||||
void dispatch_account_updates() {
|
||||
trade_data_stream::AccountUpdate u;
|
||||
auto &acct_upds = account_updates_vec;
|
||||
acct_upds.clear();
|
||||
while (incoming_account_updates.pop(acct_upds.emplace_back())) {
|
||||
}
|
||||
acct_upds.pop_back();
|
||||
if (acct_upds.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto pos_ticker_comp =
|
||||
[](const bts::orderman::ExchangePosition::UnitPosition &unit_pos,
|
||||
const string &ticker) {
|
||||
return unit_pos.exchange_sec_desc < ticker;
|
||||
};
|
||||
auto &dest = pos.total_exchange_positions;
|
||||
for (const auto &acct_upd : acct_upds) {
|
||||
for (const auto &pos : acct_upd.balances) {
|
||||
bts_assert(pos.free >= 0);
|
||||
bts_assert(pos.locked >= 0);
|
||||
auto it = std::lower_bound(
|
||||
dest.begin(), dest.end(), pos.asset, pos_ticker_comp);
|
||||
if (it == dest.end() || it->exchange_sec_desc != pos.asset) {
|
||||
const auto *sec = sec_oracle.get_security(
|
||||
VENUE, bts::api::SECURITYTYPE_SPOT, pos.asset);
|
||||
if (sec == nullptr) {
|
||||
continue;
|
||||
}
|
||||
bts::orderman::ExchangePosition::UnitPosition new_pos;
|
||||
new_pos.exchange_sec_desc = pos.asset;
|
||||
new_pos.security = sec;
|
||||
new_pos.account_id = account.id();
|
||||
new_pos.account_name = account.name();
|
||||
it = dest.insert(it, std::move(new_pos));
|
||||
}
|
||||
it->position = pos.locked + pos.free;
|
||||
it->avg_filled_price.value() =
|
||||
std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
}
|
||||
callbacks.exchange_position_state(pos);
|
||||
}
|
||||
};
|
||||
|
||||
CrossMarginGateway::CrossMarginGateway(
|
||||
bts::md::SecurityOracle sec_oracle,
|
||||
GatewayConfig session_cfg,
|
||||
bts::config::JetFIX::Connection jetfix_connection)
|
||||
: me_(std::make_unique<PImpl>(
|
||||
std::move(sec_oracle),
|
||||
std::move(session_cfg),
|
||||
std::move(jetfix_connection))) {
|
||||
bts_assert(
|
||||
me_->config.account.size() <=
|
||||
bts::orderman::ExecutionReport{}.account_name.value().size());
|
||||
}
|
||||
|
||||
CrossMarginGateway::~CrossMarginGateway() = default;
|
||||
|
||||
void CrossMarginGateway::start() { me_->start(); }
|
||||
|
||||
void CrossMarginGateway::on_securities(
|
||||
const bts::mercury::Update<NewSecuritiesEvent> & /*update*/) {
|
||||
INFO_FN(
|
||||
"on_securities", "t_id: {} t_name: {}", bts::os::get_thread_id(),
|
||||
bts::os::get_thread_name());
|
||||
}
|
||||
|
||||
void CrossMarginGateway::on_admin_data(
|
||||
const bts::mercury::Update<NewAdminDataEvent> &update) {
|
||||
INFO_FN(
|
||||
"on_admin_data", "t_id: {} t_name: {}", bts::os::get_thread_id(),
|
||||
bts::os::get_thread_name());
|
||||
if (update.events == nullptr || update.events->empty()) {
|
||||
return;
|
||||
}
|
||||
const auto *account = bts::admin::AdminServiceUtils::find_account(
|
||||
*update.events->back().admin_data, me_->config.account);
|
||||
if (account == nullptr) {
|
||||
WARN_FN(
|
||||
"on_admin_data",
|
||||
"Gateway account not found. Cannot execute orders. name: '{}'",
|
||||
me_->config.account);
|
||||
return;
|
||||
}
|
||||
if (me_->account.name() == account->name() &&
|
||||
me_->account.id() == account->id()) {
|
||||
return;
|
||||
}
|
||||
me_->account.CopyFrom(*account);
|
||||
INFO_FN("on_admin_data", "Resolved gateway account '{}'", account->name());
|
||||
}
|
||||
|
||||
void CrossMarginGateway::send_msgs(
|
||||
vector<OrderMsg> const &msg_vec, bts::trace::CorID) {
|
||||
INFO_FN("send_msgs", "count: {}", msg_vec.size());
|
||||
for (const auto &msg : msg_vec) {
|
||||
std::visit(
|
||||
bts::overloaded{
|
||||
[&](const CreateOrderMsg &m) { me_->create_order(m); },
|
||||
[&](const CancelReplaceOrderMsg &m) { me_->update_order(m); },
|
||||
[&](const CancelOrderMsg &m) { me_->cancel_order(m); },
|
||||
[&](const RFQMsg &) {
|
||||
bts_assert_msg(false, "Binance RFQ not implemented");
|
||||
},
|
||||
},
|
||||
msg);
|
||||
}
|
||||
}
|
||||
|
||||
void CrossMarginGateway::send_fix(
|
||||
const bts::jetfix::OutFIXPtr &, bts::trace::CorID) {
|
||||
bts_assert_msg(false, "Binance CrossMarginGateway does not support fix");
|
||||
}
|
||||
|
||||
void CrossMarginGateway::dispatch_incoming_fix_messages() {
|
||||
me_->dispatch_internal_rejects();
|
||||
me_->dispatch_order_updates();
|
||||
me_->dispatch_account_updates();
|
||||
}
|
||||
|
||||
// Callbacks
|
||||
bts::orderman::OnExeReport &CrossMarginGateway::on_exe_report() {
|
||||
return me_->callbacks.exe_report;
|
||||
}
|
||||
bts::orderman::OnGwStatusChange &CrossMarginGateway::on_gw_status_change() {
|
||||
return me_->callbacks.gw_status_change;
|
||||
}
|
||||
bts::orderman::OnCancelReject &CrossMarginGateway::on_cancel_reject() {
|
||||
return me_->callbacks.cancel_reject;
|
||||
}
|
||||
bts::orderman::OnExchangePositionState &
|
||||
CrossMarginGateway::on_exchange_position_state() {
|
||||
return me_->callbacks.exchange_position_state;
|
||||
}
|
||||
bts::orderman::OnExchangeOpenOrdersState &
|
||||
CrossMarginGateway::on_exchange_open_orders() {
|
||||
return me_->callbacks.exchange_open_orders;
|
||||
}
|
||||
|
||||
} // namespace binance_execution::cross_margin
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/orderman/gateways/IExecutionGateway.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace bts::config {
|
||||
class JetFIX_Connection;
|
||||
}
|
||||
|
||||
namespace bts::md {
|
||||
class SecurityOracle;
|
||||
}
|
||||
|
||||
namespace binance_execution {
|
||||
struct GatewayConfig;
|
||||
}
|
||||
|
||||
namespace binance_execution::cross_margin {
|
||||
|
||||
class CrossMarginGateway : public bts::orderman::IExecutionGateway {
|
||||
public:
|
||||
explicit CrossMarginGateway(
|
||||
bts::md::SecurityOracle sec_oracle,
|
||||
GatewayConfig session_cfg,
|
||||
bts::config::JetFIX_Connection jetfix_connection);
|
||||
~CrossMarginGateway() override;
|
||||
|
||||
void start() override;
|
||||
void dispatch_incoming_fix_messages() override;
|
||||
void on_securities(
|
||||
const bts::mercury::Update<bts::orderman::NewSecuritiesEvent> &)
|
||||
override;
|
||||
void on_admin_data(
|
||||
const bts::mercury::Update<bts::orderman::NewAdminDataEvent> &) override;
|
||||
void send_fix(const bts::jetfix::OutFIXPtr &, bts::trace::CorID) override;
|
||||
void send_msgs(
|
||||
std::vector<bts::orderman::OrderMsg> const &,
|
||||
bts::trace::CorID) override;
|
||||
|
||||
bts::orderman::OnExeReport &on_exe_report() override;
|
||||
bts::orderman::OnGwStatusChange &on_gw_status_change() override;
|
||||
bts::orderman::OnCancelReject &on_cancel_reject() override;
|
||||
bts::orderman::OnExchangePositionState &
|
||||
on_exchange_position_state() override;
|
||||
bts::orderman::OnExchangeOpenOrdersState &on_exchange_open_orders() override;
|
||||
|
||||
private:
|
||||
struct PImpl;
|
||||
std::unique_ptr<PImpl> me_;
|
||||
};
|
||||
|
||||
} // namespace binance_execution::cross_margin
|
||||
+4
-4
@@ -3,9 +3,9 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution::usds_futures {
|
||||
namespace binance_execution::cross_margin {
|
||||
|
||||
struct ExecutionGatewayConfig {
|
||||
struct CrossMarginGatewayConfig {
|
||||
std::string api_key;
|
||||
std::string secret_key;
|
||||
std::string account;
|
||||
@@ -15,7 +15,7 @@ struct ExecutionGatewayConfig {
|
||||
};
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(
|
||||
ExecutionGatewayConfig,
|
||||
CrossMarginGatewayConfig,
|
||||
api_key,
|
||||
secret_key,
|
||||
account,
|
||||
@@ -23,4 +23,4 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(
|
||||
ws_endpoint,
|
||||
ws_stream_endpoint);
|
||||
|
||||
} // namespace binance_execution::usds_futures
|
||||
} // namespace binance_execution::cross_margin
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
#include "cancel_order_request.h"
|
||||
#include "binance_execution/error_list.h"
|
||||
#include "binance_execution/order_key_map.h"
|
||||
#include "binance_execution/type_converters.h"
|
||||
#include "binance_execution/type_validators.h"
|
||||
#include "lib/io/JsonReader.h"
|
||||
#include "lib/orderman/CancelOrderMsg.h"
|
||||
#include "lib/utils/checked_arithmetic.h"
|
||||
|
||||
namespace binance_execution::cross_margin {
|
||||
|
||||
bool CancelOrderRequest::init(
|
||||
const bts::orderman::CancelOrderMsg &m,
|
||||
OrderKeyMap &key_map,
|
||||
std::string &error) {
|
||||
const std::size_t orig_error_sz = error.size();
|
||||
if (validate(m.security, bts::api::VENUE_BINANCE, error)) {
|
||||
symbol = m.security->binance().security_desc();
|
||||
}
|
||||
orderId = bts::system::math::checked_ultol(m.exchange_order_id.value());
|
||||
origClientOrderId = key_map.get_exch_key(m.key);
|
||||
// The exchange accepts either orderId, or origClientOrderId (only one
|
||||
// need be populated).
|
||||
if (orderId == 0 && origClientOrderId.empty()) {
|
||||
ErrorList::append(
|
||||
error, "Either orderId or origClientOrderId must be defined");
|
||||
} else {
|
||||
// Can't reuse the same order id (only increment order version
|
||||
// when init is successful, as this means the order will be sent).
|
||||
key_map.increment_order_version(m.key);
|
||||
newClientOrderId = key_map.get_exch_key(m.key);
|
||||
}
|
||||
timestamp = convert_time_stamp(bts::core::TimeStamp::now());
|
||||
return error.size() == orig_error_sz;
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const CancelOrderRequest &req) {
|
||||
j["symbol"] = req.symbol;
|
||||
if (req.isIsolated) {
|
||||
j["isIsolated"] = true;
|
||||
}
|
||||
if (req.orderId != 0) {
|
||||
j["orderId"] = req.orderId;
|
||||
}
|
||||
if (!req.origClientOrderId.empty()) {
|
||||
j["origClientOrderId"] = req.origClientOrderId;
|
||||
}
|
||||
if (!req.newClientOrderId.empty()) {
|
||||
j["newClientOrderId"] = req.newClientOrderId;
|
||||
}
|
||||
if (req.recvWindow > 0) {
|
||||
j["recvWindow"] = req.newClientOrderId;
|
||||
}
|
||||
if (req.timestamp > 0) {
|
||||
j["timestamp"] = req.timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, CancelOrderRequest &req) {
|
||||
const bts::io::JsonReader jr{&j};
|
||||
j.at("symbol").get_to(req.symbol);
|
||||
jr.read_or("isIsolated", req.isIsolated);
|
||||
jr.read_or("orderId", req.orderId);
|
||||
jr.read_or("origClientOrderId", req.origClientOrderId);
|
||||
jr.read_or("newClientOrderId", req.newClientOrderId);
|
||||
jr.read_or("recvWindow", req.recvWindow);
|
||||
j.at("timestamp").get_to(req.timestamp);
|
||||
}
|
||||
|
||||
void to_query_params(std::string &qps, const CancelOrderRequest &req) {
|
||||
qps.append("symbol=");
|
||||
qps.append(req.symbol);
|
||||
if (req.isIsolated) {
|
||||
qps.append("&isIsolated=true");
|
||||
}
|
||||
if (req.orderId != 0) {
|
||||
qps.append("&orderId=");
|
||||
qps.append(std::to_string(req.orderId));
|
||||
}
|
||||
if (!req.origClientOrderId.empty()) {
|
||||
qps.append("&origClientOrderId=");
|
||||
qps.append(req.origClientOrderId);
|
||||
}
|
||||
if (!req.newClientOrderId.empty()) {
|
||||
qps.append("&newClientOrderId=");
|
||||
qps.append(req.newClientOrderId);
|
||||
}
|
||||
if (req.recvWindow > 0) {
|
||||
qps.append("&recvWindow=");
|
||||
qps.append(std::to_string(req.recvWindow));
|
||||
}
|
||||
qps.append("×tamp=");
|
||||
qps.append(std::to_string(req.timestamp));
|
||||
}
|
||||
|
||||
} // namespace binance_execution::cross_margin
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace bts::orderman {
|
||||
struct CancelOrderMsg;
|
||||
}
|
||||
|
||||
namespace binance_execution {
|
||||
struct OrderKeyMap;
|
||||
}
|
||||
|
||||
namespace binance_execution::cross_margin {
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/margin_trading/trade/Margin-Account-Cancel-Order
|
||||
*/
|
||||
struct CancelOrderRequest {
|
||||
std::string symbol;
|
||||
bool isIsolated = false;
|
||||
std::int64_t orderId;
|
||||
std::string origClientOrderId;
|
||||
std::string newClientOrderId;
|
||||
std::int64_t timestamp = 0;
|
||||
std::int64_t recvWindow = 0;
|
||||
|
||||
bool init(
|
||||
const bts::orderman::CancelOrderMsg &m,
|
||||
OrderKeyMap &key_map,
|
||||
std::string &error);
|
||||
};
|
||||
|
||||
void to_json(nlohmann::json &j, const CancelOrderRequest &req);
|
||||
void from_json(const nlohmann::json &j, CancelOrderRequest &req);
|
||||
|
||||
void to_query_params(std::string &query_params, const CancelOrderRequest &req);
|
||||
|
||||
} // namespace binance_execution::cross_margin
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
#include "cancel_order_response.h"
|
||||
#include "binance_execution/enums/order_status.h"
|
||||
#include "binance_execution/enums/order_type.h"
|
||||
#include "binance_execution/enums/time_in_force.h"
|
||||
#include "lib/io/JsonReader.h"
|
||||
|
||||
namespace binance_execution::cross_margin {
|
||||
|
||||
void to_json(nlohmann::json &j, const CancelOrderResponse &res) {
|
||||
j["symbol"] = res.symbol;
|
||||
if (res.isIsolated) {
|
||||
j["isIsolated"] = true;
|
||||
}
|
||||
j["orderId"] = res.orderId;
|
||||
if (!res.origClientOrderId.empty()) {
|
||||
j["origClientOrderId"] = res.origClientOrderId;
|
||||
}
|
||||
if (!res.clientOrderId.empty()) {
|
||||
j["clientOrderId"] = res.clientOrderId;
|
||||
}
|
||||
if (!std::isnan(res.price)) {
|
||||
j["price"] = res.price;
|
||||
}
|
||||
if (!std::isnan(res.origQty)) {
|
||||
j["origQty"] = res.origQty;
|
||||
}
|
||||
if (!std::isnan(res.executedQty)) {
|
||||
j["executedQty"] = res.executedQty;
|
||||
}
|
||||
if (!std::isnan(res.cummulativeQuoteQty)) {
|
||||
j["cummulativeQuoteQty"] = res.cummulativeQuoteQty;
|
||||
}
|
||||
if (res.status != OrderStatus::NONE) {
|
||||
j["status"] = res.status;
|
||||
}
|
||||
if (res.timeInForce != TimeInForce::NONE) {
|
||||
j["timeInForce"] = res.timeInForce;
|
||||
}
|
||||
if (res.type != OrderType::NONE) {
|
||||
j["type"] = res.type;
|
||||
}
|
||||
if (res.side != Side::NONE) {
|
||||
j["side"] = res.side;
|
||||
}
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, CancelOrderResponse &res) {
|
||||
j.at("symbol").get_to(res.symbol);
|
||||
auto it = j.find("isIsolated");
|
||||
if (it != j.end()) {
|
||||
it->get_to(res.isIsolated);
|
||||
}
|
||||
const bts::io::JsonReader jr{&j};
|
||||
jr.read("orderId", res.orderId);
|
||||
jr.read("origClientOrderId", res.origClientOrderId);
|
||||
jr.read("clientOrderId", res.clientOrderId);
|
||||
jr.read("price", res.price);
|
||||
jr.read("origQty", res.origQty);
|
||||
jr.read("executedQty", res.executedQty);
|
||||
jr.read("cummulativeQuoteQty", res.cummulativeQuoteQty);
|
||||
jr.read("status", res.status);
|
||||
jr.read("timeInForce", res.timeInForce);
|
||||
jr.read("type", res.type);
|
||||
jr.read("side", res.side);
|
||||
}
|
||||
|
||||
} // namespace binance_execution::cross_margin
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/enums/order_status.h"
|
||||
#include "binance_execution/enums/order_type.h"
|
||||
#include "binance_execution/enums/side.h"
|
||||
#include "binance_execution/enums/time_in_force.h"
|
||||
|
||||
#include <limits>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution::cross_margin {
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/margin_trading/trade/Margin-Account-Cancel-Order
|
||||
* @a https://developers.binance.com/docs/margin_trading/trade/Margin-Account-Cancel-Order#response-example
|
||||
*/
|
||||
struct CancelOrderResponse {
|
||||
std::string symbol;
|
||||
bool isIsolated = false;
|
||||
std::int64_t orderId = 0;
|
||||
std::string origClientOrderId;
|
||||
std::string clientOrderId;
|
||||
double price = std::numeric_limits<double>::quiet_NaN();
|
||||
double origQty = std::numeric_limits<double>::quiet_NaN();
|
||||
double executedQty = std::numeric_limits<double>::quiet_NaN();
|
||||
// Note: 'cummulative' is deliberately mispelled to match the json payload.
|
||||
double cummulativeQuoteQty = std::numeric_limits<double>::quiet_NaN();
|
||||
OrderStatus status = OrderStatus::NONE;
|
||||
TimeInForce timeInForce = TimeInForce::NONE;
|
||||
OrderType type = OrderType::NONE;
|
||||
Side side = Side::NONE;
|
||||
};
|
||||
|
||||
void to_json(nlohmann::json &j, const CancelOrderResponse &res);
|
||||
void from_json(const nlohmann::json &j, CancelOrderResponse &res);
|
||||
|
||||
} // namespace binance_execution::cross_margin
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
// #pragma once
|
||||
|
||||
// #include "binance_execution/enums/new_order_response_type.h"
|
||||
// #include "binance_execution/enums/order_type.h"
|
||||
// #include "binance_execution/enums/side.h"
|
||||
// #include "binance_execution/enums/time_in_force.h"
|
||||
|
||||
// #include <nlohmann/json.hpp>
|
||||
// #include <string>
|
||||
|
||||
// namespace bts::orderman {
|
||||
// struct CreateOrderMsg;
|
||||
// }
|
||||
|
||||
// namespace binance_execution::cross_margin {
|
||||
|
||||
// /**
|
||||
// * @a https://developers.binance.com/docs/binance-spot-api-docs/rest-api/trading-endpoints#new-order-trade
|
||||
// */
|
||||
// struct CreateOcoRequest {
|
||||
// std::string symbol;
|
||||
// std::string listClientOrderId;
|
||||
// Side side = Side::NONE;
|
||||
// double quantity = 0;
|
||||
// std::string limitClientOrderId;
|
||||
// double price = std::numeric_limits<double>::quiet_NaN();
|
||||
// double limitIcebergQty = 0;
|
||||
// std::string stopClientOrderId;
|
||||
// double stopPrice;
|
||||
// double stopLimitPrice;
|
||||
// double stopIcebergQty;
|
||||
// TimeInForce stopLimitTimeInForce;
|
||||
// NewOrderResponseType newOrderRespType;
|
||||
// OrderType type = OrderType::NONE;
|
||||
// std::int64_t timestamp = 0;
|
||||
// std::string newClientOrderId;
|
||||
// TimeInForce timeInForce = TimeInForce::NONE;
|
||||
// std::int64_t recvWindow = 0;
|
||||
// NewOrderResponseType newOrderRespType = NewOrderResponseType::NONE;
|
||||
|
||||
// bool init(const bts::orderman::CreateOrderMsg &m, std::string &error);
|
||||
// };
|
||||
|
||||
// void to_json(nlohmann::json &j, const CreateOcoRequest &req);
|
||||
// void from_json(const nlohmann::json &j, CreateOcoRequest &req);
|
||||
|
||||
// void to_query_params(std::string &query_params, const CreateOcoRequest &req);
|
||||
|
||||
// } // namespace binance_execution::spots
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
#include "create_order_request.h"
|
||||
#include "binance_execution/enums/new_order_response_type.h"
|
||||
#include "binance_execution/price.h"
|
||||
#include "binance_execution/quantity.h"
|
||||
#include "binance_execution/type_converters.h"
|
||||
#include "binance_execution/type_validators.h"
|
||||
#include "lib/io/JsonReader.h"
|
||||
#include "lib/orderman/CreateOrderMsg.h"
|
||||
|
||||
namespace binance_execution::cross_margin {
|
||||
|
||||
bool CreateOrderRequest::init(
|
||||
const bts::orderman::CreateOrderMsg &m, std::string &error) {
|
||||
const std::size_t orig_error_sz = error.size();
|
||||
newClientOrderId = std::to_string(m.key.value());
|
||||
if (validate(m.security, bts::api::VENUE_BINANCE, error)) {
|
||||
symbol = m.security->binance().security_desc();
|
||||
}
|
||||
convert_side(m.side, side, error);
|
||||
price = m.price.value();
|
||||
quantity = m.quantity.full_value();
|
||||
convert_order_type(m.order_type, type, error);
|
||||
convert_time_in_force(m.time_in_force, timeInForce, error);
|
||||
if (error.size() != orig_error_sz) {
|
||||
return false;
|
||||
}
|
||||
timestamp = convert_time_stamp(bts::core::TimeStamp::now());
|
||||
return true;
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const CreateOrderRequest &req) {
|
||||
j["symbol"] = req.symbol;
|
||||
j["side"] = req.side;
|
||||
j["type"] = req.type;
|
||||
j["timestamp"] = req.timestamp;
|
||||
if (req.isIsolated) {
|
||||
j["isIsolated"] = req.isIsolated;
|
||||
}
|
||||
std::string str;
|
||||
if (req.quantity > 0) {
|
||||
quantity_to_string(req.quantity, str);
|
||||
j["quantity"] = str;
|
||||
str.clear();
|
||||
}
|
||||
if (req.quoteOrderQty > 0) {
|
||||
quantity_to_string(req.quoteOrderQty, str);
|
||||
j["quoteOrderQty"] = str;
|
||||
str.clear();
|
||||
}
|
||||
if (!std::isnan(req.price)) {
|
||||
price_to_string(req.price, str);
|
||||
j["price"] = str;
|
||||
str.clear();
|
||||
}
|
||||
if (!std::isnan(req.stopPrice)) {
|
||||
price_to_string(req.stopPrice, str);
|
||||
j["stopPrice"] = str;
|
||||
str.clear();
|
||||
}
|
||||
if (!req.newClientOrderId.empty()) {
|
||||
j["newClientOrderId"] = req.newClientOrderId;
|
||||
}
|
||||
if (req.icebergQty > 0) {
|
||||
quantity_to_string(req.icebergQty, str);
|
||||
j["icebergQty"] = str;
|
||||
str.clear();
|
||||
}
|
||||
if (req.newOrderRespType != NewOrderResponseType::NONE) {
|
||||
j["newOrderRespType"] = req.newOrderRespType;
|
||||
}
|
||||
if (req.sideEffectType != SideEffectType::NO_SIDE_EFFECT) {
|
||||
j["sideEffectType"] = req.sideEffectType;
|
||||
}
|
||||
if (req.timeInForce != TimeInForce::NONE) {
|
||||
j["timeInForce"] = req.timeInForce;
|
||||
}
|
||||
if (req.selfTradePreventionMode != SelfTradePreventionMode::NONE) {
|
||||
j["selfTradePreventionMode"] = req.selfTradePreventionMode;
|
||||
}
|
||||
if (req.autoRepayAtCancel) {
|
||||
j["autoRepayAtCancel"] = req.autoRepayAtCancel;
|
||||
}
|
||||
if (req.recvWindow > 0) {
|
||||
j["recvWindow"] = std::to_string(req.recvWindow);
|
||||
}
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, CreateOrderRequest &req) {
|
||||
j.at("symbol").get_to(req.symbol);
|
||||
j.at("side").get_to(req.side);
|
||||
j.at("type").get_to(req.type);
|
||||
j.at("timestamp").get_to(req.timestamp);
|
||||
auto it = j.find("isIsolated");
|
||||
bts::io::JsonReader jr{&j};
|
||||
if (it != j.end()) {
|
||||
it->get_to(req.isIsolated);
|
||||
}
|
||||
jr.read("quantity", req.price);
|
||||
jr.read("quoteOrderQty", req.price);
|
||||
jr.read("price", req.price);
|
||||
jr.read("stopPrice", req.price);
|
||||
it = j.find("newClientOrderId");
|
||||
if (it != j.end()) {
|
||||
it->get_to(req.newClientOrderId);
|
||||
}
|
||||
jr.read("icebergQty", req.price);
|
||||
if (it != j.end()) {
|
||||
it->get_to(req.icebergQty);
|
||||
}
|
||||
it = j.find("newOrderRespType");
|
||||
if (it != j.end()) {
|
||||
it->get_to(req.newOrderRespType);
|
||||
}
|
||||
it = j.find("sideEffectType");
|
||||
if (it != j.end()) {
|
||||
it->get_to(req.sideEffectType);
|
||||
}
|
||||
it = j.find("timeInForce");
|
||||
if (it != j.end()) {
|
||||
it->get_to(req.timeInForce);
|
||||
}
|
||||
it = j.find("selfTradePreventionMode");
|
||||
if (it != j.end()) {
|
||||
it->get_to(req.selfTradePreventionMode);
|
||||
}
|
||||
it = j.find("isIsolated");
|
||||
if (it != j.end()) {
|
||||
it->get_to(req.isIsolated);
|
||||
}
|
||||
it = j.find("autoRepayAtCancel");
|
||||
if (it != j.end()) {
|
||||
it->get_to(req.autoRepayAtCancel);
|
||||
}
|
||||
it = j.find("recvWindow");
|
||||
if (it != j.end()) {
|
||||
it->get_to(req.recvWindow);
|
||||
}
|
||||
}
|
||||
|
||||
void to_query_params(std::string &qps, const CreateOrderRequest &req) {
|
||||
qps.append("symbol=");
|
||||
qps.append(req.symbol);
|
||||
qps.append("&side=");
|
||||
qps.append(req.side.name());
|
||||
qps.append("&type=");
|
||||
qps.append(req.type.name());
|
||||
qps.append("×tamp=");
|
||||
qps.append(std::to_string(req.timestamp));
|
||||
if (req.isIsolated) {
|
||||
qps.append("&isIsolated=true");
|
||||
}
|
||||
if (req.quantity > 0) {
|
||||
qps.append("&quantity=");
|
||||
quantity_to_string(req.quantity, qps);
|
||||
}
|
||||
if (req.quoteOrderQty > 0) {
|
||||
qps.append(""eOrderQty=");
|
||||
quantity_to_string(req.quoteOrderQty, qps);
|
||||
}
|
||||
if (!std::isnan(req.price)) {
|
||||
qps.append("&price=");
|
||||
price_to_string(req.price, qps);
|
||||
}
|
||||
if (!std::isnan(req.stopPrice)) {
|
||||
qps.append("&stopPrice=");
|
||||
price_to_string(req.stopPrice, qps);
|
||||
}
|
||||
if (!req.newClientOrderId.empty()) {
|
||||
qps.append("&newClientOrderId=");
|
||||
qps.append(req.newClientOrderId);
|
||||
}
|
||||
if (req.icebergQty > 0) {
|
||||
qps.append("&icebergQty=");
|
||||
quantity_to_string(req.icebergQty, qps);
|
||||
}
|
||||
if (req.newOrderRespType != NewOrderResponseType::NONE) {
|
||||
qps.append("&newOrderRespType=");
|
||||
qps.append(req.newOrderRespType.name());
|
||||
}
|
||||
if (req.sideEffectType != SideEffectType::NO_SIDE_EFFECT) {
|
||||
qps.append("&sideEffectType=");
|
||||
qps.append(req.sideEffectType.name());
|
||||
}
|
||||
if (req.timeInForce != TimeInForce::NONE) {
|
||||
qps.append("&timeInForce=");
|
||||
qps.append(req.timeInForce.name());
|
||||
}
|
||||
if (req.selfTradePreventionMode != SelfTradePreventionMode::NONE) {
|
||||
qps.append("&selfTradePreventionMode=");
|
||||
qps.append(req.selfTradePreventionMode.name());
|
||||
}
|
||||
if (req.autoRepayAtCancel) {
|
||||
qps.append("&autoRepayAtCancel=true");
|
||||
}
|
||||
if (req.recvWindow > 0) {
|
||||
qps.append("&recvWindow=");
|
||||
qps.append(std::to_string(req.recvWindow));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace binance_execution::cross_margin
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/cross_margin/side_effect_type.h"
|
||||
#include "binance_execution/enums/new_order_response_type.h"
|
||||
#include "binance_execution/enums/order_type.h"
|
||||
#include "binance_execution/enums/self_trade_prevention_mode.h"
|
||||
#include "binance_execution/enums/side.h"
|
||||
#include "binance_execution/enums/time_in_force.h"
|
||||
|
||||
#include <limits>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace bts::orderman {
|
||||
struct CreateOrderMsg;
|
||||
}
|
||||
|
||||
namespace binance_execution::cross_margin {
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/margin_trading/trade/Margin-Account-New-Order
|
||||
*/
|
||||
struct CreateOrderRequest {
|
||||
std::string symbol;
|
||||
bool isIsolated = false;
|
||||
Side side;
|
||||
OrderType type;
|
||||
double quantity = std::numeric_limits<double>::quiet_NaN();
|
||||
double quoteOrderQty = std::numeric_limits<double>::quiet_NaN();
|
||||
double price = std::numeric_limits<double>::quiet_NaN();
|
||||
double stopPrice = std::numeric_limits<double>::quiet_NaN();
|
||||
std::string newClientOrderId;
|
||||
double icebergQty = std::numeric_limits<double>::quiet_NaN();
|
||||
NewOrderResponseType newOrderRespType = NewOrderResponseType::NONE;
|
||||
SideEffectType sideEffectType = SideEffectType::NO_SIDE_EFFECT;
|
||||
TimeInForce timeInForce = TimeInForce::NONE;
|
||||
SelfTradePreventionMode selfTradePreventionMode =
|
||||
SelfTradePreventionMode::NONE;
|
||||
bool autoRepayAtCancel = false;
|
||||
std::int64_t timestamp = 0;
|
||||
std::int64_t recvWindow = 0;
|
||||
|
||||
bool init(const bts::orderman::CreateOrderMsg &m, std::string &error);
|
||||
};
|
||||
|
||||
void to_json(nlohmann::json &j, const CreateOrderRequest &req);
|
||||
void from_json(const nlohmann::json &j, CreateOrderRequest &req);
|
||||
|
||||
void to_query_params(std::string &query_params, const CreateOrderRequest &req);
|
||||
|
||||
} // namespace binance_execution::cross_margin
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
#include "create_order_response.h"
|
||||
#include "binance_execution/enums/self_trade_prevention_mode.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace binance_execution::cross_margin {
|
||||
|
||||
void to_json(nlohmann::json &j, const CreateOrderResponse &res) {
|
||||
j["symbol"] = res.symbol;
|
||||
j["orderId"] = res.orderId;
|
||||
j["clientOrderId"] = res.clientOrderId;
|
||||
j["transactTime"] = res.transactTime;
|
||||
if (!std::isnan(res.price)) {
|
||||
j["price"] = res.price;
|
||||
}
|
||||
if (!std::isnan(res.origQty)) {
|
||||
j["origQty"] = res.origQty;
|
||||
}
|
||||
if (!std::isnan(res.executedQty)) {
|
||||
j["executedQty"] = res.executedQty;
|
||||
}
|
||||
if (!std::isnan(res.cummulativeQuoteQty)) {
|
||||
j["cummulativeQuoteQty"] = res.cummulativeQuoteQty;
|
||||
}
|
||||
j["status"] = res.status;
|
||||
j["timeInForce"] = res.timeInForce;
|
||||
j["type"] = res.type;
|
||||
j["side"] = res.side;
|
||||
if (!std::isnan(res.marginBuyBorrowAmount)) {
|
||||
j["marginBuyPowerAmount"] = res.marginBuyBorrowAmount;
|
||||
}
|
||||
if (!res.marginBuyBorrowAsset.empty()) {
|
||||
j["marginBuyBorrowAsset"] = res.marginBuyBorrowAsset;
|
||||
}
|
||||
if (res.isIsolated) {
|
||||
j["isIsolated"] = true;
|
||||
}
|
||||
if (res.selfTradePreventionMode != SelfTradePreventionMode::NONE) {
|
||||
j["selfTradePreventionMode"] = res.selfTradePreventionMode;
|
||||
}
|
||||
if (!res.fills.empty()) {
|
||||
j["fills"] = res.fills;
|
||||
}
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, CreateOrderResponse &res) {
|
||||
const bts::io::JsonReader jr{&j};
|
||||
j.at("symbol").get_to(res.symbol);
|
||||
j.at("orderId").get_to(res.orderId);
|
||||
j.at("clientOrderId").get_to(res.clientOrderId);
|
||||
j.at("transactTime").get_to(res.transactTime);
|
||||
jr.read_or("price", res.price, std::numeric_limits<double>::quiet_NaN());
|
||||
jr.read_or("origQty", res.origQty, std::numeric_limits<double>::quiet_NaN());
|
||||
jr.read_or(
|
||||
"executedQty", res.executedQty,
|
||||
std::numeric_limits<double>::quiet_NaN());
|
||||
jr.read_or(
|
||||
"cummulativeQuoteQty", res.cummulativeQuoteQty,
|
||||
std::numeric_limits<double>::quiet_NaN());
|
||||
j.at("status").get_to(res.status);
|
||||
j.at("timeInForce").get_to(res.timeInForce);
|
||||
j.at("type").get_to(res.type);
|
||||
j.at("side").get_to(res.side);
|
||||
jr.read_or(
|
||||
"marginBuyBorrowAmount", res.marginBuyBorrowAmount,
|
||||
std::numeric_limits<double>::quiet_NaN());
|
||||
jr.read("marginBuyBorrowAsset", res.marginBuyBorrowAsset);
|
||||
jr.read("isIsolated", res.isIsolated);
|
||||
jr.read("selfTradePreventionMode", res.selfTradePreventionMode);
|
||||
jr.read_or("fills", res.fills);
|
||||
}
|
||||
|
||||
} // namespace binance_execution::cross_margin
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/enums/order_status.h"
|
||||
#include "binance_execution/enums/order_type.h"
|
||||
#include "binance_execution/enums/self_trade_prevention_mode.h"
|
||||
#include "binance_execution/enums/side.h"
|
||||
#include "binance_execution/enums/time_in_force.h"
|
||||
#include "lib/io/JsonReader.h"
|
||||
|
||||
#include <limits>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution::cross_margin {
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/margin_trading/trade/Margin-Account-New-Order
|
||||
* @a https://developers.binance.com/docs/margin_trading/trade/Margin-Account-New-Order#response-example
|
||||
*/
|
||||
struct CreateOrderResponse {
|
||||
struct Fill {
|
||||
double price = std::numeric_limits<double>::quiet_NaN();
|
||||
double qty = std::numeric_limits<double>::quiet_NaN();
|
||||
double commission = std::numeric_limits<double>::quiet_NaN();
|
||||
std::string commissionAsset;
|
||||
std::uint32_t tradeId = 0;
|
||||
};
|
||||
|
||||
std::string symbol;
|
||||
std::int64_t orderId = 0;
|
||||
std::string clientOrderId;
|
||||
std::int64_t transactTime = 0;
|
||||
double price = std::numeric_limits<double>::quiet_NaN();
|
||||
double origQty = std::numeric_limits<double>::quiet_NaN();
|
||||
double executedQty = std::numeric_limits<double>::quiet_NaN();
|
||||
// Note: 'cummulative' is deliberately mispelled to match the json payload.
|
||||
double cummulativeQuoteQty = std::numeric_limits<double>::quiet_NaN();
|
||||
OrderStatus status = OrderStatus::NONE;
|
||||
TimeInForce timeInForce = TimeInForce::NONE;
|
||||
OrderType type = OrderType::NONE;
|
||||
Side side = Side::NONE;
|
||||
double marginBuyBorrowAmount = std::numeric_limits<double>::quiet_NaN();
|
||||
std::string marginBuyBorrowAsset;
|
||||
bool isIsolated = false;
|
||||
SelfTradePreventionMode selfTradePreventionMode =
|
||||
SelfTradePreventionMode::NONE;
|
||||
std::vector<Fill> fills;
|
||||
};
|
||||
|
||||
void to_json(nlohmann::json &j, const CreateOrderResponse &res);
|
||||
void from_json(const nlohmann::json &j, CreateOrderResponse &res);
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// Definition
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
inline void to_json(nlohmann::json &j, const CreateOrderResponse::Fill &fill) {
|
||||
j["price"] = fill.price;
|
||||
j["qty"] = fill.qty;
|
||||
j["commission"] = fill.commission;
|
||||
j["commissionAsset"] = fill.commissionAsset;
|
||||
j["tradeId"] = fill.tradeId;
|
||||
}
|
||||
|
||||
inline void from_json(
|
||||
const nlohmann::json &j, CreateOrderResponse::Fill &fill) {
|
||||
bts::io::JsonReader jr{&j};
|
||||
jr.read("price", fill.price);
|
||||
jr.read("qty", fill.qty);
|
||||
jr.read("commission", fill.commission);
|
||||
jr.read("commissionAsset", fill.commissionAsset);
|
||||
jr.read("tradeId", fill.tradeId);
|
||||
}
|
||||
|
||||
} // namespace binance_execution::cross_margin
|
||||
@@ -0,0 +1,112 @@
|
||||
#include "rest_api_client.h"
|
||||
#include "lib/io/http/http_client.h"
|
||||
#include "lib/io/http/http_response.h"
|
||||
#include "lib/utils/crypto/hmac.h"
|
||||
|
||||
namespace binance_execution::cross_margin {
|
||||
|
||||
using bts::http::HttpClient;
|
||||
using bts::http::HttpResponse;
|
||||
using bts::system::crypto::hmac_digest_hex;
|
||||
using std::string;
|
||||
|
||||
#define _LOG_FN(level, fn, ...) \
|
||||
LOG(OrderMan, level, "Binance::" fn ". " __VA_ARGS__)
|
||||
#define DIAG_FN(fn, ...) _LOG_FN(bts::Diag, fn, __VA_ARGS__)
|
||||
#define INFO_FN(fn, ...) _LOG_FN(bts::Info, fn, __VA_ARGS__)
|
||||
#define WARN_FN(fn, ...) _LOG_FN(bts::Warn, fn, __VA_ARGS__)
|
||||
#define ERROR_FN(fn, ...) _LOG_FN(bts::Error, fn, __VA_ARGS__)
|
||||
|
||||
template <std::size_t MAX_SIZE, std::size_t DEFAULT_SIZE>
|
||||
static void shrink_string(std::string &str) {
|
||||
if (str.size() > MAX_SIZE) {
|
||||
str.resize(DEFAULT_SIZE);
|
||||
str.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
|
||||
template <class T, std::size_t MAX_SIZE, std::size_t DEFAULT_SIZE>
|
||||
static void shrink_vec(std::vector<T> &vec) {
|
||||
if (vec.size() > MAX_SIZE) {
|
||||
vec.resize(DEFAULT_SIZE);
|
||||
vec.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
|
||||
bool RestApiClient::sign_query_params_(std::string &qp) {
|
||||
std::string signature;
|
||||
if (!hmac_digest_hex(secret_key_, qp, signature)) {
|
||||
LOG(OrderMan, bts::Error, "Failed to sign request. {}", qp);
|
||||
return false;
|
||||
}
|
||||
static constexpr const std::string SIGNATURE_LABEL = "&signature=";
|
||||
qp.reserve(qp.size() + SIGNATURE_LABEL.size() + signature.size());
|
||||
qp.append(SIGNATURE_LABEL);
|
||||
qp.append(signature);
|
||||
return true;
|
||||
}
|
||||
|
||||
template <class ResponseT, class RequestT>
|
||||
void RestApiClient::request_(
|
||||
const RequestT &req,
|
||||
const std::string &path,
|
||||
bts::io::http::HttpMethod method,
|
||||
const std::string &body,
|
||||
RestApiResult<ResponseT> &res) {
|
||||
// Use thread_local vars to reduce memory allocations. Shrink strings
|
||||
// to reduce memory footprint from previous requests.
|
||||
thread_local bts::http::HttpRequest http_req;
|
||||
shrink_string<4096, 128>(http_req.body);
|
||||
http_req.reset();
|
||||
// Prepare request
|
||||
http_req.url = url_;
|
||||
http_req.url.append(path);
|
||||
http_req.body = body;
|
||||
http_req.method = method;
|
||||
http_req.headers["X-MBX-APIKEY"] = api_key_;
|
||||
thread_local std::string qps;
|
||||
shrink_string<256, 16>(qps);
|
||||
qps.clear();
|
||||
to_query_params(qps, req);
|
||||
if (!qps.empty()) {
|
||||
sign_query_params_(qps);
|
||||
http_req.url.append("?");
|
||||
http_req.url.append(qps);
|
||||
}
|
||||
// Send it
|
||||
thread_local HttpResponse http_res;
|
||||
shrink_string<4096, 128>(http_res.body);
|
||||
http_res.reset();
|
||||
HttpClient::send(http_req, &http_res);
|
||||
res.http_code = http_res.code;
|
||||
res.http_error = http_res.errors;
|
||||
if (!http_res.ok()) {
|
||||
ERROR_FN("{} {}", "res: {}", method, path, http_res);
|
||||
try {
|
||||
nlohmann::json::parse(http_res.body).get_to(res.error.emplace());
|
||||
} catch (const std::exception &ex) {
|
||||
ERROR_FN("request", "Failed to parse error json. {}", http_res);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Parse response
|
||||
INFO_FN("{} {}", "res: {}", method, path, http_res);
|
||||
try {
|
||||
nlohmann::json::parse(http_res.body).get_to(res.response.emplace());
|
||||
} catch (const std::exception &ex) {
|
||||
ERROR_FN("{} {}", "ex: {}", method, path, string{ex.what()});
|
||||
bts_assert_msgf(false, "Failed to parse {} response", path);
|
||||
}
|
||||
}
|
||||
|
||||
#define REQUEST_DEF(ResponseT, RequestT) \
|
||||
template void RestApiClient::request_<ResponseT, RequestT>( \
|
||||
const RequestT &req, const std::string &path, \
|
||||
bts::io::http::HttpMethod method, const std::string &body, \
|
||||
RestApiResult<ResponseT> &res)
|
||||
|
||||
REQUEST_DEF(CreateOrderResponse, CreateOrderRequest);
|
||||
REQUEST_DEF(CancelOrderResponse, CancelOrderRequest);
|
||||
REQUEST_DEF(ListenKeyResponse, ListenKeyRequest);
|
||||
|
||||
} // namespace binance_execution::cross_margin
|
||||
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/cross_margin/messages/cancel_order_request.h"
|
||||
#include "binance_execution/cross_margin/messages/cancel_order_response.h"
|
||||
#include "binance_execution/cross_margin/messages/create_order_request.h"
|
||||
#include "binance_execution/cross_margin/messages/create_order_response.h"
|
||||
#include "binance_execution/messages/listen_key_request.h"
|
||||
#include "binance_execution/messages/listen_key_response.h"
|
||||
#include "binance_execution/rest_api_result.h"
|
||||
#include "lib/io/http/HttpMethod.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace binance_execution::cross_margin {
|
||||
|
||||
class RestApiClient {
|
||||
public:
|
||||
explicit RestApiClient(
|
||||
std::string url, std::string api_key, std::string secret_key);
|
||||
|
||||
const RestApiResult<CreateOrderResponse> &create_order(
|
||||
const CreateOrderRequest &req);
|
||||
const RestApiResult<CancelOrderResponse> &cancel_order(
|
||||
const CancelOrderRequest &req);
|
||||
const RestApiResult<ListenKeyResponse> &get_listen_key(
|
||||
const ListenKeyRequest &req = {});
|
||||
const RestApiResult<ListenKeyResponse> &refresh_listen_key(
|
||||
const ListenKeyRequest &req = {});
|
||||
|
||||
private:
|
||||
std::string url_;
|
||||
std::string api_key_;
|
||||
std::string secret_key_;
|
||||
|
||||
template <class ResponseT, class RequestT>
|
||||
void request_(
|
||||
const RequestT &req,
|
||||
const std::string &path,
|
||||
bts::io::http::HttpMethod method,
|
||||
const std::string &body,
|
||||
RestApiResult<ResponseT> &op_res);
|
||||
|
||||
bool sign_query_params_(std::string &qp);
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// Definition
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
inline RestApiClient::RestApiClient(
|
||||
std::string url, std::string api_key, std::string secret_key)
|
||||
: url_(std::move(url)), api_key_(std::move(api_key)),
|
||||
secret_key_(std::move(secret_key)) {}
|
||||
|
||||
inline const RestApiResult<CreateOrderResponse> &RestApiClient::create_order(
|
||||
const CreateOrderRequest &req) {
|
||||
static const std::string path{"/sapi/v1/margin/order"};
|
||||
thread_local RestApiResult<CreateOrderResponse> res;
|
||||
res.reset();
|
||||
request_<CreateOrderResponse>(
|
||||
req, path, bts::io::http::HttpMethod::POST, "", res);
|
||||
return res;
|
||||
}
|
||||
|
||||
inline const RestApiResult<CancelOrderResponse> &RestApiClient::cancel_order(
|
||||
const CancelOrderRequest &req) {
|
||||
static const std::string path{"/sapi/v1/margin/order"};
|
||||
thread_local RestApiResult<CancelOrderResponse> res;
|
||||
res.reset();
|
||||
request_<CancelOrderResponse>(
|
||||
req, path, bts::io::http::HttpMethod::DELETE, "", res);
|
||||
return res;
|
||||
}
|
||||
|
||||
inline const RestApiResult<ListenKeyResponse> &RestApiClient::get_listen_key(
|
||||
const ListenKeyRequest &req) {
|
||||
static const std::string path{"/sapi/v1/userDataStream"};
|
||||
thread_local RestApiResult<ListenKeyResponse> res;
|
||||
res.reset();
|
||||
request_(req, path, bts::io::http::HttpMethod::POST, "", res);
|
||||
return res;
|
||||
}
|
||||
|
||||
inline const RestApiResult<ListenKeyResponse> &
|
||||
RestApiClient::refresh_listen_key(const ListenKeyRequest &req) {
|
||||
static const std::string path{"/sapi/v1/userDataStream"};
|
||||
thread_local RestApiResult<ListenKeyResponse> res;
|
||||
res.reset();
|
||||
request_(req, path, bts::io::http::HttpMethod::PUT, "", res);
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace binance_execution::cross_margin
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/core/EnumMacros.h"
|
||||
#include "lib/core/FMT_IMPL.h"
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace binance_execution::cross_margin {
|
||||
|
||||
// clang-format off
|
||||
#define FOR_ALL_VALUES(per_value) \
|
||||
per_value(SideEffectType, NO_SIDE_EFFECT) \
|
||||
per_value(SideEffectType, MARGIN_BUY) \
|
||||
per_value(SideEffectType, AUTO_REPAY) \
|
||||
per_value(SideEffectType, AUTO_BORROW_REPAY)
|
||||
// clang-format on
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/margin_trading/trade/Margin-Account-New-OCO
|
||||
*/
|
||||
ENUM_CLASS_DEF(SideEffectType, FOR_ALL_VALUES)
|
||||
|
||||
#undef FOR_ALL_VALUES
|
||||
|
||||
} // namespace binance_execution::cross_margin
|
||||
|
||||
FMT_IMPL(binance_execution::cross_margin::SideEffectType, t, "{}", t.name());
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/cross_margin/trade_data_stream/event_type.h"
|
||||
#include "lib/io/JsonReader.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution::cross_margin::trade_data_stream {
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/margin_trading/trade-data-stream/Event-Account-Update
|
||||
*/
|
||||
struct AccountUpdate {
|
||||
struct Balance {
|
||||
std::string asset; // json: a
|
||||
double free; // json: f
|
||||
double locked; // json: l
|
||||
};
|
||||
|
||||
EventType event_type; // json: e
|
||||
std::int64_t event_time; // json: E
|
||||
std::int64_t last_update_time; // json: u
|
||||
std::vector<Balance> balances; // json: B
|
||||
};
|
||||
|
||||
void to_json(nlohmann::json &j, const AccountUpdate::Balance &b);
|
||||
void from_json(const nlohmann::json &j, AccountUpdate::Balance &b);
|
||||
|
||||
void to_json(nlohmann::json &j, const AccountUpdate &u);
|
||||
void from_json(const nlohmann::json &j, AccountUpdate &u);
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// Definition
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
inline void to_json(nlohmann::json &j, const AccountUpdate::Balance &b) {
|
||||
j["a"] = b.asset;
|
||||
j["f"] = b.free;
|
||||
j["l"] = b.locked;
|
||||
}
|
||||
|
||||
inline void from_json(const nlohmann::json &j, AccountUpdate::Balance &b) {
|
||||
bts::io::JsonReader jr{&j};
|
||||
j.at("a").get_to(b.asset);
|
||||
jr.read("f", b.free);
|
||||
jr.read("l", b.locked);
|
||||
}
|
||||
|
||||
inline void to_json(nlohmann::json &j, const AccountUpdate &u) {
|
||||
j["e"] = u.event_type;
|
||||
j["E"] = u.event_time;
|
||||
j["u"] = u.last_update_time;
|
||||
j["B"] = u.balances;
|
||||
}
|
||||
|
||||
inline void from_json(const nlohmann::json &j, AccountUpdate &u) {
|
||||
j.at("e").get_to(u.event_type);
|
||||
j.at("E").get_to(u.event_time);
|
||||
j.at("u").get_to(u.last_update_time);
|
||||
j.at("B").get_to(u.balances);
|
||||
};
|
||||
|
||||
} // namespace binance_execution::cross_margin::trade_data_stream
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/cross_margin/trade_data_stream/event_type.h"
|
||||
#include "lib/io/JsonReader.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution::cross_margin::trade_data_stream {
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/binance-spot-api-docs/user-data-stream#account-update
|
||||
*/
|
||||
struct BalanceUpdate {
|
||||
EventType event_type; // json: e
|
||||
std::int64_t event_time; // json: E
|
||||
std::string asset; // json: a
|
||||
double balance_delta; // json: d
|
||||
std::int64_t clear_time; // json: T
|
||||
};
|
||||
|
||||
void to_json(nlohmann::json &j, const BalanceUpdate &b);
|
||||
void from_json(const nlohmann::json &j, BalanceUpdate &b);
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// Definition
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
inline void to_json(nlohmann::json &j, const BalanceUpdate &b) {
|
||||
j["e"] = b.event_type;
|
||||
j["E"] = b.event_time;
|
||||
j["a"] = b.asset;
|
||||
j["d"] = b.balance_delta;
|
||||
j["T"] = b.clear_time;
|
||||
}
|
||||
|
||||
inline void from_json(const nlohmann::json &j, BalanceUpdate &b) {
|
||||
j.at("e").get_to(b.event_type);
|
||||
j.at("E").get_to(b.event_time);
|
||||
j.at("a").get_to(b.asset);
|
||||
bts::io::JsonReader jr{&j};
|
||||
jr.read("d", b.balance_delta);
|
||||
j.at("T").get_to(b.clear_time);
|
||||
}
|
||||
|
||||
} // namespace binance_execution::cross_margin::trade_data_stream
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/core/EnumMacros.h"
|
||||
#include "lib/core/FMT_IMPL.h"
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace binance_execution::cross_margin::trade_data_stream {
|
||||
|
||||
// clang-format off
|
||||
#define FOR_ALL_VALUES(per_value) \
|
||||
per_value(EventType, NONE) \
|
||||
per_value(EventType, OUTBOUND_ACCOUNT_POSITION) \
|
||||
per_value(EventType, BALANCE_UPDATE) \
|
||||
per_value(EventType, ORDER_UPDATE) \
|
||||
per_value(EventType, LIST_STATUS) \
|
||||
per_value(EventType, LISTEN_KEY_EXPIRED)
|
||||
// clang-format on
|
||||
|
||||
/**
|
||||
* @brief Represents the different Event Types for user data streams at Binance
|
||||
*
|
||||
* @a https://developers.binance.com/docs/binance-spot-api-docs/user-data-stream#web-socket-payloads
|
||||
*/
|
||||
ENUM_CLASS_DEF_NO_JSON(EventType, FOR_ALL_VALUES)
|
||||
|
||||
inline void from_json(const nlohmann ::json &j, EventType &value) {
|
||||
std::string value_str;
|
||||
j.get_to(value_str);
|
||||
if (value_str == "outboundAccountPosition") {
|
||||
value = EventType::OUTBOUND_ACCOUNT_POSITION;
|
||||
} else if (value_str == "balanceUpdate") {
|
||||
value = EventType::BALANCE_UPDATE;
|
||||
} else if (value_str == "executionReport") {
|
||||
value = EventType::ORDER_UPDATE;
|
||||
} else if (value_str == "listStatus") {
|
||||
value = EventType::LIST_STATUS;
|
||||
} else if (value_str == "listenKeyExpired") {
|
||||
value = EventType::LISTEN_KEY_EXPIRED;
|
||||
} else {
|
||||
EventType::parse(value_str, &value);
|
||||
}
|
||||
}
|
||||
|
||||
inline void to_json(nlohmann::json &j, const EventType &value) {
|
||||
switch (value.value) {
|
||||
case EventType::OUTBOUND_ACCOUNT_POSITION:
|
||||
j = "outboundAccountPosition";
|
||||
break;
|
||||
case EventType::BALANCE_UPDATE:
|
||||
j = "balanceUpdate";
|
||||
break;
|
||||
case EventType::ORDER_UPDATE:
|
||||
j = "executionReport";
|
||||
break;
|
||||
case EventType::LIST_STATUS:
|
||||
j = "listStatus";
|
||||
break;
|
||||
case EventType::LISTEN_KEY_EXPIRED:
|
||||
j = "listenKeyExpired";
|
||||
break;
|
||||
default:
|
||||
j = value.name();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#undef FOR_ALL_VALUES
|
||||
|
||||
} // namespace binance_execution::cross_margin::trade_data_stream
|
||||
|
||||
FMT_IMPL(
|
||||
binance_execution::cross_margin::trade_data_stream::EventType,
|
||||
t,
|
||||
"{}",
|
||||
t.name());
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
#include "order_update.h"
|
||||
#include "binance_execution/type_converters.h"
|
||||
#include "lib/config/protos/jetfix.pb.h"
|
||||
#include "lib/domain_model/Security.pb.h"
|
||||
#include "lib/io/JsonReader.h"
|
||||
#include "lib/orderman/orderman_utils/ExecutionReport.h"
|
||||
#include "lib/utils/checked_arithmetic.h"
|
||||
#include "lib/utils/string_utils.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution::cross_margin::trade_data_stream {
|
||||
|
||||
using bts::system::math::checked_ltoul;
|
||||
using bts::system::string_utils::copy_to_array;
|
||||
using bts::system::string_utils::parse_uint;
|
||||
|
||||
void to_json(nlohmann::json &j, const OrderUpdate &u) {
|
||||
j["e"] = u.event_type;
|
||||
j["E"] = u.event_time;
|
||||
j["s"] = u.symbol;
|
||||
j["c"] = u.client_order_id;
|
||||
j["S"] = u.side;
|
||||
j["o"] = u.order_type;
|
||||
j["f"] = u.time_in_force;
|
||||
j["q"] = u.order_quantity;
|
||||
if (!std::isnan(u.order_price)) {
|
||||
j["p"] = u.order_price;
|
||||
}
|
||||
if (!std::isnan(u.stop_price)) {
|
||||
j["P"] = u.stop_price;
|
||||
}
|
||||
if (!std::isnan(u.iceberg_quantity)) {
|
||||
j["F"] = u.iceberg_quantity;
|
||||
}
|
||||
j["g"] = u.order_list_id;
|
||||
if (!u.orig_client_order_id.empty()) {
|
||||
j["C"] = u.orig_client_order_id;
|
||||
}
|
||||
j["x"] = u.execution_type;
|
||||
j["X"] = u.order_status;
|
||||
j["r"] = u.reject_reason;
|
||||
j["i"] = u.order_id;
|
||||
j["l"] = u.last_executed_quantity;
|
||||
j["z"] = u.filled_quantity;
|
||||
j["L"] = u.last_executed_price;
|
||||
j["n"] = u.commission_amount;
|
||||
auto &commission_asset_j = j["N"];
|
||||
if (u.commission_asset) {
|
||||
commission_asset_j = *u.commission_asset;
|
||||
} else {
|
||||
commission_asset_j = nlohmann::json{};
|
||||
}
|
||||
j["T"] = u.transaction_time;
|
||||
j["t"] = u.trade_id;
|
||||
if (u.prevented_match_id) {
|
||||
j["v"] = *u.prevented_match_id;
|
||||
}
|
||||
j["I"] = u.execution_id;
|
||||
j["w"] = u.is_working;
|
||||
j["m"] = u.is_maker_side;
|
||||
j["O"] = u.order_creation_time;
|
||||
j["Z"] = u.quote_asset_transacted_quantity;
|
||||
j["Y"] = u.last_quote_asset_transacted_quantity;
|
||||
j["Q"] = u.quote_order_quantity;
|
||||
if (u.working_time) {
|
||||
j["W"] = *u.working_time;
|
||||
}
|
||||
j["V"] = u.self_trade_prevention;
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, OrderUpdate &u) {
|
||||
j.at("e").get_to(u.event_type);
|
||||
j.at("E").get_to(u.event_time);
|
||||
j.at("s").get_to(u.symbol);
|
||||
j.at("c").get_to(u.client_order_id);
|
||||
j.at("S").get_to(u.side);
|
||||
j.at("o").get_to(u.order_type);
|
||||
j.at("f").get_to(u.time_in_force);
|
||||
const bts::io::JsonReader jr{&j};
|
||||
jr.read("q", u.order_quantity);
|
||||
jr.read_or("p", u.order_price, std::numeric_limits<double>::quiet_NaN());
|
||||
jr.read_or("P", u.stop_price, std::numeric_limits<double>::quiet_NaN());
|
||||
jr.read_or(
|
||||
"F", u.iceberg_quantity, std::numeric_limits<double>::quiet_NaN());
|
||||
j.at("g").get_to(u.order_list_id);
|
||||
jr.read_or("C", u.orig_client_order_id);
|
||||
j.at("x").get_to(u.execution_type);
|
||||
j.at("X").get_to(u.order_status);
|
||||
j.at("r").get_to(u.reject_reason);
|
||||
j.at("i").get_to(u.order_id);
|
||||
jr.read("l", u.last_executed_quantity);
|
||||
jr.read("z", u.filled_quantity);
|
||||
jr.read("L", u.last_executed_price);
|
||||
jr.read("n", u.commission_amount);
|
||||
jr.read_optional_or("n", u.commission_asset);
|
||||
j.at("T").get_to(u.transaction_time);
|
||||
j.at("t").get_to(u.trade_id);
|
||||
jr.read_optional("v", u.prevented_match_id);
|
||||
j.at("I").get_to(u.execution_id);
|
||||
j.at("w").get_to(u.is_working);
|
||||
j.at("m").get_to(u.is_maker_side);
|
||||
j.at("O").get_to(u.order_creation_time);
|
||||
jr.read("Z", u.quote_asset_transacted_quantity);
|
||||
jr.read("Y", u.last_quote_asset_transacted_quantity);
|
||||
jr.read("Q", u.quote_order_quantity);
|
||||
jr.read_optional("W", u.working_time);
|
||||
j.at("V").get_to(u.self_trade_prevention);
|
||||
}
|
||||
|
||||
bool OrderUpdate::to_execution_report(
|
||||
bts::orderman::ExecutionReport &er, std::string &error) const {
|
||||
er.exchange = bts::api::VENUE_BINANCE;
|
||||
er.transact_time = bts::orderman::ExchangeTimeStamp64{
|
||||
convert_time_stamp(transaction_time).nanos()};
|
||||
if (!parse_uint(client_order_id, er.order_key.value())) {
|
||||
// For CancelReplace ops, we append a version number to the original key (separated by an
|
||||
// underscore). Extract the original key.
|
||||
const auto version_sep = client_order_id.find_last_of('_');
|
||||
if (version_sep != std::string::npos) {
|
||||
parse_uint(
|
||||
client_order_id.begin(), client_order_id.begin() + version_sep,
|
||||
er.order_key.value());
|
||||
} else {
|
||||
ErrorList::append(
|
||||
error,
|
||||
fmt::format(
|
||||
"failed to parse client order id '{}'", client_order_id));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bts_assert(checked_ltoul(
|
||||
order_id,
|
||||
er.exchange_order_id.emplace<bts::orderman::ExchangeOrderID64>()
|
||||
.value()));
|
||||
er.side = side == Side::BUY ? bts::api::SIDE_BUY : bts::api::SIDE_SELL;
|
||||
er.time_in_force = convert_time_in_force(time_in_force);
|
||||
er.order_type = convert_order_type(order_type);
|
||||
er.exec_type = convert_exec_type(execution_type, order_status);
|
||||
er.order_status = convert_order_status(order_status);
|
||||
er.manual_order_indicator = bts::api::orderman::MANUAL_ORDER_INDICATOR_NO;
|
||||
er.price.value() = order_price;
|
||||
er.quantity = order_quantity;
|
||||
er.filled_quantity = filled_quantity;
|
||||
er.last_shares = last_executed_quantity;
|
||||
er.last_price.value() = last_executed_price;
|
||||
er.avg_price.value() =
|
||||
filled_quantity > 0 ? quote_asset_transacted_quantity / filled_quantity
|
||||
: 0.0;
|
||||
er.stop_price.value() = stop_price;
|
||||
copy_to_array<40>(std::to_string(trade_id), er.exchange_fill_id.value());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace binance_execution::cross_margin::trade_data_stream
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/cross_margin/trade_data_stream/event_type.h"
|
||||
#include "binance_execution/enums/execution_type.h"
|
||||
#include "binance_execution/enums/order_status.h"
|
||||
#include "binance_execution/enums/order_type.h"
|
||||
#include "binance_execution/enums/self_trade_prevention_mode.h"
|
||||
#include "binance_execution/enums/side.h"
|
||||
#include "binance_execution/enums/time_in_force.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace bts::config {
|
||||
class JetFIX_Connection;
|
||||
}
|
||||
namespace bts::orderman {
|
||||
class ExecutionReport;
|
||||
}
|
||||
|
||||
namespace binance_execution::cross_margin::trade_data_stream {
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/margin_trading/trade-data-stream/Event-Order-Update
|
||||
*/
|
||||
struct OrderUpdate {
|
||||
EventType event_type; // json: e
|
||||
std::int64_t event_time; // json: E
|
||||
std::string symbol; // json: s
|
||||
std::string client_order_id; // json: c
|
||||
Side side; // json: S
|
||||
OrderType order_type; // json: o
|
||||
TimeInForce time_in_force; // json: f
|
||||
double order_quantity; // json: q
|
||||
double order_price; // json: p
|
||||
double stop_price; // json: P
|
||||
double iceberg_quantity; // json: F
|
||||
std::int64_t order_list_id; // json: g
|
||||
std::string orig_client_order_id; // json: C
|
||||
ExecutionType execution_type; // json: x
|
||||
OrderStatus order_status; // json: X
|
||||
std::string reject_reason; // json: r
|
||||
std::int64_t order_id; // json: i
|
||||
double last_executed_quantity; // json: l
|
||||
double filled_quantity; // json: z
|
||||
double last_executed_price; // json: L
|
||||
double commission_amount; // json: n
|
||||
std::optional<std::string> commission_asset; // json: N
|
||||
std::int64_t transaction_time; // json: T
|
||||
std::int64_t trade_id; // json: t
|
||||
std::optional<std::int64_t> prevented_match_id; // json: v
|
||||
std::int64_t execution_id; // json: I
|
||||
bool is_working; // json: w
|
||||
bool is_maker_side; // json: m
|
||||
std::int64_t order_creation_time; // json: O
|
||||
double quote_asset_transacted_quantity; // json: Z
|
||||
double last_quote_asset_transacted_quantity; // json: Y
|
||||
double quote_order_quantity; // json: Q
|
||||
std::optional<std::int64_t> working_time; // json: W
|
||||
SelfTradePreventionMode self_trade_prevention; // json: V
|
||||
|
||||
bool to_execution_report(
|
||||
bts::orderman::ExecutionReport &er, std::string &error) const;
|
||||
};
|
||||
|
||||
void to_json(nlohmann::json &j, const OrderUpdate &d);
|
||||
void from_json(const nlohmann::json &j, OrderUpdate &d);
|
||||
|
||||
} // namespace binance_execution::cross_margin::trade_data_stream
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
#include "trade_data_subscription.h"
|
||||
#include "binance_execution/cross_margin/trade_data_stream/account_update.h"
|
||||
#include "binance_execution/cross_margin/trade_data_stream/balance_update.h"
|
||||
#include "binance_execution/cross_margin/trade_data_stream/event_type.h"
|
||||
#include "binance_execution/cross_margin/trade_data_stream/order_update.h"
|
||||
#include "lib/core/TimeStamp.h"
|
||||
#include "lib/io/websocket/websocket_client.h"
|
||||
#include "lib/logging/logging.h"
|
||||
#include "lib/utils/type_name.h"
|
||||
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
namespace binance_execution::cross_margin::trade_data_stream {
|
||||
|
||||
#define _LOG_FN(level, fn, ...) \
|
||||
LOG(OrderMan, level, "Binance::" fn ". " __VA_ARGS__)
|
||||
#define DIAG_FN(fn, ...) _LOG_FN(bts::Diag, fn, __VA_ARGS__)
|
||||
#define INFO_FN(fn, ...) _LOG_FN(bts::Info, fn, __VA_ARGS__)
|
||||
#define WARN_FN(fn, ...) _LOG_FN(bts::Warn, fn, __VA_ARGS__)
|
||||
#define ERROR_FN(fn, ...) _LOG_FN(bts::Error, fn, __VA_ARGS__)
|
||||
|
||||
using bts::core::Delegate;
|
||||
using bts::core::TimeSpan;
|
||||
using bts::core::TimeStamp;
|
||||
using bts::websocket::WebsocketClient;
|
||||
using bts::websocket::WebsocketClientConfig;
|
||||
using nlohmann::json;
|
||||
using std::exception;
|
||||
using std::lock_guard;
|
||||
using std::mutex;
|
||||
using std::optional;
|
||||
using std::string;
|
||||
using std::thread;
|
||||
|
||||
struct TradeDataSubscription::PImpl {
|
||||
explicit PImpl(string url) : url(std::move(url)) {}
|
||||
~PImpl() { stop(); }
|
||||
|
||||
string url;
|
||||
mutex instance_mx;
|
||||
optional<WebsocketClient> ws_client;
|
||||
optional<thread> ws_run_thread;
|
||||
optional<thread> ws_ping_thread;
|
||||
|
||||
Delegate<void(trade_data_stream::AccountUpdate)> account_update_callback;
|
||||
Delegate<void(trade_data_stream::BalanceUpdate)> balance_update_callback;
|
||||
Delegate<void(trade_data_stream::OrderUpdate)> order_update_callback;
|
||||
|
||||
void start() {
|
||||
lock_guard<mutex> lock{instance_mx};
|
||||
INFO_FN("start", "Subscribing to user data");
|
||||
bts_assert(!ws_client);
|
||||
bts_assert(!ws_run_thread);
|
||||
bts_assert(!ws_ping_thread);
|
||||
LOG(OrderMan, bts::Info, "Creating websocket client. url: {}", url);
|
||||
WebsocketClientConfig ws_stream_client_cfg;
|
||||
ws_stream_client_cfg.url = url;
|
||||
auto &ws_cli = ws_client.emplace(ws_stream_client_cfg);
|
||||
ws_cli.message_handler().bind<&PImpl::on_message>(*this);
|
||||
ws_cli.close_handler().bind<&PImpl::on_close>(*this);
|
||||
INFO_FN("start", "Creating websocket client. url: {}", url);
|
||||
ws_cli.connect();
|
||||
ws_run_thread.emplace([&]() {
|
||||
DIAG_FN("ws_run_thread", "thread starting. url: {}", url);
|
||||
ws_cli.run();
|
||||
DIAG_FN("ws_run_thread", "thread exiting. url: {}", url);
|
||||
});
|
||||
ws_ping_thread.emplace([&]() {
|
||||
DIAG_FN("ws_ping_thread", "thread starting. url: {}", url);
|
||||
auto last_ping_time = TimeStamp::now();
|
||||
static constexpr const auto ping_interval = TimeSpan::from_seconds(25);
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(ping_interval.millis()));
|
||||
|
||||
while (ws_cli.is_connected()) {
|
||||
const auto now = bts::core::TimeStamp::now();
|
||||
if ((now - last_ping_time) > ping_interval) {
|
||||
DIAG_FN("ws_ping_thread", "Pinging websocket. url: {}", url);
|
||||
ws_cli.ping();
|
||||
last_ping_time = now;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
}
|
||||
DIAG_FN("ws_ping_thread", "thread exiting. url: {}", url);
|
||||
});
|
||||
}
|
||||
|
||||
void stop() {
|
||||
lock_guard<mutex> lock{instance_mx};
|
||||
if (ws_client) {
|
||||
bts_assert(ws_run_thread);
|
||||
bts_assert(ws_ping_thread);
|
||||
ws_client->disconnect();
|
||||
ws_ping_thread->join();
|
||||
ws_run_thread->join();
|
||||
ws_client.reset();
|
||||
}
|
||||
}
|
||||
|
||||
template <class T>
|
||||
bool on_data(const json &msg, Delegate<void(T)> &callback) {
|
||||
T data;
|
||||
try {
|
||||
msg.get_to(data);
|
||||
} catch (const std::exception &ex) {
|
||||
ERROR_FN(
|
||||
"on_data", "{} parse error: {}",
|
||||
string{bts::utils::cached_type_name<T>()}, string{ex.what()});
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
callback(std::move(data));
|
||||
} catch (const std::exception &ex) {
|
||||
ERROR_FN(
|
||||
"on_data", "{} callback threw: {}",
|
||||
string{bts::utils::cached_type_name<T>()}, string{ex.what()});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void on_message(const string &msg_str) {
|
||||
DIAG_FN("on_message", "{}", msg_str);
|
||||
json msg;
|
||||
try {
|
||||
msg = json::parse(msg_str);
|
||||
} catch (const exception &ex) {
|
||||
ERROR_FN(
|
||||
"on_message", "Failed to parse response json: {}.\n{}",
|
||||
string{ex.what()}, msg);
|
||||
return;
|
||||
}
|
||||
const auto event_type_it = msg.find("e");
|
||||
if (event_type_it == msg.end()) {
|
||||
ERROR_FN("on_message", "Failed to process response json: {}", msg_str);
|
||||
return;
|
||||
}
|
||||
const auto event_type = event_type_it->get<EventType>();
|
||||
switch (event_type.value) {
|
||||
case EventType::ORDER_UPDATE:
|
||||
on_data(msg, order_update_callback);
|
||||
break;
|
||||
case EventType::OUTBOUND_ACCOUNT_POSITION:
|
||||
on_data(msg, account_update_callback);
|
||||
break;
|
||||
case EventType::BALANCE_UPDATE:
|
||||
on_data(msg, balance_update_callback);
|
||||
break;
|
||||
default:
|
||||
ERROR_FN(
|
||||
"on_message", "Response has unexpected event type '{}'",
|
||||
event_type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void on_close(const std::error_code &ec) {
|
||||
bts_assert_msgf(
|
||||
false, "Websocket stream client disconnected. {}", ec.message());
|
||||
}
|
||||
};
|
||||
|
||||
TradeDataSubscription::TradeDataSubscription(string url)
|
||||
: me_(std::make_unique<PImpl>(std::move(url))) {}
|
||||
|
||||
TradeDataSubscription::~TradeDataSubscription() = default;
|
||||
|
||||
void TradeDataSubscription::start() { me_->start(); }
|
||||
void TradeDataSubscription::stop() { me_->stop(); }
|
||||
|
||||
Delegate<void(trade_data_stream::AccountUpdate)> &
|
||||
TradeDataSubscription::account_update_callback() {
|
||||
return me_->account_update_callback;
|
||||
}
|
||||
|
||||
Delegate<void(trade_data_stream::OrderUpdate)> &
|
||||
TradeDataSubscription::order_update_callback() {
|
||||
return me_->order_update_callback;
|
||||
}
|
||||
|
||||
} // namespace binance_execution::cross_margin::trade_data_stream
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/cross_margin/trade_data_stream/account_update.h"
|
||||
#include "binance_execution/cross_margin/trade_data_stream/balance_update.h"
|
||||
#include "binance_execution/cross_margin/trade_data_stream/order_update.h"
|
||||
#include "lib/utils/Delegate.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace binance_execution::cross_margin::trade_data_stream {
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/margin_trading/trade-data-stream
|
||||
*/
|
||||
class TradeDataSubscription {
|
||||
public:
|
||||
explicit TradeDataSubscription(std::string url);
|
||||
~TradeDataSubscription();
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
bts::core::Delegate<void(AccountUpdate)> &account_update_callback();
|
||||
bts::core::Delegate<void(BalanceUpdate)> &balance_update_callback();
|
||||
bts::core::Delegate<void(OrderUpdate)> &order_update_callback();
|
||||
|
||||
private:
|
||||
struct PImpl;
|
||||
std::unique_ptr<PImpl> me_;
|
||||
};
|
||||
|
||||
} // namespace binance_execution::cross_margin::trade_data_stream
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution {
|
||||
|
||||
struct ErrorList {
|
||||
ErrorList() = default;
|
||||
ErrorList(std::string *errors_p) : errors_p(errors_p) {}
|
||||
|
||||
static void append(std::string &errors, const char *err) {
|
||||
ErrorList el{&errors};
|
||||
el.append(err);
|
||||
}
|
||||
|
||||
static void append(std::string &errors, const std::string &err) {
|
||||
ErrorList el{&errors};
|
||||
el.append(err.c_str());
|
||||
}
|
||||
|
||||
void set_delimiter(std::string delimiter) {
|
||||
this->delimiter = std::move(delimiter);
|
||||
}
|
||||
|
||||
void append(const char *err) {
|
||||
if (err == nullptr || err[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
if (errors_p == nullptr) {
|
||||
errors_p = &errors_.emplace();
|
||||
}
|
||||
if (!errors_p->empty()) {
|
||||
errors_p->append(delimiter);
|
||||
}
|
||||
errors_p->append(err);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string *errors_p = nullptr;
|
||||
std::string delimiter = ". ";
|
||||
std::optional<std::string> errors_;
|
||||
};
|
||||
|
||||
} // namespace binance_execution
|
||||
@@ -0,0 +1,76 @@
|
||||
#include "execution_report_store.h"
|
||||
#include "lib/core/TimeStamp.h"
|
||||
#include "lib/orderman/CreateOrderMsg.h"
|
||||
#include "lib/orderman/orderman_utils/ExecutionReport.h"
|
||||
#include "lib/utils/string_utils.h"
|
||||
|
||||
namespace binance_execution {
|
||||
|
||||
using bts::orderman::ExecutionReport;
|
||||
|
||||
std::shared_ptr<const ExecutionReport> ExecutionReportStore::update(
|
||||
const std::string &exch_id,
|
||||
std::function<void(std::shared_ptr<ExecutionReport> &, bool)> init_update) {
|
||||
auto init = [&](std::shared_ptr<ExecutionReport> &er_p, bool is_new) {
|
||||
auto &er = *er_p;
|
||||
bts::system::string_utils::copy_to_array<12>(
|
||||
account, er.account_name.value());
|
||||
er.exchange = venue;
|
||||
er.cfg = jetfix_connection.get();
|
||||
// Irrelevant fields
|
||||
er.ice_cl_ord_id = 0;
|
||||
er.ice_company_id = 0;
|
||||
init_update(er_p, is_new);
|
||||
};
|
||||
const auto bts_key = get_internal_key(exch_id);
|
||||
return store.replace_or_add(bts_key, init);
|
||||
}
|
||||
|
||||
std::shared_ptr<ExecutionReport> ExecutionReportStore::reject(
|
||||
const bts::orderman::CreateOrderMsg &m, const std::string &error) {
|
||||
auto init = [&](std::shared_ptr<ExecutionReport> &er_p) {
|
||||
auto &er = *er_p;
|
||||
er.exchange = venue;
|
||||
er.exchange_order_id = bts::orderman::ExchangeOrderID64{0};
|
||||
bts::system::string_utils::copy_to_array<12>(
|
||||
account, er.account_name.value());
|
||||
er.msg = error;
|
||||
er.order_key = m.key;
|
||||
er.order_type = m.order_type;
|
||||
er.time_in_force = m.time_in_force;
|
||||
er.quantity = m.quantity;
|
||||
er.max_show = m.display_quantity;
|
||||
er.stop_price.value() = m.stop_px.value();
|
||||
er.order_status = bts::api::orderman::NATIVE_ORDER_STATUS_REJECTED;
|
||||
er.transact_time = bts::orderman::ExchangeTimeStamp64{
|
||||
bts::core::TimeStamp::now().nanos()};
|
||||
er.price.value() = std::numeric_limits<double>::quiet_NaN();
|
||||
er.avg_price.value() = std::numeric_limits<double>::quiet_NaN();
|
||||
er.filled_quantity = bts::Quantity{0};
|
||||
er.last_shares = bts::Quantity{0};
|
||||
er.last_price.value() = std::numeric_limits<double>::quiet_NaN();
|
||||
er.avg_price.value() = std::numeric_limits<double>::quiet_NaN();
|
||||
er.stop_price.value() = std::numeric_limits<double>::quiet_NaN();
|
||||
er.manual_order_indicator = bts::api::orderman::MANUAL_ORDER_INDICATOR_NO;
|
||||
er.security = nullptr;
|
||||
er.leg_security = nullptr;
|
||||
er.msg_seq_num = 0;
|
||||
};
|
||||
return store.add(m.key, init);
|
||||
}
|
||||
|
||||
std::shared_ptr<const ExecutionReport> ExecutionReportStore::set_error(
|
||||
bts::orderman::OrderKey key, const std::string &error) {
|
||||
auto init = [&error](std::shared_ptr<ExecutionReport> &er_p, bool is_new) {
|
||||
// Set underlying data to a new instance rather than mutate it. The shared
|
||||
// pointer we return may be copied to a buffer/queue and processed on
|
||||
// another thread.
|
||||
if (!is_new) {
|
||||
er_p = std::make_shared<ExecutionReport>(*er_p);
|
||||
}
|
||||
er_p->msg = error;
|
||||
};
|
||||
return store.get_or_add(key, init);
|
||||
}
|
||||
|
||||
} // namespace binance_execution
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/collections/concurrent_factory.h"
|
||||
#include "lib/domain_model/Security.pb.h"
|
||||
#include "lib/orderman/orderman_utils/orderman_common.h"
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace bts::api {
|
||||
class Security;
|
||||
}
|
||||
|
||||
namespace bts::config {
|
||||
class JetFIX_Connection;
|
||||
}
|
||||
|
||||
namespace bts::orderman {
|
||||
struct CreateOrderMsg;
|
||||
class ExecutionReport;
|
||||
} // namespace bts::orderman
|
||||
|
||||
namespace binance_execution {
|
||||
|
||||
class ExecutionReportStore {
|
||||
std::function<bts::orderman::OrderKey(const std::string &)> get_internal_key;
|
||||
std::string account;
|
||||
std::shared_ptr<bts::config::JetFIX_Connection> jetfix_connection;
|
||||
bts::api::Venue venue;
|
||||
bts::collections::ConcurrentFactory<
|
||||
bts::orderman::OrderKey,
|
||||
bts::orderman::ExecutionReport>
|
||||
store;
|
||||
|
||||
public:
|
||||
ExecutionReportStore(
|
||||
std::function<bts::orderman::OrderKey(const std::string &)>
|
||||
get_internal_key,
|
||||
std::string account,
|
||||
std::shared_ptr<bts::config::JetFIX_Connection> jetfix_connection)
|
||||
: get_internal_key(get_internal_key), account(std::move(account)),
|
||||
jetfix_connection(std::move(jetfix_connection)) {}
|
||||
|
||||
std::size_t erase(bts::orderman::OrderKey key) { return store.erase(key); }
|
||||
std::size_t size() const { return store.size(); }
|
||||
|
||||
std::shared_ptr<const bts::orderman::ExecutionReport> update(
|
||||
const std::string &exch_id,
|
||||
std::function<
|
||||
void(std::shared_ptr<bts::orderman::ExecutionReport> &, bool)>
|
||||
init_update);
|
||||
|
||||
std::shared_ptr<bts::orderman::ExecutionReport> reject(
|
||||
const bts::orderman::CreateOrderMsg &m, const std::string &error);
|
||||
|
||||
std::shared_ptr<const bts::orderman::ExecutionReport> set_error(
|
||||
bts::orderman::OrderKey key, const std::string &error);
|
||||
};
|
||||
|
||||
} // namespace binance_execution
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/io/JsonReader.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution {
|
||||
|
||||
struct GatewayConfig {
|
||||
std::string api_key;
|
||||
std::string secret_key;
|
||||
std::string account;
|
||||
std::string rest_endpoint;
|
||||
std::string ws_endpoint;
|
||||
std::string ws_stream_endpoint;
|
||||
bool use_cross_margin_api = false;
|
||||
};
|
||||
|
||||
inline void to_json(nlohmann::json &j, const GatewayConfig &gw) {
|
||||
j["api_key"] = gw.api_key;
|
||||
j["secret_key"] = gw.secret_key;
|
||||
j["account"] = gw.account;
|
||||
j["rest_endpoint"] = gw.rest_endpoint;
|
||||
j["ws_endpoint"] = gw.ws_endpoint;
|
||||
j["ws_stream_endpoint"] = gw.ws_stream_endpoint;
|
||||
if (gw.use_cross_margin_api) {
|
||||
j["use_cross_margin_api"] = gw.use_cross_margin_api;
|
||||
}
|
||||
}
|
||||
|
||||
inline void from_json(const nlohmann::json &j, GatewayConfig &gw) {
|
||||
j.at("api_key").get_to(gw.api_key);
|
||||
j.at("secret_key").get_to(gw.secret_key);
|
||||
j.at("account").get_to(gw.account);
|
||||
j.at("rest_endpoint").get_to(gw.rest_endpoint);
|
||||
j.at("ws_endpoint").get_to(gw.ws_endpoint);
|
||||
j.at("ws_stream_endpoint").get_to(gw.ws_stream_endpoint);
|
||||
const bts::io::JsonReader jr{&j};
|
||||
jr.read_or("use_cross_margin_api", gw.use_cross_margin_api);
|
||||
}
|
||||
|
||||
} // namespace binance_execution
|
||||
+19
-13
@@ -1,29 +1,30 @@
|
||||
#include "listen_key_requestor.h"
|
||||
#include "binance_execution/spots/rest_api_client.h"
|
||||
#include "lib/core/TimeStamp.h"
|
||||
#include "lib/io/json_formatter.h"
|
||||
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace binance_execution::spots {
|
||||
namespace binance_execution {
|
||||
|
||||
using bts::core::Delegate;
|
||||
using bts::core::TimeStamp;
|
||||
using bts::io::json_formatter;
|
||||
using std::optional;
|
||||
using std::string;
|
||||
using std::thread;
|
||||
|
||||
struct ListenKeyRequestor::PImpl {
|
||||
RestApiClient rest_api_client;
|
||||
std::function<RestApiResult<ListenKeyResponse>(const ListenKeyRequest &)>
|
||||
get_listen_key;
|
||||
std::function<RestApiResult<ListenKeyResponse>(const ListenKeyRequest &)>
|
||||
refresh_listen_key;
|
||||
string listen_key;
|
||||
TimeStamp listen_key_timestamp;
|
||||
optional<thread> listen_key_thread;
|
||||
Delegate<void(const string &)> callback;
|
||||
bool is_running = false;
|
||||
|
||||
PImpl(RestApiClient rest_api_client)
|
||||
: rest_api_client(std::move(rest_api_client)) {}
|
||||
|
||||
~PImpl() { stop(); }
|
||||
|
||||
void start() {
|
||||
@@ -45,21 +46,22 @@ struct ListenKeyRequestor::PImpl {
|
||||
bts::core::TimeSpan::from_seconds(60 * 30);
|
||||
const auto now = bts::core::TimeStamp::now();
|
||||
if (listen_key.empty()) {
|
||||
const auto &res = rest_api_client.get_listen_key();
|
||||
const auto &res = get_listen_key({});
|
||||
if (!res.ok()) {
|
||||
continue;
|
||||
}
|
||||
bts_assert_msgf(
|
||||
res.ok(), "Failed to get listen key. {}", res.error);
|
||||
res.ok(), "Failed to get listen key. {}", json_formatter(res));
|
||||
listen_key = res.response->listenKey;
|
||||
bts_assert(!listen_key.empty());
|
||||
listen_key_timestamp = now;
|
||||
callback(listen_key);
|
||||
}
|
||||
if ((now - listen_key_timestamp) > refresh_period) {
|
||||
const auto &res = rest_api_client.refresh_listen_key();
|
||||
const auto &res = refresh_listen_key({});
|
||||
bts_assert_msgf(
|
||||
res.ok(), "Failed to refresh listen key. {}", res.error);
|
||||
res.ok(), "Failed to refresh listen key. {}",
|
||||
json_formatter(res));
|
||||
listen_key_timestamp = now;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
@@ -67,8 +69,12 @@ struct ListenKeyRequestor::PImpl {
|
||||
}
|
||||
};
|
||||
|
||||
ListenKeyRequestor::ListenKeyRequestor(RestApiClient rest_api_client)
|
||||
: me_(std::make_unique<PImpl>(std::move(rest_api_client))) {}
|
||||
ListenKeyRequestor::ListenKeyRequestor(
|
||||
std::function<RestApiResult<ListenKeyResponse>(const ListenKeyRequest &)>
|
||||
get_listen_key,
|
||||
std::function<RestApiResult<ListenKeyResponse>(const ListenKeyRequest &)>
|
||||
refresh_listen_key)
|
||||
: me_(std::make_unique<PImpl>(get_listen_key, refresh_listen_key)) {}
|
||||
|
||||
ListenKeyRequestor::~ListenKeyRequestor() = default;
|
||||
|
||||
@@ -78,4 +84,4 @@ Delegate<void(const string &)> &ListenKeyRequestor::callback() {
|
||||
return me_->callback;
|
||||
}
|
||||
|
||||
} // namespace binance_execution::spots
|
||||
} // namespace binance_execution
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/messages/listen_key_request.h"
|
||||
#include "binance_execution/messages/listen_key_response.h"
|
||||
#include "binance_execution/rest_api_result.h"
|
||||
#include "lib/utils/Delegate.h"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace binance_execution {
|
||||
|
||||
/**
|
||||
* Requests the listen key for user data stream subscriptions, and periodically
|
||||
* refreshes it to keep subscriptions alive.
|
||||
*/
|
||||
class ListenKeyRequestor {
|
||||
public:
|
||||
explicit ListenKeyRequestor(
|
||||
std::function<RestApiResult<ListenKeyResponse>(const ListenKeyRequest &)>
|
||||
get_listen_key,
|
||||
std::function<RestApiResult<ListenKeyResponse>(const ListenKeyRequest &)>
|
||||
refresh_listen_key);
|
||||
~ListenKeyRequestor();
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
bts::core::Delegate<void(const std::string &)> &callback();
|
||||
|
||||
private:
|
||||
struct PImpl;
|
||||
std::unique_ptr<PImpl> me_;
|
||||
};
|
||||
|
||||
} // namespace binance_execution
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution {
|
||||
|
||||
struct ListenKeyRequest {};
|
||||
|
||||
inline void to_query_params(
|
||||
std::string & /*qps*/, const ListenKeyRequest & /*req*/) {}
|
||||
|
||||
} // namespace binance_execution
|
||||
+2
-5
@@ -3,15 +3,12 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution::spots {
|
||||
namespace binance_execution {
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/derivatives/usds-margined-futures/user-data-streams
|
||||
*/
|
||||
struct ListenKeyResponse {
|
||||
std::string listenKey;
|
||||
};
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ListenKeyResponse, listenKey);
|
||||
|
||||
} // namespace binance_execution::spots
|
||||
} // namespace binance_execution
|
||||
+20
-30
@@ -7,7 +7,7 @@
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace binance_execution::spots {
|
||||
namespace binance_execution {
|
||||
|
||||
struct OrderKeyMap {
|
||||
enum KeyType {
|
||||
@@ -26,6 +26,7 @@ struct OrderKeyMap {
|
||||
|
||||
std::optional<bts::orderman::OrderKey> get_bts_key(
|
||||
const std::string &exch_order_key) {
|
||||
std::lock_guard<std::mutex> lock{instance_mx};
|
||||
std::size_t n = exch_order_key.find_last_of('_');
|
||||
bts::orderman::OrderKey k;
|
||||
if (n == std::string::npos) {
|
||||
@@ -40,6 +41,7 @@ struct OrderKeyMap {
|
||||
|
||||
std::string get_exch_key(bts::orderman::OrderKey key) {
|
||||
const auto key_l = bts::system::math::checked_ultol(key.value());
|
||||
std::lock_guard<std::mutex> lock{instance_mx};
|
||||
std::string rv = std::to_string(key_l);
|
||||
uint32_t version = order_versions.try_emplace(key, 0).first->second;
|
||||
if (version > 0) {
|
||||
@@ -49,37 +51,25 @@ struct OrderKeyMap {
|
||||
return rv;
|
||||
}
|
||||
|
||||
// std::string get_exch_key(
|
||||
// bts::orderman::OrderKey key, KeyType type = KeyType::CURRENT) {
|
||||
// const auto key_l = bts::system::math::checked_ultol(key.value());
|
||||
// std::string rv = std::to_string(key_l);
|
||||
// if (type == ORIGINAL) {
|
||||
// return rv;
|
||||
// }
|
||||
// uint32_t version = order_versions.try_emplace(key, 0).first->second;
|
||||
// if (version > 0) {
|
||||
// rv.append("_");
|
||||
// rv.append(std::to_string(version));
|
||||
// }
|
||||
// switch (type) {
|
||||
// case CANCEL_REPLACE_CANCEL:
|
||||
// rv.append("crc");
|
||||
// break;
|
||||
// case CANCEL_REPLACE_REPLACE:
|
||||
// rv.append("crr");
|
||||
// break;
|
||||
// case CANCEL:
|
||||
// rv.append("c");
|
||||
// break;
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
// return rv;
|
||||
// }
|
||||
|
||||
void increment_order_version(bts::orderman::OrderKey key) {
|
||||
std::lock_guard<std::mutex> lock{instance_mx};
|
||||
order_versions.try_emplace(key, 0).first->second++;
|
||||
}
|
||||
|
||||
void increment_order_version(
|
||||
bts::orderman::OrderKey key, std::string &exch_key) {
|
||||
std::lock_guard<std::mutex> lock{instance_mx};
|
||||
order_versions.try_emplace(key, 0).first->second++;
|
||||
exch_key = std::to_string(key.value());
|
||||
std::uint32_t version = order_versions.try_emplace(key, 0).first->second;
|
||||
if (version > 0) {
|
||||
exch_key.append("_");
|
||||
exch_key.append(std::to_string(version));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex instance_mx;
|
||||
};
|
||||
|
||||
} // namespace binance_execution::spots
|
||||
} // namespace binance_execution
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution {
|
||||
|
||||
struct RestApiError {
|
||||
std::int64_t code;
|
||||
std::string msg;
|
||||
};
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(RestApiError, code, msg);
|
||||
|
||||
} // namespace binance_execution
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/rest_api_error.h"
|
||||
|
||||
#include <nlohmann/detail/meta/type_traits.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <optional>
|
||||
@@ -9,30 +11,35 @@ namespace binance_execution {
|
||||
|
||||
template <class ResponseT>
|
||||
struct RestApiResult {
|
||||
std::string error;
|
||||
std::string http_error;
|
||||
std::int64_t http_code;
|
||||
std::optional<RestApiError> error;
|
||||
|
||||
std::optional<ResponseT> response;
|
||||
|
||||
RestApiResult() = default;
|
||||
|
||||
static RestApiResult from_error(const std::string &error) {
|
||||
return RestApiResult{error};
|
||||
static RestApiResult from_error(
|
||||
const std::string &error,
|
||||
const std::string &error_body,
|
||||
std::int64_t code) {
|
||||
return RestApiResult{error, error_body, code};
|
||||
}
|
||||
|
||||
static RestApiResult from_success(ResponseT response) {
|
||||
return RestApiResult{std::move(response)};
|
||||
}
|
||||
|
||||
constexpr bool ok() const { return error.empty(); }
|
||||
constexpr bool ok() const { return !error; }
|
||||
|
||||
void reset() {
|
||||
error.clear();
|
||||
error.reset();
|
||||
response.reset();
|
||||
}
|
||||
|
||||
private:
|
||||
explicit RestApiResult(ResponseT response) : response(std::move(response)) {}
|
||||
|
||||
explicit RestApiResult(std::string error) : error(std::move(error)) {}
|
||||
explicit RestApiResult(RestApiError error) : error(std::move(error)) {}
|
||||
};
|
||||
|
||||
template <
|
||||
@@ -42,7 +49,11 @@ template <
|
||||
int> = 0>
|
||||
inline void to_json(nlohmann::json &j, const RestApiResult<ResponseT> &res) {
|
||||
if (!res.ok()) {
|
||||
j["error"] = res.error;
|
||||
if (res.error) {
|
||||
j["error"] = *res.error;
|
||||
} else {
|
||||
j["error"] = nlohmann::json{};
|
||||
}
|
||||
} else if (res.response) {
|
||||
j["response"] = *res.response;
|
||||
} else {
|
||||
@@ -58,7 +69,14 @@ template <
|
||||
inline void from_json(const nlohmann::json &j, RestApiResult<ResponseT> &res) {
|
||||
auto it = j.find("error");
|
||||
if (it != j.end()) {
|
||||
it->get_to(res.error);
|
||||
if (it->is_null()) {
|
||||
res.error.reset();
|
||||
} else {
|
||||
if (!res.error) {
|
||||
res.error.emplace();
|
||||
}
|
||||
it->get_to(*res.error);
|
||||
}
|
||||
}
|
||||
it = j.find("response");
|
||||
if (it != j.end()) {
|
||||
|
||||
@@ -1,633 +0,0 @@
|
||||
#include "execution_gateway.h"
|
||||
#include "binance_execution/rest_api_result.h"
|
||||
#include "binance_execution/spots/messages/modify_order_request.h"
|
||||
#include "binance_execution/spots/messages/order_record.h"
|
||||
#include "binance_execution/spots/order_key_map.h"
|
||||
#include "binance_execution/spots/rest_api_client.h"
|
||||
#include "binance_execution/spots/type_converters.h"
|
||||
#include "binance_execution/spots/user_data_streams/listen_key_requestor.h"
|
||||
#include "binance_execution/spots/user_data_streams/user_data_subscription.h"
|
||||
#include "binance_execution/type_converters.h"
|
||||
#include "lib/api/service_helpers/AdminServiceUtils.h"
|
||||
#include "lib/api/services/AdminService.pb.h"
|
||||
#include "lib/api/services/OrderService.h"
|
||||
#include "lib/config/protos/jetfix.pb.h"
|
||||
#include "lib/config/protos/toggles.pb.h"
|
||||
#include "lib/core/TimeStamp.h"
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/domain_model/Security.pb.h"
|
||||
#include "lib/domain_model/Side.h"
|
||||
#include "lib/io/json_formatter.h"
|
||||
#include "lib/logging/logging.h"
|
||||
#include "lib/md/SecurityOracle.h"
|
||||
#include "lib/mercury/events/Update.h"
|
||||
#include "lib/orderman/CancelOrderMsg.h"
|
||||
#include "lib/orderman/CancelReplaceOrderMsg.h"
|
||||
#include "lib/orderman/CreateOrderMsg.h"
|
||||
#include "lib/orderman/RFQMsg.h"
|
||||
#include "lib/orderman/events/NewAdminDataEvent.h"
|
||||
#include "lib/orderman/events/NewSecuritiesEvent.h"
|
||||
#include "lib/orderman/orderman_utils/ExchangePosition.h"
|
||||
#include "lib/orderman/orderman_utils/ExecutionReport.h"
|
||||
#include "lib/orderman/orderman_utils/orderman_common.h"
|
||||
#include "lib/utils/checked_arithmetic.h"
|
||||
#include "lib/utils/overloaded.h"
|
||||
|
||||
#include <boost/lockfree/spsc_queue.hpp>
|
||||
#include <fmt/format.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/hmac.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
using nlohmann::json;
|
||||
|
||||
namespace binance_execution::spots {
|
||||
|
||||
#define ASSERT_FN(test, fn, ...) \
|
||||
bts_assert_msgf(test, "Binance::" fn ". " __VA_ARGS__)
|
||||
#define LOG_FN(level, fn, ...) \
|
||||
LOG(OrderMan, level, "Binance::" fn ". " __VA_ARGS__)
|
||||
#define DIAG_FN(fn, ...) LOG_FN(bts::Diag, fn, __VA_ARGS__)
|
||||
#define INFO_FN(fn, ...) LOG_FN(bts::Info, fn, __VA_ARGS__)
|
||||
#define WARN_FN(fn, ...) LOG_FN(bts::Warn, fn, __VA_ARGS__)
|
||||
#define ERROR_FN(fn, ...) LOG_FN(bts::Error, fn, __VA_ARGS__)
|
||||
|
||||
#define VENUE bts::api::VENUE_BINANCE
|
||||
|
||||
using boost::lockfree::spsc_queue;
|
||||
using bts::core::Delegate;
|
||||
using bts::io::json_formatter;
|
||||
using bts::orderman::CancelOrderMsg;
|
||||
using bts::orderman::CancelReplaceOrderMsg;
|
||||
using bts::orderman::CreateOrderMsg;
|
||||
using bts::orderman::NewAdminDataEvent;
|
||||
using bts::orderman::NewSecuritiesEvent;
|
||||
using bts::orderman::OrderMsg;
|
||||
using bts::orderman::RFQMsg;
|
||||
using std::int64_t;
|
||||
using std::optional;
|
||||
using std::size_t;
|
||||
using std::string;
|
||||
using std::uint32_t;
|
||||
using std::vector;
|
||||
|
||||
static void append_error(string &dest, const char *err) {
|
||||
if (!dest.empty()) {
|
||||
dest.append(" ");
|
||||
}
|
||||
dest.append(err);
|
||||
}
|
||||
|
||||
static void validate(bts::orderman::OrderKey key, string &error) {
|
||||
if (key.value() == 0) {
|
||||
append_error(error, "order key is 0.");
|
||||
}
|
||||
}
|
||||
|
||||
static void validate(bts::orderman::ExchangeOrderID64 key, string &error) {
|
||||
if (key.value() == 0) {
|
||||
append_error(error, "exch order key is 0.");
|
||||
}
|
||||
}
|
||||
|
||||
static void validate(bts::orderman::ExchangeOrderIDVar key, string &error) {
|
||||
if (std::holds_alternative<bts::orderman::ExchangeOrderID64>(key)) {
|
||||
validate(std::get<bts::orderman::ExchangeOrderID64>(key), error);
|
||||
} else {
|
||||
append_error(error, "exch order id is undefined or not an integer.");
|
||||
}
|
||||
}
|
||||
|
||||
static void validate(bts::Quantity qty, string &error) {
|
||||
if (qty.full_value() == 0) {
|
||||
append_error(error, "quantity is 0.");
|
||||
}
|
||||
}
|
||||
|
||||
static void validate(const bts::api::Security *sec, string &error) {
|
||||
if (sec == nullptr) {
|
||||
append_error(error, "security is null.");
|
||||
} else if (!sec->has_binance()) {
|
||||
append_error(error, "security not of Binance.");
|
||||
}
|
||||
}
|
||||
|
||||
static void validate(bts::api::Side side, string &error) {
|
||||
switch (side) {
|
||||
case bts::api::SIDE_BUY:
|
||||
case bts::api::SIDE_SELL:
|
||||
break;
|
||||
default:
|
||||
append_error(error, "invalid side.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Store exch order id in EMNativeOrder (until then, it'll always be 0).
|
||||
// We can ignore this for now and use "orig client id" in cancel/replace requests.
|
||||
static constexpr const bool VALIDATE_EXCH_ORDER_ID = false;
|
||||
|
||||
static bool validate(const CreateOrderMsg &m, string &error) {
|
||||
const size_t orig_sz = error.size();
|
||||
validate(m.security, error);
|
||||
validate(m.key, error);
|
||||
validate(m.quantity, error);
|
||||
validate(m.side, error);
|
||||
return error.size() == orig_sz;
|
||||
}
|
||||
|
||||
static bool validate(const CancelOrderMsg &m, string &error) {
|
||||
const size_t orig_sz = error.size();
|
||||
validate(m.security, error);
|
||||
validate(m.key, error);
|
||||
if (VALIDATE_EXCH_ORDER_ID) {
|
||||
validate(m.exchange_order_id, error);
|
||||
}
|
||||
validate(m.side, error);
|
||||
return error.size() == orig_sz;
|
||||
}
|
||||
|
||||
static bool validate(const CancelReplaceOrderMsg &m, string &error) {
|
||||
const size_t orig_sz = error.size();
|
||||
validate(m.create_fields, error);
|
||||
if (VALIDATE_EXCH_ORDER_ID) {
|
||||
validate(m.exchange_order_id, error);
|
||||
}
|
||||
return error.size() == orig_sz;
|
||||
}
|
||||
|
||||
static bool validate(const bts::orderman::ExecutionReport &er, string &error) {
|
||||
const size_t orig_sz = error.size();
|
||||
validate(er.order_key, error);
|
||||
validate(er.quantity, error);
|
||||
validate(er.exchange_order_id, error);
|
||||
if (er.account_name.value().empty()) {
|
||||
append_error(error, "account is empty.");
|
||||
}
|
||||
validate(er.security, error);
|
||||
if (er.cfg == nullptr) {
|
||||
append_error(error, "jetfix config is null.");
|
||||
}
|
||||
return error.size() == orig_sz;
|
||||
}
|
||||
|
||||
struct ExecutionGatewayCallbacks {
|
||||
bts::orderman::OnExeReport exe_report;
|
||||
bts::orderman::OnGwStatusChange gw_status_change;
|
||||
bts::orderman::OnCancelReject cancel_reject;
|
||||
bts::orderman::OnExchangePositionState exchange_position_state;
|
||||
bts::orderman::OnExchangeOpenOrdersState exchange_open_orders;
|
||||
};
|
||||
|
||||
struct ExecutionGateway::PImpl {
|
||||
explicit PImpl(
|
||||
bts::md::SecurityOracle sec_oracle,
|
||||
ExecutionGatewayConfig config,
|
||||
bts::config::JetFIX::Connection jetfix_connection)
|
||||
: sec_oracle(std::move(sec_oracle)), config(std::move(config)),
|
||||
jetfix_connection(std::move(jetfix_connection)),
|
||||
rest_api_client(
|
||||
this->config.rest_endpoint,
|
||||
this->config.api_key,
|
||||
this->config.secret_key),
|
||||
listen_key_requestor(this->rest_api_client) {
|
||||
bts_assert(this->jetfix_connection.venue() == bts::api::VENUE_BINANCE);
|
||||
bts_assert(!this->jetfix_connection.name().empty());
|
||||
pos.exchange = bts::api::VENUE_BINANCE;
|
||||
}
|
||||
|
||||
static constexpr const char *CANCEL_REPLACE_CANCEL_SUFFIX = "crc";
|
||||
|
||||
bts::md::SecurityOracle sec_oracle;
|
||||
ExecutionGatewayConfig config;
|
||||
bts::config::JetFIX_Connection jetfix_connection;
|
||||
|
||||
RestApiClient rest_api_client;
|
||||
optional<user_data_streams::UserDataSubscription> user_data_sub;
|
||||
Delegate<void(user_data_streams::AccountUpdate)> account_update_callback;
|
||||
Delegate<void(user_data_streams::OrderUpdate)> order_update_callback;
|
||||
ListenKeyRequestor listen_key_requestor;
|
||||
|
||||
spsc_queue<nlohmann::json> incoming_msgs{1024};
|
||||
spsc_queue<user_data_streams::AccountUpdate> incoming_account_updates{1024};
|
||||
spsc_queue<user_data_streams::OrderUpdate> incoming_order_updates{1024};
|
||||
spsc_queue<RestApiResult<OrderRecord>> incoming_new_orders{1024};
|
||||
|
||||
vector<user_data_streams::AccountUpdate> account_updates_vec;
|
||||
|
||||
ExecutionGatewayCallbacks callbacks;
|
||||
bts::orderman::ExchangePosition pos;
|
||||
bts::admin::AdminServiceUtils::ExchangeSubAccountResolver
|
||||
subaccount_resolver{};
|
||||
bts::api::admin::AdminData admin_data = {};
|
||||
bts::api::admin::BTSAccount account;
|
||||
// Since modifications are implemented as cancel/replace at Binance, modified
|
||||
// orders don't keep their original id. The version number correlates the
|
||||
// original id with the current one.
|
||||
std::unordered_map<bts::orderman::OrderKey, std::uint32_t> order_versions;
|
||||
OrderKeyMap key_map;
|
||||
|
||||
void start() {
|
||||
INFO_FN("start");
|
||||
listen_key_requestor.callback().bind<&PImpl::on_listen_key_>(*this);
|
||||
listen_key_requestor.start();
|
||||
}
|
||||
|
||||
void stop() {
|
||||
INFO_FN("stop");
|
||||
user_data_sub.reset();
|
||||
}
|
||||
|
||||
void on_account_update(user_data_streams::AccountUpdate u) {
|
||||
INFO_FN("on_account_update", "\n{:2}", json_formatter(u));
|
||||
incoming_account_updates.push(std::move(u));
|
||||
}
|
||||
|
||||
void on_order_update(user_data_streams::OrderUpdate u) {
|
||||
INFO_FN("on_order_update", "\n{:2}", json_formatter(u));
|
||||
if (u.orig_client_order_id.ends_with(CANCEL_REPLACE_CANCEL_SUFFIX)) {
|
||||
// This is the "canceled order part of a cancel/replace op. This op
|
||||
// generates another update without such a suffix. That's the new
|
||||
// order; that's the one we want.
|
||||
DIAG_FN(
|
||||
"on_order_update",
|
||||
"Ignoring cancel/replace cancel order update. id: {}",
|
||||
u.client_order_id);
|
||||
return;
|
||||
}
|
||||
incoming_order_updates.push(std::move(u));
|
||||
}
|
||||
|
||||
void on_listen_key_(const string &listen_key) {
|
||||
INFO_FN("on_listen_key", "value: {}", listen_key);
|
||||
user_data_sub.reset();
|
||||
if (listen_key.empty()) {
|
||||
LOG(OrderMan, bts::Error,
|
||||
"Unable to subscribe to user data because 'listen_key' "
|
||||
"is empty.");
|
||||
return;
|
||||
}
|
||||
auto &uds = user_data_sub.emplace(
|
||||
config.ws_stream_endpoint + "/ws/" + listen_key);
|
||||
uds.account_update_callback().bind<&PImpl::on_account_update>(*this);
|
||||
uds.order_update_callback().bind<&PImpl::on_order_update>(*this);
|
||||
uds.start();
|
||||
}
|
||||
|
||||
void create_order(const CreateOrderMsg &m) {
|
||||
string sec_desc = m.security == nullptr
|
||||
? "(null)"
|
||||
: bts::securities::descriptor(*m.security);
|
||||
RestApiResult<OrderRecord> res;
|
||||
validate(m, res.error);
|
||||
LOG_FN(
|
||||
res.ok() ? bts::Info : bts::Error, "create_order",
|
||||
"{} {} {} @ {} (id={}, type={}, tif={}, err={})", m.side, m.quantity,
|
||||
sec_desc, m.price, m.key, m.order_type, m.time_in_force, res.error);
|
||||
if (res.ok()) {
|
||||
CreateOrderRequest req;
|
||||
req.newClientOrderId = std::to_string(m.key.value());
|
||||
req.symbol = bts::securities::descriptor(*m.security);
|
||||
req.side = m.side == bts::api::SIDE_BUY ? Side::BUY : Side::SELL;
|
||||
req.price = m.price.value();
|
||||
req.quantity = m.quantity.full_value();
|
||||
req.type = convert_order_type(m.order_type);
|
||||
req.timeInForce = convert_time_in_force(m.time_in_force);
|
||||
req.timestamp = convert_time_stamp(bts::core::TimeStamp::now());
|
||||
res = rest_api_client.create_order(req);
|
||||
LOG_FN(
|
||||
res.ok() ? bts::Info : bts::Error, "create_order", "id={}; {}",
|
||||
m.key, json_formatter(res));
|
||||
}
|
||||
incoming_new_orders.push(std::move(res));
|
||||
}
|
||||
|
||||
void update_order(const CancelReplaceOrderMsg &m) {
|
||||
string sec_desc =
|
||||
m.create_fields.security == nullptr
|
||||
? "(null)"
|
||||
: bts::securities::descriptor(*m.create_fields.security);
|
||||
RestApiResult<ModifyOrderResponse> res;
|
||||
validate(m, res.error);
|
||||
LOG_FN(
|
||||
res.ok() ? bts::Info : bts::Error, "update_order",
|
||||
"{} {} {} @ {} (id={}, type={}, tif={}, err={})",
|
||||
m.create_fields.side, m.create_fields.quantity, sec_desc,
|
||||
m.create_fields.price, m.create_fields.key,
|
||||
m.create_fields.order_type, m.create_fields.time_in_force, res.error);
|
||||
if (res.ok()) {
|
||||
ModifyOrderRequest req;
|
||||
const auto orig_key = m.create_fields.key;
|
||||
req.cancelOrderId = 0;
|
||||
req.cancelOrigClientOrderId = key_map.get_exch_key(orig_key);
|
||||
// Can't reuse the same order id.
|
||||
key_map.increment_order_version(orig_key);
|
||||
req.cancelNewClientOrderId = key_map.get_exch_key(orig_key).append(
|
||||
CANCEL_REPLACE_CANCEL_SUFFIX);
|
||||
key_map.increment_order_version(orig_key);
|
||||
req.newClientOrderId = key_map.get_exch_key(orig_key);
|
||||
// The exchange accepts either cancelOrderId, or cancelOrigClientOrderId (only one
|
||||
// need be populated).
|
||||
bts_assert(
|
||||
req.cancelOrderId != 0 || !req.cancelOrigClientOrderId.empty());
|
||||
req.price = m.create_fields.price.value();
|
||||
req.quantity = m.create_fields.quantity.full_value();
|
||||
req.symbol = m.create_fields.security->binance().security_desc();
|
||||
req.side = convert_side(m.create_fields.side);
|
||||
req.type = convert_order_type(m.create_fields.order_type);
|
||||
req.cancelReplaceMode = CancelReplaceMode::STOP_ON_FAILURE;
|
||||
req.timeInForce = convert_time_in_force(m.create_fields.time_in_force);
|
||||
req.timestamp = convert_time_stamp(bts::core::TimeStamp::now());
|
||||
res = rest_api_client.modify_order(req);
|
||||
LOG_FN(
|
||||
res.ok() ? bts::Info : bts::Error, "update_order", "id={}; {}",
|
||||
req.cancelOrderId, json_formatter(res));
|
||||
}
|
||||
}
|
||||
|
||||
void cancel_order(const CancelOrderMsg &m) {
|
||||
string sec_desc = m.security == nullptr
|
||||
? "(null)"
|
||||
: bts::securities::descriptor(*m.security);
|
||||
RestApiResult<OrderRecord> res;
|
||||
validate(m, res.error);
|
||||
CancelOrderRequest req;
|
||||
LOG_FN(
|
||||
res.ok() ? bts::Info : bts::Error, "cancel_order",
|
||||
"{} {} (id={}, exch_id={}, err={})", m.side, sec_desc, m.key,
|
||||
m.exchange_order_id, res.error);
|
||||
if (res.ok()) {
|
||||
req.orderId =
|
||||
bts::system::math::checked_ultol(m.exchange_order_id.value());
|
||||
req.origClientOrderId = key_map.get_exch_key(m.key);
|
||||
// Can't reuse the same order id.
|
||||
key_map.increment_order_version(m.key);
|
||||
req.newClientOrderId = key_map.get_exch_key(m.key);
|
||||
// The exchange accepts either orderId, or origClientOrderId (only one
|
||||
// need be populated).
|
||||
bts_assert(req.orderId != 0 || !req.origClientOrderId.empty());
|
||||
req.symbol = m.security->binance().security_desc();
|
||||
req.timestamp = convert_time_stamp(bts::core::TimeStamp::now());
|
||||
res = rest_api_client.cancel_order(req);
|
||||
LOG_FN(
|
||||
res.ok() ? bts::Info : bts::Error, "cancel_order",
|
||||
"id={}, exch_id={}; {}", m.key, m.exchange_order_id,
|
||||
json_formatter(res));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ExecutionGateway::ExecutionGateway(
|
||||
bts::md::SecurityOracle sec_oracle,
|
||||
ExecutionGatewayConfig session_cfg,
|
||||
bts::config::JetFIX::Connection jetfix_connection)
|
||||
: me_(std::make_unique<PImpl>(
|
||||
std::move(sec_oracle),
|
||||
std::move(session_cfg),
|
||||
std::move(jetfix_connection))) {
|
||||
bts_assert(
|
||||
me_->config.account.size() <=
|
||||
bts::orderman::ExecutionReport{}.account_name.value().size());
|
||||
}
|
||||
|
||||
ExecutionGateway::~ExecutionGateway() = default;
|
||||
|
||||
void ExecutionGateway::start() { me_->start(); }
|
||||
|
||||
void ExecutionGateway::on_securities(
|
||||
const bts::mercury::Update<NewSecuritiesEvent> & /*update*/) {
|
||||
INFO_FN(
|
||||
"on_securities", "t_id: {} t_name: {}", bts::os::get_thread_id(),
|
||||
bts::os::get_thread_name());
|
||||
}
|
||||
|
||||
void ExecutionGateway::on_admin_data(
|
||||
const bts::mercury::Update<NewAdminDataEvent> &update) {
|
||||
INFO_FN(
|
||||
"on_admin_data", "t_id: {} t_name: {}", bts::os::get_thread_id(),
|
||||
bts::os::get_thread_name());
|
||||
if (update.events == nullptr) {
|
||||
return;
|
||||
}
|
||||
for (const auto &ev : *(update.events)) {
|
||||
me_->admin_data.CopyFrom(*ev.admin_data);
|
||||
}
|
||||
auto *exch_sub_acct =
|
||||
me_->subaccount_resolver.find_entry(me_->config.account, false);
|
||||
if (exch_sub_acct == nullptr) {
|
||||
WARN_FN(
|
||||
"on_admin_data",
|
||||
"Failed to map exchange account '{}' to internal account",
|
||||
me_->config.account);
|
||||
return;
|
||||
}
|
||||
auto *account = bts::admin::AdminServiceUtils::find_account(
|
||||
me_->admin_data, exch_sub_acct->bts_account_name());
|
||||
if (account == nullptr) {
|
||||
WARN_FN(
|
||||
"on_admin_data",
|
||||
"Failed to find internal account '{}' (mapped from exchange account "
|
||||
"'{}')",
|
||||
exch_sub_acct->bts_account_name(), me_->config.account);
|
||||
return;
|
||||
}
|
||||
if (me_->account.name() == account->name() &&
|
||||
me_->account.id() == account->id()) {
|
||||
return;
|
||||
}
|
||||
me_->account = *account;
|
||||
INFO_FN(
|
||||
"on_admin_data", "bts_account: {}; exch_subaccount: {}", account->name(),
|
||||
exch_sub_acct->exchange_subaccount());
|
||||
}
|
||||
|
||||
void ExecutionGateway::send_msgs(
|
||||
vector<OrderMsg> const &msg_vec, bts::trace::CorID) {
|
||||
INFO_FN("send_msgs", "count: {}", msg_vec.size());
|
||||
for (const auto &msg : msg_vec) {
|
||||
std::visit(
|
||||
bts::overloaded{
|
||||
[&](const CreateOrderMsg &m) { me_->create_order(m); },
|
||||
[&](const CancelReplaceOrderMsg &m) { me_->update_order(m); },
|
||||
[&](const CancelOrderMsg &m) { me_->cancel_order(m); },
|
||||
[&](const RFQMsg &) {
|
||||
bts_assert_msg(false, "Binance RFQ not implemented");
|
||||
},
|
||||
},
|
||||
msg);
|
||||
}
|
||||
}
|
||||
|
||||
void ExecutionGateway::send_fix(
|
||||
const bts::jetfix::OutFIXPtr &, bts::trace::CorID) {
|
||||
bts_assert_msg(false, "ExecutionGateway does not support fix");
|
||||
}
|
||||
|
||||
void ExecutionGateway::dispatch_incoming_fix_messages() {
|
||||
// Note: We send orders through the REST api, and receive order updates over
|
||||
// websocket (user data stream). The REST responses are added to the
|
||||
// "incoming_new_orders" queue for logging purposes, but we only generate
|
||||
// ExecutionReports in response to the websocket updates (which are added to
|
||||
// the "incoming_order_updates" queue).
|
||||
static constexpr const bool IGNORE_CREATE_ORDER_RESPONSE = true;
|
||||
// Order creates
|
||||
{
|
||||
RestApiResult<OrderRecord> curr;
|
||||
while (me_->incoming_new_orders.pop(curr)) {
|
||||
if (!curr.ok() || !curr.response) {
|
||||
ERROR_FN(
|
||||
"dispatch_incoming_fix_messages",
|
||||
"Failed to generate exe report due to invalid REST response. "
|
||||
"{}",
|
||||
curr.error);
|
||||
continue;
|
||||
}
|
||||
const auto bts_key =
|
||||
me_->key_map.get_bts_key(curr.response->clientOrderId);
|
||||
if (!bts_key) {
|
||||
ERROR_FN(
|
||||
"dispatch_incoming_fix_messages",
|
||||
"Failed to generate exe report. Did not resolve bts key from "
|
||||
"exch clientOrderId '{}'",
|
||||
curr.response->clientOrderId);
|
||||
continue;
|
||||
}
|
||||
const auto exch_key = me_->key_map.get_exch_key(*bts_key);
|
||||
if (exch_key != curr.response->clientOrderId) {
|
||||
INFO_FN(
|
||||
"dispatch_incoming_fix_messages",
|
||||
"Ignoring update w/ old clientOrderId '{}'. The current "
|
||||
"clientOrderId is '{}' (this is expected for CancelReplace "
|
||||
"ops)",
|
||||
curr.response->clientOrderId, exch_key);
|
||||
continue;
|
||||
}
|
||||
bts::orderman::ExecutionReport er;
|
||||
if (!convert_execution_report(
|
||||
*curr.response, VENUE, me_->account.name(),
|
||||
&me_->jetfix_connection, er)) {
|
||||
ERROR_FN(
|
||||
"dispatch_incoming_fix_messages",
|
||||
"Failed to generate exe report from REST response. {}",
|
||||
json_formatter(*curr.response));
|
||||
continue;
|
||||
}
|
||||
er.security = me_->sec_oracle.get_security(
|
||||
VENUE, bts::api::SECURITYTYPE_SPOT, curr.response->symbol);
|
||||
string error;
|
||||
if (!validate(er, error)) {
|
||||
ERROR_FN(
|
||||
"dispatch_incoming_fix_messages",
|
||||
"Generated invalid ExecutionReport. {}", error);
|
||||
continue;
|
||||
}
|
||||
if (!IGNORE_CREATE_ORDER_RESPONSE) {
|
||||
INFO_FN(
|
||||
"dispatch_incoming_fix_messages",
|
||||
"Dispatching ExecutionReport. bts_id: {}, exch_id: {}",
|
||||
er.order_key, er.exchange_order_id);
|
||||
me_->callbacks.exe_report(
|
||||
std::make_shared<bts::orderman::ExecutionReport>(std::move(er)),
|
||||
{});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Order updates
|
||||
{
|
||||
user_data_streams::OrderUpdate curr;
|
||||
while (me_->incoming_order_updates.pop(curr)) {
|
||||
auto bts_key = me_->key_map.get_bts_key(curr.client_order_id);
|
||||
if (!bts_key) {
|
||||
ERROR_FN(
|
||||
"dispatch_incoming_fix_messages",
|
||||
"Failed to generate exe report. Did not resolve bts key from "
|
||||
"exch clientOrderId '{}'",
|
||||
curr.client_order_id);
|
||||
continue;
|
||||
}
|
||||
auto exch_key = me_->key_map.get_exch_key(*bts_key);
|
||||
if (exch_key != curr.client_order_id) {
|
||||
INFO_FN(
|
||||
"dispatch_incoming_fix_messages",
|
||||
"Ignoring update w/ old clientOrderId '{}'. The current "
|
||||
"clientOrderId is '{}' (this is expected for CancelReplace "
|
||||
"ops)",
|
||||
curr.client_order_id);
|
||||
continue;
|
||||
}
|
||||
bts::orderman::ExecutionReport er;
|
||||
if (!convert_execution_report(
|
||||
curr, VENUE, me_->account.name(), &me_->jetfix_connection,
|
||||
er)) {
|
||||
WARN_FN(
|
||||
"dispatch_incoming_fix_messages",
|
||||
"Failed to generate exe report from OrderUpdate: {}",
|
||||
json_formatter(curr));
|
||||
continue;
|
||||
}
|
||||
er.security = me_->sec_oracle.get_security(VENUE, curr.symbol);
|
||||
string error;
|
||||
if (!validate(er, error)) {
|
||||
ERROR_FN(
|
||||
"dispatch_incoming_fix_messages",
|
||||
"Generated invalid ExecutionReport. {}", error);
|
||||
continue;
|
||||
}
|
||||
INFO_FN(
|
||||
"dispatch_incoming_fix_messages",
|
||||
"Dispatching ExecutionReport. bts_id: {}, exch_id: {}",
|
||||
er.order_key, er.exchange_order_id);
|
||||
me_->callbacks.exe_report(
|
||||
std::make_shared<bts::orderman::ExecutionReport>(std::move(er)),
|
||||
{});
|
||||
}
|
||||
}
|
||||
|
||||
// Account updates
|
||||
{
|
||||
user_data_streams::AccountUpdate u;
|
||||
auto &acct_upds = me_->account_updates_vec;
|
||||
acct_upds.clear();
|
||||
while (me_->incoming_account_updates.pop(acct_upds.emplace_back())) {
|
||||
}
|
||||
acct_upds.pop_back();
|
||||
if (!acct_upds.empty()) {
|
||||
convert_positions(
|
||||
VENUE, me_->account, acct_upds,
|
||||
[sec_oracle = &me_->sec_oracle](
|
||||
bts::api::Venue venue, const std::string &name) {
|
||||
return sec_oracle->get_security(venue, name);
|
||||
},
|
||||
me_->pos.total_exchange_positions);
|
||||
if (me_->callbacks.exchange_position_state) {
|
||||
me_->callbacks.exchange_position_state(me_->pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Callbacks
|
||||
bts::orderman::OnExeReport &ExecutionGateway::on_exe_report() {
|
||||
return me_->callbacks.exe_report;
|
||||
}
|
||||
bts::orderman::OnGwStatusChange &ExecutionGateway::on_gw_status_change() {
|
||||
return me_->callbacks.gw_status_change;
|
||||
}
|
||||
bts::orderman::OnCancelReject &ExecutionGateway::on_cancel_reject() {
|
||||
return me_->callbacks.cancel_reject;
|
||||
}
|
||||
bts::orderman::OnExchangePositionState &
|
||||
ExecutionGateway::on_exchange_position_state() {
|
||||
return me_->callbacks.exchange_position_state;
|
||||
}
|
||||
bts::orderman::OnExchangeOpenOrdersState &
|
||||
ExecutionGateway::on_exchange_open_orders() {
|
||||
return me_->callbacks.exchange_open_orders;
|
||||
}
|
||||
|
||||
} // namespace binance_execution::spots
|
||||
@@ -1,26 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution::spots {
|
||||
|
||||
struct ExecutionGatewayConfig {
|
||||
std::string api_key;
|
||||
std::string secret_key;
|
||||
std::string account;
|
||||
std::string rest_endpoint;
|
||||
std::string ws_endpoint;
|
||||
std::string ws_stream_endpoint;
|
||||
};
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(
|
||||
ExecutionGatewayConfig,
|
||||
api_key,
|
||||
secret_key,
|
||||
account,
|
||||
rest_endpoint,
|
||||
ws_endpoint,
|
||||
ws_stream_endpoint);
|
||||
|
||||
} // namespace binance_execution::spots
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "cancel_order_request.h"
|
||||
#include "binance_execution/error_list.h"
|
||||
#include "binance_execution/order_key_map.h"
|
||||
#include "binance_execution/type_converters.h"
|
||||
#include "binance_execution/type_validators.h"
|
||||
#include "lib/core/TimeStamp.h"
|
||||
#include "lib/orderman/CancelOrderMsg.h"
|
||||
#include "lib/utils/checked_arithmetic.h"
|
||||
|
||||
namespace binance_execution::spots {
|
||||
|
||||
bool CancelOrderRequest::init(
|
||||
const bts::orderman::CancelOrderMsg &m,
|
||||
OrderKeyMap &key_map,
|
||||
std::string &error) {
|
||||
const std::size_t orig_error_sz = error.size();
|
||||
if (validate(m.security, bts::api::VENUE_BINANCE, error)) {
|
||||
symbol = m.security->binance().security_desc();
|
||||
}
|
||||
orderId = bts::system::math::checked_ultol(m.exchange_order_id.value());
|
||||
origClientOrderId = key_map.get_exch_key(m.key);
|
||||
// The exchange accepts either orderId, or origClientOrderId (only one
|
||||
// need be populated).
|
||||
if (orderId == 0 && origClientOrderId.empty()) {
|
||||
ErrorList::append(
|
||||
error, "Either orderId or origClientOrderId must be defined");
|
||||
} else {
|
||||
// Can't reuse the same order id (only increment order version
|
||||
// when init is successful, as this means the order will be sent).
|
||||
key_map.increment_order_version(m.key);
|
||||
newClientOrderId = key_map.get_exch_key(m.key);
|
||||
}
|
||||
timestamp = convert_time_stamp(bts::core::TimeStamp::now());
|
||||
return error.size() == orig_error_sz;
|
||||
}
|
||||
|
||||
} // namespace binance_execution::spots
|
||||
@@ -4,6 +4,14 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace bts::orderman {
|
||||
struct CancelOrderMsg;
|
||||
}
|
||||
|
||||
namespace binance_execution {
|
||||
struct OrderKeyMap;
|
||||
}
|
||||
|
||||
namespace binance_execution::spots {
|
||||
|
||||
/**
|
||||
@@ -17,6 +25,11 @@ struct CancelOrderRequest {
|
||||
OrderCancelRestriction cancelRestrictions = OrderCancelRestriction::NONE;
|
||||
std::int64_t recvWindow = 0;
|
||||
std::int64_t timestamp = 0;
|
||||
|
||||
bool init(
|
||||
const bts::orderman::CancelOrderMsg &m,
|
||||
OrderKeyMap &key_map,
|
||||
std::string &err);
|
||||
};
|
||||
|
||||
void to_json(nlohmann::json &j, const CancelOrderRequest &req);
|
||||
|
||||
@@ -2,9 +2,31 @@
|
||||
#include "binance_execution/enums/new_order_response_type.h"
|
||||
#include "binance_execution/price.h"
|
||||
#include "binance_execution/quantity.h"
|
||||
#include "binance_execution/type_converters.h"
|
||||
#include "binance_execution/type_validators.h"
|
||||
#include "lib/orderman/CreateOrderMsg.h"
|
||||
|
||||
namespace binance_execution::spots {
|
||||
|
||||
bool CreateOrderRequest::init(
|
||||
const bts::orderman::CreateOrderMsg &m, std::string &error) {
|
||||
const std::size_t orig_error_sz = error.size();
|
||||
newClientOrderId = std::to_string(m.key.value());
|
||||
if (validate(m.security, bts::api::VENUE_BINANCE, error)) {
|
||||
symbol = m.security->binance().security_desc();
|
||||
}
|
||||
convert_side(m.side, side, error);
|
||||
price = m.price.value();
|
||||
quantity = m.quantity.full_value();
|
||||
convert_order_type(m.order_type, type, error);
|
||||
convert_time_in_force(m.time_in_force, timeInForce, error);
|
||||
if (error.size() != orig_error_sz) {
|
||||
return false;
|
||||
}
|
||||
timestamp = convert_time_stamp(bts::core::TimeStamp::now());
|
||||
return true;
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const CreateOrderRequest &req) {
|
||||
j["symbol"] = req.symbol;
|
||||
j["side"] = req.side;
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace bts::orderman {
|
||||
struct CreateOrderMsg;
|
||||
}
|
||||
|
||||
namespace binance_execution::spots {
|
||||
|
||||
/**
|
||||
@@ -24,6 +28,8 @@ struct CreateOrderRequest {
|
||||
TimeInForce timeInForce = TimeInForce::NONE;
|
||||
std::int64_t recvWindow = 0;
|
||||
NewOrderResponseType newOrderRespType = NewOrderResponseType::NONE;
|
||||
|
||||
bool init(const bts::orderman::CreateOrderMsg &m, std::string &error);
|
||||
};
|
||||
|
||||
void to_json(nlohmann::json &j, const CreateOrderRequest &req);
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution::spots {
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/derivatives/usds-margined-futures/user-data-streams
|
||||
*/
|
||||
struct ListenKeyRequest {};
|
||||
|
||||
inline void to_query_params(
|
||||
std::string & /*qps*/, const ListenKeyRequest & /*req*/) {}
|
||||
|
||||
} // namespace binance_execution::spots
|
||||
@@ -1,9 +1,14 @@
|
||||
#include "modify_order_request.h"
|
||||
#include "binance_execution/enums/new_order_response_type.h"
|
||||
#include "binance_execution/enums/time_in_force.h"
|
||||
#include "binance_execution/error_list.h"
|
||||
#include "binance_execution/order_key_map.h"
|
||||
#include "binance_execution/price.h"
|
||||
#include "binance_execution/quantity.h"
|
||||
#include "binance_execution/type_converters.h"
|
||||
#include "binance_execution/type_validators.h"
|
||||
#include "lib/io/JsonReader.h"
|
||||
#include "lib/orderman/CancelReplaceOrderMsg.h"
|
||||
|
||||
namespace binance_execution::spots {
|
||||
|
||||
@@ -177,4 +182,44 @@ void to_query_params(std::string &qps, const ModifyOrderRequest &req) {
|
||||
qps.append(std::to_string(req.timestamp));
|
||||
}
|
||||
|
||||
bool ModifyOrderRequest::init(
|
||||
const bts::orderman::CancelReplaceOrderMsg &m,
|
||||
OrderKeyMap &key_map,
|
||||
std::string &error) {
|
||||
const auto orig_error_sz = error.size();
|
||||
const auto orig_key = m.create_fields.key;
|
||||
cancelOrderId = 0;
|
||||
cancelOrigClientOrderId = key_map.get_exch_key(orig_key);
|
||||
// The exchange accepts either cancelOrderId, or cancelOrigClientOrderId (only one
|
||||
// need be populated).
|
||||
if (cancelOrderId == 0 && cancelOrigClientOrderId.empty()) {
|
||||
ErrorList::append(
|
||||
error,
|
||||
"Either cancelOrderId or cancelOrigClientOrderId must be defined");
|
||||
}
|
||||
price = m.create_fields.price.value();
|
||||
quantity = m.create_fields.quantity.full_value();
|
||||
if (quantity == 0) {
|
||||
ErrorList::append(error, "quantity is 0");
|
||||
}
|
||||
if (validate(m.create_fields.security, bts::api::VENUE_BINANCE, error)) {
|
||||
symbol = m.create_fields.security->binance().security_desc();
|
||||
}
|
||||
convert_side(m.create_fields.side, side, error);
|
||||
convert_order_type(m.create_fields.order_type, type, error);
|
||||
cancelReplaceMode = CancelReplaceMode::STOP_ON_FAILURE;
|
||||
convert_time_in_force(m.create_fields.time_in_force, timeInForce, error);
|
||||
const bool is_valid = error.size() == orig_error_sz;
|
||||
if (is_valid) {
|
||||
// Can't reuse the same order id.
|
||||
key_map.increment_order_version(orig_key);
|
||||
cancelNewClientOrderId =
|
||||
key_map.get_exch_key(orig_key).append(CANCEL_REPLACE_CANCEL_SUFFIX);
|
||||
key_map.increment_order_version(orig_key);
|
||||
newClientOrderId = key_map.get_exch_key(orig_key);
|
||||
timestamp = convert_time_stamp(bts::core::TimeStamp::now());
|
||||
}
|
||||
return is_valid;
|
||||
}
|
||||
|
||||
} // namespace binance_execution::spots
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "binance_execution/enums/self_trade_prevention_mode.h"
|
||||
#include "binance_execution/enums/side.h"
|
||||
#include "binance_execution/enums/time_in_force.h"
|
||||
#include "binance_execution/order_key_map.h"
|
||||
#include "lib/orderman/CancelReplaceOrderMsg.h"
|
||||
|
||||
#include <limits>
|
||||
#include <nlohmann/json.hpp>
|
||||
@@ -35,6 +37,8 @@ struct ModifyOrderRequest {
|
||||
orderRateLimitExceededMode(OrderRateLimitExceededMode::NONE),
|
||||
recvWindow(0), timestamp(0) {}
|
||||
|
||||
static constexpr const char *CANCEL_REPLACE_CANCEL_SUFFIX = "crc";
|
||||
|
||||
std::string symbol; // mandatory
|
||||
Side side; // mandatory
|
||||
OrderType type; // mandatory
|
||||
@@ -59,6 +63,11 @@ struct ModifyOrderRequest {
|
||||
std::int64_t recvWindow; // optional
|
||||
std::int64_t timestamp; // mandatory
|
||||
// * Either cancelOrderId or cancelOrigClientOrderId must be specified
|
||||
|
||||
bool init(
|
||||
const bts::orderman::CancelReplaceOrderMsg &m,
|
||||
OrderKeyMap &key_map,
|
||||
std::string &error);
|
||||
};
|
||||
|
||||
void to_json(nlohmann::json &j, const ModifyOrderRequest &req);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "rest_api_client.h"
|
||||
#include "binance_execution/rest_api_error.h"
|
||||
#include "lib/io/http/http_client.h"
|
||||
#include "lib/io/http/http_response.h"
|
||||
#include "lib/io/json_formatter.h"
|
||||
#include "lib/utils/crypto/hmac.h"
|
||||
|
||||
namespace binance_execution::spots {
|
||||
@@ -78,9 +80,15 @@ void RestApiClient::request_(
|
||||
shrink_string<4096, 128>(http_res.body);
|
||||
http_res.reset();
|
||||
HttpClient::send(http_req, &http_res);
|
||||
res.http_code = http_res.code;
|
||||
res.http_error = http_res.errors;
|
||||
if (!http_res.ok()) {
|
||||
ERROR_FN("{} {}", "res: {}", method, path, http_res);
|
||||
res.error = std::move(http_res.errors);
|
||||
try {
|
||||
nlohmann::json::parse(http_res.body).get_to(res.error.emplace());
|
||||
} catch (const std::exception &ex) {
|
||||
ERROR_FN("request", "Failed to parse error json. {}", http_res);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Parse response
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/messages/listen_key_request.h"
|
||||
#include "binance_execution/messages/listen_key_response.h"
|
||||
#include "binance_execution/rest_api_result.h"
|
||||
#include "binance_execution/spots/messages/all_orders_request.h"
|
||||
#include "binance_execution/spots/messages/cancel_order_request.h"
|
||||
#include "binance_execution/spots/messages/create_order_request.h"
|
||||
#include "binance_execution/spots/messages/listen_key_request.h"
|
||||
#include "binance_execution/spots/messages/listen_key_response.h"
|
||||
#include "binance_execution/spots/messages/modify_order_request.h"
|
||||
#include "binance_execution/spots/messages/modify_order_response.h"
|
||||
#include "binance_execution/spots/messages/order_record.h"
|
||||
|
||||
@@ -0,0 +1,490 @@
|
||||
#include "spots_gateway.h"
|
||||
#include "binance_execution/execution_report_store.h"
|
||||
#include "binance_execution/gateway_config.h"
|
||||
#include "binance_execution/listen_key_requestor.h"
|
||||
#include "binance_execution/order_key_map.h"
|
||||
#include "binance_execution/rest_api_result.h"
|
||||
#include "binance_execution/spots/messages/modify_order_request.h"
|
||||
#include "binance_execution/spots/rest_api_client.h"
|
||||
#include "binance_execution/spots/user_data_stream/user_data_subscription.h"
|
||||
#include "binance_execution/type_validators.h"
|
||||
#include "lib/api/service_helpers/AdminServiceUtils.h"
|
||||
#include "lib/api/services/AdminService.pb.h"
|
||||
#include "lib/api/services/OrderService.h"
|
||||
#include "lib/config/protos/jetfix.pb.h"
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/domain_model/Security.pb.h"
|
||||
#include "lib/domain_model/Side.h"
|
||||
#include "lib/io/json_formatter.h"
|
||||
#include "lib/logging/logging.h"
|
||||
#include "lib/md/SecurityOracle.h"
|
||||
#include "lib/mercury/events/Update.h"
|
||||
#include "lib/orderman/CancelOrderMsg.h"
|
||||
#include "lib/orderman/CancelReplaceOrderMsg.h"
|
||||
#include "lib/orderman/CreateOrderMsg.h"
|
||||
#include "lib/orderman/RFQMsg.h"
|
||||
#include "lib/orderman/events/NewAdminDataEvent.h"
|
||||
#include "lib/orderman/events/NewSecuritiesEvent.h"
|
||||
#include "lib/orderman/orderman_utils/ExchangePosition.h"
|
||||
#include "lib/orderman/orderman_utils/ExecutionReport.h"
|
||||
#include "lib/orderman/orderman_utils/orderman_common.h"
|
||||
#include "lib/utils/overloaded.h"
|
||||
|
||||
#include <boost/lockfree/spsc_queue.hpp>
|
||||
#include <fmt/format.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
using nlohmann::json;
|
||||
|
||||
namespace binance_execution::spots {
|
||||
|
||||
#define ASSERT_FN(test, fn, ...) \
|
||||
bts_assert_msgf(test, "Binance::" fn ". " __VA_ARGS__)
|
||||
#define LOG_FN(level, fn, ...) \
|
||||
LOG(OrderMan, level, "Binance::" fn ". " __VA_ARGS__)
|
||||
#define DIAG_FN(fn, ...) LOG_FN(bts::Diag, fn, __VA_ARGS__)
|
||||
#define INFO_FN(fn, ...) LOG_FN(bts::Info, fn, __VA_ARGS__)
|
||||
#define WARN_FN(fn, ...) LOG_FN(bts::Warn, fn, __VA_ARGS__)
|
||||
#define ERROR_FN(fn, ...) LOG_FN(bts::Error, fn, __VA_ARGS__)
|
||||
|
||||
#define VENUE bts::api::VENUE_BINANCE
|
||||
|
||||
using boost::lockfree::spsc_queue;
|
||||
using bts::core::Delegate;
|
||||
using bts::io::json_formatter;
|
||||
using bts::orderman::CancelOrderMsg;
|
||||
using bts::orderman::CancelReplaceOrderMsg;
|
||||
using bts::orderman::CreateOrderMsg;
|
||||
using bts::orderman::ExecutionReport;
|
||||
using bts::orderman::NewAdminDataEvent;
|
||||
using bts::orderman::NewSecuritiesEvent;
|
||||
using bts::orderman::OrderMsg;
|
||||
using bts::orderman::RFQMsg;
|
||||
using std::int64_t;
|
||||
using std::optional;
|
||||
using std::shared_ptr;
|
||||
using std::size_t;
|
||||
using std::string;
|
||||
using std::uint32_t;
|
||||
using std::vector;
|
||||
|
||||
struct ExecutionGatewayCallbacks {
|
||||
bts::orderman::OnExeReport exe_report;
|
||||
bts::orderman::OnGwStatusChange gw_status_change;
|
||||
bts::orderman::OnCancelReject cancel_reject;
|
||||
bts::orderman::OnExchangePositionState exchange_position_state;
|
||||
bts::orderman::OnExchangeOpenOrdersState exchange_open_orders;
|
||||
};
|
||||
|
||||
struct SpotsGateway::PImpl {
|
||||
explicit PImpl(
|
||||
bts::md::SecurityOracle sec_oracle,
|
||||
GatewayConfig config,
|
||||
bts::config::JetFIX_Connection jetfix_connection)
|
||||
: sec_oracle(std::move(sec_oracle)), config(std::move(config)),
|
||||
jetfix_connection(std::make_shared<bts::config::JetFIX_Connection>(
|
||||
std::move(jetfix_connection))),
|
||||
rest_api_client(
|
||||
this->config.rest_endpoint,
|
||||
this->config.api_key,
|
||||
this->config.secret_key),
|
||||
listen_key_requestor(
|
||||
[this](const auto &req) {
|
||||
return rest_api_client.get_listen_key(req);
|
||||
},
|
||||
[this](const auto &req) {
|
||||
return rest_api_client.refresh_listen_key(req);
|
||||
}),
|
||||
key_map(std::make_shared<OrderKeyMap>()),
|
||||
orders(
|
||||
[this](const std::string &exch_key) {
|
||||
return *key_map->get_bts_key(exch_key);
|
||||
},
|
||||
this->config.account,
|
||||
this->jetfix_connection) {
|
||||
bts_assert(this->jetfix_connection->venue() == VENUE);
|
||||
bts_assert(!this->jetfix_connection->name().empty());
|
||||
pos.exchange = VENUE;
|
||||
}
|
||||
|
||||
static constexpr const char *CANCEL_REPLACE_CANCEL_SUFFIX = "crc";
|
||||
|
||||
bts::md::SecurityOracle sec_oracle;
|
||||
GatewayConfig config;
|
||||
std::shared_ptr<bts::config::JetFIX_Connection> jetfix_connection;
|
||||
|
||||
RestApiClient rest_api_client;
|
||||
optional<user_data_stream::UserDataSubscription> user_data_sub;
|
||||
Delegate<void(user_data_stream::AccountUpdate)> account_update_callback;
|
||||
Delegate<void(user_data_stream::OrderUpdate)> order_update_callback;
|
||||
ListenKeyRequestor listen_key_requestor;
|
||||
|
||||
spsc_queue<user_data_stream::AccountUpdate> incoming_account_updates{1024};
|
||||
spsc_queue<user_data_stream::OrderUpdate> incoming_order_updates{1024};
|
||||
spsc_queue<shared_ptr<const ExecutionReport>> internal_order_rejects{256};
|
||||
vector<user_data_stream::AccountUpdate> account_updates_vec;
|
||||
|
||||
ExecutionGatewayCallbacks callbacks;
|
||||
bts::orderman::ExchangePosition pos;
|
||||
bts::api::admin::BTSAccount account;
|
||||
std::shared_ptr<OrderKeyMap> key_map;
|
||||
ExecutionReportStore orders;
|
||||
|
||||
void start() {
|
||||
INFO_FN("start");
|
||||
listen_key_requestor.callback().bind<&PImpl::on_listen_key_>(*this);
|
||||
listen_key_requestor.start();
|
||||
}
|
||||
|
||||
void stop() {
|
||||
INFO_FN("stop");
|
||||
user_data_sub.reset();
|
||||
}
|
||||
|
||||
void on_account_update(user_data_stream::AccountUpdate u) {
|
||||
INFO_FN("on_account_update", "\n{:2}", json_formatter(u));
|
||||
incoming_account_updates.push(std::move(u));
|
||||
}
|
||||
|
||||
void on_order_update(user_data_stream::OrderUpdate u) {
|
||||
INFO_FN("on_order_update", "\n{:2}", json_formatter(u));
|
||||
if (u.orig_client_order_id.ends_with(CANCEL_REPLACE_CANCEL_SUFFIX)) {
|
||||
// This is the "canceled" order part of a cancel/replace op. This op
|
||||
// generates another update without such a suffix. That's the new
|
||||
// order; that's the one we want.
|
||||
DIAG_FN(
|
||||
"on_order_update",
|
||||
"Ignoring cancel/replace cancel order update. id: {}",
|
||||
u.client_order_id);
|
||||
return;
|
||||
}
|
||||
incoming_order_updates.push(std::move(u));
|
||||
}
|
||||
|
||||
void on_listen_key_(const string &listen_key) {
|
||||
INFO_FN("on_listen_key", "value: {}", listen_key);
|
||||
user_data_sub.reset();
|
||||
if (listen_key.empty()) {
|
||||
LOG(OrderMan, bts::Error,
|
||||
"Unable to subscribe to user data because 'listen_key' "
|
||||
"is empty.");
|
||||
return;
|
||||
}
|
||||
auto &uds = user_data_sub.emplace(
|
||||
config.ws_stream_endpoint + "/ws/" + listen_key);
|
||||
uds.account_update_callback().bind<&PImpl::on_account_update>(*this);
|
||||
uds.order_update_callback().bind<&PImpl::on_order_update>(*this);
|
||||
uds.start();
|
||||
}
|
||||
|
||||
void create_order(const CreateOrderMsg &m) {
|
||||
CreateOrderRequest req;
|
||||
std::string err;
|
||||
if (!req.init(m, err)) {
|
||||
ERROR_FN(
|
||||
"create_order",
|
||||
"{} {} {} @ {} (id={}, type={}, tif={}). Rejected "
|
||||
"internally (request not sent). {}",
|
||||
m.side, m.quantity, req.symbol, m.price, m.key, m.order_type,
|
||||
m.time_in_force, err);
|
||||
internal_order_rejects.push(orders.reject(m, err));
|
||||
return;
|
||||
}
|
||||
INFO_FN(
|
||||
"create_order",
|
||||
"{} {} {} @ {} (id={}, type={}, tif={}). Sending request", m.side,
|
||||
m.quantity, req.symbol, m.price, m.key, m.order_type,
|
||||
m.time_in_force);
|
||||
const auto res = rest_api_client.create_order(req);
|
||||
if (!res.ok()) {
|
||||
ERROR_FN("create_order", "id={}. {}", m.key, json_formatter(res));
|
||||
internal_order_rejects.push(
|
||||
orders.reject(m, json_formatter(*res.error).to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
void update_order(const CancelReplaceOrderMsg &m) {
|
||||
const auto &fields = m.create_fields;
|
||||
RestApiResult<ModifyOrderResponse> res;
|
||||
ModifyOrderRequest req;
|
||||
std::string err;
|
||||
if (!req.init(m, *key_map, err)) {
|
||||
ERROR_FN(
|
||||
"update_order",
|
||||
"{} {} {} @ {} (id={}, exch_id={}, type={}, tif={}). Rejected "
|
||||
"internally (request not sent). {}",
|
||||
fields.side, fields.quantity, req.symbol, fields.price, fields.key,
|
||||
m.exchange_order_id, fields.order_type, fields.time_in_force, err);
|
||||
internal_order_rejects.push(orders.set_error(
|
||||
fields.key,
|
||||
"Update rejected internally (request not sent). " + err));
|
||||
return;
|
||||
}
|
||||
INFO_FN(
|
||||
"update_order",
|
||||
"{} {} {} @ {} (id={}, type={}, tif={}). Sending request",
|
||||
fields.side, fields.quantity, req.symbol, fields.price, fields.key,
|
||||
m.exchange_order_id, fields.order_type, fields.time_in_force);
|
||||
res = rest_api_client.modify_order(req);
|
||||
if (!res.ok()) {
|
||||
ERROR_FN(
|
||||
"update_order", "id={}, exch_id={}. {}", fields.key,
|
||||
m.exchange_order_id, json_formatter(res));
|
||||
internal_order_rejects.push(orders.set_error(
|
||||
fields.key,
|
||||
fmt::format(
|
||||
"Update request failed. {}", json_formatter(*res.error))));
|
||||
}
|
||||
}
|
||||
|
||||
void cancel_order(const CancelOrderMsg &m) {
|
||||
CancelOrderRequest req;
|
||||
std::string err;
|
||||
if (!req.init(m, *key_map, err)) {
|
||||
ERROR_FN(
|
||||
"cancel_order", "{} {} (id={}, exch_id={}). {}", m.side,
|
||||
req.symbol, m.key, m.exchange_order_id, err);
|
||||
internal_order_rejects.push(orders.set_error(
|
||||
m.key, "Rejected internally (request not sent). " + err));
|
||||
return;
|
||||
}
|
||||
INFO_FN(
|
||||
"cancel_order", "{} {} (id={}, exch_id={})", m.side, req.symbol,
|
||||
m.key, m.exchange_order_id);
|
||||
const auto res = rest_api_client.cancel_order(req);
|
||||
if (!res.ok()) {
|
||||
ERROR_FN(
|
||||
"cancel_order", "{} {} (id={}, exch_id={}). {}", m.side,
|
||||
req.symbol, m.key, m.exchange_order_id, json_formatter(res));
|
||||
internal_order_rejects.push(orders.set_error(
|
||||
m.key,
|
||||
fmt::format(
|
||||
"Cancel request failed. {}", json_formatter(*res.error))));
|
||||
}
|
||||
}
|
||||
|
||||
void dispatch_internal_rejects() {
|
||||
shared_ptr<const ExecutionReport> curr;
|
||||
while (internal_order_rejects.pop(curr)) {
|
||||
callbacks.exe_report(curr, {});
|
||||
}
|
||||
}
|
||||
|
||||
void dispatch_order_updates() {
|
||||
if (incoming_order_updates.empty()) {
|
||||
return;
|
||||
}
|
||||
user_data_stream::OrderUpdate curr;
|
||||
auto &km = *key_map;
|
||||
while (incoming_order_updates.pop(curr)) {
|
||||
auto bts_key = km.get_bts_key(curr.client_order_id);
|
||||
if (!bts_key) {
|
||||
ERROR_FN(
|
||||
"dispatch_order_updates",
|
||||
"Failed to generate exe report. Did not resolve bts key from "
|
||||
"exch clientOrderId '{}'",
|
||||
curr.client_order_id);
|
||||
continue;
|
||||
}
|
||||
auto exch_key = km.get_exch_key(*bts_key);
|
||||
if (exch_key != curr.client_order_id) {
|
||||
INFO_FN(
|
||||
"dispatch_order_updates",
|
||||
"Ignoring update w/ old clientOrderId '{}' (expected '{}', "
|
||||
"exch_id={}). This is expected for CancelReplace ops.",
|
||||
curr.client_order_id, curr.order_id, exch_key);
|
||||
continue;
|
||||
}
|
||||
// orders
|
||||
shared_ptr<const ExecutionReport> er_p =
|
||||
orders.update(curr.client_order_id, [&](auto &er_p, bool) {
|
||||
auto &er = *er_p;
|
||||
std::string error;
|
||||
if (!curr.to_execution_report(er, error)) {
|
||||
WARN_FN(
|
||||
"update", "Failed to initialize exe report. {}. {}",
|
||||
error, json_formatter(curr));
|
||||
return;
|
||||
}
|
||||
er.security = sec_oracle.get_security(
|
||||
VENUE, bts::api::SECURITYTYPE_SPOT, curr.symbol);
|
||||
});
|
||||
const ExecutionReport &er = *er_p;
|
||||
string error;
|
||||
if (!validate(er, error)) {
|
||||
ERROR_FN(
|
||||
"dispatch_order_updates",
|
||||
"Generated invalid ExecutionReport (id={}, exch_id={}). {}",
|
||||
curr.client_order_id, curr.order_id, error);
|
||||
continue;
|
||||
}
|
||||
switch (curr.order_status.value) {
|
||||
case OrderStatus::FILLED:
|
||||
case OrderStatus::CANCELED:
|
||||
case OrderStatus::REJECTED: {
|
||||
const size_t n_active = orders.erase(er.order_key);
|
||||
INFO_FN(
|
||||
"dispatch_order_updates",
|
||||
"Order reached terminal state {} (id={}, exch_id={}). {} "
|
||||
"active orders.",
|
||||
curr.order_status, curr.client_order_id, curr.order_id,
|
||||
n_active);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
INFO_FN(
|
||||
"dispatch_order_updates",
|
||||
"Firing exe report callback. {} {} {} @ {} {} (filled {}/{}, "
|
||||
"id={}). {} active orders",
|
||||
er.side, er.security->binance().security_desc(), er.quantity,
|
||||
er.price, er.order_status, er.filled_quantity, er.quantity,
|
||||
er.order_key, orders.size());
|
||||
callbacks.exe_report(std::move(er_p), {});
|
||||
}
|
||||
}
|
||||
|
||||
void dispatch_account_updates() {
|
||||
user_data_stream::AccountUpdate u;
|
||||
auto &acct_upds = account_updates_vec;
|
||||
acct_upds.clear();
|
||||
while (incoming_account_updates.pop(acct_upds.emplace_back())) {
|
||||
}
|
||||
acct_upds.pop_back();
|
||||
if (acct_upds.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto pos_ticker_comp =
|
||||
[](const bts::orderman::ExchangePosition::UnitPosition &unit_pos,
|
||||
const string &ticker) {
|
||||
return unit_pos.exchange_sec_desc < ticker;
|
||||
};
|
||||
auto &dest = pos.total_exchange_positions;
|
||||
for (const auto &acct_upd : acct_upds) {
|
||||
for (const auto &pos : acct_upd.balances) {
|
||||
bts_assert(pos.free >= 0);
|
||||
bts_assert(pos.locked >= 0);
|
||||
auto it = std::lower_bound(
|
||||
dest.begin(), dest.end(), pos.asset, pos_ticker_comp);
|
||||
if (it == dest.end() || it->exchange_sec_desc != pos.asset) {
|
||||
const auto *sec = sec_oracle.get_security(
|
||||
VENUE, bts::api::SECURITYTYPE_SPOT, pos.asset);
|
||||
if (sec == nullptr) {
|
||||
continue;
|
||||
}
|
||||
bts::orderman::ExchangePosition::UnitPosition new_pos;
|
||||
new_pos.exchange_sec_desc = pos.asset;
|
||||
new_pos.security = sec;
|
||||
new_pos.account_id = account.id();
|
||||
new_pos.account_name = account.name();
|
||||
it = dest.insert(it, std::move(new_pos));
|
||||
}
|
||||
it->position = pos.locked + pos.free;
|
||||
it->avg_filled_price.value() =
|
||||
std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
}
|
||||
callbacks.exchange_position_state(pos);
|
||||
}
|
||||
};
|
||||
|
||||
SpotsGateway::SpotsGateway(
|
||||
bts::md::SecurityOracle sec_oracle,
|
||||
GatewayConfig session_cfg,
|
||||
bts::config::JetFIX::Connection jetfix_connection)
|
||||
: me_(std::make_unique<PImpl>(
|
||||
std::move(sec_oracle),
|
||||
std::move(session_cfg),
|
||||
std::move(jetfix_connection))) {
|
||||
bts_assert(
|
||||
me_->config.account.size() <=
|
||||
bts::orderman::ExecutionReport{}.account_name.value().size());
|
||||
}
|
||||
|
||||
SpotsGateway::~SpotsGateway() = default;
|
||||
|
||||
void SpotsGateway::start() { me_->start(); }
|
||||
|
||||
void SpotsGateway::on_securities(
|
||||
const bts::mercury::Update<NewSecuritiesEvent> & /*update*/) {
|
||||
INFO_FN(
|
||||
"on_securities", "t_id: {} t_name: {}", bts::os::get_thread_id(),
|
||||
bts::os::get_thread_name());
|
||||
}
|
||||
|
||||
void SpotsGateway::on_admin_data(
|
||||
const bts::mercury::Update<NewAdminDataEvent> &update) {
|
||||
INFO_FN(
|
||||
"on_admin_data", "t_id: {} t_name: {}", bts::os::get_thread_id(),
|
||||
bts::os::get_thread_name());
|
||||
if (update.events == nullptr || update.events->empty()) {
|
||||
return;
|
||||
}
|
||||
const auto *account = bts::admin::AdminServiceUtils::find_account(
|
||||
*update.events->back().admin_data, me_->config.account);
|
||||
if (account == nullptr) {
|
||||
WARN_FN(
|
||||
"on_admin_data",
|
||||
"Gateway account not found. Cannot execute orders. name: '{}'",
|
||||
me_->config.account);
|
||||
return;
|
||||
}
|
||||
if (me_->account.name() == account->name() &&
|
||||
me_->account.id() == account->id()) {
|
||||
return;
|
||||
}
|
||||
me_->account.CopyFrom(*account);
|
||||
INFO_FN("on_admin_data", "Resolved gateway account '{}'", account->name());
|
||||
}
|
||||
|
||||
void SpotsGateway::send_msgs(
|
||||
vector<OrderMsg> const &msg_vec, bts::trace::CorID) {
|
||||
INFO_FN("send_msgs", "count: {}", msg_vec.size());
|
||||
for (const auto &msg : msg_vec) {
|
||||
std::visit(
|
||||
bts::overloaded{
|
||||
[&](const CreateOrderMsg &m) { me_->create_order(m); },
|
||||
[&](const CancelReplaceOrderMsg &m) { me_->update_order(m); },
|
||||
[&](const CancelOrderMsg &m) { me_->cancel_order(m); },
|
||||
[&](const RFQMsg &) {
|
||||
bts_assert_msg(false, "Binance RFQ not implemented");
|
||||
},
|
||||
},
|
||||
msg);
|
||||
}
|
||||
}
|
||||
|
||||
void SpotsGateway::send_fix(const bts::jetfix::OutFIXPtr &, bts::trace::CorID) {
|
||||
bts_assert_msg(false, "ExecutionGateway does not support fix");
|
||||
}
|
||||
|
||||
void SpotsGateway::dispatch_incoming_fix_messages() {
|
||||
me_->dispatch_internal_rejects();
|
||||
me_->dispatch_order_updates();
|
||||
me_->dispatch_account_updates();
|
||||
}
|
||||
|
||||
// Callbacks
|
||||
bts::orderman::OnExeReport &SpotsGateway::on_exe_report() {
|
||||
return me_->callbacks.exe_report;
|
||||
}
|
||||
bts::orderman::OnGwStatusChange &SpotsGateway::on_gw_status_change() {
|
||||
return me_->callbacks.gw_status_change;
|
||||
}
|
||||
bts::orderman::OnCancelReject &SpotsGateway::on_cancel_reject() {
|
||||
return me_->callbacks.cancel_reject;
|
||||
}
|
||||
bts::orderman::OnExchangePositionState &
|
||||
SpotsGateway::on_exchange_position_state() {
|
||||
return me_->callbacks.exchange_position_state;
|
||||
}
|
||||
bts::orderman::OnExchangeOpenOrdersState &
|
||||
SpotsGateway::on_exchange_open_orders() {
|
||||
return me_->callbacks.exchange_open_orders;
|
||||
}
|
||||
|
||||
} // namespace binance_execution::spots
|
||||
+17
-8
@@ -1,21 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/spots/execution_gateway_config.h"
|
||||
#include "lib/config/protos/jetfix.pb.h"
|
||||
#include "lib/md/SecurityOracle.h"
|
||||
#include "lib/orderman/gateways/IExecutionGateway.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace bts::config {
|
||||
class JetFIX_Connection;
|
||||
}
|
||||
|
||||
namespace bts::md {
|
||||
class SecurityOracle;
|
||||
}
|
||||
|
||||
namespace binance_execution {
|
||||
struct GatewayConfig;
|
||||
}
|
||||
|
||||
namespace binance_execution::spots {
|
||||
|
||||
class ExecutionGateway : public bts::orderman::IExecutionGateway {
|
||||
class SpotsGateway : public bts::orderman::IExecutionGateway {
|
||||
public:
|
||||
explicit ExecutionGateway(
|
||||
explicit SpotsGateway(
|
||||
bts::md::SecurityOracle sec_oracle,
|
||||
ExecutionGatewayConfig session_cfg,
|
||||
bts::config::JetFIX::Connection jetfix_connection);
|
||||
~ExecutionGateway() override;
|
||||
GatewayConfig session_cfg,
|
||||
bts::config::JetFIX_Connection jetfix_connection);
|
||||
~SpotsGateway() override;
|
||||
|
||||
void start() override;
|
||||
void dispatch_incoming_fix_messages() override;
|
||||
@@ -1,187 +0,0 @@
|
||||
#include "type_converters.h"
|
||||
#include "binance_execution/spots/messages/order_record.h"
|
||||
#include "binance_execution/type_converters.h"
|
||||
#include "lib/api/services/AdminService.pb.h"
|
||||
#include "lib/domain_model/common.h"
|
||||
#include "lib/utils/checked_arithmetic.h"
|
||||
#include "lib/utils/string_utils.h"
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
#include <utility>
|
||||
|
||||
namespace binance_execution::spots {
|
||||
|
||||
using bts::system::string_utils::copy_to_array;
|
||||
using bts::system::string_utils::parse_uint;
|
||||
using std::string;
|
||||
using std::vector;
|
||||
|
||||
bool convert_execution_report(
|
||||
const user_data_streams::OrderUpdate &u,
|
||||
bts::api::Venue venue,
|
||||
const std::string &account,
|
||||
const bts::config::JetFIX_Connection *jetfix_cfg,
|
||||
bts::orderman::ExecutionReport &er) {
|
||||
copy_to_array<12>(account, er.account_name.value());
|
||||
er.exchange = venue;
|
||||
er.transact_time = bts::orderman::ExchangeTimeStamp64{
|
||||
convert_time_stamp(u.transaction_time).nanos()};
|
||||
if (!parse_uint(u.client_order_id, er.order_key.value())) {
|
||||
// For CancelReplace ops, we append a version number to the original key (separated by an
|
||||
// underscore). Extract the original key.
|
||||
const auto version_sep = u.client_order_id.find_last_of('_');
|
||||
if (version_sep != std::string::npos) {
|
||||
parse_uint(
|
||||
u.client_order_id.begin(), u.client_order_id.begin() + version_sep,
|
||||
er.order_key.value());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bts_assert(bts::system::math::checked_ltoul(
|
||||
u.order_id,
|
||||
er.exchange_order_id.emplace<bts::orderman::ExchangeOrderID64>()
|
||||
.value()));
|
||||
er.security = nullptr;
|
||||
er.leg_security = nullptr;
|
||||
er.side = u.side == Side::BUY ? bts::api::SIDE_BUY : bts::api::SIDE_SELL;
|
||||
er.time_in_force = convert_time_in_force(u.time_in_force);
|
||||
er.order_type = convert_order_type(u.order_type);
|
||||
er.exec_type = convert_exec_type(u.execution_type, u.order_status);
|
||||
er.order_status = convert_order_status(u.order_status);
|
||||
er.manual_order_indicator = bts::api::orderman::MANUAL_ORDER_INDICATOR_NO;
|
||||
er.price.value() = u.order_price;
|
||||
er.quantity = u.order_quantity;
|
||||
er.filled_quantity = u.filled_quantity;
|
||||
er.last_shares = u.last_executed_quantity;
|
||||
// er.max_show = u.data.;
|
||||
er.last_price.value() = u.last_executed_price;
|
||||
er.avg_price.value() =
|
||||
u.filled_quantity > 0
|
||||
? u.quote_asset_transacted_quantity / u.filled_quantity
|
||||
: 0.0;
|
||||
er.stop_price.value() = u.stop_price;
|
||||
copy_to_array<40>(std::to_string(u.trade_id), er.exchange_fill_id.value());
|
||||
// er.msg = u.reject_reason;
|
||||
er.cfg = jetfix_cfg;
|
||||
// Irrelevant fields
|
||||
er.ice_cl_ord_id = 0;
|
||||
er.ice_company_id = 0;
|
||||
er.memo = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool convert_execution_report(
|
||||
const OrderRecord &res,
|
||||
bts::api::Venue venue,
|
||||
const std::string &account,
|
||||
const bts::config::JetFIX_Connection *jetfix_cfg,
|
||||
bts::orderman::ExecutionReport &er) {
|
||||
copy_to_array<12>(account, er.account_name.value());
|
||||
er.exchange = venue;
|
||||
er.transact_time = bts::orderman::ExchangeTimeStamp64{
|
||||
convert_time_stamp(res.workingTime).nanos()};
|
||||
if (!parse_uint(res.clientOrderId, er.order_key.value())) {
|
||||
return false;
|
||||
}
|
||||
bts_assert(bts::system::math::checked_ltoul(
|
||||
res.orderId,
|
||||
er.exchange_order_id.emplace<bts::orderman::ExchangeOrderID64>()
|
||||
.value()));
|
||||
er.security = nullptr;
|
||||
er.leg_security = nullptr;
|
||||
er.side = res.side == Side::BUY ? bts::api::SIDE_BUY : bts::api::SIDE_SELL;
|
||||
er.time_in_force = convert_time_in_force(res.timeInForce);
|
||||
er.order_type = convert_order_type(res.type);
|
||||
er.exec_type = bts::api::orderman::EXEC_TYPE_NEW;
|
||||
er.order_status = convert_order_status(res.status);
|
||||
er.manual_order_indicator = bts::api::orderman::MANUAL_ORDER_INDICATOR_NO;
|
||||
er.price.value() = res.price;
|
||||
er.quantity = res.origQty;
|
||||
er.filled_quantity = res.executedQty;
|
||||
er.max_show = 0;
|
||||
std::tie(er.last_price.value(), er.last_shares) =
|
||||
res.fills.empty() ? std::make_pair(
|
||||
std::numeric_limits<double>::quiet_NaN(),
|
||||
bts::Quantity::create_new(
|
||||
std::numeric_limits<double>::quiet_NaN()))
|
||||
: std::make_pair(
|
||||
res.fills.back().price,
|
||||
bts::Quantity::create_new(res.fills.back().qty));
|
||||
|
||||
const auto [total_fill_cost, total_fill_qty] = std::accumulate(
|
||||
res.fills.begin(), res.fills.end(), std::make_pair(0.0, 0.0),
|
||||
[](std::pair<double, double> sum, const OrderRecord::Fill &f) {
|
||||
return std::make_pair(sum.first + f.price, sum.second + f.qty);
|
||||
});
|
||||
if (total_fill_qty != res.executedQty) {
|
||||
LOG(OrderMan, bts::Error,
|
||||
"Binance ExecutionReport total_fill_qty != res.executedQty. {} != {}",
|
||||
total_fill_qty, res.executedQty);
|
||||
bts_assert_msgf(
|
||||
false,
|
||||
"Binance ExecutionReport total_fill_qty != res.executedQty. {} != {}",
|
||||
total_fill_qty, res.executedQty);
|
||||
return false;
|
||||
}
|
||||
er.avg_price.value() = total_fill_qty > 0
|
||||
? total_fill_cost / total_fill_qty
|
||||
: std::numeric_limits<double>::quiet_NaN();
|
||||
er.stop_price.value() = std::numeric_limits<double>::quiet_NaN();
|
||||
er.msg = "";
|
||||
er.cfg = jetfix_cfg;
|
||||
// Irrelevant fields
|
||||
er.ice_cl_ord_id = 0;
|
||||
er.ice_company_id = 0;
|
||||
er.memo = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
struct ExchPosCmp {
|
||||
bool operator()(
|
||||
const string &ticker,
|
||||
const bts::orderman::ExchangePosition::UnitPosition &u) {
|
||||
return ticker < u.exchange_sec_desc;
|
||||
}
|
||||
|
||||
bool operator()(
|
||||
const bts::orderman::ExchangePosition::UnitPosition &u,
|
||||
const string &ticker) {
|
||||
return u.exchange_sec_desc < ticker;
|
||||
}
|
||||
};
|
||||
|
||||
void convert_positions(
|
||||
bts::api::Venue venue,
|
||||
const bts::api::admin::BTSAccount &account,
|
||||
const vector<user_data_streams::AccountUpdate> &acct_upds,
|
||||
std::function<const bts::api::Security *(
|
||||
bts::api::Venue venue, const std::string &name)> get_security,
|
||||
vector<bts::orderman::ExchangePosition::UnitPosition> &dest) {
|
||||
ExchPosCmp exch_pos_cmp;
|
||||
for (const auto &acct_upd : acct_upds) {
|
||||
for (const auto &pos : acct_upd.balances) {
|
||||
bts_assert(pos.free >= 0);
|
||||
bts_assert(pos.locked >= 0);
|
||||
auto it = std::lower_bound(
|
||||
dest.begin(), dest.end(), pos.asset, exch_pos_cmp);
|
||||
if (it == dest.end() || it->exchange_sec_desc != pos.asset) {
|
||||
const auto *sec = get_security(venue, pos.asset);
|
||||
if (sec == nullptr) {
|
||||
continue;
|
||||
}
|
||||
bts::orderman::ExchangePosition::UnitPosition new_pos;
|
||||
new_pos.exchange_sec_desc = pos.asset;
|
||||
new_pos.security = sec;
|
||||
new_pos.account_id = account.id();
|
||||
new_pos.account_name = account.name();
|
||||
it = dest.insert(it, new_pos);
|
||||
}
|
||||
it->position = pos.locked + pos.free;
|
||||
it->avg_filled_price.value() =
|
||||
std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace binance_execution::spots
|
||||
@@ -1,37 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/spots/messages/order_record.h"
|
||||
#include "binance_execution/spots/user_data_streams/account_update.h"
|
||||
#include "binance_execution/spots/user_data_streams/order_update.h"
|
||||
#include "lib/config/protos/jetfix.pb.h"
|
||||
#include "lib/orderman/orderman_utils/ExchangePosition.h"
|
||||
#include "lib/orderman/orderman_utils/ExecutionReport.h"
|
||||
|
||||
namespace bts::api::admin {
|
||||
class BTSAccount;
|
||||
}
|
||||
|
||||
namespace binance_execution::spots {
|
||||
|
||||
bool convert_execution_report(
|
||||
const user_data_streams::OrderUpdate &u,
|
||||
bts::api::Venue venue,
|
||||
const std::string &account,
|
||||
const bts::config::JetFIX_Connection *jetfix_cfg,
|
||||
bts::orderman::ExecutionReport &er);
|
||||
bool convert_execution_report(
|
||||
const OrderRecord &res,
|
||||
bts::api::Venue venue,
|
||||
const std::string &account,
|
||||
const bts::config::JetFIX_Connection *jetfix_cfg,
|
||||
bts::orderman::ExecutionReport &er);
|
||||
|
||||
void convert_positions(
|
||||
bts::api::Venue venue,
|
||||
const bts::api::admin::BTSAccount &account,
|
||||
const std::vector<user_data_streams::AccountUpdate> &acct_upds,
|
||||
std::function<const bts::api::Security *(
|
||||
bts::api::Venue venue, const std::string &name)> get_security,
|
||||
std::vector<bts::orderman::ExchangePosition::UnitPosition> &dest);
|
||||
|
||||
} // namespace binance_execution::spots
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/spots/user_data_streams/event_type.h"
|
||||
#include "binance_execution/spots/user_data_stream/event_type.h"
|
||||
#include "lib/io/JsonReader.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution::spots::user_data_streams {
|
||||
namespace binance_execution::spots::user_data_stream {
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/binance-spot-api-docs/user-data-stream#account-update
|
||||
@@ -61,4 +61,4 @@ inline void from_json(const nlohmann::json &j, AccountUpdate &u) {
|
||||
j.at("B").get_to(u.balances);
|
||||
};
|
||||
|
||||
} // namespace binance_execution::spots::user_data_streams
|
||||
} // namespace binance_execution::spots::user_data_stream
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/spots/user_data_streams/event_type.h"
|
||||
#include "binance_execution/spots/user_data_stream/event_type.h"
|
||||
#include "lib/io/JsonReader.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution::spots::user_data_streams {
|
||||
namespace binance_execution::spots::user_data_stream {
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/binance-spot-api-docs/user-data-stream#account-update
|
||||
@@ -43,4 +43,4 @@ inline void from_json(const nlohmann::json &j, BalanceUpdate &b) {
|
||||
j.at("T").get_to(b.clear_time);
|
||||
}
|
||||
|
||||
} // namespace binance_execution::spots::user_data_streams
|
||||
} // namespace binance_execution::spots::user_data_stream
|
||||
+3
-3
@@ -6,7 +6,7 @@
|
||||
#include <fmt/format.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace binance_execution::spots::user_data_streams {
|
||||
namespace binance_execution::spots::user_data_stream {
|
||||
|
||||
// clang-format off
|
||||
#define FOR_ALL_VALUES(per_value) \
|
||||
@@ -68,7 +68,7 @@ inline void to_json(nlohmann::json &j, const EventType &value) {
|
||||
|
||||
#undef FOR_ALL_VALUES
|
||||
|
||||
} // namespace binance_execution::spots::user_data_streams
|
||||
} // namespace binance_execution::spots::user_data_stream
|
||||
|
||||
FMT_IMPL(
|
||||
binance_execution::spots::user_data_streams::EventType, t, "{}", t.name());
|
||||
binance_execution::spots::user_data_stream::EventType, t, "{}", t.name());
|
||||
+58
-2
@@ -1,7 +1,19 @@
|
||||
#include "order_update.h"
|
||||
#include "binance_execution/type_converters.h"
|
||||
#include "lib/config/protos/jetfix.pb.h"
|
||||
#include "lib/domain_model/Security.pb.h"
|
||||
#include "lib/io/JsonReader.h"
|
||||
#include "lib/orderman/orderman_utils/ExecutionReport.h"
|
||||
#include "lib/utils/checked_arithmetic.h"
|
||||
#include "lib/utils/string_utils.h"
|
||||
|
||||
namespace binance_execution::spots::user_data_streams {
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution::spots::user_data_stream {
|
||||
|
||||
using bts::system::math::checked_ltoul;
|
||||
using bts::system::string_utils::copy_to_array;
|
||||
using bts::system::string_utils::parse_uint;
|
||||
|
||||
void to_json(nlohmann::json &j, const OrderUpdate &u) {
|
||||
j["e"] = u.event_type;
|
||||
@@ -96,4 +108,48 @@ void from_json(const nlohmann::json &j, OrderUpdate &u) {
|
||||
j.at("V").get_to(u.self_trade_prevention);
|
||||
}
|
||||
|
||||
} // namespace binance_execution::spots::user_data_streams
|
||||
bool OrderUpdate::to_execution_report(
|
||||
bts::orderman::ExecutionReport &er, std::string &error) const {
|
||||
er.exchange = bts::api::VENUE_BINANCE;
|
||||
er.transact_time = bts::orderman::ExchangeTimeStamp64{
|
||||
convert_time_stamp(transaction_time).nanos()};
|
||||
if (!parse_uint(client_order_id, er.order_key.value())) {
|
||||
// For CancelReplace ops, we append a version number to the original key (separated by an
|
||||
// underscore). Extract the original key.
|
||||
const auto version_sep = client_order_id.find_last_of('_');
|
||||
if (version_sep != std::string::npos) {
|
||||
parse_uint(
|
||||
client_order_id.begin(), client_order_id.begin() + version_sep,
|
||||
er.order_key.value());
|
||||
} else {
|
||||
ErrorList::append(
|
||||
error,
|
||||
fmt::format(
|
||||
"failed to parse client order id '{}'", client_order_id));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bts_assert(checked_ltoul(
|
||||
order_id,
|
||||
er.exchange_order_id.emplace<bts::orderman::ExchangeOrderID64>()
|
||||
.value()));
|
||||
er.side = side == Side::BUY ? bts::api::SIDE_BUY : bts::api::SIDE_SELL;
|
||||
er.time_in_force = convert_time_in_force(time_in_force);
|
||||
er.order_type = convert_order_type(order_type);
|
||||
er.exec_type = convert_exec_type(execution_type, order_status);
|
||||
er.order_status = convert_order_status(order_status);
|
||||
er.manual_order_indicator = bts::api::orderman::MANUAL_ORDER_INDICATOR_NO;
|
||||
er.price.value() = order_price;
|
||||
er.quantity = order_quantity;
|
||||
er.filled_quantity = filled_quantity;
|
||||
er.last_shares = last_executed_quantity;
|
||||
er.last_price.value() = last_executed_price;
|
||||
er.avg_price.value() =
|
||||
filled_quantity > 0 ? quote_asset_transacted_quantity / filled_quantity
|
||||
: 0.0;
|
||||
er.stop_price.value() = stop_price;
|
||||
copy_to_array<40>(std::to_string(trade_id), er.exchange_fill_id.value());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace binance_execution::spots::user_data_stream
|
||||
+13
-3
@@ -6,14 +6,21 @@
|
||||
#include "binance_execution/enums/self_trade_prevention_mode.h"
|
||||
#include "binance_execution/enums/side.h"
|
||||
#include "binance_execution/enums/time_in_force.h"
|
||||
#include "binance_execution/spots/user_data_streams/event_type.h"
|
||||
#include "binance_execution/spots/user_data_stream/event_type.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution::spots::user_data_streams {
|
||||
namespace bts::config {
|
||||
class JetFIX_Connection;
|
||||
}
|
||||
namespace bts::orderman {
|
||||
class ExecutionReport;
|
||||
}
|
||||
|
||||
namespace binance_execution::spots::user_data_stream {
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/binance-spot-api-docs/user-data-stream#order-update
|
||||
@@ -53,9 +60,12 @@ struct OrderUpdate {
|
||||
double quote_order_quantity; // json: Q
|
||||
std::optional<std::int64_t> working_time; // json: W
|
||||
SelfTradePreventionMode self_trade_prevention; // json: V
|
||||
|
||||
bool to_execution_report(
|
||||
bts::orderman::ExecutionReport &er, std::string &error) const;
|
||||
};
|
||||
|
||||
void to_json(nlohmann::json &j, const OrderUpdate &d);
|
||||
void from_json(const nlohmann::json &j, OrderUpdate &d);
|
||||
|
||||
} // namespace binance_execution::spots::user_data_streams
|
||||
} // namespace binance_execution::spots::user_data_stream
|
||||
+11
-13
@@ -1,8 +1,8 @@
|
||||
#include "user_data_subscription.h"
|
||||
#include "binance_execution/spots/user_data_streams/account_update.h"
|
||||
#include "binance_execution/spots/user_data_streams/balance_update.h"
|
||||
#include "binance_execution/spots/user_data_streams/event_type.h"
|
||||
#include "binance_execution/spots/user_data_streams/order_update.h"
|
||||
#include "binance_execution/spots/user_data_stream/account_update.h"
|
||||
#include "binance_execution/spots/user_data_stream/balance_update.h"
|
||||
#include "binance_execution/spots/user_data_stream/event_type.h"
|
||||
#include "binance_execution/spots/user_data_stream/order_update.h"
|
||||
#include "lib/core/TimeStamp.h"
|
||||
#include "lib/io/websocket/websocket_client.h"
|
||||
#include "lib/logging/logging.h"
|
||||
@@ -13,7 +13,7 @@
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
namespace binance_execution::spots::user_data_streams {
|
||||
namespace binance_execution::spots::user_data_stream {
|
||||
|
||||
#define _LOG_FN(level, fn, ...) \
|
||||
LOG(OrderMan, level, "Binance::" fn ". " __VA_ARGS__)
|
||||
@@ -45,9 +45,9 @@ struct UserDataSubscription::PImpl {
|
||||
optional<thread> ws_run_thread;
|
||||
optional<thread> ws_ping_thread;
|
||||
|
||||
Delegate<void(user_data_streams::AccountUpdate)> account_update_callback;
|
||||
Delegate<void(user_data_streams::BalanceUpdate)> balance_update_callback;
|
||||
Delegate<void(user_data_streams::OrderUpdate)> order_update_callback;
|
||||
Delegate<void(AccountUpdate)> account_update_callback;
|
||||
Delegate<void(BalanceUpdate)> balance_update_callback;
|
||||
Delegate<void(OrderUpdate)> order_update_callback;
|
||||
|
||||
void start() {
|
||||
lock_guard<mutex> lock{instance_mx};
|
||||
@@ -171,14 +171,12 @@ UserDataSubscription::~UserDataSubscription() = default;
|
||||
void UserDataSubscription::start() { me_->start(); }
|
||||
void UserDataSubscription::stop() { me_->stop(); }
|
||||
|
||||
Delegate<void(user_data_streams::AccountUpdate)> &
|
||||
UserDataSubscription::account_update_callback() {
|
||||
Delegate<void(AccountUpdate)> &UserDataSubscription::account_update_callback() {
|
||||
return me_->account_update_callback;
|
||||
}
|
||||
|
||||
Delegate<void(user_data_streams::OrderUpdate)> &
|
||||
UserDataSubscription::order_update_callback() {
|
||||
Delegate<void(OrderUpdate)> &UserDataSubscription::order_update_callback() {
|
||||
return me_->order_update_callback;
|
||||
}
|
||||
|
||||
} // namespace binance_execution::spots::user_data_streams
|
||||
} // namespace binance_execution::spots::user_data_stream
|
||||
+5
-5
@@ -1,13 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/spots/user_data_streams/account_update.h"
|
||||
#include "binance_execution/spots/user_data_streams/balance_update.h"
|
||||
#include "binance_execution/spots/user_data_streams/order_update.h"
|
||||
#include "binance_execution/spots/user_data_stream/account_update.h"
|
||||
#include "binance_execution/spots/user_data_stream/balance_update.h"
|
||||
#include "binance_execution/spots/user_data_stream/order_update.h"
|
||||
#include "lib/utils/Delegate.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace binance_execution::spots::user_data_streams {
|
||||
namespace binance_execution::spots::user_data_stream {
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/binance-spot-api-docs/user-data-stream
|
||||
@@ -28,4 +28,4 @@ class UserDataSubscription {
|
||||
std::unique_ptr<PImpl> me_;
|
||||
};
|
||||
|
||||
} // namespace binance_execution::spots::user_data_streams
|
||||
} // namespace binance_execution::spots::user_data_stream
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/spots/rest_api_client.h"
|
||||
#include "lib/utils/Delegate.h"
|
||||
#include <memory>
|
||||
|
||||
namespace binance_execution::spots {
|
||||
|
||||
/**
|
||||
* Requests the listen key for user data stream subscriptions, and periodically
|
||||
* refreshes it to keep subscriptions alive.
|
||||
*
|
||||
* @a https://developers.binance.com/docs/derivatives/usds-margined-futures/user-data-streams
|
||||
*/
|
||||
class ListenKeyRequestor {
|
||||
public:
|
||||
explicit ListenKeyRequestor(RestApiClient rest_api_client);
|
||||
~ListenKeyRequestor();
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
bts::core::Delegate<void(const std::string &)> &callback();
|
||||
|
||||
private:
|
||||
struct PImpl;
|
||||
std::unique_ptr<PImpl> me_;
|
||||
};
|
||||
|
||||
} // namespace binance_execution::spots
|
||||
@@ -5,15 +5,12 @@
|
||||
#include "binance_execution/enums/order_type.h"
|
||||
#include "binance_execution/enums/side.h"
|
||||
#include "binance_execution/enums/time_in_force.h"
|
||||
#include "binance_execution/error_list.h"
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/core/TimeStamp.h"
|
||||
#include "lib/domain/Side.h"
|
||||
#include "lib/utils/bts_assert.h"
|
||||
|
||||
namespace bts::api::admin {
|
||||
class BTSAccount;
|
||||
}
|
||||
|
||||
namespace binance_execution {
|
||||
|
||||
bts::api::orderman::NativeOrderType convert_order_type(OrderType tif);
|
||||
@@ -33,6 +30,7 @@ bts::core::TimeSpan convert_time_span(std::int64_t ts);
|
||||
bts::domain::Side convert_side(Side side);
|
||||
Side convert_side(bts::domain::Side side);
|
||||
Side convert_side(bts::api::Side side);
|
||||
bool convert_side(bts::api::Side side, Side &res, std::string &error);
|
||||
|
||||
bts::api::orderman::ExecType convert_exec_type(
|
||||
ExecutionType exec_type, OrderStatus order_status);
|
||||
@@ -138,10 +136,27 @@ inline Side convert_side(bts::api::Side side) {
|
||||
}
|
||||
}
|
||||
|
||||
inline bool convert_side(bts::api::Side side, Side &res, std::string &error) {
|
||||
switch (side) {
|
||||
case bts::api::SIDE_BUY:
|
||||
res = Side::BUY;
|
||||
break;
|
||||
case bts::api::SIDE_SELL:
|
||||
res = Side::SELL;
|
||||
break;
|
||||
default:
|
||||
ErrorList::append(error, "invalid side");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bts::api::orderman::TimeInForce convert_time_in_force(TimeInForce tif) {
|
||||
switch (tif.value) {
|
||||
case TimeInForce::GTC:
|
||||
return bts::api::orderman::TIME_IN_FORCE_GTC;
|
||||
case TimeInForce::GTD:
|
||||
return bts::api::orderman::TIME_IN_FORCE_DAY;
|
||||
case TimeInForce::IOC:
|
||||
case TimeInForce::FOK:
|
||||
return bts::api::orderman::TIME_IN_FORCE_IOC;
|
||||
@@ -199,11 +214,36 @@ inline OrderType convert_order_type(bts::api::orderman::NativeOrderType tif) {
|
||||
}
|
||||
}
|
||||
|
||||
inline bool convert_order_type(
|
||||
bts::api::orderman::NativeOrderType native_type,
|
||||
OrderType &result,
|
||||
std::string &error) {
|
||||
switch (native_type) {
|
||||
case bts::api::orderman::NATIVE_ORDER_TYPE_LIMIT:
|
||||
result = OrderType::LIMIT;
|
||||
break;
|
||||
case bts::api::orderman::NATIVE_ORDER_TYPE_MARKET:
|
||||
result = OrderType::MARKET;
|
||||
break;
|
||||
case bts::api::orderman::NATIVE_ORDER_TYPE_UNDEFINED:
|
||||
result = OrderType::NONE;
|
||||
break;
|
||||
default:
|
||||
ErrorList::append(
|
||||
error, fmt::format(
|
||||
"NativeOrderType '{}' not implemented at Binance",
|
||||
bts::api::orderman::NativeOrderType_Name(native_type)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline TimeInForce convert_time_in_force(bts::api::orderman::TimeInForce tif) {
|
||||
switch (tif) {
|
||||
case bts::api::orderman::TIME_IN_FORCE_GTC:
|
||||
case bts::api::orderman::TIME_IN_FORCE_DAY:
|
||||
return TimeInForce::GTC;
|
||||
case bts::api::orderman::TIME_IN_FORCE_DAY:
|
||||
return TimeInForce::GTD;
|
||||
case bts::api::orderman::TIME_IN_FORCE_IOC:
|
||||
return TimeInForce::IOC;
|
||||
case bts::api::orderman::TIME_IN_FORCE_UNDEFINED:
|
||||
@@ -214,6 +254,35 @@ inline TimeInForce convert_time_in_force(bts::api::orderman::TimeInForce tif) {
|
||||
}
|
||||
}
|
||||
|
||||
inline bool convert_time_in_force(
|
||||
bts::api::orderman::TimeInForce tif,
|
||||
TimeInForce &result,
|
||||
std::string &error,
|
||||
bool allow_none = false) {
|
||||
switch (tif) {
|
||||
case bts::api::orderman::TIME_IN_FORCE_GTC:
|
||||
result = TimeInForce::GTC;
|
||||
break;
|
||||
case bts::api::orderman::TIME_IN_FORCE_DAY:
|
||||
result = TimeInForce::GTD;
|
||||
break;
|
||||
case bts::api::orderman::TIME_IN_FORCE_IOC:
|
||||
result = TimeInForce::IOC;
|
||||
break;
|
||||
case bts::api::orderman::TIME_IN_FORCE_UNDEFINED:
|
||||
if (allow_none) {
|
||||
result = TimeInForce::NONE;
|
||||
break;
|
||||
}
|
||||
ErrorList::append(error, "TimeInForce is undefined");
|
||||
return false;
|
||||
default:
|
||||
ErrorList::append(error, "Unknown BinanceTimeInForce");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline ExecutionType convert_exec_type(bts::api::orderman::ExecType exec_type) {
|
||||
switch (exec_type) {
|
||||
case bts::api::orderman::EXEC_TYPE_NEW:
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
#include "type_validators.h"
|
||||
|
||||
namespace binance_execution {
|
||||
|
||||
bool validate(
|
||||
const bts::api::Security *sec, bts::api::Venue venue, std::string &error) {
|
||||
if (sec == nullptr) {
|
||||
ErrorList::append(error, "security is null");
|
||||
return false;
|
||||
}
|
||||
switch (venue) {
|
||||
case bts::api::VENUE_BINANCE:
|
||||
if (!sec->has_binance()) {
|
||||
ErrorList::append(error, "security not of Binance");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case bts::api::VENUE_BINANCE_USDS_FUTURES:
|
||||
if (!sec->has_binance_usds_futures()) {
|
||||
ErrorList::append(error, "security not of BinanceUsdsFutures");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case bts::api::VENUE_BINANCE_COIN_FUTURES:
|
||||
if (!sec->has_binance_coin_futures()) {
|
||||
ErrorList::append(error, "security not of BinanceCoinFutures");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case bts::api::VENUE_BINANCE_US:
|
||||
if (!sec->has_binance_us()) {
|
||||
ErrorList::append(error, "security not of BinanceUs");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case bts::api::VENUE_UNSET:
|
||||
switch (sec->ExchangeData_case()) {
|
||||
case bts::api::Security::kBinance:
|
||||
case bts::api::Security::kBinanceCoinFutures:
|
||||
case bts::api::Security::kBinanceUsdsFutures:
|
||||
case bts::api::Security::kBinanceUs:
|
||||
break;
|
||||
default:
|
||||
ErrorList::append(error, "security not of any Binance sub-venue");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool validate(const bts::orderman::ExecutionReport &er, std::string &error) {
|
||||
const size_t orig_sz = error.size();
|
||||
validate(er.order_key, error);
|
||||
validate(er.quantity, error);
|
||||
validate(er.exchange_order_id, error);
|
||||
if (er.account_name.value().empty()) {
|
||||
ErrorList::append(error, "account is empty");
|
||||
}
|
||||
validate(er.security, error);
|
||||
if (er.cfg == nullptr) {
|
||||
ErrorList::append(error, "jetfix config is null");
|
||||
}
|
||||
return error.size() == orig_sz;
|
||||
}
|
||||
|
||||
} // namespace binance_execution
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/error_list.h"
|
||||
#include "lib/domain_model/Security.pb.h"
|
||||
#include "lib/orderman/orderman_utils/ExecutionReport.h"
|
||||
#include "lib/orderman/orderman_utils/orderman_common.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution {
|
||||
|
||||
bool validate(bts::orderman::OrderKey key, std::string &error);
|
||||
bool validate(bts::orderman::ExchangeOrderID64 key, std::string &error);
|
||||
bool validate(bts::orderman::ExchangeOrderIDVar key, std::string &error);
|
||||
bool validate(bts::Quantity qty, std::string &error);
|
||||
bool validate(
|
||||
const bts::api::Security *sec, bts::api::Venue venue, std::string &error);
|
||||
bool validate(const bts::orderman::ExecutionReport &er, std::string &error);
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// Definition
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
inline bool validate(bts::orderman::OrderKey key, std::string &error) {
|
||||
if (key.value() == 0) {
|
||||
ErrorList::append(error, "order key is 0");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool validate(bts::orderman::ExchangeOrderID64 key, std::string &error) {
|
||||
if (key.value() == 0) {
|
||||
ErrorList::append(error, "exch order key is 0");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool validate(
|
||||
bts::orderman::ExchangeOrderIDVar key, std::string &error) {
|
||||
if (std::holds_alternative<bts::orderman::ExchangeOrderID64>(key)) {
|
||||
return validate(std::get<bts::orderman::ExchangeOrderID64>(key), error);
|
||||
} else {
|
||||
ErrorList::append(error, "exch order id is undefined or not an integer");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool validate(bts::Quantity qty, std::string &error) {
|
||||
if (qty.full_value() == 0) {
|
||||
ErrorList::append(error, "quantity is 0");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool validate(const bts::api::Security *sec, std::string &error) {
|
||||
return validate(sec, bts::api::VENUE_UNSET, error);
|
||||
}
|
||||
|
||||
} // namespace binance_execution
|
||||
@@ -1,555 +0,0 @@
|
||||
#include "execution_gateway.h"
|
||||
#include "binance_execution/rest_api_result.h"
|
||||
#include "binance_execution/type_converters.h"
|
||||
#include "binance_execution/usds_futures/messages/create_order_response.h"
|
||||
#include "binance_execution/usds_futures/messages/modify_order_request.h"
|
||||
#include "binance_execution/usds_futures/rest_api_client.h"
|
||||
#include "binance_execution/usds_futures/type_converters.h"
|
||||
#include "binance_execution/usds_futures/user_data_streams/listen_key_requestor.h"
|
||||
#include "binance_execution/usds_futures/user_data_streams/user_data_subscription.h"
|
||||
#include "lib/api/service_helpers/AdminServiceUtils.h"
|
||||
#include "lib/api/services/AdminService.pb.h"
|
||||
#include "lib/api/services/OrderService.h"
|
||||
#include "lib/config/protos/jetfix.pb.h"
|
||||
#include "lib/config/protos/toggles.pb.h"
|
||||
#include "lib/core/TimeStamp.h"
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/domain_model/Security.pb.h"
|
||||
#include "lib/domain_model/Side.h"
|
||||
#include "lib/io/json_formatter.h"
|
||||
#include "lib/logging/logging.h"
|
||||
#include "lib/md/SecurityOracle.h"
|
||||
#include "lib/mercury/events/Update.h"
|
||||
#include "lib/orderman/CancelOrderMsg.h"
|
||||
#include "lib/orderman/CancelReplaceOrderMsg.h"
|
||||
#include "lib/orderman/CreateOrderMsg.h"
|
||||
#include "lib/orderman/RFQMsg.h"
|
||||
#include "lib/orderman/events/NewAdminDataEvent.h"
|
||||
#include "lib/orderman/events/NewSecuritiesEvent.h"
|
||||
#include "lib/orderman/orderman_utils/ExchangePosition.h"
|
||||
#include "lib/orderman/orderman_utils/ExecutionReport.h"
|
||||
#include "lib/orderman/orderman_utils/orderman_common.h"
|
||||
#include "lib/utils/checked_arithmetic.h"
|
||||
#include "lib/utils/overloaded.h"
|
||||
|
||||
#include <boost/lockfree/spsc_queue.hpp>
|
||||
#include <fmt/format.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/hmac.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
using nlohmann::json;
|
||||
|
||||
namespace binance_execution::usds_futures {
|
||||
|
||||
#define ASSERT_FN(test, fn, ...) \
|
||||
bts_assert_msgf(test, "BinanceUsdsFutures::" fn ". " __VA_ARGS__)
|
||||
#define LOG_FN(level, fn, ...) \
|
||||
LOG(OrderMan, level, "BinanceUsdsFutures::" fn ". " __VA_ARGS__)
|
||||
#define INFO_FN(fn, ...) LOG_FN(bts::Info, fn, __VA_ARGS__)
|
||||
#define WARN_FN(fn, ...) LOG_FN(bts::Warn, fn, __VA_ARGS__)
|
||||
#define ERROR_FN(fn, ...) LOG_FN(bts::Error, fn, __VA_ARGS__)
|
||||
|
||||
#define VENUE bts::api::VENUE_BINANCE_USDS_FUTURES
|
||||
|
||||
using boost::lockfree::spsc_queue;
|
||||
using bts::core::Delegate;
|
||||
using bts::io::json_formatter;
|
||||
using bts::orderman::CancelOrderMsg;
|
||||
using bts::orderman::CancelReplaceOrderMsg;
|
||||
using bts::orderman::CreateOrderMsg;
|
||||
using bts::orderman::NewAdminDataEvent;
|
||||
using bts::orderman::NewSecuritiesEvent;
|
||||
using bts::orderman::OrderMsg;
|
||||
using bts::orderman::RFQMsg;
|
||||
using std::int64_t;
|
||||
using std::optional;
|
||||
using std::size_t;
|
||||
using std::string;
|
||||
using std::uint32_t;
|
||||
using std::unordered_map;
|
||||
using std::vector;
|
||||
|
||||
static void append_error(string &dest, const char *err) {
|
||||
if (!dest.empty()) {
|
||||
dest.append(" ");
|
||||
}
|
||||
dest.append(err);
|
||||
}
|
||||
|
||||
static void validate(bts::orderman::OrderKey key, string &error) {
|
||||
if (key.value() == 0) {
|
||||
append_error(error, "order key is 0.");
|
||||
}
|
||||
}
|
||||
|
||||
static void validate(bts::orderman::ExchangeOrderID64 key, string &error) {
|
||||
if (key.value() == 0) {
|
||||
append_error(error, "exch order key is 0.");
|
||||
}
|
||||
}
|
||||
|
||||
static void validate(bts::orderman::ExchangeOrderIDVar key, string &error) {
|
||||
if (std::holds_alternative<bts::orderman::ExchangeOrderID64>(key)) {
|
||||
validate(std::get<bts::orderman::ExchangeOrderID64>(key), error);
|
||||
} else {
|
||||
append_error(error, "exch order id is undefined or not an integer.");
|
||||
}
|
||||
}
|
||||
|
||||
static void validate(bts::Quantity qty, string &error) {
|
||||
if (qty.full_value() == 0) {
|
||||
append_error(error, "quantity is 0.");
|
||||
}
|
||||
}
|
||||
|
||||
static void validate(const bts::api::Security *sec, string &error) {
|
||||
if (sec == nullptr) {
|
||||
append_error(error, "security is null.");
|
||||
} else if (!sec->has_binance_usds_futures()) {
|
||||
append_error(error, "security not of Binance Usds Futures.");
|
||||
}
|
||||
}
|
||||
|
||||
static void validate(bts::api::Side side, string &error) {
|
||||
switch (side) {
|
||||
case bts::api::SIDE_BUY:
|
||||
case bts::api::SIDE_SELL:
|
||||
break;
|
||||
default:
|
||||
append_error(error, "invalid side.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Store exch order id in EMNativeOrder (until then, it'll always be 0).
|
||||
// We can ignore this for now and use "orig client id" in cancel/replace requests.
|
||||
static constexpr const bool VALIDATE_EXCH_ORDER_ID = false;
|
||||
|
||||
static bool validate(const CreateOrderMsg &m, string &error) {
|
||||
const size_t orig_sz = error.size();
|
||||
validate(m.security, error);
|
||||
validate(m.key, error);
|
||||
validate(m.quantity, error);
|
||||
validate(m.side, error);
|
||||
return error.size() == orig_sz;
|
||||
}
|
||||
|
||||
static bool validate(const CancelOrderMsg &m, string &error) {
|
||||
const size_t orig_sz = error.size();
|
||||
validate(m.security, error);
|
||||
validate(m.key, error);
|
||||
if (VALIDATE_EXCH_ORDER_ID) {
|
||||
validate(m.exchange_order_id, error);
|
||||
}
|
||||
validate(m.side, error);
|
||||
return error.size() == orig_sz;
|
||||
}
|
||||
|
||||
static bool validate(const CancelReplaceOrderMsg &m, string &error) {
|
||||
const size_t orig_sz = error.size();
|
||||
validate(m.create_fields, error);
|
||||
if (VALIDATE_EXCH_ORDER_ID) {
|
||||
validate(m.exchange_order_id, error);
|
||||
}
|
||||
return error.size() == orig_sz;
|
||||
}
|
||||
|
||||
static bool validate(const bts::orderman::ExecutionReport &er, string &error) {
|
||||
const size_t orig_sz = error.size();
|
||||
validate(er.order_key, error);
|
||||
validate(er.quantity, error);
|
||||
validate(er.exchange_order_id, error);
|
||||
if (er.account_name.value().empty()) {
|
||||
append_error(error, "account is empty.");
|
||||
}
|
||||
validate(er.security, error);
|
||||
if (er.cfg == nullptr) {
|
||||
append_error(error, "jetfix config is null.");
|
||||
}
|
||||
return error.size() == orig_sz;
|
||||
}
|
||||
|
||||
struct ExecutionGatewayCallbacks {
|
||||
bts::orderman::OnExeReport exe_report;
|
||||
bts::orderman::OnGwStatusChange gw_status_change;
|
||||
bts::orderman::OnCancelReject cancel_reject;
|
||||
bts::orderman::OnExchangePositionState exchange_position_state;
|
||||
bts::orderman::OnExchangeOpenOrdersState exchange_open_orders;
|
||||
};
|
||||
|
||||
struct ExecutionGateway::PImpl {
|
||||
explicit PImpl(
|
||||
bts::md::SecurityOracle sec_oracle,
|
||||
ExecutionGatewayConfig config,
|
||||
bts::config::JetFIX::Connection jetfix_connection)
|
||||
: sec_oracle(std::move(sec_oracle)), config(std::move(config)),
|
||||
jetfix_connection(std::move(jetfix_connection)),
|
||||
rest_api_client(
|
||||
this->config.rest_endpoint,
|
||||
this->config.api_key,
|
||||
this->config.secret_key),
|
||||
listen_key_requestor(this->rest_api_client) {
|
||||
bts_assert(
|
||||
this->jetfix_connection.venue() ==
|
||||
bts::api::VENUE_BINANCE_USDS_FUTURES);
|
||||
bts_assert(!this->jetfix_connection.name().empty());
|
||||
pos.exchange = bts::api::VENUE_BINANCE_USDS_FUTURES;
|
||||
}
|
||||
|
||||
bts::md::SecurityOracle sec_oracle;
|
||||
ExecutionGatewayConfig config;
|
||||
bts::config::JetFIX_Connection jetfix_connection;
|
||||
|
||||
unordered_map<int64_t, Delegate<void(json &)>> response_handlers;
|
||||
RestApiClient rest_api_client;
|
||||
optional<user_data_streams::UserDataSubscription> user_data_sub;
|
||||
Delegate<void(user_data_streams::AccountUpdate)> account_update_callback;
|
||||
Delegate<void(user_data_streams::OrderUpdate)> order_update_callback;
|
||||
ListenKeyRequestor listen_key_requestor;
|
||||
|
||||
spsc_queue<nlohmann::json> incoming_msgs{1024};
|
||||
spsc_queue<user_data_streams::AccountUpdate> incoming_account_updates{1024};
|
||||
spsc_queue<user_data_streams::OrderUpdate> incoming_order_updates{1024};
|
||||
spsc_queue<RestApiResult<CreateOrderResponse>> incoming_new_orders{1024};
|
||||
|
||||
vector<user_data_streams::AccountUpdate> account_updates_vec;
|
||||
|
||||
ExecutionGatewayCallbacks callbacks;
|
||||
bts::orderman::ExchangePosition pos;
|
||||
bts::admin::AdminServiceUtils::ExchangeSubAccountResolver
|
||||
subaccount_resolver{};
|
||||
bts::api::admin::AdminData admin_data = {};
|
||||
bts::api::admin::BTSAccount account;
|
||||
|
||||
void start() {
|
||||
INFO_FN("start");
|
||||
listen_key_requestor.callback().bind<&PImpl::on_listen_key_>(*this);
|
||||
listen_key_requestor.start();
|
||||
}
|
||||
|
||||
void stop() {
|
||||
INFO_FN("stop");
|
||||
user_data_sub.reset();
|
||||
}
|
||||
|
||||
void on_account_update(user_data_streams::AccountUpdate u) {
|
||||
INFO_FN("on_account_update", "\n{:2}", json_formatter(u));
|
||||
incoming_account_updates.push(std::move(u));
|
||||
}
|
||||
|
||||
void on_order_update(user_data_streams::OrderUpdate u) {
|
||||
INFO_FN("on_order_update", "\n{:2}", json_formatter(u));
|
||||
incoming_order_updates.push(std::move(u));
|
||||
}
|
||||
|
||||
void on_listen_key_(const string &listen_key) {
|
||||
INFO_FN("on_listen_key", "value: {}", listen_key);
|
||||
user_data_sub.reset();
|
||||
if (listen_key.empty()) {
|
||||
LOG(OrderMan, bts::Error,
|
||||
"Unable to subscribe to user data because 'listen_key' "
|
||||
"is empty.");
|
||||
return;
|
||||
}
|
||||
auto &uds = user_data_sub.emplace(
|
||||
config.ws_stream_endpoint + "/ws/" + listen_key);
|
||||
uds.account_update_callback().bind<&PImpl::on_account_update>(*this);
|
||||
uds.order_update_callback().bind<&PImpl::on_order_update>(*this);
|
||||
uds.start();
|
||||
}
|
||||
|
||||
void create_order(const CreateOrderMsg &m) {
|
||||
string sec_desc = m.security == nullptr
|
||||
? "(null)"
|
||||
: bts::securities::descriptor(*m.security);
|
||||
RestApiResult<CreateOrderResponse> res;
|
||||
validate(m, res.error);
|
||||
LOG_FN(
|
||||
res.ok() ? bts::Info : bts::Error, "create_order",
|
||||
"{} {} {} @ {} (id={}, type={}, tif={}, err={})", m.side, m.quantity,
|
||||
sec_desc, m.price, m.key, m.order_type, m.time_in_force, res.error);
|
||||
if (res.ok()) {
|
||||
CreateOrderRequest req;
|
||||
req.newClientOrderId = std::to_string(m.key.value());
|
||||
req.symbol = bts::securities::descriptor(*m.security);
|
||||
req.side = m.side == bts::api::SIDE_BUY ? Side::BUY : Side::SELL;
|
||||
req.price = m.price.value();
|
||||
req.quantity = m.quantity.full_value();
|
||||
req.type = convert_order_type(m.order_type);
|
||||
req.timeInForce = convert_time_in_force(m.time_in_force);
|
||||
req.timestamp = convert_time_stamp(bts::core::TimeStamp::now());
|
||||
res = rest_api_client.create_order(req);
|
||||
LOG_FN(
|
||||
res.ok() ? bts::Info : bts::Error, "create_order", "id={}; {}",
|
||||
m.key, json_formatter(res));
|
||||
}
|
||||
incoming_new_orders.push(std::move(res));
|
||||
}
|
||||
|
||||
void update_order(const CancelReplaceOrderMsg &m) {
|
||||
string sec_desc =
|
||||
m.create_fields.security == nullptr
|
||||
? "(null)"
|
||||
: bts::securities::descriptor(*m.create_fields.security);
|
||||
RestApiResult<ModifyOrderResponse> res;
|
||||
validate(m, res.error);
|
||||
LOG_FN(
|
||||
res.ok() ? bts::Info : bts::Error, "update_order",
|
||||
"{} {} {} @ {} (id={}, type={}, tif={}, err={})",
|
||||
m.create_fields.side, m.create_fields.quantity, sec_desc,
|
||||
m.create_fields.price, m.create_fields.key,
|
||||
m.create_fields.order_type, m.create_fields.time_in_force, res.error);
|
||||
if (res.ok()) {
|
||||
ModifyOrderRequest req;
|
||||
req.orderId =
|
||||
bts::system::math::checked_ultol(m.exchange_order_id.value());
|
||||
req.origClientOrderId = std::to_string(m.create_fields.key.value());
|
||||
// The exchange accepts either orderId, or origClientOrderId (only one
|
||||
// need be populated).
|
||||
bts_assert(req.orderId != 0 || !req.origClientOrderId.empty());
|
||||
req.price = m.create_fields.price.value();
|
||||
req.priceMatch = PriceMatchMode::NONE;
|
||||
req.quantity = m.create_fields.quantity.full_value();
|
||||
req.symbol =
|
||||
m.create_fields.security->binance_usds_futures().security_desc();
|
||||
req.side = convert_side(m.create_fields.side);
|
||||
req.timestamp = convert_time_stamp(bts::core::TimeStamp::now());
|
||||
res = rest_api_client.modify_order(req);
|
||||
LOG_FN(
|
||||
res.ok() ? bts::Info : bts::Error, "update_order", "id={}; {}",
|
||||
req.orderId, json_formatter(res));
|
||||
}
|
||||
}
|
||||
|
||||
void cancel_order(const CancelOrderMsg &m) {
|
||||
string sec_desc = m.security == nullptr
|
||||
? "(null)"
|
||||
: bts::securities::descriptor(*m.security);
|
||||
RestApiResult<CancelOrderResponse> res;
|
||||
validate(m, res.error);
|
||||
CancelOrderRequest req;
|
||||
LOG_FN(
|
||||
res.ok() ? bts::Info : bts::Error, "cancel_order",
|
||||
"{} {} (id={}, exch_id={}, err={})", m.side, sec_desc, m.key,
|
||||
m.exchange_order_id, res.error);
|
||||
if (res.ok()) {
|
||||
req.orderId =
|
||||
bts::system::math::checked_ultol(m.exchange_order_id.value());
|
||||
req.origClientOrderId = std::to_string(m.key.value());
|
||||
// The exchange accepts either orderId, or origClientOrderId (only one
|
||||
// need be populated).
|
||||
bts_assert(req.orderId != 0 || !req.origClientOrderId.empty());
|
||||
req.symbol = m.security->binance_usds_futures().security_desc();
|
||||
req.timestamp = convert_time_stamp(bts::core::TimeStamp::now());
|
||||
res = rest_api_client.cancel_order(req);
|
||||
LOG_FN(
|
||||
res.ok() ? bts::Info : bts::Error, "cancel_order",
|
||||
"id={}, exch_id={}; {}", m.key, m.exchange_order_id,
|
||||
json_formatter(res));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ExecutionGateway::ExecutionGateway(
|
||||
bts::md::SecurityOracle sec_oracle,
|
||||
ExecutionGatewayConfig session_cfg,
|
||||
bts::config::JetFIX::Connection jetfix_connection)
|
||||
: me_(std::make_unique<PImpl>(
|
||||
std::move(sec_oracle),
|
||||
std::move(session_cfg),
|
||||
std::move(jetfix_connection))) {
|
||||
bts_assert(
|
||||
me_->config.account.size() <=
|
||||
bts::orderman::ExecutionReport{}.account_name.value().size());
|
||||
}
|
||||
|
||||
ExecutionGateway::~ExecutionGateway() = default;
|
||||
|
||||
void ExecutionGateway::start() { me_->start(); }
|
||||
|
||||
void ExecutionGateway::on_securities(
|
||||
const bts::mercury::Update<NewSecuritiesEvent> & /*update*/) {
|
||||
INFO_FN(
|
||||
"on_securities", "t_id: {} t_name: {}", bts::os::get_thread_id(),
|
||||
bts::os::get_thread_name());
|
||||
}
|
||||
|
||||
void ExecutionGateway::on_admin_data(
|
||||
const bts::mercury::Update<NewAdminDataEvent> &update) {
|
||||
INFO_FN(
|
||||
"on_admin_data", "t_id: {} t_name: {}", bts::os::get_thread_id(),
|
||||
bts::os::get_thread_name());
|
||||
if (update.events == nullptr) {
|
||||
return;
|
||||
}
|
||||
for (const auto &ev : *(update.events)) {
|
||||
me_->admin_data.CopyFrom(*ev.admin_data);
|
||||
}
|
||||
auto *exch_sub_acct =
|
||||
me_->subaccount_resolver.find_entry(me_->config.account, false);
|
||||
if (exch_sub_acct == nullptr) {
|
||||
WARN_FN(
|
||||
"on_admin_data",
|
||||
"Failed to map exchange account '{}' to internal account",
|
||||
me_->config.account);
|
||||
return;
|
||||
}
|
||||
auto *account = bts::admin::AdminServiceUtils::find_account(
|
||||
me_->admin_data, exch_sub_acct->bts_account_name());
|
||||
if (account == nullptr) {
|
||||
WARN_FN(
|
||||
"on_admin_data",
|
||||
"Failed to find internal account '{}' (mapped from exchange account "
|
||||
"'{}')",
|
||||
exch_sub_acct->bts_account_name(), me_->config.account);
|
||||
return;
|
||||
}
|
||||
if (me_->account.name() == account->name() &&
|
||||
me_->account.id() == account->id()) {
|
||||
return;
|
||||
}
|
||||
me_->account = *account;
|
||||
INFO_FN(
|
||||
"on_admin_data", "bts_account: {}; exch_subaccount: {}", account->name(),
|
||||
exch_sub_acct->exchange_subaccount());
|
||||
}
|
||||
|
||||
void ExecutionGateway::send_msgs(
|
||||
vector<OrderMsg> const &msg_vec, bts::trace::CorID) {
|
||||
INFO_FN("send_msgs", "count: {}", msg_vec.size());
|
||||
for (const auto &msg : msg_vec) {
|
||||
std::visit(
|
||||
bts::overloaded{
|
||||
[&](const CreateOrderMsg &m) { me_->create_order(m); },
|
||||
[&](const CancelReplaceOrderMsg &m) { me_->update_order(m); },
|
||||
[&](const CancelOrderMsg &m) { me_->cancel_order(m); },
|
||||
[&](const RFQMsg &) {
|
||||
bts_assert_msg(false, "Binance RFQ not implemented");
|
||||
},
|
||||
},
|
||||
msg);
|
||||
}
|
||||
}
|
||||
|
||||
void ExecutionGateway::send_fix(
|
||||
const bts::jetfix::OutFIXPtr &, bts::trace::CorID) {
|
||||
bts_assert_msg(false, "ExecutionGateway does not support fix");
|
||||
}
|
||||
|
||||
void ExecutionGateway::dispatch_incoming_fix_messages() {
|
||||
// Note: We send orders through the REST api, and receive order updates over
|
||||
// websocket (user data stream). The REST responses are added to the
|
||||
// "incoming_new_orders" queue for logging purposes, but we only generate
|
||||
// ExecutionReports in response to the websocket updates (which are added to
|
||||
// the "incoming_order_updates" queue).
|
||||
static constexpr const bool IGNORE_CREATE_ORDER_RESPONSE = true;
|
||||
// Order creates
|
||||
{
|
||||
RestApiResult<CreateOrderResponse> r;
|
||||
while (me_->callbacks.exe_report && me_->incoming_new_orders.pop(r)) {
|
||||
if (!r.ok() || !r.response) {
|
||||
ERROR_FN(
|
||||
"dispatch_incoming_fix_messages",
|
||||
"Cannot generate exe report from BinanceCreateOrderResult. {}",
|
||||
r.error);
|
||||
continue;
|
||||
}
|
||||
bts::orderman::ExecutionReport er;
|
||||
if (!convert_execution_report(
|
||||
*r.response, VENUE, me_->account.name(),
|
||||
&me_->jetfix_connection, er)) {
|
||||
WARN_FN(
|
||||
"dispatch_incoming_fix_messages",
|
||||
"Failed to generate exe report from CreateOrderResponse: {}",
|
||||
json_formatter(*r.response));
|
||||
continue;
|
||||
}
|
||||
er.security = me_->sec_oracle.get_security(VENUE, r.response->symbol);
|
||||
string error;
|
||||
if (!validate(er, error)) {
|
||||
ERROR_FN(
|
||||
"dispatch_incoming_fix_messages",
|
||||
"Generated invalid ExecutionReport. {}", error);
|
||||
continue;
|
||||
}
|
||||
if (!IGNORE_CREATE_ORDER_RESPONSE) {
|
||||
me_->callbacks.exe_report(
|
||||
std::make_shared<bts::orderman::ExecutionReport>(std::move(er)),
|
||||
{});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Order updates
|
||||
{
|
||||
user_data_streams::OrderUpdate u;
|
||||
while (me_->callbacks.exe_report && me_->incoming_order_updates.pop(u)) {
|
||||
bts::orderman::ExecutionReport er;
|
||||
if (!convert_execution_report(
|
||||
u, VENUE, me_->account.name(), &me_->jetfix_connection, er)) {
|
||||
WARN_FN(
|
||||
"dispatch_incoming_fix_messages",
|
||||
"Failed to generate exe report from OrderUpdate: {}",
|
||||
json_formatter(u));
|
||||
continue;
|
||||
}
|
||||
er.security = me_->sec_oracle.get_security(VENUE, u.data.symbol);
|
||||
string error;
|
||||
if (!validate(er, error)) {
|
||||
ERROR_FN(
|
||||
"dispatch_incoming_fix_messages",
|
||||
"Generated invalid ExecutionReport. {}", error);
|
||||
continue;
|
||||
}
|
||||
me_->callbacks.exe_report(
|
||||
std::make_shared<bts::orderman::ExecutionReport>(std::move(er)),
|
||||
{});
|
||||
}
|
||||
}
|
||||
// Account updates
|
||||
{
|
||||
user_data_streams::AccountUpdate u;
|
||||
auto &acct_upds = me_->account_updates_vec;
|
||||
acct_upds.clear();
|
||||
while (me_->incoming_account_updates.pop(acct_upds.emplace_back())) {
|
||||
}
|
||||
acct_upds.pop_back();
|
||||
if (!acct_upds.empty()) {
|
||||
convert_positions(
|
||||
VENUE, me_->account, acct_upds,
|
||||
[sec_oracle = &me_->sec_oracle](
|
||||
bts::api::Venue venue, const std::string &name) {
|
||||
return sec_oracle->get_security(venue, name);
|
||||
},
|
||||
me_->pos.total_exchange_positions);
|
||||
if (me_->callbacks.exchange_position_state) {
|
||||
me_->callbacks.exchange_position_state(me_->pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Callbacks
|
||||
bts::orderman::OnExeReport &ExecutionGateway::on_exe_report() {
|
||||
return me_->callbacks.exe_report;
|
||||
}
|
||||
bts::orderman::OnGwStatusChange &ExecutionGateway::on_gw_status_change() {
|
||||
return me_->callbacks.gw_status_change;
|
||||
}
|
||||
bts::orderman::OnCancelReject &ExecutionGateway::on_cancel_reject() {
|
||||
return me_->callbacks.cancel_reject;
|
||||
}
|
||||
bts::orderman::OnExchangePositionState &
|
||||
ExecutionGateway::on_exchange_position_state() {
|
||||
return me_->callbacks.exchange_position_state;
|
||||
}
|
||||
bts::orderman::OnExchangeOpenOrdersState &
|
||||
ExecutionGateway::on_exchange_open_orders() {
|
||||
return me_->callbacks.exchange_open_orders;
|
||||
}
|
||||
|
||||
} // namespace binance_execution::usds_futures
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
#include "cancel_order_request.h"
|
||||
#include "binance_execution/error_list.h"
|
||||
#include "binance_execution/type_converters.h"
|
||||
#include "binance_execution/type_validators.h"
|
||||
#include "lib/domain_model/Security.pb.h"
|
||||
#include "lib/orderman/CancelOrderMsg.h"
|
||||
#include "lib/utils/checked_arithmetic.h"
|
||||
|
||||
namespace binance_execution::usds_futures {
|
||||
|
||||
bool CancelOrderRequest::init(
|
||||
const bts::orderman::CancelOrderMsg &m, std::string &error) {
|
||||
const auto orig_error_sz = error.size();
|
||||
if (validate(m.security, bts::api::VENUE_BINANCE_USDS_FUTURES, error)) {
|
||||
symbol = m.security->binance_usds_futures().security_desc();
|
||||
}
|
||||
orderId = bts::system::math::checked_ultol(m.exchange_order_id.value());
|
||||
origClientOrderId = std::to_string(m.key.value());
|
||||
// The exchange accepts either orderId, or origClientOrderId (only one
|
||||
// need be populated).
|
||||
if (orderId == 0 && origClientOrderId.empty()) {
|
||||
ErrorList::append(
|
||||
error, "Either orderId or origClientOrderId must be defined");
|
||||
}
|
||||
timestamp = convert_time_stamp(bts::core::TimeStamp::now());
|
||||
return error.size() == orig_error_sz;
|
||||
}
|
||||
|
||||
} // namespace binance_execution::usds_futures
|
||||
+6
@@ -3,6 +3,10 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace bts::orderman {
|
||||
struct CancelOrderMsg;
|
||||
}
|
||||
|
||||
namespace binance_execution::usds_futures {
|
||||
|
||||
/**
|
||||
@@ -13,6 +17,8 @@ struct CancelOrderRequest {
|
||||
std::string origClientOrderId;
|
||||
std::string symbol;
|
||||
std::int64_t timestamp = 0;
|
||||
|
||||
bool init(const bts::orderman::CancelOrderMsg &m, std::string &error);
|
||||
};
|
||||
|
||||
void to_json(nlohmann::json &j, const CancelOrderRequest &req);
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
#include "create_order_request.h"
|
||||
#include "binance_execution/type_converters.h"
|
||||
#include "binance_execution/type_validators.h"
|
||||
#include "lib/orderman/CreateOrderMsg.h"
|
||||
|
||||
namespace binance_execution::usds_futures {
|
||||
|
||||
bool CreateOrderRequest::init(
|
||||
const bts::orderman::CreateOrderMsg &m, std::string &error) {
|
||||
const auto orig_error_sz = error.size();
|
||||
newClientOrderId = std::to_string(m.key.value());
|
||||
if (validate(m.security, bts::api::VENUE_BINANCE_USDS_FUTURES, error)) {
|
||||
symbol = m.security->binance_usds_futures().security_desc();
|
||||
}
|
||||
side = m.side == bts::api::SIDE_BUY ? Side::BUY : Side::SELL;
|
||||
price = m.price.value();
|
||||
quantity = m.quantity.full_value();
|
||||
type = convert_order_type(m.order_type);
|
||||
timeInForce = convert_time_in_force(m.time_in_force);
|
||||
if (error.size() != orig_error_sz) {
|
||||
return false;
|
||||
}
|
||||
timestamp = convert_time_stamp(bts::core::TimeStamp::now());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace binance_execution::usds_futures
|
||||
+6
@@ -9,6 +9,10 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace bts::orderman {
|
||||
struct CreateOrderMsg;
|
||||
}
|
||||
|
||||
namespace binance_execution::usds_futures {
|
||||
|
||||
/**
|
||||
@@ -24,6 +28,8 @@ struct CreateOrderRequest {
|
||||
double quantity = 0;
|
||||
TimeInForce timeInForce = TimeInForce::NONE;
|
||||
std::int64_t recvWindow = 0;
|
||||
|
||||
bool init(const bts::orderman::CreateOrderMsg &m, std::string &error);
|
||||
};
|
||||
|
||||
void to_json(nlohmann::json &j, const CreateOrderRequest &req);
|
||||
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution::usds_futures {
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/derivatives/usds-margined-futures/user-data-streams
|
||||
*/
|
||||
struct ListenKeyRequest {};
|
||||
|
||||
inline void to_query_params(
|
||||
std::string & /*qps*/, const ListenKeyRequest & /*req*/) {}
|
||||
|
||||
} // namespace binance_execution::usds_futures
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution::usds_futures {
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/derivatives/usds-margined-futures/user-data-streams
|
||||
*/
|
||||
struct ListenKeyResponse {
|
||||
std::string listenKey;
|
||||
};
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ListenKeyResponse, listenKey);
|
||||
|
||||
} // namespace binance_execution::usds_futures
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
#include "modify_order_request.h"
|
||||
#include "binance_execution/error_list.h"
|
||||
#include "binance_execution/type_converters.h"
|
||||
#include "lib/orderman/CancelReplaceOrderMsg.h"
|
||||
#include "lib/utils/checked_arithmetic.h"
|
||||
|
||||
namespace binance_execution::usds_futures {
|
||||
|
||||
bool ModifyOrderRequest::init(
|
||||
const bts::orderman::CancelReplaceOrderMsg &m, std::string &error) {
|
||||
const auto orig_error_sz = error.size();
|
||||
const auto &cf = m.create_fields;
|
||||
if (cf.security == nullptr) {
|
||||
ErrorList::append(error, "security is null");
|
||||
} else if (!cf.security->has_binance_usds_futures()) {
|
||||
ErrorList::append(error, "security not of Binance Usds Futures");
|
||||
} else {
|
||||
symbol = cf.security->binance_usds_futures().security_desc();
|
||||
}
|
||||
orderId = bts::system::math::checked_ultol(m.exchange_order_id.value());
|
||||
origClientOrderId = std::to_string(cf.key.value());
|
||||
// The exchange accepts either orderId, or origClientOrderId (only one
|
||||
// need be populated).
|
||||
if (orderId == 0 && origClientOrderId.empty()) {
|
||||
ErrorList::append(
|
||||
error, "Either orderId or origClientOrderId must be defined");
|
||||
}
|
||||
bts_assert(orderId != 0 || !origClientOrderId.empty());
|
||||
price = cf.price.value();
|
||||
priceMatch = PriceMatchMode::NONE;
|
||||
quantity = cf.quantity.full_value();
|
||||
side = convert_side(cf.side);
|
||||
timestamp = convert_time_stamp(bts::core::TimeStamp::now());
|
||||
return error.size() == orig_error_sz;
|
||||
}
|
||||
|
||||
} // namespace binance_execution::usds_futures
|
||||
+6
@@ -8,6 +8,10 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace bts::orderman {
|
||||
struct CancelReplaceOrderMsg;
|
||||
}
|
||||
|
||||
namespace binance_execution::usds_futures {
|
||||
|
||||
/**
|
||||
@@ -24,6 +28,8 @@ struct ModifyOrderRequest {
|
||||
std::int64_t recvWindow = 0; // optional
|
||||
std::int64_t timestamp = 0; // mandatory
|
||||
// * Either orderId or origClientOrderId must be specified
|
||||
|
||||
bool init(const bts::orderman::CancelReplaceOrderMsg &m, std::string &error);
|
||||
};
|
||||
|
||||
void to_json(nlohmann::json &j, const ModifyOrderRequest &req);
|
||||
|
||||
@@ -78,9 +78,15 @@ void RestApiClient::request_(
|
||||
shrink_string<4096, 128>(http_res.body);
|
||||
http_res.reset();
|
||||
HttpClient::send(http_req, &http_res);
|
||||
res.http_code = http_res.code;
|
||||
res.http_error = http_res.errors;
|
||||
if (!http_res.ok()) {
|
||||
ERROR_FN("{} {}", "res: {}", method, path, http_res);
|
||||
res.error = std::move(http_res.errors);
|
||||
try {
|
||||
nlohmann::json::parse(http_res.body).get_to(res.error.emplace());
|
||||
} catch (const std::exception &ex) {
|
||||
ERROR_FN("request", "Failed to parse error json. {}", http_res);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Parse response
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/messages/listen_key_request.h"
|
||||
#include "binance_execution/messages/listen_key_response.h"
|
||||
#include "binance_execution/rest_api_result.h"
|
||||
#include "binance_execution/usds_futures/messages/all_orders_request.h"
|
||||
#include "binance_execution/usds_futures/messages/cancel_order_request.h"
|
||||
#include "binance_execution/usds_futures/messages/cancel_order_response.h"
|
||||
#include "binance_execution/usds_futures/messages/create_order_request.h"
|
||||
#include "binance_execution/usds_futures/messages/create_order_response.h"
|
||||
#include "binance_execution/usds_futures/messages/listen_key_request.h"
|
||||
#include "binance_execution/usds_futures/messages/listen_key_response.h"
|
||||
#include "binance_execution/usds_futures/messages/modify_order_request.h"
|
||||
#include "binance_execution/usds_futures/messages/modify_order_response.h"
|
||||
#include "binance_execution/usds_futures/messages/order_record.h"
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
#include "type_converters.h"
|
||||
#include "binance_execution/type_converters.h"
|
||||
#include "lib/api/services/AdminService.pb.h"
|
||||
#include "lib/utils/checked_arithmetic.h"
|
||||
#include "lib/utils/string_utils.h"
|
||||
|
||||
namespace binance_execution::usds_futures {
|
||||
|
||||
using bts::system::string_utils::copy_to_array;
|
||||
using bts::system::string_utils::parse_uint;
|
||||
using std::string;
|
||||
using std::vector;
|
||||
|
||||
bool convert_execution_report(
|
||||
const user_data_streams::OrderUpdate &u,
|
||||
bts::api::Venue venue,
|
||||
const std::string &account,
|
||||
const bts::config::JetFIX_Connection *jetfix_cfg,
|
||||
bts::orderman::ExecutionReport &er) {
|
||||
copy_to_array<12>(account, er.account_name.value());
|
||||
er.exchange = venue;
|
||||
er.transact_time = bts::orderman::ExchangeTimeStamp64{
|
||||
convert_time_stamp(u.transaction_time).nanos()};
|
||||
if (!parse_uint(u.data.client_order_id, er.order_key.value())) {
|
||||
return false;
|
||||
}
|
||||
bts_assert(bts::system::math::checked_ltoul(
|
||||
u.data.order_id,
|
||||
er.exchange_order_id.emplace<bts::orderman::ExchangeOrderID64>()
|
||||
.value()));
|
||||
er.security = nullptr;
|
||||
er.leg_security = nullptr;
|
||||
er.side =
|
||||
u.data.side == Side::BUY ? bts::api::SIDE_BUY : bts::api::SIDE_SELL;
|
||||
er.time_in_force = convert_time_in_force(u.data.time_in_force);
|
||||
er.order_type = convert_order_type(u.data.order_type);
|
||||
er.exec_type = convert_exec_type(u.data.execution_type, u.data.order_status);
|
||||
er.order_status = convert_order_status(u.data.order_status);
|
||||
er.manual_order_indicator = bts::api::orderman::MANUAL_ORDER_INDICATOR_NO;
|
||||
er.price.value() = u.data.orig_price;
|
||||
er.quantity = u.data.orig_quantity;
|
||||
er.filled_quantity = u.data.total_filled_qty;
|
||||
er.last_shares = u.data.last_filled_qty;
|
||||
// er.max_show = u.data.;
|
||||
er.last_price.value() = u.data.last_filled_price;
|
||||
er.avg_price.value() = u.data.avg_price;
|
||||
er.stop_price.value() = u.data.stop_price;
|
||||
copy_to_array<40>(
|
||||
std::to_string(u.data.trade_id), er.exchange_fill_id.value());
|
||||
// er.msg = u.reject_reason;
|
||||
er.cfg = jetfix_cfg;
|
||||
// Irrelevant fields
|
||||
er.ice_cl_ord_id = 0;
|
||||
er.ice_company_id = 0;
|
||||
er.memo = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool convert_execution_report(
|
||||
const CreateOrderResponse &res,
|
||||
bts::api::Venue venue,
|
||||
const std::string &account,
|
||||
const bts::config::JetFIX_Connection *jetfix_cfg,
|
||||
bts::orderman::ExecutionReport &er) {
|
||||
copy_to_array<12>(account, er.account_name.value());
|
||||
er.exchange = venue;
|
||||
er.transact_time = bts::orderman::ExchangeTimeStamp64{
|
||||
convert_time_stamp(res.updateTime).nanos()};
|
||||
if (!parse_uint(res.clientOrderId, er.order_key.value())) {
|
||||
return false;
|
||||
}
|
||||
bts_assert(bts::system::math::checked_ltoul(
|
||||
res.orderId,
|
||||
er.exchange_order_id.emplace<bts::orderman::ExchangeOrderID64>()
|
||||
.value()));
|
||||
er.security = nullptr;
|
||||
er.leg_security = nullptr;
|
||||
er.side = res.side == Side::BUY ? bts::api::SIDE_BUY : bts::api::SIDE_SELL;
|
||||
er.time_in_force = convert_time_in_force(res.timeInForce);
|
||||
er.order_type = convert_order_type(res.type);
|
||||
er.exec_type = bts::api::orderman::EXEC_TYPE_NEW;
|
||||
er.order_status = convert_order_status(res.status);
|
||||
er.manual_order_indicator = bts::api::orderman::MANUAL_ORDER_INDICATOR_NO;
|
||||
er.price.value() = res.price;
|
||||
er.quantity = res.origQty;
|
||||
er.filled_quantity = res.executedQty;
|
||||
er.last_shares = res.executedQty;
|
||||
er.max_show = 0;
|
||||
er.last_price.value() = res.avgPrice;
|
||||
er.avg_price.value() = res.avgPrice;
|
||||
er.stop_price.value() = res.stopPrice;
|
||||
er.msg = "";
|
||||
er.cfg = jetfix_cfg;
|
||||
// Irrelevant fields
|
||||
er.ice_cl_ord_id = 0;
|
||||
er.ice_company_id = 0;
|
||||
er.memo = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
struct ExchPosCmp {
|
||||
bool operator()(
|
||||
const string &ticker,
|
||||
const bts::orderman::ExchangePosition::UnitPosition &u) {
|
||||
return ticker < u.exchange_sec_desc;
|
||||
}
|
||||
|
||||
bool operator()(
|
||||
const bts::orderman::ExchangePosition::UnitPosition &u,
|
||||
const string &ticker) {
|
||||
return u.exchange_sec_desc < ticker;
|
||||
}
|
||||
};
|
||||
|
||||
void convert_positions(
|
||||
bts::api::Venue venue,
|
||||
const bts::api::admin::BTSAccount &account,
|
||||
const vector<user_data_streams::AccountUpdate> &acct_upds,
|
||||
std::function<const bts::api::Security *(
|
||||
bts::api::Venue venue, const std::string &name)> get_security,
|
||||
vector<bts::orderman::ExchangePosition::UnitPosition> &dest) {
|
||||
ExchPosCmp exch_pos_cmp;
|
||||
for (const auto &acct_upd : acct_upds) {
|
||||
for (const auto &pos : acct_upd.data.positions) {
|
||||
bts_assert(pos.position_amount >= 0);
|
||||
auto it = std::lower_bound(
|
||||
dest.begin(), dest.end(), pos.symbol, exch_pos_cmp);
|
||||
if (it == dest.end() || it->exchange_sec_desc != pos.symbol) {
|
||||
const auto *sec = get_security(venue, pos.symbol);
|
||||
if (sec == nullptr) {
|
||||
continue;
|
||||
}
|
||||
bts::orderman::ExchangePosition::UnitPosition new_pos;
|
||||
new_pos.exchange_sec_desc = pos.symbol;
|
||||
new_pos.security = sec;
|
||||
new_pos.account_id = account.id();
|
||||
new_pos.account_name = account.name();
|
||||
it = dest.insert(it, new_pos);
|
||||
}
|
||||
it->position = pos.position_side == PositionSide::SHORT
|
||||
? -pos.position_amount
|
||||
: pos.position_amount;
|
||||
it->avg_filled_price.value() = pos.break_even_price;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace binance_execution::usds_futures
|
||||
@@ -1,37 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/usds_futures/messages/create_order_response.h"
|
||||
#include "binance_execution/usds_futures/user_data_streams/account_update.h"
|
||||
#include "binance_execution/usds_futures/user_data_streams/order_update.h"
|
||||
#include "lib/config/protos/jetfix.pb.h"
|
||||
#include "lib/orderman/orderman_utils/ExchangePosition.h"
|
||||
#include "lib/orderman/orderman_utils/ExecutionReport.h"
|
||||
|
||||
namespace bts::api::admin {
|
||||
class BTSAccount;
|
||||
}
|
||||
|
||||
namespace binance_execution::usds_futures {
|
||||
|
||||
bool convert_execution_report(
|
||||
const user_data_streams::OrderUpdate &u,
|
||||
bts::api::Venue venue,
|
||||
const std::string &account,
|
||||
const bts::config::JetFIX_Connection *jetfix_cfg,
|
||||
bts::orderman::ExecutionReport &er);
|
||||
bool convert_execution_report(
|
||||
const CreateOrderResponse &res,
|
||||
bts::api::Venue venue,
|
||||
const std::string &account,
|
||||
const bts::config::JetFIX_Connection *jetfix_cfg,
|
||||
bts::orderman::ExecutionReport &er);
|
||||
|
||||
void convert_positions(
|
||||
bts::api::Venue venue,
|
||||
const bts::api::admin::BTSAccount &account,
|
||||
const std::vector<user_data_streams::AccountUpdate> &acct_upds,
|
||||
std::function<const bts::api::Security *(
|
||||
bts::api::Venue venue, const std::string &name)> get_security,
|
||||
std::vector<bts::orderman::ExchangePosition::UnitPosition> &dest);
|
||||
|
||||
} // namespace binance_execution::usds_futures
|
||||
@@ -0,0 +1,456 @@
|
||||
#include "usds_futures_gateway.h"
|
||||
#include "binance_execution/enums/order_status.h"
|
||||
#include "binance_execution/execution_report_store.h"
|
||||
#include "binance_execution/gateway_config.h"
|
||||
#include "binance_execution/listen_key_requestor.h"
|
||||
#include "binance_execution/rest_api_result.h"
|
||||
#include "binance_execution/type_validators.h"
|
||||
#include "binance_execution/usds_futures/messages/create_order_request.h"
|
||||
#include "binance_execution/usds_futures/messages/modify_order_request.h"
|
||||
#include "binance_execution/usds_futures/rest_api_client.h"
|
||||
#include "binance_execution/usds_futures/user_data_stream/user_data_subscription.h"
|
||||
#include "lib/api/service_helpers/AdminServiceUtils.h"
|
||||
#include "lib/api/services/AdminService.pb.h"
|
||||
#include "lib/api/services/OrderService.h"
|
||||
#include "lib/config/protos/jetfix.pb.h"
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/domain_model/Security.pb.h"
|
||||
#include "lib/domain_model/Side.h"
|
||||
#include "lib/io/json_formatter.h"
|
||||
#include "lib/logging/logging.h"
|
||||
#include "lib/md/SecurityOracle.h"
|
||||
#include "lib/mercury/events/Update.h"
|
||||
#include "lib/orderman/CancelOrderMsg.h"
|
||||
#include "lib/orderman/CancelReplaceOrderMsg.h"
|
||||
#include "lib/orderman/CreateOrderMsg.h"
|
||||
#include "lib/orderman/RFQMsg.h"
|
||||
#include "lib/orderman/events/NewAdminDataEvent.h"
|
||||
#include "lib/orderman/events/NewSecuritiesEvent.h"
|
||||
#include "lib/orderman/orderman_utils/ExchangePosition.h"
|
||||
#include "lib/orderman/orderman_utils/ExecutionReport.h"
|
||||
#include "lib/orderman/orderman_utils/orderman_common.h"
|
||||
#include "lib/utils/checked_arithmetic.h"
|
||||
#include "lib/utils/overloaded.h"
|
||||
#include "lib/utils/string_utils.h"
|
||||
|
||||
#include <boost/lockfree/spsc_queue.hpp>
|
||||
#include <fmt/format.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
using nlohmann::json;
|
||||
|
||||
namespace binance_execution::usds_futures {
|
||||
|
||||
#define ASSERT_FN(test, fn, ...) \
|
||||
bts_assert_msgf(test, "BinanceUsdsFutures::" fn ". " __VA_ARGS__)
|
||||
#define LOG_FN(level, fn, ...) \
|
||||
LOG(OrderMan, level, "BinanceUsdsFutures::" fn ". " __VA_ARGS__)
|
||||
#define INFO_FN(fn, ...) LOG_FN(bts::Info, fn, __VA_ARGS__)
|
||||
#define WARN_FN(fn, ...) LOG_FN(bts::Warn, fn, __VA_ARGS__)
|
||||
#define ERROR_FN(fn, ...) LOG_FN(bts::Error, fn, __VA_ARGS__)
|
||||
|
||||
#define VENUE bts::api::VENUE_BINANCE_USDS_FUTURES
|
||||
|
||||
using boost::lockfree::spsc_queue;
|
||||
using bts::core::Delegate;
|
||||
using bts::io::json_formatter;
|
||||
using bts::orderman::CancelOrderMsg;
|
||||
using bts::orderman::CancelReplaceOrderMsg;
|
||||
using bts::orderman::CreateOrderMsg;
|
||||
using bts::orderman::ExecutionReport;
|
||||
using bts::orderman::NewAdminDataEvent;
|
||||
using bts::orderman::NewSecuritiesEvent;
|
||||
using bts::orderman::OrderKey;
|
||||
using bts::orderman::OrderMsg;
|
||||
using bts::orderman::RFQMsg;
|
||||
using bts::system::math::checked_ltou;
|
||||
using std::int64_t;
|
||||
using std::optional;
|
||||
using std::shared_ptr;
|
||||
using std::size_t;
|
||||
using std::string;
|
||||
using std::uint32_t;
|
||||
using std::vector;
|
||||
|
||||
struct ExecutionGatewayCallbacks {
|
||||
bts::orderman::OnExeReport exe_report;
|
||||
bts::orderman::OnGwStatusChange gw_status_change;
|
||||
bts::orderman::OnCancelReject cancel_reject;
|
||||
bts::orderman::OnExchangePositionState exchange_position_state;
|
||||
bts::orderman::OnExchangeOpenOrdersState exchange_open_orders;
|
||||
};
|
||||
|
||||
struct UsdsFuturesGateway::PImpl {
|
||||
explicit PImpl(
|
||||
bts::md::SecurityOracle sec_oracle,
|
||||
GatewayConfig config,
|
||||
bts::config::JetFIX::Connection jetfix_connection)
|
||||
: sec_oracle(std::move(sec_oracle)), config(std::move(config)),
|
||||
jetfix_connection(std::make_shared<bts::config::JetFIX_Connection>(
|
||||
std::move(jetfix_connection))),
|
||||
rest_api_client(
|
||||
this->config.rest_endpoint,
|
||||
this->config.api_key,
|
||||
this->config.secret_key),
|
||||
listen_key_requestor(
|
||||
[this](const auto &req) {
|
||||
return rest_api_client.get_listen_key(req);
|
||||
},
|
||||
[this](const auto &req) {
|
||||
return rest_api_client.refresh_listen_key(req);
|
||||
}),
|
||||
orders(
|
||||
[](const std::string &exch_key) {
|
||||
std::uint32_t k;
|
||||
bts::system::string_utils::parse_uint(exch_key, k);
|
||||
return OrderKey{k};
|
||||
},
|
||||
this->config.account,
|
||||
this->jetfix_connection) {
|
||||
bts_assert(this->jetfix_connection->venue() == VENUE);
|
||||
bts_assert(!this->jetfix_connection->name().empty());
|
||||
pos.exchange = VENUE;
|
||||
}
|
||||
|
||||
bts::md::SecurityOracle sec_oracle;
|
||||
GatewayConfig config;
|
||||
std::shared_ptr<bts::config::JetFIX_Connection> jetfix_connection;
|
||||
|
||||
RestApiClient rest_api_client;
|
||||
optional<user_data_stream::UserDataSubscription> user_data_sub;
|
||||
Delegate<void(user_data_stream::AccountUpdate)> account_update_callback;
|
||||
Delegate<void(user_data_stream::OrderUpdate)> order_update_callback;
|
||||
ListenKeyRequestor listen_key_requestor;
|
||||
|
||||
spsc_queue<user_data_stream::AccountUpdate> incoming_account_updates{1024};
|
||||
spsc_queue<user_data_stream::OrderUpdate> incoming_order_updates{1024};
|
||||
spsc_queue<shared_ptr<const ExecutionReport>> internal_order_rejects{256};
|
||||
vector<user_data_stream::AccountUpdate> account_updates_vec;
|
||||
|
||||
ExecutionReportStore orders;
|
||||
ExecutionGatewayCallbacks callbacks;
|
||||
bts::orderman::ExchangePosition pos;
|
||||
bts::api::admin::BTSAccount account;
|
||||
|
||||
void start() {
|
||||
INFO_FN("start");
|
||||
listen_key_requestor.callback().bind<&PImpl::on_listen_key_>(*this);
|
||||
listen_key_requestor.start();
|
||||
}
|
||||
|
||||
void stop() {
|
||||
INFO_FN("stop");
|
||||
user_data_sub.reset();
|
||||
}
|
||||
|
||||
void on_account_update(user_data_stream::AccountUpdate u) {
|
||||
INFO_FN("on_account_update", "\n{:2}", json_formatter(u));
|
||||
incoming_account_updates.push(std::move(u));
|
||||
}
|
||||
|
||||
void on_order_update(user_data_stream::OrderUpdate u) {
|
||||
INFO_FN("on_order_update", "\n{:2}", json_formatter(u));
|
||||
incoming_order_updates.push(std::move(u));
|
||||
}
|
||||
|
||||
void on_listen_key_(const string &listen_key) {
|
||||
INFO_FN("on_listen_key", "value: {}", listen_key);
|
||||
user_data_sub.reset();
|
||||
if (listen_key.empty()) {
|
||||
LOG(OrderMan, bts::Error,
|
||||
"Unable to subscribe to user data because 'listen_key' "
|
||||
"is empty.");
|
||||
return;
|
||||
}
|
||||
auto &uds = user_data_sub.emplace(
|
||||
config.ws_stream_endpoint + "/ws/" + listen_key);
|
||||
uds.account_update_callback().bind<&PImpl::on_account_update>(*this);
|
||||
uds.order_update_callback().bind<&PImpl::on_order_update>(*this);
|
||||
uds.start();
|
||||
}
|
||||
|
||||
void create_order(const CreateOrderMsg &m) {
|
||||
CreateOrderRequest req;
|
||||
std::string err;
|
||||
if (!req.init(m, err)) {
|
||||
ERROR_FN(
|
||||
"create_order",
|
||||
"{} {} {} @ {} (id={}, type={}, tif={}). Rejected "
|
||||
"internally (request not sent). {}",
|
||||
m.side, m.quantity, req.symbol, m.price, m.key, m.order_type,
|
||||
m.time_in_force, err);
|
||||
internal_order_rejects.push(orders.reject(m, err));
|
||||
return;
|
||||
}
|
||||
INFO_FN(
|
||||
"create_order",
|
||||
"{} {} {} @ {} (id={}, type={}, tif={}). Sending request", m.side,
|
||||
m.quantity, req.symbol, m.price, m.key, m.order_type,
|
||||
m.time_in_force);
|
||||
const auto res = rest_api_client.create_order(req);
|
||||
if (!res.ok()) {
|
||||
ERROR_FN("create_order", "id={}. {}", m.key, json_formatter(res));
|
||||
internal_order_rejects.push(
|
||||
orders.reject(m, json_formatter(*res.error).to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
void update_order(const CancelReplaceOrderMsg &m) {
|
||||
const auto &fields = m.create_fields;
|
||||
std::string err;
|
||||
ModifyOrderRequest req;
|
||||
if (!req.init(m, err)) {
|
||||
ERROR_FN(
|
||||
"update_order",
|
||||
"{} {} {} @ {} (id={}, exch_id={}, type={}, tif={}). Rejected "
|
||||
"internally (request not sent). {}",
|
||||
fields.side, fields.quantity, req.symbol, fields.price, fields.key,
|
||||
m.exchange_order_id, fields.order_type, fields.time_in_force, err);
|
||||
internal_order_rejects.push(orders.set_error(
|
||||
fields.key,
|
||||
"Update rejected internally (request not sent). " + err));
|
||||
return;
|
||||
}
|
||||
INFO_FN(
|
||||
"update_order",
|
||||
"{} {} {} @ {} (id={}, type={}, tif={}). Sending request",
|
||||
fields.side, fields.quantity, req.symbol, fields.price, fields.key,
|
||||
m.exchange_order_id, fields.order_type, fields.time_in_force);
|
||||
const auto res = rest_api_client.modify_order(req);
|
||||
if (!res.ok()) {
|
||||
ERROR_FN(
|
||||
"update_order", "id={}, exch_id={}. {}", fields.key,
|
||||
m.exchange_order_id, json_formatter(res));
|
||||
}
|
||||
}
|
||||
|
||||
void cancel_order(const CancelOrderMsg &m) {
|
||||
CancelOrderRequest req;
|
||||
string err;
|
||||
if (!req.init(m, err)) {
|
||||
ERROR_FN(
|
||||
"cancel_order", "{} {} (id={}, exch_id={}). {}", m.side,
|
||||
req.symbol, m.key, m.exchange_order_id, err);
|
||||
internal_order_rejects.push(orders.set_error(
|
||||
m.key, "Rejected internally (request not sent). " + err));
|
||||
return;
|
||||
}
|
||||
INFO_FN(
|
||||
"cancel_order", "{} {} (id={}, exch_id={})", m.side, req.symbol,
|
||||
m.key, m.exchange_order_id);
|
||||
const auto res = rest_api_client.cancel_order(req);
|
||||
if (!res.ok()) {
|
||||
ERROR_FN(
|
||||
"cancel_order", "{} {} (id={}, exch_id={}). {}", m.side,
|
||||
req.symbol, m.key, m.exchange_order_id, json_formatter(res));
|
||||
internal_order_rejects.push(orders.set_error(
|
||||
m.key,
|
||||
fmt::format(
|
||||
"Cancel request failed. {}", json_formatter(*res.error))));
|
||||
}
|
||||
}
|
||||
|
||||
void dispatch_internal_rejects() {
|
||||
shared_ptr<const ExecutionReport> curr;
|
||||
while (internal_order_rejects.pop(curr)) {
|
||||
INFO_FN("dispatch_internal_rejects", "{}", curr->to_string());
|
||||
callbacks.exe_report(curr, {});
|
||||
}
|
||||
}
|
||||
|
||||
void dispatch_order_updates() {
|
||||
if (incoming_order_updates.empty()) {
|
||||
return;
|
||||
}
|
||||
user_data_stream::OrderUpdate u;
|
||||
OrderKey key;
|
||||
while (incoming_order_updates.pop(u)) {
|
||||
bts_assert(checked_ltou(u.data.order_id, key.value()));
|
||||
shared_ptr<const ExecutionReport> er_p =
|
||||
orders.update(u.data.client_order_id, [&](auto &er_p, bool) {
|
||||
auto &er = *er_p;
|
||||
if (!u.to_execution_report(er)) {
|
||||
WARN_FN(
|
||||
"update", "Failed to initialize exe report. {}",
|
||||
json_formatter(u));
|
||||
return;
|
||||
}
|
||||
er.security = sec_oracle.get_security(
|
||||
VENUE, bts::api::SECURITYTYPE_FUTURE, u.data.symbol);
|
||||
});
|
||||
const ExecutionReport &er = *er_p;
|
||||
string error;
|
||||
if (!validate(er, error)) {
|
||||
ERROR_FN(
|
||||
"dispatch_order_updates",
|
||||
"Generated invalid ExecutionReport. {}", error);
|
||||
continue;
|
||||
}
|
||||
switch (u.data.order_status.value) {
|
||||
case OrderStatus::FILLED:
|
||||
case OrderStatus::CANCELED:
|
||||
case OrderStatus::REJECTED: {
|
||||
std::size_t n_active = orders.erase(er.order_key);
|
||||
INFO_FN(
|
||||
"dispatch_order_updates",
|
||||
"Order reached terminal state {} (id={}). {} active orders.",
|
||||
u.data.order_status, u.data.client_order_id, n_active);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
INFO_FN(
|
||||
"dispatch_order_updates",
|
||||
"Firing exe report callback. {} {} {} @ {} {} (filled {}/{}, "
|
||||
"id={}). {} active orders",
|
||||
er.side, er.security->binance_usds_futures().security_desc(),
|
||||
er.quantity, er.price, er.order_status, er.filled_quantity,
|
||||
er.quantity, er.order_key, orders.size());
|
||||
callbacks.exe_report(er_p, {});
|
||||
}
|
||||
}
|
||||
|
||||
void dispatch_account_updates() {
|
||||
user_data_stream::AccountUpdate u;
|
||||
account_updates_vec.clear();
|
||||
while (incoming_account_updates.pop(account_updates_vec.emplace_back())) {
|
||||
}
|
||||
account_updates_vec.pop_back();
|
||||
if (account_updates_vec.empty()) {
|
||||
return;
|
||||
}
|
||||
auto &dest = pos.total_exchange_positions;
|
||||
for (const auto &acct_upd : account_updates_vec) {
|
||||
for (const auto &pos : acct_upd.data.positions) {
|
||||
bts_assert(pos.position_amount >= 0);
|
||||
auto it = std::lower_bound(
|
||||
dest.begin(), dest.end(), pos.symbol,
|
||||
[](const bts::orderman::ExchangePosition::UnitPosition &u,
|
||||
const string &ticker) {
|
||||
return u.exchange_sec_desc < ticker;
|
||||
});
|
||||
if (it == dest.end() || it->exchange_sec_desc != pos.symbol) {
|
||||
const auto *sec = sec_oracle.get_security(
|
||||
VENUE, bts::api::SECURITYTYPE_FUTURE, pos.symbol);
|
||||
if (sec == nullptr) {
|
||||
continue;
|
||||
}
|
||||
bts::orderman::ExchangePosition::UnitPosition new_pos;
|
||||
new_pos.exchange_sec_desc = pos.symbol;
|
||||
new_pos.security = sec;
|
||||
new_pos.account_id = account.id();
|
||||
new_pos.account_name = account.name();
|
||||
it = dest.insert(it, new_pos);
|
||||
}
|
||||
it->position = pos.position_side == PositionSide::SHORT
|
||||
? -pos.position_amount
|
||||
: pos.position_amount;
|
||||
it->avg_filled_price.value() = pos.break_even_price;
|
||||
}
|
||||
}
|
||||
callbacks.exchange_position_state(pos);
|
||||
}
|
||||
};
|
||||
|
||||
UsdsFuturesGateway::UsdsFuturesGateway(
|
||||
bts::md::SecurityOracle sec_oracle,
|
||||
GatewayConfig session_cfg,
|
||||
bts::config::JetFIX::Connection jetfix_connection)
|
||||
: me_(std::make_unique<PImpl>(
|
||||
std::move(sec_oracle),
|
||||
std::move(session_cfg),
|
||||
std::move(jetfix_connection))) {
|
||||
bts_assert(
|
||||
me_->config.account.size() <=
|
||||
ExecutionReport{}.account_name.value().size());
|
||||
}
|
||||
|
||||
UsdsFuturesGateway::~UsdsFuturesGateway() = default;
|
||||
|
||||
void UsdsFuturesGateway::start() { me_->start(); }
|
||||
|
||||
void UsdsFuturesGateway::on_securities(
|
||||
const bts::mercury::Update<NewSecuritiesEvent> & /*update*/) {
|
||||
INFO_FN(
|
||||
"on_securities", "t_id: {} t_name: {}", bts::os::get_thread_id(),
|
||||
bts::os::get_thread_name());
|
||||
}
|
||||
|
||||
void UsdsFuturesGateway::on_admin_data(
|
||||
const bts::mercury::Update<NewAdminDataEvent> &update) {
|
||||
INFO_FN(
|
||||
"on_admin_data", "t_id: {} t_name: {}", bts::os::get_thread_id(),
|
||||
bts::os::get_thread_name());
|
||||
if (update.events == nullptr) {
|
||||
return;
|
||||
}
|
||||
const auto *account = bts::admin::AdminServiceUtils::find_account(
|
||||
*update.events->back().admin_data, me_->config.account);
|
||||
if (account == nullptr) {
|
||||
WARN_FN(
|
||||
"on_admin_data",
|
||||
"Gateway account not found. Cannot execute orders. name: '{}'",
|
||||
me_->config.account);
|
||||
return;
|
||||
}
|
||||
if (me_->account.name() == account->name() &&
|
||||
me_->account.id() == account->id()) {
|
||||
return;
|
||||
}
|
||||
me_->account.CopyFrom(*account);
|
||||
INFO_FN("on_admin_data", "Resolved gateway account '{}'", account->name());
|
||||
}
|
||||
|
||||
void UsdsFuturesGateway::send_msgs(
|
||||
vector<OrderMsg> const &msg_vec, bts::trace::CorID) {
|
||||
INFO_FN("send_msgs", "count: {}", msg_vec.size());
|
||||
for (const auto &msg : msg_vec) {
|
||||
std::visit(
|
||||
bts::overloaded{
|
||||
[&](const CreateOrderMsg &m) { me_->create_order(m); },
|
||||
[&](const CancelReplaceOrderMsg &m) { me_->update_order(m); },
|
||||
[&](const CancelOrderMsg &m) { me_->cancel_order(m); },
|
||||
[&](const RFQMsg &) {
|
||||
bts_assert_msg(
|
||||
false, "BinanceUsdsFutures RFQ not implemented");
|
||||
},
|
||||
},
|
||||
msg);
|
||||
}
|
||||
}
|
||||
|
||||
void UsdsFuturesGateway::send_fix(
|
||||
const bts::jetfix::OutFIXPtr &, bts::trace::CorID) {
|
||||
bts_assert_msg(false, "ExecutionGateway does not support fix");
|
||||
}
|
||||
|
||||
void UsdsFuturesGateway::dispatch_incoming_fix_messages() {
|
||||
me_->dispatch_internal_rejects();
|
||||
me_->dispatch_order_updates();
|
||||
me_->dispatch_account_updates();
|
||||
}
|
||||
|
||||
// Callbacks
|
||||
bts::orderman::OnExeReport &UsdsFuturesGateway::on_exe_report() {
|
||||
return me_->callbacks.exe_report;
|
||||
}
|
||||
bts::orderman::OnGwStatusChange &UsdsFuturesGateway::on_gw_status_change() {
|
||||
return me_->callbacks.gw_status_change;
|
||||
}
|
||||
bts::orderman::OnCancelReject &UsdsFuturesGateway::on_cancel_reject() {
|
||||
return me_->callbacks.cancel_reject;
|
||||
}
|
||||
bts::orderman::OnExchangePositionState &
|
||||
UsdsFuturesGateway::on_exchange_position_state() {
|
||||
return me_->callbacks.exchange_position_state;
|
||||
}
|
||||
bts::orderman::OnExchangeOpenOrdersState &
|
||||
UsdsFuturesGateway::on_exchange_open_orders() {
|
||||
return me_->callbacks.exchange_open_orders;
|
||||
}
|
||||
|
||||
} // namespace binance_execution::usds_futures
|
||||
+8
-5
@@ -1,21 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/usds_futures/execution_gateway_config.h"
|
||||
#include "lib/config/protos/jetfix.pb.h"
|
||||
#include "lib/md/SecurityOracle.h"
|
||||
#include "lib/orderman/gateways/IExecutionGateway.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace binance_execution {
|
||||
struct GatewayConfig;
|
||||
}
|
||||
|
||||
namespace binance_execution::usds_futures {
|
||||
|
||||
class ExecutionGateway : public bts::orderman::IExecutionGateway {
|
||||
class UsdsFuturesGateway : public bts::orderman::IExecutionGateway {
|
||||
public:
|
||||
explicit ExecutionGateway(
|
||||
explicit UsdsFuturesGateway(
|
||||
bts::md::SecurityOracle sec_oracle,
|
||||
ExecutionGatewayConfig session_cfg,
|
||||
GatewayConfig session_cfg,
|
||||
bts::config::JetFIX::Connection jetfix_connection);
|
||||
~ExecutionGateway() override;
|
||||
~UsdsFuturesGateway() override;
|
||||
|
||||
void start() override;
|
||||
void dispatch_incoming_fix_messages() override;
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution::usds_futures::user_data_streams {
|
||||
namespace binance_execution::usds_futures::user_data_stream {
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/derivatives/usds-margined-futures/user-data-streams/Event-Balance-and-Position-Update
|
||||
@@ -130,4 +130,4 @@ inline void from_json(const nlohmann::json &j, AccountUpdate &u) {
|
||||
j.at("a").get_to(u.data);
|
||||
};
|
||||
|
||||
} // namespace binance_execution::usds_futures::user_data_streams
|
||||
} // namespace binance_execution::usds_futures::user_data_stream
|
||||
+45
-2
@@ -1,7 +1,50 @@
|
||||
#include "order_update.h"
|
||||
#include "binance_execution/type_converters.h"
|
||||
#include "lib/config/protos/jetfix.pb.h"
|
||||
#include "lib/domain_model/Security.pb.h"
|
||||
#include "lib/io/JsonReader.h"
|
||||
#include "lib/orderman/orderman_utils/ExecutionReport.h"
|
||||
#include "lib/utils/checked_arithmetic.h"
|
||||
#include "lib/utils/string_utils.h"
|
||||
|
||||
namespace binance_execution::usds_futures::user_data_streams {
|
||||
namespace binance_execution::usds_futures::user_data_stream {
|
||||
|
||||
using bts::system::math::checked_ltoul;
|
||||
using bts::system::string_utils::copy_to_array;
|
||||
using bts::system::string_utils::parse_uint;
|
||||
|
||||
bool OrderUpdate::to_execution_report(
|
||||
bts::orderman::ExecutionReport &er) const {
|
||||
er.exchange = bts::api::VENUE_BINANCE_USDS_FUTURES;
|
||||
er.transact_time = bts::orderman::ExchangeTimeStamp64{
|
||||
convert_time_stamp(transaction_time).nanos()};
|
||||
if (!parse_uint(data.client_order_id, er.order_key.value())) {
|
||||
return false;
|
||||
}
|
||||
bts_assert(checked_ltoul(
|
||||
data.order_id,
|
||||
er.exchange_order_id.emplace<bts::orderman::ExchangeOrderID64>()
|
||||
.value()));
|
||||
er.security = nullptr;
|
||||
er.leg_security = nullptr;
|
||||
er.side = data.side == Side::BUY ? bts::api::SIDE_BUY : bts::api::SIDE_SELL;
|
||||
er.time_in_force = convert_time_in_force(data.time_in_force);
|
||||
er.order_type = convert_order_type(data.order_type);
|
||||
er.exec_type = convert_exec_type(data.execution_type, data.order_status);
|
||||
er.order_status = convert_order_status(data.order_status);
|
||||
er.manual_order_indicator = bts::api::orderman::MANUAL_ORDER_INDICATOR_NO;
|
||||
er.price.value() = data.orig_price;
|
||||
er.quantity = data.orig_quantity;
|
||||
er.filled_quantity = data.total_filled_qty;
|
||||
er.last_shares = data.last_filled_qty;
|
||||
// er.max_show = data.;
|
||||
er.last_price.value() = data.last_filled_price;
|
||||
er.avg_price.value() = data.avg_price;
|
||||
er.stop_price.value() = data.stop_price;
|
||||
copy_to_array<40>(
|
||||
std::to_string(data.trade_id), er.exchange_fill_id.value());
|
||||
return true;
|
||||
}
|
||||
|
||||
void to_json(nlohmann::json &j, const OrderUpdate::Data &d) {
|
||||
j["s"] = d.symbol;
|
||||
@@ -78,4 +121,4 @@ void from_json(const nlohmann::json &j, OrderUpdate::Data &d) {
|
||||
j.at("gtd").get_to(d.good_till_time);
|
||||
}
|
||||
|
||||
} // namespace binance_execution::usds_futures::user_data_streams
|
||||
} // namespace binance_execution::usds_futures::user_data_stream
|
||||
+12
-2
@@ -12,7 +12,15 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace binance_execution::usds_futures::user_data_streams {
|
||||
namespace bts::config {
|
||||
class JetFIX_Connection;
|
||||
}
|
||||
|
||||
namespace bts::orderman {
|
||||
class ExecutionReport;
|
||||
}
|
||||
|
||||
namespace binance_execution::usds_futures::user_data_stream {
|
||||
|
||||
/**
|
||||
* @a https://developers.binance.com/docs/derivatives/usds-margined-futures/user-data-streams/Event-Order-Update
|
||||
@@ -60,6 +68,8 @@ struct OrderUpdate {
|
||||
};
|
||||
|
||||
Data data; // json: o
|
||||
|
||||
bool to_execution_report(bts::orderman::ExecutionReport &er) const;
|
||||
};
|
||||
|
||||
void to_json(nlohmann::json &j, const OrderUpdate::Data &d);
|
||||
@@ -86,4 +96,4 @@ inline void from_json(const nlohmann::json &j, OrderUpdate &u) {
|
||||
j.at("o").get_to(u.data);
|
||||
}
|
||||
|
||||
} // namespace binance_execution::usds_futures::user_data_streams
|
||||
} // namespace binance_execution::usds_futures::user_data_stream
|
||||
+11
-14
@@ -1,6 +1,6 @@
|
||||
#include "user_data_subscription.h"
|
||||
#include "binance_execution/usds_futures/user_data_streams/account_update.h"
|
||||
#include "binance_execution/usds_futures/user_data_streams/order_update.h"
|
||||
#include "binance_execution/usds_futures/user_data_stream/account_update.h"
|
||||
#include "binance_execution/usds_futures/user_data_stream/order_update.h"
|
||||
#include "lib/core/TimeStamp.h"
|
||||
#include "lib/io/websocket/websocket_client.h"
|
||||
#include "lib/logging/logging.h"
|
||||
@@ -10,7 +10,7 @@
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
namespace binance_execution::usds_futures::user_data_streams {
|
||||
namespace binance_execution::usds_futures::user_data_stream {
|
||||
|
||||
#define _LOG_FN(level, fn, ...) \
|
||||
LOG(OrderMan, level, "BinanceUsdsFutures::" fn ". " __VA_ARGS__)
|
||||
@@ -42,8 +42,8 @@ struct UserDataSubscription::PImpl {
|
||||
optional<thread> ws_run_thread;
|
||||
optional<thread> ws_ping_thread;
|
||||
|
||||
Delegate<void(user_data_streams::AccountUpdate)> account_update_callback;
|
||||
Delegate<void(user_data_streams::OrderUpdate)> order_update_callback;
|
||||
Delegate<void(AccountUpdate)> account_update_callback;
|
||||
Delegate<void(OrderUpdate)> order_update_callback;
|
||||
|
||||
void start() {
|
||||
lock_guard<mutex> lock{instance_mx};
|
||||
@@ -58,7 +58,7 @@ struct UserDataSubscription::PImpl {
|
||||
ws_cli.message_handler().bind<&PImpl::on_message>(*this);
|
||||
ws_cli.close_handler().bind<&PImpl::on_close>(*this);
|
||||
INFO_FN("start", "Creating websocket client. url: {}", url);
|
||||
ws_cli.connect({}, false);
|
||||
ws_cli.connect();
|
||||
ws_run_thread.emplace([&]() {
|
||||
INFO_FN("ws_run_thread", "thread starting. url: {}", url);
|
||||
ws_cli.run();
|
||||
@@ -112,8 +112,7 @@ struct UserDataSubscription::PImpl {
|
||||
const auto event_type = event_type_it->get<string>();
|
||||
if (event_type == "ACCOUNT_UPDATE") {
|
||||
try {
|
||||
account_update_callback(
|
||||
msg.get<user_data_streams::AccountUpdate>());
|
||||
account_update_callback(msg.get<AccountUpdate>());
|
||||
} catch (const std::exception &ex) {
|
||||
ERROR_FN(
|
||||
"on_message", "account_update callback threw: {}",
|
||||
@@ -121,7 +120,7 @@ struct UserDataSubscription::PImpl {
|
||||
}
|
||||
} else if (event_type == "ORDER_TRADE_UPDATE") {
|
||||
try {
|
||||
order_update_callback(msg.get<user_data_streams::OrderUpdate>());
|
||||
order_update_callback(msg.get<OrderUpdate>());
|
||||
} catch (const std::exception &ex) {
|
||||
ERROR_FN(
|
||||
"on_message", "order_update callback threw: {}",
|
||||
@@ -158,14 +157,12 @@ void UserDataSubscription::stop() {
|
||||
me_->stop();
|
||||
}
|
||||
|
||||
Delegate<void(user_data_streams::AccountUpdate)> &
|
||||
UserDataSubscription::account_update_callback() {
|
||||
Delegate<void(AccountUpdate)> &UserDataSubscription::account_update_callback() {
|
||||
return me_->account_update_callback;
|
||||
}
|
||||
|
||||
Delegate<void(user_data_streams::OrderUpdate)> &
|
||||
UserDataSubscription::order_update_callback() {
|
||||
Delegate<void(OrderUpdate)> &UserDataSubscription::order_update_callback() {
|
||||
return me_->order_update_callback;
|
||||
}
|
||||
|
||||
} // namespace binance_execution::usds_futures::user_data_streams
|
||||
} // namespace binance_execution::usds_futures::user_data_stream
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/usds_futures/user_data_streams/account_update.h"
|
||||
#include "binance_execution/usds_futures/user_data_streams/order_update.h"
|
||||
#include "binance_execution/usds_futures/user_data_stream/account_update.h"
|
||||
#include "binance_execution/usds_futures/user_data_stream/order_update.h"
|
||||
#include "lib/utils/Delegate.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace binance_execution::usds_futures::user_data_streams {
|
||||
namespace binance_execution::usds_futures::user_data_stream {
|
||||
|
||||
|
||||
/**
|
||||
@@ -27,4 +27,4 @@ class UserDataSubscription {
|
||||
std::unique_ptr<PImpl> me_;
|
||||
};
|
||||
|
||||
} // namespace binance_execution::usds_futures::user_data_streams
|
||||
} // namespace binance_execution::usds_futures::user_data_stream
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
#include "listen_key_requestor.h"
|
||||
#include "binance_execution/usds_futures/rest_api_client.h"
|
||||
#include "lib/core/TimeStamp.h"
|
||||
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace binance_execution::usds_futures {
|
||||
|
||||
using bts::core::Delegate;
|
||||
using bts::core::TimeStamp;
|
||||
using std::optional;
|
||||
using std::string;
|
||||
using std::thread;
|
||||
|
||||
struct ListenKeyRequestor::PImpl {
|
||||
RestApiClient rest_api_client;
|
||||
string listen_key;
|
||||
TimeStamp listen_key_timestamp;
|
||||
optional<thread> listen_key_thread;
|
||||
Delegate<void(const string &)> callback;
|
||||
bool is_running = false;
|
||||
|
||||
PImpl(RestApiClient rest_api_client)
|
||||
: rest_api_client(std::move(rest_api_client)) {}
|
||||
|
||||
~PImpl() { stop(); }
|
||||
|
||||
void start() {
|
||||
bts_assert(!is_running);
|
||||
is_running = true;
|
||||
listen_key_thread.emplace([this]() { run(); });
|
||||
}
|
||||
|
||||
void stop() {
|
||||
is_running = false;
|
||||
if (listen_key_thread) {
|
||||
listen_key_thread->join();
|
||||
}
|
||||
}
|
||||
|
||||
void run() {
|
||||
while (is_running) {
|
||||
static const auto refresh_period =
|
||||
bts::core::TimeSpan::from_seconds(60 * 30);
|
||||
const auto now = bts::core::TimeStamp::now();
|
||||
if (listen_key.empty()) {
|
||||
const auto &res = rest_api_client.get_listen_key();
|
||||
if (!res.ok()) {
|
||||
continue;
|
||||
}
|
||||
bts_assert_msgf(
|
||||
res.ok(), "Failed to get listen key. {}", res.error);
|
||||
listen_key = res.response->listenKey;
|
||||
bts_assert(!listen_key.empty());
|
||||
listen_key_timestamp = now;
|
||||
callback(listen_key);
|
||||
}
|
||||
if ((now - listen_key_timestamp) > refresh_period) {
|
||||
const auto &res = rest_api_client.refresh_listen_key();
|
||||
bts_assert_msgf(
|
||||
res.ok(), "Failed to refresh listen key. {}", res.error);
|
||||
listen_key_timestamp = now;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ListenKeyRequestor::ListenKeyRequestor(RestApiClient rest_api_client)
|
||||
: me_(std::make_unique<PImpl>(std::move(rest_api_client))) {}
|
||||
|
||||
ListenKeyRequestor::~ListenKeyRequestor() = default;
|
||||
|
||||
void ListenKeyRequestor::start() { me_->start(); }
|
||||
void ListenKeyRequestor::stop() { me_->stop(); }
|
||||
Delegate<void(const string &)> &ListenKeyRequestor::callback() {
|
||||
return me_->callback;
|
||||
}
|
||||
|
||||
} // namespace binance_execution::usds_futures
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "binance_execution/usds_futures/rest_api_client.h"
|
||||
#include "lib/utils/Delegate.h"
|
||||
#include <memory>
|
||||
|
||||
namespace binance_execution::usds_futures {
|
||||
|
||||
/**
|
||||
* Requests the listen key for user data stream subscriptions, and periodically
|
||||
* refreshes it to keep subscriptions alive.
|
||||
*
|
||||
* @a https://developers.binance.com/docs/derivatives/usds-margined-futures/user-data-streams
|
||||
*/
|
||||
class ListenKeyRequestor {
|
||||
public:
|
||||
explicit ListenKeyRequestor(RestApiClient rest_api_client);
|
||||
~ListenKeyRequestor();
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
bts::core::Delegate<void(const std::string &)> &callback();
|
||||
|
||||
private:
|
||||
struct PImpl;
|
||||
std::unique_ptr<PImpl> me_;
|
||||
};
|
||||
|
||||
} // namespace binance_execution::usds_futures
|
||||
File renamed without changes.
File renamed without changes.
+9
-9
@@ -50,15 +50,15 @@ Logging {
|
||||
initial_queue_size: 120001
|
||||
max_queue_growth: 4
|
||||
console {
|
||||
default_level: "Warn"
|
||||
subsystem {
|
||||
name: "OrderMan"
|
||||
level: "Diag"
|
||||
}
|
||||
subsystem {
|
||||
name: "Http"
|
||||
level: "Diag"
|
||||
}
|
||||
default_level: "Info"
|
||||
# subsystem {
|
||||
# name: "OrderMan"
|
||||
# level: "Diag"
|
||||
# }
|
||||
# subsystem {
|
||||
# name: "Http"
|
||||
# level: "Diag"
|
||||
# }
|
||||
}
|
||||
file {
|
||||
default_level: "Info"
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "execution_gateway_provider.h"
|
||||
#include "binance_execution/spots/execution_gateway.h"
|
||||
#include "binance_execution/spots/execution_gateway_config.h"
|
||||
#include "binance_execution/usds_futures/execution_gateway.h"
|
||||
#include "binance_execution/usds_futures/execution_gateway_config.h"
|
||||
#include "binance_execution/cross_margin/cross_margin_gateway.h"
|
||||
#include "binance_execution/gateway_config.h"
|
||||
#include "binance_execution/spots/spots_gateway.h"
|
||||
#include "binance_execution/usds_futures/usds_futures_gateway.h"
|
||||
#include "lib/domain_model/Security.pb.h"
|
||||
#include "lib/io/JsonReader.h"
|
||||
#include "lib/orderman/gateways/CCAPIExecutionGateway.h"
|
||||
@@ -88,17 +88,27 @@ struct ExecutionGatewayProvider::PImpl {
|
||||
return std::make_unique<OkxExecutionGateway>(o);
|
||||
}
|
||||
case api::VENUE_BINANCE: {
|
||||
const auto cfg = parse_config_json<
|
||||
binance_execution::spots::ExecutionGatewayConfig>(conn.name());
|
||||
return std::make_unique<binance_execution::spots::ExecutionGateway>(
|
||||
const auto cfg =
|
||||
parse_config_json<binance_execution::GatewayConfig>(conn.name());
|
||||
if (cfg.use_cross_margin_api) {
|
||||
return std::make_unique<
|
||||
binance_execution::cross_margin::CrossMarginGateway>(
|
||||
o, std::move(cfg), conn);
|
||||
}
|
||||
return std::make_unique<binance_execution::spots::SpotsGateway>(
|
||||
o, std::move(cfg), conn);
|
||||
}
|
||||
case api::VENUE_BINANCE_USDS_FUTURES: {
|
||||
auto cfg = parse_config_json<
|
||||
binance_execution::usds_futures::ExecutionGatewayConfig>(
|
||||
conn.name());
|
||||
auto cfg =
|
||||
parse_config_json<binance_execution::GatewayConfig>(conn.name());
|
||||
if (cfg.use_cross_margin_api) {
|
||||
WARN_FN(
|
||||
"create_gateway(BinanceUsdsFutures)",
|
||||
"Config has 'use_cross_margin_api' set to true, but this is "
|
||||
"only supported for spots (using regular api).");
|
||||
}
|
||||
return std::make_unique<
|
||||
binance_execution::usds_futures::ExecutionGateway>(
|
||||
binance_execution::usds_futures::UsdsFuturesGateway>(
|
||||
o, std::move(cfg), conn);
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -22,8 +22,10 @@ print_help_and_exit() {
|
||||
}
|
||||
|
||||
this_dir="$(dirname "${BASH_SOURCE[0]:-$0}")"
|
||||
profiles_dir="$(realpath "$this_dir/../src/lib/config/profiles")"
|
||||
declare -r spark_conf_dir="${HOME:-/home/bts}/sparkdata/conf"
|
||||
profiles_dir="$(realpath "$this_dir/../src/server/sparkd/configurations")"
|
||||
declare -r spark_data_dir="${HOME:-/home/bts}/sparkdata"
|
||||
declare -r spark_conf_dir="$spark_data_dir/conf"
|
||||
mkdir -p "$spark_data_dir"
|
||||
|
||||
list() {
|
||||
if [ ! -d "$profiles_dir" ]; then
|
||||
|
||||
Reference in new issue
Block a user