diff --git a/CONFIG.md b/CONFIG.md index b4222176..94063d87 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -162,6 +162,6 @@ Refer to the hardcoded default json example as a model in case of doubt. ##### Notes -- `updateFrequency` is itself a json document containing all duration values as query frequencies. - See [ExchangeConfig default file](src/objects/src/exchangeconfigdefault.hpp) as an example for the syntax. +- `updateFrequency` is itself a json document containing all duration values as query frequencies. + See [ExchangeConfig default file](src/basic-objects/include/exchange-config-default.hpp) as an example for the syntax. - Unused and not explicitly set values (so, when loaded from default values) from your personal `exchangeconfig.json` file will be logged for information about what will actually be used by `coincenter`. diff --git a/src/api/common/src/exchangeprivateapi.cpp b/src/api/common/src/exchangeprivateapi.cpp index 733e02a6..a8ec3120 100644 --- a/src/api/common/src/exchangeprivateapi.cpp +++ b/src/api/common/src/exchangeprivateapi.cpp @@ -586,7 +586,7 @@ PlaceOrderInfo ExchangePrivate::computeSimulatedMatchedPlacedOrderInfo(MonetaryA const bool isSell = tradeInfo.tradeContext.side == TradeSide::kSell; MonetaryAmount toAmount = isSell ? volume.convertTo(price) : volume; - ExchangeConfig::FeeType feeType = isTakerStrategy ? ExchangeConfig::FeeType::kTaker : ExchangeConfig::FeeType::kMaker; + ExchangeConfig::FeeType feeType = isTakerStrategy ? ExchangeConfig::FeeType::Taker : ExchangeConfig::FeeType::Maker; toAmount = _coincenterInfo.exchangeConfig(_exchangePublic.name()).applyFee(toAmount, feeType); PlaceOrderInfo placeOrderInfo(OrderInfo(TradedAmounts(isSell ? volume : volume.toNeutral() * price, toAmount)), OrderId("SimulatedOrderId")); diff --git a/src/api/common/src/exchangepublicapi.cpp b/src/api/common/src/exchangepublicapi.cpp index 8a5d384e..7e09f7e8 100644 --- a/src/api/common/src/exchangepublicapi.cpp +++ b/src/api/common/src/exchangepublicapi.cpp @@ -82,7 +82,7 @@ std::optional ExchangePublic::convert(MonetaryAmount from, Curre return std::nullopt; } const ExchangeConfig::FeeType feeType = - priceOptions.isTakerStrategy() ? ExchangeConfig::FeeType::kTaker : ExchangeConfig::FeeType::kMaker; + priceOptions.isTakerStrategy() ? ExchangeConfig::FeeType::Taker : ExchangeConfig::FeeType::Maker; if (marketOrderBookMap.empty()) { std::lock_guard guard(_publicRequestsMutex); diff --git a/src/api/common/test/exchangeprivateapi_test.cpp b/src/api/common/test/exchangeprivateapi_test.cpp index 49b9d45b..9f1b5d0c 100644 --- a/src/api/common/test/exchangeprivateapi_test.cpp +++ b/src/api/common/test/exchangeprivateapi_test.cpp @@ -347,7 +347,7 @@ TEST_F(ExchangePrivateTest, SimulatedOrderShouldNotCallPlaceOrder) { // In simulation mode, fee is applied MonetaryAmount toAmount = - exchangePublic.exchangeConfig().applyFee(from.toNeutral() * askPrice1, ExchangeConfig::FeeType::kMaker); + exchangePublic.exchangeConfig().applyFee(from.toNeutral() * askPrice1, ExchangeConfig::FeeType::Maker); EXPECT_EQ(exchangePrivate.trade(from, market.quote(), tradeOptions), TradedAmounts(from, toAmount)); } diff --git a/src/api/common/test/exchangepublicapi_test.cpp b/src/api/common/test/exchangepublicapi_test.cpp index 75c166dc..06d2aae2 100644 --- a/src/api/common/test/exchangepublicapi_test.cpp +++ b/src/api/common/test/exchangepublicapi_test.cpp @@ -195,7 +195,7 @@ TEST_F(ExchangePublicConvertTest, ConvertSimple) { exchangePublic.convert(from, toCurrency, conversionPath, fiats, marketOrderBookMap, priceOptions); ASSERT_TRUE(ret.has_value()); MonetaryAmount res = exchangePublic.exchangeConfig().applyFee( - marketOrderBook1.convert(from, priceOptions).value_or(MonetaryAmount{-1}), ExchangeConfig::FeeType::kMaker); + marketOrderBook1.convert(from, priceOptions).value_or(MonetaryAmount{-1}), ExchangeConfig::FeeType::Maker); EXPECT_EQ(ret, std::optional(res)); } @@ -210,9 +210,9 @@ TEST_F(ExchangePublicConvertTest, ConvertDouble) { ASSERT_TRUE(ret.has_value()); MonetaryAmount res = exchangePublic.exchangeConfig().applyFee( - marketOrderBook1.convert(from, priceOptions).value_or(MonetaryAmount{-1}), ExchangeConfig::FeeType::kMaker); + marketOrderBook1.convert(from, priceOptions).value_or(MonetaryAmount{-1}), ExchangeConfig::FeeType::Maker); res = exchangePublic.exchangeConfig().applyFee( - marketOrderBook2.convert(res, priceOptions).value_or(MonetaryAmount{-1}), ExchangeConfig::FeeType::kMaker); + marketOrderBook2.convert(res, priceOptions).value_or(MonetaryAmount{-1}), ExchangeConfig::FeeType::Maker); EXPECT_EQ(ret.value_or(MonetaryAmount{}), res); } @@ -232,7 +232,7 @@ TEST_F(ExchangePublicConvertTest, ConvertWithFiatAtBeginning) { MonetaryAmount res = exchangePublic.exchangeConfig().applyFee( marketOrderBook3.convert(optRes.value_or(MonetaryAmount{}), priceOptions).value_or(MonetaryAmount{-1}), - ExchangeConfig::FeeType::kMaker); + ExchangeConfig::FeeType::Maker); EXPECT_EQ(ret.value_or(MonetaryAmount{}), res); } @@ -249,11 +249,11 @@ TEST_F(ExchangePublicConvertTest, ConvertWithFiatAtEnd) { ASSERT_TRUE(ret.has_value()); MonetaryAmount res = exchangePublic.exchangeConfig().applyFee( - marketOrderBook1.convert(from, priceOptions).value_or(MonetaryAmount{-1}), ExchangeConfig::FeeType::kMaker); + marketOrderBook1.convert(from, priceOptions).value_or(MonetaryAmount{-1}), ExchangeConfig::FeeType::Maker); res = exchangePublic.exchangeConfig().applyFee( - marketOrderBook4.convert(res, priceOptions).value_or(MonetaryAmount{-1}), ExchangeConfig::FeeType::kMaker); + marketOrderBook4.convert(res, priceOptions).value_or(MonetaryAmount{-1}), ExchangeConfig::FeeType::Maker); res = exchangePublic.exchangeConfig().applyFee( - marketOrderBook3.convert(res, priceOptions).value_or(MonetaryAmount{-1}), ExchangeConfig::FeeType::kMaker); + marketOrderBook3.convert(res, priceOptions).value_or(MonetaryAmount{-1}), ExchangeConfig::FeeType::Maker); EXPECT_EQ(ret, fiatConverter.convert(res, toCurrency)); } diff --git a/src/api/exchanges/src/bithumbprivateapi.cpp b/src/api/exchanges/src/bithumbprivateapi.cpp index c92ced3f..583eed9a 100644 --- a/src/api/exchanges/src/bithumbprivateapi.cpp +++ b/src/api/exchanges/src/bithumbprivateapi.cpp @@ -867,8 +867,7 @@ PlaceOrderInfo BithumbPrivate::placeOrder(MonetaryAmount /*from*/, MonetaryAmoun // Volume is gross amount if from amount is in quote currency, we should remove the fees if (fromCurrencyCode == mk.quote()) { - ExchangeConfig::FeeType feeType = - isTakerStrategy ? ExchangeConfig::FeeType::kTaker : ExchangeConfig::FeeType::kMaker; + ExchangeConfig::FeeType feeType = isTakerStrategy ? ExchangeConfig::FeeType::Taker : ExchangeConfig::FeeType::Maker; const ExchangeConfig& exchangeConfig = _coincenterInfo.exchangeConfig(_exchangePublic.name()); volume = exchangeConfig.applyFee(volume, feeType); } diff --git a/src/api/exchanges/src/upbitprivateapi.cpp b/src/api/exchanges/src/upbitprivateapi.cpp index b8d46b93..86158aba 100644 --- a/src/api/exchanges/src/upbitprivateapi.cpp +++ b/src/api/exchanges/src/upbitprivateapi.cpp @@ -554,8 +554,7 @@ void UpbitPrivate::applyFee(Market mk, CurrencyCode fromCurrencyCode, bool isTak MonetaryAmount& volume) { if (fromCurrencyCode == mk.quote()) { // For 'buy', from amount is fee excluded - ExchangeConfig::FeeType feeType = - isTakerStrategy ? ExchangeConfig::FeeType::kTaker : ExchangeConfig::FeeType::kMaker; + ExchangeConfig::FeeType feeType = isTakerStrategy ? ExchangeConfig::FeeType::Taker : ExchangeConfig::FeeType::Maker; const ExchangeConfig& exchangeConfig = _coincenterInfo.exchangeConfig(_exchangePublic.name()); if (isTakerStrategy) { from = exchangeConfig.applyFee(from, feeType); diff --git a/src/objects/src/exchangeconfigdefault.hpp b/src/basic-objects/include/exchange-config-default.hpp similarity index 88% rename from src/objects/src/exchangeconfigdefault.hpp rename to src/basic-objects/include/exchange-config-default.hpp index 73e3a466..afded700 100644 --- a/src/objects/src/exchangeconfigdefault.hpp +++ b/src/basic-objects/include/exchange-config-default.hpp @@ -1,14 +1,11 @@ #pragma once -#include "cct_json-container.hpp" +#include namespace cct { -struct ExchangeConfigDefault { - static json::container Prod() { - // Use a static method instead of an inline static const variable to avoid the infamous 'static initialization order - // fiasco' problem. - static const json::container kProd = R"( +struct ExchangeConfigDefault { + static constexpr std::string_view kProd = R"( { "asset": { "default": { @@ -142,17 +139,11 @@ struct ExchangeConfigDefault { "validateDepositAddressesInFile": true } } -} -)"_json; - return kProd; - } +} +)"; /// ExchangeInfos for tests only. Some tests rely on provided values so changing them may make them fail. - static json::container Test() { - // Use a static method instead of an inline static const variable to avoid the infamous 'static initialization order - // fiasco' problem. - - static const json::container kTest = R"( + static constexpr std::string_view kTest = R"( { "asset": { "default": { @@ -212,7 +203,7 @@ struct ExchangeConfigDefault { "validateApiKey": true } }, - "tradefees": { + "tradeFees": { "default": { "maker": "0.1", "taker": "0.2" @@ -224,8 +215,6 @@ struct ExchangeConfigDefault { } } } -)"_json; - return kTest; - } +)"; }; } // namespace cct \ No newline at end of file diff --git a/src/objects/include/loadconfiguration.hpp b/src/basic-objects/include/loadconfiguration.hpp similarity index 100% rename from src/objects/include/loadconfiguration.hpp rename to src/basic-objects/include/loadconfiguration.hpp diff --git a/src/objects/src/loadconfiguration.cpp b/src/basic-objects/src/loadconfiguration.cpp similarity index 100% rename from src/objects/src/loadconfiguration.cpp rename to src/basic-objects/src/loadconfiguration.cpp diff --git a/src/objects/include/exchangeconfig.hpp b/src/objects/include/exchangeconfig.hpp index 4d3e552e..8ff0f204 100644 --- a/src/objects/include/exchangeconfig.hpp +++ b/src/objects/include/exchangeconfig.hpp @@ -18,7 +18,7 @@ namespace cct { class ExchangeConfig { public: - enum class FeeType : int8_t { kMaker, kTaker }; + enum class FeeType : int8_t { Maker, Taker }; enum class MarketDataSerialization : int8_t { kYes, kNo }; struct APIUpdateFrequencies { @@ -62,7 +62,7 @@ class ExchangeConfig { /// Apply the general maker fee defined for this exchange on given MonetaryAmount. /// In other words, convert a gross amount into a net amount with maker fees MonetaryAmount applyFee(MonetaryAmount ma, FeeType feeType) const { - return ma * (feeType == FeeType::kMaker ? _generalMakerRatio : _generalTakerRatio); + return ma * (feeType == FeeType::Maker ? _generalMakerRatio : _generalTakerRatio); } MonetaryAmount getMakerFeeRatio() const { return _generalMakerRatio; } diff --git a/src/objects/include/exchangeconfigparser.hpp b/src/objects/include/exchangeconfigparser.hpp index 848a5e78..227ab36f 100644 --- a/src/objects/include/exchangeconfigparser.hpp +++ b/src/objects/include/exchangeconfigparser.hpp @@ -10,6 +10,7 @@ #include "currencycode.hpp" #include "currencycodevector.hpp" #include "durationstring.hpp" +#include "exchange-config.hpp" #include "monetaryamount.hpp" #include "monetaryamountbycurrencyset.hpp" @@ -24,7 +25,7 @@ class TopLevelOption { // Top level option names static constexpr std::string_view kAssetsOptionStr = "asset"; static constexpr std::string_view kQueryOptionStr = "query"; - static constexpr std::string_view kTradeFeesOptionStr = "tradefees"; + static constexpr std::string_view kTradeFeesOptionStr = "tradeFees"; static constexpr std::string_view kWithdrawOptionStr = "withdraw"; /// @brief Create a TopLevelOption from given json data. diff --git a/src/objects/src/exchangeconfigmap.cpp b/src/objects/src/exchangeconfigmap.cpp index e76f337b..6c2b6bed 100644 --- a/src/objects/src/exchangeconfigmap.cpp +++ b/src/objects/src/exchangeconfigmap.cpp @@ -5,11 +5,11 @@ #include #include "cct_const.hpp" -#include "cct_json-container.hpp" #include "cct_log.hpp" #include "cct_string.hpp" +#include "exchange-config-default.hpp" +#include "exchange-config.hpp" #include "exchangeconfig.hpp" -#include "exchangeconfigdefault.hpp" #include "exchangeconfigparser.hpp" #include "http-config.hpp" #include "monetaryamountbycurrencyset.hpp" @@ -24,7 +24,7 @@ namespace cct { ExchangeConfigMap ComputeExchangeConfigMap(std::string_view fileName, const json::container &jsonData) { ExchangeConfigMap map; - const json::container &prodDefault = ExchangeConfigDefault::Prod(); + json::container prodDefault = json::container::parse(ExchangeConfigDefault::kProd); TopLevelOption assetTopLevelOption(TopLevelOption::kAssetsOptionStr, prodDefault, jsonData); TopLevelOption queryTopLevelOption(TopLevelOption::kQueryOptionStr, prodDefault, jsonData); diff --git a/src/objects/src/exchangeconfigparser.cpp b/src/objects/src/exchangeconfigparser.cpp index 4bc7bfa3..b25de1b9 100644 --- a/src/objects/src/exchangeconfigparser.cpp +++ b/src/objects/src/exchangeconfigparser.cpp @@ -9,7 +9,7 @@ #include "cct_log.hpp" #include "cct_string.hpp" #include "currencycodevector.hpp" -#include "exchangeconfigdefault.hpp" +#include "exchange-config-default.hpp" #include "file.hpp" #include "loadconfiguration.hpp" #include "unreachable.hpp" @@ -26,7 +26,7 @@ json::container LoadExchangeConfigData(const LoadConfiguration& loadConfiguratio case LoadConfiguration::ExchangeConfigFileType::kProd: { std::string_view filename = loadConfiguration.exchangeConfigFileName(); File exchangeConfigFile(loadConfiguration.dataDir(), File::Type::kStatic, filename, File::IfError::kNoThrow); - json::container jsonData = ExchangeConfigDefault::Prod(); + json::container jsonData = json::container::parse(ExchangeConfigDefault::kProd); json::container exchangeConfigJsonData = exchangeConfigFile.readAllJson(); if (exchangeConfigJsonData.empty()) { // Create a file with default values. User can then update them as he wishes. @@ -45,7 +45,7 @@ json::container LoadExchangeConfigData(const LoadConfiguration& loadConfiguratio return exchangeConfigJsonData; } case LoadConfiguration::ExchangeConfigFileType::kTest: - return ExchangeConfigDefault::Test(); + return json::container::parse(ExchangeConfigDefault::kTest); default: unreachable(); } diff --git a/src/objects/test/exchangeconfig_test.cpp b/src/objects/test/exchangeconfig_test.cpp index 5a17f500..d468cbd9 100644 --- a/src/objects/test/exchangeconfig_test.cpp +++ b/src/objects/test/exchangeconfig_test.cpp @@ -35,9 +35,9 @@ TEST_F(ExchangeConfigTest, ExcludedAssets) { } TEST_F(ExchangeConfigTest, TradeFees) { - EXPECT_EQ(binanceExchangeInfo.applyFee(MonetaryAmount("120.5 ETH"), ExchangeConfig::FeeType::kMaker), + EXPECT_EQ(binanceExchangeInfo.applyFee(MonetaryAmount("120.5 ETH"), ExchangeConfig::FeeType::Maker), MonetaryAmount("120.3795 ETH")); - EXPECT_EQ(binanceExchangeInfo.applyFee(MonetaryAmount("2.356097 ETH"), ExchangeConfig::FeeType::kTaker), + EXPECT_EQ(binanceExchangeInfo.applyFee(MonetaryAmount("2.356097 ETH"), ExchangeConfig::FeeType::Taker), MonetaryAmount("2.351384806 ETH")); } diff --git a/src/schema/CMakeLists.txt b/src/schema/CMakeLists.txt index b09c45c2..094fde15 100644 --- a/src/schema/CMakeLists.txt +++ b/src/schema/CMakeLists.txt @@ -18,6 +18,13 @@ add_unit_test( coincenter_schema ) +add_unit_test( + exchange-config_test + test/exchange-config_test.cpp + LIBRARIES + coincenter_schema +) + add_unit_test( general-config_test test/general-config_test.cpp diff --git a/src/schema/include/exchange-asset-config.hpp b/src/schema/include/exchange-asset-config.hpp new file mode 100644 index 00000000..9f945f27 --- /dev/null +++ b/src/schema/include/exchange-asset-config.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "currencycodeset.hpp" + +namespace cct::schema { + +struct ExchangeAssetConfig { + using trivially_relocatable = is_trivially_relocatable::type; + + void mergeWith(const ExchangeAssetConfig &other) { + allExclude.insert(other.allExclude.begin(), other.allExclude.end()); + preferredPaymentCurrencies.insert(other.preferredPaymentCurrencies.begin(), other.preferredPaymentCurrencies.end()); + withdrawExclude.insert(other.withdrawExclude.begin(), other.withdrawExclude.end()); + } + + CurrencyCodeSet allExclude; + CurrencyCodeSet preferredPaymentCurrencies; + CurrencyCodeSet withdrawExclude; +}; + +} // namespace cct::schema \ No newline at end of file diff --git a/src/schema/include/exchange-config.hpp b/src/schema/include/exchange-config.hpp new file mode 100644 index 00000000..069f2941 --- /dev/null +++ b/src/schema/include/exchange-config.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include +#include + +#include "cct_const.hpp" +#include "cct_string.hpp" +#include "exchange-asset-config.hpp" +#include "exchange-query-config.hpp" +#include "exchange-tradefees-config.hpp" +#include "exchange-withdraw-config.hpp" + +namespace cct { + +namespace schema { + +namespace details { + +template +struct ExchangeConfigPart { + T def; // default is a reserved keyword - we override the json field name below + std::map> exchange; +}; + +struct AllExchangeConfigsOptional { + ExchangeConfigPart asset; + ExchangeConfigPart query; + ExchangeConfigPart tradeFees; + ExchangeConfigPart withdraw; +}; + +} // namespace details + +struct ExchangeConfig { + ExchangeAssetConfig asset; + ExchangeQueryConfig query; + ExchangeTradeFeesConfig tradeFees; + ExchangeWithdrawConfig withdraw; +}; + +struct AllExchangeConfigs { + void mergeWith(const details::AllExchangeConfigsOptional &other) { + for (int exchangePos = 0; exchangePos < kNbSupportedExchanges; ++exchangePos) { + auto &exchangeConfig = exchangeConfigs[exchangePos]; + std::string_view exchangeName = kSupportedExchanges[exchangePos]; + + auto assetIt = other.asset.exchange.find(exchangeName); + auto queryIt = other.query.exchange.find(exchangeName); + auto tradeFeesIt = other.tradeFees.exchange.find(exchangeName); + auto withdrawIt = other.withdraw.exchange.find(exchangeName); + + exchangeConfig.asset.mergeWith(other.asset.def); + if (assetIt != other.asset.exchange.end()) { + exchangeConfig.asset.mergeWith(assetIt->second); + } + + exchangeConfig.query.mergeWith(other.query.def); + if (queryIt != other.query.exchange.end()) { + exchangeConfig.query.mergeWith(queryIt->second); + } + + exchangeConfig.tradeFees.mergeWith(other.tradeFees.def); + if (tradeFeesIt != other.tradeFees.exchange.end()) { + exchangeConfig.tradeFees.mergeWith(tradeFeesIt->second); + } + + exchangeConfig.withdraw.mergeWith(other.withdraw.def); + if (withdrawIt != other.withdraw.exchange.end()) { + exchangeConfig.withdraw.mergeWith(withdrawIt->second); + } + } + } + + const ExchangeConfig &operator[](ExchangeNameEnum exchangeName) const { + return exchangeConfigs[static_cast(exchangeName)]; + } + + std::array exchangeConfigs; +}; + +} // namespace schema + +class LoadConfiguration; + +schema::AllExchangeConfigs ReadExchangeConfigs(const LoadConfiguration &loadConfiguration); + +} // namespace cct + +template +struct glz::meta<::cct::schema::details::ExchangeConfigPart> { + using V = ::cct::schema::details::ExchangeConfigPart; + static constexpr auto value = object("default", &V::def, "exchange", &V::exchange); +}; diff --git a/src/schema/include/exchange-query-config.hpp b/src/schema/include/exchange-query-config.hpp new file mode 100644 index 00000000..af007a2a --- /dev/null +++ b/src/schema/include/exchange-query-config.hpp @@ -0,0 +1,155 @@ +#pragma once + +#include +#include +#include + +#include "cct_string.hpp" +#include "cct_vector.hpp" +#include "currencycode.hpp" +#include "duration-schema.hpp" +#include "monetaryamount.hpp" +#include "monetaryamountbycurrencyset.hpp" +#include "optional-or-type.hpp" + +namespace cct::schema { + +namespace details { + +template +struct ExchangeQueryHttpConfig { + template > && !Optional, bool> = true> + void mergeWith(const T &other) { + if (other.timeout) { + timeout = *other.timeout; + } + } + + optional_or_t timeout; +}; + +template +struct ExchangeQueryTradeConfig { + using trivially_relocatable = is_trivially_relocatable::type; + + template > && !Optional, bool> = true> + void mergeWith(const T &other) { + if (other.minPriceUpdateDuration) { + minPriceUpdateDuration = *other.minPriceUpdateDuration; + } + if (other.timeout) { + timeout = *other.timeout; + } + if (other.strategy) { + strategy = *other.strategy; + } + if (other.timeoutMatch) { + timeoutMatch = *other.timeoutMatch; + } + } + + optional_or_t minPriceUpdateDuration{}; + optional_or_t timeout{}; + optional_or_t strategy; + optional_or_t timeoutMatch{}; +}; + +template +struct ExchangeQueryLogLevelsConfig { + using trivially_relocatable = is_trivially_relocatable::type; + + template > && !Optional, bool> = true> + void mergeWith(const T &other) { + if (other.requestsCall) { + requestsCall = *other.requestsCall; + } + if (other.requestsAnswer) { + requestsAnswer = *other.requestsAnswer; + } + } + + optional_or_t requestsCall; + optional_or_t requestsAnswer; +}; + +} // namespace details + +using ExchangeQueryHttpConfig = details::ExchangeQueryHttpConfig; +using ExchangeQueryHttpConfigOptional = details::ExchangeQueryHttpConfig; + +using ExchangeQueryUpdateFrequencyConfig = std::map>; + +using ExchangeQueryTradeConfig = details::ExchangeQueryTradeConfig; +using ExchangeQueryTradeConfigOptional = details::ExchangeQueryTradeConfig; + +namespace details { + +template +struct ExchangeQueryConfig { + template > && !Optional, bool> = true> + void mergeWith(const T &other) { + if (other.http) { + http.mergeWith(*other.http); + } + if (other.logLevels) { + logLevels.mergeWith(*other.logLevels); + } + if (other.trade) { + trade.mergeWith(*other.trade); + } + if (other.updateFrequency) { + for (const auto &[key, value] : *other.updateFrequency) { + updateFrequency.insert_or_assign(key, value); + } + } + if (other.acceptEncoding) { + acceptEncoding = *other.acceptEncoding; + } + if (other.privateAPIRate) { + privateAPIRate = *other.privateAPIRate; + } + if (other.publicAPIRate) { + publicAPIRate = *other.publicAPIRate; + } + if (!other.dustAmountsThreshold.empty()) { + dustAmountsThreshold.insert_or_assign(other.dustAmountsThreshold.begin(), other.dustAmountsThreshold.end()); + } + if (other.dustSweeperMaxNbTrades) { + dustSweeperMaxNbTrades = *other.dustSweeperMaxNbTrades; + } + if (other.marketDataSerialization) { + marketDataSerialization = *other.marketDataSerialization; + } + if (other.multiTradeAllowedByDefault) { + multiTradeAllowedByDefault = *other.multiTradeAllowedByDefault; + } + if (other.placeSimulateRealOrder) { + placeSimulateRealOrder = *other.placeSimulateRealOrder; + } + if (other.validateApiKey) { + validateApiKey = *other.validateApiKey; + } + } + + optional_or_t, Optional> http; + optional_or_t, Optional> logLevels; + optional_or_t, Optional> trade; + optional_or_t updateFrequency; + optional_or_t acceptEncoding; + optional_or_t privateAPIRate{}; + optional_or_t publicAPIRate{}; + MonetaryAmountByCurrencySet dustAmountsThreshold; + optional_or_t dustSweeperMaxNbTrades{}; + optional_or_t marketDataSerialization{}; + optional_or_t multiTradeAllowedByDefault{}; + optional_or_t placeSimulateRealOrder{}; + optional_or_t validateApiKey{}; +}; + +using ExchangeQueryConfigOptional = ExchangeQueryConfig; + +} // namespace details + +using ExchangeQueryConfig = details::ExchangeQueryConfig; + +} // namespace cct::schema \ No newline at end of file diff --git a/src/schema/include/exchange-tradefees-config.hpp b/src/schema/include/exchange-tradefees-config.hpp new file mode 100644 index 00000000..08dc8e1a --- /dev/null +++ b/src/schema/include/exchange-tradefees-config.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +#include "monetaryamount.hpp" +#include "optional-or-type.hpp" + +namespace cct::schema { + +namespace details { + +template +struct ExchangeTradeFeesConfig { + template > && !Optional, bool> = true> + void mergeWith(const T &other) { + if (other.maker) { + maker = *other.maker; + } + if (other.taker) { + taker = *other.taker; + } + } + + enum class FeeType : int8_t { Maker, Taker }; + + template >, bool> = true> + /// Apply the general maker fee defined for this exchange trade fees config on given MonetaryAmount. + /// In other words, convert a gross amount into a net amount with maker fees + MonetaryAmount applyFee(MA ma, FeeType feeType) const { + return ma * (feeType == FeeType::Maker ? maker : taker); + } + + optional_or_t maker; + optional_or_t taker; +}; + +using ExchangeTradeFeesConfigOptional = ExchangeTradeFeesConfig; + +} // namespace details + +using ExchangeTradeFeesConfig = details::ExchangeTradeFeesConfig; + +} // namespace cct::schema \ No newline at end of file diff --git a/src/schema/include/exchange-withdraw-config.hpp b/src/schema/include/exchange-withdraw-config.hpp new file mode 100644 index 00000000..21429204 --- /dev/null +++ b/src/schema/include/exchange-withdraw-config.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "optional-or-type.hpp" + +namespace cct::schema { + +namespace details { + +template +struct ExchangeWithdrawConfig { + template > && !Optional, bool> = true> + void mergeWith(const T &other) { + if (other.validateDepositAddressesInFile) { + validateDepositAddressesInFile = *other.validateDepositAddressesInFile; + } + } + + optional_or_t validateDepositAddressesInFile{}; +}; + +using ExchangeWithdrawConfigOptional = ExchangeWithdrawConfig; + +} // namespace details + +using ExchangeWithdrawConfig = details::ExchangeWithdrawConfig; + +} // namespace cct::schema \ No newline at end of file diff --git a/src/schema/include/optional-or-type.hpp b/src/schema/include/optional-or-type.hpp new file mode 100644 index 00000000..337e05a2 --- /dev/null +++ b/src/schema/include/optional-or-type.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace cct::schema { + +template +struct optional_or { + using type = T; +}; + +template +struct optional_or { + using type = std::optional; +}; + +template +using optional_or_t = optional_or::type; + +} // namespace cct::schema \ No newline at end of file diff --git a/src/schema/src/exchange-config.cpp b/src/schema/src/exchange-config.cpp new file mode 100644 index 00000000..8a23a9be --- /dev/null +++ b/src/schema/src/exchange-config.cpp @@ -0,0 +1,54 @@ +#include "exchange-config.hpp" + +#include + +#include "cct_const.hpp" +#include "cct_log.hpp" +#include "exchange-config-default.hpp" +#include "file.hpp" +#include "loadconfiguration.hpp" +#include "read-json.hpp" +#include "unreachable.hpp" +#include "write-json.hpp" + +namespace cct { + +schema::AllExchangeConfigs ReadExchangeConfigs(const LoadConfiguration &loadConfiguration) { + schema::AllExchangeConfigs allExchangeConfigs; + + switch (loadConfiguration.exchangeConfigFileType()) { + case LoadConfiguration::ExchangeConfigFileType::kProd: { + std::string_view filename = loadConfiguration.exchangeConfigFileName(); + File exchangeConfigFile(loadConfiguration.dataDir(), File::Type::kStatic, filename, File::IfError::kNoThrow); + + string contentStr = exchangeConfigFile.readAll(); + if (contentStr.empty()) { + log::warn("No {} file found. Creating a default one which can be updated freely at your convenience", + kExchangeConfigFileName); + + auto allExchangesConfigOptional = + ReadJsonOrThrow(ExchangeConfigDefault::kProd); + WriteJsonOrThrow(allExchangesConfigOptional); + + allExchangeConfigs.mergeWith(allExchangesConfigOptional); + + } else { + auto allExchangesConfigOptional = ReadJsonOrThrow(contentStr); + + allExchangeConfigs.mergeWith(allExchangesConfigOptional); + } + } + case LoadConfiguration::ExchangeConfigFileType::kTest: { + auto allExchangesConfigOptional = + ReadJsonOrThrow(ExchangeConfigDefault::kTest); + + allExchangeConfigs.mergeWith(allExchangesConfigOptional); + } + default: + unreachable(); + } + + return allExchangeConfigs; +} + +} // namespace cct \ No newline at end of file diff --git a/src/schema/test/exchange-config_test.cpp b/src/schema/test/exchange-config_test.cpp new file mode 100644 index 00000000..36992b57 --- /dev/null +++ b/src/schema/test/exchange-config_test.cpp @@ -0,0 +1,199 @@ +#include "exchange-config.hpp" + +#include + +#include "cct_string.hpp" +#include "read-json.hpp" +#include "reader.hpp" + +namespace cct::schema { + +class ExchangeConfigTest : public ::testing::Test { + protected: + class NominalCase : public Reader { + [[nodiscard]] string readAll() const override { + return R"( +{ + "asset": { + "default": { + "allExclude": [], + "withdrawExclude": [ + "AUD", + "CAD", + "CHF", + "EUR", + "GBP", + "JPY", + "KRW", + "USD" + ], + "preferredPaymentCurrencies": [ + "USDT", + "USDC" + ] + }, + "exchange": { + "binance": { + "allExclude": [ + "BQX" + ] + }, + "kraken": { + "withdrawExclude": [ + "KFEE" + ] + } + } + }, + "query": { + "default": { + "acceptEncoding": "", + "dustAmountsThreshold": [ + "1 EUR", + "1 USD", + "1 USDT", + "1000 KRW", + "0.000001 BTC" + ], + "dustSweeperMaxNbTrades": 7, + "http": { + "timeout": "15s" + }, + "logLevels": { + "requestsCall": "info", + "requestsAnswer": "trace" + }, + "marketDataSerialization": true, + "multiTradeAllowedByDefault": false, + "placeSimulateRealOrder": false, + "trade": { + "minPriceUpdateDuration": "5s", + "strategy": "maker", + "timeout": "30s", + "timeoutMatch": false + }, + "updateFrequency": { + "currencies": "8h", + "markets": "8h", + "withdrawalFees": "4d", + "allOrderbooks": "3s", + "orderbook": "1s", + "tradedVolume": "1h", + "lastPrice": "1s", + "depositWallet": "1min", + "currencyInfo": "4d" + }, + "validateApiKey": false + }, + "exchange": { + "binance": { + "acceptEncoding": "gzip", + "privateAPIRate": "150ms", + "publicAPIRate": "55ms" + }, + "bithumb": { + "privateAPIRate": "8ms", + "publicAPIRate": "8ms" + }, + "huobi": { + "privateAPIRate": "100ms", + "publicAPIRate": "50ms" + }, + "kraken": { + "privateAPIRate": "2000ms", + "publicAPIRate": "500ms" + }, + "kucoin": { + "privateAPIRate": "200ms", + "publicAPIRate": "200ms" + }, + "upbit": { + "privateAPIRate": "350ms", + "publicAPIRate": "100ms" + } + } + }, + "tradeFees": { + "exchange": { + "binance": { + "maker": "0.1", + "taker": "0.1" + }, + "bithumb": { + "maker": "0.25", + "taker": "0.25" + }, + "huobi": { + "maker": "0.2", + "taker": "0.2" + }, + "kraken": { + "maker": "0.16", + "taker": "0.26" + }, + "kucoin": { + "maker": "0.1", + "taker": "0.1" + }, + "upbit": { + "maker": "0.25", + "taker": "0.25" + } + } + }, + "withdraw": { + "default": { + "validateDepositAddressesInFile": true + } + } +} +)"; + } + }; +}; + +TEST_F(ExchangeConfigTest, DirectRead) { + auto exchangeConfigOptional = ReadJsonOrThrow(NominalCase{}); + + EXPECT_EQ(exchangeConfigOptional.asset.def.allExclude.size(), 0); + EXPECT_EQ(exchangeConfigOptional.asset.def.withdrawExclude.size(), 8); + EXPECT_EQ(exchangeConfigOptional.asset.def.preferredPaymentCurrencies.size(), 2); + EXPECT_EQ(exchangeConfigOptional.asset.exchange.size(), 2); + EXPECT_EQ(exchangeConfigOptional.asset.exchange.at("binance").allExclude.size(), 1); + EXPECT_EQ(exchangeConfigOptional.asset.exchange.at("kraken").withdrawExclude.size(), 1); + EXPECT_EQ(exchangeConfigOptional.query.def.acceptEncoding, ""); + EXPECT_EQ(exchangeConfigOptional.query.def.dustAmountsThreshold.size(), 5); + EXPECT_EQ(exchangeConfigOptional.query.def.dustSweeperMaxNbTrades, 7); + EXPECT_EQ(exchangeConfigOptional.query.def.http->timeout->duration, std::chrono::seconds(15)); + EXPECT_EQ(exchangeConfigOptional.query.def.logLevels->requestsCall, "info"); + EXPECT_EQ(exchangeConfigOptional.query.def.logLevels->requestsAnswer, "trace"); + EXPECT_EQ(exchangeConfigOptional.query.def.marketDataSerialization, true); + EXPECT_EQ(exchangeConfigOptional.query.def.multiTradeAllowedByDefault, false); + EXPECT_EQ(exchangeConfigOptional.query.def.placeSimulateRealOrder, false); + EXPECT_EQ(exchangeConfigOptional.query.def.trade->minPriceUpdateDuration->duration, std::chrono::seconds(5)); + EXPECT_EQ(exchangeConfigOptional.query.def.trade->strategy, "maker"); + EXPECT_EQ(exchangeConfigOptional.query.def.trade->timeout->duration, std::chrono::seconds(30)); + EXPECT_EQ(exchangeConfigOptional.query.def.trade->timeoutMatch, false); + EXPECT_EQ(exchangeConfigOptional.query.def.updateFrequency->size(), 9); + EXPECT_EQ(exchangeConfigOptional.query.def.validateApiKey, false); + EXPECT_EQ(exchangeConfigOptional.query.exchange.size(), 6); + EXPECT_EQ(exchangeConfigOptional.query.exchange.at("binance").acceptEncoding, "gzip"); +} + +TEST_F(ExchangeConfigTest, ExchangeValuesShouldOverrideDefault) { + auto exchangeConfigOptional = ReadJsonOrThrow(NominalCase{}); + + schema::AllExchangeConfigs allExchangeConfigs; + + allExchangeConfigs.mergeWith(exchangeConfigOptional); + + EXPECT_EQ(allExchangeConfigs[ExchangeNameEnum::binance].asset.allExclude, CurrencyCodeSet{"BQX"}); + EXPECT_EQ(allExchangeConfigs[ExchangeNameEnum::kraken].asset.withdrawExclude, + CurrencyCodeSet({"AUD", "CAD", "CHF", "EUR", "GBP", "JPY", "KRW", "USD", "KFEE"})); + EXPECT_EQ(allExchangeConfigs[ExchangeNameEnum::binance].query.acceptEncoding, "gzip"); + EXPECT_EQ(allExchangeConfigs[ExchangeNameEnum::binance].query.privateAPIRate.duration, + std::chrono::milliseconds(150)); + EXPECT_EQ(allExchangeConfigs[ExchangeNameEnum::binance].query.publicAPIRate.duration, std::chrono::milliseconds(55)); +} + +} // namespace cct::schema \ No newline at end of file diff --git a/src/trading/common/src/market-trader-engine-state.cpp b/src/trading/common/src/market-trader-engine-state.cpp index a1833ad1..33808efb 100644 --- a/src/trading/common/src/market-trader-engine-state.cpp +++ b/src/trading/common/src/market-trader-engine-state.cpp @@ -69,11 +69,11 @@ void MarketTraderEngineState::countMatchedPart(const ExchangeConfig &exchangeCon TimePoint matchedTime) { switch (matchedOrder.side()) { case TradeSide::kBuy: - _availableBaseAmount += exchangeConfig.applyFee(newMatchedVolume, ExchangeConfig::FeeType::kMaker); + _availableBaseAmount += exchangeConfig.applyFee(newMatchedVolume, ExchangeConfig::FeeType::Maker); break; case TradeSide::kSell: _availableQuoteAmount += - exchangeConfig.applyFee(newMatchedVolume.toNeutral() * price, ExchangeConfig::FeeType::kMaker); + exchangeConfig.applyFee(newMatchedVolume.toNeutral() * price, ExchangeConfig::FeeType::Maker); break; default: throw exception("Unknown trade side {}", static_cast(matchedOrder.side())); diff --git a/src/trading/common/src/market-trader-engine.cpp b/src/trading/common/src/market-trader-engine.cpp index b99d8888..9219ea68 100644 --- a/src/trading/common/src/market-trader-engine.cpp +++ b/src/trading/common/src/market-trader-engine.cpp @@ -230,7 +230,7 @@ void MarketTraderEngine::buy(const MarketOrderBook &marketOrderBook, MonetaryAmo constexpr MonetaryAmount matchedVolume; _marketTraderEngineState.placeBuyOrder(_exchangeConfig, ts, remainingVolume, price, matchedVolume, from, - ExchangeConfig::FeeType::kMaker); + ExchangeConfig::FeeType::Maker); break; } case PriceStrategy::kNibble: { @@ -240,7 +240,7 @@ void MarketTraderEngine::buy(const MarketOrderBook &marketOrderBook, MonetaryAmo const MonetaryAmount remainingVolume = volume - matchedVolume; _marketTraderEngineState.placeBuyOrder(_exchangeConfig, ts, remainingVolume, price, matchedVolume, from, - ExchangeConfig::FeeType::kTaker); + ExchangeConfig::FeeType::Taker); break; } case PriceStrategy::kTaker: { @@ -249,7 +249,7 @@ void MarketTraderEngine::buy(const MarketOrderBook &marketOrderBook, MonetaryAmo constexpr MonetaryAmount remainingVolume; _marketTraderEngineState.placeBuyOrder(_exchangeConfig, ts, remainingVolume, avgPrice, totalMatchedAmount, from, - ExchangeConfig::FeeType::kTaker); + ExchangeConfig::FeeType::Taker); } break; } @@ -266,7 +266,7 @@ void MarketTraderEngine::sell(const MarketOrderBook &marketOrderBook, MonetaryAm constexpr MonetaryAmount matchedVolume; _marketTraderEngineState.placeSellOrder(_exchangeConfig, marketOrderBook.time(), volume, price, matchedVolume, - ExchangeConfig::FeeType::kMaker); + ExchangeConfig::FeeType::Maker); break; } case PriceStrategy::kNibble: { @@ -274,7 +274,7 @@ void MarketTraderEngine::sell(const MarketOrderBook &marketOrderBook, MonetaryAm const MonetaryAmount matchedVolume = std::min(marketOrderBook.amountAtBidPrice(), volume); _marketTraderEngineState.placeSellOrder(_exchangeConfig, marketOrderBook.time(), volume - matchedVolume, price, - matchedVolume, ExchangeConfig::FeeType::kTaker); + matchedVolume, ExchangeConfig::FeeType::Taker); break; } case PriceStrategy::kTaker: { @@ -284,7 +284,7 @@ void MarketTraderEngine::sell(const MarketOrderBook &marketOrderBook, MonetaryAm constexpr MonetaryAmount remainingVolume; _marketTraderEngineState.placeSellOrder(_exchangeConfig, marketOrderBook.time(), remainingVolume, avgPrice, - totalMatchedAmount, ExchangeConfig::FeeType::kTaker); + totalMatchedAmount, ExchangeConfig::FeeType::Taker); } break; }