From abbb9e5b352fdf55d6deea67175266e2abb8621b Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Tue, 26 May 2026 13:31:39 +0100 Subject: [PATCH 1/5] Adding elasticsearch client --- CMakeLists.txt | 4 +- .../project.pbxproj | 32 +++++-- include/elasticClient.hpp | 17 ++++ include/tradingResults.hpp | 46 ++++++++++ source/elasticClient.cpp | 83 +++++++++++++++++++ source/tradingResults.cpp | 49 +++++++++++ 6 files changed, 225 insertions(+), 6 deletions(-) create mode 100644 include/elasticClient.hpp create mode 100644 include/tradingResults.hpp create mode 100644 source/elasticClient.cpp create mode 100644 source/tradingResults.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e2f4b1..8886861 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,11 +70,13 @@ endif() find_package(Boost REQUIRED CONFIG) find_package(OpenSSL REQUIRED) find_package(Threads REQUIRED) +find_package(CURL REQUIRED) target_link_libraries(BacktestingEngineLib PUBLIC Boost::headers OpenSSL::SSL OpenSSL::Crypto - Threads::Threads) + Threads::Threads + CURL::libcurl) add_subdirectory(external/boost-decimal) target_link_libraries(BacktestingEngineLib PUBLIC Boost::decimal) diff --git a/backtesting-engine-cpp.xcodeproj/project.pbxproj b/backtesting-engine-cpp.xcodeproj/project.pbxproj index a04d080..7e8142a 100644 --- a/backtesting-engine-cpp.xcodeproj/project.pbxproj +++ b/backtesting-engine-cpp.xcodeproj/project.pbxproj @@ -25,6 +25,12 @@ 942EC56A2FBEF95000CCBB5D /* backtestRunner.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942EC5642FBEF95000CCBB5D /* backtestRunner.cpp */; }; 942EC56B2FBEF95000CCBB5D /* redisLoader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942EC5652FBEF95000CCBB5D /* redisLoader.cpp */; }; 942EC56C2FBEF95000CCBB5D /* redisRunner.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942EC5662FBEF95000CCBB5D /* redisRunner.cpp */; }; + 942FDDE02FC5C8B20096F318 /* tradingResults.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942FDDDF2FC5C8B20096F318 /* tradingResults.cpp */; }; + 942FDDE12FC5C8B20096F318 /* elasticClient.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942FDDDE2FC5C8B20096F318 /* elasticClient.cpp */; }; + 942FDDE22FC5C8B20096F318 /* tradingResults.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942FDDDF2FC5C8B20096F318 /* tradingResults.cpp */; }; + 942FDDE32FC5C8B20096F318 /* elasticClient.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942FDDDE2FC5C8B20096F318 /* elasticClient.cpp */; }; + 942FDDE52FC5C9D30096F318 /* libcurl.4.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 942FDDE42FC5C9D30096F318 /* libcurl.4.tbd */; }; + 942FDDE62FC5C9DB0096F318 /* libcurl.4.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 942FDDE42FC5C9D30096F318 /* libcurl.4.tbd */; }; 943398242D57E53400287A2D /* jsonParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 943398232D57E53400287A2D /* jsonParser.cpp */; }; 943398252D57E53400287A2D /* jsonParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 943398232D57E53400287A2D /* jsonParser.cpp */; }; 943398272D57E54000287A2D /* jsonParser.mm in Sources */ = {isa = PBXBuildFile; fileRef = 943398262D57E54000287A2D /* jsonParser.mm */; }; @@ -90,6 +96,11 @@ 942EC5642FBEF95000CCBB5D /* backtestRunner.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = backtestRunner.cpp; sourceTree = ""; }; 942EC5652FBEF95000CCBB5D /* redisLoader.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = redisLoader.cpp; sourceTree = ""; }; 942EC5662FBEF95000CCBB5D /* redisRunner.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = redisRunner.cpp; sourceTree = ""; }; + 942FDDDC2FC5C8950096F318 /* elasticClient.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = elasticClient.hpp; sourceTree = ""; }; + 942FDDDD2FC5C8A30096F318 /* tradingResults.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = tradingResults.hpp; sourceTree = ""; }; + 942FDDDE2FC5C8B20096F318 /* elasticClient.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = elasticClient.cpp; sourceTree = ""; }; + 942FDDDF2FC5C8B20096F318 /* tradingResults.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = tradingResults.cpp; sourceTree = ""; }; + 942FDDE42FC5C9D30096F318 /* libcurl.4.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libcurl.4.tbd; path = usr/lib/libcurl.4.tbd; sourceTree = SDKROOT; }; 943398222D57E52900287A2D /* jsonParser.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = jsonParser.hpp; sourceTree = ""; }; 943398232D57E53400287A2D /* jsonParser.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = jsonParser.cpp; sourceTree = ""; }; 943398262D57E54000287A2D /* jsonParser.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = jsonParser.mm; sourceTree = ""; }; @@ -1296,6 +1307,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 942FDDE62FC5C9DB0096F318 /* libcurl.4.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1303,6 +1315,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 942FDDE52FC5C9D30096F318 /* libcurl.4.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1436,6 +1449,8 @@ 94280BA72D2FC29F00F1CF56 /* utilities */, 942EC5642FBEF95000CCBB5D /* backtestRunner.cpp */, 942EC5652FBEF95000CCBB5D /* redisLoader.cpp */, + 942FDDDE2FC5C8B20096F318 /* elasticClient.cpp */, + 942FDDDF2FC5C8B20096F318 /* tradingResults.cpp */, 942EC5662FBEF95000CCBB5D /* redisRunner.cpp */, 9470B5A32C8C5AD0007D9CC6 /* main.cpp */, 940A61112C92CE210083FEB8 /* configManager.cpp */, @@ -2218,6 +2233,7 @@ 94CD87342D2D2EE100041BBA /* Frameworks */ = { isa = PBXGroup; children = ( + 942FDDE42FC5C9D30096F318 /* libcurl.4.tbd */, 94CD8B982D2DCDD800041BBA /* libpqxx-7.10.a */, 94CD8B9A2D2DCF6E00041BBA /* libpqxx-7.10.a */, 94CD8B712D2D34C800041BBA /* config */, @@ -3579,17 +3595,19 @@ isa = PBXGroup; children = ( 94D3A7232FC1B3A600EBEA32 /* commands */, - 942EC55E2FBEF93A00CCBB5D /* backtestRunner.hpp */, - 942EC55F2FBEF93A00CCBB5D /* redisLoader.hpp */, - 942EC5602FBEF93A00CCBB5D /* redisRunner.hpp */, - 94D601132FA9CD890066F51A /* strategies */, - 94674B842D533B2F00973137 /* trading */, 942966D72D48E84100532862 /* models */, + 94674B842D533B2F00973137 /* trading */, + 94D601132FA9CD890066F51A /* strategies */, 94B8C7932D3D770800E17EB6 /* utilities */, 941B548F2D3BBA3B00E3BF64 /* trading_definitions */, 941B549C2D3BBFB900E3BF64 /* trading_definitions.hpp */, + 942EC55E2FBEF93A00CCBB5D /* backtestRunner.hpp */, + 942EC55F2FBEF93A00CCBB5D /* redisLoader.hpp */, + 942EC5602FBEF93A00CCBB5D /* redisRunner.hpp */, 940A61162C92CE960083FEB8 /* serviceA.hpp */, + 942FDDDC2FC5C8950096F318 /* elasticClient.hpp */, 943398222D57E52900287A2D /* jsonParser.hpp */, + 942FDDDD2FC5C8A30096F318 /* tradingResults.hpp */, 940A61122C92CE210083FEB8 /* configManager.hpp */, 941408B02D59F954000ED1F9 /* sqlManager.hpp */, 94724A852F8B92E30029B940 /* operations.hpp */, @@ -3695,6 +3713,8 @@ 94D3A7272FC1B3AD00EBEA32 /* loadCommand.cpp in Sources */, 9470B5A42C8C5AD0007D9CC6 /* main.cpp in Sources */, 943398252D57E53400287A2D /* jsonParser.cpp in Sources */, + 942FDDE02FC5C8B20096F318 /* tradingResults.cpp in Sources */, + 942FDDE12FC5C8B20096F318 /* elasticClient.cpp in Sources */, 94D3A72A2FC1B41500EBEA32 /* runCommand.cpp in Sources */, 942EC56A2FBEF95000CCBB5D /* backtestRunner.cpp in Sources */, 942EC56B2FBEF95000CCBB5D /* redisLoader.cpp in Sources */, @@ -3737,6 +3757,8 @@ 94674B882D533B4000973137 /* tradeManager.cpp in Sources */, 946EFF7F2FB9F44E008D9647 /* reporting.cpp in Sources */, 943398272D57E54000287A2D /* jsonParser.mm in Sources */, + 942FDDE22FC5C8B20096F318 /* tradingResults.cpp in Sources */, + 942FDDE32FC5C8B20096F318 /* elasticClient.cpp in Sources */, 9470B5B62C8C5BFD007D9CC6 /* main.cpp in Sources */, 94364CB62D416D8D00F35B55 /* db.mm in Sources */, 940A61142C92CE210083FEB8 /* configManager.cpp in Sources */, diff --git a/include/elasticClient.hpp b/include/elasticClient.hpp new file mode 100644 index 0000000..72506c5 --- /dev/null +++ b/include/elasticClient.hpp @@ -0,0 +1,17 @@ +// Backtesting Engine in C++ +// +// (c) 2026 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- + +#pragma once + +#include "tradingResults.hpp" + +// Minimal Elasticsearch HTTP client — PUT-only, for indexing TradingResults. +// Host is read from $ELASTICSEARCH_URL (default http://localhost:9200); +// docs land in index "trading_results" with a freshly generated UUID per put. +class ElasticClient { +public: + static int putTradingResults(const TradingResults& results); +}; diff --git a/include/tradingResults.hpp b/include/tradingResults.hpp new file mode 100644 index 0000000..ea8d740 --- /dev/null +++ b/include/tradingResults.hpp @@ -0,0 +1,46 @@ +// Backtesting Engine in C++ +// +// (c) 2026 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- + +#pragma once + +#include +#include +#include + +#include +#include + +#include "utilities/decimal_json.hpp" +#include "trading_definitions.hpp" + +// Per-run summary mirroring what Reporting::summarise prints today. +struct TradingResultsStats { + boost::decimal::decimal64_t finalPnl; + std::size_t tradesOpened; + std::size_t tradesClosed; + std::size_t openedLong; + std::size_t openedShort; + std::size_t closedLong; + std::size_t closedShort; + std::size_t winners; + std::size_t losers; + std::size_t breakeven; + std::optional avgPnl; +}; + +// Wire shape pushed to Elasticsearch: the input Configuration plus the run's +// output stats. Serialises the timestamp as `@timestamp` for Kibana. +struct TradingResults { + std::string RUN_ID; + std::string timestamp; + trading_definitions::Configuration config; + TradingResultsStats results; + + static std::string nowIsoUtc(); +}; + +void to_json(nlohmann::json& j, const TradingResultsStats& s); +void to_json(nlohmann::json& j, const TradingResults& r); diff --git a/source/elasticClient.cpp b/source/elasticClient.cpp new file mode 100644 index 0000000..a7b9436 --- /dev/null +++ b/source/elasticClient.cpp @@ -0,0 +1,83 @@ +// Backtesting Engine in C++ +// +// (c) 2026 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- + +#include "elasticClient.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +void ensureCurlInit() { + static const struct CurlGlobal { + CurlGlobal() { curl_global_init(CURL_GLOBAL_ALL); } + ~CurlGlobal() { curl_global_cleanup(); } + } guard; + (void)guard; +} + +std::string envOr(const char* name, std::string fallback) { + const char* val = std::getenv(name); + return val ? std::string{val} : std::move(fallback); +} + +std::string generateUuid() { + static thread_local boost::uuids::random_generator gen; + return boost::uuids::to_string(gen()); +} + +} // namespace + +int ElasticClient::putTradingResults(const TradingResults& results) { + ensureCurlInit(); + + const std::string host = envOr("ELASTICSEARCH_URL", "http://localhost:9200"); + const std::string url = host + "/trading_results/_doc/" + generateUuid(); + const std::string body = nlohmann::json(results).dump(); + + CURL* curl = curl_easy_init(); + if (!curl) { + std::cerr << "ElasticClient: curl_easy_init failed" << std::endl; + return 1; + } + + struct curl_slist* headers = nullptr; + headers = curl_slist_append(headers, "Content-Type: application/json"); + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(body.size())); + + const CURLcode rc = curl_easy_perform(curl); + + long httpStatus = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpStatus); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + if (rc != CURLE_OK) { + std::cerr << "ElasticClient: PUT failed: " << curl_easy_strerror(rc) + << std::endl; + return 2; + } + if (httpStatus < 200 || httpStatus >= 300) { + std::cerr << "ElasticClient: HTTP " << httpStatus << " from " << url + << std::endl; + return 3; + } + std::cout << "ElasticClient: PUT " << url << " (HTTP " << httpStatus << ")" + << std::endl; + return 0; +} diff --git a/source/tradingResults.cpp b/source/tradingResults.cpp new file mode 100644 index 0000000..e9619d8 --- /dev/null +++ b/source/tradingResults.cpp @@ -0,0 +1,49 @@ +// Backtesting Engine in C++ +// +// (c) 2026 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- + +#include "tradingResults.hpp" + +#include +#include + +std::string TradingResults::nowIsoUtc() { + const auto now = std::chrono::system_clock::to_time_t( + std::chrono::system_clock::now()); + std::tm tm_buf{}; + gmtime_r(&now, &tm_buf); + char buf[32]; + std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &tm_buf); + return std::string{buf}; +} + +void to_json(nlohmann::json& j, const TradingResultsStats& s) { + j = nlohmann::json{ + {"finalPnl", s.finalPnl}, + {"tradesOpened", s.tradesOpened}, + {"tradesClosed", s.tradesClosed}, + {"openedLong", s.openedLong}, + {"openedShort", s.openedShort}, + {"closedLong", s.closedLong}, + {"closedShort", s.closedShort}, + {"winners", s.winners}, + {"losers", s.losers}, + {"breakeven", s.breakeven}, + }; + if (s.avgPnl) { + j["avgPnl"] = *s.avgPnl; + } else { + j["avgPnl"] = nullptr; + } +} + +void to_json(nlohmann::json& j, const TradingResults& r) { + j = nlohmann::json{ + {"RUN_ID", r.RUN_ID}, + {"@timestamp", r.timestamp}, + {"config", r.config}, + {"results", r.results}, + }; +} From e14a2fe7063dca14c9a784712c7e7e0d7f101b57 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Tue, 26 May 2026 15:40:22 +0100 Subject: [PATCH 2/5] updated build --- .github/workflows/build.yml | 2 +- source/redisRunner.cpp | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d86dd3..2a65572 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -79,7 +79,7 @@ jobs: with: fetch-depth: 0 - name: Install dependencies - run: sudo apt install -y libssl-dev libpq-dev + run: sudo apt install -y libssl-dev libpq-dev libcurl4-openssl-dev # Ubuntu's apt Boost (1.83 on 24.04) predates Boost.Redis, which was added # in Boost 1.84, so is absent. Install a newer Boost. # Boost.Redis/Asio are header-only, so we only need the headers plus the diff --git a/source/redisRunner.cpp b/source/redisRunner.cpp index b282d6a..49fb739 100644 --- a/source/redisRunner.cpp +++ b/source/redisRunner.cpp @@ -49,12 +49,17 @@ int RedisRunner::run(const std::string& questdbHost, const std::string& redisHost, int redisPort, const std::string& queueKey) { + + // Event loop that drives all async Redis I/O on this thread. asio::io_context ioc; auto conn = redis_util::makeRedisConnection(ioc, redisHost, redisPort); + // Outputs of the coroutine — filled in by the completion handler below. std::optional popped; std::exception_ptr popError; + // Schedule popOnce() onto the io_context. When the coroutine finishes, + // the lambda is invoked with either an exception or the popped value. asio::co_spawn( ioc, popOnce(conn, queueKey), @@ -67,6 +72,7 @@ int RedisRunner::run(const std::string& questdbHost, popped = std::move(r); }); + // Block until the coroutine (and the Redis connection loop) finish. ioc.run(); if (popError) { From 257fa1795acef0b0e4364b377926edee29fc3a9a Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Tue, 26 May 2026 16:01:29 +0100 Subject: [PATCH 3/5] Fixing some of the code smells --- source/databaseConnection.cpp | 25 ++++++++++++++----------- source/elasticClient.cpp | 6 ++++++ source/redisLoader.cpp | 7 ++++--- source/trading/tradeManager.cpp | 3 ++- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/source/databaseConnection.cpp b/source/databaseConnection.cpp index f4354f2..c173f22 100644 --- a/source/databaseConnection.cpp +++ b/source/databaseConnection.cpp @@ -9,9 +9,15 @@ #include #include #include +#include #include #include +class InvalidTimestampFormatError : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + static std::chrono::system_clock::time_point fastParseTimestamp(const char* ts) { int year = 0; int month = 0; @@ -23,14 +29,15 @@ static std::chrono::system_clock::time_point fastParseTimestamp(const char* ts) const int parsedFields = std::sscanf(ts, "%4d-%2d-%2d %2d:%2d:%2d.%d", &year, &month, &day, &hour, &min, &sec, &usec); if (parsedFields != 6 && parsedFields != 7) { - throw std::runtime_error("Invalid timestamp format"); + throw InvalidTimestampFormatError("Invalid timestamp format: " + std::string(ts)); } // Cache timegm per date — tick data is time-ordered so date changes rarely - static char cachedDate[11] = {}; + static std::string cachedDate; static time_t cachedEpoch = 0; - if (std::memcmp(ts, cachedDate, 10) != 0) { - std::memcpy(cachedDate, ts, 10); + const std::string_view date(ts, 10); + if (cachedDate != date) { + cachedDate.assign(date); std::tm tm = {}; tm.tm_year = year - 1900; tm.tm_mon = month - 1; @@ -46,13 +53,9 @@ static std::chrono::system_clock::time_point fastParseTimestamp(const char* ts) DatabaseConnection::DatabaseConnection(const std::string& endpoint, int port, const std::string& dbname, const std::string& user, const std::string& password) { - connection_string = - "host=" + endpoint + " " - "port=" + std::to_string(port) + " " - "dbname=" + dbname + " " - "user=" + user + " " - "password=" + password + " " - "connect_timeout=3"; + connection_string = std::format( + "host={} port={} dbname={} user={} password={} connect_timeout=3", + endpoint, port, dbname, user, password); } diff --git a/source/elasticClient.cpp b/source/elasticClient.cpp index a7b9436..fbb9a38 100644 --- a/source/elasticClient.cpp +++ b/source/elasticClient.cpp @@ -59,6 +59,12 @@ int ElasticClient::putTradingResults(const TradingResults& results) { curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(body.size())); + // Require TLS 1.2 or newer and enforce certificate / hostname verification + // for any HTTPS endpoint (Sonar cpp:S4423 / S5527). + curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); + const CURLcode rc = curl_easy_perform(curl); long httpStatus = 0; diff --git a/source/redisLoader.cpp b/source/redisLoader.cpp index 6fe7d70..24ce5cf 100644 --- a/source/redisLoader.cpp +++ b/source/redisLoader.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include @@ -92,7 +93,7 @@ int RedisLoader::load(const std::string& rawJson, if (pushError) { try { std::rethrow_exception(pushError); - } catch (const std::exception& ex) { + } catch (const boost::system::system_error& ex) { std::cerr << "Redis LPUSH failed: " << ex.what() << std::endl; } return 3; @@ -145,7 +146,7 @@ int RedisLoader::loadPayload(const std::string& redisHost, if (pushError) { try { std::rethrow_exception(pushError); - } catch (const std::exception& ex) { + } catch (const boost::system::system_error& ex) { std::cerr << "Redis LPUSH failed: " << ex.what() << std::endl; } return 3; @@ -153,7 +154,7 @@ int RedisLoader::loadPayload(const std::string& redisHost, try { JsonParser::parseConfigurationFromBase64(encoded); - } catch (const std::exception& ex) { + } catch (const nlohmann::json::exception& ex) { std::cerr << "RedisLoader: failed to parse payload: " << ex.what() << std::endl; return 2; diff --git a/source/trading/tradeManager.cpp b/source/trading/tradeManager.cpp index 357b629..b8aef89 100644 --- a/source/trading/tradeManager.cpp +++ b/source/trading/tradeManager.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -16,7 +17,7 @@ namespace { std::string nextTradeId() { static std::atomic counter{0}; - return "T" + std::to_string(counter.fetch_add(1)); + return std::format("T{}", counter.fetch_add(1)); } } From 9fdd89a15bb10b9cea6c6ed7f73ca6073cc63638 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sun, 31 May 2026 08:38:31 +0100 Subject: [PATCH 4/5] Updated trade tracking to post to elasticsearch and switched to using env variables with Infisical --- README.md | 21 ++++++++++++++- include/elasticClient.hpp | 5 ++-- include/strategies/strategy.hpp | 8 +++--- include/trading/reporting.hpp | 2 ++ include/utilities/env.hpp | 16 ++++++++++++ scripts/run.sh | 14 +++++++--- source/backtestRunner.cpp | 3 +++ source/commands/loadCommand.cpp | 3 ++- source/commands/runCommand.cpp | 3 ++- source/elasticClient.cpp | 19 +++++++++----- source/main.cpp | 1 + source/operations.cpp | 32 ++++++++++++++++++++--- source/trading/reporting.cpp | 45 ++++++++++++++++++++++++--------- source/utilities/env.cpp | 19 ++++++++++++++ 14 files changed, 155 insertions(+), 36 deletions(-) create mode 100644 include/utilities/env.hpp create mode 100644 source/utilities/env.cpp diff --git a/README.md b/README.md index 01417f1..90b4947 100644 --- a/README.md +++ b/README.md @@ -87,9 +87,28 @@ Xcode - Library Path `bash ./scripts/build.sh` +### Environment variables + +The engine reads its connection configuration from the environment. The following variables are **required** — `scripts/run.sh` validates them up front and aborts if any are missing or empty: + +| Variable | Used for | +| --- | --- | +| `ELASTIC_HOST` | Elasticsearch base URL that trading results are PUT to (e.g. `https://elastic.example.com:9200`) | +| `ELASTIC_USER` | Elasticsearch HTTP basic-auth username | +| `ELASTIC_USER_PASSWORD` | Elasticsearch HTTP basic-auth password | +| `REDIS_HOST` | Redis host for the `strategy_queue` list | + +I manage these secrets with [Infisical](https://infisical.com/), which injects them into the process environment at runtime, so I run the engine with: + +``` +infisical run -- sh ./scripts/run.sh +``` + +If you're not using Infisical, export the variables yourself (e.g. via your shell profile or a sourced `.env`) before invoking the script. + ### Run via terminal -`bash ./scripts/run.sh` builds the project, then, if `redis-cli ping` reaches a local Redis, enqueues an inline JSON strategy via `load` and executes it via `run localhost`. If Redis is unreachable the script prints a message and exits cleanly (see `scripts/run.sh:22-25`), so first-time users without Redis still get a clear signal. +`bash ./scripts/run.sh` builds the project, then, if `redis-cli ping` reaches a local Redis, enqueues an inline JSON strategy via `load` and executes it via `run localhost`. If Redis is unreachable the script prints a message and exits cleanly (see `scripts/run.sh:22-25`), so first-time users without Redis still get a clear signal. The script requires the [environment variables](#environment-variables) listed above; with Infisical that becomes `infisical run -- sh ./scripts/run.sh`. The `BacktestingEngine` binary exposes a subcommand CLI: diff --git a/include/elasticClient.hpp b/include/elasticClient.hpp index 72506c5..7411546 100644 --- a/include/elasticClient.hpp +++ b/include/elasticClient.hpp @@ -9,8 +9,9 @@ #include "tradingResults.hpp" // Minimal Elasticsearch HTTP client — PUT-only, for indexing TradingResults. -// Host is read from $ELASTICSEARCH_URL (default http://localhost:9200); -// docs land in index "trading_results" with a freshly generated UUID per put. +// Host is read from $ELASTIC_HOST (default http://localhost:9200) with optional +// HTTP basic auth from $ELASTIC_USER / $ELASTIC_USER_PASSWORD; docs land in +// index "trading_results" with a freshly generated UUID per put. class ElasticClient { public: static int putTradingResults(const TradingResults& results); diff --git a/include/strategies/strategy.hpp b/include/strategies/strategy.hpp index 5310726..a514368 100644 --- a/include/strategies/strategy.hpp +++ b/include/strategies/strategy.hpp @@ -11,7 +11,7 @@ #include "models/trade.hpp" // Forward declaration. We only refer to `TradeManager` by reference in -// this header, so the compiler doesn't need its full definition here — +// this header, so the compiler doesn't need its full definition here, // just to know "it's a class". Pulling in the full header would drag // every TradeManager dependency into every strategy. C# doesn't really // have an equivalent because it resolves types at the assembly level @@ -25,7 +25,7 @@ class TradeManager; // - This is the C++ equivalent of a C# `interface`. C++ has no // dedicated `interface` keyword, so you express it as a class // whose methods are all pure virtual (the `= 0` suffix). The -// `I` prefix is borrowed from C# — C++ has no fixed convention, +// `I` prefix is borrowed from C#, C++ has no fixed convention, // but it's a useful hint because C++ classes routinely mix // virtual and concrete methods, so "is this an interface?" isn't // always obvious from the keyword alone. @@ -34,7 +34,7 @@ class TradeManager; // destructor would skip the derived destructor and leak resources. // C# handles this for you; in C++ you opt in. // - `= 0` makes a method pure virtual, which makes the class -// abstract — you cannot instantiate `IStrategy` directly, only +// abstract, you cannot instantiate `IStrategy` directly, only // concrete subclasses. Same behaviour as a C# interface. // - Derived classes annotate their implementations with `override` // (see `RandomStrategy`). It's optional in C++ but catches @@ -52,7 +52,7 @@ class IStrategy { // reference so strategies can both inspect open positions // (`tradeManager.getActiveTrades()`) and act on them // (`closeTrade`, future scale/adjust hooks). A reference signals - // "borrow, don't own" — the strategy must not delete it. The C# + // "borrow, don't own", the strategy must not delete it. The C# // analogue is just passing the manager as a parameter; C# has no // distinction between reference and pointer so the by-ref nature // is implicit there. diff --git a/include/trading/reporting.hpp b/include/trading/reporting.hpp index b584f71..3bafcfe 100644 --- a/include/trading/reporting.hpp +++ b/include/trading/reporting.hpp @@ -6,9 +6,11 @@ #pragma once #include "tradeManager.hpp" +#include "tradingResults.hpp" class Reporting { public: + static TradingResultsStats collect(const TradeManager& tradeManager); static void summarise(const TradeManager& tradeManager); }; diff --git a/include/utilities/env.hpp b/include/utilities/env.hpp new file mode 100644 index 0000000..e782a1b --- /dev/null +++ b/include/utilities/env.hpp @@ -0,0 +1,16 @@ +// Backtesting Engine in C++ +// +// (c) 2026 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- + +#pragma once + +#include + +namespace env { + +// Returns $name, or `fallback` when the variable is unset or empty. +std::string getOr(const char* name, std::string fallback); + +} // namespace env diff --git a/scripts/run.sh b/scripts/run.sh index 63b8567..c1d9f12 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -1,6 +1,16 @@ #!/bin/bash # This executes the run script +required_vars=(ELASTIC_HOST ELASTIC_USER ELASTIC_USER_PASSWORD REDIS_HOST) +missing=() +for var in "${required_vars[@]}"; do + [[ -z "${!var}" ]] && missing+=("$var") +done +if [[ ${#missing[@]} -gt 0 ]]; then + echo "Error: missing required environment variables: ${missing[*]}" + exit 1 +fi + current_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # Build the source code @@ -24,10 +34,6 @@ if ! redis-cli -h localhost ping >/dev/null 2>&1; then exit 0 fi -if ! ./"$BUILD_DIR/$EXECUTABLE_NAME" load; then - exit 1 -fi - start_time=$(date +%s%N) # Invoke the `run` subcommand: BacktestingEngine pops a Base64-encoded # strategy off the Redis `strategy_queue` and executes it against the diff --git a/source/backtestRunner.cpp b/source/backtestRunner.cpp index ecaef4f..7d9ceae 100644 --- a/source/backtestRunner.cpp +++ b/source/backtestRunner.cpp @@ -17,14 +17,17 @@ int runBacktest(const std::string& questdbHost, const trading_definitions::Configuration& config) { + DatabaseConnection db(questdbHost, 8812, "qdb", "admin", "quest"); + // Get a list of symbols std::vector symbols; std::istringstream ss(config.SYMBOLS); for (std::string token; std::getline(ss, token, ',');) { symbols.push_back(token); } + // Get all the tick data out of QuestDB for these symbols std::vector ticks = SqlManager::streamPriceData(db, symbols, config.LAST_MONTHS); diff --git a/source/commands/loadCommand.cpp b/source/commands/loadCommand.cpp index b1fc6ef..1e0b1f6 100644 --- a/source/commands/loadCommand.cpp +++ b/source/commands/loadCommand.cpp @@ -9,6 +9,7 @@ #include #include +#include "env.hpp" #include "redisLoader.hpp" #include "trading_definitions.hpp" @@ -38,5 +39,5 @@ int LoadCommand::run() { }; const nlohmann::json j = config; - return RedisLoader::load(j.dump()); + return RedisLoader::load(j.dump(), env::getOr("REDIS_HOST", "127.0.0.1")); } diff --git a/source/commands/runCommand.cpp b/source/commands/runCommand.cpp index caca9b9..fe0a9b3 100644 --- a/source/commands/runCommand.cpp +++ b/source/commands/runCommand.cpp @@ -10,6 +10,7 @@ #include #include "backtestRunner.hpp" +#include "env.hpp" #include "jsonParser.hpp" #include "redisRunner.hpp" @@ -31,7 +32,7 @@ int RunCommand::run(int argc, const char* argv[]) { return 1; } if (argc == 3) { - return RedisRunner::run(argv[2]); + return RedisRunner::run(argv[2], env::getOr("REDIS_HOST", "127.0.0.1")); } return runBacktestFromBase64(argv[2], argv[3]); } diff --git a/source/elasticClient.cpp b/source/elasticClient.cpp index fbb9a38..df27b53 100644 --- a/source/elasticClient.cpp +++ b/source/elasticClient.cpp @@ -6,7 +6,6 @@ #include "elasticClient.hpp" -#include #include #include @@ -15,6 +14,8 @@ #include #include +#include "env.hpp" + namespace { void ensureCurlInit() { @@ -25,11 +26,6 @@ void ensureCurlInit() { (void)guard; } -std::string envOr(const char* name, std::string fallback) { - const char* val = std::getenv(name); - return val ? std::string{val} : std::move(fallback); -} - std::string generateUuid() { static thread_local boost::uuids::random_generator gen; return boost::uuids::to_string(gen()); @@ -40,7 +36,9 @@ std::string generateUuid() { int ElasticClient::putTradingResults(const TradingResults& results) { ensureCurlInit(); - const std::string host = envOr("ELASTICSEARCH_URL", "http://localhost:9200"); + const std::string host = env::getOr("ELASTIC_HOST", "http://localhost:9200"); + const std::string user = env::getOr("ELASTIC_USER", ""); + const std::string password = env::getOr("ELASTIC_USER_PASSWORD", ""); const std::string url = host + "/trading_results/_doc/" + generateUuid(); const std::string body = nlohmann::json(results).dump(); @@ -55,6 +53,13 @@ int ElasticClient::putTradingResults(const TradingResults& results) { curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + // HTTP basic auth when credentials are supplied via the environment. + if (!user.empty()) { + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_easy_setopt(curl, CURLOPT_USERNAME, user.c_str()); + curl_easy_setopt(curl, CURLOPT_PASSWORD, password.c_str()); + } curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(body.size())); diff --git a/source/main.cpp b/source/main.cpp index b03d55c..968ff32 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -23,6 +23,7 @@ int main(int argc, const char* argv[]) { const std::string_view subcommand = argv[1]; + // Two paths, load or run if (subcommand == "load") return LoadCommand::run(); if (subcommand == "run") return RunCommand::run(argc, argv); diff --git a/source/operations.cpp b/source/operations.cpp index 078c0b2..4c1456d 100644 --- a/source/operations.cpp +++ b/source/operations.cpp @@ -7,11 +7,14 @@ #include "operations.hpp" #include #include -#include #include +#include +#include #include "tradeManager.hpp" #include "reviewStopAndLimit.hpp" #include "reporting.hpp" +#include "tradingResults.hpp" +#include "elasticClient.hpp" #include "strategies/strategy.hpp" #include "strategies/randomStrategy.hpp" #include "strategies/strategyErrors.hpp" @@ -20,8 +23,7 @@ namespace { // Adding a new strategy means adding one branch here; nothing else in // Operations needs to know about the concrete type. -std::unique_ptr -selectStrategy(const trading_definitions::Configuration& config) { +std::unique_ptr selectStrategy(const trading_definitions::Configuration& config) { const auto& name = config.STRATEGY.TRADING_VARIABLES.STRATEGY; if (name == "RandomStrategy") { return std::make_unique(config.STRATEGY); @@ -42,7 +44,7 @@ void Operations::run(const std::vector& ticks, for (const auto& tick : ticks) { // Close any trade whose stop-loss or take-profit fired on this tick - // before we consider opening a new one — otherwise an exit and an + // before we consider opening a new one otherwise an exit and an // entry could race within the same tick. trading::reviewStopAndLimit(*tradeManager, tick); @@ -64,4 +66,26 @@ void Operations::run(const std::vector& ticks, } Reporting::summarise(*tradeManager); + + // Best-effort: persist this run's results to Elasticsearch. The backtest + // has already produced its summary, so nothing here may abort the run. + // putTradingResults logs every transport/HTTP outcome itself (so we do not + // re-log on a non-zero return, that would double the warning), but a throw + // from JSON serialisation or the network layer bypasses its logging, so the + // catch handlers emit the single warning for that path. + try { + const TradingResults results{ + config.RUN_ID, + TradingResults::nowIsoUtc(), + config, + Reporting::collect(*tradeManager), + }; + ElasticClient::putTradingResults(results); + } catch (const std::exception& e) { + std::cerr << "Operations: trading-results put failed: " << e.what() + << std::endl; + } catch (...) { + std::cerr << "Operations: trading-results put failed: unknown error" + << std::endl; + } } diff --git a/source/trading/reporting.cpp b/source/trading/reporting.cpp index 5ed00e8..dc7849f 100644 --- a/source/trading/reporting.cpp +++ b/source/trading/reporting.cpp @@ -10,10 +10,9 @@ #include #include #include "trade.hpp" +#include "tradingResults.hpp" -void Reporting::summarise(const TradeManager& tradeManager) { - std::cout << "Final PnL: " << std::fixed << std::setprecision(2) << tradeManager.calculatePnl() << std::endl; - +TradingResultsStats Reporting::collect(const TradeManager& tradeManager) { const auto& activeTrades = tradeManager.getActiveTrades(); const auto& closedTrades = tradeManager.getClosedTrades(); @@ -45,18 +44,40 @@ void Reporting::summarise(const TradeManager& tradeManager) { openedLong += closedLong; openedShort += closedShort; - std::cout << "Trades opened: " << openedCount - << " (LONG: " << openedLong << ", SHORT: " << openedShort << ")" << std::endl; - std::cout << "Trades closed: " << closedCount - << " (LONG: " << closedLong << ", SHORT: " << closedShort << ")" << std::endl; - std::cout << "Winners: " << winners - << " Losers: " << losers - << " Breakeven: " << breakeven << std::endl; + TradingResultsStats stats; + stats.finalPnl = tradeManager.calculatePnl(); + stats.tradesOpened = openedCount; + stats.tradesClosed = closedCount; + stats.openedLong = openedLong; + stats.openedShort = openedShort; + stats.closedLong = closedLong; + stats.closedShort = closedShort; + stats.winners = winners; + stats.losers = losers; + stats.breakeven = breakeven; if (closedCount == 0) { + stats.avgPnl = std::nullopt; + } else { + stats.avgPnl = pnlSum / boost::decimal::decimal64_t{static_cast(closedCount)}; + } + return stats; +} + +void Reporting::summarise(const TradeManager& tradeManager) { + const auto stats = collect(tradeManager); + + std::cout << "Final PnL: " << std::fixed << std::setprecision(2) << stats.finalPnl << std::endl; + std::cout << "Trades opened: " << stats.tradesOpened + << " (LONG: " << stats.openedLong << ", SHORT: " << stats.openedShort << ")" << std::endl; + std::cout << "Trades closed: " << stats.tradesClosed + << " (LONG: " << stats.closedLong << ", SHORT: " << stats.closedShort << ")" << std::endl; + std::cout << "Winners: " << stats.winners + << " Losers: " << stats.losers + << " Breakeven: " << stats.breakeven << std::endl; + if (!stats.avgPnl) { std::cout << "Average PnL per closed trade: n/a (0 closed)" << std::endl; } else { - const auto avgPnl = pnlSum / boost::decimal::decimal64_t{static_cast(closedCount)}; std::cout << "Average PnL per closed trade: " - << std::fixed << std::setprecision(2) << avgPnl << std::endl; + << std::fixed << std::setprecision(2) << *stats.avgPnl << std::endl; } } diff --git a/source/utilities/env.cpp b/source/utilities/env.cpp new file mode 100644 index 0000000..f695fc6 --- /dev/null +++ b/source/utilities/env.cpp @@ -0,0 +1,19 @@ +// Backtesting Engine in C++ +// +// (c) 2026 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- + +#include "env.hpp" + +#include +#include + +namespace env { + +std::string getOr(const char* name, std::string fallback) { + const char* val = std::getenv(name); + return (val && *val) ? std::string{val} : std::move(fallback); +} + +} // namespace env From b2ae29b50c69ede0615d170a1ba635fa4559c4ce Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sun, 31 May 2026 08:39:33 +0100 Subject: [PATCH 5/5] fixed references in xcode --- backtesting-engine-cpp.xcodeproj/project.pbxproj | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backtesting-engine-cpp.xcodeproj/project.pbxproj b/backtesting-engine-cpp.xcodeproj/project.pbxproj index 7e8142a..be60366 100644 --- a/backtesting-engine-cpp.xcodeproj/project.pbxproj +++ b/backtesting-engine-cpp.xcodeproj/project.pbxproj @@ -47,6 +47,8 @@ 9470B5B62C8C5BFD007D9CC6 /* main.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9470B5A32C8C5AD0007D9CC6 /* main.cpp */; }; 94724A832F8B92C10029B940 /* operations.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94724A822F8B92C10029B940 /* operations.cpp */; }; 94724A842F8B92C10029B940 /* operations.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94724A822F8B92C10029B940 /* operations.cpp */; }; + 94829FC52FCC1D1A00710E6E /* env.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94829FC42FCC1D1A00710E6E /* env.cpp */; }; + 94829FC62FCC1D1A00710E6E /* env.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94829FC42FCC1D1A00710E6E /* env.cpp */; }; 94CD8BA02D2E8CE500041BBA /* databaseConnection.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94CD8B9F2D2E8CE500041BBA /* databaseConnection.cpp */; }; 94CD8BA12D2E8CE500041BBA /* databaseConnection.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94CD8B9F2D2E8CE500041BBA /* databaseConnection.cpp */; }; 94D3A7262FC1B3AD00EBEA32 /* loadCommand.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94D3A7242FC1B3AD00EBEA32 /* loadCommand.cpp */; }; @@ -130,6 +132,8 @@ 9470B5AC2C8C5B99007D9CC6 /* tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 94724A822F8B92C10029B940 /* operations.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = operations.cpp; sourceTree = ""; }; 94724A852F8B92E30029B940 /* operations.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = operations.hpp; sourceTree = ""; }; + 94829FC32FCC1D1200710E6E /* env.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = env.hpp; sourceTree = ""; }; + 94829FC42FCC1D1A00710E6E /* env.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = env.cpp; sourceTree = ""; }; 948A9CCD2C906A5600E23669 /* CONVENTIONS.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONVENTIONS.md; sourceTree = ""; }; 94BBA4512D2EA2640010E04D /* build.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = build.sh; sourceTree = ""; }; 94C331A02FA899A8006BD690 /* decimal_json.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = decimal_json.hpp; sourceTree = ""; }; @@ -1346,6 +1350,7 @@ 94280BA72D2FC29F00F1CF56 /* utilities */ = { isa = PBXGroup; children = ( + 94829FC42FCC1D1A00710E6E /* env.cpp */, 942EC5612FBEF94700CCBB5D /* redisConnection.cpp */, 94280BA22D2FC00200F1CF56 /* base64.cpp */, 943398232D57E53400287A2D /* jsonParser.cpp */, @@ -1476,6 +1481,7 @@ 94B8C7932D3D770800E17EB6 /* utilities */ = { isa = PBXGroup; children = ( + 94829FC32FCC1D1200710E6E /* env.hpp */, 942EC55D2FBEF92F00CCBB5D /* redisConnection.hpp */, 94C331A02FA899A8006BD690 /* decimal_json.hpp */, 94280BA12D2FC00200F1CF56 /* base64.hpp */, @@ -3718,6 +3724,7 @@ 94D3A72A2FC1B41500EBEA32 /* runCommand.cpp in Sources */, 942EC56A2FBEF95000CCBB5D /* backtestRunner.cpp in Sources */, 942EC56B2FBEF95000CCBB5D /* redisLoader.cpp in Sources */, + 94829FC52FCC1D1A00710E6E /* env.cpp in Sources */, 942EC56C2FBEF95000CCBB5D /* redisRunner.cpp in Sources */, 94280BA32D2FC00200F1CF56 /* base64.cpp in Sources */, 94674B8E2D533E7800973137 /* trade.cpp in Sources */, @@ -3752,6 +3759,7 @@ 94D3A7292FC1B41500EBEA32 /* runCommand.cpp in Sources */, 94D601102FA9CD700066F51A /* randomStrategy.cpp in Sources */, 94674B8A2D533BDA00973137 /* tradeManager.mm in Sources */, + 94829FC62FCC1D1A00710E6E /* env.cpp in Sources */, 94724A832F8B92C10029B940 /* operations.cpp in Sources */, 940A61182C92CE960083FEB8 /* serviceA.cpp in Sources */, 94674B882D533B4000973137 /* tradeManager.cpp in Sources */,