diff --git a/.github/workflows/ubuntu-clang-tidy.yml b/.github/workflows/ubuntu-clang-tidy.yml index f2d2fbf5..4a7b02ea 100644 --- a/.github/workflows/ubuntu-clang-tidy.yml +++ b/.github/workflows/ubuntu-clang-tidy.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - clang-version: [19] + clang-version: [18] buildmode: [Debug] steps: diff --git a/src/api/exchanges/src/bithumbpublicapi.cpp b/src/api/exchanges/src/bithumbpublicapi.cpp index 0aee9c8a..d34476b2 100644 --- a/src/api/exchanges/src/bithumbpublicapi.cpp +++ b/src/api/exchanges/src/bithumbpublicapi.cpp @@ -15,7 +15,6 @@ #include "apiquerytypeenum.hpp" #include "cachedresult.hpp" -#include "cct_exception.hpp" #include "cct_json.hpp" #include "cct_log.hpp" #include "cct_string.hpp" diff --git a/src/api/exchanges/src/kucoinprivateapi.cpp b/src/api/exchanges/src/kucoinprivateapi.cpp index d8ee0dc5..26cee369 100644 --- a/src/api/exchanges/src/kucoinprivateapi.cpp +++ b/src/api/exchanges/src/kucoinprivateapi.cpp @@ -2,7 +2,6 @@ #include #include -#include #include #include #include diff --git a/src/api/exchanges/src/upbitpublicapi.cpp b/src/api/exchanges/src/upbitpublicapi.cpp index 95947393..15da7ece 100644 --- a/src/api/exchanges/src/upbitpublicapi.cpp +++ b/src/api/exchanges/src/upbitpublicapi.cpp @@ -11,7 +11,6 @@ #include "apiquerytypeenum.hpp" #include "cachedresult.hpp" -#include "cct_exception.hpp" #include "cct_json.hpp" #include "cct_log.hpp" #include "cct_string.hpp" diff --git a/src/engine/include/coincenter-commands-processor.hpp b/src/engine/include/coincenter-commands-processor.hpp new file mode 100644 index 00000000..d6b73d31 --- /dev/null +++ b/src/engine/include/coincenter-commands-processor.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include + +#include "coincentercommand.hpp" +#include "queryresultprinter.hpp" +#include "transferablecommandresult.hpp" + +namespace cct { + +class Coincenter; +class CoincenterInfo; +class CoincenterCommands; + +class CoincenterCommandsProcessor { + public: + explicit CoincenterCommandsProcessor(Coincenter &coincenter); + + /// Launch given commands and return the number of processed commands. + int process(const CoincenterCommands &coincenterCommands); + + private: + TransferableCommandResultVector processGroupedCommands( + std::span groupedCommands, + std::span previousTransferableResults); + + Coincenter &_coincenter; + QueryResultPrinter _queryResultPrinter; +}; +} // namespace cct diff --git a/src/engine/include/coincenter.hpp b/src/engine/include/coincenter.hpp index 3cc68998..605d269d 100644 --- a/src/engine/include/coincenter.hpp +++ b/src/engine/include/coincenter.hpp @@ -17,7 +17,6 @@ #include "market.hpp" #include "metricsexporter.hpp" #include "ordersconstraints.hpp" -#include "queryresultprinter.hpp" #include "queryresulttypes.hpp" #include "replay-options.hpp" #include "transferablecommandresult.hpp" @@ -36,9 +35,6 @@ class Coincenter { Coincenter(const CoincenterInfo &coincenterInfo, const ExchangeSecretsInfo &exchangeSecretsInfo); - /// Launch given commands and return the number of processed commands. - int process(const CoincenterCommands &coincenterCommands); - ExchangeHealthCheckStatus healthCheck(ExchangeNameSpan exchangeNames); /// Retrieve all tradable currencies for given selected public exchanges, or all if empty. @@ -150,8 +146,8 @@ class Coincenter { /// Replay all markets for exchanges selection that has some data during the last /// 'replayDuration' time (so within the time frame [now - replayDuration, now]) - void replay(const AbstractMarketTraderFactory &marketTraderFactory, const ReplayOptions &replayOptions, Market market, - ExchangeNameSpan exchangeNames); + ReplayResults replay(const AbstractMarketTraderFactory &marketTraderFactory, const ReplayOptions &replayOptions, + Market market, ExchangeNameSpan exchangeNames); /// Dumps the content of all file caches in data directory to save cURL queries. void updateFileCaches() const; @@ -168,15 +164,13 @@ class Coincenter { const FiatConverter &fiatConverter() const { return _fiatConverter; } private: - TransferableCommandResultVector processGroupedCommands( - std::span groupedCommands, - std::span previousTransferableResults); - using MarketTraderEngineVector = FixedCapacityVector; - void replayAlgorithm(const AbstractMarketTraderFactory &marketTraderFactory, std::string_view algorithmName, - const ReplayOptions &replayOptions, std::span marketTraderEngines, - const PublicExchangeNameVector &exchangesWithThisMarketData); + MarketTradingGlobalResultPerExchange replayAlgorithm(const AbstractMarketTraderFactory &marketTraderFactory, + std::string_view algorithmName, + const ReplayOptions &replayOptions, + std::span marketTraderEngines, + const PublicExchangeNameVector &exchangesWithThisMarketData); // TODO: may be moved somewhere else? MarketTraderEngineVector createMarketTraderEngines(const ReplayOptions &replayOptions, Market market, @@ -195,6 +189,5 @@ class Coincenter { ExchangePool _exchangePool; ExchangesOrchestrator _exchangesOrchestrator; - QueryResultPrinter _queryResultPrinter; }; } // namespace cct diff --git a/src/engine/include/queryresultprinter.hpp b/src/engine/include/queryresultprinter.hpp index 81e692ac..b0ce8b51 100644 --- a/src/engine/include/queryresultprinter.hpp +++ b/src/engine/include/queryresultprinter.hpp @@ -108,8 +108,7 @@ class QueryResultPrinter { void printMarketsForReplay(TimeWindow timeWindow, const MarketTimestampSetsPerExchange &marketTimestampSetsPerExchange); - void printMarketTradingResults(TimeWindow timeWindow, - const MarketTradingGlobalResultPerExchange &marketTradingResultPerExchange, + void printMarketTradingResults(TimeWindow inputTimeWindow, const ReplayResults &replayResults, CoincenterCommandType commandType) const; private: diff --git a/src/engine/include/queryresulttypes.hpp b/src/engine/include/queryresulttypes.hpp index 5af47573..786ece5f 100644 --- a/src/engine/include/queryresulttypes.hpp +++ b/src/engine/include/queryresulttypes.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -10,6 +11,7 @@ #include "cct_const.hpp" #include "cct_fixedcapacityvector.hpp" #include "cct_smallvector.hpp" +#include "cct_vector.hpp" #include "currencyexchangeflatset.hpp" #include "exchangeprivateapitypes.hpp" #include "exchangepublicapitypes.hpp" @@ -87,4 +89,6 @@ using MarketTradingResultPerExchange = FixedCapacityVector, kNbSupportedExchanges>; +using ReplayResults = std::map>; + } // namespace cct diff --git a/src/engine/src/coincenter-commands-processor.cpp b/src/engine/src/coincenter-commands-processor.cpp new file mode 100644 index 00000000..dd9d2f79 --- /dev/null +++ b/src/engine/src/coincenter-commands-processor.cpp @@ -0,0 +1,355 @@ +#include "coincenter-commands-processor.hpp" + +#include +#include +#include +#include +#include +#include + +#include "balanceoptions.hpp" +#include "cct_const.hpp" +#include "cct_exception.hpp" +#include "cct_invalid_argument_exception.hpp" +#include "cct_log.hpp" +#include "coincenter-commands-iterator.hpp" +#include "coincenter.hpp" +#include "coincentercommand.hpp" +#include "coincentercommands.hpp" +#include "coincentercommandtype.hpp" +#include "coincenterinfo.hpp" +#include "currencycode.hpp" +#include "durationstring.hpp" +#include "exchange-names.hpp" +#include "exchangename.hpp" +#include "exchangepublicapi.hpp" +#include "market-trader-factory.hpp" +#include "market.hpp" +#include "monetaryamount.hpp" +#include "queryresultprinter.hpp" +#include "queryresulttypes.hpp" +#include "replay-options.hpp" +#include "timedef.hpp" +#include "transferablecommandresult.hpp" + +namespace cct { +namespace { + +void FillTradeTransferableCommandResults(const TradeResultPerExchange &tradeResultPerExchange, + TransferableCommandResultVector &transferableResults) { + for (const auto &[exchangePtr, tradeResult] : tradeResultPerExchange) { + if (tradeResult.isComplete()) { + transferableResults.emplace_back(exchangePtr->createExchangeName(), tradeResult.tradedAmounts().to); + } + } +} + +void FillConversionTransferableCommandResults(const MonetaryAmountPerExchange &monetaryAmountPerExchange, + TransferableCommandResultVector &transferableResults) { + for (const auto &[exchangePtr, amount] : monetaryAmountPerExchange) { + transferableResults.emplace_back(exchangePtr->createExchangeName(), amount); + } +} + +volatile sig_atomic_t g_signalStatus = 0; + +} // namespace + +// According to the standard, 'SignalHandler' function should have C linkage: +// https://en.cppreference.com/w/cpp/utility/program/signal +// Thus it's not possible to use a lambda and pass some +// objects to it. This is why for this rare occasion we will rely on a static variable. This solution has been inspired +// by: https://wiki.sei.cmu.edu/confluence/display/cplusplus/MSC54-CPP.+A+signal+handler+must+be+a+plain+old+function +extern "C" void SignalHandler(int sigNum) { + log::warn("Signal {} received, will stop after current request", sigNum); + g_signalStatus = sigNum; + + // Revert to standard signal handler (to allow for standard kill in case program does not react) + std::signal(sigNum, SIG_DFL); +} + +CoincenterCommandsProcessor::CoincenterCommandsProcessor(Coincenter &coincenter) + : _coincenter(coincenter), + _queryResultPrinter(coincenter.coincenterInfo().apiOutputType(), coincenter.coincenterInfo().loggingInfo()) { + // Register the signal handler to gracefully shutdown the main loop for repeated requests. + std::signal(SIGINT, SignalHandler); + std::signal(SIGTERM, SignalHandler); +} + +int CoincenterCommandsProcessor::process(const CoincenterCommands &coincenterCommands) { + const auto commands = coincenterCommands.commands(); + const int nbRepeats = commands.empty() ? 0 : coincenterCommands.repeats(); + const auto repeatTime = coincenterCommands.repeatTime(); + + int nbCommandsProcessed{}; + TimePoint lastCommandTime; + for (int repeatPos{}; repeatPos != nbRepeats && g_signalStatus == 0; ++repeatPos) { + const auto earliestTimeNextCommand = lastCommandTime + repeatTime; + const bool doLog = nbRepeats != 1 && (repeatPos < 100 || repeatPos % 100 == 0); + + lastCommandTime = Clock::now(); + + if (lastCommandTime < earliestTimeNextCommand) { + const auto waitingDuration = earliestTimeNextCommand - lastCommandTime; + + lastCommandTime += waitingDuration; + + if (doLog) { + log::debug("Sleep for {} before next command", DurationToString(waitingDuration)); + } + std::this_thread::sleep_for(waitingDuration); + } + if (doLog) { + if (nbRepeats == -1) { + log::info("Process request {}", repeatPos + 1); + } else { + log::info("Process request {}/{}", repeatPos + 1, nbRepeats); + } + } + TransferableCommandResultVector transferableResults; + CoincenterCommandsIterator commandsIterator(commands); + while (commandsIterator.hasNextCommandGroup()) { + const auto groupedCommands = commandsIterator.nextCommandGroup(); + transferableResults = processGroupedCommands(groupedCommands, transferableResults); + ++nbCommandsProcessed; + } + } + return nbCommandsProcessed; +} + +TransferableCommandResultVector CoincenterCommandsProcessor::processGroupedCommands( + std::span groupedCommands, + std::span previousTransferableResults) { + TransferableCommandResultVector transferableResults; + const auto &firstCmd = groupedCommands.front(); + // All grouped commands have same type - logic to handle multiple commands in a group should be handled per use case + switch (firstCmd.type()) { + case CoincenterCommandType::kHealthCheck: { + const auto healthCheckStatus = _coincenter.healthCheck(firstCmd.exchangeNames()); + _queryResultPrinter.printHealthCheck(healthCheckStatus); + break; + } + case CoincenterCommandType::kCurrencies: { + const auto currenciesPerExchange = _coincenter.getCurrenciesPerExchange(firstCmd.exchangeNames()); + _queryResultPrinter.printCurrencies(currenciesPerExchange); + break; + } + case CoincenterCommandType::kMarkets: { + const auto marketsPerExchange = + _coincenter.getMarketsPerExchange(firstCmd.cur1(), firstCmd.cur2(), firstCmd.exchangeNames()); + _queryResultPrinter.printMarkets(firstCmd.cur1(), firstCmd.cur2(), marketsPerExchange, firstCmd.type()); + break; + } + case CoincenterCommandType::kConversion: { + if (firstCmd.amount().isDefault()) { + std::array startAmountsPerExchangePos; + bool oneSet = false; + for (const auto &transferableResult : previousTransferableResults) { + auto publicExchangePos = transferableResult.targetedExchange().publicExchangePos(); + if (startAmountsPerExchangePos[publicExchangePos].isDefault()) { + startAmountsPerExchangePos[publicExchangePos] = transferableResult.resultedAmount(); + oneSet = true; + } else { + throw invalid_argument( + "Transferable results to conversion should have at most one amount per public exchange"); + } + } + if (!oneSet) { + throw invalid_argument("Missing input amount to convert from"); + } + + const auto conversionPerExchange = + _coincenter.getConversion(startAmountsPerExchangePos, firstCmd.cur1(), firstCmd.exchangeNames()); + _queryResultPrinter.printConversion(startAmountsPerExchangePos, firstCmd.cur1(), conversionPerExchange); + FillConversionTransferableCommandResults(conversionPerExchange, transferableResults); + } else { + const auto conversionPerExchange = + _coincenter.getConversion(firstCmd.amount(), firstCmd.cur1(), firstCmd.exchangeNames()); + _queryResultPrinter.printConversion(firstCmd.amount(), firstCmd.cur1(), conversionPerExchange); + FillConversionTransferableCommandResults(conversionPerExchange, transferableResults); + } + break; + } + case CoincenterCommandType::kConversionPath: { + const auto conversionPathPerExchange = + _coincenter.getConversionPaths(firstCmd.market(), firstCmd.exchangeNames()); + _queryResultPrinter.printConversionPath(firstCmd.market(), conversionPathPerExchange); + break; + } + case CoincenterCommandType::kLastPrice: { + const auto lastPricePerExchange = + _coincenter.getLastPricePerExchange(firstCmd.market(), firstCmd.exchangeNames()); + _queryResultPrinter.printLastPrice(firstCmd.market(), lastPricePerExchange); + break; + } + case CoincenterCommandType::kTicker: { + const auto exchangeTickerMaps = _coincenter.getTickerInformation(firstCmd.exchangeNames()); + _queryResultPrinter.printTickerInformation(exchangeTickerMaps); + break; + } + case CoincenterCommandType::kOrderbook: { + const auto marketOrderBooksConversionRates = _coincenter.getMarketOrderBooks( + firstCmd.market(), firstCmd.exchangeNames(), firstCmd.cur1(), firstCmd.optDepth()); + _queryResultPrinter.printMarketOrderBooks(firstCmd.market(), firstCmd.cur1(), firstCmd.optDepth(), + marketOrderBooksConversionRates); + break; + } + case CoincenterCommandType::kLastTrades: { + const auto lastTradesPerExchange = + _coincenter.getLastTradesPerExchange(firstCmd.market(), firstCmd.exchangeNames(), firstCmd.optDepth()); + _queryResultPrinter.printLastTrades(firstCmd.market(), firstCmd.optDepth(), lastTradesPerExchange); + break; + } + case CoincenterCommandType::kLast24hTradedVolume: { + const auto tradedVolumePerExchange = + _coincenter.getLast24hTradedVolumePerExchange(firstCmd.market(), firstCmd.exchangeNames()); + _queryResultPrinter.printLast24hTradedVolume(firstCmd.market(), tradedVolumePerExchange); + break; + } + case CoincenterCommandType::kWithdrawFees: { + const auto withdrawFeesPerExchange = _coincenter.getWithdrawFees(firstCmd.cur1(), firstCmd.exchangeNames()); + _queryResultPrinter.printWithdrawFees(withdrawFeesPerExchange, firstCmd.cur1()); + break; + } + + case CoincenterCommandType::kBalance: { + const auto amountIncludePolicy = firstCmd.withBalanceInUse() + ? BalanceOptions::AmountIncludePolicy::kWithBalanceInUse + : BalanceOptions::AmountIncludePolicy::kOnlyAvailable; + const BalanceOptions balanceOptions(amountIncludePolicy, firstCmd.cur1()); + const auto balancePerExchange = _coincenter.getBalance(firstCmd.exchangeNames(), balanceOptions); + _queryResultPrinter.printBalance(balancePerExchange, firstCmd.cur1()); + break; + } + case CoincenterCommandType::kDepositInfo: { + const auto walletPerExchange = _coincenter.getDepositInfo(firstCmd.exchangeNames(), firstCmd.cur1()); + _queryResultPrinter.printDepositInfo(firstCmd.cur1(), walletPerExchange); + break; + } + case CoincenterCommandType::kOrdersClosed: { + const auto closedOrdersPerExchange = + _coincenter.getClosedOrders(firstCmd.exchangeNames(), firstCmd.ordersConstraints()); + _queryResultPrinter.printClosedOrders(closedOrdersPerExchange, firstCmd.ordersConstraints()); + break; + } + case CoincenterCommandType::kOrdersOpened: { + const auto openedOrdersPerExchange = + _coincenter.getOpenedOrders(firstCmd.exchangeNames(), firstCmd.ordersConstraints()); + _queryResultPrinter.printOpenedOrders(openedOrdersPerExchange, firstCmd.ordersConstraints()); + break; + } + case CoincenterCommandType::kOrdersCancel: { + const auto nbCancelledOrdersPerExchange = + _coincenter.cancelOrders(firstCmd.exchangeNames(), firstCmd.ordersConstraints()); + _queryResultPrinter.printCancelledOrders(nbCancelledOrdersPerExchange, firstCmd.ordersConstraints()); + break; + } + case CoincenterCommandType::kRecentDeposits: { + const auto depositsPerExchange = + _coincenter.getRecentDeposits(firstCmd.exchangeNames(), firstCmd.withdrawsOrDepositsConstraints()); + _queryResultPrinter.printRecentDeposits(depositsPerExchange, firstCmd.withdrawsOrDepositsConstraints()); + break; + } + case CoincenterCommandType::kRecentWithdraws: { + const auto withdrawsPerExchange = + _coincenter.getRecentWithdraws(firstCmd.exchangeNames(), firstCmd.withdrawsOrDepositsConstraints()); + _queryResultPrinter.printRecentWithdraws(withdrawsPerExchange, firstCmd.withdrawsOrDepositsConstraints()); + break; + } + case CoincenterCommandType::kTrade: { + // 2 input styles are possible: + // - standard full information with an amount to trade, a destination currency and an optional list of exchanges + // where to trade + // - a currency - the destination one, and start amount and exchange(s) should come from previous command result + auto [startAmount, exchangeNames] = ComputeTradeAmountAndExchanges(firstCmd, previousTransferableResults); + if (startAmount.isDefault()) { + break; + } + const auto tradeResultPerExchange = _coincenter.trade(startAmount, firstCmd.isPercentageAmount(), firstCmd.cur1(), + exchangeNames, firstCmd.tradeOptions()); + _queryResultPrinter.printTrades(tradeResultPerExchange, startAmount, firstCmd.isPercentageAmount(), + firstCmd.cur1(), firstCmd.tradeOptions()); + FillTradeTransferableCommandResults(tradeResultPerExchange, transferableResults); + break; + } + case CoincenterCommandType::kBuy: { + const auto tradeResultPerExchange = + _coincenter.smartBuy(firstCmd.amount(), firstCmd.exchangeNames(), firstCmd.tradeOptions()); + _queryResultPrinter.printBuyTrades(tradeResultPerExchange, firstCmd.amount(), firstCmd.tradeOptions()); + FillTradeTransferableCommandResults(tradeResultPerExchange, transferableResults); + break; + } + case CoincenterCommandType::kSell: { + auto [startAmount, exchangeNames] = ComputeTradeAmountAndExchanges(firstCmd, previousTransferableResults); + if (startAmount.isDefault()) { + break; + } + const auto tradeResultPerExchange = + _coincenter.smartSell(startAmount, firstCmd.isPercentageAmount(), exchangeNames, firstCmd.tradeOptions()); + _queryResultPrinter.printSellTrades(tradeResultPerExchange, firstCmd.amount(), firstCmd.isPercentageAmount(), + firstCmd.tradeOptions()); + FillTradeTransferableCommandResults(tradeResultPerExchange, transferableResults); + break; + } + case CoincenterCommandType::kWithdrawApply: { + const auto [grossAmount, exchangeName] = ComputeWithdrawAmount(firstCmd, previousTransferableResults); + if (grossAmount.isDefault()) { + break; + } + const auto deliveredWithdrawInfoWithExchanges = + _coincenter.withdraw(grossAmount, firstCmd.isPercentageAmount(), exchangeName, + firstCmd.exchangeNames().back(), firstCmd.withdrawOptions()); + _queryResultPrinter.printWithdraw(deliveredWithdrawInfoWithExchanges, firstCmd.isPercentageAmount(), + firstCmd.withdrawOptions()); + transferableResults.emplace_back(deliveredWithdrawInfoWithExchanges.first[1]->createExchangeName(), + deliveredWithdrawInfoWithExchanges.second.receivedAmount()); + break; + } + case CoincenterCommandType::kDustSweeper: { + const auto res = _coincenter.dustSweeper(firstCmd.exchangeNames(), firstCmd.cur1()); + _queryResultPrinter.printDustSweeper(res, firstCmd.cur1()); + break; + } + case CoincenterCommandType::kMarketData: { + std::array marketPerPublicExchange; + for (const auto &cmd : groupedCommands) { + if (cmd.exchangeNames().empty()) { + std::ranges::fill(marketPerPublicExchange, cmd.market()); + } else { + for (const auto &exchangeName : cmd.exchangeNames()) { + marketPerPublicExchange[exchangeName.publicExchangePos()] = cmd.market(); + } + } + } + // No return value here, this command is made only for storing purposes. + _coincenter.queryMarketDataPerExchange(marketPerPublicExchange); + break; + } + case CoincenterCommandType::kReplay: { + /// This implementation of AbstractMarketTraderFactory is only provided as an example. + /// You can extend coincenter library and: + /// - Provide your own algorithms by implementing your own MarketTraderFactory will all your algorithms. + /// - Create your own CommandType that will call coincenter.replay with the same parameters as below, with your + /// own MarketTraderFactory. + MarketTraderFactory marketTraderFactory; + const auto replayResults = _coincenter.replay(marketTraderFactory, firstCmd.replayOptions(), firstCmd.market(), + firstCmd.exchangeNames()); + + _queryResultPrinter.printMarketTradingResults(firstCmd.replayOptions().timeWindow(), replayResults, + CoincenterCommandType::kReplay); + + break; + } + case CoincenterCommandType::kReplayMarkets: { + const auto marketTimestampSetsPerExchange = + _coincenter.getMarketsAvailableForReplay(firstCmd.replayOptions(), firstCmd.exchangeNames()); + _queryResultPrinter.printMarketsForReplay(firstCmd.replayOptions().timeWindow(), marketTimestampSetsPerExchange); + break; + } + default: + throw exception("Unknown command type"); + } + return transferableResults; +} + +} // namespace cct diff --git a/src/engine/src/coincenter.cpp b/src/engine/src/coincenter.cpp index 1cd69896..03747e99 100644 --- a/src/engine/src/coincenter.cpp +++ b/src/engine/src/coincenter.cpp @@ -1,28 +1,20 @@ #include "coincenter.hpp" #include -#include #include #include #include #include -#include #include +#include "abstract-market-trader-factory.hpp" #include "algorithm-name-iterator.hpp" #include "balanceoptions.hpp" #include "cct_const.hpp" -#include "cct_exception.hpp" -#include "cct_invalid_argument_exception.hpp" #include "cct_log.hpp" -#include "coincenter-commands-iterator.hpp" -#include "coincentercommand.hpp" -#include "coincentercommands.hpp" -#include "coincentercommandtype.hpp" #include "coincenterinfo.hpp" #include "currencycode.hpp" #include "depositsconstraints.hpp" -#include "durationstring.hpp" #include "exchange-names.hpp" #include "exchangename.hpp" #include "exchangepublicapi.hpp" @@ -31,54 +23,17 @@ #include "exchangesecretsinfo.hpp" #include "market-timestamp-set.hpp" #include "market-trader-engine.hpp" -#include "market-trader-factory.hpp" #include "market.hpp" #include "monetaryamount.hpp" #include "ordersconstraints.hpp" #include "query-result-type-helpers.hpp" -#include "queryresultprinter.hpp" #include "queryresulttypes.hpp" #include "replay-options.hpp" #include "time-window.hpp" #include "timedef.hpp" -#include "transferablecommandresult.hpp" #include "withdrawsconstraints.hpp" namespace cct { -namespace { - -void FillTradeTransferableCommandResults(const TradeResultPerExchange &tradeResultPerExchange, - TransferableCommandResultVector &transferableResults) { - for (const auto &[exchangePtr, tradeResult] : tradeResultPerExchange) { - if (tradeResult.isComplete()) { - transferableResults.emplace_back(exchangePtr->createExchangeName(), tradeResult.tradedAmounts().to); - } - } -} - -void FillConversionTransferableCommandResults(const MonetaryAmountPerExchange &monetaryAmountPerExchange, - TransferableCommandResultVector &transferableResults) { - for (const auto &[exchangePtr, amount] : monetaryAmountPerExchange) { - transferableResults.emplace_back(exchangePtr->createExchangeName(), amount); - } -} - -volatile sig_atomic_t g_signalStatus = 0; - -} // namespace - -// According to the standard, 'SignalHandler' function should have C linkage: -// https://en.cppreference.com/w/cpp/utility/program/signal -// Thus it's not possible to use a lambda and pass some -// objects to it. This is why for this rare occasion we will rely on a static variable. This solution has been inspired -// by: https://wiki.sei.cmu.edu/confluence/display/cplusplus/MSC54-CPP.+A+signal+handler+must+be+a+plain+old+function -extern "C" void SignalHandler(int sigNum) { - log::warn("Signal {} received, will stop after current request", sigNum); - g_signalStatus = sigNum; - - // Revert to standard signal handler (to allow for standard kill in case program does not react) - std::signal(sigNum, SIG_DFL); -} using UniquePublicSelectedExchanges = ExchangeRetriever::UniquePublicSelectedExchanges; @@ -89,275 +44,7 @@ Coincenter::Coincenter(const CoincenterInfo &coincenterInfo, const ExchangeSecre _apiKeyProvider(coincenterInfo.dataDir(), exchangeSecretsInfo, coincenterInfo.getRunMode()), _metricsExporter(coincenterInfo.metricGatewayPtr()), _exchangePool(coincenterInfo, _fiatConverter, _commonAPI, _apiKeyProvider), - _exchangesOrchestrator(coincenterInfo.requestsConfig(), _exchangePool.exchanges()), - _queryResultPrinter(coincenterInfo.apiOutputType(), _coincenterInfo.loggingInfo()) { - // Register the signal handler to gracefully shutdown the main loop for repeated requests. - std::signal(SIGINT, SignalHandler); - std::signal(SIGTERM, SignalHandler); -} - -int Coincenter::process(const CoincenterCommands &coincenterCommands) { - const auto commands = coincenterCommands.commands(); - const int nbRepeats = commands.empty() ? 0 : coincenterCommands.repeats(); - const auto repeatTime = coincenterCommands.repeatTime(); - - int nbCommandsProcessed = 0; - TimePoint lastCommandTime; - for (int repeatPos = 0; repeatPos != nbRepeats && g_signalStatus == 0; ++repeatPos) { - const auto earliestTimeNextCommand = lastCommandTime + repeatTime; - const bool doLog = nbRepeats != 1 && (repeatPos < 100 || repeatPos % 100 == 0); - - lastCommandTime = Clock::now(); - - if (earliestTimeNextCommand > lastCommandTime) { - const auto waitingDuration = earliestTimeNextCommand - lastCommandTime; - - lastCommandTime += waitingDuration; - - if (doLog) { - log::debug("Sleep for {} before next command", DurationToString(waitingDuration)); - } - std::this_thread::sleep_for(waitingDuration); - } - if (doLog) { - if (nbRepeats == -1) { - log::info("Process request {}", repeatPos + 1); - } else { - log::info("Process request {}/{}", repeatPos + 1, nbRepeats); - } - } - TransferableCommandResultVector transferableResults; - CoincenterCommandsIterator commandsIterator(commands); - while (commandsIterator.hasNextCommandGroup()) { - const auto groupedCommands = commandsIterator.nextCommandGroup(); - transferableResults = processGroupedCommands(groupedCommands, transferableResults); - ++nbCommandsProcessed; - } - } - return nbCommandsProcessed; -} - -TransferableCommandResultVector Coincenter::processGroupedCommands( - std::span groupedCommands, - std::span previousTransferableResults) { - TransferableCommandResultVector transferableResults; - const auto &firstCmd = groupedCommands.front(); - // All grouped commands have same type - logic to handle multiple commands in a group should be handled per use case - switch (firstCmd.type()) { - case CoincenterCommandType::kHealthCheck: { - const auto healthCheckStatus = healthCheck(firstCmd.exchangeNames()); - _queryResultPrinter.printHealthCheck(healthCheckStatus); - break; - } - case CoincenterCommandType::kCurrencies: { - const auto currenciesPerExchange = getCurrenciesPerExchange(firstCmd.exchangeNames()); - _queryResultPrinter.printCurrencies(currenciesPerExchange); - break; - } - case CoincenterCommandType::kMarkets: { - const auto marketsPerExchange = getMarketsPerExchange(firstCmd.cur1(), firstCmd.cur2(), firstCmd.exchangeNames()); - _queryResultPrinter.printMarkets(firstCmd.cur1(), firstCmd.cur2(), marketsPerExchange, firstCmd.type()); - break; - } - case CoincenterCommandType::kConversion: { - if (firstCmd.amount().isDefault()) { - std::array startAmountsPerExchangePos; - bool oneSet = false; - for (const auto &transferableResult : previousTransferableResults) { - auto publicExchangePos = transferableResult.targetedExchange().publicExchangePos(); - if (startAmountsPerExchangePos[publicExchangePos].isDefault()) { - startAmountsPerExchangePos[publicExchangePos] = transferableResult.resultedAmount(); - oneSet = true; - } else { - throw invalid_argument( - "Transferable results to conversion should have at most one amount per public exchange"); - } - } - if (!oneSet) { - throw invalid_argument("Missing input amount to convert from"); - } - - const auto conversionPerExchange = - getConversion(startAmountsPerExchangePos, firstCmd.cur1(), firstCmd.exchangeNames()); - _queryResultPrinter.printConversion(startAmountsPerExchangePos, firstCmd.cur1(), conversionPerExchange); - FillConversionTransferableCommandResults(conversionPerExchange, transferableResults); - } else { - const auto conversionPerExchange = getConversion(firstCmd.amount(), firstCmd.cur1(), firstCmd.exchangeNames()); - _queryResultPrinter.printConversion(firstCmd.amount(), firstCmd.cur1(), conversionPerExchange); - FillConversionTransferableCommandResults(conversionPerExchange, transferableResults); - } - break; - } - case CoincenterCommandType::kConversionPath: { - const auto conversionPathPerExchange = getConversionPaths(firstCmd.market(), firstCmd.exchangeNames()); - _queryResultPrinter.printConversionPath(firstCmd.market(), conversionPathPerExchange); - break; - } - case CoincenterCommandType::kLastPrice: { - const auto lastPricePerExchange = getLastPricePerExchange(firstCmd.market(), firstCmd.exchangeNames()); - _queryResultPrinter.printLastPrice(firstCmd.market(), lastPricePerExchange); - break; - } - case CoincenterCommandType::kTicker: { - const auto exchangeTickerMaps = getTickerInformation(firstCmd.exchangeNames()); - _queryResultPrinter.printTickerInformation(exchangeTickerMaps); - break; - } - case CoincenterCommandType::kOrderbook: { - const auto marketOrderBooksConversionRates = - getMarketOrderBooks(firstCmd.market(), firstCmd.exchangeNames(), firstCmd.cur1(), firstCmd.optDepth()); - _queryResultPrinter.printMarketOrderBooks(firstCmd.market(), firstCmd.cur1(), firstCmd.optDepth(), - marketOrderBooksConversionRates); - break; - } - case CoincenterCommandType::kLastTrades: { - const auto lastTradesPerExchange = - getLastTradesPerExchange(firstCmd.market(), firstCmd.exchangeNames(), firstCmd.optDepth()); - _queryResultPrinter.printLastTrades(firstCmd.market(), firstCmd.optDepth(), lastTradesPerExchange); - break; - } - case CoincenterCommandType::kLast24hTradedVolume: { - const auto tradedVolumePerExchange = - getLast24hTradedVolumePerExchange(firstCmd.market(), firstCmd.exchangeNames()); - _queryResultPrinter.printLast24hTradedVolume(firstCmd.market(), tradedVolumePerExchange); - break; - } - case CoincenterCommandType::kWithdrawFees: { - const auto withdrawFeesPerExchange = getWithdrawFees(firstCmd.cur1(), firstCmd.exchangeNames()); - _queryResultPrinter.printWithdrawFees(withdrawFeesPerExchange, firstCmd.cur1()); - break; - } - - case CoincenterCommandType::kBalance: { - const auto amountIncludePolicy = firstCmd.withBalanceInUse() - ? BalanceOptions::AmountIncludePolicy::kWithBalanceInUse - : BalanceOptions::AmountIncludePolicy::kOnlyAvailable; - const BalanceOptions balanceOptions(amountIncludePolicy, firstCmd.cur1()); - const auto balancePerExchange = getBalance(firstCmd.exchangeNames(), balanceOptions); - _queryResultPrinter.printBalance(balancePerExchange, firstCmd.cur1()); - break; - } - case CoincenterCommandType::kDepositInfo: { - const auto walletPerExchange = getDepositInfo(firstCmd.exchangeNames(), firstCmd.cur1()); - _queryResultPrinter.printDepositInfo(firstCmd.cur1(), walletPerExchange); - break; - } - case CoincenterCommandType::kOrdersClosed: { - const auto closedOrdersPerExchange = getClosedOrders(firstCmd.exchangeNames(), firstCmd.ordersConstraints()); - _queryResultPrinter.printClosedOrders(closedOrdersPerExchange, firstCmd.ordersConstraints()); - break; - } - case CoincenterCommandType::kOrdersOpened: { - const auto openedOrdersPerExchange = getOpenedOrders(firstCmd.exchangeNames(), firstCmd.ordersConstraints()); - _queryResultPrinter.printOpenedOrders(openedOrdersPerExchange, firstCmd.ordersConstraints()); - break; - } - case CoincenterCommandType::kOrdersCancel: { - const auto nbCancelledOrdersPerExchange = cancelOrders(firstCmd.exchangeNames(), firstCmd.ordersConstraints()); - _queryResultPrinter.printCancelledOrders(nbCancelledOrdersPerExchange, firstCmd.ordersConstraints()); - break; - } - case CoincenterCommandType::kRecentDeposits: { - const auto depositsPerExchange = - getRecentDeposits(firstCmd.exchangeNames(), firstCmd.withdrawsOrDepositsConstraints()); - _queryResultPrinter.printRecentDeposits(depositsPerExchange, firstCmd.withdrawsOrDepositsConstraints()); - break; - } - case CoincenterCommandType::kRecentWithdraws: { - const auto withdrawsPerExchange = - getRecentWithdraws(firstCmd.exchangeNames(), firstCmd.withdrawsOrDepositsConstraints()); - _queryResultPrinter.printRecentWithdraws(withdrawsPerExchange, firstCmd.withdrawsOrDepositsConstraints()); - break; - } - case CoincenterCommandType::kTrade: { - // 2 input styles are possible: - // - standard full information with an amount to trade, a destination currency and an optional list of exchanges - // where to trade - // - a currency - the destination one, and start amount and exchange(s) should come from previous command result - auto [startAmount, exchangeNames] = ComputeTradeAmountAndExchanges(firstCmd, previousTransferableResults); - if (startAmount.isDefault()) { - break; - } - const auto tradeResultPerExchange = - trade(startAmount, firstCmd.isPercentageAmount(), firstCmd.cur1(), exchangeNames, firstCmd.tradeOptions()); - _queryResultPrinter.printTrades(tradeResultPerExchange, startAmount, firstCmd.isPercentageAmount(), - firstCmd.cur1(), firstCmd.tradeOptions()); - FillTradeTransferableCommandResults(tradeResultPerExchange, transferableResults); - break; - } - case CoincenterCommandType::kBuy: { - const auto tradeResultPerExchange = - smartBuy(firstCmd.amount(), firstCmd.exchangeNames(), firstCmd.tradeOptions()); - _queryResultPrinter.printBuyTrades(tradeResultPerExchange, firstCmd.amount(), firstCmd.tradeOptions()); - FillTradeTransferableCommandResults(tradeResultPerExchange, transferableResults); - break; - } - case CoincenterCommandType::kSell: { - auto [startAmount, exchangeNames] = ComputeTradeAmountAndExchanges(firstCmd, previousTransferableResults); - if (startAmount.isDefault()) { - break; - } - const auto tradeResultPerExchange = - smartSell(startAmount, firstCmd.isPercentageAmount(), exchangeNames, firstCmd.tradeOptions()); - _queryResultPrinter.printSellTrades(tradeResultPerExchange, firstCmd.amount(), firstCmd.isPercentageAmount(), - firstCmd.tradeOptions()); - FillTradeTransferableCommandResults(tradeResultPerExchange, transferableResults); - break; - } - case CoincenterCommandType::kWithdrawApply: { - const auto [grossAmount, exchangeName] = ComputeWithdrawAmount(firstCmd, previousTransferableResults); - if (grossAmount.isDefault()) { - break; - } - const auto deliveredWithdrawInfoWithExchanges = - withdraw(grossAmount, firstCmd.isPercentageAmount(), exchangeName, firstCmd.exchangeNames().back(), - firstCmd.withdrawOptions()); - _queryResultPrinter.printWithdraw(deliveredWithdrawInfoWithExchanges, firstCmd.isPercentageAmount(), - firstCmd.withdrawOptions()); - transferableResults.emplace_back(deliveredWithdrawInfoWithExchanges.first[1]->createExchangeName(), - deliveredWithdrawInfoWithExchanges.second.receivedAmount()); - break; - } - case CoincenterCommandType::kDustSweeper: { - _queryResultPrinter.printDustSweeper(dustSweeper(firstCmd.exchangeNames(), firstCmd.cur1()), firstCmd.cur1()); - break; - } - case CoincenterCommandType::kMarketData: { - std::array marketPerPublicExchange; - for (const auto &cmd : groupedCommands) { - if (cmd.exchangeNames().empty()) { - std::ranges::fill(marketPerPublicExchange, cmd.market()); - } else { - for (const auto &exchangeName : cmd.exchangeNames()) { - marketPerPublicExchange[exchangeName.publicExchangePos()] = cmd.market(); - } - } - } - // No return value here, this command is made only for storing purposes. - queryMarketDataPerExchange(marketPerPublicExchange); - break; - } - case CoincenterCommandType::kReplay: { - /// This implementation of AbstractMarketTraderFactory is only provided as an example. - /// You can extend coincenter library and: - /// - Provide your own algorithms by implementing your own MarketTraderFactory will all your algorithms. - /// - Create your own CommandType that will call coincenter.replay with the same parameters as below, with your - /// own MarketTraderFactory. - MarketTraderFactory marketTraderFactory; - replay(marketTraderFactory, firstCmd.replayOptions(), firstCmd.market(), firstCmd.exchangeNames()); - break; - } - case CoincenterCommandType::kReplayMarkets: { - const auto marketTimestampSetsPerExchange = - getMarketsAvailableForReplay(firstCmd.replayOptions(), firstCmd.exchangeNames()); - _queryResultPrinter.printMarketsForReplay(firstCmd.replayOptions().timeWindow(), marketTimestampSetsPerExchange); - break; - } - default: - throw exception("Unknown command type"); - } - return transferableResults; -} + _exchangesOrchestrator(coincenterInfo.requestsConfig(), _exchangePool.exchanges()) {} ExchangeHealthCheckStatus Coincenter::healthCheck(ExchangeNameSpan exchangeNames) { const auto ret = _exchangesOrchestrator.healthCheck(exchangeNames); @@ -578,12 +265,10 @@ void CreateAndRegisterTraderAlgorithms(const AbstractMarketTraderFactory &market } bool Filter(Market market, MarketTimestampSet &marketTimestampSet) { - auto it = std::partition_point(marketTimestampSet.begin(), marketTimestampSet.end(), - [market](const auto &marketTimestamp) { return marketTimestamp.market < market; }); + auto it = std::ranges::partition_point( + marketTimestampSet, [market](const auto &marketTimestamp) { return marketTimestamp.market < market; }); if (it != marketTimestampSet.end() && it->market == market) { - auto marketTimestamp = *it; - marketTimestampSet.clear(); - marketTimestampSet.insert(marketTimestamp); + marketTimestampSet = MarketTimestampSet{*it}; return false; } @@ -607,8 +292,8 @@ void Filter(Market market, MarketTimestampSetsPerExchange &marketTimestampSetsPe } // namespace -void Coincenter::replay(const AbstractMarketTraderFactory &marketTraderFactory, const ReplayOptions &replayOptions, - Market market, ExchangeNameSpan exchangeNames) { +ReplayResults Coincenter::replay(const AbstractMarketTraderFactory &marketTraderFactory, + const ReplayOptions &replayOptions, Market market, ExchangeNameSpan exchangeNames) { const TimeWindow timeWindow = replayOptions.timeWindow(); auto marketTimestampSetsPerExchange = _exchangesOrchestrator.pullAvailableMarketsForReplay(timeWindow, exchangeNames); @@ -616,7 +301,9 @@ void Coincenter::replay(const AbstractMarketTraderFactory &marketTraderFactory, Filter(market, marketTimestampSetsPerExchange); } - MarketSet allMarkets = ComputeAllMarkets(marketTimestampSetsPerExchange); + const MarketSet allMarkets = ComputeAllMarkets(marketTimestampSetsPerExchange); + + ReplayResults replayResults; AlgorithmNameIterator replayAlgorithmNameIterator(replayOptions.algorithmNames(), marketTraderFactory.allSupportedAlgorithms()); @@ -624,6 +311,10 @@ void Coincenter::replay(const AbstractMarketTraderFactory &marketTraderFactory, while (replayAlgorithmNameIterator.hasNext()) { std::string_view algorithmName = replayAlgorithmNameIterator.next(); + ReplayResults::mapped_type algorithmResults; + + algorithmResults.reserve(allMarkets.size()); + for (const Market replayMarket : allMarkets) { auto exchangesWithThisMarketData = CreateExchangeNameVector(replayMarket, marketTimestampSetsPerExchange); @@ -631,48 +322,46 @@ void Coincenter::replay(const AbstractMarketTraderFactory &marketTraderFactory, // trade auto marketTraderEngines = createMarketTraderEngines(replayOptions, replayMarket, exchangesWithThisMarketData); - replayAlgorithm(marketTraderFactory, algorithmName, replayOptions, marketTraderEngines, - exchangesWithThisMarketData); + MarketTradingGlobalResultPerExchange marketTradingResultPerExchange = replayAlgorithm( + marketTraderFactory, algorithmName, replayOptions, marketTraderEngines, exchangesWithThisMarketData); + + algorithmResults.push_back(std::move(marketTradingResultPerExchange)); } + + replayResults.insert({algorithmName, std::move(algorithmResults)}); } + + return replayResults; } -void Coincenter::replayAlgorithm(const AbstractMarketTraderFactory &marketTraderFactory, std::string_view algorithmName, - const ReplayOptions &replayOptions, std::span marketTraderEngines, - const PublicExchangeNameVector &exchangesWithThisMarketData) { +MarketTradingGlobalResultPerExchange Coincenter::replayAlgorithm( + const AbstractMarketTraderFactory &marketTraderFactory, std::string_view algorithmName, + const ReplayOptions &replayOptions, std::span marketTraderEngines, + const PublicExchangeNameVector &exchangesWithThisMarketData) { CreateAndRegisterTraderAlgorithms(marketTraderFactory, algorithmName, marketTraderEngines); MarketTradeRangeStatsPerExchange tradeRangeStatsPerExchange = tradingProcess(replayOptions, marketTraderEngines, exchangesWithThisMarketData); - // Finally retrieve and print results for this market - MarketTradingGlobalResultPerExchange marketTradingResultPerExchange = - _exchangesOrchestrator.getMarketTraderResultPerExchange( - marketTraderEngines, std::move(tradeRangeStatsPerExchange), exchangesWithThisMarketData); - - _queryResultPrinter.printMarketTradingResults(replayOptions.timeWindow(), marketTradingResultPerExchange, - CoincenterCommandType::kReplay); + return _exchangesOrchestrator.getMarketTraderResultPerExchange( + marketTraderEngines, std::move(tradeRangeStatsPerExchange), exchangesWithThisMarketData); } namespace { MonetaryAmount ComputeStartAmount(CurrencyCode currencyCode, MonetaryAmount convertedAmount) { - MonetaryAmount startAmount = convertedAmount; - - if (startAmount.currencyCode() != currencyCode) { + if (convertedAmount.currencyCode() != currencyCode) { // This is possible as conversion may use equivalent fiats and stable coins log::info("Target converted currency is different from market one, replace with market currency {} -> {}", - startAmount.currencyCode(), currencyCode); - startAmount = MonetaryAmount(startAmount.amount(), currencyCode, startAmount.nbDecimals()); + convertedAmount.currencyCode(), currencyCode); + return {convertedAmount.amount(), currencyCode, convertedAmount.nbDecimals()}; } - return startAmount; + return convertedAmount; } } // namespace Coincenter::MarketTraderEngineVector Coincenter::createMarketTraderEngines( const ReplayOptions &replayOptions, Market market, PublicExchangeNameVector &exchangesWithThisMarketData) { - auto nbExchanges = exchangesWithThisMarketData.size(); - const auto &automationConfig = _coincenterInfo.generalConfig().tradingConfig().automationConfig(); const auto startBaseAmountEquivalent = automationConfig.startBaseAmountEquivalent(); const auto startQuoteAmountEquivalent = automationConfig.startQuoteAmountEquivalent(); @@ -686,7 +375,8 @@ Coincenter::MarketTraderEngineVector Coincenter::createMarketTraderEngines( : getConversion(startQuoteAmountEquivalent, market.quote(), exchangesWithThisMarketData); MarketTraderEngineVector marketTraderEngines; - for (decltype(nbExchanges) exchangePos = 0; exchangePos < nbExchanges; ++exchangePos) { + for (PublicExchangeNameVector::size_type exchangePos{}; exchangePos < exchangesWithThisMarketData.size(); + ++exchangePos) { const auto startBaseAmount = isValidateOnly ? MonetaryAmount{0, market.base()} : ComputeStartAmount(market.base(), convertedBaseAmountPerExchange[exchangePos].second); @@ -701,7 +391,6 @@ Coincenter::MarketTraderEngineVector Coincenter::createMarketTraderEngines( convertedBaseAmountPerExchange.erase(convertedBaseAmountPerExchange.begin() + exchangePos); convertedQuoteAmountPerExchange.erase(convertedQuoteAmountPerExchange.begin() + exchangePos); --exchangePos; - --nbExchanges; continue; } @@ -724,23 +413,20 @@ MarketTradeRangeStatsPerExchange Coincenter::tradingProcess(const ReplayOptions // Main loop - parallelized by exchange, with time window chunks of loadChunkDuration - TimeWindow subTimeWindow(timeWindow.from(), loadChunkDuration); - while (subTimeWindow.overlaps(timeWindow)) { + for (TimeWindow subTimeWindow(timeWindow.from(), loadChunkDuration); subTimeWindow.overlaps(timeWindow); + subTimeWindow += loadChunkDuration) { auto subRangeResultsPerExchange = _exchangesOrchestrator.traderConsumeRange( replayOptions, subTimeWindow, marketTraderEngines, exchangesWithThisMarketData); if (tradeRangeResultsPerExchange.empty()) { tradeRangeResultsPerExchange = std::move(subRangeResultsPerExchange); } else { - int pos{}; + int pos{}; // TODO: we can use std::views::enumerate from C++23 when available for (auto &[exchange, result] : subRangeResultsPerExchange) { tradeRangeResultsPerExchange[pos].second += result; ++pos; } } - - // Go to next sub time window - subTimeWindow = TimeWindow(subTimeWindow.to(), loadChunkDuration); } return tradeRangeResultsPerExchange; diff --git a/src/engine/src/exchangesorchestrator.cpp b/src/engine/src/exchangesorchestrator.cpp index cc309f66..4afb6dd9 100644 --- a/src/engine/src/exchangesorchestrator.cpp +++ b/src/engine/src/exchangesorchestrator.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include diff --git a/src/engine/src/queryresultprinter.cpp b/src/engine/src/queryresultprinter.cpp index e92a79dc..8438ae83 100644 --- a/src/engine/src/queryresultprinter.cpp +++ b/src/engine/src/queryresultprinter.cpp @@ -44,6 +44,7 @@ #include "stringconv.hpp" #include "time-window.hpp" #include "timestring.hpp" +#include "trade-range-stats.hpp" #include "tradedamounts.hpp" #include "tradedefinitions.hpp" #include "tradeside.hpp" @@ -767,52 +768,78 @@ json DustSweeperJson(const TradedAmountsVectorWithFinalAmountPerExchange &traded return ToJson(CoincenterCommandType::kDustSweeper, std::move(in), std::move(out)); } -json MarketTradingResultsJson(TimeWindow timeWindow, - const MarketTradingGlobalResultPerExchange &marketTradingResultPerExchange, +json MarketTradingResultsJson(TimeWindow inputTimeWindow, const ReplayResults &replayResults, CoincenterCommandType commandType) { - json in; json inOpt; - inOpt.emplace("time-window", timeWindow.str()); + + json timeStats; + timeStats.emplace("from", TimeToString(inputTimeWindow.from())); + timeStats.emplace("to", TimeToString(inputTimeWindow.to())); + + inOpt.emplace("time", std::move(timeStats)); + + json in; in.emplace("opt", std::move(inOpt)); json out = json::object(); - for (const auto &[exchangePtr, marketGlobalTradingResult] : marketTradingResultPerExchange) { - const auto &marketTradingResult = marketGlobalTradingResult.result; - const auto &stats = marketGlobalTradingResult.stats; + for (const auto &[algorithmName, marketTradingResultPerExchangeVector] : replayResults) { + json algorithmNameResults = json::array_t(); + for (const auto &marketTradingResultPerExchange : marketTradingResultPerExchangeVector) { + json allResults = json::array_t(); + for (const auto &[exchangePtr, marketGlobalTradingResult] : marketTradingResultPerExchange) { + const auto &marketTradingResult = marketGlobalTradingResult.result; + const auto &stats = marketGlobalTradingResult.stats; - json startAmounts; - startAmounts.emplace("base", marketTradingResult.startBaseAmount().str()); - startAmounts.emplace("quote", marketTradingResult.startQuoteAmount().str()); + const auto computeTradeRangeResultsStats = [](const TradeRangeResultsStats &tradeRangeResultsStats) { + json stats; + stats.emplace("nb-successful", tradeRangeResultsStats.nbSuccessful); + stats.emplace("nb-error", tradeRangeResultsStats.nbError); - json orderBookStats; - orderBookStats.emplace("nb-successful", stats.marketOrderBookStats.nbSuccessful); - orderBookStats.emplace("nb-error", stats.marketOrderBookStats.nbError); + json timeStats; + timeStats.emplace("from", TimeToString(tradeRangeResultsStats.timeWindow.from())); + timeStats.emplace("to", TimeToString(tradeRangeResultsStats.timeWindow.to())); - json tradeStats; - tradeStats.emplace("nb-successful", stats.publicTradeStats.nbSuccessful); - tradeStats.emplace("nb-error", stats.publicTradeStats.nbError); + stats.emplace("time", std::move(timeStats)); + return stats; + }; - json jsonStats; - jsonStats.emplace("order-books", std::move(orderBookStats)); - jsonStats.emplace("trades", std::move(tradeStats)); + json startAmounts; + startAmounts.emplace("base", marketTradingResult.startBaseAmount().str()); + startAmounts.emplace("quote", marketTradingResult.startQuoteAmount().str()); - json marketTradingResultJson; - marketTradingResultJson.emplace("algorithm", marketTradingResult.algorithmName()); - marketTradingResultJson.emplace("market", marketTradingResult.market().str()); - marketTradingResultJson.emplace("start-amounts", std::move(startAmounts)); - marketTradingResultJson.emplace("profit-and-loss", marketTradingResult.quoteAmountDelta().str()); - marketTradingResultJson.emplace("stats", std::move(jsonStats)); + json orderBookStats = computeTradeRangeResultsStats(stats.marketOrderBookStats); - json closedOrdersArray = json::array_t(); + json tradeStats = computeTradeRangeResultsStats(stats.publicTradeStats); - for (const ClosedOrder &closedOrder : marketTradingResult.matchedOrders()) { - closedOrdersArray.push_back(OrderJson(closedOrder)); - } + json jsonStats; + jsonStats.emplace("order-books", std::move(orderBookStats)); + jsonStats.emplace("trades", std::move(tradeStats)); - marketTradingResultJson.emplace("matched-orders", std::move(closedOrdersArray)); + json marketTradingResultJson; + marketTradingResultJson.emplace("algorithm", marketTradingResult.algorithmName()); + marketTradingResultJson.emplace("market", marketTradingResult.market().str()); + marketTradingResultJson.emplace("start-amounts", std::move(startAmounts)); + marketTradingResultJson.emplace("profit-and-loss", marketTradingResult.quoteAmountDelta().str()); + marketTradingResultJson.emplace("stats", std::move(jsonStats)); - out.emplace(exchangePtr->name(), std::move(marketTradingResultJson)); + json closedOrdersArray = json::array_t(); + + for (const ClosedOrder &closedOrder : marketTradingResult.matchedOrders()) { + closedOrdersArray.push_back(OrderJson(closedOrder)); + } + + marketTradingResultJson.emplace("matched-orders", std::move(closedOrdersArray)); + + json exchangeMarketResults; + exchangeMarketResults.emplace(exchangePtr->name(), std::move(marketTradingResultJson)); + + allResults.push_back(std::move(exchangeMarketResults)); + } + algorithmNameResults.push_back(std::move(allResults)); + } + + out.emplace(algorithmName, std::move(algorithmNameResults)); } return ToJson(commandType, std::move(in), std::move(out)); @@ -1602,56 +1629,61 @@ void QueryResultPrinter::printMarketsForReplay(TimeWindow timeWindow, logActivity(CoincenterCommandType::kReplayMarkets, jsonData); } -void QueryResultPrinter::printMarketTradingResults( - TimeWindow timeWindow, const MarketTradingGlobalResultPerExchange &marketTradingResultPerExchange, - CoincenterCommandType commandType) const { - json jsonData = MarketTradingResultsJson(timeWindow, marketTradingResultPerExchange, commandType); +void QueryResultPrinter::printMarketTradingResults(TimeWindow inputTimeWindow, const ReplayResults &replayResults, + CoincenterCommandType commandType) const { + json jsonData = MarketTradingResultsJson(inputTimeWindow, replayResults, commandType); switch (_apiOutputType) { case ApiOutputType::kFormattedTable: { SimpleTable table; - table.reserve(1U + marketTradingResultPerExchange.size()); - table.emplace_back("Exchange", "Time window", "Market", "Algorithm", "Start amounts", "Profit / Loss", + table.emplace_back("Algorithm", "Exchange", "Time window", "Market", "Start amounts", "Profit / Loss", "Matched orders", "Stats"); - for (const auto &[exchangePtr, marketGlobalTradingResults] : marketTradingResultPerExchange) { - const auto &marketTradingResults = marketGlobalTradingResults.result; - const auto &stats = marketGlobalTradingResults.stats; - - table::Cell trades; - for (const ClosedOrder &closedOrder : marketTradingResults.matchedOrders()) { - string orderStr = closedOrder.placedTimeStr(); - orderStr.append(" - "); - orderStr.append(closedOrder.sideStr()); - orderStr.append(" - "); - orderStr.append(closedOrder.matchedVolume().str()); - orderStr.append(" @ "); - orderStr.append(closedOrder.price().str()); - trades.emplace_back(std::move(orderStr)); - } + for (const auto &[algorithmName, marketTradingResultPerExchangeVector] : replayResults) { + for (const auto &marketTradingResultPerExchange : marketTradingResultPerExchangeVector) { + for (const auto &[exchangePtr, marketGlobalTradingResults] : marketTradingResultPerExchange) { + const auto &marketTradingResults = marketGlobalTradingResults.result; + const auto &stats = marketGlobalTradingResults.stats; + + table::Cell trades; + for (const ClosedOrder &closedOrder : marketTradingResults.matchedOrders()) { + string orderStr = closedOrder.placedTimeStr(); + orderStr.append(" - "); + orderStr.append(closedOrder.sideStr()); + orderStr.append(" - "); + orderStr.append(closedOrder.matchedVolume().str()); + orderStr.append(" @ "); + orderStr.append(closedOrder.price().str()); + trades.emplace_back(std::move(orderStr)); + } - string orderBookStats("order books: "); - orderBookStats.append(std::string_view(IntegralToCharVector(stats.marketOrderBookStats.nbSuccessful))); - orderBookStats.append(" OK"); - if (stats.marketOrderBookStats.nbError != 0) { - orderBookStats.append(", "); - orderBookStats.append(std::string_view(IntegralToCharVector(stats.marketOrderBookStats.nbError))); - orderBookStats.append(" KO"); - } + string orderBookStats("order books: "); + orderBookStats.append(std::string_view(IntegralToCharVector(stats.marketOrderBookStats.nbSuccessful))); + orderBookStats.append(" OK"); + if (stats.marketOrderBookStats.nbError != 0) { + orderBookStats.append(", "); + orderBookStats.append(std::string_view(IntegralToCharVector(stats.marketOrderBookStats.nbError))); + orderBookStats.append(" KO"); + } - string tradesStats("trades: "); - tradesStats.append(std::string_view(IntegralToCharVector(stats.publicTradeStats.nbSuccessful))); - tradesStats.append(" OK"); - if (stats.publicTradeStats.nbError != 0) { - tradesStats.append(", "); - tradesStats.append(std::string_view(IntegralToCharVector(stats.publicTradeStats.nbError))); - tradesStats.append(" KO"); - } + string tradesStats("trades: "); + tradesStats.append(std::string_view(IntegralToCharVector(stats.publicTradeStats.nbSuccessful))); + tradesStats.append(" OK"); + if (stats.publicTradeStats.nbError != 0) { + tradesStats.append(", "); + tradesStats.append(std::string_view(IntegralToCharVector(stats.publicTradeStats.nbError))); + tradesStats.append(" KO"); + } + + const TimeWindow marketTimeWindow = stats.marketOrderBookStats.timeWindow; - table.emplace_back( - exchangePtr->name(), table::Cell{TimeToString(timeWindow.from()), TimeToString(timeWindow.to())}, - marketTradingResults.market().str(), marketTradingResults.algorithmName(), - table::Cell{marketTradingResults.startBaseAmount().str(), marketTradingResults.startQuoteAmount().str()}, - marketTradingResults.quoteAmountDelta().str(), std::move(trades), - table::Cell{std::move(orderBookStats), std::move(tradesStats)}); + table.emplace_back(marketTradingResults.algorithmName(), exchangePtr->name(), + table::Cell{TimeToString(marketTimeWindow.from()), TimeToString(marketTimeWindow.to())}, + marketTradingResults.market().str(), + table::Cell{marketTradingResults.startBaseAmount().str(), + marketTradingResults.startQuoteAmount().str()}, + marketTradingResults.quoteAmountDelta().str(), std::move(trades), + table::Cell{std::move(orderBookStats), std::move(tradesStats)}); + } + } } printTable(table); break; diff --git a/src/engine/test/queryresultprinter_public_test.cpp b/src/engine/test/queryresultprinter_public_test.cpp index 81d18d6f..3dcb92d6 100644 --- a/src/engine/test/queryresultprinter_public_test.cpp +++ b/src/engine/test/queryresultprinter_public_test.cpp @@ -1334,152 +1334,459 @@ TEST_F(QueryResultPrinterReplayMarketsTest, NoPrint) { class QueryResultPrinterReplayTest : public QueryResultPrinterReplayBaseTest { protected: - ClosedOrder closedOrder1{"1", MonetaryAmount(15, "BTC", 1), MonetaryAmount(35000, "USDT"), tp1, tp1, TradeSide::kBuy}; - ClosedOrder closedOrder2{"2", MonetaryAmount(25, "BTC", 1), MonetaryAmount(45000, "USDT"), tp2, tp2, TradeSide::kBuy}; - ClosedOrder closedOrder3{"3", MonetaryAmount(5, "BTC", 2), MonetaryAmount(35000, "USDT"), tp3, tp4, TradeSide::kSell}; - ClosedOrder closedOrder4{ - "4", MonetaryAmount(17, "BTC", 1), MonetaryAmount(50000, "USDT"), tp3, tp4, TradeSide::kSell}; - ClosedOrder closedOrder5{ - "5", MonetaryAmount(36, "BTC", 3), MonetaryAmount(47899, "USDT"), tp4, tp5, TradeSide::kSell}; - - std::string_view algorithmName = "test-algo"; - MonetaryAmount startBaseAmount{1, "BTC"}; - MonetaryAmount startQuoteAmount{1000, "EUR"}; - - MarketTradingResult marketTradingResult1{algorithmName, startBaseAmount, startQuoteAmount, MonetaryAmount{0, "EUR"}, + CurrencyCode base{"BTC"}; + CurrencyCode quote{"USDT"}; + ClosedOrder closedOrder1{"1", MonetaryAmount(15, base, 1), MonetaryAmount(35000, quote), tp1, tp1, TradeSide::kBuy}; + ClosedOrder closedOrder2{"2", MonetaryAmount(25, base, 1), MonetaryAmount(45000, quote), tp2, tp2, TradeSide::kBuy}; + ClosedOrder closedOrder3{"3", MonetaryAmount(5, base, 2), MonetaryAmount(35000, quote), tp3, tp4, TradeSide::kSell}; + ClosedOrder closedOrder4{"4", MonetaryAmount(17, base, 1), MonetaryAmount(50000, quote), tp3, tp4, TradeSide::kSell}; + ClosedOrder closedOrder5{"5", MonetaryAmount(36, base, 3), MonetaryAmount(47899, quote), tp4, tp5, TradeSide::kSell}; + + MonetaryAmount startBaseAmount{1, base}; + MonetaryAmount startQuoteAmount{1000, quote}; + + std::string_view alg1Name{"first-alg"}; + std::string_view alg2Name{"second-alg"}; + + MarketTradingResult marketTradingResult1{alg1Name, startBaseAmount, startQuoteAmount, MonetaryAmount{0, quote}, ClosedOrderVector{}}; - MarketTradingResult marketTradingResult3{algorithmName, startBaseAmount, startQuoteAmount, MonetaryAmount{500, "EUR"}, + MarketTradingResult marketTradingResult2{alg1Name, startBaseAmount, startQuoteAmount, MonetaryAmount{-334, quote}, + ClosedOrderVector{closedOrder1, closedOrder3}}; + MarketTradingResult marketTradingResult3{alg2Name, startBaseAmount, startQuoteAmount, MonetaryAmount{500, quote}, ClosedOrderVector{closedOrder1, closedOrder5}}; - MarketTradingResult marketTradingResult4{algorithmName, startBaseAmount, startQuoteAmount, MonetaryAmount{780, "EUR"}, + MarketTradingResult marketTradingResult4{alg2Name, startBaseAmount, startQuoteAmount, MonetaryAmount{780, quote}, ClosedOrderVector{closedOrder2, closedOrder3, closedOrder4}}; - TradeRangeStats tradeRangeStats1{TradeRangeResultsStats{42, 0}, TradeRangeResultsStats{3, 10}}; - TradeRangeStats tradeRangeStats3{TradeRangeResultsStats{500000, 2}, TradeRangeResultsStats{0, 0}}; - TradeRangeStats tradeRangeStats4{TradeRangeResultsStats{79009, 0}, TradeRangeResultsStats{1555555555, 45}}; + TradeRangeStats tradeRangeStats1{TradeRangeResultsStats{TimeWindow{tp1, tp1}, 42, 0}, + TradeRangeResultsStats{TimeWindow{tp1, tp2}, 3, 10}}; + TradeRangeStats tradeRangeStats2{TradeRangeResultsStats{TimeWindow{tp1, tp1}, 23, 1}, + TradeRangeResultsStats{TimeWindow{tp1, tp5}, 0, 10}}; + TradeRangeStats tradeRangeStats3{TradeRangeResultsStats{TimeWindow{tp1, tp2}, 500000, 2}, + TradeRangeResultsStats{TimeWindow{tp1, tp3}, 0, 0}}; + TradeRangeStats tradeRangeStats4{TradeRangeResultsStats{TimeWindow{tp1, tp3}, 79009, 0}, + TradeRangeResultsStats{TimeWindow{tp2, tp4}, 1555555555, 45}}; - MarketTradingGlobalResultPerExchange marketTradingResultPerExchange{ + MarketTradingGlobalResultPerExchange marketTradingResultPerExchange1{ {&exchange1, MarketTradingGlobalResult{marketTradingResult1, tradeRangeStats1}}, {&exchange3, MarketTradingGlobalResult{marketTradingResult3, tradeRangeStats3}}, {&exchange4, MarketTradingGlobalResult{marketTradingResult4, tradeRangeStats4}}}; + + MarketTradingGlobalResultPerExchange marketTradingResultPerExchange2{ + {&exchange2, MarketTradingGlobalResult{marketTradingResult2, tradeRangeStats2}}}; + + ReplayResults replayResults{{alg1Name, {marketTradingResultPerExchange1}}, + {alg2Name, {marketTradingResultPerExchange1, marketTradingResultPerExchange2}}}; + CoincenterCommandType commandType{CoincenterCommandType::kReplay}; }; TEST_F(QueryResultPrinterReplayTest, FormattedTable) { basicQueryResultPrinter(ApiOutputType::kFormattedTable) - .printMarketTradingResults(timeWindow, marketTradingResultPerExchange, commandType); + .printMarketTradingResults(timeWindow, replayResults, commandType); static constexpr std::string_view kExpected = R"( -+----------+----------------------+---------+-----------+---------------+---------------+------------------------------------------------------+------------------------------+ -| Exchange | Time window | Market | Algorithm | Start amounts | Profit / Loss | Matched orders | Stats | -+----------+----------------------+---------+-----------+---------------+---------------+------------------------------------------------------+------------------------------+ -| binance | 1999-03-25T04:46:43Z | BTC-EUR | test-algo | 1 BTC | 0 EUR | | order books: 42 OK | -| | 2000-10-07T01:14:27Z | | | 1000 EUR | | | trades: 3 OK, 10 KO | -|~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~|~~~~~~~~~~~|~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| -| huobi | 1999-03-25T04:46:43Z | BTC-EUR | test-algo | 1 BTC | 500 EUR | 1999-03-25T04:46:43Z - Buy - 1.5 BTC @ 35000 USDT | order books: 500000 OK, 2 KO | -| | 2000-10-07T01:14:27Z | | | 1000 EUR | | 2000-06-11T23:58:40Z - Sell - 0.036 BTC @ 47899 USDT | trades: 0 OK | -|~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~|~~~~~~~~~~~|~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| -| huobi | 1999-03-25T04:46:43Z | BTC-EUR | test-algo | 1 BTC | 780 EUR | 1999-07-11T00:42:21Z - Buy - 2.5 BTC @ 45000 USDT | order books: 79009 OK | -| | 2000-10-07T01:14:27Z | | | 1000 EUR | | 1999-10-29T01:26:51Z - Sell - 0.05 BTC @ 35000 USDT | trades: 1555555555 OK, 45 KO | -| | | | | | | 1999-10-29T01:26:51Z - Sell - 1.7 BTC @ 50000 USDT | | -+----------+----------------------+---------+-----------+---------------+---------------+------------------------------------------------------+------------------------------+ ++------------+----------+----------------------+----------+---------------+---------------+------------------------------------------------------+------------------------------+ +| Algorithm | Exchange | Time window | Market | Start amounts | Profit / Loss | Matched orders | Stats | ++------------+----------+----------------------+----------+---------------+---------------+------------------------------------------------------+------------------------------+ +| first-alg | binance | 1999-03-25T04:46:43Z | BTC-USDT | 1 BTC | 0 USDT | | order books: 42 OK | +| | | 1999-03-25T04:46:43Z | | 1000 USDT | | | trades: 3 OK, 10 KO | +|~~~~~~~~~~~~|~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~|~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| +| second-alg | huobi | 1999-03-25T04:46:43Z | BTC-USDT | 1 BTC | 500 USDT | 1999-03-25T04:46:43Z - Buy - 1.5 BTC @ 35000 USDT | order books: 500000 OK, 2 KO | +| | | 1999-07-11T00:42:21Z | | 1000 USDT | | 2000-06-11T23:58:40Z - Sell - 0.036 BTC @ 47899 USDT | trades: 0 OK | +|~~~~~~~~~~~~|~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~|~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| +| second-alg | huobi | 1999-03-25T04:46:43Z | BTC-USDT | 1 BTC | 780 USDT | 1999-07-11T00:42:21Z - Buy - 2.5 BTC @ 45000 USDT | order books: 79009 OK | +| | | 1999-10-29T01:26:51Z | | 1000 USDT | | 1999-10-29T01:26:51Z - Sell - 0.05 BTC @ 35000 USDT | trades: 1555555555 OK, 45 KO | +| | | | | | | 1999-10-29T01:26:51Z - Sell - 1.7 BTC @ 50000 USDT | | +|~~~~~~~~~~~~|~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~|~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| +| first-alg | binance | 1999-03-25T04:46:43Z | BTC-USDT | 1 BTC | 0 USDT | | order books: 42 OK | +| | | 1999-03-25T04:46:43Z | | 1000 USDT | | | trades: 3 OK, 10 KO | +|~~~~~~~~~~~~|~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~|~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| +| second-alg | huobi | 1999-03-25T04:46:43Z | BTC-USDT | 1 BTC | 500 USDT | 1999-03-25T04:46:43Z - Buy - 1.5 BTC @ 35000 USDT | order books: 500000 OK, 2 KO | +| | | 1999-07-11T00:42:21Z | | 1000 USDT | | 2000-06-11T23:58:40Z - Sell - 0.036 BTC @ 47899 USDT | trades: 0 OK | +|~~~~~~~~~~~~|~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~|~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| +| second-alg | huobi | 1999-03-25T04:46:43Z | BTC-USDT | 1 BTC | 780 USDT | 1999-07-11T00:42:21Z - Buy - 2.5 BTC @ 45000 USDT | order books: 79009 OK | +| | | 1999-10-29T01:26:51Z | | 1000 USDT | | 1999-10-29T01:26:51Z - Sell - 0.05 BTC @ 35000 USDT | trades: 1555555555 OK, 45 KO | +| | | | | | | 1999-10-29T01:26:51Z - Sell - 1.7 BTC @ 50000 USDT | | +|~~~~~~~~~~~~|~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~|~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| +| first-alg | bithumb | 1999-03-25T04:46:43Z | BTC-USDT | 1 BTC | -334 USDT | 1999-03-25T04:46:43Z - Buy - 1.5 BTC @ 35000 USDT | order books: 23 OK, 1 KO | +| | | 1999-03-25T04:46:43Z | | 1000 USDT | | 1999-10-29T01:26:51Z - Sell - 0.05 BTC @ 35000 USDT | trades: 0 OK, 10 KO | ++------------+----------+----------------------+----------+---------------+---------------+------------------------------------------------------+------------------------------+ )"; expectStr(kExpected); } TEST_F(QueryResultPrinterReplayTest, EmptyJson) { - basicQueryResultPrinter(ApiOutputType::kJson) - .printMarketTradingResults(timeWindow, MarketTradingGlobalResultPerExchange{}, commandType); + basicQueryResultPrinter(ApiOutputType::kJson).printMarketTradingResults(timeWindow, ReplayResults{}, commandType); static constexpr std::string_view kExpected = R"json( { "in": { "opt": { - "time-window": "[1999-03-25 04:46:43 -> 2000-10-07 01:14:27)" + "time": { + "from": "1999-03-25T04:46:43Z", + "to": "2000-10-07T01:14:27Z" + } }, "req": "Replay" }, "out": {} })json"; + expectJson(kExpected); } TEST_F(QueryResultPrinterReplayTest, Json) { - basicQueryResultPrinter(ApiOutputType::kJson) - .printMarketTradingResults(timeWindow, marketTradingResultPerExchange, commandType); + basicQueryResultPrinter(ApiOutputType::kJson).printMarketTradingResults(timeWindow, replayResults, commandType); static constexpr std::string_view kExpected = R"json( { "in": { "opt": { - "time-window": "[1999-03-25 04:46:43 -> 2000-10-07 01:14:27)" + "time": { + "from": "1999-03-25T04:46:43Z", + "to": "2000-10-07T01:14:27Z" + } }, "req": "Replay" }, "out": { - "binance": { - "algorithm": "test-algo", - "market": "BTC-EUR", - "matched-orders": [], - "profit-and-loss": "0 EUR", - "start-amounts": { - "base": "1 BTC", - "quote": "1000 EUR" - }, - "stats": { - "order-books": { - "nb-error": 0, - "nb-successful": 42 + "first-alg": [ + [ + { + "binance": { + "algorithm": "first-alg", + "market": "BTC-USDT", + "matched-orders": [], + "profit-and-loss": "0 USDT", + "start-amounts": { + "base": "1 BTC", + "quote": "1000 USDT" + }, + "stats": { + "order-books": { + "nb-error": 0, + "nb-successful": 42, + "time": { + "from": "1999-03-25T04:46:43Z", + "to": "1999-03-25T04:46:43Z" + } + }, + "trades": { + "nb-error": 10, + "nb-successful": 3, + "time": { + "from": "1999-03-25T04:46:43Z", + "to": "1999-07-11T00:42:21Z" + } + } + } + } + }, + { + "huobi": { + "algorithm": "second-alg", + "market": "BTC-USDT", + "matched-orders": [ + { + "id": "1", + "matched": "1.5", + "matchedTime": "1999-03-25T04:46:43Z", + "pair": "BTC-USDT", + "placedTime": "1999-03-25T04:46:43Z", + "price": "35000", + "side": "Buy" + }, + { + "id": "5", + "matched": "0.036", + "matchedTime": "2000-10-07T01:14:27Z", + "pair": "BTC-USDT", + "placedTime": "2000-06-11T23:58:40Z", + "price": "47899", + "side": "Sell" + } + ], + "profit-and-loss": "500 USDT", + "start-amounts": { + "base": "1 BTC", + "quote": "1000 USDT" + }, + "stats": { + "order-books": { + "nb-error": 2, + "nb-successful": 500000, + "time": { + "from": "1999-03-25T04:46:43Z", + "to": "1999-07-11T00:42:21Z" + } + }, + "trades": { + "nb-error": 0, + "nb-successful": 0, + "time": { + "from": "1999-03-25T04:46:43Z", + "to": "1999-10-29T01:26:51Z" + } + } + } + } }, - "trades": { - "nb-error": 10, - "nb-successful": 3 + { + "huobi": { + "algorithm": "second-alg", + "market": "BTC-USDT", + "matched-orders": [ + { + "id": "2", + "matched": "2.5", + "matchedTime": "1999-07-11T00:42:21Z", + "pair": "BTC-USDT", + "placedTime": "1999-07-11T00:42:21Z", + "price": "45000", + "side": "Buy" + }, + { + "id": "3", + "matched": "0.05", + "matchedTime": "2000-06-11T23:58:40Z", + "pair": "BTC-USDT", + "placedTime": "1999-10-29T01:26:51Z", + "price": "35000", + "side": "Sell" + }, + { + "id": "4", + "matched": "1.7", + "matchedTime": "2000-06-11T23:58:40Z", + "pair": "BTC-USDT", + "placedTime": "1999-10-29T01:26:51Z", + "price": "50000", + "side": "Sell" + } + ], + "profit-and-loss": "780 USDT", + "start-amounts": { + "base": "1 BTC", + "quote": "1000 USDT" + }, + "stats": { + "order-books": { + "nb-error": 0, + "nb-successful": 79009, + "time": { + "from": "1999-03-25T04:46:43Z", + "to": "1999-10-29T01:26:51Z" + } + }, + "trades": { + "nb-error": 45, + "nb-successful": 1555555555, + "time": { + "from": "1999-07-11T00:42:21Z", + "to": "2000-06-11T23:58:40Z" + } + } + } + } } - } - }, - "huobi": { - "algorithm": "test-algo", - "market": "BTC-EUR", - "matched-orders": [ + ] + ], + "second-alg": [ + [ + { + "binance": { + "algorithm": "first-alg", + "market": "BTC-USDT", + "matched-orders": [], + "profit-and-loss": "0 USDT", + "start-amounts": { + "base": "1 BTC", + "quote": "1000 USDT" + }, + "stats": { + "order-books": { + "nb-error": 0, + "nb-successful": 42, + "time": { + "from": "1999-03-25T04:46:43Z", + "to": "1999-03-25T04:46:43Z" + } + }, + "trades": { + "nb-error": 10, + "nb-successful": 3, + "time": { + "from": "1999-03-25T04:46:43Z", + "to": "1999-07-11T00:42:21Z" + } + } + } + } + }, { - "id": "1", - "matched": "1.5", - "matchedTime": "1999-03-25T04:46:43Z", - "pair": "BTC-USDT", - "placedTime": "1999-03-25T04:46:43Z", - "price": "35000", - "side": "Buy" + "huobi": { + "algorithm": "second-alg", + "market": "BTC-USDT", + "matched-orders": [ + { + "id": "1", + "matched": "1.5", + "matchedTime": "1999-03-25T04:46:43Z", + "pair": "BTC-USDT", + "placedTime": "1999-03-25T04:46:43Z", + "price": "35000", + "side": "Buy" + }, + { + "id": "5", + "matched": "0.036", + "matchedTime": "2000-10-07T01:14:27Z", + "pair": "BTC-USDT", + "placedTime": "2000-06-11T23:58:40Z", + "price": "47899", + "side": "Sell" + } + ], + "profit-and-loss": "500 USDT", + "start-amounts": { + "base": "1 BTC", + "quote": "1000 USDT" + }, + "stats": { + "order-books": { + "nb-error": 2, + "nb-successful": 500000, + "time": { + "from": "1999-03-25T04:46:43Z", + "to": "1999-07-11T00:42:21Z" + } + }, + "trades": { + "nb-error": 0, + "nb-successful": 0, + "time": { + "from": "1999-03-25T04:46:43Z", + "to": "1999-10-29T01:26:51Z" + } + } + } + } }, { - "id": "5", - "matched": "0.036", - "matchedTime": "2000-10-07T01:14:27Z", - "pair": "BTC-USDT", - "placedTime": "2000-06-11T23:58:40Z", - "price": "47899", - "side": "Sell" + "huobi": { + "algorithm": "second-alg", + "market": "BTC-USDT", + "matched-orders": [ + { + "id": "2", + "matched": "2.5", + "matchedTime": "1999-07-11T00:42:21Z", + "pair": "BTC-USDT", + "placedTime": "1999-07-11T00:42:21Z", + "price": "45000", + "side": "Buy" + }, + { + "id": "3", + "matched": "0.05", + "matchedTime": "2000-06-11T23:58:40Z", + "pair": "BTC-USDT", + "placedTime": "1999-10-29T01:26:51Z", + "price": "35000", + "side": "Sell" + }, + { + "id": "4", + "matched": "1.7", + "matchedTime": "2000-06-11T23:58:40Z", + "pair": "BTC-USDT", + "placedTime": "1999-10-29T01:26:51Z", + "price": "50000", + "side": "Sell" + } + ], + "profit-and-loss": "780 USDT", + "start-amounts": { + "base": "1 BTC", + "quote": "1000 USDT" + }, + "stats": { + "order-books": { + "nb-error": 0, + "nb-successful": 79009, + "time": { + "from": "1999-03-25T04:46:43Z", + "to": "1999-10-29T01:26:51Z" + } + }, + "trades": { + "nb-error": 45, + "nb-successful": 1555555555, + "time": { + "from": "1999-07-11T00:42:21Z", + "to": "2000-06-11T23:58:40Z" + } + } + } + } } ], - "profit-and-loss": "500 EUR", - "start-amounts": { - "base": "1 BTC", - "quote": "1000 EUR" - }, - "stats": { - "order-books": { - "nb-error": 2, - "nb-successful": 500000 - }, - "trades": { - "nb-error": 0, - "nb-successful": 0 + [ + { + "bithumb": { + "algorithm": "first-alg", + "market": "BTC-USDT", + "matched-orders": [ + { + "id": "1", + "matched": "1.5", + "matchedTime": "1999-03-25T04:46:43Z", + "pair": "BTC-USDT", + "placedTime": "1999-03-25T04:46:43Z", + "price": "35000", + "side": "Buy" + }, + { + "id": "3", + "matched": "0.05", + "matchedTime": "2000-06-11T23:58:40Z", + "pair": "BTC-USDT", + "placedTime": "1999-10-29T01:26:51Z", + "price": "35000", + "side": "Sell" + } + ], + "profit-and-loss": "-334 USDT", + "start-amounts": { + "base": "1 BTC", + "quote": "1000 USDT" + }, + "stats": { + "order-books": { + "nb-error": 1, + "nb-successful": 23, + "time": { + "from": "1999-03-25T04:46:43Z", + "to": "1999-03-25T04:46:43Z" + } + }, + "trades": { + "nb-error": 10, + "nb-successful": 0, + "time": { + "from": "1999-03-25T04:46:43Z", + "to": "2000-10-07T01:14:27Z" + } + } + } + } } - } - } + ] + ] } })json"; expectJson(kExpected); } TEST_F(QueryResultPrinterReplayTest, NoPrint) { - basicQueryResultPrinter(ApiOutputType::kNoPrint) - .printMarketTradingResults(timeWindow, marketTradingResultPerExchange, commandType); + basicQueryResultPrinter(ApiOutputType::kNoPrint).printMarketTradingResults(timeWindow, replayResults, commandType); expectNoStr(); } diff --git a/src/main/src/processcommandsfromcli.cpp b/src/main/src/processcommandsfromcli.cpp index 98076226..45cb63c9 100644 --- a/src/main/src/processcommandsfromcli.cpp +++ b/src/main/src/processcommandsfromcli.cpp @@ -3,6 +3,7 @@ #include #include "cct_exception.hpp" +#include "coincenter-commands-processor.hpp" #include "coincenter.hpp" #include "coincenterinfo.hpp" #include "coincenterinfo_create.hpp" @@ -21,8 +22,9 @@ void ProcessCommandsFromCLI(std::string_view programName, const CoincenterComman try { Coincenter coincenter(coincenterInfo, ExchangeSecretsInfo_Create(generalOptions)); + CoincenterCommandsProcessor coincenterCommandsProcessor(coincenter); - const auto nbCommandsProcessed = coincenter.process(coincenterCommands); + const auto nbCommandsProcessed = coincenterCommandsProcessor.process(coincenterCommands); if (nbCommandsProcessed != 0) { // Write potentially updated cache data on disk at end of program diff --git a/src/objects/include/time-window.hpp b/src/objects/include/time-window.hpp index 4d641d90..d524a4ea 100644 --- a/src/objects/include/time-window.hpp +++ b/src/objects/include/time-window.hpp @@ -40,6 +40,13 @@ class TimeWindow { bool overlaps(TimeWindow rhs) const { return _from < rhs._to && rhs._from < _to; } + /// Returns a new time window with maximum boundaries of both. + TimeWindow aggregateMinMax(TimeWindow rhs) const; + + TimeWindow operator+(Duration dur) const { return TimeWindow(_from + dur, _to + dur); } + + TimeWindow& operator+=(Duration dur) { return *this = *this + dur; } + string str() const; bool operator==(const TimeWindow&) const noexcept = default; diff --git a/src/objects/src/time-window.cpp b/src/objects/src/time-window.cpp index 93918f6e..62af8bb9 100644 --- a/src/objects/src/time-window.cpp +++ b/src/objects/src/time-window.cpp @@ -1,10 +1,24 @@ #include "time-window.hpp" +#include + #include "cct_string.hpp" +#include "timedef.hpp" #include "timestring.hpp" namespace cct { +TimeWindow TimeWindow::aggregateMinMax(TimeWindow rhs) const { + TimeWindow ret{_from, std::max(_to, rhs._to)}; + if (_from == TimePoint{}) { + ret._from = rhs._from; + } else if (rhs._from != TimePoint{}) { + ret._from = std::min(_from, rhs._from); + } + + return ret; +} + string TimeWindow::str() const { string ret; ret.push_back('['); diff --git a/src/objects/test/time-window_test.cpp b/src/objects/test/time-window_test.cpp index c47fb997..732ae3ec 100644 --- a/src/objects/test/time-window_test.cpp +++ b/src/objects/test/time-window_test.cpp @@ -138,4 +138,30 @@ TEST_F(TimeWindowTest, NoOverlapEqual) { EXPECT_FALSE(tw2.contains(tw1)); } +TEST_F(TimeWindowTest, OperatorPlus) { + TimeWindow tw1(tp1, tp2); + const TimeWindow expected(tp1 + dur1, tp2 + dur1); + + EXPECT_EQ(tw1 + dur1, expected); + + tw1 += dur1; + + EXPECT_EQ(tw1, expected); +} + +TEST_F(TimeWindowTest, AggregateMinMax) { + TimeWindow tw1(tp1, tp2); + TimeWindow tw2(tp3, tp4); + + EXPECT_EQ(tw1.aggregateMinMax(tw2), TimeWindow(tp1, tp4)); +} + +TEST_F(TimeWindowTest, AggregateMinMaxWithNeutral) { + TimeWindow tw1(tp1, tp2); + TimeWindow tw2; + + EXPECT_EQ(tw1.aggregateMinMax(tw2), tw1); + EXPECT_EQ(tw2.aggregateMinMax(tw1), tw1); +} + } // namespace cct diff --git a/src/trading/common/include/trade-range-stats.hpp b/src/trading/common/include/trade-range-stats.hpp index a5499d54..93563e9c 100644 --- a/src/trading/common/include/trade-range-stats.hpp +++ b/src/trading/common/include/trade-range-stats.hpp @@ -2,28 +2,31 @@ #include +#include "time-window.hpp" + namespace cct { struct TradeRangeResultsStats { - int32_t nbSuccessful{}; - int32_t nbError{}; - TradeRangeResultsStats operator+(const TradeRangeResultsStats &rhs) const { - return TradeRangeResultsStats{nbSuccessful + rhs.nbSuccessful, nbError + rhs.nbError}; + return {timeWindow.aggregateMinMax(rhs.timeWindow), nbSuccessful + rhs.nbSuccessful, nbError + rhs.nbError}; } TradeRangeResultsStats &operator+=(const TradeRangeResultsStats &rhs) { return *this = *this + rhs; } + + TimeWindow timeWindow; + int32_t nbSuccessful{}; + int32_t nbError{}; }; struct TradeRangeStats { - TradeRangeResultsStats marketOrderBookStats; - TradeRangeResultsStats publicTradeStats; - TradeRangeStats operator+(const TradeRangeStats &rhs) const { - return TradeRangeStats{marketOrderBookStats + rhs.marketOrderBookStats, publicTradeStats + rhs.publicTradeStats}; + return {marketOrderBookStats + rhs.marketOrderBookStats, publicTradeStats + rhs.publicTradeStats}; } TradeRangeStats &operator+=(const TradeRangeStats &rhs) { return *this = *this + rhs; } + + TradeRangeResultsStats marketOrderBookStats; + TradeRangeResultsStats publicTradeStats; }; } // namespace cct \ No newline at end of file diff --git a/src/trading/common/src/market-trader-engine.cpp b/src/trading/common/src/market-trader-engine.cpp index 17cc0c0a..b99d8888 100644 --- a/src/trading/common/src/market-trader-engine.cpp +++ b/src/trading/common/src/market-trader-engine.cpp @@ -77,10 +77,14 @@ TradeRangeResultsStats ValidateRange(VectorType &vec, TimePoint earliestPossible earliestPossibleTime = obj.time(); return false; }); + if (nbUnsortedObjectsRemoved != 0) { log::error("{} {}(s) are not in chronological order", nbUnsortedObjectsRemoved, kObjName); } + if (!vec.empty()) { + stats.timeWindow = TimeWindow(vec.front().time(), vec.back().time()); + } stats.nbError = nbInvalidObjects + nbUnsortedObjectsRemoved; stats.nbSuccessful -= stats.nbError; @@ -116,19 +120,26 @@ TradeRangeStats MarketTraderEngine::validateRange(MarketOrderBookVector &&market TradeRangeStats MarketTraderEngine::tradeRange(MarketOrderBookVector &&marketOrderBooks, PublicTradeVector &&publicTrades) { - if (!_marketTrader) { - throw exception("registerMarketTrader should have been called before launching the trade engine"); - } - - TradeRangeStats tradeRangeStats{{TradeRangeResultsStats{static_cast(marketOrderBooks.size()), 0}}, - TradeRangeResultsStats{static_cast(publicTrades.size()), 0}}; + // errors set to 0 here as it is for unchecked launch + TradeRangeStats tradeRangeStats{ + {TradeRangeResultsStats{TimeWindow{}, static_cast(marketOrderBooks.size()), 0}}, + TradeRangeResultsStats{TimeWindow{}, static_cast(publicTrades.size()), 0}}; if (marketOrderBooks.empty()) { return tradeRangeStats; } + const auto fromOrderBooksTime = marketOrderBooks.front().time(); + const auto toOrderBooksTime = marketOrderBooks.back().time(); + + tradeRangeStats.marketOrderBookStats.timeWindow = TimeWindow(fromOrderBooksTime, toOrderBooksTime); + + if (!publicTrades.empty()) { + tradeRangeStats.publicTradeStats.timeWindow = TimeWindow(publicTrades.front().time(), publicTrades.back().time()); + } + log::info("[{}] at {} on {} replaying {} order books and {} trades", _marketTrader->name(), - TimeToString(marketOrderBooks.front().time()), _market, marketOrderBooks.size(), publicTrades.size()); + TimeToString(fromOrderBooksTime), _market, marketOrderBooks.size(), publicTrades.size()); // Rolling window of data provided to underlying market trader with data up to latest market order book. MarketDataView marketDataView(marketOrderBooks.data(), publicTrades.data(), @@ -183,10 +194,6 @@ TradeRangeStats MarketTraderEngine::tradeRange(MarketOrderBookVector &&marketOrd } MarketTradingResult MarketTraderEngine::finalizeAndComputeResult() { - if (!_marketTrader) { - throw exception("registerMarketTrader should have been called before computing results"); - } - _marketTraderEngineState.cancelAllOpenedOrders(); // How to compute gain / losses ? diff --git a/src/trading/indicators/src/basic-stats.cpp b/src/trading/indicators/src/basic-stats.cpp index 3cc6d16e..aa746735 100644 --- a/src/trading/indicators/src/basic-stats.cpp +++ b/src/trading/indicators/src/basic-stats.cpp @@ -2,6 +2,7 @@ #include +#include "currencycode.hpp" #include "market-data-view.hpp" #include "marketorderbook.hpp" #include "monetaryamount.hpp"