diff --git a/data/static/auto-trade-example.json b/data/static/auto-trade-example.json new file mode 100644 index 00000000..2f91d19a --- /dev/null +++ b/data/static/auto-trade-example.json @@ -0,0 +1,63 @@ +{ + "kraken": { + "BTC-EUR": { + "accounts": [ + "user1", + "user2" + ], + "algorithmName": "example-trader", + "repeatTime": "5s", + "baseStartAmount": "0.5BTC", + "quoteStartAmount": "50%EUR", + "stopCriteria": [ + { + "type": "duration", + "value": "4h" + }, + { + "type": "protectLoss", + "value": "-30%" + }, + { + "type": "secureProfit", + "value": "80%" + } + ] + }, + "ETH-EUR": { + "accounts": [ + "user1" + ], + "algorithmName": "example-trader", + "repeatTime": "3s", + "baseStartAmount": "45ETH", + "quoteStartAmount": "50%EUR", + "stopCriteria": [ + { + "type": "duration", + "value": "4h" + }, + { + "type": "protectLoss", + "value": "-30%" + }, + { + "type": "secureProfit", + "value": "80%" + } + ] + } + }, + "binance": { + "XRP-USDT": { + "accounts": [ + "user1" + ], + "algorithmName": "example-trader", + "repeatTime": "1s", + "baseStartAmount": "50000.56XRP", + "quoteStartAmount": "100%USDT", + "stopCriteria": [] + } + } +} \ No newline at end of file diff --git a/src/basic-objects/include/coincentercommandtype.hpp b/src/basic-objects/include/coincentercommandtype.hpp index b5c4b398..e5dfc9af 100644 --- a/src/basic-objects/include/coincentercommandtype.hpp +++ b/src/basic-objects/include/coincentercommandtype.hpp @@ -36,6 +36,7 @@ enum class CoincenterCommandType : int8_t { MarketData, Replay, ReplayMarkets, + AutoTrade, Last }; @@ -56,7 +57,7 @@ struct glz::meta<::cct::CoincenterCommandType> { Balance, DepositInfo, OrdersClosed, OrdersOpened, OrdersCancel, RecentDeposits, RecentWithdraws, Trade, Buy, Sell, Withdraw, DustSweeper, - MarketData, Replay, ReplayMarkets, + MarketData, Replay, ReplayMarkets, AutoTrade, Last); }; diff --git a/src/basic-objects/src/coincentercommandtype.cpp b/src/basic-objects/src/coincentercommandtype.cpp index f836bd40..55c51aae 100644 --- a/src/basic-objects/src/coincentercommandtype.cpp +++ b/src/basic-objects/src/coincentercommandtype.cpp @@ -11,9 +11,6 @@ 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) { diff --git a/src/engine/include/auto-trade-options.hpp b/src/engine/include/auto-trade-options.hpp new file mode 100644 index 00000000..20ebdc99 --- /dev/null +++ b/src/engine/include/auto-trade-options.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include "cct_fixedcapacityvector.hpp" +#include "cct_json-container.hpp" +#include "cct_smallvector.hpp" +#include "cct_vector.hpp" +#include "exchange-names.hpp" +#include "exchangename.hpp" +#include "public-exchange-auto-trade-options.hpp" + +namespace cct { + +class AutoTradeOptions { + public: + using AccountAutoTradeOptionsPtrVector = + SmallVector; + + struct MarketExchanges { + Market market; + ExchangeNames privateExchangeNames; + const MarketAutoTradeOptions *pMarketAutoTradeOptions{}; + }; + + using MarketStatusVector = vector; + + struct MarketExchangeOptions { + MarketStatusVector marketStatusVector; + }; + + struct PublicExchangeMarketOptions { + ExchangeName publicExchangeName; + MarketExchangeOptions marketExchangeOptions; + }; + + using PublicExchangeMarketOptionsVector = FixedCapacityVector; + + AutoTradeOptions() noexcept = default; + + explicit AutoTradeOptions(const json::container &data); + + auto begin() const { return _options.begin(); } + auto end() const { return _options.end(); } + + ExchangeNames exchangeNames() const; + + PublicExchangeNameVector publicExchanges() const; + + AccountAutoTradeOptionsPtrVector accountAutoTradeOptionsPtr(std::string_view publicExchangeName) const; + + const PublicExchangeAutoTradeOptions &operator[](const ExchangeName &exchangeName) const; + + private: + std::map _options; +}; + +} // namespace cct \ No newline at end of file diff --git a/src/engine/include/auto-trade-processor.hpp b/src/engine/include/auto-trade-processor.hpp new file mode 100644 index 00000000..ff9d9d06 --- /dev/null +++ b/src/engine/include/auto-trade-processor.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +#include "cct_smallvector.hpp" +#include "cct_vector.hpp" +#include "exchange-names.hpp" +#include "exchange.hpp" +#include "market.hpp" +#include "public-exchange-auto-trade-options.hpp" +#include "timedef.hpp" + +namespace cct { +class AutoTradeOptions; + +class AutoTradeProcessor { + public: + explicit AutoTradeProcessor(const AutoTradeOptions& autoTradeOptions); + + struct SelectedMarket { + ExchangeNames privateExchangeNames; + Market market; + }; + + using SelectedMarketVector = SmallVector; + + SelectedMarketVector computeSelectedMarkets(); + + private: + struct MarketStatus { + ExchangeNames privateExchangeNames; + Market market; + TimePoint lastQueryTime; + const MarketAutoTradeOptions* pMarketAutoTradeOptions{}; + }; + + using MarketStatusVector = vector; + + struct ExchangeStatus { + MarketStatusVector marketStatusVector; + const PublicExchangeAutoTradeOptions* pPublicExchangeAutoTradeOptions{}; + }; + + using ExchangeStatusVector = SmallVector; + + ExchangeStatusVector _exchangeStatusVector; + TimePoint _startTs = Clock::now(); + TimePoint _ts{_startTs}; +}; +} // namespace cct \ No newline at end of file diff --git a/src/engine/include/coincenter.hpp b/src/engine/include/coincenter.hpp index 00a1bd1f..951d337e 100644 --- a/src/engine/include/coincenter.hpp +++ b/src/engine/include/coincenter.hpp @@ -4,6 +4,7 @@ #include #include "apikeysprovider.hpp" +#include "auto-trade-options.hpp" #include "cct_const.hpp" #include "cct_fixedcapacityvector.hpp" #include "coincenterinfo.hpp" @@ -149,6 +150,9 @@ class Coincenter { ReplayResults replay(const AbstractMarketTraderFactory &marketTraderFactory, const ReplayOptions &replayOptions, Market market, ExchangeNameSpan exchangeNames); + /// Run auto trade. + void autoTrade(const AutoTradeOptions &autoTradeOptions); + /// Dumps the content of all file caches in data directory to save cURL queries. void updateFileCaches() const; diff --git a/src/engine/include/coincentercommand.hpp b/src/engine/include/coincentercommand.hpp index db0894d1..0d5c3750 100644 --- a/src/engine/include/coincentercommand.hpp +++ b/src/engine/include/coincentercommand.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -47,6 +48,8 @@ class CoincenterCommand { CoincenterCommand& setReplayOptions(ReplayOptions replayOptions); + CoincenterCommand& setJsonConfigFile(std::string_view jsonConfigFile); + CoincenterCommand& setPercentageAmount(bool value = true); CoincenterCommand& withBalanceInUse(bool value = true); @@ -79,6 +82,8 @@ class CoincenterCommand { const ReplayOptions& replayOptions() const { return std::get(_specialOptions); } + std::string_view getJsonConfigFile() const { return std::get(_specialOptions); } + bool operator==(const CoincenterCommand&) const noexcept = default; using trivially_relocatable = @@ -89,7 +94,7 @@ class CoincenterCommand { private: using SpecialOptions = std::variant; + WithdrawOptions, ReplayOptions, std::string_view>; ExchangeNames _exchangeNames; SpecialOptions _specialOptions; diff --git a/src/engine/include/coincenteroptions.hpp b/src/engine/include/coincenteroptions.hpp index 24b44af3..f64df1db 100644 --- a/src/engine/include/coincenteroptions.hpp +++ b/src/engine/include/coincenteroptions.hpp @@ -104,6 +104,8 @@ class CoincenterCmdLineOptions { std::string_view marketData; + std::string_view autoTrade; + std::optional replay; std::string_view algorithmNames; std::string_view market; diff --git a/src/engine/include/coincenteroptionsdef.hpp b/src/engine/include/coincenteroptionsdef.hpp index aeb4c8e1..6242c413 100644 --- a/src/engine/include/coincenteroptionsdef.hpp +++ b/src/engine/include/coincenteroptionsdef.hpp @@ -476,6 +476,22 @@ struct CoincenterAllowedOptions : private CoincenterCmdLineOptionsDefinitions { "\nNominal replay will not validate input data to optimize performance, use this option to validate data once " "and for all."}, &OptValueType::validateOnly}, + {{{"Automation", 8004}, + "auto-trade", + "", + "Automatic live trading mode. Once you have validated on historical market-data the performance of an " + "algorithm, it's time to try it for real!\n" + "This command has some particularities:\n" + "- next commands will never be executed\n" + "- repeat is ignored (the auto trade will continue until one of terminating signals defined in the " + "configuration file is reached)\n" + "Configuration will be loaded from given json file, with following options (check README to get full " + "configuration schema):\n" + "- 'algorithm' : algorithm name to use\n" + "- 'market' : the market to trade onto\n" + "- 'startAmount' : the starting amount in base currency (can be a percentage of available amount)\n" + "- 'exchange' : exchange with account key (not needed if not ambiguous)"}, + &OptValueType::autoTrade}, {{{"Monitoring", 9000}, "--monitoring", "", diff --git a/src/engine/include/exchangesorchestrator.hpp b/src/engine/include/exchangesorchestrator.hpp index 4ab28249..3be9c378 100644 --- a/src/engine/include/exchangesorchestrator.hpp +++ b/src/engine/include/exchangesorchestrator.hpp @@ -3,6 +3,8 @@ #include #include +#include "auto-trade-options.hpp" +#include "auto-trade-processor.hpp" #include "exchange-names.hpp" #include "exchangename.hpp" #include "exchangeretriever.hpp" diff --git a/src/engine/include/market-auto-trade-options.hpp b/src/engine/include/market-auto-trade-options.hpp new file mode 100644 index 00000000..5b7cb439 --- /dev/null +++ b/src/engine/include/market-auto-trade-options.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +#include "auto-trade-stop-criterion.hpp" +#include "cct_json-container.hpp" +#include "cct_string.hpp" +#include "cct_type_traits.hpp" +#include "cct_vector.hpp" +#include "monetaryamount.hpp" +#include "timedef.hpp" + +namespace cct { + +class MarketAutoTradeOptions { + public: + explicit MarketAutoTradeOptions(const json::container &data); + + std::span accounts() const { return _accounts; } + + std::string_view algorithmName() const { return _algorithmName; } + + Duration repeatTime() const { return _repeatTime; } + + MonetaryAmount baseStartAmount() const { return _baseStartAmount; } + + MonetaryAmount quoteStartAmount() const { return _quoteStartAmount; } + + std::span stopCriterion() const { return _stopCriteria; } + + using trivially_relocatable = is_trivially_relocatable::type; + + private: + vector _accounts; + string _algorithmName; + Duration _repeatTime; + MonetaryAmount _baseStartAmount; + MonetaryAmount _quoteStartAmount; + vector _stopCriteria; +}; + +} // namespace cct \ No newline at end of file diff --git a/src/engine/include/public-exchange-auto-trade-options.hpp b/src/engine/include/public-exchange-auto-trade-options.hpp new file mode 100644 index 00000000..9a08893f --- /dev/null +++ b/src/engine/include/public-exchange-auto-trade-options.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +#include "market-auto-trade-options.hpp" +#include "market.hpp" + +namespace cct { + +using PublicExchangeAutoTradeOptions = std::map; + +} \ No newline at end of file diff --git a/src/engine/src/auto-trade-options.cpp b/src/engine/src/auto-trade-options.cpp new file mode 100644 index 00000000..fc1886dd --- /dev/null +++ b/src/engine/src/auto-trade-options.cpp @@ -0,0 +1,69 @@ +#include "auto-trade-options.hpp" + +#include + +#include "cct_invalid_argument_exception.hpp" +#include "cct_json-container.hpp" + +namespace cct { + +AutoTradeOptions::AutoTradeOptions(const json::container &data) { + for (const auto &[publicExchangeName, marketAutoTradeOptions] : data.items()) { + PublicExchangeAutoTradeOptions publicExchangeAutoTradeOptions; + for (const auto &marketJson : marketAutoTradeOptions.items()) { + publicExchangeAutoTradeOptions.emplace(marketJson.key(), MarketAutoTradeOptions(marketJson.value())); + } + _options.emplace(publicExchangeName, std::move(publicExchangeAutoTradeOptions)); + } +} + +ExchangeNames AutoTradeOptions::exchangeNames() const { + ExchangeNames exchangeNames; + for (const auto &[publicExchangeName, publicExchangeAutoTradeOptions] : _options) { + const int posPublicExchangeName = exchangeNames.size(); + for (const auto &[market, marketAutoTradeOptions] : publicExchangeAutoTradeOptions) { + const int posMarket = exchangeNames.size(); + for (std::string_view account : marketAutoTradeOptions.accounts()) { + ExchangeName exchangeName(publicExchangeName.name(), account); + const auto it = std::find(exchangeNames.begin() + posPublicExchangeName, exchangeNames.end(), exchangeName); + if (it == exchangeNames.end()) { + exchangeNames.push_back(std::move(exchangeName)); + } else if (it >= exchangeNames.begin() + posMarket) { + throw invalid_argument("Duplicated account {} for exchange {}", account, publicExchangeName); + } + } + } + } + return exchangeNames; +} + +PublicExchangeNameVector AutoTradeOptions::publicExchanges() const { + PublicExchangeNameVector exchanges; + for (const auto &[publicExchangeName, _] : _options) { + exchanges.emplace_back(publicExchangeName); + } + std::ranges::sort(exchanges); + return exchanges; +} + +AutoTradeOptions::AccountAutoTradeOptionsPtrVector AutoTradeOptions::accountAutoTradeOptionsPtr( + std::string_view publicExchangeName) const { + AccountAutoTradeOptionsPtrVector accountAutoTradeOptionsPtr; + for (const auto &[exchangeStr, publicExchangeAutoTradeOptions] : _options) { + ExchangeName exchangeName(exchangeStr); + if (exchangeStr.name() == publicExchangeName) { + accountAutoTradeOptionsPtr.emplace_back(&publicExchangeAutoTradeOptions); + } + } + return accountAutoTradeOptionsPtr; +} + +const PublicExchangeAutoTradeOptions &AutoTradeOptions::operator[](const ExchangeName &exchangeName) const { + const auto it = _options.find(exchangeName); + if (it == _options.end()) { + throw exception("No auto trade options for exchange {}", exchangeName); + } + return it->second; +} + +} // namespace cct \ No newline at end of file diff --git a/src/engine/src/auto-trade-processor.cpp b/src/engine/src/auto-trade-processor.cpp new file mode 100644 index 00000000..fdd2d08c --- /dev/null +++ b/src/engine/src/auto-trade-processor.cpp @@ -0,0 +1,101 @@ +#include "auto-trade-processor.hpp" + +#include + +#include "auto-trade-options.hpp" +#include "cct_exception.hpp" +#include "timestring.hpp" + +namespace cct { + +AutoTradeProcessor::AutoTradeProcessor(const AutoTradeOptions &autoTradeOptions) + : _exchangeStatusVector(autoTradeOptions.publicExchanges().size()) { + int publicExchangePos = 0; + for (const auto &[publicExchangeName, publicExchangeAutoTradeOptions] : autoTradeOptions) { + ExchangeStatus &selectedExchangesStatus = _exchangeStatusVector[publicExchangePos]; + selectedExchangesStatus.pPublicExchangeAutoTradeOptions = &publicExchangeAutoTradeOptions; + for (const auto &[market, marketAutoTradeOptions] : publicExchangeAutoTradeOptions) { + MarketStatus &marketStatus = selectedExchangesStatus.marketStatusVector.emplace_back(); + marketStatus.market = market; + marketStatus.pMarketAutoTradeOptions = &marketAutoTradeOptions; + for (std::string_view account : marketAutoTradeOptions.accounts()) { + marketStatus.privateExchangeNames.emplace_back(publicExchangeName.name(), account); + } + } + ++publicExchangePos; + } +} + +namespace { +const MarketAutoTradeOptions &GetMarketAutoTradeOptions( + Market market, const PublicExchangeAutoTradeOptions &publicExchangeAutoTradeOptions) { + const auto it = publicExchangeAutoTradeOptions.find(market); + if (it == publicExchangeAutoTradeOptions.end()) { + throw exception("Should not happen - market not found in account auto trade options"); + } + return it->second; +} + +bool IsQueryTooEarly(TimePoint nowTs, const auto &marketStatus, + const PublicExchangeAutoTradeOptions &publicExchangeAutoTradeOptions) { + const auto &marketAutoTradeOptions = GetMarketAutoTradeOptions(marketStatus.market, publicExchangeAutoTradeOptions); + return marketStatus.lastQueryTime + marketAutoTradeOptions.repeatTime() > nowTs; +} +} // namespace + +AutoTradeProcessor::SelectedMarketVector AutoTradeProcessor::computeSelectedMarkets() { + SelectedMarketVector selectedMarketVector; + + auto ts = Clock::now(); + + TimePoint earliestQueryTime = TimePoint::max(); + + for (ExchangeStatus &exchangeStatus : _exchangeStatusVector) { + const PublicExchangeAutoTradeOptions &publicExchangeAutoTradeOptions = + *exchangeStatus.pPublicExchangeAutoTradeOptions; + + auto &marketStatusVector = exchangeStatus.marketStatusVector; + + if (marketStatusVector.empty()) { + continue; + } + + // Sort markets by ascending last query time, discarding those (placed at the end of the vector) which cannot be + // queried right now + std::ranges::sort(marketStatusVector, + [ts, &publicExchangeAutoTradeOptions](const MarketStatus &lhs, const MarketStatus &rhs) { + const bool lhsIsTooEarly = IsQueryTooEarly(ts, lhs, publicExchangeAutoTradeOptions); + const bool rhsIsTooEarly = IsQueryTooEarly(ts, rhs, publicExchangeAutoTradeOptions); + + if (lhsIsTooEarly != rhsIsTooEarly) { + return !lhsIsTooEarly; + } + + return lhs.lastQueryTime < rhs.lastQueryTime; + }); + + MarketStatus &selectedMarketStatus = marketStatusVector.front(); + if (IsQueryTooEarly(ts, selectedMarketStatus, publicExchangeAutoTradeOptions)) { + const auto repeatTime = + GetMarketAutoTradeOptions(selectedMarketStatus.market, publicExchangeAutoTradeOptions).repeatTime(); + earliestQueryTime = std::min(earliestQueryTime, selectedMarketStatus.lastQueryTime + repeatTime); + continue; + } + + selectedMarketStatus.lastQueryTime = ts; + selectedMarketVector.emplace_back(selectedMarketStatus.privateExchangeNames, selectedMarketStatus.market); + } + + if (selectedMarketVector.empty() && earliestQueryTime != TimePoint::max()) { + log::debug("Sleeping until {}", TimeToString(earliestQueryTime)); + std::this_thread::sleep_until(earliestQueryTime + std::chrono::milliseconds(1)); + selectedMarketVector = computeSelectedMarkets(); + if (selectedMarketVector.empty()) { + throw exception("Waiting sufficient time should return at least one market for the next turn"); + } + } + + return selectedMarketVector; +} + +} // namespace cct \ No newline at end of file diff --git a/src/engine/src/coincenter-commands-processor.cpp b/src/engine/src/coincenter-commands-processor.cpp index 933cd9da..8eb5dab5 100644 --- a/src/engine/src/coincenter-commands-processor.cpp +++ b/src/engine/src/coincenter-commands-processor.cpp @@ -6,6 +6,7 @@ #include #include +#include "auto-trade-options.hpp" #include "balanceoptions.hpp" #include "cct_const.hpp" #include "cct_exception.hpp" @@ -22,6 +23,7 @@ #include "exchange-names.hpp" #include "exchangename.hpp" #include "exchangepublicapi.hpp" +#include "file.hpp" #include "market-trader-factory.hpp" #include "market.hpp" #include "monetaryamount.hpp" @@ -292,18 +294,18 @@ TransferableCommandResultVector CoincenterCommandsProcessor::processGroupedComma break; } case CoincenterCommandType::MarketData: { - std::array marketPerPublicExchange; + std::array marketPerPublicExchangePos; for (const auto &cmd : groupedCommands) { if (cmd.exchangeNames().empty()) { - std::ranges::fill(marketPerPublicExchange, cmd.market()); + std::ranges::fill(marketPerPublicExchangePos, cmd.market()); } else { for (const auto &exchangeName : cmd.exchangeNames()) { - marketPerPublicExchange[exchangeName.publicExchangePos()] = cmd.market(); + marketPerPublicExchangePos[exchangeName.publicExchangePos()] = cmd.market(); } } } - // No return value here, this command is made only for storing purposes. - _coincenter.queryMarketDataPerExchange(marketPerPublicExchange); + // No need to retrieve the returned value here, this command is made only for storing purposes. + _coincenter.queryMarketDataPerExchange(marketPerPublicExchangePos); break; } case CoincenterCommandType::Replay: { @@ -327,6 +329,13 @@ TransferableCommandResultVector CoincenterCommandsProcessor::processGroupedComma _queryResultPrinter.printMarketsForReplay(firstCmd.replayOptions().timeWindow(), marketTimestampSetsPerExchange); break; } + case CoincenterCommandType::AutoTrade: { + const File configFile(firstCmd.getJsonConfigFile(), File::IfError::kThrow); + const AutoTradeOptions autoTradeOptions(configFile.readAllJson()); + + _coincenter.autoTrade(autoTradeOptions); + break; + } default: throw exception("Unknown command type"); } diff --git a/src/engine/src/coincenter.cpp b/src/engine/src/coincenter.cpp index 24f6f4fc..1ec0cfaa 100644 --- a/src/engine/src/coincenter.cpp +++ b/src/engine/src/coincenter.cpp @@ -9,6 +9,7 @@ #include "abstract-market-trader-factory.hpp" #include "algorithm-name-iterator.hpp" +#include "auto-trade-processor.hpp" #include "balanceoptions.hpp" #include "cct_const.hpp" #include "cct_log.hpp" @@ -29,6 +30,7 @@ #include "query-result-type-helpers.hpp" #include "queryresulttypes.hpp" #include "replay-options.hpp" +#include "signal-handler.hpp" #include "time-window.hpp" #include "timedef.hpp" #include "withdrawsconstraints.hpp" @@ -336,6 +338,29 @@ ReplayResults Coincenter::replay(const AbstractMarketTraderFactory &marketTrader return replayResults; } +void Coincenter::autoTrade(const AutoTradeOptions &autoTradeOptions) { + AutoTradeProcessor autoTradeProcessor(autoTradeOptions); + + while (!IsStopRequested()) { + // 1: select exchanges positions for which we are allowed to send a request. + AutoTradeProcessor::SelectedMarketVector selectedMarkets = autoTradeProcessor.computeSelectedMarkets(); + if (selectedMarkets.empty()) { + break; + } + + // 2: Query order books for those exchanges + std::array selectedMarketsPerPublicExchangePos; + for (const AutoTradeProcessor::SelectedMarket &selectedMarket : selectedMarkets) { + selectedMarketsPerPublicExchangePos[selectedMarket.privateExchangeNames.front().publicExchangePos()] = + selectedMarket.market; + } + MarketDataPerExchange marketDataPerExchange = queryMarketDataPerExchange(selectedMarketsPerPublicExchangePos); + + // 3: call algorithms and retrieve their actions + // 4: perform actual actions (Trades, cancel, exit criteria) + } +} + MarketTradingGlobalResultPerExchange Coincenter::replayAlgorithm( const AbstractMarketTraderFactory &marketTraderFactory, std::string_view algorithmName, const ReplayOptions &replayOptions, std::span marketTraderEngines, diff --git a/src/engine/src/coincentercommand.cpp b/src/engine/src/coincentercommand.cpp index 0f1dfba2..4e59e3ff 100644 --- a/src/engine/src/coincentercommand.cpp +++ b/src/engine/src/coincentercommand.cpp @@ -131,4 +131,9 @@ CoincenterCommand& CoincenterCommand::setReplayOptions(ReplayOptions replayOptio return *this; } +CoincenterCommand& CoincenterCommand::setJsonConfigFile(std::string_view jsonConfigFile) { + _specialOptions = jsonConfigFile; + return *this; +} + } // namespace cct diff --git a/src/engine/src/coincentercommands.cpp b/src/engine/src/coincentercommands.cpp index aaa9934e..4c061b5b 100644 --- a/src/engine/src/coincentercommands.cpp +++ b/src/engine/src/coincentercommands.cpp @@ -242,6 +242,10 @@ void CoincenterCommands::addOption(const CoincenterCmdLineOptions &cmdLineOption .setExchangeNames(optionParser.parseExchanges()); } + if (!cmdLineOptions.autoTrade.empty()) { + _commands.emplace_back(CoincenterCommandType::AutoTrade).setJsonConfigFile(cmdLineOptions.autoTrade); + } + optionParser.checkEndParsing(); // No more option part should be remaining } diff --git a/src/engine/src/exchangesorchestrator.cpp b/src/engine/src/exchangesorchestrator.cpp index ffd174c7..b913955f 100644 --- a/src/engine/src/exchangesorchestrator.cpp +++ b/src/engine/src/exchangesorchestrator.cpp @@ -12,6 +12,7 @@ #include #include +#include "auto-trade-processor.hpp" #include "balanceoptions.hpp" #include "balanceportfolio.hpp" #include "cct_const.hpp" diff --git a/src/engine/src/market-auto-trade-options.cpp b/src/engine/src/market-auto-trade-options.cpp new file mode 100644 index 00000000..8041294e --- /dev/null +++ b/src/engine/src/market-auto-trade-options.cpp @@ -0,0 +1,49 @@ +#include "market-auto-trade-options.hpp" + +#include "cct_invalid_argument_exception.hpp" +#include "cct_json-container.hpp" +#include "durationstring.hpp" + +namespace cct { + +namespace { +auto GetFieldOrThrow(const json::container &data, std::string_view fieldName) { + const auto it = data.find(fieldName); + if (it == data.end()) { + throw invalid_argument("Expected field '{}' in auto trade configuration {}", fieldName, data.dump()); + } + return it; +} +} // namespace + +MarketAutoTradeOptions::MarketAutoTradeOptions(const json::container &data) + : _accounts(), + _algorithmName(GetFieldOrThrow(data, "algorithmName")->get()), + _repeatTime(ParseDuration(GetFieldOrThrow(data, "repeatTime")->get())), + _baseStartAmount(GetFieldOrThrow(data, "baseStartAmount")->get()), + _quoteStartAmount(GetFieldOrThrow(data, "quoteStartAmount")->get()), + _stopCriteria() { + const auto accountsIt = GetFieldOrThrow(data, "accounts"); + if (!accountsIt->is_array()) { + throw invalid_argument("Expected 'accounts' field to be an array in auto trade configuration {}", data.dump()); + } + if (accountsIt->empty()) { + throw invalid_argument("Expected 'accounts' field to be non empty in auto trade configuration {}", data.dump()); + } + _accounts.reserve(accountsIt->size()); + std::ranges::transform(*accountsIt, std::back_inserter(_accounts), [](const json::container &accountJson) { + return string(accountJson.get()); + }); + + const auto stopCritJsonIt = GetFieldOrThrow(data, "stopCriteria"); + if (!stopCritJsonIt->is_array()) { + throw invalid_argument("Expected 'stopCriteria' field to be an array in auto trade configuration {}", data.dump()); + } + _stopCriteria.reserve(stopCritJsonIt->size()); + std::ranges::transform(*stopCritJsonIt, std::back_inserter(_stopCriteria), [](const json::container &stopCrit) { + return AutoTradeStopCriterion(GetFieldOrThrow(stopCrit, "type")->get(), + GetFieldOrThrow(stopCrit, "value")->get()); + }); +} + +} // namespace cct \ No newline at end of file diff --git a/src/objects/include/auto-trade-stop-criterion.hpp b/src/objects/include/auto-trade-stop-criterion.hpp new file mode 100644 index 00000000..e617366c --- /dev/null +++ b/src/objects/include/auto-trade-stop-criterion.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +#include "timedef.hpp" + +namespace cct { + +class AutoTradeStopCriterion { + public: + enum class Type : int8_t { kDuration, kProtectLoss, kSecureProfit }; + + AutoTradeStopCriterion(std::string_view typeStr, std::string_view valueStr); + + Duration duration() const { return std::get(_value); } + + int maxEvolutionPercentage() const { return std::get(_value); } + + Type type() const { return _type; } + + private: + using Value = std::variant; + + Type _type; + Value _value; +}; + +} // namespace cct \ No newline at end of file diff --git a/src/objects/src/auto-trade-stop-criterion.cpp b/src/objects/src/auto-trade-stop-criterion.cpp new file mode 100644 index 00000000..94147288 --- /dev/null +++ b/src/objects/src/auto-trade-stop-criterion.cpp @@ -0,0 +1,47 @@ +#include "auto-trade-stop-criterion.hpp" + +#include "cct_invalid_argument_exception.hpp" +#include "durationstring.hpp" +#include "stringconv.hpp" + +namespace cct { + +namespace { +auto TypeFromStr(std::string_view typeStr) { + if (typeStr == "duration") { + return AutoTradeStopCriterion::Type::kDuration; + } + if (typeStr == "protectLoss") { + return AutoTradeStopCriterion::Type::kProtectLoss; + } + if (typeStr == "secureProfit") { + return AutoTradeStopCriterion::Type::kSecureProfit; + } + throw invalid_argument("Unknown stop criterion type {}", typeStr); +} + +auto PercentageIntFromStr(std::string_view valueStr) { + const std::string_view integralStr = valueStr.substr(0, valueStr.find('%')); + return StringToIntegral(integralStr); +} + +} // namespace + +AutoTradeStopCriterion::AutoTradeStopCriterion(std::string_view typeStr, std::string_view valueStr) + : _type(TypeFromStr(typeStr)), _value() { + switch (_type) { + case Type::kDuration: + _value = ParseDuration(valueStr); + break; + case Type::kProtectLoss: + [[fallthrough]]; + case Type::kSecureProfit: + _value = PercentageIntFromStr(valueStr); + break; + default: { + throw invalid_argument("Unknown stop criterion type {}", static_cast(_type)); + } + } +} + +} // namespace cct \ No newline at end of file