diff --git a/src/api/exchanges/CMakeLists.txt b/src/api/exchanges/CMakeLists.txt index 92d57e56..0493c8e1 100644 --- a/src/api/exchanges/CMakeLists.txt +++ b/src/api/exchanges/CMakeLists.txt @@ -45,6 +45,11 @@ add_exchange_test( test/bithumbapi_test.cpp ) +add_exchange_test( + bithumb_place_order_test + test/bithumb_place_order_test.cpp +) + add_exchange_test( huobiapi_test test/huobiapi_test.cpp diff --git a/src/api/exchanges/include/bithumbprivateapi.hpp b/src/api/exchanges/include/bithumbprivateapi.hpp index 2cde4475..94b7c282 100644 --- a/src/api/exchanges/include/bithumbprivateapi.hpp +++ b/src/api/exchanges/include/bithumbprivateapi.hpp @@ -54,6 +54,8 @@ class BithumbPrivate : public ExchangePrivate { InitiatedWithdrawInfo launchWithdraw(MonetaryAmount grossAmount, Wallet&& destinationWallet) override; private: + friend class BithumbPrivateAPIPlaceOrderTest; + struct DepositWalletFunc { #ifndef CCT_AGGR_INIT_CXX20 DepositWalletFunc(CurlHandle& curlHandle, const APIKey& apiKey, BithumbPublic& exchangePublic) diff --git a/src/api/exchanges/src/bithumbprivateapi.cpp b/src/api/exchanges/src/bithumbprivateapi.cpp index df2d059f..94354ce0 100644 --- a/src/api/exchanges/src/bithumbprivateapi.cpp +++ b/src/api/exchanges/src/bithumbprivateapi.cpp @@ -45,15 +45,12 @@ constexpr std::string_view kWalletAddressEndpointStr = "/info/wallet_address"; std::pair GetStrData(std::string_view endpoint, std::string_view postDataStr) { Nonce nonce = Nonce_TimeSinceEpochInMs(); - string strData(endpoint); - strData.reserve(strData.size() + 2U + postDataStr.size() + nonce.size()); - static constexpr char kParChar = 1; - strData.push_back(kParChar); - strData.append(postDataStr); - strData.push_back(kParChar); + string strData(endpoint.size() + 2U + postDataStr.size() + nonce.size(), kParChar); - strData.append(nonce.begin(), nonce.end()); + auto it = std::ranges::copy(endpoint, strData.begin()).out; + it = std::ranges::copy(postDataStr, it + 1).out; + it = std::ranges::copy(nonce, it + 1).out; return std::make_pair(std::move(strData), std::move(nonce)); } @@ -276,19 +273,21 @@ BithumbPrivate::BithumbPrivate(const CoincenterInfo& config, BithumbPublic& bith _depositWalletsCache( CachedResultOptions(exchangeInfo().getAPICallUpdateFrequency(kDepositWallet), _cachedResultVault), _curlHandle, _apiKey, bithumbPublic) { - json data = GetBithumbCurrencyInfoMapCache(_coincenterInfo.dataDir()).readAllJson(); - for (const auto& [currencyCodeStr, currencyOrderInfoJson] : data.items()) { - CurrencyOrderInfo currencyOrderInfo; - - LoadCurrencyInfoField(currencyOrderInfoJson, kNbDecimalsStr, currencyOrderInfo.nbDecimals, - currencyOrderInfo.lastNbDecimalsUpdatedTime); - LoadCurrencyInfoField(currencyOrderInfoJson, kMinOrderSizeJsonKeyStr, currencyOrderInfo.minOrderSize, - currencyOrderInfo.lastMinOrderSizeUpdatedTime); - LoadCurrencyInfoField(currencyOrderInfoJson, kMinOrderPriceJsonKeyStr, currencyOrderInfo.minOrderPrice, - currencyOrderInfo.lastMinOrderPriceUpdatedTime); - LoadCurrencyInfoField(currencyOrderInfoJson, kMaxOrderPriceJsonKeyStr, currencyOrderInfo.maxOrderPrice, - currencyOrderInfo.lastMaxOrderPriceUpdatedTime); - _currencyOrderInfoMap.insert_or_assign(CurrencyCode(currencyCodeStr), std::move(currencyOrderInfo)); + if (config.getRunMode() != settings::RunMode::kQueryResponseOverriden) { + json data = GetBithumbCurrencyInfoMapCache(_coincenterInfo.dataDir()).readAllJson(); + for (const auto& [currencyCodeStr, currencyOrderInfoJson] : data.items()) { + CurrencyOrderInfo currencyOrderInfo; + + LoadCurrencyInfoField(currencyOrderInfoJson, kNbDecimalsStr, currencyOrderInfo.nbDecimals, + currencyOrderInfo.lastNbDecimalsUpdatedTime); + LoadCurrencyInfoField(currencyOrderInfoJson, kMinOrderSizeJsonKeyStr, currencyOrderInfo.minOrderSize, + currencyOrderInfo.lastMinOrderSizeUpdatedTime); + LoadCurrencyInfoField(currencyOrderInfoJson, kMinOrderPriceJsonKeyStr, currencyOrderInfo.minOrderPrice, + currencyOrderInfo.lastMinOrderPriceUpdatedTime); + LoadCurrencyInfoField(currencyOrderInfoJson, kMaxOrderPriceJsonKeyStr, currencyOrderInfo.maxOrderPrice, + currencyOrderInfo.lastMaxOrderPriceUpdatedTime); + _currencyOrderInfoMap.insert_or_assign(CurrencyCode(currencyCodeStr), std::move(currencyOrderInfo)); + } } } @@ -757,17 +756,21 @@ PlaceOrderInfo BithumbPrivate::placeOrder(MonetaryAmount /*from*/, MonetaryAmoun if (LoadCurrencyInfoField(result, kNbDecimalsStr, currencyOrderInfo.nbDecimals, currencyOrderInfo.lastNbDecimalsUpdatedTime)) { volume.truncate(currencyOrderInfo.nbDecimals); + if (volume == 0) { + log::warn("No trade of {} into {} because volume is 0 after truncating to {} decimals", volume, + toCurrencyCode, static_cast(currencyOrderInfo.nbDecimals)); + break; + } placePostData.set("units", volume.amountStr()); } else if (LoadCurrencyInfoField(result, kMinOrderSizeJsonKeyStr, currencyOrderInfo.minOrderSize, currencyOrderInfo.lastMinOrderSizeUpdatedTime)) { - if (isSimulationWithRealOrder && currencyOrderInfo.minOrderSize.currencyCode() == price.currencyCode()) { - volume = MonetaryAmount(currencyOrderInfo.minOrderSize / price, volume.currencyCode()); - placePostData.set("units", volume.amountStr()); - } else { + if (!isSimulationWithRealOrder || currencyOrderInfo.minOrderSize.currencyCode() != price.currencyCode()) { log::warn("No trade of {} into {} because min order size is {} for this market", volume, toCurrencyCode, currencyOrderInfo.minOrderSize); break; } + volume = MonetaryAmount(currencyOrderInfo.minOrderSize / price, volume.currencyCode()); + placePostData.set("units", volume.amountStr()); } else if (LoadCurrencyInfoField(result, kMinOrderPriceJsonKeyStr, currencyOrderInfo.minOrderPrice, currencyOrderInfo.lastMinOrderPriceUpdatedTime)) { if (isSimulationWithRealOrder) { diff --git a/src/api/exchanges/test/bithumb_place_order_test.cpp b/src/api/exchanges/test/bithumb_place_order_test.cpp new file mode 100644 index 00000000..f6ffa961 --- /dev/null +++ b/src/api/exchanges/test/bithumb_place_order_test.cpp @@ -0,0 +1,69 @@ +#include + +#include "apikeysprovider.hpp" +#include "bithumbprivateapi.hpp" +#include "bithumbpublicapi.hpp" +#include "coincenterinfo.hpp" +#include "fiatconverter.hpp" + +namespace cct::api { +class BithumbPrivateAPIPlaceOrderTest : public ::testing::Test { + protected: + PlaceOrderInfo placeOrder(MonetaryAmount volume, MonetaryAmount price, TradeSide tradeSide) { + Market market{volume.currencyCode(), price.currencyCode()}; + TradeContext tradeContext{market, tradeSide}; + TradeOptions tradeOptions; + TradeInfo tradeInfo{tradeContext, tradeOptions}; + + return exchangePrivate.placeOrder(from, volume, price, tradeInfo); + } + + void setOverridenQueryResponses(const std::map& queryResponses) { + exchangePrivate._curlHandle.setOverridenQueryResponses(queryResponses); + } + + settings::RunMode runMode = settings::RunMode::kQueryResponseOverriden; + LoadConfiguration loadConfig{kDefaultDataDir, LoadConfiguration::ExchangeConfigFileType::kTest}; + CoincenterInfo coincenterInfo{runMode, loadConfig}; + FiatConverter fiatConverter{coincenterInfo, Duration::max()}; // max to avoid real Fiat converter queries + CryptowatchAPI cryptowatchAPI{coincenterInfo, runMode}; + BithumbPublic exchangePublic{coincenterInfo, fiatConverter, cryptowatchAPI}; + APIKeysProvider apiKeysProvider{coincenterInfo.dataDir(), coincenterInfo.getRunMode()}; + ExchangeName exchangeName{exchangePublic.name(), apiKeysProvider.getKeyNames(exchangePublic.name()).front()}; + const APIKey& testKey = apiKeysProvider.get(exchangeName); + BithumbPrivate exchangePrivate{coincenterInfo, exchangePublic, testKey}; + + MonetaryAmount from; +}; + +TEST_F(BithumbPrivateAPIPlaceOrderTest, PlaceOrderShortenDecimals) { + setOverridenQueryResponses( + {/// Place order, with high number of decimals + {"/trade/" + "place?endpoint=%2Ftrade%2Fplace&order_currency=ETH&payment_currency=EUR&type=ask&price=1500&units=2.000001", + "{\"status\": \"5600\", \"message\":\"수량은 소수점 4자\"}"}, + /// Replace order with decimals correctly truncated + {"/trade/" + "place?endpoint=%2Ftrade%2Fplace&order_currency=ETH&payment_currency=EUR&type=ask&price=1500&units=2", + "{\"status\": \"0000\", \"order_id\": \"ID0001\"}"}, + /// Query once order info, order not matched + {"/info/orders?endpoint=%2Finfo%2Forders&order_currency=ETH&payment_currency=EUR&type=ask&order_id=ID0001", + "{\"status\": \"0000\", \"data\": [{\"order_id\": \"ID0001\"}]}"}}); + + PlaceOrderInfo placeOrderInfo = + placeOrder(MonetaryAmount("2.000001ETH"), MonetaryAmount("1500EUR"), TradeSide::kSell); + EXPECT_EQ(placeOrderInfo.orderId, "ID0001"); +} + +TEST_F(BithumbPrivateAPIPlaceOrderTest, NoPlaceOrderTooSmallAmount) { + setOverridenQueryResponses( + {/// Place order, with high number of decimals + {"/trade/" + "place?endpoint=%2Ftrade%2Fplace&order_currency=ETH&payment_currency=EUR&type=ask&price=1500&units=0.000001", + "{\"status\": \"5600\", \"message\":\"수량은 소수점 4자\"}"}}); + + PlaceOrderInfo placeOrderInfo = + placeOrder(MonetaryAmount("0.000001ETH"), MonetaryAmount("1500EUR"), TradeSide::kSell); + EXPECT_EQ(placeOrderInfo.orderId, "UndefinedId"); +} +} // namespace cct::api \ No newline at end of file