Compare commits

...
Author SHA1 Message Date
John Colvin 0e08bad534 cleanup: Remove unnecessary includes 2025-03-04 05:02:35 +00:00
John Colvin 09573167b8 feat: Add binance gw for cross margin orders
Spots only
2025-03-04 05:02:35 +00:00
John Colvin caa03dcfa7 refactor: Rename user_data_streams dir, ns without plural 's'
There is one user data stream for Binance Spots, and another for Usds
Futures. We model both of these user data streams in separate folders,
so each "user_data_stream" dir represents a single stream.
2025-03-03 19:32:16 +00:00
John Colvin d010ba7ae7 refactor: Abstract out common binance gw code
For all sub-venues to use
2025-03-03 19:32:16 +00:00
John Colvin c60df8bcfa refactor: Rename binance exe gw filenames so they are unique
The debugger gets confused when a particular lib contains multiple
source files with the same name (in different dirs).
2025-03-03 19:32:16 +00:00
John Colvin 634e69f72b fix: Trim account name string when converting to Fill proto 2025-03-03 19:32:16 +00:00
John Colvin 2397a15b23 fix: Disable CURLOPT_FAILONERROR to always get response body
Even when the http error code indicates failure. The payload may contain useful
information about the error.
2025-03-03 19:32:16 +00:00
John Colvin dfacf73289 Improve logging/error reporting in binance execution gw
In the process, consolidate binance exe report stores into single class.
2025-03-03 19:32:16 +00:00
John Colvin 0339fe9749 cleanup: Specify bts account directly in binance gw config
This eliminates the need for an ExchangeSubAccountResolver.
2025-03-03 19:32:16 +00:00
John Colvin ff1d71a36d cleanup: Replace includes w/ fwd declares in header 2025-03-03 19:32:16 +00:00
John Colvin 5b9fc54ea6 fix: Reject orders w/ invalid TimeInForce at binance
Do this internally (before sending them to the exchange)
2025-03-03 19:32:16 +00:00
John Colvin 24f0eef2eb fix: Report internal reject msgs to ExecutionReport
For Binance execution gateways (spots and usds futures)
2025-03-03 19:32:16 +00:00
John Colvin 01e90c2497 Add reconnect behavior to WebsocketClient 2025-03-03 19:32:16 +00:00
John Colvin a62b4af667 cleanup: Perform basic init in WebsocketClient ctr 2025-03-03 19:32:16 +00:00
John Colvin 3aebd25838 Move config profiles dir to server/sparkd 2025-03-03 19:32:16 +00:00
95 changed files with 4367 additions and 2060 deletions

No files matched your search

+9
View File
@@ -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,
+98
View File
@@ -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
+1 -1
View File
@@ -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) {
+123 -73
View File
@@ -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
+10 -4
View File
@@ -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
+1 -1
View File
@@ -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
@@ -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
@@ -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("&timestamp=");
qps.append(std::to_string(req.timestamp));
}
} // namespace binance_execution::cross_margin
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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("&timestamp=");
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("&quoteOrderQty=");
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
@@ -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
@@ -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
@@ -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());
@@ -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
@@ -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
@@ -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());
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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());
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
@@ -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
@@ -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);
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
@@ -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
@@ -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,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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:
+4 -2
View File
@@ -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