From 9c96c42b30de836ff0e0e3bc175ae895e2bc3f25 Mon Sep 17 00:00:00 2001 From: Stephane Janel Date: Sat, 29 Jun 2024 17:59:56 +0200 Subject: [PATCH] Initiate basic indicators for trading --- CMakeLists.txt | 4 +- Dockerfile | 8 +- src/engine/CMakeLists.txt | 7 - src/engine/src/coincenter.cpp | 13 +- src/objects/src/monetaryamount.cpp | 7 +- src/trading/CMakeLists.txt | 3 +- src/trading/algorithms/CMakeLists.txt | 2 + .../include/market-trader-factory.hpp | 1 + src/trading/common/CMakeLists.txt | 9 +- .../include/algorithm-name-iterator.hpp} | 4 +- .../common/include/market-trader-engine.hpp | 1 + .../common/src/algorithm-name-iterator.cpp} | 10 +- .../test/algorithm-name-iterator_test.cpp} | 22 +-- src/trading/indicators/CMakeLists.txt | 8 ++ .../indicators/include/basic-stats.hpp | 26 ++++ src/trading/indicators/src/basic-stats.cpp | 133 ++++++++++++++++++ 16 files changed, 216 insertions(+), 42 deletions(-) rename src/{engine/include/replay-algorithm-name-iterator.hpp => trading/common/include/algorithm-name-iterator.hpp} (82%) rename src/{engine/src/replay-algorithm-name-iterator.cpp => trading/common/src/algorithm-name-iterator.cpp} (81%) rename src/{engine/test/replay-algorithm-name-iterator_test.cpp => trading/common/test/algorithm-name-iterator_test.cpp} (70%) create mode 100644 src/trading/indicators/CMakeLists.txt create mode 100644 src/trading/indicators/include/basic-stats.hpp create mode 100644 src/trading/indicators/src/basic-stats.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e3280317..fd635081 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -161,7 +161,7 @@ FetchContent_Declare( set(JWT_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) -list(APPEND fetchContentPackagesToMakeAvailable jwt-cpp) +list(APPEND fetchContentPackagesToMakeAvailable jwt-cpp) # protobuf - serialization / deserialization library set(PROTOBUF_FETCHED_CONTENT OFF) @@ -172,7 +172,7 @@ if(CCT_ENABLE_PROTO) else() # Check here for a new version: https://protobuf.dev/support/version-support/#cpp if (NOT PROTOBUF_VERSION) - set(PROTOBUF_VERSION v5.27.1) + set(PROTOBUF_VERSION v5.27.2) endif() message(STATUS "Configuring protobuf ${PROTOBUF_VERSION} from sources") diff --git a/Dockerfile b/Dockerfile index bb189794..ede88bb9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ ARG BUILD_WITH_PROTOBUF=1 # Install base & build dependencies, needed certificates for curl to work with https RUN apt update && \ apt upgrade -y && \ - apt install build-essential ninja-build libssl-dev zlib1g-dev libcurl4-openssl-dev cmake git ca-certificates -y --no-install-recommends + apt install -y --no-install-recommends build-essential ninja-build libssl-dev zlib1g-dev libcurl4-openssl-dev cmake git ca-certificates # Copy source files WORKDIR /app/src @@ -38,12 +38,14 @@ COPY data/cache/fiatcache.json ./ WORKDIR /app/bin # Configure -RUN cmake -DCMAKE_BUILD_TYPE=${BUILD_MODE} \ +RUN cmake \ + -DCMAKE_BUILD_TYPE=${BUILD_MODE} \ -DCCT_ENABLE_TESTS=${BUILD_TEST} \ -DCCT_ENABLE_ASAN=${BUILD_ASAN} \ -DCCT_BUILD_PROMETHEUS_FROM_SRC=${BUILD_WITH_PROMETHEUS} \ -DCCT_ENABLE_PROTO=${BUILD_WITH_PROTOBUF} \ - -GNinja .. + -GNinja \ + .. # Build RUN cmake --build . diff --git a/src/engine/CMakeLists.txt b/src/engine/CMakeLists.txt index 791f7987..620e2364 100644 --- a/src/engine/CMakeLists.txt +++ b/src/engine/CMakeLists.txt @@ -83,13 +83,6 @@ add_unit_test( ../api/common/test/include ) -add_unit_test( - replay-algorithm-name-iterator_test - test/replay-algorithm-name-iterator_test.cpp - LIBRARIES - coincenter_engine -) - add_unit_test( stringoptionparser_test test/stringoptionparser_test.cpp diff --git a/src/engine/src/coincenter.cpp b/src/engine/src/coincenter.cpp index 4c035ef9..1cd69896 100644 --- a/src/engine/src/coincenter.cpp +++ b/src/engine/src/coincenter.cpp @@ -9,6 +9,7 @@ #include #include +#include "algorithm-name-iterator.hpp" #include "balanceoptions.hpp" #include "cct_const.hpp" #include "cct_exception.hpp" @@ -37,7 +38,6 @@ #include "query-result-type-helpers.hpp" #include "queryresultprinter.hpp" #include "queryresulttypes.hpp" -#include "replay-algorithm-name-iterator.hpp" #include "replay-options.hpp" #include "time-window.hpp" #include "timedef.hpp" @@ -618,8 +618,8 @@ void Coincenter::replay(const AbstractMarketTraderFactory &marketTraderFactory, MarketSet allMarkets = ComputeAllMarkets(marketTimestampSetsPerExchange); - ReplayAlgorithmNameIterator replayAlgorithmNameIterator(replayOptions.algorithmNames(), - marketTraderFactory.allSupportedAlgorithms()); + AlgorithmNameIterator replayAlgorithmNameIterator(replayOptions.algorithmNames(), + marketTraderFactory.allSupportedAlgorithms()); while (replayAlgorithmNameIterator.hasNext()) { std::string_view algorithmName = replayAlgorithmNameIterator.next(); @@ -629,8 +629,7 @@ void Coincenter::replay(const AbstractMarketTraderFactory &marketTraderFactory, // Create the MarketTraderEngines based on this market, filtering out exchanges without available amount to // trade - MarketTraderEngineVector marketTraderEngines = - createMarketTraderEngines(replayOptions, replayMarket, exchangesWithThisMarketData); + auto marketTraderEngines = createMarketTraderEngines(replayOptions, replayMarket, exchangesWithThisMarketData); replayAlgorithm(marketTraderFactory, algorithmName, replayOptions, marketTraderEngines, exchangesWithThisMarketData); @@ -688,10 +687,10 @@ Coincenter::MarketTraderEngineVector Coincenter::createMarketTraderEngines( MarketTraderEngineVector marketTraderEngines; for (decltype(nbExchanges) exchangePos = 0; exchangePos < nbExchanges; ++exchangePos) { - const MonetaryAmount startBaseAmount = + const auto startBaseAmount = isValidateOnly ? MonetaryAmount{0, market.base()} : ComputeStartAmount(market.base(), convertedBaseAmountPerExchange[exchangePos].second); - const MonetaryAmount startQuoteAmount = + const auto startQuoteAmount = isValidateOnly ? MonetaryAmount{0, market.quote()} : ComputeStartAmount(market.quote(), convertedQuoteAmountPerExchange[exchangePos].second); diff --git a/src/objects/src/monetaryamount.cpp b/src/objects/src/monetaryamount.cpp index 103a7084..03bc951c 100644 --- a/src/objects/src/monetaryamount.cpp +++ b/src/objects/src/monetaryamount.cpp @@ -69,11 +69,13 @@ inline int ParseNegativeChar(std::string_view &amountStr) { } inline MonetaryAmount::AmountType HeuristicRounding(std::size_t dotPos, std::string_view &amountStr) { + static constexpr std::string_view kHeuristicRoundingPatterns[] = {"000", "999"}; + std::size_t bestFindPos = 0; - for (std::string_view pattern : {"000", "999"}) { + for (std::string_view pattern : kHeuristicRoundingPatterns) { std::size_t findPos = amountStr.rfind(pattern); if (findPos != std::string_view::npos && findPos > dotPos) { - while (amountStr[findPos - 1] == pattern.front()) { + while (amountStr[findPos - 1] == amountStr[findPos]) { --findPos; } if (amountStr[findPos - 1] == '.') { @@ -84,7 +86,6 @@ inline MonetaryAmount::AmountType HeuristicRounding(std::size_t dotPos, std::str } if (bestFindPos != 0) { const bool roundingUp = amountStr[bestFindPos] == '9'; - log::trace("Heuristic rounding {} for {}", roundingUp ? "up" : "down", amountStr); amountStr.remove_suffix(amountStr.size() - bestFindPos); if (roundingUp) { return 1; diff --git a/src/trading/CMakeLists.txt b/src/trading/CMakeLists.txt index 5df5ce59..04859b3e 100644 --- a/src/trading/CMakeLists.txt +++ b/src/trading/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(algorithms) -add_subdirectory(common) \ No newline at end of file +add_subdirectory(common) +add_subdirectory(indicators) \ No newline at end of file diff --git a/src/trading/algorithms/CMakeLists.txt b/src/trading/algorithms/CMakeLists.txt index 51e64005..1a82e1bf 100644 --- a/src/trading/algorithms/CMakeLists.txt +++ b/src/trading/algorithms/CMakeLists.txt @@ -5,6 +5,8 @@ add_library(coincenter_trading-algorithms STATIC ${TRADING-ALGORITHMS_SRC}) target_link_libraries(coincenter_trading-algorithms PUBLIC coincenter_api-objects) target_link_libraries(coincenter_trading-algorithms PUBLIC coincenter_objects) target_link_libraries(coincenter_trading-algorithms PUBLIC coincenter_tech) + target_link_libraries(coincenter_trading-algorithms PUBLIC coincenter_trading-common) +target_link_libraries(coincenter_trading-algorithms PUBLIC coincenter_trading-indicators) target_include_directories(coincenter_trading-algorithms PUBLIC include) \ No newline at end of file diff --git a/src/trading/algorithms/include/market-trader-factory.hpp b/src/trading/algorithms/include/market-trader-factory.hpp index a4cfb3d6..56096087 100644 --- a/src/trading/algorithms/include/market-trader-factory.hpp +++ b/src/trading/algorithms/include/market-trader-factory.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "abstract-market-trader-factory.hpp" diff --git a/src/trading/common/CMakeLists.txt b/src/trading/common/CMakeLists.txt index cd1bb22f..06307601 100644 --- a/src/trading/common/CMakeLists.txt +++ b/src/trading/common/CMakeLists.txt @@ -4,4 +4,11 @@ add_library(coincenter_trading-common STATIC ${TRADING-COMMON_SRC}) target_link_libraries(coincenter_trading-common PUBLIC coincenter_api-objects) target_link_libraries(coincenter_trading-common PUBLIC coincenter_objects) target_link_libraries(coincenter_trading-common PUBLIC coincenter_tech) -target_include_directories(coincenter_trading-common PUBLIC include) \ No newline at end of file +target_include_directories(coincenter_trading-common PUBLIC include) + +add_unit_test( + algorithm-name-iterator_test + test/algorithm-name-iterator_test.cpp + LIBRARIES + coincenter_trading-common +) \ No newline at end of file diff --git a/src/engine/include/replay-algorithm-name-iterator.hpp b/src/trading/common/include/algorithm-name-iterator.hpp similarity index 82% rename from src/engine/include/replay-algorithm-name-iterator.hpp rename to src/trading/common/include/algorithm-name-iterator.hpp index 21995286..2f35112b 100644 --- a/src/engine/include/replay-algorithm-name-iterator.hpp +++ b/src/trading/common/include/algorithm-name-iterator.hpp @@ -8,9 +8,9 @@ namespace cct { /// Convenient class to iterate on the algorithm names, comma separated. /// If 'algorithmNames' is empty, it will loop on all available ones (given by 'allAlgorithms') -class ReplayAlgorithmNameIterator { +class AlgorithmNameIterator { public: - ReplayAlgorithmNameIterator(std::string_view algorithmNames, std::span allAlgorithms); + AlgorithmNameIterator(std::string_view algorithmNames, std::span allAlgorithms); /// Returns true if and only if there is at least one additional algorithm name to iterate on. bool hasNext() const; diff --git a/src/trading/common/include/market-trader-engine.hpp b/src/trading/common/include/market-trader-engine.hpp index 9fb686e7..2d2d87d6 100644 --- a/src/trading/common/include/market-trader-engine.hpp +++ b/src/trading/common/include/market-trader-engine.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include diff --git a/src/engine/src/replay-algorithm-name-iterator.cpp b/src/trading/common/src/algorithm-name-iterator.cpp similarity index 81% rename from src/engine/src/replay-algorithm-name-iterator.cpp rename to src/trading/common/src/algorithm-name-iterator.cpp index 1a35a3a8..476ba685 100644 --- a/src/engine/src/replay-algorithm-name-iterator.cpp +++ b/src/trading/common/src/algorithm-name-iterator.cpp @@ -1,4 +1,4 @@ -#include "replay-algorithm-name-iterator.hpp" +#include "algorithm-name-iterator.hpp" #include #include @@ -20,8 +20,8 @@ auto FindNextSeparatorPos(std::string_view str, std::string_view::size_type pos } } // namespace -ReplayAlgorithmNameIterator::ReplayAlgorithmNameIterator(std::string_view algorithmNames, - std::span allAlgorithms) +AlgorithmNameIterator::AlgorithmNameIterator(std::string_view algorithmNames, + std::span allAlgorithms) : _allAlgorithms(allAlgorithms), _algorithmNames(algorithmNames), _begPos(0), @@ -33,7 +33,7 @@ ReplayAlgorithmNameIterator::ReplayAlgorithmNameIterator(std::string_view algori } } -bool ReplayAlgorithmNameIterator::hasNext() const { +bool AlgorithmNameIterator::hasNext() const { using PosT = decltype(_begPos); if (_algorithmNames.empty()) { @@ -43,7 +43,7 @@ bool ReplayAlgorithmNameIterator::hasNext() const { return _begPos != static_cast(_algorithmNames.length()); } -std::string_view ReplayAlgorithmNameIterator::next() { +std::string_view AlgorithmNameIterator::next() { if (_algorithmNames.empty()) { return _allAlgorithms[_begPos++]; } diff --git a/src/engine/test/replay-algorithm-name-iterator_test.cpp b/src/trading/common/test/algorithm-name-iterator_test.cpp similarity index 70% rename from src/engine/test/replay-algorithm-name-iterator_test.cpp rename to src/trading/common/test/algorithm-name-iterator_test.cpp index ee930a0b..cf062db7 100644 --- a/src/engine/test/replay-algorithm-name-iterator_test.cpp +++ b/src/trading/common/test/algorithm-name-iterator_test.cpp @@ -1,4 +1,4 @@ -#include "replay-algorithm-name-iterator.hpp" +#include "algorithm-name-iterator.hpp" #include @@ -7,20 +7,20 @@ #include "cct_exception.hpp" namespace cct { -class ReplayAlgorithmNameIteratorTest : public ::testing::Test { +class AlgorithmNameIteratorTest : public ::testing::Test { protected: static constexpr std::string_view kInvalidAlgorithmNames[] = {"any", "so-what,"}; static constexpr std::string_view kAlgorithmNames[] = {"any", "so-what", "angry", "bird", "Jack", "a-more-complex algorithm Name"}; }; -TEST_F(ReplayAlgorithmNameIteratorTest, AlgorithmNamesValidity) { - EXPECT_THROW(ReplayAlgorithmNameIterator("", kInvalidAlgorithmNames), exception); - EXPECT_NO_THROW(ReplayAlgorithmNameIterator("", kAlgorithmNames)); +TEST_F(AlgorithmNameIteratorTest, AlgorithmNamesValidity) { + EXPECT_THROW(AlgorithmNameIterator("", kInvalidAlgorithmNames), exception); + EXPECT_NO_THROW(AlgorithmNameIterator("", kAlgorithmNames)); } -TEST_F(ReplayAlgorithmNameIteratorTest, IteratorWithAll) { - ReplayAlgorithmNameIterator it("", kAlgorithmNames); +TEST_F(AlgorithmNameIteratorTest, IteratorWithAll) { + AlgorithmNameIterator it("", kAlgorithmNames); int algorithmPos = 0; while (it.hasNext()) { @@ -50,8 +50,8 @@ TEST_F(ReplayAlgorithmNameIteratorTest, IteratorWithAll) { EXPECT_EQ(algorithmPos, 6); } -TEST_F(ReplayAlgorithmNameIteratorTest, IteratorWithUniqueAlgorithmSpecified) { - ReplayAlgorithmNameIterator it("so-What", kAlgorithmNames); +TEST_F(AlgorithmNameIteratorTest, IteratorWithUniqueAlgorithmSpecified) { + AlgorithmNameIterator it("so-What", kAlgorithmNames); int algorithmPos = 0; while (it.hasNext()) { @@ -71,8 +71,8 @@ TEST_F(ReplayAlgorithmNameIteratorTest, IteratorWithUniqueAlgorithmSpecified) { EXPECT_EQ(algorithmPos, 1); } -TEST_F(ReplayAlgorithmNameIteratorTest, IteratorWithSpecifiedList) { - ReplayAlgorithmNameIterator it("Jack,whatever,so-what,some-algorithmNameThatIsNotInAll,with spaces", kAlgorithmNames); +TEST_F(AlgorithmNameIteratorTest, IteratorWithSpecifiedList) { + AlgorithmNameIterator it("Jack,whatever,so-what,some-algorithmNameThatIsNotInAll,with spaces", kAlgorithmNames); int algorithmPos = 0; while (it.hasNext()) { diff --git a/src/trading/indicators/CMakeLists.txt b/src/trading/indicators/CMakeLists.txt new file mode 100644 index 00000000..dca6df46 --- /dev/null +++ b/src/trading/indicators/CMakeLists.txt @@ -0,0 +1,8 @@ +aux_source_directory(src TRADING-INDICATORS_SRC) + +add_library(coincenter_trading-indicators STATIC ${TRADING-INDICATORS_SRC}) +target_link_libraries(coincenter_trading-indicators PUBLIC coincenter_trading-common) +target_link_libraries(coincenter_trading-indicators PUBLIC coincenter_api-objects) +target_link_libraries(coincenter_trading-indicators PUBLIC coincenter_objects) +target_link_libraries(coincenter_trading-indicators PUBLIC coincenter_tech) +target_include_directories(coincenter_trading-indicators PUBLIC include) \ No newline at end of file diff --git a/src/trading/indicators/include/basic-stats.hpp b/src/trading/indicators/include/basic-stats.hpp new file mode 100644 index 00000000..8304d596 --- /dev/null +++ b/src/trading/indicators/include/basic-stats.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "monetaryamount.hpp" +#include "timedef.hpp" + +namespace cct { + +class MarketDataView; + +class BasicStats { + public: + explicit BasicStats(const MarketDataView &marketDataView) : _marketDataView(marketDataView) {} + + MonetaryAmount movingAverageFromLastPublicTradesPrice(TimePoint oldestTime) const; + + MonetaryAmount movingAverageFromMarketOrderBooks(TimePoint oldestTime, + Duration minFrequencyBetweenTwoPoints = Duration{}) const; + + MonetaryAmount standardDeviationFromMarketOrderBooks(TimePoint oldestTime, + Duration minFrequencyBetweenTwoPoints = Duration{}) const; + + private: + const MarketDataView &_marketDataView; +}; + +} // namespace cct \ No newline at end of file diff --git a/src/trading/indicators/src/basic-stats.cpp b/src/trading/indicators/src/basic-stats.cpp new file mode 100644 index 00000000..3cc6d16e --- /dev/null +++ b/src/trading/indicators/src/basic-stats.cpp @@ -0,0 +1,133 @@ +#include "basic-stats.hpp" + +#include + +#include "market-data-view.hpp" +#include "marketorderbook.hpp" +#include "monetaryamount.hpp" +#include "publictrade.hpp" +#include "timedef.hpp" + +namespace cct { + +MonetaryAmount BasicStats::movingAverageFromLastPublicTradesPrice(TimePoint oldestTime) const { + MonetaryAmount totalWeightedPrice; + MonetaryAmount totalVolume; + + const auto lastPublicTrades = _marketDataView.pastPublicTrades(); + + for (auto publicTradeIt = lastPublicTrades.end(); publicTradeIt != lastPublicTrades.begin();) { + const PublicTrade &publicTrade = *(--publicTradeIt); + + if (publicTrade.time() < oldestTime) { + break; + } + + totalWeightedPrice += publicTrade.price() * publicTrade.amount(); + totalVolume += publicTrade.amount(); + } + + if (totalVolume == 0) { + return totalWeightedPrice; + } + + return totalWeightedPrice / totalVolume.toNeutral(); +} + +MonetaryAmount BasicStats::movingAverageFromMarketOrderBooks(TimePoint oldestTime, + Duration minFrequencyBetweenTwoPoints) const { + MonetaryAmount totalPrice; + + const auto lastOrderBooks = _marketDataView.pastMarketOrderBooks(); + + auto orderBookIt = lastOrderBooks.end(); + + int nbPoints{}; + + TimePoint previousTime = TimePoint::max(); + + while (orderBookIt != lastOrderBooks.begin()) { + const MarketOrderBook &marketOrderBook = *(--orderBookIt); + const auto ts = marketOrderBook.time(); + + if (ts < oldestTime) { + break; + } + + if (previousTime < ts + minFrequencyBetweenTwoPoints) { + continue; + } + + previousTime = ts; + + const auto optPrice = marketOrderBook.averagePrice(); + + if (!optPrice) { + continue; + } + + totalPrice += *optPrice; + ++nbPoints; + } + + if (nbPoints == 0) { + return totalPrice; + } + + return totalPrice / nbPoints; +} + +MonetaryAmount BasicStats::standardDeviationFromMarketOrderBooks(TimePoint oldestTime, + Duration minFrequencyBetweenTwoPoints) const { + double average = movingAverageFromMarketOrderBooks(oldestTime).toDouble(); + + double squareDiffsSum{}; + + const auto lastOrderBooks = _marketDataView.pastMarketOrderBooks(); + + if (lastOrderBooks.empty()) { + return MonetaryAmount{}; + } + + CurrencyCode priceCur = lastOrderBooks.back().market().quote(); + + auto orderBookIt = lastOrderBooks.end(); + + int nbPoints{}; + + TimePoint previousTime = TimePoint::max(); + + while (orderBookIt != lastOrderBooks.begin()) { + const MarketOrderBook &marketOrderBook = *(--orderBookIt); + const auto ts = marketOrderBook.time(); + + if (ts < oldestTime) { + break; + } + + if (previousTime < ts + minFrequencyBetweenTwoPoints) { + continue; + } + + previousTime = ts; + + const auto optPrice = marketOrderBook.averagePrice(); + + if (!optPrice) { + continue; + } + + const auto diff = average - optPrice->toDouble(); + + squareDiffsSum += diff * diff; + ++nbPoints; + } + + if (nbPoints == 0) { + return MonetaryAmount{0, priceCur}; + } + + return MonetaryAmount{std::sqrt(squareDiffsSum), priceCur}; +} + +} // namespace cct \ No newline at end of file