diff --git a/.github/workflows/ubuntu-monitoring.yml b/.github/workflows/ubuntu-special.yml
similarity index 85%
rename from .github/workflows/ubuntu-monitoring.yml
rename to .github/workflows/ubuntu-special.yml
index a0dc9091..d7554cff 100644
--- a/.github/workflows/ubuntu-monitoring.yml
+++ b/.github/workflows/ubuntu-special.yml
@@ -1,4 +1,4 @@
-name: Monitoring
+name: Special
on:
push:
@@ -7,14 +7,14 @@ on:
pull_request:
jobs:
- ubuntu-monitoring-build:
- name: Build on Ubuntu with monitoring support
+ ubuntu-special-build:
+ name: Build on Ubuntu with monitoring / protobuf support
runs-on: ubuntu-latest
strategy:
matrix:
compiler: [g++-11]
buildmode: [Debug]
- build-prometheus-from-source: [0, 1]
+ build-special-from-source: [0, 1]
steps:
- name: Checkout repository code
@@ -38,7 +38,7 @@ jobs:
ninja
sudo cmake --install .
- if: matrix.build-prometheus-from-source == 0
+ if: matrix.build-special-from-source == 0
- name: Create Build Environment
run: cmake -E make_directory ${{github.workspace}}/build
@@ -46,7 +46,7 @@ jobs:
- name: Configure CMake
working-directory: ${{github.workspace}}/build
shell: bash
- run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=${{matrix.buildmode}} -DCMAKE_CXX_COMPILER=${{matrix.compiler}} -DCCT_BUILD_PROMETHEUS_FROM_SRC=${{matrix.build-prometheus-from-source}} -GNinja
+ run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=${{matrix.buildmode}} -DCMAKE_CXX_COMPILER=${{matrix.compiler}} -DCCT_BUILD_PROMETHEUS_FROM_SRC=${{matrix.build-special-from-source}} -DCCT_ENABLE_PROTO=${{matrix.build-special-from-source}} -GNinja
- name: Build
working-directory: ${{github.workspace}}/build
diff --git a/CMakeLists.txt b/CMakeLists.txt
index a8d536a0..45e25a80 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -36,6 +36,7 @@ option(CCT_ENABLE_TESTS "Build the unit tests" ${MAIN_PROJECT})
option(CCT_BUILD_EXEC "Build an executable instead of a static library" ${MAIN_PROJECT})
option(CCT_ENABLE_ASAN "Compile with AddressSanitizer" ${CCT_ASAN_BUILD})
option(CCT_ENABLE_CLANG_TIDY "Compile with clang-tidy checks" OFF)
+option(CCT_ENABLE_PROTO "Compile with protobuf support (to export data to the outside world)" ON)
option(CCT_BUILD_PROMETHEUS_FROM_SRC "Fetch and build from prometheus-cpp sources" OFF)
set(CCT_DATA_DIR "${CMAKE_CURRENT_SOURCE_DIR}/data" CACHE PATH "Needed data directory for coincenter. Can also be overriden at runtime with this environment variable")
@@ -83,6 +84,7 @@ if(CCT_ENABLE_TESTS)
enable_testing()
endif()
+# nlohmann_json - coincenter json library
find_package(nlohmann_json CONFIG)
if(NOT nlohmann_json_FOUND)
FetchContent_Declare(
@@ -94,6 +96,7 @@ if(NOT nlohmann_json_FOUND)
FetchContent_MakeAvailable(nlohmann_json)
endif()
+# spdlog - coincenter logging library
find_package(spdlog CONFIG)
if(NOT spdlog_FOUND)
FetchContent_Declare(
@@ -133,6 +136,34 @@ else()
endif()
endif()
+if(CCT_ENABLE_PROTO)
+ find_package(Protobuf CONFIG)
+ if(protobuf_FOUND)
+ message(STATUS "Linking with protobuf ${protobuf_VERSION}")
+ else()
+ set(PROTOBUF_VERSION v25.2)
+ if (MSVC)
+ # protobuf v25.0 does not compile yet with MSVC: https://github.com/protocolbuffers/protobuf/issues/14602
+ set(PROTOBUF_VERSION v24.4)
+ endif()
+
+ message(STATUS "Compiling protobuf ${PROTOBUF_VERSION} from sources")
+
+ set(protobuf_BUILD_TESTS OFF)
+ set(ABSL_PROPAGATE_CXX_STD ON)
+
+ FetchContent_Declare(
+ protobuf
+ GIT_REPOSITORY https://github.com/protocolbuffers/protobuf.git
+ GIT_TAG ${PROTOBUF_VERSION}
+ )
+ FetchContent_MakeAvailable(protobuf)
+
+ include(${protobuf_SOURCE_DIR}/cmake/protobuf-generate.cmake)
+
+ endif()
+endif()
+
# Unit Tests
#[[ Create an executable
@@ -248,12 +279,18 @@ if(CCT_ENABLE_PROMETHEUS)
add_compile_definitions(CCT_ENABLE_PROMETHEUS)
endif()
+if(CCT_ENABLE_PROTO)
+ add_compile_definitions(CCT_ENABLE_PROTO)
+ add_compile_definitions("CCT_PROTOBUF_VERSION=\"${PROTOBUF_VERSION}\"")
+endif()
+
# Link to sub folders CMakeLists.txt, from the lowest level to the highest level for documentation
# (beware of cyclic dependencies)
add_subdirectory(src/tech)
add_subdirectory(src/monitoring)
add_subdirectory(src/http-request)
add_subdirectory(src/objects)
+add_subdirectory(src/serialization)
add_subdirectory(src/api-objects)
add_subdirectory(src/api)
add_subdirectory(src/engine)
diff --git a/CONFIG.md b/CONFIG.md
index efba43e0..b2c031c0 100644
--- a/CONFIG.md
+++ b/CONFIG.md
@@ -150,6 +150,7 @@ Refer to the hardcoded default json example as a model in case of doubt.
| *query* | **updateFrequency.depositWallet** | Duration string (ex: `1min`) | Minimum duration between two consecutive requests of deposit information (including wallet) |
| *query* | **updateFrequency.currencyInfo** | Duration string (ex: `4h`) | Minimum duration between two consecutive requests of dynamic currency info retrieval on Bithumb only (used for place order) |
| *query* | **placeSimulateRealOrder** | Boolean (`true` or `false`) | If `true`, in trade simulation mode (with `--sim`) exchanges which do not support simulated mode in place order will actually place a real order, with the following characteristics:
- trade strategy forced to `maker`
- price will be changed to a maximum for a sell, to a minimum for a buy
This will allow place of a 'real' order that cannot be matched in practice (if it is, lucky you!) |
+| *query* | **marketDataSerialization** | Boolean (`true` or `false`) | If `true` and `coincenter` is compiled with **protobuf** support, some market data will automatically be exported in the `data/serialization` directory (`orderbook` and `last-trades`) for a long term storage |
| *query* | **multiTradeAllowedByDefault** | Boolean (`true` or `false`) | If `true`, [multi-trade](README.md#multi-trade) will be allowed by default for `trade`, `buy` and `sell`. It can be overridden at command line level with `--no-multi-trade` and `--multi-trade`. |
| *query* | **validateApiKey** | Boolean (`true` or `false`) | If `true`, each loaded private key will be tested at start of the program. In case of a failure, it will be removed from the list of private accounts loaded by `coincenter`, so that later queries do not consider it instead of raising a runtime exception. The downside is that it will make an additional check that will make startup slower. | |
| *tradefees* | **maker** | String as decimal number representing a percentage (for instance, "0.15") | Trade fees occurring when a maker order is matched |
diff --git a/Dockerfile b/Dockerfile
index 73202ad0..32dd1a6c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -23,12 +23,14 @@ ARG BUILD_MODE=Release
ARG BUILD_TEST=0
ARG BUILD_ASAN=0
ARG BUILD_WITH_PROMETHEUS=1
+ARG BUILD_WITH_PROTOBUF=1
# Build and launch tests if any
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 .. && \
ninja && \
if [ "$BUILD_TEST" = "1" -o "$BUILD_TEST" = "ON" ]; then \
diff --git a/alpine.Dockerfile b/alpine.Dockerfile
index f862cc90..86e83c6e 100644
--- a/alpine.Dockerfile
+++ b/alpine.Dockerfile
@@ -2,7 +2,7 @@
FROM alpine:3.19.0 AS build
# Install base & build dependencies, needed certificates for curl to work with https
-RUN apk add --update --upgrade --no-cache g++ libc-dev curl-dev cmake ninja git ca-certificates
+RUN apk add --update --upgrade --no-cache g++ linux-headers libc-dev curl-dev cmake ninja git ca-certificates
# Set default directory for application
WORKDIR /app
@@ -18,12 +18,14 @@ ARG BUILD_MODE=Release
ARG BUILD_TEST=0
ARG BUILD_ASAN=0
ARG BUILD_WITH_PROMETHEUS=1
+ARG BUILD_WITH_PROTOBUF=1
# Build and launch tests if any
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 .. && \
ninja && \
if [ "$BUILD_TEST" = "1" -o "$BUILD_TEST" = "ON" ]; then \
diff --git a/src/api/interface/CMakeLists.txt b/src/api/interface/CMakeLists.txt
index a17d1968..df998a70 100644
--- a/src/api/interface/CMakeLists.txt
+++ b/src/api/interface/CMakeLists.txt
@@ -3,6 +3,7 @@ aux_source_directory(src API_INTERFACE_SRC)
add_library(coincenter_api-interface STATIC ${API_INTERFACE_SRC})
target_link_libraries(coincenter_api-interface PUBLIC coincenter_api-exchange)
+target_link_libraries(coincenter_api-interface PUBLIC coincenter_serialization)
target_link_libraries(coincenter_api-interface PRIVATE coincenter_monitoring)
target_include_directories(coincenter_api-interface PUBLIC include)
diff --git a/src/api/interface/include/exchange.hpp b/src/api/interface/include/exchange.hpp
index 9d561f78..6a56ff8b 100644
--- a/src/api/interface/include/exchange.hpp
+++ b/src/api/interface/include/exchange.hpp
@@ -1,7 +1,9 @@
#pragma once
+#include
#include
#include
+#include
#include "cct_exception.hpp"
#include "currencycode.hpp"
@@ -17,17 +19,20 @@
#include "monetaryamountbycurrencyset.hpp"
namespace cct {
+class AbstractMarketDataSerializer;
class Exchange {
public:
using ExchangePublic = api::ExchangePublic;
/// Builds a Exchange without private exchange. All private requests will be forbidden.
- Exchange(const ExchangeConfig &exchangeConfig, api::ExchangePublic &exchangePublic);
+ Exchange(std::string_view dataDir, const ExchangeConfig &exchangeConfig, api::ExchangePublic &exchangePublic);
/// Build a Exchange with both private and public exchanges
- Exchange(const ExchangeConfig &exchangeConfig, api::ExchangePublic &exchangePublic,
+ Exchange(std::string_view dataDir, const ExchangeConfig &exchangeConfig, api::ExchangePublic &exchangePublic,
api::ExchangePrivate &exchangePrivate);
+ ~Exchange();
+
std::string_view name() const { return _exchangePublic.name(); }
std::string_view keyName() const { return apiPrivate().keyName(); }
@@ -86,16 +91,12 @@ class Exchange {
return _exchangePublic.queryAllApproximatedOrderBooks(depth);
}
- MarketOrderBook queryOrderBook(Market mk, int depth = ExchangePublic::kDefaultDepth) {
- return _exchangePublic.queryOrderBook(mk, depth);
- }
+ MarketOrderBook queryOrderBook(Market mk, int depth = ExchangePublic::kDefaultDepth);
MonetaryAmount queryLast24hVolume(Market mk) { return _exchangePublic.queryLast24hVolume(mk); }
/// Retrieve an ordered vector of recent last trades
- LastTradesVector queryLastTrades(Market mk, int nbTrades = ExchangePublic::kNbLastTradesDefault) {
- return _exchangePublic.queryLastTrades(mk, nbTrades);
- }
+ LastTradesVector queryLastTrades(Market mk, int nbTrades = ExchangePublic::kNbLastTradesDefault);
/// Retrieve the last price of given market.
MonetaryAmount queryLastPrice(Market mk) { return _exchangePublic.queryLastPrice(mk); }
@@ -110,9 +111,15 @@ class Exchange {
void updateCacheFile() const;
+ using trivially_relocatable = std::true_type;
+
private:
+ Exchange(std::string_view dataDir, const ExchangeConfig &exchangeConfig, api::ExchangePublic &exchangePublic,
+ api::ExchangePrivate *pExchangePrivate);
+
api::ExchangePublic &_exchangePublic;
api::ExchangePrivate *_pExchangePrivate = nullptr;
const ExchangeConfig &_exchangeConfig;
+ std::unique_ptr _marketDataSerializerPtr;
};
} // namespace cct
diff --git a/src/api/interface/src/exchange.cpp b/src/api/interface/src/exchange.cpp
index b6ed7dd8..651c29be 100644
--- a/src/api/interface/src/exchange.cpp
+++ b/src/api/interface/src/exchange.cpp
@@ -1,6 +1,7 @@
#include "exchange.hpp"
#include
+#include
#include "cct_log.hpp"
#include "currencycode.hpp"
@@ -8,17 +9,39 @@
#include "exchangeconfig.hpp"
#include "exchangeprivateapi.hpp"
#include "exchangepublicapi.hpp"
+#include "timedef.hpp"
+
+#ifdef CCT_ENABLE_PROTO
+#include "proto-market-data-serializer.hpp"
+#else
+#include "dummy-market-data-serializer.hpp"
+#endif
namespace cct {
-Exchange::Exchange(const ExchangeConfig &exchangeConfig, api::ExchangePublic &exchangePublic,
+#ifdef CCT_ENABLE_PROTO
+using MarketDataSerializer = ProtobufMarketDataSerializer;
+#else
+using MarketDataSerializer = DummyMarketDataSerializer;
+#endif
+
+Exchange::Exchange(std::string_view dataDir, const ExchangeConfig &exchangeConfig, api::ExchangePublic &exchangePublic,
api::ExchangePrivate &exchangePrivate)
+ : Exchange(dataDir, exchangeConfig, exchangePublic, std::addressof(exchangePrivate)) {}
+
+Exchange::Exchange(std::string_view dataDir, const ExchangeConfig &exchangeConfig, api::ExchangePublic &exchangePublic)
+ : Exchange(dataDir, exchangeConfig, exchangePublic, nullptr) {}
+
+Exchange::Exchange(std::string_view dataDir, const ExchangeConfig &exchangeConfig, api::ExchangePublic &exchangePublic,
+ api::ExchangePrivate *pExchangePrivate)
: _exchangePublic(exchangePublic),
- _pExchangePrivate(std::addressof(exchangePrivate)),
- _exchangeConfig(exchangeConfig) {}
+ _pExchangePrivate(pExchangePrivate),
+ _exchangeConfig(exchangeConfig),
+ _marketDataSerializerPtr(_exchangeConfig.withMarketDataSerialization()
+ ? new MarketDataSerializer(dataDir, exchangePublic.name())
+ : nullptr) {}
-Exchange::Exchange(const ExchangeConfig &exchangeConfig, api::ExchangePublic &exchangePublic)
- : _exchangePublic(exchangePublic), _exchangeConfig(exchangeConfig) {}
+Exchange::~Exchange() = default; // declared here to have definition of ~MarketDataSerializer
bool Exchange::canWithdraw(CurrencyCode currencyCode, const CurrencyExchangeFlatSet ¤cyExchangeSet) const {
if (_exchangeConfig.excludedCurrenciesWithdrawal().contains(currencyCode)) {
@@ -41,6 +64,23 @@ bool Exchange::canDeposit(CurrencyCode currencyCode, const CurrencyExchangeFlatS
return lb->canDeposit();
}
+MarketOrderBook Exchange::queryOrderBook(Market mk, int depth) {
+ auto marketOrderBook = _exchangePublic.queryOrderBook(mk, depth);
+ if (_marketDataSerializerPtr) {
+ _marketDataSerializerPtr->push(Clock::now(), marketOrderBook);
+ }
+ return marketOrderBook;
+}
+
+/// Retrieve an ordered vector of recent last trades
+LastTradesVector Exchange::queryLastTrades(Market mk, int nbTrades) {
+ auto lastTrades = _exchangePublic.queryLastTrades(mk, nbTrades);
+ if (_marketDataSerializerPtr) {
+ _marketDataSerializerPtr->push(lastTrades);
+ }
+ return lastTrades;
+}
+
void Exchange::updateCacheFile() const {
_exchangePublic.updateCacheFile();
if (_pExchangePrivate != nullptr) {
diff --git a/src/api/interface/src/exchangepool.cpp b/src/api/interface/src/exchangepool.cpp
index 1d04d5ef..6886f76f 100644
--- a/src/api/interface/src/exchangepool.cpp
+++ b/src/api/interface/src/exchangepool.cpp
@@ -31,6 +31,7 @@ ExchangePool::ExchangePool(const CoincenterInfo& coincenterInfo, FiatConverter&
_krakenPublic(_coincenterInfo, _fiatConverter, _commonAPI),
_kucoinPublic(_coincenterInfo, _fiatConverter, _commonAPI),
_upbitPublic(_coincenterInfo, _fiatConverter, _commonAPI) {
+ const auto dataDir = coincenterInfo.dataDir();
for (std::string_view exchangeStr : kSupportedExchanges) {
api::ExchangePublic* exchangePublic;
if (exchangeStr == "binance") {
@@ -81,10 +82,10 @@ ExchangePool::ExchangePool(const CoincenterInfo& coincenterInfo, FiatConverter&
}
}
- _exchanges.emplace_back(exchangeConfig, *exchangePublic, *exchangePrivate);
+ _exchanges.emplace_back(dataDir, exchangeConfig, *exchangePublic, *exchangePrivate);
}
} else {
- _exchanges.emplace_back(exchangeConfig, *exchangePublic);
+ _exchanges.emplace_back(dataDir, exchangeConfig, *exchangePublic);
}
}
_exchanges.shrink_to_fit();
diff --git a/src/engine/src/coincenteroptions.cpp b/src/engine/src/coincenteroptions.cpp
index 7871fee2..cf50b19d 100644
--- a/src/engine/src/coincenteroptions.cpp
+++ b/src/engine/src/coincenteroptions.cpp
@@ -37,6 +37,10 @@ std::ostream& CoincenterCmdLineOptions::PrintVersion(std::string_view programNam
os << "compiled with " << CCT_COMPILER_VERSION << " on " << __DATE__ << " at " << __TIME__ << '\n';
os << " " << GetCurlVersionInfo() << '\n';
os << " " << ssl::GetOpenSSLVersion() << '\n';
+#ifdef CCT_PROTOBUF_VERSION
+ os << " "
+ << "protobuf " << CCT_PROTOBUF_VERSION << '\n';
+#endif
return os;
}
diff --git a/src/engine/test/exchangedata_test.hpp b/src/engine/test/exchangedata_test.hpp
index e9aaa74d..9c9b46ac 100644
--- a/src/engine/test/exchangedata_test.hpp
+++ b/src/engine/test/exchangedata_test.hpp
@@ -40,14 +40,22 @@ class ExchangesBaseTest : public ::testing::Test {
api::MockExchangePrivate exchangePrivate6{exchangePublic3, coincenterInfo, key4};
api::MockExchangePrivate exchangePrivate7{exchangePublic3, coincenterInfo, key5};
api::MockExchangePrivate exchangePrivate8{exchangePublic1, coincenterInfo, key2};
- Exchange exchange1{coincenterInfo.exchangeConfig(exchangePublic1.name()), exchangePublic1, exchangePrivate1};
- Exchange exchange2{coincenterInfo.exchangeConfig(exchangePublic2.name()), exchangePublic2, exchangePrivate2};
- Exchange exchange3{coincenterInfo.exchangeConfig(exchangePublic3.name()), exchangePublic3, exchangePrivate3};
- Exchange exchange4{coincenterInfo.exchangeConfig(exchangePublic3.name()), exchangePublic3, exchangePrivate4};
- Exchange exchange5{coincenterInfo.exchangeConfig(exchangePublic3.name()), exchangePublic3, exchangePrivate5};
- Exchange exchange6{coincenterInfo.exchangeConfig(exchangePublic3.name()), exchangePublic3, exchangePrivate6};
- Exchange exchange7{coincenterInfo.exchangeConfig(exchangePublic3.name()), exchangePublic3, exchangePrivate7};
- Exchange exchange8{coincenterInfo.exchangeConfig(exchangePublic1.name()), exchangePublic1, exchangePrivate8};
+ Exchange exchange1{kDefaultDataDir, coincenterInfo.exchangeConfig(exchangePublic1.name()), exchangePublic1,
+ exchangePrivate1};
+ Exchange exchange2{kDefaultDataDir, coincenterInfo.exchangeConfig(exchangePublic2.name()), exchangePublic2,
+ exchangePrivate2};
+ Exchange exchange3{kDefaultDataDir, coincenterInfo.exchangeConfig(exchangePublic3.name()), exchangePublic3,
+ exchangePrivate3};
+ Exchange exchange4{kDefaultDataDir, coincenterInfo.exchangeConfig(exchangePublic3.name()), exchangePublic3,
+ exchangePrivate4};
+ Exchange exchange5{kDefaultDataDir, coincenterInfo.exchangeConfig(exchangePublic3.name()), exchangePublic3,
+ exchangePrivate5};
+ Exchange exchange6{kDefaultDataDir, coincenterInfo.exchangeConfig(exchangePublic3.name()), exchangePublic3,
+ exchangePrivate6};
+ Exchange exchange7{kDefaultDataDir, coincenterInfo.exchangeConfig(exchangePublic3.name()), exchangePublic3,
+ exchangePrivate7};
+ Exchange exchange8{kDefaultDataDir, coincenterInfo.exchangeConfig(exchangePublic1.name()), exchangePublic1,
+ exchangePrivate8};
Market m1{"ETH", "EUR"};
Market m2{"BTC", "EUR"};
@@ -102,4 +110,4 @@ class ExchangesBaseTest : public ::testing::Test {
BalancePortfolio balancePortfolio4{amounts4};
BalancePortfolio emptyBalance;
};
-} // namespace cct
\ No newline at end of file
+} // namespace cct
diff --git a/src/main/CMakeLists.txt b/src/main/CMakeLists.txt
index d97cde5b..56d9fc2c 100644
--- a/src/main/CMakeLists.txt
+++ b/src/main/CMakeLists.txt
@@ -15,6 +15,10 @@ endif()
target_link_libraries(coincenter PUBLIC coincenter_engine)
+if(CCT_ENABLE_PROTO)
+ target_link_libraries(coincenter PUBLIC protobuf::libprotobuf)
+endif()
+
set_target_properties(coincenter PROPERTIES
VERSION ${PROJECT_VERSION}
COMPILE_DEFINITIONS_DEBUG "JSON_DEBUG;JSON_SAFE;JSON_ISO_STRICT"
diff --git a/src/monitoring/include/prometheusmetricgateway.hpp b/src/monitoring/include/prometheusmetricgateway.hpp
index 6be53b15..6615b86f 100644
--- a/src/monitoring/include/prometheusmetricgateway.hpp
+++ b/src/monitoring/include/prometheusmetricgateway.hpp
@@ -5,9 +5,7 @@
#include
#include
-#include
#include
-#include
#include "abstractmetricgateway.hpp"
#include "timedef.hpp"
diff --git a/src/monitoring/include/voidmetricgateway.hpp b/src/monitoring/include/voidmetricgateway.hpp
index a0341a23..e4386ca9 100644
--- a/src/monitoring/include/voidmetricgateway.hpp
+++ b/src/monitoring/include/voidmetricgateway.hpp
@@ -1,8 +1,8 @@
#pragma once
-#include
-
#include "abstractmetricgateway.hpp"
+#include "metric.hpp"
+#include "monitoringinfo.hpp"
namespace cct {
class VoidMetricGateway : public AbstractMetricGateway {
diff --git a/src/objects/include/exchangeconfig.hpp b/src/objects/include/exchangeconfig.hpp
index 6af42bf3..255dbc11 100644
--- a/src/objects/include/exchangeconfig.hpp
+++ b/src/objects/include/exchangeconfig.hpp
@@ -1,5 +1,6 @@
#pragma once
+#include
#include
#include "apiquerytypeenum.hpp"
@@ -17,6 +18,7 @@ namespace cct {
class ExchangeConfig {
public:
enum struct FeeType { kMaker, kTaker };
+ enum class MarketDataSerialization : int8_t { kYes, kNo };
struct APIUpdateFrequencies {
Duration freq[api::kQueryTypeMax];
@@ -29,7 +31,7 @@ class ExchangeConfig {
std::string_view acceptEncoding, int dustSweeperMaxNbTrades,
log::level::level_enum requestsCallLogLevel, log::level::level_enum requestsAnswerLogLevel,
bool multiTradeAllowedByDefault, bool validateDepositAddressesInFile, bool placeSimulateRealOrder,
- bool validateApiKey, TradeConfig tradeConfig);
+ bool validateApiKey, TradeConfig tradeConfig, MarketDataSerialization marketDataSerialization);
/// Get a reference to the list of statically excluded currency codes to consider for the exchange,
/// In both trading and withdrawal.
@@ -98,6 +100,8 @@ class ExchangeConfig {
const TradeConfig &tradeConfig() const { return _tradeConfig; }
+ bool withMarketDataSerialization() const { return _withMarketSerialization; }
+
private:
CurrencyCodeSet _excludedCurrenciesAll; // Currencies will be completely ignored by the exchange
CurrencyCodeSet _excludedCurrenciesWithdrawal; // Currencies unavailable for withdrawals
@@ -118,5 +122,6 @@ class ExchangeConfig {
bool _validateDepositAddressesInFile;
bool _placeSimulateRealOrder;
bool _validateApiKey;
+ bool _withMarketSerialization;
};
} // namespace cct
diff --git a/src/objects/include/market.hpp b/src/objects/include/market.hpp
index 576018ed..f4d3c007 100644
--- a/src/objects/include/market.hpp
+++ b/src/objects/include/market.hpp
@@ -101,4 +101,4 @@ struct hash {
return cct::HashCombine(hash()(mk.base()), hash()(mk.quote()));
}
};
-} // namespace std
\ No newline at end of file
+} // namespace std
diff --git a/src/objects/include/marketorderbook.hpp b/src/objects/include/marketorderbook.hpp
index ea9a3184..27c7fbaa 100644
--- a/src/objects/include/marketorderbook.hpp
+++ b/src/objects/include/marketorderbook.hpp
@@ -3,6 +3,7 @@
#include
#include
#include
+#include
#include "cct_smallvector.hpp"
#include "market.hpp"
@@ -138,6 +139,8 @@ class MarketOrderBook {
std::optional computeAvgPrice(MonetaryAmount from, const PriceOptions& priceOptions) const;
+ VolAndPriNbDecimals volAndPriNbDecimals() const noexcept { return _volAndPriNbDecimals; }
+
/// Print the market order book in a SimpleTable and returns it.
/// @param conversionPriceRate prices will be multiplied to given amount to display an additional column of equivalent
/// currency
@@ -149,7 +152,7 @@ class MarketOrderBook {
struct AmountPrice {
using AmountType = MonetaryAmount::AmountType;
- bool operator==(const AmountPrice& o) const noexcept = default;
+ bool operator==(const AmountPrice&) const noexcept = default;
AmountType amount = 0;
AmountType price = 0;
@@ -161,7 +164,7 @@ class MarketOrderBook {
public:
using trivially_relocatable = is_trivially_relocatable::type;
- bool operator==(const MarketOrderBook&) const = default;
+ bool operator==(const MarketOrderBook&) const noexcept = default;
private:
/// Represents a total amount of waiting orders at a given price.
diff --git a/src/objects/include/monetaryamount.hpp b/src/objects/include/monetaryamount.hpp
index 1285600d..084a3a32 100644
--- a/src/objects/include/monetaryamount.hpp
+++ b/src/objects/include/monetaryamount.hpp
@@ -100,9 +100,13 @@ class MonetaryAmount {
setNbDecimals(monetaryAmount.nbDecimals());
}
+ /// Get an integral representation of this MonetaryAmount multiplied by current number of decimals.
+ /// Example: "5.6235" with 6 decimals will return 5623500
+ [[nodiscard]] AmountType amount() const { return _amount; }
+
/// Get an integral representation of this MonetaryAmount multiplied by given number of decimals.
/// If an overflow would occur for the resulting amount, return std::nullopt
- /// Example : "5.6235" with 6 decimals will return 5623500
+ /// Example: "5.6235" with 6 decimals will return 5623500
[[nodiscard]] std::optional amount(int8_t nbDecimals) const;
/// Get the integer part of the amount of this MonetaryAmount.
diff --git a/src/api-objects/include/publictrade.hpp b/src/objects/include/publictrade.hpp
similarity index 83%
rename from src/api-objects/include/publictrade.hpp
rename to src/objects/include/publictrade.hpp
index 96671651..6dc0ad11 100644
--- a/src/api-objects/include/publictrade.hpp
+++ b/src/objects/include/publictrade.hpp
@@ -3,6 +3,7 @@
#include
#include "cct_string.hpp"
+#include "market.hpp"
#include "monetaryamount.hpp"
#include "timedef.hpp"
#include "tradeside.hpp"
@@ -17,6 +18,8 @@ class PublicTrade {
TradeSide side() const { return _side; }
+ Market market() const { return Market(_amount.currencyCode(), _price.currencyCode()); }
+
MonetaryAmount amount() const { return _amount; }
MonetaryAmount price() const { return _price; }
@@ -26,7 +29,7 @@ class PublicTrade {
/// 3 way operator - make compiler generate all 6 operators (including == and !=)
/// we order by time first, then amount, price, etc. Do not change the fields order!
- auto operator<=>(const PublicTrade &) const = default;
+ std::strong_ordering operator<=>(const PublicTrade&) const noexcept = default;
private:
TimePoint _time;
diff --git a/src/api-objects/include/tradeside.hpp b/src/objects/include/tradeside.hpp
similarity index 100%
rename from src/api-objects/include/tradeside.hpp
rename to src/objects/include/tradeside.hpp
diff --git a/src/objects/src/coincenterinfo.cpp b/src/objects/src/coincenterinfo.cpp
index 053c4ad9..b02d16a0 100644
--- a/src/objects/src/coincenterinfo.cpp
+++ b/src/objects/src/coincenterinfo.cpp
@@ -2,7 +2,6 @@
#include
#include
-#include
#include
#include
@@ -21,6 +20,7 @@
#include "runmodes.hpp"
#include "toupperlower-string.hpp"
#include "toupperlower.hpp"
+
#ifdef CCT_ENABLE_PROMETHEUS
#include "prometheusmetricgateway.hpp"
#else
diff --git a/src/objects/src/exchangeconfig.cpp b/src/objects/src/exchangeconfig.cpp
index 4cfa4a24..95f2e6ca 100644
--- a/src/objects/src/exchangeconfig.cpp
+++ b/src/objects/src/exchangeconfig.cpp
@@ -57,14 +57,17 @@ auto DustSweeperMaxNbTrades(int dustSweeperMaxNbTrades) {
}
} // namespace
-ExchangeConfig::ExchangeConfig(
- std::string_view exchangeNameStr, std::string_view makerStr, std::string_view takerStr,
- CurrencyCodeVector &&excludedAllCurrencies, CurrencyCodeVector &&excludedCurrenciesWithdraw,
- CurrencyCodeVector &&preferredPaymentCurrencies, MonetaryAmountByCurrencySet &&dustAmountsThreshold,
- const APIUpdateFrequencies &apiUpdateFrequencies, Duration publicAPIRate, Duration privateAPIRate,
- std::string_view acceptEncoding, int dustSweeperMaxNbTrades, log::level::level_enum requestsCallLogLevel,
- log::level::level_enum requestsAnswerLogLevel, bool multiTradeAllowedByDefault, bool validateDepositAddressesInFile,
- bool placeSimulateRealOrder, bool validateApiKey, TradeConfig tradeConfig)
+ExchangeConfig::ExchangeConfig(std::string_view exchangeNameStr, std::string_view makerStr, std::string_view takerStr,
+ CurrencyCodeVector &&excludedAllCurrencies,
+ CurrencyCodeVector &&excludedCurrenciesWithdraw,
+ CurrencyCodeVector &&preferredPaymentCurrencies,
+ MonetaryAmountByCurrencySet &&dustAmountsThreshold,
+ const APIUpdateFrequencies &apiUpdateFrequencies, Duration publicAPIRate,
+ Duration privateAPIRate, std::string_view acceptEncoding, int dustSweeperMaxNbTrades,
+ log::level::level_enum requestsCallLogLevel,
+ log::level::level_enum requestsAnswerLogLevel, bool multiTradeAllowedByDefault,
+ bool validateDepositAddressesInFile, bool placeSimulateRealOrder, bool validateApiKey,
+ TradeConfig tradeConfig, MarketDataSerialization marketDataSerialization)
: _excludedCurrenciesAll(std::move(excludedAllCurrencies)),
_excludedCurrenciesWithdrawal(std::move(excludedCurrenciesWithdraw)),
_preferredPaymentCurrencies(std::move(preferredPaymentCurrencies)),
@@ -82,7 +85,8 @@ ExchangeConfig::ExchangeConfig(
_multiTradeAllowedByDefault(multiTradeAllowedByDefault),
_validateDepositAddressesInFile(validateDepositAddressesInFile),
_placeSimulateRealOrder(placeSimulateRealOrder),
- _validateApiKey(validateApiKey) {
+ _validateApiKey(validateApiKey),
+ _withMarketSerialization(marketDataSerialization == MarketDataSerialization::kYes) {
if (dustSweeperMaxNbTrades > std::numeric_limits::max() || dustSweeperMaxNbTrades < 0) {
throw exception("Invalid number of dust sweeper max trades '{}', should be in [0, {}]", dustSweeperMaxNbTrades,
std::numeric_limits::max());
@@ -108,6 +112,7 @@ ExchangeConfig::ExchangeConfig(
_validateDepositAddressesInFile ? kDepositAddressesFileName : "");
log::trace(" - Order placing in simulation : {}", _placeSimulateRealOrder ? "real, unmatchable" : "none");
log::trace(" - Validate API Key : {}", _validateApiKey ? "yes" : "no");
+ log::trace(" - Market data serialization : {}", _withMarketSerialization ? "yes" : "no");
}
if (_preferredPaymentCurrencies.empty()) {
log::warn("{} list of preferred currencies is empty, buy and sell commands cannot perform trades", exchangeNameStr);
diff --git a/src/objects/src/exchangeconfigdefault.hpp b/src/objects/src/exchangeconfigdefault.hpp
index f1b0143f..0bdbf0f4 100644
--- a/src/objects/src/exchangeconfigdefault.hpp
+++ b/src/objects/src/exchangeconfigdefault.hpp
@@ -56,6 +56,7 @@ struct ExchangeConfigDefault {
"requestsCall": "info",
"requestsAnswer": "trace"
},
+ "marketDataSerialization": true,
"multiTradeAllowedByDefault": false,
"placeSimulateRealOrder": false,
"trade": {
@@ -180,6 +181,7 @@ struct ExchangeConfigDefault {
"requestsCall": "info",
"requestsAnswer": "trace"
},
+ "marketDataSerialization": false,
"multiTradeAllowedByDefault": true,
"privateAPIRate": "1055ms",
"publicAPIRate": "1236ms",
diff --git a/src/objects/src/exchangeconfigmap.cpp b/src/objects/src/exchangeconfigmap.cpp
index 1f1cabc2..69494b67 100644
--- a/src/objects/src/exchangeconfigmap.cpp
+++ b/src/objects/src/exchangeconfigmap.cpp
@@ -55,6 +55,10 @@ ExchangeConfigMap ComputeExchangeConfigMap(std::string_view fileName, const json
withdrawTopLevelOption.getBool(exchangeName, "validateDepositAddressesInFile");
const bool placeSimulatedRealOrder = queryTopLevelOption.getBool(exchangeName, "placeSimulateRealOrder");
const bool validateApiKey = queryTopLevelOption.getBool(exchangeName, "validateApiKey");
+ const ExchangeConfig::MarketDataSerialization marketDataSerialization =
+ queryTopLevelOption.getBool(exchangeName, "marketDataSerialization")
+ ? ExchangeConfig::MarketDataSerialization::kYes
+ : ExchangeConfig::MarketDataSerialization::kNo;
MonetaryAmountByCurrencySet dustAmountsThresholds(
queryTopLevelOption.getMonetaryAmountsArray(exchangeName, "dustAmountsThreshold"));
@@ -82,7 +86,7 @@ ExchangeConfigMap ComputeExchangeConfigMap(std::string_view fileName, const json
std::move(dustAmountsThresholds), std::move(apiUpdateFrequencies), publicAPIRate, privateAPIRate,
acceptEncoding, dustSweeperMaxNbTrades, requestsCallLogLevel, requestsAnswerLogLevel,
multiTradeAllowedByDefault, validateDepositAddressesInFile, placeSimulatedRealOrder,
- validateApiKey, std::move(tradeConfig)));
+ validateApiKey, std::move(tradeConfig), marketDataSerialization));
} // namespace cct
// Print json unused values
@@ -112,4 +116,4 @@ ExchangeConfigMap ComputeExchangeConfigMap(std::string_view fileName, const json
return map;
}
-} // namespace cct
\ No newline at end of file
+} // namespace cct
diff --git a/src/objects/src/marketorderbook.cpp b/src/objects/src/marketorderbook.cpp
index 749546b4..c5f1bc3e 100644
--- a/src/objects/src/marketorderbook.cpp
+++ b/src/objects/src/marketorderbook.cpp
@@ -91,6 +91,7 @@ MarketOrderBook::MarketOrderBook(MonetaryAmount askPrice, MonetaryAmount askVolu
if (bidVolume <= 0) {
throw exception("Invalid bid volume {}{}", bidVolume, kErrNegVolumeMsg);
}
+
static constexpr MonetaryAmount::RoundType roundType = MonetaryAmount::RoundType::kNearest;
askPrice.round(_volAndPriNbDecimals.priNbDecimals, roundType);
diff --git a/src/objects/src/monetaryamount.cpp b/src/objects/src/monetaryamount.cpp
index 5dc95f03..0563ebf5 100644
--- a/src/objects/src/monetaryamount.cpp
+++ b/src/objects/src/monetaryamount.cpp
@@ -193,7 +193,6 @@ MonetaryAmount::MonetaryAmount(double amount, CurrencyCode currencyCode, RoundTy
}
std::optional MonetaryAmount::amount(int8_t nbDecimals) const {
- assert(nbDecimals >= 0);
AmountType integralAmount = _amount;
const int8_t ourNbDecimals = this->nbDecimals();
for (; nbDecimals < ourNbDecimals; ++nbDecimals) {
diff --git a/src/api-objects/src/publictrade.cpp b/src/objects/src/publictrade.cpp
similarity index 98%
rename from src/api-objects/src/publictrade.cpp
rename to src/objects/src/publictrade.cpp
index 5ba601fa..423275d0 100644
--- a/src/api-objects/src/publictrade.cpp
+++ b/src/objects/src/publictrade.cpp
@@ -4,5 +4,7 @@
#include "timestring.hpp"
namespace cct {
+
string PublicTrade::timeStr() const { return ToString(_time); }
+
} // namespace cct
\ No newline at end of file
diff --git a/src/objects/test/balanceportfolio_test.cpp b/src/objects/test/balanceportfolio_test.cpp
index 2848b2a7..df3ae051 100644
--- a/src/objects/test/balanceportfolio_test.cpp
+++ b/src/objects/test/balanceportfolio_test.cpp
@@ -4,6 +4,7 @@
#include
#include "monetaryamount.hpp"
+
namespace cct {
class BalancePortfolioTest1 : public ::testing::Test {
diff --git a/src/objects/test/market_test.cpp b/src/objects/test/market_test.cpp
index 432d083a..c9fdd98e 100644
--- a/src/objects/test/market_test.cpp
+++ b/src/objects/test/market_test.cpp
@@ -51,4 +51,4 @@ TEST(MarketTest, StringRepresentationFiatConversionMarket) {
EXPECT_EQ(market.assetsPairStrUpper('('), "*USDT(EUR");
EXPECT_EQ(market.assetsPairStrLower(')'), "*usdt)eur");
}
-} // namespace cct
\ No newline at end of file
+} // namespace cct
diff --git a/src/serialization/CMakeLists.txt b/src/serialization/CMakeLists.txt
new file mode 100644
index 00000000..92125438
--- /dev/null
+++ b/src/serialization/CMakeLists.txt
@@ -0,0 +1,34 @@
+if(CCT_ENABLE_PROTO)
+ aux_source_directory(src SERIALIZATION_SRC)
+
+ list(APPEND SERIALIZATION_SRC "${CMAKE_CURRENT_LIST_DIR}/proto/market-order-book-timed-data.proto")
+ list(APPEND SERIALIZATION_SRC "${CMAKE_CURRENT_LIST_DIR}/proto/trade-data.proto")
+else()
+ set(SERIALIZATION_SRC "src/dummy-market-data-serializer.cpp")
+endif()
+
+add_library(coincenter_serialization OBJECT ${SERIALIZATION_SRC})
+
+target_include_directories(coincenter_serialization PUBLIC include)
+target_link_libraries(coincenter_serialization PUBLIC coincenter_objects)
+
+if(CCT_ENABLE_PROTO)
+ set(PROTO_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated")
+
+ target_include_directories(coincenter_serialization PUBLIC "$")
+
+ target_link_libraries(coincenter_serialization PUBLIC protobuf::libprotobuf)
+
+ protobuf_generate(
+ TARGET coincenter_serialization
+ IMPORT_DIRS "${CMAKE_CURRENT_LIST_DIR}/proto"
+ PROTOC_OUT_DIR "${PROTO_BINARY_DIR}"
+ )
+
+ add_unit_test(
+ proto-multiple-messages-handler_test
+ test/proto-multiple-messages-handler_test.cpp
+ LIBRARIES
+ coincenter_serialization
+ )
+endif()
\ No newline at end of file
diff --git a/src/serialization/include/abstract-market-data-serializer.hpp b/src/serialization/include/abstract-market-data-serializer.hpp
new file mode 100644
index 00000000..4e1c6bd3
--- /dev/null
+++ b/src/serialization/include/abstract-market-data-serializer.hpp
@@ -0,0 +1,18 @@
+#pragma once
+
+#include
+
+#include "marketorderbook.hpp"
+#include "publictrade.hpp"
+#include "timedef.hpp"
+
+namespace cct {
+class AbstractMarketDataSerializer {
+ public:
+ virtual ~AbstractMarketDataSerializer() = default;
+
+ virtual void push(TimePoint timeStamp, const MarketOrderBook &marketOrderBook) = 0;
+
+ virtual void push(std::span publicTrades) = 0;
+};
+} // namespace cct
\ No newline at end of file
diff --git a/src/serialization/include/dummy-market-data-serializer.hpp b/src/serialization/include/dummy-market-data-serializer.hpp
new file mode 100644
index 00000000..a93932e1
--- /dev/null
+++ b/src/serialization/include/dummy-market-data-serializer.hpp
@@ -0,0 +1,22 @@
+#pragma once
+
+#include
+#include
+
+#include "abstract-market-data-serializer.hpp"
+#include "marketorderbook.hpp"
+#include "publictrade.hpp"
+#include "timedef.hpp"
+
+namespace cct {
+/// Implementation of a market data serializer that does nothing.
+/// Useful if coincenter is not compiled with protobuf support.
+class DummyMarketDataSerializer : public AbstractMarketDataSerializer {
+ public:
+ DummyMarketDataSerializer([[maybe_unused]] std::string_view dataDir, [[maybe_unused]] std::string_view exchangeName);
+
+ void push([[maybe_unused]] TimePoint timeStamp, [[maybe_unused]] const MarketOrderBook &marketOrderBook) override;
+
+ void push([[maybe_unused]] std::span publicTrades) override;
+};
+} // namespace cct
\ No newline at end of file
diff --git a/src/serialization/include/proto-constants.hpp b/src/serialization/include/proto-constants.hpp
new file mode 100644
index 00000000..f820e648
--- /dev/null
+++ b/src/serialization/include/proto-constants.hpp
@@ -0,0 +1,14 @@
+#pragma once
+
+#include
+#include
+
+namespace cct {
+
+enum class ProtobufObject : int8_t { kMarketOrderBook, kTrade };
+
+static constexpr std::string_view kBinProtobufExtension = ".binpb";
+
+static constexpr std::string_view kSubPathMarketOrderBook = "market-order-book";
+static constexpr std::string_view kSubPathTrades = "trades";
+} // namespace cct
\ No newline at end of file
diff --git a/src/serialization/include/proto-market-accumulator.hpp b/src/serialization/include/proto-market-accumulator.hpp
new file mode 100644
index 00000000..015aa578
--- /dev/null
+++ b/src/serialization/include/proto-market-accumulator.hpp
@@ -0,0 +1,191 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "cct_log.hpp"
+#include "cct_vector.hpp"
+#include "durationstring.hpp"
+#include "market.hpp"
+#include "proto-constants.hpp"
+#include "proto-multiple-messages-handler.hpp"
+#include "stringhelpers.hpp"
+#include "timedef.hpp"
+
+namespace cct {
+/// Class responsible to accumulate protobuf objects in memory and perform regular flushes of its data to the disk.
+/// Data is accumulated by Market and will write to following files (from subPath):
+/// 'BASECUR-QUOTECUR/YYYY/MM/DD/HH:00:00_HH:59:59.binpb'
+///
+/// If you may push duplicated objects, you have to provide Comp and Equal types.
+/// In this case, Equal must be consistent with Comp, and the first criteria of the comparison should be the timestamp
+/// (ordered from oldest to youngest).
+///
+/// You may not provide any Comp and Equal if by design you will not push duplicated data.
+template
+class MarketAccumulator {
+ public:
+ static_assert((std::is_void_v && std::is_void_v) || (!std::is_void_v && !std::is_void_v));
+
+ using ProtobufObjectTypeVector = vector;
+ using ProtobufObjectTypePerMarketMap = std::unordered_map;
+
+ explicit MarketAccumulator(std::string subPath) noexcept : _subPath(std::move(subPath)) {}
+
+ MarketAccumulator(const MarketAccumulator &) = delete;
+ MarketAccumulator &operator=(const MarketAccumulator &) = delete;
+
+ MarketAccumulator(MarketAccumulator &&other) noexcept { swap(other); }
+
+ MarketAccumulator &operator=(MarketAccumulator &&other) noexcept {
+ finalWriteOnDiskNoExcept();
+ swap(other);
+ return *this;
+ }
+
+ ~MarketAccumulator() { finalWriteOnDiskNoExcept(); }
+
+ void push(Market market, const ProtobufObjectType &protoObj) {
+ auto &protobufObjectsVector = _toBeFlushedData[market];
+ protobufObjectsVector.push_back(protoObj);
+ registerPush(market, protobufObjectsVector);
+ }
+
+ void push(Market market, ProtobufObjectType &&protoObj) {
+ auto &protobufObjectsVector = _toBeFlushedData[market];
+ protobufObjectsVector.push_back(std::move(protoObj));
+ registerPush(market, protobufObjectsVector);
+ }
+
+ void setDirectory(Market market, TimePoint tp, std::string &pathStr) const {
+ // Note: below code could be simplified once compilers fully implement std::format and chrono C++20
+ // libraries.
+ const auto dp = std::chrono::floor(tp);
+ const std::chrono::year_month_day ymd{dp};
+
+ pathStr.replace(pathStr.begin() + _subPath.size(), pathStr.end(),
+ fmt::format("{}/{:04}/{:02}/{:02}/", market.str(), static_cast(ymd.year()),
+ static_cast(ymd.month()), static_cast(ymd.day())));
+ }
+
+ static void AddFilePath(TimePoint tp, std::string &pathStr) {
+ const auto dp = std::chrono::floor(tp);
+ const std::chrono::hh_mm_ss time{std::chrono::floor(tp - dp)};
+
+ const auto dayHour = std::chrono::duration_cast(time.hours()).count();
+ const auto beforeHoursSize = pathStr.size();
+ if (dayHour < 10) {
+ pathStr.push_back('0');
+ }
+ AppendString(pathStr, dayHour);
+ const auto afterHoursSize = pathStr.size();
+ pathStr.append(":00:00_");
+ pathStr.append(pathStr.begin() + beforeHoursSize, pathStr.begin() + afterHoursSize);
+ pathStr.append(":59:59");
+ pathStr.append(kBinProtobufExtension);
+ }
+
+ void swap(MarketAccumulator &rhs) noexcept {
+ _toBeFlushedData.swap(rhs._toBeFlushedData);
+ _subPath.swap(rhs._subPath);
+ }
+
+ private:
+ void finalWriteOnDiskNoExcept() noexcept {
+ try {
+ for (auto &[market, protobufObjectsVector] : _toBeFlushedData) {
+ writeOnDisk(market, protobufObjectsVector);
+ }
+ } catch (const std::exception &e) {
+ log::error("exception caught in writeOnDisk: {}", e.what());
+ }
+ }
+
+ void registerPush(Market market, ProtobufObjectTypeVector &protobufObjectsVector) {
+ if (protobufObjectsVector.size() == kFlushPeriod) {
+ writeOnDisk(market, protobufObjectsVector);
+ }
+ }
+
+ void writeOnDisk(Market market, ProtobufObjectTypeVector &protobufObjectsVector) {
+ const auto nowTime = std::chrono::steady_clock::now();
+
+ Filter(market, protobufObjectsVector);
+
+ std::string pathStr = _subPath;
+
+ std::chrono::time_point prevHours{};
+ ProtobufMessagesWriter protobufMessagesWriter;
+ for (const auto &protobufObject : protobufObjectsVector) {
+ openFile(market, protobufObject, prevHours, pathStr, protobufMessagesWriter);
+
+ protobufMessagesWriter.write(protobufObject);
+ }
+
+ // shrink_to_fit as vector will never grow-up larger than its current size
+ protobufObjectsVector.shrink_to_fit();
+
+ auto nbElemsWritten = protobufObjectsVector.size();
+ protobufObjectsVector.clear();
+
+ if (nbElemsWritten != 0) {
+ log::info("Wrote {} objects for {} timed data in {}, last in {}", nbElemsWritten, market,
+ DurationToString(std::chrono::steady_clock::now() - nowTime), pathStr);
+ }
+ }
+
+ static void Filter(Market market, ProtobufObjectTypeVector &protobufObjectsVector) {
+ // Sort by timestamp (required by 'writeOnDisk' algorithm) and removes invalid elements (if any)
+ std::ranges::sort(protobufObjectsVector, [](const auto &lhs, const auto &rhs) {
+ if (lhs.has_unixtimestampinms() != rhs.has_unixtimestampinms()) {
+ return lhs.has_unixtimestampinms();
+ }
+ if (!lhs.has_unixtimestampinms()) {
+ return false;
+ }
+ return lhs.unixtimestampinms() < rhs.unixtimestampinms();
+ });
+ auto begInvalidElemsIt = std::ranges::partition_point(
+ protobufObjectsVector, [](const auto &protoObj) { return protoObj.has_unixtimestampinms(); });
+ if (begInvalidElemsIt != protobufObjectsVector.end()) {
+ log::error("Invalid data for {} - no timestamp set for {} objects", market,
+ protobufObjectsVector.end() - begInvalidElemsIt);
+ protobufObjectsVector.erase(begInvalidElemsIt, protobufObjectsVector.end());
+ }
+
+ // If duplicate elements are possible, remove them
+ if constexpr (!std::is_void_v) {
+ std::ranges::sort(protobufObjectsVector, Comp{});
+ const auto [ret, last] = std::ranges::unique(protobufObjectsVector, Equal{});
+ protobufObjectsVector.erase(ret, protobufObjectsVector.end());
+ }
+ }
+
+ void openFile(Market market, const ProtobufObjectType &protobufObject,
+ std::chrono::time_point &prevHours, std::string &pathStr,
+ ProtobufMessagesWriter &protobufMessagesWriter) {
+ const TimePoint tp{std::chrono::milliseconds{protobufObject.unixtimestampinms()}};
+ const auto hours = std::chrono::floor(tp);
+
+ if (prevHours != hours) {
+ // reset outfile
+ setDirectory(market, tp, pathStr);
+ std::filesystem::create_directories(std::filesystem::path(pathStr));
+ AddFilePath(tp, pathStr);
+ std::filesystem::path filePath(pathStr);
+
+ protobufMessagesWriter.open(std::ofstream(filePath, std::ios_base::app));
+ prevHours = hours;
+ }
+ }
+
+ ProtobufObjectTypePerMarketMap _toBeFlushedData;
+ std::string _subPath;
+};
+} // namespace cct
\ No newline at end of file
diff --git a/src/serialization/include/proto-market-data-serializer.hpp b/src/serialization/include/proto-market-data-serializer.hpp
new file mode 100644
index 00000000..a7136361
--- /dev/null
+++ b/src/serialization/include/proto-market-data-serializer.hpp
@@ -0,0 +1,39 @@
+#pragma once
+
+#include
+#include
+
+#include "abstract-market-data-serializer.hpp"
+#include "market-order-book-timed-data.pb.h"
+#include "marketorderbook.hpp"
+#include "proto-market-accumulator.hpp"
+#include "publictrade.hpp"
+#include "timedef.hpp"
+#include "trade-data.pb.h"
+
+namespace cct {
+/// This class is responsible of managing the periodic writes to disk of timed market data, for a given exchange.
+/// This class is not thread safe
+class ProtobufMarketDataSerializer : public AbstractMarketDataSerializer {
+ public:
+ ProtobufMarketDataSerializer(std::string_view dataDir, std::string_view exchangeName);
+
+ /// Push market order book timed data in the MarketDataSerializer.
+ void push(TimePoint timeStamp, const MarketOrderBook &marketOrderBook) override;
+
+ /// Push public trades timed data in the MarketDataSerializer.
+ void push(std::span publicTrades) override;
+
+ private:
+ struct TradeDataComp {
+ bool operator()(const ::objects::TradeData &lhs, const ::objects::TradeData &rhs) const;
+ };
+
+ struct TradeDataEqual {
+ bool operator()(const ::objects::TradeData &lhs, const ::objects::TradeData &rhs) const;
+ };
+
+ MarketAccumulator<::objects::MarketOrderBookTimedData, 500> _marketOrderBookAccumulator;
+ MarketAccumulator<::objects::TradeData, 5000, TradeDataComp, TradeDataEqual> _tradesAccumulator;
+};
+} // namespace cct
\ No newline at end of file
diff --git a/src/serialization/include/proto-market-order-book.hpp b/src/serialization/include/proto-market-order-book.hpp
new file mode 100644
index 00000000..d37de577
--- /dev/null
+++ b/src/serialization/include/proto-market-order-book.hpp
@@ -0,0 +1,10 @@
+#pragma once
+
+#include "market-order-book-timed-data.pb.h"
+#include "marketorderbook.hpp"
+#include "timedef.hpp"
+
+namespace cct {
+::objects::MarketOrderBookTimedData CreateMarketOrderBookTimedData(const MarketOrderBook &marketOrderBook,
+ TimePoint timeStamp);
+}
\ No newline at end of file
diff --git a/src/serialization/include/proto-multiple-messages-handler.hpp b/src/serialization/include/proto-multiple-messages-handler.hpp
new file mode 100644
index 00000000..8a14566f
--- /dev/null
+++ b/src/serialization/include/proto-multiple-messages-handler.hpp
@@ -0,0 +1,75 @@
+#pragma once
+
+#include
+
+#include
+#include
+
+#include "cct_exception.hpp"
+#include "cct_log.hpp"
+
+namespace cct {
+class ProtobufMessagesReader {
+ public:
+ explicit ProtobufMessagesReader(std::istream& is) : _is(is), _iis(&_is), _cis(&_iis) {}
+
+ bool hasNext() { return _cis.ReadVarint64(&_nextSize); }
+
+ template
+ MsgT next() {
+ MsgT msg;
+ auto msgLimit = _cis.PushLimit(_nextSize);
+ if (!msg.ParseFromCodedStream(&_cis)) {
+ log::error("Error reading single protobuf message of size {}", _nextSize);
+ }
+ _cis.PopLimit(msgLimit);
+ return msg;
+ }
+
+ private:
+ std::istream& _is;
+ ::google::protobuf::io::IstreamInputStream _iis;
+ ::google::protobuf::io::CodedInputStream _cis;
+ uint64_t _nextSize{};
+};
+
+template
+class ProtobufMessagesWriter {
+ public:
+ void open(OStreamType&& newOs) {
+ // reverse destroy streams to flush latest data. Recreate the streams after creation of new ofstream
+ _cos.reset();
+ _oos.reset();
+ _os = std::move(newOs);
+ _oos = std::make_unique<::google::protobuf::io::OstreamOutputStream>(&_os);
+ _cos = std::make_unique<::google::protobuf::io::CodedOutputStream>(_oos.get());
+ }
+
+ template
+ void write(const MsgT& msg) {
+ if (!_cos) {
+ throw exception("ProtobufMessagesWriter::open should have been called first");
+ }
+
+ _cos->WriteVarint64(msg.ByteSizeLong());
+
+ if (!msg.SerializeToCodedStream(_cos.get())) {
+ log::error("Failed to serialize to coded stream");
+ }
+ }
+
+ OStreamType flush() {
+ _cos.reset();
+ _oos.reset();
+
+ OStreamType ret(std::move(_os));
+ _os = OStreamType();
+ return ret;
+ }
+
+ private:
+ OStreamType _os;
+ std::unique_ptr<::google::protobuf::io::OstreamOutputStream> _oos;
+ std::unique_ptr<::google::protobuf::io::CodedOutputStream> _cos;
+};
+} // namespace cct
\ No newline at end of file
diff --git a/src/serialization/include/proto-public-trade.hpp b/src/serialization/include/proto-public-trade.hpp
new file mode 100644
index 00000000..96c22333
--- /dev/null
+++ b/src/serialization/include/proto-public-trade.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "market.hpp"
+#include "publictrade.hpp"
+#include "trade-data.pb.h"
+
+namespace cct {
+::objects::TradeData CreateTradeData(const PublicTrade &publicTrade);
+
+PublicTrade CreatePublicTrade(Market market, const ::objects::TradeData &tradeData);
+} // namespace cct
\ No newline at end of file
diff --git a/src/serialization/include/proto-reader.hpp b/src/serialization/include/proto-reader.hpp
new file mode 100644
index 00000000..75ab24cb
--- /dev/null
+++ b/src/serialization/include/proto-reader.hpp
@@ -0,0 +1,137 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "cct_log.hpp"
+#include "cct_vector.hpp"
+#include "durationstring.hpp"
+#include "market.hpp"
+#include "proto-multiple-messages-handler.hpp"
+#include "timedef.hpp"
+
+namespace cct {
+
+class ProtobufObjectsGateway {
+ public:
+ template
+ using ProtobufObjVector = vector;
+
+ template
+ using ProtobufObjPerMarketMap = std::unordered_map>;
+
+ explicit ProtobufObjectsGateway(std::string_view exchangeSerializedDataPath);
+
+ /// Load all data found on disk for the time window [fromTimeStamp, toTimeStamp]
+ template
+ ProtobufObjPerMarketMap load(TimePoint fromTimeStamp, TimePoint toTimeStamp) {
+ ProtobufObjPerMarketMap ret;
+
+ const auto pathStr = _exchangeSerializedDataPath;
+ if (!std::filesystem::is_directory(pathStr)) {
+ return ret;
+ }
+
+ const auto nowTime = std::chrono::steady_clock::now();
+ const auto fromDays = std::chrono::floor(fromTimeStamp);
+ const std::chrono::year_month_day fromYmd{fromDays};
+ const std::chrono::hh_mm_ss fromTime{std::chrono::floor(fromTimeStamp - fromDays)};
+
+ const auto toDays = std::chrono::floor(toTimeStamp);
+ const std::chrono::year_month_day toYmd{toDays};
+ const std::chrono::hh_mm_ss toTime{std::chrono::floor(toTimeStamp - toDays)};
+
+ int nbElemsRead = 0;
+
+ for (const auto& marketDirectory : std::filesystem::directory_iterator(pathStr)) {
+ if (!marketDirectory.is_directory()) {
+ continue;
+ }
+ const auto& marketPath = marketDirectory.path();
+ const auto marketStr = marketPath.filename().string();
+ const Market market(marketStr);
+
+ const int fromYear = static_cast(fromYmd.year());
+ const int toYear = static_cast(toYmd.year());
+ for (int year = fromYear; year <= toYear; ++year) {
+ const auto yearPath = marketPath / fmt::format("{:04}", year);
+ if (!std::filesystem::is_directory(yearPath)) {
+ continue;
+ }
+ const auto fromMonth = year == fromYear ? static_cast(fromYmd.month()) : 1U;
+ const auto toMonth = year == toYear ? static_cast(toYmd.month()) : 12U;
+ for (std::remove_const_t month = fromMonth; month <= toMonth; ++month) {
+ const auto monthPath = yearPath / fmt::format("{:02}", month);
+ if (!std::filesystem::is_directory(monthPath)) {
+ continue;
+ }
+ const auto fromDay = year == fromYear && month == fromMonth ? static_cast(fromYmd.day()) : 1U;
+ const auto toDay = year == toYear && month == toMonth ? static_cast(toYmd.day()) : 31U;
+ for (std::remove_const_t day = fromDay; day <= toDay; ++day) {
+ const auto dayPath = monthPath / fmt::format("{:02}", day);
+ if (!std::filesystem::is_directory(dayPath)) {
+ continue;
+ }
+ for (const auto& binProtobufFile : std::filesystem::directory_iterator(dayPath)) {
+ if (!binProtobufFile.is_regular_file()) {
+ continue;
+ }
+ const auto& filePath = binProtobufFile.path();
+ const auto fileName = filePath.filename().string();
+
+ int hour{};
+ const auto [ptr, ec] = std::from_chars(fileName.data(), fileName.data() + 2, hour);
+ if (ec != std::errc() || ptr - fileName.data() != 2) {
+ log::error("Unable to load bin protobuf file {} because of error {} trying to convert to hour range",
+ filePath.string(), static_cast(ec));
+ continue;
+ }
+ if (year == toYear && month == toMonth && day == toDay &&
+ hour > std::chrono::duration_cast(toTime.hours()).count()) {
+ // not within our time window
+ continue;
+ }
+ std::ifstream ifs(filePath, std::ios::in | std::ios::binary);
+ ProtobufMessagesReader multipleProtobufMessagesReader(ifs);
+ while (multipleProtobufMessagesReader.hasNext()) {
+ auto msg = multipleProtobufMessagesReader.next();
+ if (!ValidateTimestamp(msg, fromTimeStamp, toTimeStamp)) {
+ continue;
+ }
+ ret[market].push_back(std::move(msg));
+ ++nbElemsRead;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ const auto steadyClockDuration = std::chrono::steady_clock::now() - nowTime;
+ const Duration dur = std::chrono::duration_cast(steadyClockDuration);
+
+ log::info("Read {} protobuf data in {}", nbElemsRead, DurationToString(dur));
+
+ return ret;
+ }
+
+ private:
+ template
+ static bool ValidateTimestamp(const ProtobufObjType& msg, TimePoint fromTimeStamp, TimePoint toTimeStamp) {
+ if (!msg.has_unixtimestampinms()) {
+ log::error("Invalid data loaded for protobuf object, no unix timestamp set");
+ return false;
+ }
+ const auto timeStampInMs = msg.unixtimestampinms();
+ return timeStampInMs >= TimestampToMs(fromTimeStamp) && timeStampInMs <= TimestampToMs(toTimeStamp);
+ }
+
+ std::string_view _exchangeSerializedDataPath;
+};
+
+} // namespace cct
\ No newline at end of file
diff --git a/src/serialization/proto/market-order-book-timed-data.proto b/src/serialization/proto/market-order-book-timed-data.proto
new file mode 100644
index 00000000..286fdf0b
--- /dev/null
+++ b/src/serialization/proto/market-order-book-timed-data.proto
@@ -0,0 +1,21 @@
+syntax = "proto3";
+
+package objects;
+
+message MarketOrderBookTimedData {
+ optional int64 unixTimestampInMs = 1;
+ optional int32 volumeNbDecimals = 2;
+ optional int32 priceNbDecimals = 3;
+
+ message PricedVolume {
+ optional int64 price = 1;
+ optional int64 volume = 2;
+ }
+
+ message OrderBook {
+ repeated PricedVolume asks = 1;
+ repeated PricedVolume bids = 2;
+ }
+
+ optional OrderBook orderBook = 4;
+}
\ No newline at end of file
diff --git a/src/serialization/proto/trade-data.proto b/src/serialization/proto/trade-data.proto
new file mode 100644
index 00000000..f1399429
--- /dev/null
+++ b/src/serialization/proto/trade-data.proto
@@ -0,0 +1,18 @@
+syntax = "proto3";
+
+package objects;
+
+enum TradeSide {
+ TRADE_UNSPECIFIED = 0;
+ TRADE_BUY = 1;
+ TRADE_SELL = 2;
+}
+
+message TradeData {
+ optional int64 unixTimestampInMs = 1;
+ optional int64 priceAmount = 2;
+ optional int64 volumeAmount = 3;
+ optional int32 priceNbDecimals = 4;
+ optional int32 volumeNbDecimals = 5;
+ TradeSide tradeSide = 6;
+}
\ No newline at end of file
diff --git a/src/serialization/src/dummy-market-data-serializer.cpp b/src/serialization/src/dummy-market-data-serializer.cpp
new file mode 100644
index 00000000..8c60adaa
--- /dev/null
+++ b/src/serialization/src/dummy-market-data-serializer.cpp
@@ -0,0 +1,20 @@
+#include "dummy-market-data-serializer.hpp"
+
+#include
+#include
+
+#include "marketorderbook.hpp"
+#include "publictrade.hpp"
+#include "timedef.hpp"
+
+namespace cct {
+
+DummyMarketDataSerializer::DummyMarketDataSerializer([[maybe_unused]] std::string_view dataDir,
+ [[maybe_unused]] std::string_view exchangeName) {}
+
+void DummyMarketDataSerializer::push([[maybe_unused]] TimePoint timeStamp,
+ [[maybe_unused]] const MarketOrderBook &marketOrderBook) {}
+
+void DummyMarketDataSerializer::push([[maybe_unused]] std::span publicTrades) {}
+
+} // namespace cct
\ No newline at end of file
diff --git a/src/serialization/src/proto-market-data-serializer.cpp b/src/serialization/src/proto-market-data-serializer.cpp
new file mode 100644
index 00000000..6d5ab140
--- /dev/null
+++ b/src/serialization/src/proto-market-data-serializer.cpp
@@ -0,0 +1,73 @@
+#include "proto-market-data-serializer.hpp"
+
+#include
+#include
+
+#include "marketorderbook.hpp"
+#include "monetaryamount.hpp"
+#include "proto-constants.hpp"
+#include "proto-market-order-book.hpp"
+#include "proto-public-trade.hpp"
+#include "publictrade.hpp"
+#include "timedef.hpp"
+
+namespace cct {
+namespace {
+std::string ComputeSubPath(std::string_view dataDir, std::string_view exchangeName,
+ std::string_view protobufObjectName) {
+ std::string ret(dataDir);
+ ret.append("/serialized/");
+ ret.append(protobufObjectName);
+ ret.push_back('/');
+ ret.append(exchangeName);
+ ret.push_back('/');
+ return ret;
+}
+} // namespace
+
+bool ProtobufMarketDataSerializer::TradeDataComp::operator()(const ::objects::TradeData& lhs,
+ const ::objects::TradeData& rhs) const {
+ if (lhs.unixtimestampinms() != rhs.unixtimestampinms()) {
+ return lhs.unixtimestampinms() < rhs.unixtimestampinms();
+ }
+ if (lhs.tradeside() != rhs.tradeside()) {
+ return lhs.tradeside() < rhs.tradeside();
+ }
+ MonetaryAmount lhsPrice(lhs.priceamount(), CurrencyCode{}, lhs.pricenbdecimals());
+ MonetaryAmount rhsPrice(rhs.priceamount(), CurrencyCode{}, rhs.pricenbdecimals());
+ if (lhsPrice != rhsPrice) {
+ return lhsPrice < rhsPrice;
+ }
+ MonetaryAmount lhsAmount(lhs.volumeamount(), CurrencyCode{}, lhs.volumenbdecimals());
+ MonetaryAmount rhsAmount(rhs.volumeamount(), CurrencyCode{}, rhs.volumenbdecimals());
+ if (lhsAmount != rhsAmount) {
+ return lhsAmount < rhsAmount;
+ }
+ return false;
+}
+
+bool ProtobufMarketDataSerializer::TradeDataEqual::operator()(const ::objects::TradeData& lhs,
+ const ::objects::TradeData& rhs) const {
+ return lhs.unixtimestampinms() == rhs.unixtimestampinms() && lhs.tradeside() == rhs.tradeside() &&
+ MonetaryAmount(lhs.priceamount(), CurrencyCode{}, lhs.pricenbdecimals()) ==
+ MonetaryAmount(rhs.priceamount(), CurrencyCode{}, rhs.pricenbdecimals()) &&
+ MonetaryAmount(lhs.volumeamount(), CurrencyCode{}, lhs.volumenbdecimals()) ==
+ MonetaryAmount(rhs.volumeamount(), CurrencyCode{}, rhs.volumenbdecimals());
+}
+
+ProtobufMarketDataSerializer::ProtobufMarketDataSerializer(std::string_view dataDir, std::string_view exchangeName)
+ : _marketOrderBookAccumulator(ComputeSubPath(dataDir, exchangeName, kSubPathMarketOrderBook)),
+ _tradesAccumulator(ComputeSubPath(dataDir, exchangeName, kSubPathTrades)) {}
+
+void ProtobufMarketDataSerializer::push(TimePoint timeStamp, const MarketOrderBook& marketOrderBook) {
+ _marketOrderBookAccumulator.push(marketOrderBook.market(),
+ CreateMarketOrderBookTimedData(marketOrderBook, timeStamp));
+}
+
+void ProtobufMarketDataSerializer::push(std::span publicTrades) {
+ for (const auto& publicTrade : publicTrades) {
+ _tradesAccumulator.push(publicTrade.market(), CreateTradeData(publicTrade));
+ }
+}
+
+} // namespace cct
\ No newline at end of file
diff --git a/src/serialization/src/proto-market-order-book.cpp b/src/serialization/src/proto-market-order-book.cpp
new file mode 100644
index 00000000..493cad6c
--- /dev/null
+++ b/src/serialization/src/proto-market-order-book.cpp
@@ -0,0 +1,44 @@
+#include "proto-market-order-book.hpp"
+
+#include "market-order-book-timed-data.pb.h"
+#include "marketorderbook.hpp"
+#include "timedef.hpp"
+
+namespace cct {
+::objects::MarketOrderBookTimedData CreateMarketOrderBookTimedData(const MarketOrderBook& marketOrderBook,
+ TimePoint timeStamp) {
+ ::objects::MarketOrderBookTimedData protoObj;
+
+ const auto volAndPriNbDecimals = marketOrderBook.volAndPriNbDecimals();
+ const auto unixTimestampInMs = TimestampToMs(timeStamp);
+
+ protoObj.set_unixtimestampinms(unixTimestampInMs);
+ protoObj.set_volumenbdecimals(volAndPriNbDecimals.volNbDecimals);
+ protoObj.set_pricenbdecimals(volAndPriNbDecimals.priNbDecimals);
+
+ auto& orderBook = *protoObj.mutable_orderbook();
+
+ const auto priNbDecimals = protoObj.pricenbdecimals();
+ const auto volNbDecimals = protoObj.volumenbdecimals();
+
+ const int nbBids = marketOrderBook.nbBidPrices();
+ for (int bidPos = 1; bidPos <= nbBids; ++bidPos) {
+ auto [price, volume] = marketOrderBook[-bidPos];
+ auto& pricedVolume = *orderBook.add_bids();
+
+ pricedVolume.set_price(price.amount(priNbDecimals).value());
+ pricedVolume.set_volume(volume.amount(volNbDecimals).value());
+ }
+
+ const int nbAsks = marketOrderBook.nbAskPrices();
+ for (int askPos = 1; askPos <= nbAsks; ++askPos) {
+ auto [price, volume] = marketOrderBook[askPos];
+ auto& pricedVolume = *orderBook.add_asks();
+
+ pricedVolume.set_price(price.amount(priNbDecimals).value());
+ pricedVolume.set_volume(volume.amount(volNbDecimals).value());
+ }
+
+ return protoObj;
+}
+} // namespace cct
\ No newline at end of file
diff --git a/src/serialization/src/proto-public-trade.cpp b/src/serialization/src/proto-public-trade.cpp
new file mode 100644
index 00000000..58d36144
--- /dev/null
+++ b/src/serialization/src/proto-public-trade.cpp
@@ -0,0 +1,48 @@
+#include "proto-public-trade.hpp"
+
+#include "monetaryamount.hpp"
+#include "publictrade.hpp"
+#include "timedef.hpp"
+#include "trade-data.pb.h"
+#include "tradeside.hpp"
+#include "unreachable.hpp"
+
+namespace cct {
+namespace {
+::objects::TradeSide ProtobufTradeSide(TradeSide tradeSide) {
+ switch (tradeSide) {
+ case TradeSide::kBuy:
+ return ::objects::TRADE_BUY;
+ case TradeSide::kSell:
+ return ::objects::TRADE_SELL;
+ default:
+ unreachable();
+ }
+}
+} // namespace
+
+::objects::TradeData CreateTradeData(const PublicTrade &publicTrade) {
+ ::objects::TradeData protoObj;
+
+ protoObj.set_unixtimestampinms(TimestampToMs(publicTrade.time()));
+
+ protoObj.set_priceamount(publicTrade.price().amount());
+ protoObj.set_pricenbdecimals(publicTrade.price().nbDecimals());
+
+ protoObj.set_volumeamount(publicTrade.amount().amount());
+ protoObj.set_volumenbdecimals(publicTrade.amount().nbDecimals());
+
+ protoObj.set_tradeside(ProtobufTradeSide(publicTrade.side()));
+
+ return protoObj;
+}
+
+PublicTrade CreatePublicTrade(Market market, const ::objects::TradeData &tradeData) {
+ TradeSide tradeSide = tradeData.tradeside() == ::objects::TradeSide::TRADE_BUY ? TradeSide::kBuy : TradeSide::kSell;
+ MonetaryAmount amount(tradeData.volumeamount(), market.base(), tradeData.volumenbdecimals());
+ MonetaryAmount price(tradeData.priceamount(), market.quote(), tradeData.pricenbdecimals());
+ TimePoint timeStamp(TimeInMs(tradeData.unixtimestampinms()));
+
+ return {tradeSide, amount, price, timeStamp};
+}
+} // namespace cct
\ No newline at end of file
diff --git a/src/serialization/src/proto-reader.cpp b/src/serialization/src/proto-reader.cpp
new file mode 100644
index 00000000..d8af50e6
--- /dev/null
+++ b/src/serialization/src/proto-reader.cpp
@@ -0,0 +1,10 @@
+#include "proto-reader.hpp"
+
+#include
+
+namespace cct {
+
+ProtobufObjectsGateway::ProtobufObjectsGateway(std::string_view exchangeSerializedDataPath)
+ : _exchangeSerializedDataPath(exchangeSerializedDataPath) {}
+
+} // namespace cct
\ No newline at end of file
diff --git a/src/serialization/test/proto-market-accumulator_test.cpp b/src/serialization/test/proto-market-accumulator_test.cpp
new file mode 100644
index 00000000..7b243ad7
--- /dev/null
+++ b/src/serialization/test/proto-market-accumulator_test.cpp
@@ -0,0 +1 @@
+#include
\ No newline at end of file
diff --git a/src/serialization/test/proto-multiple-messages-handler_test.cpp b/src/serialization/test/proto-multiple-messages-handler_test.cpp
new file mode 100644
index 00000000..40ffdf65
--- /dev/null
+++ b/src/serialization/test/proto-multiple-messages-handler_test.cpp
@@ -0,0 +1,120 @@
+#include "proto-multiple-messages-handler.hpp"
+
+#include
+
+#include
+#include
+
+#include "proto-public-trade.hpp"
+#include "publictrade.hpp"
+#include "trade-data.pb.h"
+
+namespace cct {
+class ProtobufMessagesTest : public ::testing::Test {
+ protected:
+ ProtobufMessagesWriter writer;
+
+ TimePoint tp1{TimeInMs{std::numeric_limits::max() / 10000000}};
+ TimePoint tp2{TimeInMs{std::numeric_limits::max() / 9000000}};
+ TimePoint tp3{TimeInMs{std::numeric_limits::max() / 8000000}};
+
+ Market market{"ETH", "USDT"};
+
+ PublicTrade pt1{TradeSide::kBuy, MonetaryAmount{"0.13", "ETH"}, MonetaryAmount{"1500.5", "USDT"}, tp1};
+ PublicTrade pt2{TradeSide::kSell, MonetaryAmount{"3.7", "ETH"}, MonetaryAmount{"1500.5", "USDT"}, tp2};
+ PublicTrade pt3{TradeSide::kBuy, MonetaryAmount{"0.004", "ETH"}, MonetaryAmount{1501, "USDT"}, tp3};
+
+ ::objects::TradeData td1{CreateTradeData(pt1)};
+ ::objects::TradeData td2{CreateTradeData(pt2)};
+ ::objects::TradeData td3{CreateTradeData(pt3)};
+};
+
+TEST_F(ProtobufMessagesTest, WriteReadSingle) {
+ writer.open(std::stringstream{});
+ writer.write(td1);
+
+ std::stringstream ss = writer.flush();
+
+ ProtobufMessagesReader reader{ss};
+
+ int nbObjectsRead = 0;
+
+ while (reader.hasNext()) {
+ ::objects::TradeData nextObj = reader.next<::objects::TradeData>();
+ PublicTrade pt = CreatePublicTrade(market, nextObj);
+
+ EXPECT_EQ(pt, pt1);
+ ++nbObjectsRead;
+ }
+ EXPECT_EQ(nbObjectsRead, 1);
+}
+
+TEST_F(ProtobufMessagesTest, WriteRead2Flushes) {
+ writer.open(std::stringstream{});
+ writer.write(td1);
+ std::stringstream ss1 = writer.flush();
+
+ writer.open(std::stringstream{});
+ writer.write(td2);
+ std::stringstream ss2 = writer.flush();
+
+ ProtobufMessagesReader reader1{ss1};
+
+ int nbObjectsRead = 0;
+
+ while (reader1.hasNext()) {
+ auto nextObj = reader1.next<::objects::TradeData>();
+ PublicTrade pt = CreatePublicTrade(market, nextObj);
+
+ EXPECT_EQ(pt, pt1);
+ ++nbObjectsRead;
+ }
+ EXPECT_EQ(nbObjectsRead, 1);
+
+ ProtobufMessagesReader reader2{ss2};
+
+ while (reader2.hasNext()) {
+ auto nextObj = reader2.next<::objects::TradeData>();
+ PublicTrade pt = CreatePublicTrade(market, nextObj);
+
+ EXPECT_EQ(pt, pt2);
+ ++nbObjectsRead;
+ }
+ EXPECT_EQ(nbObjectsRead, 2);
+}
+
+TEST_F(ProtobufMessagesTest, WriteReadSeveral) {
+ writer.open(std::stringstream{});
+ writer.write(td1);
+ writer.write(td2);
+ writer.write(td3);
+
+ std::stringstream ss = writer.flush();
+
+ ProtobufMessagesReader reader{ss};
+
+ int nbObjectsRead = 0;
+
+ while (reader.hasNext()) {
+ ::objects::TradeData nextObj = reader.next<::objects::TradeData>();
+ PublicTrade pt = CreatePublicTrade(market, nextObj);
+
+ switch (nbObjectsRead) {
+ case 0:
+ EXPECT_EQ(pt, pt1);
+ break;
+ case 1:
+ EXPECT_EQ(pt, pt2);
+ break;
+ case 2:
+ EXPECT_EQ(pt, pt3);
+ break;
+ default:
+ break;
+ }
+
+ ++nbObjectsRead;
+ }
+ EXPECT_EQ(nbObjectsRead, 3);
+}
+} // namespace cct
\ No newline at end of file