diff --git a/data/static/auto-trade-example.json b/data/static/auto-trade-example.json new file mode 100644 index 00000000..50a648a8 --- /dev/null +++ b/data/static/auto-trade-example.json @@ -0,0 +1,53 @@ +{ + "kraken": { + "BTC-EUR": { + "algorithmName": "example-trader", + "repeatTime": "3s", + "baseStartAmount": "0.5BTC", + "quoteStartAmount": "50%EUR", + "stopCriteria": [ + { + "type": "duration", + "value": "4h" + }, + { + "type": "protectLoss", + "value": "-30%" + }, + { + "type": "secureProfit", + "value": "80%" + } + ] + }, + "ETH-EUR": { + "algorithmName": "example-trader", + "repeatTime": "3s", + "baseStartAmount": "45ETH", + "quoteStartAmount": "50%EUR", + "stopCriteria": [ + { + "type": "duration", + "value": "4h" + }, + { + "type": "protectLoss", + "value": "-30%" + }, + { + "type": "secureProfit", + "value": "80%" + } + ] + } + }, + "binance_user1": { + "XRP-USDT": { + "algorithmName": "example-trader", + "repeatTime": "1s", + "baseStartAmount": "50000.56XRP", + "quoteStartAmount": "100%USDT", + "stopCriteria": [] + } + } +} \ No newline at end of file diff --git a/src/engine/include/account-auto-trade-options.hpp b/src/engine/include/account-auto-trade-options.hpp new file mode 100644 index 00000000..bc257ac3 --- /dev/null +++ b/src/engine/include/account-auto-trade-options.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +#include "market-auto-trade-options.hpp" +#include "market.hpp" + +namespace cct { + +using AccountAutoTradeOptions = std::map; + +} \ No newline at end of file diff --git a/src/engine/include/auto-trade-options.hpp b/src/engine/include/auto-trade-options.hpp new file mode 100644 index 00000000..c7e2ebeb --- /dev/null +++ b/src/engine/include/auto-trade-options.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include "account-auto-trade-options.hpp" +#include "cct_json.hpp" +#include "cct_smallvector.hpp" +#include "exchange-names.hpp" +#include "exchangename.hpp" + +namespace cct { + +class AutoTradeOptions { + public: + using AccountAutoTradeOptionsPtrVector = SmallVector; + + AutoTradeOptions() noexcept = default; + + explicit AutoTradeOptions(const json &data); + + auto size() const noexcept { return _options.size(); } + + PublicExchangeNameVector getExchanges() const; + + AccountAutoTradeOptionsPtrVector getAccountAutoTradeOptionsPtr(std::string_view publicExchangeName) 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..73b1a0ff --- /dev/null +++ b/src/engine/include/auto-trade-processor.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "threadpool.hpp" + +namespace cct { +class AutoTradeOptions; + +class AutoTradeProcessor { + public: + explicit AutoTradeProcessor(const AutoTradeOptions& autoTradeOptions); + + void start(); + + private: + const AutoTradeOptions& _autoTradeOptions; + ThreadPool _threadPool; +}; +} // namespace cct \ No newline at end of file diff --git a/src/engine/include/coincenter.hpp b/src/engine/include/coincenter.hpp index 605d269d..7a92857e 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 4498a709..b60da86e 100644 --- a/src/engine/include/exchangesorchestrator.hpp +++ b/src/engine/include/exchangesorchestrator.hpp @@ -3,6 +3,7 @@ #include #include +#include "auto-trade-options.hpp" #include "exchange-names.hpp" #include "exchangename.hpp" #include "exchangeretriever.hpp" @@ -106,6 +107,8 @@ class ExchangesOrchestrator { std::span marketTraderEngines, MarketTradeRangeStatsPerExchange &&tradeRangeStatsPerExchange, ExchangeNameSpan exchangeNames); + void autoTrade(const AutoTradeOptions &autoTradeOptions); + private: ExchangeRetriever _exchangeRetriever; ThreadPool _threadPool; 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..0921288f --- /dev/null +++ b/src/engine/include/market-auto-trade-options.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +#include "auto-trade-stop-criterion.hpp" +#include "cct_json.hpp" +#include "cct_string.hpp" +#include "cct_vector.hpp" +#include "monetaryamount.hpp" +#include "timedef.hpp" + +namespace cct { + +class MarketAutoTradeOptions { + public: + explicit MarketAutoTradeOptions(const json &data); + + 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; } + + private: + string _algorithmName; + Duration _repeatTime; + MonetaryAmount _baseStartAmount; + MonetaryAmount _quoteStartAmount; + vector _stopCriteria; +}; + +} // namespace cct \ 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..54a22d92 --- /dev/null +++ b/src/engine/src/auto-trade-options.cpp @@ -0,0 +1,40 @@ +#include "auto-trade-options.hpp" + +namespace cct { + +AutoTradeOptions::AutoTradeOptions(const json &data) { + for (const auto &[exchangeName, marketAutoTradeOptions] : data.items()) { + AccountAutoTradeOptions accountAutoTradeOptions; + for (const auto &marketJson : marketAutoTradeOptions.items()) { + accountAutoTradeOptions.emplace(marketJson.key(), MarketAutoTradeOptions(marketJson.value())); + } + _options.emplace(exchangeName, std::move(accountAutoTradeOptions)); + } +} + +PublicExchangeNameVector AutoTradeOptions::getExchanges() const { + PublicExchangeNameVector exchanges; + for (const auto &[exchangeStr, _] : _options) { + ExchangeName exchangeName(exchangeStr); + std::string_view exchangeNameStr = exchangeName.name(); + // It's possible because std::map keys are lexicographically ordered + if (exchanges.empty() || exchanges.back().name() != exchangeNameStr) { + exchanges.emplace_back(exchangeNameStr); + } + } + return exchanges; +} + +AutoTradeOptions::AccountAutoTradeOptionsPtrVector AutoTradeOptions::getAccountAutoTradeOptionsPtr( + std::string_view publicExchangeName) const { + AccountAutoTradeOptionsPtrVector accountAutoTradeOptionsPtr; + for (const auto &[exchangeStr, accountAutoTradeOptions] : _options) { + ExchangeName exchangeName(exchangeStr); + if (exchangeStr.name() == publicExchangeName) { + accountAutoTradeOptionsPtr.emplace_back(&accountAutoTradeOptions); + } + } + return accountAutoTradeOptionsPtr; +} + +} // 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..233501e4 --- /dev/null +++ b/src/engine/src/auto-trade-processor.cpp @@ -0,0 +1,12 @@ +#include "auto-trade-processor.hpp" + +#include "auto-trade-options.hpp" + +namespace cct { + +AutoTradeProcessor::AutoTradeProcessor(const AutoTradeOptions& autoTradeOptions) + : _autoTradeOptions(autoTradeOptions), _threadPool(autoTradeOptions.size()) {} + +void AutoTradeProcessor::start() {} + +} // 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 e0071411..5c3dfd9d 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" @@ -327,6 +329,13 @@ TransferableCommandResultVector CoincenterCommandsProcessor::processGroupedComma _queryResultPrinter.printMarketsForReplay(firstCmd.replayOptions().timeWindow(), marketTimestampSetsPerExchange); break; } + case CoincenterCommandType::kAutoTrade: { + 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 03747e99..7ec1ce43 100644 --- a/src/engine/src/coincenter.cpp +++ b/src/engine/src/coincenter.cpp @@ -334,6 +334,10 @@ ReplayResults Coincenter::replay(const AbstractMarketTraderFactory &marketTrader return replayResults; } +void Coincenter::autoTrade(const AutoTradeOptions &autoTradeOptions) { + _exchangesOrchestrator.autoTrade(autoTradeOptions); +} + 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 d49fd30b..83a2cdd0 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 f819a993..77192dd3 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::kAutoTrade).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 4afb6dd9..ded24d3b 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" @@ -1071,4 +1072,10 @@ MarketTradingGlobalResultPerExchange ExchangesOrchestrator::getMarketTraderResul return marketTradingGlobalResultPerExchange; } -} // namespace cct +void ExchangesOrchestrator::autoTrade(const AutoTradeOptions &autoTradeOptions) { + AutoTradeProcessor autoTradeProcessor(autoTradeOptions); + + autoTradeProcessor.start(); +} + +} // namespace cct \ No newline at end of file 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..7c0cd2cf --- /dev/null +++ b/src/engine/src/market-auto-trade-options.cpp @@ -0,0 +1,34 @@ +#include "market-auto-trade-options.hpp" + +#include "cct_invalid_argument_exception.hpp" +#include "durationstring.hpp" + +namespace cct { + +namespace { +auto GetStrFieldOrThrow(const json &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->get(); +} +} // namespace + +MarketAutoTradeOptions::MarketAutoTradeOptions(const json &data) + : _algorithmName(GetStrFieldOrThrow(data, "algorithmName")), + _repeatTime(ParseDuration(GetStrFieldOrThrow(data, "repeatTime"))), + _baseStartAmount(GetStrFieldOrThrow(data, "baseStartAmount")), + _quoteStartAmount(GetStrFieldOrThrow(data, "quoteStartAmount")), + _stopCriteria() { + const auto stopCritJsonIt = data.find("stopCriteria"); + if (stopCritJsonIt == data.end()) { + throw invalid_argument("Expected field 'stopCriteria' in auto trade configuration {}", data.dump()); + } + _stopCriteria.reserve(stopCritJsonIt->size()); + std::ranges::transform(*stopCritJsonIt, std::back_inserter(_stopCriteria), [](const json &stopCrit) { + return AutoTradeStopCriterion(GetStrFieldOrThrow(stopCrit, "type"), GetStrFieldOrThrow(stopCrit, "value")); + }); +} + +} // 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/include/coincentercommandtype.hpp b/src/objects/include/coincentercommandtype.hpp index 49967dc6..6c76c2cd 100644 --- a/src/objects/include/coincentercommandtype.hpp +++ b/src/objects/include/coincentercommandtype.hpp @@ -34,6 +34,7 @@ enum class CoincenterCommandType : int8_t { kMarketData, kReplay, kReplayMarkets, + kAutoTrade, kLast }; diff --git a/src/objects/include/file.hpp b/src/objects/include/file.hpp index 85cd88a0..2bda4c92 100644 --- a/src/objects/include/file.hpp +++ b/src/objects/include/file.hpp @@ -15,6 +15,11 @@ class File : public Reader, public Writer { enum class Type : int8_t { kCache, kSecret, kStatic, kLog }; enum class IfError : int8_t { kThrow, kNoThrow }; + /// Creates a File directly from a file path. + File(std::string_view filePath, IfError ifError); + + /// Creates a File from the coincenter data directory, with the type of the file and its name in the main data + /// directory. File(std::string_view dataDir, Type type, std::string_view name, IfError ifError); [[nodiscard]] string readAll() const override; 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 diff --git a/src/objects/src/coincentercommandtype.cpp b/src/objects/src/coincentercommandtype.cpp index bbb481ef..3e8353e8 100644 --- a/src/objects/src/coincentercommandtype.cpp +++ b/src/objects/src/coincentercommandtype.cpp @@ -16,7 +16,8 @@ constexpr std::string_view kCommandTypeNames[] = { "Balance", "DepositInfo", "OrdersClosed", "OrdersOpened", "OrdersCancel", "RecentDeposits", "RecentWithdraws", "Trade", "Buy", "Sell", - "Withdraw", "DustSweeper", "MarketData", "Replay", "ReplayMarkets"}; + "Withdraw", "DustSweeper", "MarketData", "Replay", "ReplayMarkets", + "AutoTrade"}; static_assert(std::size(kCommandTypeNames) == static_cast(CoincenterCommandType::kLast)); } // namespace diff --git a/src/objects/src/file.cpp b/src/objects/src/file.cpp index c35898d1..5ba541cc 100644 --- a/src/objects/src/file.cpp +++ b/src/objects/src/file.cpp @@ -37,6 +37,8 @@ string FullFileName(std::string_view dataDir, std::string_view fileName, File::T } } // namespace +File::File(std::string_view filePath, IfError ifError) : _filePath(filePath), _ifError(ifError) {} + File::File(std::string_view dataDir, Type type, std::string_view name, IfError ifError) : _filePath(FullFileName(dataDir, name, type)), _ifError(ifError) {} @@ -49,7 +51,7 @@ string File::readAll() const { throw exception("Unable to open {} for reading", _filePath); } try { - data = string((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + data = string(std::istreambuf_iterator(file), std::istreambuf_iterator()); } catch (const std::exception& e) { if (_ifError == IfError::kThrow) { throw e;