From 88b67c9f89d1a0f687571e22834c4cffc7ea0514 Mon Sep 17 00:00:00 2001 From: Stephane Janel Date: Mon, 18 Nov 2024 21:50:06 +0100 Subject: [PATCH] Use glaze::json for Binance private API --- CMakeLists.txt | 4 +- src/api/common/include/binance-common-api.hpp | 29 +- .../common/include/binance-common-schema.hpp | 41 ++ src/api/common/src/binance-common-api.cpp | 123 ++--- src/api/exchanges/src/binance-schema.hpp | 175 +++++++ src/api/exchanges/src/binanceprivateapi.cpp | 426 ++++++++++-------- .../include/coincentercommandtype.hpp | 10 +- .../src/coincentercommandtype.cpp | 27 +- src/http-request/include/request-retry.hpp | 60 ++- 9 files changed, 581 insertions(+), 314 deletions(-) create mode 100644 src/api/common/include/binance-common-schema.hpp create mode 100644 src/api/exchanges/src/binance-schema.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b3ddb19..31df70e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -117,8 +117,8 @@ if(NOT glaze) FetchContent_Declare( glaze - URL https://github.com/stephenberry/glaze/archive/refs/tags/v4.0.0.tar.gz - URL_HASH SHA256=6114cd6fc2eb39e396e229c7971b2ca5aeb8a670f0dfcd37d6223d766f4afecf + URL https://github.com/stephenberry/glaze/archive/refs/tags/v4.0.1.tar.gz + URL_HASH SHA256=0026aca33201ee6d3a820fb5926f36ba8c838bfd3120e2e179b0eee62b5bd231 ) list(APPEND fetchContentPackagesToMakeAvailable glaze) diff --git a/src/api/common/include/binance-common-api.hpp b/src/api/common/include/binance-common-api.hpp index 1b4e2435..4e825cc2 100644 --- a/src/api/common/include/binance-common-api.hpp +++ b/src/api/common/include/binance-common-api.hpp @@ -2,8 +2,8 @@ #include +#include "binance-common-schema.hpp" #include "cachedresult.hpp" -#include "cct_json-container.hpp" #include "curlhandle.hpp" #include "currencycode.hpp" #include "currencycodeset.hpp" @@ -11,7 +11,6 @@ #include "monetaryamount.hpp" #include "monetaryamountbycurrencyset.hpp" #include "runmodes.hpp" -#include "timedef.hpp" namespace cct { @@ -20,17 +19,6 @@ class PermanentCurlOptions; namespace api { -class BinanceGlobalInfosFunc { - public: - BinanceGlobalInfosFunc(AbstractMetricGateway* pMetricGateway, const PermanentCurlOptions& permanentCurlOptions, - settings::RunMode runMode); - - json::container operator()(); - - private: - CurlHandle _curlHandle; -}; - class BinanceGlobalInfos { public: BinanceGlobalInfos(CachedResultOptions&& cachedResultOptions, AbstractMetricGateway* pMetricGateway, @@ -45,8 +33,19 @@ class BinanceGlobalInfos { private: friend class BinancePrivate; - static CurrencyExchangeFlatSet ExtractTradableCurrencies(const json::container& allCoins, - const CurrencyCodeSet& excludedCurrencies); + class BinanceGlobalInfosFunc { + public: + BinanceGlobalInfosFunc(AbstractMetricGateway* pMetricGateway, const PermanentCurlOptions& permanentCurlOptions, + settings::RunMode runMode); + + schema::binance::NetworkCoinDataVector operator()(); + + private: + CurlHandle _curlHandle; + }; + + static CurrencyExchangeFlatSet ExtractTradableCurrencies( + const schema::binance::NetworkCoinDataVector& networkCoinDataVector, const CurrencyCodeSet& excludedCurrencies); std::mutex _mutex; CachedResult _globalInfosCache; diff --git a/src/api/common/include/binance-common-schema.hpp b/src/api/common/include/binance-common-schema.hpp new file mode 100644 index 00000000..dae75082 --- /dev/null +++ b/src/api/common/include/binance-common-schema.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include "cct_smallvector.hpp" +#include "cct_string.hpp" +#include "cct_type_traits.hpp" +#include "cct_vector.hpp" +#include "monetaryamount.hpp" + +namespace cct::schema::binance { + +struct NetworkListElement { + bool isDefault{}; + bool depositEnable{}; + bool withdrawEnable{}; + MonetaryAmount withdrawFee; + + auto operator<=>(const NetworkListElement&) const = default; +}; + +struct NetworkCoinData { + string coin; + bool isLegalMoney{}; + SmallVector networkList; + + using trivially_relocatable = + std::bool_constant && + is_trivially_relocatable_v>>::type; + + auto operator<=>(const NetworkCoinData&) const = default; +}; + +using NetworkCoinDataVector = vector; + +struct NetworkCoinAll { + string code; + NetworkCoinDataVector data; +}; + +} // namespace cct::schema::binance \ No newline at end of file diff --git a/src/api/common/src/binance-common-api.cpp b/src/api/common/src/binance-common-api.cpp index 05115fbc..daa24824 100644 --- a/src/api/common/src/binance-common-api.cpp +++ b/src/api/common/src/binance-common-api.cpp @@ -7,9 +7,10 @@ #include #include "abstractmetricgateway.hpp" +#include "binance-common-schema.hpp" #include "cachedresult.hpp" -#include "cct_json-container.hpp" #include "cct_log.hpp" +#include "cct_smallvector.hpp" #include "curlhandle.hpp" #include "currencycode.hpp" #include "currencycodeset.hpp" @@ -27,67 +28,48 @@ namespace cct::api { namespace { -json::container PublicQuery(CurlHandle& curlHandle, std::string_view method) { - RequestRetry requestRetry(curlHandle, CurlOptions(HttpRequestType::kGet)); - - return requestRetry.queryJson(method, [](const json::container& jsonResponse) { - const auto foundErrorIt = jsonResponse.find("code"); - const auto foundMsgIt = jsonResponse.find("msg"); - if (foundErrorIt != jsonResponse.end() && foundMsgIt != jsonResponse.end()) { - const int statusCode = foundErrorIt->get(); // "1100" for instance - log::warn("Binance error ({}), full: '{}'", statusCode, jsonResponse.dump()); - return RequestRetry::Status::kResponseError; - } - return RequestRetry::Status::kResponseOK; - }); -} - constexpr std::string_view kCryptoFeeBaseUrl = "https://www.binance.com"; } // namespace -BinanceGlobalInfosFunc::BinanceGlobalInfosFunc(AbstractMetricGateway* pMetricGateway, - const PermanentCurlOptions& permanentCurlOptions, - settings::RunMode runMode) +BinanceGlobalInfos::BinanceGlobalInfosFunc::BinanceGlobalInfosFunc(AbstractMetricGateway* pMetricGateway, + const PermanentCurlOptions& permanentCurlOptions, + settings::RunMode runMode) : _curlHandle(kCryptoFeeBaseUrl, pMetricGateway, permanentCurlOptions, runMode) {} -json::container BinanceGlobalInfosFunc::operator()() { - json::container ret = PublicQuery(_curlHandle, "/bapi/capital/v1/public/capital/getNetworkCoinAll"); - auto dataIt = ret.find("data"); - json::container dataRet; - if (dataIt == ret.end() || !dataIt->is_array()) { - log::error("Unexpected reply from binance getNetworkCoinAll, no data array"); - dataRet = json::container::array_t(); - } else { - dataRet = std::move(*dataIt); +schema::binance::NetworkCoinDataVector BinanceGlobalInfos::BinanceGlobalInfosFunc::operator()() { + RequestRetry requestRetry(_curlHandle, CurlOptions(HttpRequestType::kGet)); + + schema::binance::NetworkCoinAll ret = + requestRetry.query( + "/bapi/capital/v1/public/capital/getNetworkCoinAll", [](const auto& response) { + static constexpr std::string_view kExpectedCode = "000000"; + if (response.code != kExpectedCode) { + log::warn("Binance error ({})", response.code); + return RequestRetry::Status::kResponseError; + } + return RequestRetry::Status::kResponseOK; + }); + + const auto [endIt, oldEndIt] = + std::ranges::remove_if(ret.data, [](const auto& el) { return el.coin.size() > CurrencyCode::kMaxLen; }); + + if (endIt != ret.data.end()) { + log::debug("{} currencies discarded for binance as code too long", ret.data.end() - endIt); + ret.data.erase(endIt, ret.data.end()); } - const auto endIt = std::remove_if(dataRet.begin(), dataRet.end(), [](const json::container& el) { - return el["coin"].get().size() > CurrencyCode::kMaxLen; - }); - - if (endIt != dataRet.end()) { - log::debug("{} currencies discarded for binance as code too long", dataRet.end() - endIt); - dataRet.erase(endIt, dataRet.end()); - } + std::ranges::sort(ret.data); - std::sort(dataRet.begin(), dataRet.end(), [](const json::container& lhs, const json::container& rhs) { - return lhs["coin"].get() < rhs["coin"].get(); - }); - return dataRet; + return ret.data; } namespace { -MonetaryAmount ComputeWithdrawalFeesFromNetworkList(CurrencyCode cur, const json::container& coinElem) { +MonetaryAmount ComputeWithdrawalFeesFromNetworkList(CurrencyCode cur, const auto& coinElem) { MonetaryAmount withdrawFee(0, cur); - auto networkListIt = coinElem.find("networkList"); - if (networkListIt == coinElem.end()) { - log::error("Unexpected Binance public coin data format, returning 0 monetary amount"); - return withdrawFee; - } - for (const json::container& networkListPart : *networkListIt) { - MonetaryAmount fee(networkListPart["withdrawFee"].get(), cur); - auto isDefaultIt = networkListPart.find("isDefault"); - if (isDefaultIt != networkListPart.end() && isDefaultIt->get()) { + for (const auto& networkListPart : coinElem.networkList) { + MonetaryAmount fee(networkListPart.withdrawFee, cur); + if (networkListPart.isDefault) { withdrawFee = fee; break; } @@ -105,13 +87,10 @@ MonetaryAmountByCurrencySet BinanceGlobalInfos::queryWithdrawalFees() { std::lock_guard guard(_mutex); const auto& allCoins = _globalInfosCache.get(); - MonetaryAmountVector fees; - - fees.reserve(allCoins.size()); + MonetaryAmountVector fees(allCoins.size()); - std::transform(allCoins.begin(), allCoins.end(), std::back_inserter(fees), [](const json::container& el) { - CurrencyCode cur(el["coin"].get()); - return ComputeWithdrawalFeesFromNetworkList(cur, el); + std::ranges::transform(allCoins, fees.begin(), [](const auto& el) { + return ComputeWithdrawalFeesFromNetworkList(CurrencyCode{el.coin}, el); }); log::info("Retrieved {} withdrawal fees for binance", fees.size()); @@ -123,10 +102,8 @@ MonetaryAmount BinanceGlobalInfos::queryWithdrawalFee(CurrencyCode currencyCode) const auto& allCoins = _globalInfosCache.get(); const auto curStr = currencyCode.str(); - const auto it = std::partition_point(allCoins.begin(), allCoins.end(), [&curStr](const json::container& el) { - return el["coin"].get() < curStr; - }); - if (it != allCoins.end() && (*it)["coin"].get() == curStr) { + const auto it = std::ranges::partition_point(allCoins, [&curStr](const auto& el) { return el.coin < curStr; }); + if (it != allCoins.end() && it->coin == curStr) { return ComputeWithdrawalFeesFromNetworkList(currencyCode, *it); } return MonetaryAmount(0, currencyCode); @@ -137,30 +114,28 @@ CurrencyExchangeFlatSet BinanceGlobalInfos::queryTradableCurrencies(const Curren return ExtractTradableCurrencies(_globalInfosCache.get(), excludedCurrencies); } -CurrencyExchangeFlatSet BinanceGlobalInfos::ExtractTradableCurrencies(const json::container& allCoins, - const CurrencyCodeSet& excludedCurrencies) { +CurrencyExchangeFlatSet BinanceGlobalInfos::ExtractTradableCurrencies( + const schema::binance::NetworkCoinDataVector& networkCoinDataVector, const CurrencyCodeSet& excludedCurrencies) { CurrencyExchangeVector currencies; - for (const json::container& coinJson : allCoins) { - CurrencyCode cur(coinJson["coin"].get()); + for (const auto& coinJson : networkCoinDataVector) { + CurrencyCode cur{coinJson.coin}; if (excludedCurrencies.contains(cur)) { log::trace("Discard {} excluded by config", cur.str()); continue; } - const bool isFiat = coinJson["isLegalMoney"]; - const auto& networkList = coinJson["networkList"]; - if (networkList.size() > 1) { + const auto& networkList = coinJson.networkList; + if (coinJson.networkList.size() > 1) { log::debug("Several networks found for {}, considering only default network", cur.str()); } - const auto it = std::find_if(networkList.begin(), networkList.end(), - [](const json::container& el) { return el["isDefault"].get(); }); + const auto it = std::ranges::find_if(networkList, [](const auto& el) { return el.isDefault; }); if (it != networkList.end()) { - auto deposit = (*it)["depositEnable"].get() ? CurrencyExchange::Deposit::kAvailable - : CurrencyExchange::Deposit::kUnavailable; - auto withdraw = (*it)["withdrawEnable"].get() ? CurrencyExchange::Withdraw::kAvailable - : CurrencyExchange::Withdraw::kUnavailable; + auto deposit = + it->depositEnable ? CurrencyExchange::Deposit::kAvailable : CurrencyExchange::Deposit::kUnavailable; + auto withdraw = + it->withdrawEnable ? CurrencyExchange::Withdraw::kAvailable : CurrencyExchange::Withdraw::kUnavailable; currencies.emplace_back(cur, cur, cur, deposit, withdraw, - isFiat ? CurrencyExchange::Type::kFiat : CurrencyExchange::Type::kCrypto); + coinJson.isLegalMoney ? CurrencyExchange::Type::kFiat : CurrencyExchange::Type::kCrypto); } } CurrencyExchangeFlatSet ret(std::move(currencies)); diff --git a/src/api/exchanges/src/binance-schema.hpp b/src/api/exchanges/src/binance-schema.hpp new file mode 100644 index 00000000..2451b3bd --- /dev/null +++ b/src/api/exchanges/src/binance-schema.hpp @@ -0,0 +1,175 @@ +#pragma once + +#include +#include +#include + +#include "cct_smallvector.hpp" +#include "cct_string.hpp" +#include "cct_vector.hpp" +#include "monetaryamount.hpp" + +namespace cct::schema::binance { + +using OrderId = uint64_t; + +// https://binance-docs.github.io/apidocs/spot/en/#account-status-user_data +struct V1AccountStatus { + string data; + + std::optional code; + std::optional msg; +}; + +// https://binance-docs.github.io/apidocs/spot/en/#account-information-user_data +struct V3AccountBalance { + struct Asset { + string asset; + MonetaryAmount free; // without unit + MonetaryAmount locked; // without unit + + using trivially_relocatable = is_trivially_relocatable::type; + + auto operator<=>(const Asset&) const = default; + }; + + vector balances; + + std::optional code; + std::optional msg; +}; + +// https://binance-docs.github.io/apidocs/spot/en/#fetch-deposit-address-list-with-network-user_data +struct V1CapitalDepositAddressListElement { + string address; + string tag; + + std::optional code; + std::optional msg; + + using trivially_relocatable = is_trivially_relocatable::type; + + auto operator<=>(const V1CapitalDepositAddressListElement&) const = default; +}; + +// https://binance-docs.github.io/apidocs/spot/en/#all-orders-user_data +// https://binance-docs.github.io/apidocs/spot/en/#cancel-all-open-orders-on-a-symbol-trade +struct V3GetAllOrder { + string symbol; + int64_t time{}; + OrderId orderId{}; + MonetaryAmount executedQty; + MonetaryAmount price; + string side; + MonetaryAmount origQty; + int64_t updateTime{}; + + using trivially_relocatable = is_trivially_relocatable::type; + + auto operator<=>(const V3GetAllOrder&) const = default; +}; + +using V3GetAllOrders = vector; + +// https://binance-docs.github.io/apidocs/spot/en/#cancel-all-open-orders-on-a-symbol-trade +struct V3CancelOrder { + OrderId orderId{}; +}; + +using V3CancelAllOrders = vector; + +// https://binance-docs.github.io/apidocs/spot/en/#deposit-history-supporting-network-user_data +struct V1CapitalDeposit { + int64_t status{-1}; + string coin; + string id; + string address; + double amount{}; + int64_t insertTime{}; + + using trivially_relocatable = is_trivially_relocatable::type; +}; + +using V1CapitalDepositHisRec = vector; + +// https://binance-docs.github.io/apidocs/spot/en/#withdraw-history-supporting-network-user_data +struct V1CapitalWithdraw { + int64_t status{-1}; + string coin; + string id; + double amount{}; + double transactionFee{}; + int64_t applyTime{}; + int64_t completeTime{}; + + using trivially_relocatable = is_trivially_relocatable::type; +}; + +using V1CapitalWithdrawHistory = vector; + +// https://binance-docs.github.io/apidocs/spot/en/#asset-detail-user_data +struct V1AssetDetail { + MonetaryAmount withdrawFee; + bool withdrawStatus{}; +}; + +using V1AssetDetailMap = std::unordered_map; + +// https://binance-docs.github.io/apidocs/spot/en/#dust-transfer-user_data + +struct V1AssetDustResult { + OrderId tranId{}; + MonetaryAmount transferedAmount; + + using trivially_relocatable = is_trivially_relocatable::type; + + auto operator<=>(const V1AssetDustResult&) const = default; +}; + +struct V1AssetDust { + SmallVector transferResult; + + std::optional code; + std::optional msg; +}; + +// https://binance-docs.github.io/apidocs/spot/en/#new-order-trade + +struct V3NewOrderFills { + MonetaryAmount price; + MonetaryAmount qty; + MonetaryAmount commission; + CurrencyCode commissionAsset; + OrderId orderId{}; + + auto operator<=>(const V3NewOrderFills&) const = default; +}; + +struct V3NewOrder { + string status; + OrderId orderId{}; + SmallVector fills; + + using trivially_relocatable = is_trivially_relocatable::type; +}; + +// https://binance-docs.github.io/apidocs/spot/en/#query-order-user_data + +struct V3GetOrder { + string status; + int64_t time{}; + + using trivially_relocatable = is_trivially_relocatable::type; +}; + +// https://binance-docs.github.io/apidocs/spot/en/#account-trade-list-user_data + +using V3MyTrades = vector; + +// https://binance-docs.github.io/apidocs/spot/en/#withdraw-user_data + +struct V1CapitalWithdrawApply { + string id; +}; + +} // namespace cct::schema::binance \ No newline at end of file diff --git a/src/api/exchanges/src/binanceprivateapi.cpp b/src/api/exchanges/src/binanceprivateapi.cpp index 05cc3bda..12c4a75c 100644 --- a/src/api/exchanges/src/binanceprivateapi.cpp +++ b/src/api/exchanges/src/binanceprivateapi.cpp @@ -17,10 +17,11 @@ #include "balanceoptions.hpp" #include "balanceportfolio.hpp" #include "binance-common-api.hpp" +#include "binance-common-schema.hpp" +#include "binance-schema.hpp" #include "binancepublicapi.hpp" #include "cachedresult.hpp" #include "cct_exception.hpp" -#include "cct_json-container.hpp" #include "cct_log.hpp" #include "cct_smallvector.hpp" #include "cct_string.hpp" @@ -60,6 +61,7 @@ #include "withdraw.hpp" #include "withdrawinfo.hpp" #include "withdrawsconstraints.hpp" +#include "write-json.hpp" namespace cct::api { @@ -112,45 +114,58 @@ void SetNonceAndSignature(const APIKey& apiKey, CurlPostData& postData, Duration postData.emplace_back(kSignatureKey, std::string_view(sha256Hex)); } -bool CheckErrorDoRetry(int statusCode, const json::container& ret, QueryDelayDir& queryDelayDir, Duration& sleepingTime, - Duration& queryDelay) { +bool CheckErrorMsg(std::string_view msg, QueryDelayDir& queryDelayDir, Duration& sleepingTime, Duration& queryDelay) { static constexpr Duration kInitialDurationQueryDelay = milliseconds(200); + + // 'Timestamp for this request was 1000ms ahead of the server's time.' may be the error message. + // I guess this could happen when client time is not synchronized with binance time. + // Let's try to induce a delay in this case. + auto aheadPos = msg.find("ahead of the server's time"); + if (aheadPos != std::string_view::npos) { + if (queryDelayDir != QueryDelayDir::kAhead) { + queryDelayDir = QueryDelayDir::kAhead; + + sleepingTime = kInitialDurationQueryDelay; + } + queryDelay -= sleepingTime; + log::warn("Our local time is ahead of Binance server's time. Query delay modified to {}", + DurationToString(queryDelay)); + // Ensure Nonce is increasing while modifying the query delay + std::this_thread::sleep_for(sleepingTime); + return true; + } + + // If we are behind Binance clock, it returns below error message. + auto behindPos = msg.find("Timestamp for this request is outside of the recvWindow."); + if (behindPos != std::string_view::npos) { + if (queryDelayDir != QueryDelayDir::kBehind) { + queryDelayDir = QueryDelayDir::kBehind; + + sleepingTime = kInitialDurationQueryDelay; + } + queryDelay += sleepingTime; + log::warn("Our local time is behind of Binance server's time. Query delay modified to {}", + DurationToString(queryDelay)); + return true; + } + + return false; +} + +template +using has_msg_t = decltype(std::declval().msg); + +template +using has_code_t = decltype(std::declval().code); + +template +bool CheckErrorDoRetry(int statusCode, const T& ret, QueryDelayDir& queryDelayDir, Duration& sleepingTime, + Duration& queryDelay) { switch (statusCode) { case kInvalidTimestamp: { - auto msgIt = ret.find("msg"); - if (msgIt != ret.end()) { - std::string_view msg = msgIt->get(); - - // 'Timestamp for this request was 1000ms ahead of the server's time.' may be the error message. - // I guess this could happen when client time is not synchronized with binance time. - // Let's try to induce a delay in this case. - auto aheadPos = msg.find("ahead of the server's time"); - if (aheadPos != std::string_view::npos) { - if (queryDelayDir != QueryDelayDir::kAhead) { - queryDelayDir = QueryDelayDir::kAhead; - - sleepingTime = kInitialDurationQueryDelay; - } - queryDelay -= sleepingTime; - log::warn("Our local time is ahead of Binance server's time. Query delay modified to {}", - DurationToString(queryDelay)); - // Ensure Nonce is increasing while modifying the query delay - std::this_thread::sleep_for(sleepingTime); - return true; - } - - // If we are behind Binance clock, it returns below error message. - auto behindPos = msg.find("Timestamp for this request is outside of the recvWindow."); - if (behindPos != std::string_view::npos) { - if (queryDelayDir != QueryDelayDir::kBehind) { - queryDelayDir = QueryDelayDir::kBehind; - - sleepingTime = kInitialDurationQueryDelay; - } - queryDelay += sleepingTime; - log::warn("Our local time is behind of Binance server's time. Query delay modified to {}", - DurationToString(queryDelay)); - return true; + if constexpr (amc::is_detected::value) { + if (ret.msg) { + return CheckErrorMsg(*ret.msg, queryDelayDir, sleepingTime, queryDelay); } } break; @@ -172,17 +187,18 @@ bool CheckErrorDoRetry(int statusCode, const json::container& ret, QueryDelayDir return false; } -template -json::container PrivateQuery(CurlHandle& curlHandle, const APIKey& apiKey, HttpRequestType requestType, - std::string_view endpoint, Duration& queryDelay, - CurlPostDataT&& curlPostData = CurlPostData(), bool throwIfError = true) { +template +T PrivateQuery(CurlHandle& curlHandle, const APIKey& apiKey, HttpRequestType requestType, std::string_view endpoint, + Duration& queryDelay, CurlPostDataT&& curlPostData = CurlPostData(), bool throwIfError = true) { CurlOptions opts(requestType, std::forward(curlPostData)); opts.mutableHttpHeaders().emplace_back("X-MBX-APIKEY", apiKey.key()); Duration sleepingTime = curlHandle.minDurationBetweenQueries(); int statusCode{}; QueryDelayDir queryDelayDir = QueryDelayDir::kNoDir; - json::container ret; + T ret; for (int retryPos = 0; retryPos < kNbOrderRequestsRetries; ++retryPos) { if (retryPos != 0) { log::trace("Wait {}...", DurationToString(sleepingTime)); @@ -192,22 +208,28 @@ json::container PrivateQuery(CurlHandle& curlHandle, const APIKey& apiKey, HttpR SetNonceAndSignature(apiKey, opts.mutablePostData(), queryDelay); - static constexpr bool kAllowExceptions = false; + auto resStr = curlHandle.query(endpoint, opts); - ret = json::container::parse(curlHandle.query(endpoint, opts), nullptr, kAllowExceptions); - if (ret.is_discarded()) { - log::error("Badly formatted response from Binance, retry"); + auto ec = json::read(ret, resStr); + if (ec) { + std::string_view prefixJsonContent = resStr.substr(0, std::min(resStr.size(), 20)); + log::error("Error while reading json content '{}{}': {}", prefixJsonContent, + prefixJsonContent.size() < resStr.size() ? "..." : "", json::format_error(ec, resStr)); + statusCode = -1; continue; } - auto codeIt = ret.find("code"); - if (codeIt == ret.end() || !ret.contains("msg")) { + if constexpr (amc::is_detected::value) { + if (!ret.code || *ret.code == 0) { + return ret; + } + // error in query + statusCode = *ret.code; // 1100 for instance + } else { + // if no code, assume OK return ret; } - // error in query - statusCode = *codeIt; // "1100" for instance - if (CheckErrorDoRetry(statusCode, ret, queryDelayDir, sleepingTime, queryDelay)) { continue; } @@ -215,8 +237,16 @@ json::container PrivateQuery(CurlHandle& curlHandle, const APIKey& apiKey, HttpR break; } if (throwIfError) { - log::error("Full Binance error for {}: '{}'", apiKey.name(), ret.dump()); - throw exception("Error: {}, msg: {}", MonetaryAmount(statusCode), ret["msg"].get()); + std::string_view errorMsg; + string jsonStr = WriteJsonOrThrow(ret); + if constexpr (amc::is_detected::value) { + if (ret.msg) { + errorMsg = *ret.msg; + } + } + + log::error("Full Binance error for {}: '{}'", apiKey.name(), jsonStr); + throw exception("Error: {}, msg: {}", MonetaryAmount(statusCode), errorMsg); } return ret; } @@ -245,39 +275,40 @@ BinancePrivate::BinancePrivate(const CoincenterInfo& coincenterInfo, BinancePubl _curlHandle, _apiKey, binancePublic, _queryDelay) {} CurrencyExchangeFlatSet BinancePrivate::TradableCurrenciesCache::operator()() { - json::container allCoins = - PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/sapi/v1/capital/config/getall", _queryDelay); + auto allCoins = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, + "/sapi/v1/capital/config/getall", _queryDelay); return BinanceGlobalInfos::ExtractTradableCurrencies(allCoins, _exchangePublic.exchangeConfig().asset.allExclude); } bool BinancePrivate::validateApiKey() { static constexpr bool throwIfError = false; - json::container result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/sapi/v1/account/status", - _queryDelay, CurlPostData(), throwIfError); - return result.find("code") == result.end(); + auto result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, + "/sapi/v1/account/status", _queryDelay, CurlPostData(), + throwIfError); + static constexpr std::string_view kNormalStatus = "Normal"; + return result.data == kNormalStatus; } BalancePortfolio BinancePrivate::queryAccountBalance(const BalanceOptions& balanceOptions) { - const json::container result = - PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/api/v3/account", _queryDelay); + const auto v3AccountBalance = + PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/api/v3/account", + _queryDelay, CurlPostData{{"omitZeroBalances", "true"}}); const bool withBalanceInUse = balanceOptions.amountIncludePolicy() == BalanceOptions::AmountIncludePolicy::kWithBalanceInUse; BalancePortfolio balancePortfolio; + balancePortfolio.reserve(static_cast(v3AccountBalance.balances.size())); - auto dataIt = result.find("balances"); - if (dataIt == result.end()) { - log::error("Unexpected get account balance reply from {}", exchangeName()); - return balancePortfolio; - } - - balancePortfolio.reserve(static_cast(dataIt->size())); - for (const json::container& balance : *dataIt) { - CurrencyCode currencyCode(balance["asset"].get()); - MonetaryAmount amount(balance["free"].get(), currencyCode); + for (const auto& balance : v3AccountBalance.balances) { + if (balance.asset.size() > CurrencyCode::kMaxLen) { + log::warn("Skipping {} asset '{}' because it's too long", _exchangePublic.name(), balance.asset); + continue; + } + CurrencyCode currencyCode(balance.asset); + MonetaryAmount amount(balance.free, currencyCode); if (withBalanceInUse) { - MonetaryAmount usedAmount(balance["locked"].get(), currencyCode); + MonetaryAmount usedAmount(balance.locked, currencyCode); amount += usedAmount; } @@ -288,16 +319,17 @@ BalancePortfolio BinancePrivate::queryAccountBalance(const BalanceOptions& balan Wallet BinancePrivate::DepositWalletFunc::operator()(CurrencyCode currencyCode) { // Limitation : we do not provide network here, we use default in accordance of getTradableCurrenciesService - json::container result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/sapi/v1/capital/deposit/address", - _queryDelay, {{"coin", currencyCode.str()}}); - std::string_view tag(result["tag"].get()); + const auto result = PrivateQuery( + _curlHandle, _apiKey, HttpRequestType::kGet, "/sapi/v1/capital/deposit/address", _queryDelay, + {{"coin", currencyCode.str()}}); const CoincenterInfo& coincenterInfo = _exchangePublic.coincenterInfo(); - bool doCheckWallet = + const bool doCheckWallet = coincenterInfo.exchangeConfig(_exchangePublic.exchangeNameEnum()).withdraw.validateDepositAddressesInFile; + WalletCheck walletCheck(coincenterInfo.dataDir(), doCheckWallet); Wallet wallet(ExchangeName(_exchangePublic.exchangeNameEnum(), _apiKey.name()), currencyCode, - std::move(result["address"].get_ref()), tag, walletCheck, _apiKey.accountOwner()); - log::info("Retrieved {} (URL: '{}')", wallet, result["url"].get()); + std::move(result.address), std::move(result.tag), walletCheck, _apiKey.accountOwner()); + log::info("Retrieved {}", wallet); return wallet; } @@ -312,14 +344,14 @@ bool BinancePrivate::checkMarketAppendSymbol(Market mk, CurlPostData& params) { namespace { template -void FillOrders(const OrdersConstraints& ordersConstraints, const json::container& ordersArray, +void FillOrders(const OrdersConstraints& ordersConstraints, std::span ordersArray, ExchangePublic& exchangePublic, OrderVectorType& orderVector) { const auto cur1Str = ordersConstraints.curStr1(); const auto cur2Str = ordersConstraints.curStr2(); MarketSet markets; - for (const json::container& orderDetails : ordersArray) { - std::string_view marketStr = orderDetails["symbol"].get(); // already higher case + for (const auto& orderDetails : ordersArray) { + std::string_view marketStr = orderDetails.symbol; // already higher case std::size_t cur1Pos = marketStr.find(cur1Str); if (ordersConstraints.isCurDefined() && cur1Pos == std::string_view::npos) { continue; @@ -327,7 +359,7 @@ void FillOrders(const OrdersConstraints& ordersConstraints, const json::containe if (ordersConstraints.isCur2Defined() && marketStr.find(cur2Str) == std::string_view::npos) { continue; } - const auto placedTimeMsSinceEpoch = orderDetails["time"].get(); + const auto placedTimeMsSinceEpoch = orderDetails.time; const TimePoint placedTime{milliseconds(placedTimeMsSinceEpoch)}; if (!ordersConstraints.validatePlacedTime(placedTime)) { @@ -342,25 +374,25 @@ void FillOrders(const OrdersConstraints& ordersConstraints, const json::containe const CurrencyCode volumeCur = optMarket->base(); const CurrencyCode priceCur = optMarket->quote(); - const int64_t orderId = orderDetails["orderId"].get(); + const int64_t orderId = orderDetails.orderId; string id = IntegralToString(orderId); if (!ordersConstraints.validateId(id)) { continue; } - const MonetaryAmount matchedVolume(orderDetails["executedQty"].get(), volumeCur); - const MonetaryAmount price(orderDetails["price"].get(), priceCur); - const TradeSide side = orderDetails["side"].get() == "BUY" ? TradeSide::kBuy : TradeSide::kSell; + const MonetaryAmount matchedVolume(orderDetails.executedQty, volumeCur); + const MonetaryAmount price(orderDetails.price, priceCur); + const TradeSide side = orderDetails.side == "BUY" ? TradeSide::kBuy : TradeSide::kSell; using OrderType = std::remove_cvref_t().begin())>; if constexpr (std::is_same_v) { - const MonetaryAmount originalVolume(orderDetails["origQty"].get(), volumeCur); + const MonetaryAmount originalVolume(orderDetails.origQty, volumeCur); const MonetaryAmount remainingVolume = originalVolume - matchedVolume; orderVector.emplace_back(std::move(id), matchedVolume, remainingVolume, price, placedTime, side); } else if constexpr (std::is_same_v) { - const auto matchedTimeMsSinceEpoch = orderDetails["updateTime"].get(); + const auto matchedTimeMsSinceEpoch = orderDetails.updateTime; const TimePoint matchedTime{milliseconds(matchedTimeMsSinceEpoch)}; orderVector.emplace_back(std::move(id), matchedVolume, price, placedTime, matchedTime, side); @@ -386,8 +418,8 @@ ClosedOrderVector BinancePrivate::queryClosedOrders(const OrdersConstraints& clo if (closedOrdersConstraints.isPlacedTimeBeforeDefined()) { params.emplace_back("endTime", TimestampToMillisecondsSinceEpoch(closedOrdersConstraints.placedBefore())); } - const json::container result = - PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/api/v3/allOrders", _queryDelay, std::move(params)); + const auto result = PrivateQuery( + _curlHandle, _apiKey, HttpRequestType::kGet, "/api/v3/allOrders", _queryDelay, std::move(params)); FillOrders(closedOrdersConstraints, result, _exchangePublic, closedOrders); log::info("Retrieved {} closed orders from {}", closedOrders.size(), _exchangePublic.name()); @@ -408,8 +440,8 @@ OpenedOrderVector BinancePrivate::queryOpenedOrders(const OrdersConstraints& ope return openedOrders; } } - const json::container result = - PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/api/v3/openOrders", _queryDelay, std::move(params)); + const auto result = PrivateQuery( + _curlHandle, _apiKey, HttpRequestType::kGet, "/api/v3/openOrders", _queryDelay, std::move(params)); FillOrders(openedOrdersConstraints, result, _exchangePublic, openedOrders); @@ -426,8 +458,8 @@ int BinancePrivate::cancelOpenedOrders(const OrdersConstraints& openedOrdersCons return 0; } if (canUseCancelAllEndpoint) { - json::container cancelledOrders = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kDelete, - "/api/v3/openOrders", _queryDelay, std::move(params)); + const auto cancelledOrders = PrivateQuery( + _curlHandle, _apiKey, HttpRequestType::kDelete, "/api/v3/openOrders", _queryDelay, std::move(params)); return static_cast(cancelledOrders.size()); } } @@ -448,15 +480,19 @@ int BinancePrivate::cancelOpenedOrders(const OrdersConstraints& openedOrdersCons } if (orders.size() > 1 && canUseCancelAllEndpoint) { params.erase("orderId"); - json::container cancelledOrders = - PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kDelete, "/api/v3/openOrders", _queryDelay, params); + const auto cancelledOrders = PrivateQuery( + _curlHandle, _apiKey, HttpRequestType::kDelete, "/api/v3/openOrders", _queryDelay, params); nbOrdersCancelled += static_cast(cancelledOrders.size()); } else { for (const OpenedOrder& order : orders) { params.set("orderId", order.id()); - PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kDelete, "/api/v3/order", _queryDelay, params); + auto cancelledOrder = PrivateQuery( + _curlHandle, _apiKey, HttpRequestType::kDelete, "/api/v3/order", _queryDelay, params); + + if (cancelledOrder.orderId != 0) { + ++nbOrdersCancelled; + } } - nbOrdersCancelled += orders.size(); } } return nbOrdersCancelled; @@ -497,21 +533,27 @@ DepositsSet BinancePrivate::queryRecentDeposits(const DepositsConstraints& depos options.emplace_back("txId", depositsConstraints.idSet().front()); } } - json::container depositStatus = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, - "/sapi/v1/capital/deposit/hisrec", _queryDelay, std::move(options)); + + auto depositStatus = PrivateQuery( + _curlHandle, _apiKey, HttpRequestType::kGet, "/sapi/v1/capital/deposit/hisrec", _queryDelay, std::move(options)); + Deposits deposits; deposits.reserve(static_cast(depositStatus.size())); - for (json::container& depositDetail : depositStatus) { - int statusInt = depositDetail["status"].get(); - Deposit::Status status = DepositStatusFromCode(statusInt); - - CurrencyCode currencyCode(depositDetail["coin"].get()); - string& id = depositDetail["id"].get_ref(); - MonetaryAmount amountReceived(depositDetail["amount"].get(), currencyCode); - int64_t millisecondsSinceEpoch = depositDetail["insertTime"].get(); + + for (auto& depositDetail : depositStatus) { + if (depositDetail.coin.size() > CurrencyCode::kMaxLen) { + log::warn("Skipping {} deposit '{}' because it's too long", exchangeName(), depositDetail.coin); + continue; + } + + Deposit::Status status = DepositStatusFromCode(depositDetail.status); + + CurrencyCode currencyCode(depositDetail.coin); + MonetaryAmount amountReceived(depositDetail.amount, currencyCode); + int64_t millisecondsSinceEpoch = depositDetail.insertTime; TimePoint timestamp{milliseconds(millisecondsSinceEpoch)}; - deposits.emplace_back(std::move(id), timestamp, amountReceived, status); + deposits.emplace_back(std::move(depositDetail.id), timestamp, amountReceived, status); } DepositsSet depositsSet(std::move(deposits)); log::info("Retrieved {} recent deposits for {}", depositsSet.size(), exchangeName()); @@ -561,13 +603,12 @@ Withdraw::Status WithdrawStatusFromStatusStr(int statusInt, bool logStatus) { } } -TimePoint RetrieveTimeStampFromWithdrawJson(const json::container& withdrawJson) { +TimePoint RetrieveTimeStampFromWithdrawJson(const auto& withdrawJson) { int64_t millisecondsSinceEpoch; - auto completeTimeIt = withdrawJson.find("completeTime"); - if (completeTimeIt != withdrawJson.end()) { - millisecondsSinceEpoch = completeTimeIt->get(); + if (withdrawJson.completeTime != 0) { + millisecondsSinceEpoch = withdrawJson.completeTime; } else { - millisecondsSinceEpoch = withdrawJson["applyTime"].get(); + millisecondsSinceEpoch = withdrawJson.applyTime; } return TimePoint{milliseconds(millisecondsSinceEpoch)}; } @@ -593,20 +634,24 @@ WithdrawsSet BinancePrivate::queryRecentWithdraws(const WithdrawsConstraints& wi // Binance provides field 'withdrawOrderId' tu customize user id, but it's not well documented // so we use Binance generated 'id' instead. // What is important is that the same field is considered in both queries 'launchWithdraw' and 'queryRecentWithdraws' - json::container data = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/sapi/v1/capital/withdraw/history", - _queryDelay, CreateOptionsFromWithdrawConstraints(withdrawsConstraints)); - for (json::container& withdrawJson : data) { - int statusInt = withdrawJson["status"].get(); - Withdraw::Status status = WithdrawStatusFromStatusStr(statusInt, withdrawsConstraints.isCurDefined()); - CurrencyCode currencyCode(withdrawJson["coin"].get()); - string& id = withdrawJson["id"].get_ref(); - if (!withdrawsConstraints.validateId(id)) { + auto data = PrivateQuery( + _curlHandle, _apiKey, HttpRequestType::kGet, "/sapi/v1/capital/withdraw/history", _queryDelay, + CreateOptionsFromWithdrawConstraints(withdrawsConstraints)); + for (auto& withdrawJson : data) { + if (withdrawJson.coin.size() > CurrencyCode::kMaxLen) { + log::warn("Skipping {} deposit '{}' because it's too long", exchangeName(), withdrawJson.coin); continue; } - MonetaryAmount netEmittedAmount(withdrawJson["amount"].get(), currencyCode); - MonetaryAmount withdrawFee(withdrawJson["transactionFee"].get(), currencyCode); + + Withdraw::Status status = WithdrawStatusFromStatusStr(withdrawJson.status, withdrawsConstraints.isCurDefined()); + CurrencyCode currencyCode(withdrawJson.coin); + if (!withdrawsConstraints.validateId(withdrawJson.id)) { + continue; + } + MonetaryAmount netEmittedAmount(withdrawJson.amount, currencyCode); + MonetaryAmount withdrawFee(withdrawJson.transactionFee, currencyCode); TimePoint timestamp = RetrieveTimeStampFromWithdrawJson(withdrawJson); - withdraws.emplace_back(std::move(id), timestamp, netEmittedAmount, status, withdrawFee); + withdraws.emplace_back(std::move(withdrawJson.id), timestamp, netEmittedAmount, status, withdrawFee); } WithdrawsSet withdrawsSet(std::move(withdraws)); log::info("Retrieved {} recent withdraws for {}", withdrawsSet.size(), exchangeName()); @@ -614,40 +659,46 @@ WithdrawsSet BinancePrivate::queryRecentWithdraws(const WithdrawsConstraints& wi } MonetaryAmountByCurrencySet BinancePrivate::AllWithdrawFeesFunc::operator()() { - json::container result = - PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/sapi/v1/asset/assetDetail", _queryDelay); + auto result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, + "/sapi/v1/asset/assetDetail", _queryDelay); MonetaryAmountVector fees; - for (const auto& [curCodeStr, withdrawFeeDetails] : result.items()) { - if (withdrawFeeDetails["withdrawStatus"].get()) { + for (const auto& [curCodeStr, withdrawFeeDetails] : result) { + if (withdrawFeeDetails.withdrawStatus) { + if (curCodeStr.size() > CurrencyCode::kMaxLen) { + log::warn("Skipping {} deposit '{}' because it's too long", _exchangePublic.name(), curCodeStr); + continue; + } + CurrencyCode cur(curCodeStr); - fees.emplace_back(withdrawFeeDetails["withdrawFee"].get(), cur); + fees.emplace_back(withdrawFeeDetails.withdrawFee, cur); } } return MonetaryAmountByCurrencySet(std::move(fees)); } std::optional BinancePrivate::WithdrawFeesFunc::operator()(CurrencyCode currencyCode) { - json::container result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/sapi/v1/asset/assetDetail", - _queryDelay, {{"asset", currencyCode.str()}}); - if (!result.contains(currencyCode.str())) { + auto result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, + "/sapi/v1/asset/assetDetail", _queryDelay, + {{"asset", currencyCode.str()}}); + const auto it = result.find(currencyCode.str()); + if (it == result.end()) { return {}; } - const json::container& withdrawFeeDetails = result[currencyCode.str()]; - if (!withdrawFeeDetails["withdrawStatus"].get()) { + const auto& withdrawFeeDetails = it->second; + if (!withdrawFeeDetails.withdrawStatus) { log::error("{} is currently unavailable for withdraw from {}", currencyCode, _exchangePublic.name()); } - return MonetaryAmount(withdrawFeeDetails["withdrawFee"].get(), currencyCode); + return MonetaryAmount(withdrawFeeDetails.withdrawFee, currencyCode); } namespace { -TradedAmounts ParseTrades(Market mk, CurrencyCode fromCurrencyCode, const json::container& fillDetail) { - MonetaryAmount price(fillDetail["price"].get(), mk.quote()); - MonetaryAmount quantity(fillDetail["qty"].get(), mk.base()); +TradedAmounts ParseTrades(Market mk, CurrencyCode fromCurrencyCode, const auto& fillDetail) { + MonetaryAmount price(fillDetail.price, mk.quote()); + MonetaryAmount quantity(fillDetail.qty, mk.base()); MonetaryAmount quantityTimesPrice = quantity.toNeutral() * price; TradedAmounts detailTradedInfo(fromCurrencyCode == mk.quote() ? quantityTimesPrice : quantity, fromCurrencyCode == mk.quote() ? quantity : quantityTimesPrice); - MonetaryAmount fee(fillDetail["commission"].get(), - fillDetail["commissionAsset"].get()); + MonetaryAmount fee(fillDetail.commission, fillDetail.commissionAsset); log::debug("Gross {} has been matched at {} price, with a fee of {}", quantity, price, fee); if (fee.currencyCode() == detailTradedInfo.from.currencyCode()) { detailTradedInfo.from += fee; @@ -659,14 +710,12 @@ TradedAmounts ParseTrades(Market mk, CurrencyCode fromCurrencyCode, const json:: return detailTradedInfo; } -TradedAmounts QueryOrdersAfterPlace(Market mk, CurrencyCode fromCurrencyCode, const json::container& orderJson) { +TradedAmounts QueryOrdersAfterPlace(Market mk, CurrencyCode fromCurrencyCode, const auto& orderJson) { CurrencyCode toCurrencyCode(fromCurrencyCode == mk.quote() ? mk.base() : mk.quote()); TradedAmounts ret(fromCurrencyCode, toCurrencyCode); - if (orderJson.contains("fills")) { - for (const json::container& fillDetail : orderJson["fills"]) { - ret += ParseTrades(mk, fromCurrencyCode, fillDetail); - } + for (const auto& fillDetail : orderJson.fills) { + ret += ParseTrades(mk, fromCurrencyCode, fillDetail); } return ret; @@ -696,16 +745,19 @@ PlaceOrderInfo BinancePrivate::placeOrder(MonetaryAmount from, MonetaryAmount vo if (!isSimulation && toCurrencyCode == kBinanceCoinCur) { // Use special Binance Dust transfer log::info("Volume too low for standard trade, but we can use Dust transfer to trade to {}", kBinanceCoinCur); - json::container result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kPost, "/sapi/v1/asset/dust", - _queryDelay, {{"asset", from.currencyStr()}}); - auto transferResultIt = result.find("transferResult"); - if (transferResultIt == result.end() || transferResultIt->empty()) { - throw exception("Unexpected dust transfer result"); + auto result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kPost, + "/sapi/v1/asset/dust", _queryDelay, + {{"asset", from.currencyStr()}}); + if (result.transferResult.empty()) { + log::error("Unable to find any transfer result for dust transfer"); + placeOrderInfo.setClosed(); + return placeOrderInfo; } - const json::container& res = transferResultIt->front(); - placeOrderInfo.orderId = IntegralToString(res["tranId"].get()); + + const auto& transferResult = result.transferResult.front(); + placeOrderInfo.orderId = IntegralToString(transferResult.tranId); // 'transfered' is misspelled (against 'transferred') but the field is really named like this in Binance REST API - MonetaryAmount netTransferredAmount(res["transferedAmount"].get(), kBinanceCoinCur); + MonetaryAmount netTransferredAmount(transferResult.transferedAmount, kBinanceCoinCur); placeOrderInfo.tradedAmounts() += TradedAmounts(from, netTransferredAmount); } else { log::warn("No trade of {} into {} because min vol order is {} for this market", volume, toCurrencyCode, @@ -727,14 +779,14 @@ PlaceOrderInfo BinancePrivate::placeOrder(MonetaryAmount from, MonetaryAmount vo const std::string_view methodName = isSimulation ? "/api/v3/order/test" : "/api/v3/order"; - json::container result = - PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kPost, methodName, _queryDelay, placePostData); + auto result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kPost, methodName, + _queryDelay, placePostData); if (isSimulation) { placeOrderInfo.setClosed(); return placeOrderInfo; } - placeOrderInfo.orderId = IntegralToString(result["orderId"].get()); - std::string_view status = result["status"].get(); + placeOrderInfo.orderId = IntegralToString(result.orderId); + std::string_view status = result.status; if (status == "FILLED" || status == "REJECTED" || status == "EXPIRED") { if (status == "FILLED") { placeOrderInfo.tradedAmounts() += QueryOrdersAfterPlace(mk, fromCurrencyCode, result); @@ -754,33 +806,37 @@ OrderInfo BinancePrivate::queryOrder(OrderIdView orderId, const TradeContext& tr const CurrencyCode toCurrencyCode = tradeContext.side == TradeSide::kBuy ? mk.base() : mk.quote(); const string assetsStr = mk.assetsPairStrUpper(); const std::string_view assets(assetsStr); - json::container result = PrivateQuery(_curlHandle, _apiKey, requestType, "/api/v3/order", _queryDelay, - {{"symbol", assets}, {"orderId", orderId}}); - const std::string_view status = result["status"].get(); + const auto result = PrivateQuery( + _curlHandle, _apiKey, requestType, "/api/v3/order", _queryDelay, {{"symbol", assets}, {"orderId", orderId}}); + bool isClosed = false; bool queryClosedOrder = false; - if (status == "FILLED" || status == "CANCELED") { + if (result.status == "FILLED" || result.status == "CANCELED") { isClosed = true; queryClosedOrder = true; - } else if (status == "REJECTED" || status == "EXPIRED") { - log::error("{} rejected our order {} with status {}", _exchangePublic.name(), orderId, status); + } else if (result.status == "REJECTED" || result.status == "EXPIRED") { + log::error("{} rejected our order {} with status {}", _exchangePublic.name(), orderId, result.status); isClosed = true; } + OrderInfo orderInfo{TradedAmounts(fromCurrencyCode, toCurrencyCode), isClosed}; + if (queryClosedOrder) { CurlPostData myTradesOpts{{"symbol", assets}}; - auto timeIt = result.find("time"); - if (timeIt != result.end()) { - myTradesOpts.emplace_back("startTime", timeIt->get() - 100L); // -100 just to be sure - } - result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/api/v3/myTrades", _queryDelay, myTradesOpts); - int64_t integralOrderId = StringToIntegral(orderId); - for (const json::container& tradeDetails : result) { - if (tradeDetails["orderId"].get() == integralOrderId) { + if (result.time != 0) { + myTradesOpts.emplace_back("startTime", result.time - 100L); // -100 just to be sure + } + const auto myTradesResult = PrivateQuery( + _curlHandle, _apiKey, HttpRequestType::kGet, "/api/v3/myTrades", _queryDelay, myTradesOpts); + const auto integralOrderId = + StringToIntegral()[0].orderId)>(orderId); + for (const auto& tradeDetails : myTradesResult) { + if (tradeDetails.orderId == integralOrderId) { orderInfo.tradedAmounts += ParseTrades(mk, fromCurrencyCode, tradeDetails); } } } + return orderInfo; } @@ -791,28 +847,30 @@ InitiatedWithdrawInfo BinancePrivate::launchWithdraw(MonetaryAmount grossAmount, if (destinationWallet.hasTag()) { withdrawPostData.emplace_back("addressTag", destinationWallet.tag()); } - json::container result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kPost, "/sapi/v1/capital/withdraw/apply", - _queryDelay, std::move(withdrawPostData)); - return {std::move(destinationWallet), std::move(result["id"].get_ref()), grossAmount}; + auto result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kPost, + "/sapi/v1/capital/withdraw/apply", _queryDelay, + std::move(withdrawPostData)); + return {std::move(destinationWallet), std::move(result.id), grossAmount}; } ReceivedWithdrawInfo BinancePrivate::queryWithdrawDelivery(const InitiatedWithdrawInfo& initiatedWithdrawInfo, const SentWithdrawInfo& sentWithdrawInfo) { const CurrencyCode currencyCode = initiatedWithdrawInfo.grossEmittedAmount().currencyCode(); - json::container depositStatus = - PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/sapi/v1/capital/deposit/hisrec", _queryDelay, - {{"coin", currencyCode.str()}}); const Wallet& wallet = initiatedWithdrawInfo.receivingWallet(); - auto newEndIt = std::ranges::remove_if(depositStatus, [&wallet](const json::container& el) { - return el["status"].get() != 1 || el["address"].get() != wallet.address(); + auto depositStatus = PrivateQuery( + _curlHandle, _apiKey, HttpRequestType::kGet, "/sapi/v1/capital/deposit/hisrec", _queryDelay, + {{"coin", currencyCode.str()}}); + + auto newEndIt = std::ranges::remove_if(depositStatus, [&wallet](const auto& el) { + return el.status != 1 || el.address != wallet.address(); }).begin(); depositStatus.erase(newEndIt, depositStatus.end()); - const auto recentDepositFromJsonEl = [currencyCode](const json::container& el) { - const MonetaryAmount amountReceived(el["amount"].get(), currencyCode); - const TimePoint timestamp{milliseconds(el["insertTime"].get())}; + const auto recentDepositFromJsonEl = [currencyCode](const auto& el) { + const MonetaryAmount amountReceived(el.amount, currencyCode); + const TimePoint timestamp{milliseconds(el.insertTime)}; return RecentDeposit(amountReceived, timestamp); }; @@ -828,10 +886,10 @@ ReceivedWithdrawInfo BinancePrivate::queryWithdrawDelivery(const InitiatedWithdr return {}; } - json::container& depositEl = depositStatus[closestDepositPos]; + auto& depositEl = depositStatus[closestDepositPos]; const RecentDeposit recentDeposit = recentDepositFromJsonEl(depositEl); - return {std::move(depositEl["id"].get_ref()), recentDeposit.amount(), recentDeposit.timePoint()}; + return {std::move(depositEl.id), recentDeposit.amount(), recentDeposit.timePoint()}; } } // namespace cct::api diff --git a/src/basic-objects/include/coincentercommandtype.hpp b/src/basic-objects/include/coincentercommandtype.hpp index 72cdbafe..83b76d18 100644 --- a/src/basic-objects/include/coincentercommandtype.hpp +++ b/src/basic-objects/include/coincentercommandtype.hpp @@ -14,17 +14,13 @@ namespace cct { Balance, DepositInfo, OrdersClosed, OrdersOpened, OrdersCancel, RecentDeposits, RecentWithdraws, Trade, Buy, \ Sell, Withdraw, DustSweeper, \ \ - MarketData, Replay, ReplayMarkets, \ - \ - Last + MarketData, Replay, ReplayMarkets enum class CoincenterCommandType : int8_t { CCT_COINCENTER_COMMAND_TYPES }; -std::string_view CoincenterCommandTypeToString(CoincenterCommandType type); - -CoincenterCommandType CoincenterCommandTypeFromString(std::string_view str); +std::string_view CoincenterCommandTypeToString(CoincenterCommandType coincenterCommandType); -bool IsAnyTrade(CoincenterCommandType type); +bool IsAnyTrade(CoincenterCommandType coincenterCommandType); } // namespace cct template <> diff --git a/src/basic-objects/src/coincentercommandtype.cpp b/src/basic-objects/src/coincentercommandtype.cpp index f836bd40..2f3d24cb 100644 --- a/src/basic-objects/src/coincentercommandtype.cpp +++ b/src/basic-objects/src/coincentercommandtype.cpp @@ -1,40 +1,21 @@ #include "coincentercommandtype.hpp" -#include -#include #include #include -#include "cct_exception.hpp" #include "cct_json-serialization.hpp" namespace cct { namespace { constexpr auto kCommandTypeNames = json::reflect::keys; - -static_assert(std::size(kCommandTypeNames) == static_cast(CoincenterCommandType::Last) + 1); - } // namespace -std::string_view CoincenterCommandTypeToString(CoincenterCommandType type) { - const auto intValue = static_cast>(type); - if (intValue < decltype(intValue){} || - intValue >= static_cast>(CoincenterCommandType::Last)) { - throw exception("Unknown command type {}", intValue); - } - return kCommandTypeNames[intValue]; -} - -CoincenterCommandType CoincenterCommandTypeFromString(std::string_view str) { - const auto cmdIt = std::ranges::find(kCommandTypeNames, str); - if (cmdIt == std::end(kCommandTypeNames)) { - throw exception("Unknown command type {}", str); - } - return static_cast(cmdIt - std::begin(kCommandTypeNames)); +std::string_view CoincenterCommandTypeToString(CoincenterCommandType coincenterCommandType) { + return kCommandTypeNames[static_cast>(coincenterCommandType)]; } -bool IsAnyTrade(CoincenterCommandType type) { - switch (type) { +bool IsAnyTrade(CoincenterCommandType coincenterCommandType) { + switch (coincenterCommandType) { case CoincenterCommandType::Trade: [[fallthrough]]; case CoincenterCommandType::Buy: diff --git a/src/http-request/include/request-retry.hpp b/src/http-request/include/request-retry.hpp index 3960d896..df7589b0 100644 --- a/src/http-request/include/request-retry.hpp +++ b/src/http-request/include/request-retry.hpp @@ -6,6 +6,7 @@ #include "cct_exception.hpp" #include "cct_json-container.hpp" +#include "cct_json-serialization.hpp" #include "cct_log.hpp" #include "cct_type_traits.hpp" #include "curlhandle.hpp" @@ -14,6 +15,7 @@ #include "query-retry-policy.hpp" #include "timedef.hpp" #include "unreachable.hpp" +#include "write-json.hpp" namespace cct { @@ -44,31 +46,71 @@ class RequestRetry { template json::container queryJson(const StringType &endpoint, ResponseStatusT responseStatus, PostDataFuncT postDataUpdateFunc) { - decltype(_queryRetryPolicy.nbMaxRetries) nbRetries = 0; + return query(endpoint, responseStatus, + postDataUpdateFunc); + } + + template + T query(const StringType &endpoint, ResponseStatusT responseStatus) { + return query(endpoint, responseStatus, [](CurlOptions &) {}); + } + + template + T query(const StringType &endpoint, ResponseStatusT responseStatus, PostDataFuncT postDataUpdateFunc) { auto sleepingTime = _queryRetryPolicy.initialRetryDelay; - json::container ret; + decltype(_queryRetryPolicy.nbMaxRetries) nbRetries = 0; + bool parsingError; + T ret; do { if (nbRetries != 0) { - log::warn("Got query error: '{}' for {}, retry {}/{} after {}", ret.dump(), endpoint, nbRetries, - _queryRetryPolicy.nbMaxRetries, DurationToString(sleepingTime)); + if (log::get_level() <= log::level::warn) { + string strContent; + if constexpr (std::is_same_v) { + strContent = ret.dump(); + } else { + strContent = WriteJsonOrThrow(ret); + } + log::warn("Got query error: '{}' for {}, retry {}/{} after {}", strContent, endpoint, nbRetries, + _queryRetryPolicy.nbMaxRetries, DurationToString(sleepingTime)); + } + std::this_thread::sleep_for(sleepingTime); sleepingTime *= _queryRetryPolicy.exponentialBackoff; } postDataUpdateFunc(_curlOptions); - static constexpr bool kAllowExceptions = false; - ret = json::container::parse(_curlHandle.query(endpoint, _curlOptions), nullptr, kAllowExceptions); + auto queryStrRes = _curlHandle.query(endpoint, _curlOptions); + if constexpr (std::is_same_v) { + static constexpr bool kAllowExceptions = false; + ret = json::container::parse(queryStrRes, nullptr, kAllowExceptions); + parsingError = ret.is_discarded(); + } else { + auto ec = json::read(ret, queryStrRes); + if (ec) { + auto prefixJsonContent = queryStrRes.substr(0, std::min(queryStrRes.size(), 20)); + log::error("Error while reading json content '{}{}': {}", prefixJsonContent, + prefixJsonContent.size() < queryStrRes.size() ? "..." : "", json::format_error(ec, queryStrRes)); + parsingError = true; + } else { + parsingError = false; + } + } - } while ((ret.is_discarded() || responseStatus(ret) == Status::kResponseError) && + } while ((parsingError || responseStatus(ret) == Status::kResponseError) && ++nbRetries <= _queryRetryPolicy.nbMaxRetries); if (nbRetries > _queryRetryPolicy.nbMaxRetries) { switch (_queryRetryPolicy.tooManyFailuresPolicy) { case QueryRetryPolicy::TooManyFailuresPolicy::kReturnEmpty: - log::error("Too many query errors, returning empty result"); - ret = json::container::object(); + if constexpr (std::is_same_v) { + log::error("Too many query errors, returning empty json"); + ret = json::container::object(); + } else { + log::error("Too many query errors, returning value initialized object"); + ret = T(); + } break; case QueryRetryPolicy::TooManyFailuresPolicy::kThrowException: throw exception("Too many query errors");