From 05435e438db31a4eee24bf7724e10c2f69641ca0 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sat, 11 Apr 2026 11:28:27 +0100 Subject: [PATCH 01/39] Added QuestDB tick streaming --- CMakeLists.txt | 15 ++++++-- documents/questdb.md | 5 +++ include/databaseConnection.hpp | 1 + include/models/priceData.hpp | 2 + include/sqlManager.hpp | 5 ++- scripts/build_dep.sh | 11 ++++++ scripts/run.sh | 8 ++++ source/CMakeLists.txt | 68 ++++++++++++++++++++++++++++++++++ source/databaseConnection.cpp | 45 ++++++++++++++++++++++ source/main.cpp | 39 ++++++++++--------- source/sqlManager.cpp | 10 +++-- 11 files changed, 184 insertions(+), 25 deletions(-) create mode 100644 documents/questdb.md create mode 100644 scripts/build_dep.sh create mode 100644 source/CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index b6c5634..f39c14e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,6 @@ project(BacktestingEngine) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) - # Configure libpqxx build set(PQXX_LIBRARIES_INSTALL ON) set(SKIP_BUILD_TEST ON) @@ -51,8 +50,18 @@ file(GLOB_RECURSE SOURCES "source/*.cpp") # Create a library of your project's code add_library(BacktestingEngineLib STATIC ${SOURCES}) -# Link against pqxx -target_link_libraries(BacktestingEngineLib pqxx) +# Replace find_package(OpenMP REQUIRED) with this: +if(APPLE) + set(OpenMP_C_FLAGS "-Xclang -fopenmp") + set(OpenMP_CXX_FLAGS "-Xclang -fopenmp") + set(OpenMP_C_LIB_NAMES "omp") + set(OpenMP_CXX_LIB_NAMES "omp") + set(OpenMP_omp_LIBRARY /opt/homebrew/opt/libomp/lib/libomp.dylib) + find_package(OpenMP REQUIRED) + target_include_directories(BacktestingEngineLib PRIVATE /opt/homebrew/opt/libomp/include) +endif() + +target_link_libraries(BacktestingEngineLib PUBLIC pqxx OpenMP::OpenMP_CXX) # Main executable add_executable(BacktestingEngine source/main.cpp) diff --git a/documents/questdb.md b/documents/questdb.md new file mode 100644 index 0000000..015d25c --- /dev/null +++ b/documents/questdb.md @@ -0,0 +1,5 @@ +Start QuestDB (on macOS) + +``` +JAVA_HOME="/opt/homebrew/opt/openjdk@17" sh $HOME/dev/questdb/questdb.sh start -d $HOME/dev/questdb/data +``` \ No newline at end of file diff --git a/include/databaseConnection.hpp b/include/databaseConnection.hpp index 1502ec2..b885761 100644 --- a/include/databaseConnection.hpp +++ b/include/databaseConnection.hpp @@ -21,6 +21,7 @@ class DatabaseConnection { void printResults(const std::vector& results) const; std::vector executeQuery(const std::string& query) const; + std::vector streamQuery(const std::string& query) const; const std::string& getConnectionString() const { return connection_string; diff --git a/include/models/priceData.hpp b/include/models/priceData.hpp index 4047ffd..cecdd1a 100644 --- a/include/models/priceData.hpp +++ b/include/models/priceData.hpp @@ -14,4 +14,6 @@ struct PriceData { // Constructor for easy creation PriceData(double v1, double v2, const std::chrono::system_clock::time_point& ts) : value1(v1), value2(v2), timestamp(ts) {} + + PriceData() : value1(0.0), value2(0.0), timestamp{} {} }; diff --git a/include/sqlManager.hpp b/include/sqlManager.hpp index 1f65287..62f1935 100644 --- a/include/sqlManager.hpp +++ b/include/sqlManager.hpp @@ -11,8 +11,9 @@ class SqlManager { public: - static std::vector getInitialPriceData(const DatabaseConnection& db); + static std::vector streamPriceData(const DatabaseConnection& db); static std::string getBaseQuery(); private: - static constexpr int DEFAULT_LIMIT = 1000; + static constexpr int LAST_MONTHS = 1; + static constexpr int STREAM_LIMIT = 200000; }; diff --git a/scripts/build_dep.sh b/scripts/build_dep.sh new file mode 100644 index 0000000..9afd5e6 --- /dev/null +++ b/scripts/build_dep.sh @@ -0,0 +1,11 @@ +cd ./external/libpqxx + +mkdir -p build +cd ./build + +export PATH="$(brew --prefix libpq)/bin:$PATH" +export PKG_CONFIG_PATH="$(brew --prefix libpq)/lib/pkgconfig:$PKG_CONFIG_PATH" +export PostgreSQL_ROOT="$(brew --prefix libpq)" + +cmake .. -DCMAKE_CXX_STANDARD=20 -DCMAKE_BUILD_TYPE=Release +make \ No newline at end of file diff --git a/scripts/run.sh b/scripts/run.sh index fa3cd8d..f4b400c 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -7,6 +7,10 @@ current_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # source $current_dir/environment.sh - no longer necessary source $current_dir/clean.sh source $current_dir/build.sh +if [ $? -ne 0 ]; then + echo "Error: Build failed. Aborting." + exit 1 +fi # Debug: Check if the executable exists if [ -f "$BUILD_DIR/$EXECUTABLE_NAME" ]; then @@ -49,5 +53,9 @@ output=$(echo "$json" | base64) # Step 6: Run the tests for now (/executable) from the root directory # Passing two arguements, the destination of the QuestDB and the Strategy JSON (in base64) +start_time=$(date +%s%N) ./"$BUILD_DIR/$EXECUTABLE_NAME" localhost "$output" +end_time=$(date +%s%N) +elapsed=$(( (end_time - start_time) / 1000000 )) +echo "Execution time: ${elapsed}ms" diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt new file mode 100644 index 0000000..f39c14e --- /dev/null +++ b/source/CMakeLists.txt @@ -0,0 +1,68 @@ +cmake_minimum_required(VERSION 3.30) + +# CMAKE_OSX_SYSROOT is a macOS-specific setting that specifies the SDK path. +# This is ignored on non-Apple platforms, so it's safe to include in cross-platform builds. +execute_process( + COMMAND xcrun --show-sdk-path + OUTPUT_VARIABLE CMAKE_OSX_SYSROOT + OUTPUT_STRIP_TRAILING_WHITESPACE +) + +project(BacktestingEngine) + +# Set the C++ standard +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Configure libpqxx build +set(PQXX_LIBRARIES_INSTALL ON) +set(SKIP_BUILD_TEST ON) +set(SKIP_CONFIGURE_LIBPQXX OFF) + +# Disable warningsfor external libraries +set(PREV_CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS}) +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -w") +elseif(MSVC) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /w") +endif() + +# Quiet CMAKE output +set(CMAKE_INSTALL_MESSAGE NEVER) +set(CMAKE_MESSAGE_LOG_LEVEL "WARNING") + +# Build libpqxx from source +add_subdirectory(external/libpqxx EXCLUDE_FROM_ALL) + +# Include directories +include_directories( + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/include/utilities + ${CMAKE_SOURCE_DIR}/include/models + ${CMAKE_SOURCE_DIR}/include/trading + ${CMAKE_SOURCE_DIR}/include/trading_definitions + ${CMAKE_SOURCE_DIR}/external +) + +# Collect all .cpp files in the src directory +file(GLOB_RECURSE SOURCES "source/*.cpp") + +# Create a library of your project's code +add_library(BacktestingEngineLib STATIC ${SOURCES}) + +# Replace find_package(OpenMP REQUIRED) with this: +if(APPLE) + set(OpenMP_C_FLAGS "-Xclang -fopenmp") + set(OpenMP_CXX_FLAGS "-Xclang -fopenmp") + set(OpenMP_C_LIB_NAMES "omp") + set(OpenMP_CXX_LIB_NAMES "omp") + set(OpenMP_omp_LIBRARY /opt/homebrew/opt/libomp/lib/libomp.dylib) + find_package(OpenMP REQUIRED) + target_include_directories(BacktestingEngineLib PRIVATE /opt/homebrew/opt/libomp/include) +endif() + +target_link_libraries(BacktestingEngineLib PUBLIC pqxx OpenMP::OpenMP_CXX) + +# Main executable +add_executable(BacktestingEngine source/main.cpp) +target_link_libraries(BacktestingEngine BacktestingEngineLib) diff --git a/source/databaseConnection.cpp b/source/databaseConnection.cpp index 292da31..d5b17f3 100644 --- a/source/databaseConnection.cpp +++ b/source/databaseConnection.cpp @@ -7,6 +7,31 @@ #include "databaseConnection.hpp" #include "base64.hpp" #include +#include +#include +#include +#include + +static std::chrono::system_clock::time_point fastParseTimestamp(const char* ts) { + int year, month, day, hour, min, sec, usec = 0; + std::sscanf(ts, "%4d-%2d-%2d %2d:%2d:%2d.%d", &year, &month, &day, &hour, &min, &sec, &usec); + + // Cache timegm per date — tick data is time-ordered so date changes rarely + static char cachedDate[11] = {}; + static time_t cachedEpoch = 0; + if (std::memcmp(ts, cachedDate, 10) != 0) { + std::memcpy(cachedDate, ts, 10); + std::tm tm = {}; + tm.tm_year = year - 1900; + tm.tm_mon = month - 1; + tm.tm_mday = day; + tm.tm_isdst = 0; + cachedEpoch = timegm(&tm); + } + + time_t t = cachedEpoch + hour * 3600 + min * 60 + sec; + return std::chrono::system_clock::from_time_t(t) + std::chrono::microseconds(usec); +} DatabaseConnection::DatabaseConnection(const std::string& endpoint, int port, const std::string& dbname, const std::string& user, @@ -56,6 +81,26 @@ std::vector DatabaseConnection::executeQuery(const std::string& query return results; } +std::vector DatabaseConnection::streamQuery(const std::string& query) const { + pqxx::connection conn(this->connection_string); + pqxx::nontransaction txn(conn); + pqxx::result result = txn.exec(query); + + std::vector results(result.size()); + + for (int i = 0; i < (int)result.size(); ++i) { + const auto& row = result[i]; + double value1, value2; + auto sv1 = row[0].view(); + auto sv2 = row[1].view(); + std::from_chars(sv1.data(), sv1.data() + sv1.size(), value1); + std::from_chars(sv2.data(), sv2.data() + sv2.size(), value2); + results[i] = PriceData(value1, value2, fastParseTimestamp(row[2].c_str())); + } + + return results; +} + // Example usage function to demonstrate how to work with the results void DatabaseConnection::printResults(const std::vector& results) const { for (const auto& data : results) { diff --git a/source/main.cpp b/source/main.cpp index 9fb01da..97b8985 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -1,6 +1,6 @@ // Backtesting Engine in C++ // -// (c) 2025 Ryan McCaffery | https://mccaffers.com +// (c) 2026 Ryan McCaffery | https://mccaffers.com // This code is licensed under MIT license (see LICENSE.txt for details) // --------------------------------------- @@ -26,33 +26,38 @@ using json = nlohmann::json; +// Entry point. Expects two command-line arguments: +// argv[1] — hostname/IP of the QuestDB instance +// argv[2] — Base64-encoded JSON strategy configuration int main(int argc, const char * argv[]) { - // Connect to QuestDb argv[1] DatabaseConnection db(argv[1], 8812, "qdb", "admin", "quest"); - // Load strategy from Base64 argv[2] JsonParser::parseConfigurationFromBase64(argv[2]); - std::vector priceData = SqlManager::getInitialPriceData(db); - - // Convert timestamp to readable format for debugging - auto timeT = std::chrono::system_clock::to_time_t(priceData[0].timestamp); - std::cout << "Timestamp: " << std::put_time(std::localtime(&timeT), "%Y-%m-%d %H:%M:%S") << std::endl; + std::vector ticks = SqlManager::streamPriceData(db); + printf("Total ticks streamed: %zu\n", ticks.size()); + + // print first tick + auto time_t = std::chrono::system_clock::to_time_t(ticks[0].timestamp); + struct tm tm = {}; + if (localtime_r(&time_t, &tm) == nullptr) { + std::cerr << "Error: failed to convert timestamp" << std::endl; + } + char buffer[20]; + std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm); + printf("First tick: ask=%.4f, value2=%.4f timestamp=%s\n", ticks[0].value1, ticks[0].value2, buffer); auto tradeManager = TradeManager::getInstance(); - // Open a trade - std::string tradeId = tradeManager->openTrade(1.2345, 100000, true); - std::cout << "Opened trade: " << tradeId << std::endl; + // std::string tradeId = tradeManager->openTrade(1.2345, 100000, true); + // std::cout << "Opened trade: " << tradeId << std::endl; - // Review account - size_t openTrades = tradeManager->reviewAccount(); - std::cout << "Number of open trades: " << openTrades << std::endl; + // size_t openTrades = tradeManager->reviewAccount(); + // std::cout << "Number of open trades: " << openTrades << std::endl; - // Close trade - bool closed = tradeManager->closeTrade(tradeId); - std::cout << "Trade closed: " << (closed ? "yes" : "no") << std::endl; + // bool closed = tradeManager->closeTrade(tradeId); + // std::cout << "Trade closed: " << (closed ? "yes" : "no") << std::endl; return 0; diff --git a/source/sqlManager.cpp b/source/sqlManager.cpp index 12152cd..68391d7 100644 --- a/source/sqlManager.cpp +++ b/source/sqlManager.cpp @@ -4,11 +4,15 @@ // This code is licensed under MIT license (see LICENSE.txt for details) // --------------------------------------- #include "sqlManager.hpp" +#include +#include std::string SqlManager::getBaseQuery() { - return "SELECT * FROM EURUSD LIMIT " + std::to_string(DEFAULT_LIMIT) + ";"; + return "SELECT * FROM EURUSD WHERE timestamp >= dateadd('M', -" + std::to_string(LAST_MONTHS) + ", now()) LIMIT 40000000"; } -std::vector SqlManager::getInitialPriceData(const DatabaseConnection& db) { - return db.executeQuery(getBaseQuery()); +std::vector SqlManager::streamPriceData(const DatabaseConnection& db) { + std::string query = getBaseQuery(); + std::cout << "Executing query: " << query << std::endl; + return db.streamQuery(query); } From 16fb2c3800991a767ca35423ee77585bbe7f0f8b Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sat, 11 Apr 2026 15:15:01 +0100 Subject: [PATCH 02/39] Refactoring tick flow --- include/models/priceData.hpp | 10 +++++----- include/sqlManager.hpp | 5 ++--- source/databaseConnection.cpp | 20 ++++++++++---------- source/main.cpp | 16 ++++++++-------- source/sqlManager.cpp | 8 ++++---- 5 files changed, 29 insertions(+), 30 deletions(-) diff --git a/include/models/priceData.hpp b/include/models/priceData.hpp index cecdd1a..09f52ed 100644 --- a/include/models/priceData.hpp +++ b/include/models/priceData.hpp @@ -7,13 +7,13 @@ #include struct PriceData { - double value1; - double value2; + double ask; + double bid; std::chrono::system_clock::time_point timestamp; // Constructor for easy creation - PriceData(double v1, double v2, const std::chrono::system_clock::time_point& ts) - : value1(v1), value2(v2), timestamp(ts) {} + PriceData(double ask, double bid, const std::chrono::system_clock::time_point& ts) + : ask(ask), bid(bid), timestamp(ts) {} - PriceData() : value1(0.0), value2(0.0), timestamp{} {} + PriceData() : ask(0.0), bid(0.0), timestamp{} {} }; diff --git a/include/sqlManager.hpp b/include/sqlManager.hpp index 62f1935..21a03b5 100644 --- a/include/sqlManager.hpp +++ b/include/sqlManager.hpp @@ -11,9 +11,8 @@ class SqlManager { public: - static std::vector streamPriceData(const DatabaseConnection& db); - static std::string getBaseQuery(); + static std::vector streamPriceData(const DatabaseConnection& db, int LAST_MONTHS = 1); + static std::string getBaseQuery(int LAST_MONTHS = 1); private: - static constexpr int LAST_MONTHS = 1; static constexpr int STREAM_LIMIT = 200000; }; diff --git a/source/databaseConnection.cpp b/source/databaseConnection.cpp index d5b17f3..37ef8d4 100644 --- a/source/databaseConnection.cpp +++ b/source/databaseConnection.cpp @@ -63,13 +63,13 @@ std::vector DatabaseConnection::executeQuery(const std::string& query // Convert results to PriceData objects for (const auto& row : result) { - double value1 = row[0].as(); - double value2 = row[1].as(); + double ask = row[0].as(); + double bid = row[1].as(); std::string timestamp_str = row[2].as(); auto timestamp = Utilities::parseTimestamp(timestamp_str); - results.emplace_back(value1, value2, timestamp); + results.emplace_back(ask, bid, timestamp); } txn.commit(); @@ -90,13 +90,13 @@ std::vector DatabaseConnection::streamQuery(const std::string& query) for (int i = 0; i < (int)result.size(); ++i) { const auto& row = result[i]; - double value1, value2; + double ask, bid; auto sv1 = row[0].view(); auto sv2 = row[1].view(); - std::from_chars(sv1.data(), sv1.data() + sv1.size(), value1); - std::from_chars(sv2.data(), sv2.data() + sv2.size(), value2); - results[i] = PriceData(value1, value2, fastParseTimestamp(row[2].c_str())); - } + std::from_chars(sv1.data(), sv1.data() + sv1.size(), ask); + std::from_chars(sv2.data(), sv2.data() + sv2.size(), bid); + results[i] = PriceData(ask, bid, fastParseTimestamp(row[2].c_str())); + } return results; } @@ -115,8 +115,8 @@ void DatabaseConnection::printResults(const std::vector& results) con ss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S"); std::cout << std::fixed << std::setprecision(4) - << data.value1 << "\t" - << data.value2 << "\t" + << data.ask << "\t" + << data.bid << "\t" << ss.str() << std::endl; } } diff --git a/source/main.cpp b/source/main.cpp index 97b8985..2f1e4c3 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -35,7 +35,7 @@ int main(int argc, const char * argv[]) { JsonParser::parseConfigurationFromBase64(argv[2]); - std::vector ticks = SqlManager::streamPriceData(db); + std::vector ticks = SqlManager::streamPriceData(db, 1); printf("Total ticks streamed: %zu\n", ticks.size()); // print first tick @@ -46,18 +46,18 @@ int main(int argc, const char * argv[]) { } char buffer[20]; std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm); - printf("First tick: ask=%.4f, value2=%.4f timestamp=%s\n", ticks[0].value1, ticks[0].value2, buffer); + printf("First tick: ask=%.4f, bid=%.4f timestamp=%s\n", ticks[0].ask, ticks[0].bid, buffer); auto tradeManager = TradeManager::getInstance(); - // std::string tradeId = tradeManager->openTrade(1.2345, 100000, true); - // std::cout << "Opened trade: " << tradeId << std::endl; + std::string tradeId = tradeManager->openTrade(ticks[0].ask, 100000, true); + std::cout << "Opened trade: " << tradeId << std::endl; - // size_t openTrades = tradeManager->reviewAccount(); - // std::cout << "Number of open trades: " << openTrades << std::endl; + size_t openTrades = tradeManager->reviewAccount(); + std::cout << "Number of open trades: " << openTrades << std::endl; - // bool closed = tradeManager->closeTrade(tradeId); - // std::cout << "Trade closed: " << (closed ? "yes" : "no") << std::endl; + bool closed = tradeManager->closeTrade(tradeId); + std::cout << "Trade closed: " << (closed ? "yes" : "no") << std::endl; return 0; diff --git a/source/sqlManager.cpp b/source/sqlManager.cpp index 68391d7..6d74f3d 100644 --- a/source/sqlManager.cpp +++ b/source/sqlManager.cpp @@ -7,12 +7,12 @@ #include #include -std::string SqlManager::getBaseQuery() { - return "SELECT * FROM EURUSD WHERE timestamp >= dateadd('M', -" + std::to_string(LAST_MONTHS) + ", now()) LIMIT 40000000"; +std::string SqlManager::getBaseQuery(int LAST_MONTHS) { + return "SELECT * FROM EURUSD WHERE timestamp >= dateadd('M', -" + std::to_string(LAST_MONTHS) + ", now()) LIMIT " + std::to_string(STREAM_LIMIT); } -std::vector SqlManager::streamPriceData(const DatabaseConnection& db) { - std::string query = getBaseQuery(); +std::vector SqlManager::streamPriceData(const DatabaseConnection& db, int LAST_MONTHS) { + std::string query = getBaseQuery(LAST_MONTHS); std::cout << "Executing query: " << query << std::endl; return db.streamQuery(query); } From 3ba4cec8d7e3f9de578cb221923c730167135e30 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sun, 12 Apr 2026 08:56:04 +0100 Subject: [PATCH 03/39] Looping around tick data from multiple symbols --- include/models/priceData.hpp | 8 +++-- include/operations.hpp | 15 ++++++++ include/sqlManager.hpp | 7 ++-- source/databaseConnection.cpp | 42 +++------------------- source/main.cpp | 25 +++---------- source/operations.cpp | 52 +++++++++++++++++++++++++++ source/sqlManager.cpp | 23 +++++++++--- source/{ => utilities}/jsonParser.cpp | 0 8 files changed, 101 insertions(+), 71 deletions(-) create mode 100644 include/operations.hpp create mode 100644 source/operations.cpp rename source/{ => utilities}/jsonParser.cpp (100%) diff --git a/include/models/priceData.hpp b/include/models/priceData.hpp index 09f52ed..23fe3a6 100644 --- a/include/models/priceData.hpp +++ b/include/models/priceData.hpp @@ -5,15 +5,17 @@ // --------------------------------------- #pragma once #include +#include struct PriceData { double ask; double bid; std::chrono::system_clock::time_point timestamp; + std::string symbol; // Constructor for easy creation - PriceData(double ask, double bid, const std::chrono::system_clock::time_point& ts) - : ask(ask), bid(bid), timestamp(ts) {} + PriceData(double ask, double bid, const std::chrono::system_clock::time_point& ts, const std::string& symbol) + : ask(ask), bid(bid), timestamp(ts), symbol(symbol) {} - PriceData() : ask(0.0), bid(0.0), timestamp{} {} + PriceData() : ask(0.0), bid(0.0), timestamp{}, symbol("") {} }; diff --git a/include/operations.hpp b/include/operations.hpp new file mode 100644 index 0000000..cdb7cec --- /dev/null +++ b/include/operations.hpp @@ -0,0 +1,15 @@ +// 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 "models/priceData.hpp" + +class Operations { + +public: + static void run(const std::vector& priceData); +}; diff --git a/include/sqlManager.hpp b/include/sqlManager.hpp index 21a03b5..24b70d0 100644 --- a/include/sqlManager.hpp +++ b/include/sqlManager.hpp @@ -11,8 +11,7 @@ class SqlManager { public: - static std::vector streamPriceData(const DatabaseConnection& db, int LAST_MONTHS = 1); - static std::string getBaseQuery(int LAST_MONTHS = 1); -private: - static constexpr int STREAM_LIMIT = 200000; + static std::vector streamPriceData(const DatabaseConnection& db, const std::vector& symbols, int LAST_MONTHS = 1); + static std::string getBaseQuery(const std::vector& symbols, int LAST_MONTHS = 1); + }; diff --git a/source/databaseConnection.cpp b/source/databaseConnection.cpp index 37ef8d4..ef15d38 100644 --- a/source/databaseConnection.cpp +++ b/source/databaseConnection.cpp @@ -46,41 +46,6 @@ DatabaseConnection::DatabaseConnection(const std::string& endpoint, int port, } -std::vector DatabaseConnection::executeQuery(const std::string& query) const { - std::vector results; - - try { - pqxx::connection conn(this->connection_string); - - if (!conn.is_open()) { - throw std::invalid_argument("Failed to open database connection"); - } - - std::cout << "Connected to database successfully!" << std::endl; - - pqxx::work txn(conn); - pqxx::result result = txn.exec(query); - - // Convert results to PriceData objects - for (const auto& row : result) { - double ask = row[0].as(); - double bid = row[1].as(); - std::string timestamp_str = row[2].as(); - - auto timestamp = Utilities::parseTimestamp(timestamp_str); - - results.emplace_back(ask, bid, timestamp); - } - - txn.commit(); - - } catch (const std::exception& e) { - std::cerr << "Error: " << e.what() << std::endl; - } - - return results; -} - std::vector DatabaseConnection::streamQuery(const std::string& query) const { pqxx::connection conn(this->connection_string); pqxx::nontransaction txn(conn); @@ -91,11 +56,12 @@ std::vector DatabaseConnection::streamQuery(const std::string& query) for (int i = 0; i < (int)result.size(); ++i) { const auto& row = result[i]; double ask, bid; - auto sv1 = row[0].view(); - auto sv2 = row[1].view(); + auto symbol = row[0].view(); + auto sv1 = row[1].view(); + auto sv2 = row[2].view(); std::from_chars(sv1.data(), sv1.data() + sv1.size(), ask); std::from_chars(sv2.data(), sv2.data() + sv2.size(), bid); - results[i] = PriceData(ask, bid, fastParseTimestamp(row[2].c_str())); + results[i] = PriceData(ask, bid, fastParseTimestamp(row[3].c_str()), std::string(symbol)); } return results; diff --git a/source/main.cpp b/source/main.cpp index 2f1e4c3..ec8d98a 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -23,6 +23,7 @@ #include "tradeManager.hpp" #include "jsonParser.hpp" #include "sqlManager.hpp" +#include "operations.hpp" using json = nlohmann::json; @@ -35,29 +36,11 @@ int main(int argc, const char * argv[]) { JsonParser::parseConfigurationFromBase64(argv[2]); - std::vector ticks = SqlManager::streamPriceData(db, 1); + std::vector symbols = {"AUSIDXAUD", "EURUSD"}; + std::vector ticks = SqlManager::streamPriceData(db, symbols, 1); printf("Total ticks streamed: %zu\n", ticks.size()); - // print first tick - auto time_t = std::chrono::system_clock::to_time_t(ticks[0].timestamp); - struct tm tm = {}; - if (localtime_r(&time_t, &tm) == nullptr) { - std::cerr << "Error: failed to convert timestamp" << std::endl; - } - char buffer[20]; - std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm); - printf("First tick: ask=%.4f, bid=%.4f timestamp=%s\n", ticks[0].ask, ticks[0].bid, buffer); - - auto tradeManager = TradeManager::getInstance(); - - std::string tradeId = tradeManager->openTrade(ticks[0].ask, 100000, true); - std::cout << "Opened trade: " << tradeId << std::endl; - - size_t openTrades = tradeManager->reviewAccount(); - std::cout << "Number of open trades: " << openTrades << std::endl; - - bool closed = tradeManager->closeTrade(tradeId); - std::cout << "Trade closed: " << (closed ? "yes" : "no") << std::endl; + Operations::run(ticks); return 0; diff --git a/source/operations.cpp b/source/operations.cpp new file mode 100644 index 0000000..e3cf9cd --- /dev/null +++ b/source/operations.cpp @@ -0,0 +1,52 @@ +// Backtesting Engine in C++ +// +// (c) 2026 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- + +#include "operations.hpp" +// std headers +#include +#include +#include +#include +#include +#include "tradeManager.hpp" + +void Operations::run(const std::vector& ticks) { + + // Loop aroudn every tick + // Example output: + // symbol=AUSIDXAUD, ask=8602.4000, bid=8599.4000 timestamp=2026-03-12 18:39:01.076 + // symbol=AUSIDXAUD, ask=8602.9000, bid=8599.9000 timestamp=2026-03-12 18:39:01.584 + // symbol=EURUSD, ask=1.1513, bid=1.1512 timestamp=2026-03-12 18:39:01.644 + // symbol=AUSIDXAUD, ask=8602.4000, bid=8599.4000 timestamp=2026-03-12 18:39:01.770 + // symbol=AUSIDXAUD, ask=8601.9000, bid=8598.9000 timestamp=2026-03-12 18:39:01.982 + + for (const auto& tick : ticks) { + // (void)tick; + + // print first tick + auto time_t = std::chrono::system_clock::to_time_t(tick.timestamp); + struct tm tm = {}; + if (localtime_r(&time_t, &tm) == nullptr) { + std::cerr << "Error: failed to convert timestamp" << std::endl; + } + char buffer[20]; + std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm); + auto ms = std::chrono::duration_cast(tick.timestamp.time_since_epoch()) % 1000; + printf("symbol=%s, ask=%.4f, bid=%.4f timestamp=%s.%03lld\n", tick.symbol.c_str(), tick.ask, tick.bid, buffer, + static_cast(ms.count())); + } + + auto tradeManager = TradeManager::getInstance(); + + // std::string tradeId = tradeManager->openTrade(ticks[0].ask, 100000, true); + // std::cout << "Opened trade: " << tradeId << std::endl; + + // size_t openTrades = tradeManager->reviewAccount(); + // std::cout << "Number of open trades: " << openTrades << std::endl; + + // bool closed = tradeManager->closeTrade(tradeId); + // std::cout << "Trade closed: " << (closed ? "yes" : "no") << std::endl; +} diff --git a/source/sqlManager.cpp b/source/sqlManager.cpp index 6d74f3d..37542f9 100644 --- a/source/sqlManager.cpp +++ b/source/sqlManager.cpp @@ -1,18 +1,31 @@ // Backtesting Engine in C++ // -// (c) 2025 Ryan McCaffery | https://mccaffers.com +// (c) 2026 Ryan McCaffery | https://mccaffers.com // This code is licensed under MIT license (see LICENSE.txt for details) // --------------------------------------- #include "sqlManager.hpp" #include #include -std::string SqlManager::getBaseQuery(int LAST_MONTHS) { - return "SELECT * FROM EURUSD WHERE timestamp >= dateadd('M', -" + std::to_string(LAST_MONTHS) + ", now()) LIMIT " + std::to_string(STREAM_LIMIT); +std::string SqlManager::getBaseQuery(const std::vector& symbols, int LAST_MONTHS) { + if (symbols.empty()) { + return ""; + } + + std::string query; + for (size_t i = 0; i < symbols.size(); ++i) { + if (i > 0) { + query += " UNION ALL "; + } + query += "SELECT '" + symbols[i] + "' as symbol, * FROM '" + symbols[i] + "' WHERE timestamp >= dateadd('M', -" + std::to_string(LAST_MONTHS) + ", now())"; + } + query += " ORDER BY timestamp"; + + return query; } -std::vector SqlManager::streamPriceData(const DatabaseConnection& db, int LAST_MONTHS) { - std::string query = getBaseQuery(LAST_MONTHS); +std::vector SqlManager::streamPriceData(const DatabaseConnection& db, const std::vector& symbols, int LAST_MONTHS) { + std::string query = getBaseQuery(symbols, LAST_MONTHS); std::cout << "Executing query: " << query << std::endl; return db.streamQuery(query); } diff --git a/source/jsonParser.cpp b/source/utilities/jsonParser.cpp similarity index 100% rename from source/jsonParser.cpp rename to source/utilities/jsonParser.cpp From 46e720dc9ef26bd5da49180dad1cd5686e62249b Mon Sep 17 00:00:00 2001 From: mccaffers <16667079+mccaffers@users.noreply.github.com> Date: Sun, 12 Apr 2026 09:37:55 +0100 Subject: [PATCH 04/39] Update source/CMakeLists.txt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- source/CMakeLists.txt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index f39c14e..ce98442 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -1,12 +1,14 @@ cmake_minimum_required(VERSION 3.30) # CMAKE_OSX_SYSROOT is a macOS-specific setting that specifies the SDK path. -# This is ignored on non-Apple platforms, so it's safe to include in cross-platform builds. -execute_process( - COMMAND xcrun --show-sdk-path - OUTPUT_VARIABLE CMAKE_OSX_SYSROOT - OUTPUT_STRIP_TRAILING_WHITESPACE -) +# Only query and set it on Apple platforms, where xcrun is expected to exist. +if(APPLE) + execute_process( + COMMAND xcrun --show-sdk-path + OUTPUT_VARIABLE CMAKE_OSX_SYSROOT + OUTPUT_STRIP_TRAILING_WHITESPACE + ) +endif() project(BacktestingEngine) From 61309bd91ab0dc099c640dfd4b872fd4e96ee543 Mon Sep 17 00:00:00 2001 From: mccaffers <16667079+mccaffers@users.noreply.github.com> Date: Sun, 12 Apr 2026 09:41:07 +0100 Subject: [PATCH 05/39] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CMakeLists.txt | 4 ++-- include/databaseConnection.hpp | 1 - scripts/run.sh | 5 ++--- source/databaseConnection.cpp | 13 ++++++++----- source/main.cpp | 4 ++++ source/operations.cpp | 4 +++- source/sqlManager.cpp | 1 + 7 files changed, 20 insertions(+), 12 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f39c14e..ad2c316 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,17 +50,17 @@ file(GLOB_RECURSE SOURCES "source/*.cpp") # Create a library of your project's code add_library(BacktestingEngineLib STATIC ${SOURCES}) -# Replace find_package(OpenMP REQUIRED) with this: +# Configure OpenMP. On Apple, provide Homebrew libomp hints before discovery. if(APPLE) set(OpenMP_C_FLAGS "-Xclang -fopenmp") set(OpenMP_CXX_FLAGS "-Xclang -fopenmp") set(OpenMP_C_LIB_NAMES "omp") set(OpenMP_CXX_LIB_NAMES "omp") set(OpenMP_omp_LIBRARY /opt/homebrew/opt/libomp/lib/libomp.dylib) - find_package(OpenMP REQUIRED) target_include_directories(BacktestingEngineLib PRIVATE /opt/homebrew/opt/libomp/include) endif() +find_package(OpenMP REQUIRED) target_link_libraries(BacktestingEngineLib PUBLIC pqxx OpenMP::OpenMP_CXX) # Main executable diff --git a/include/databaseConnection.hpp b/include/databaseConnection.hpp index b885761..1f81d45 100644 --- a/include/databaseConnection.hpp +++ b/include/databaseConnection.hpp @@ -20,7 +20,6 @@ class DatabaseConnection { const std::string& password = ""); void printResults(const std::vector& results) const; - std::vector executeQuery(const std::string& query) const; std::vector streamQuery(const std::string& query) const; const std::string& getConnectionString() const { diff --git a/scripts/run.sh b/scripts/run.sh index f4b400c..c92878e 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -5,9 +5,8 @@ current_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # Build the source code # source $current_dir/environment.sh - no longer necessary -source $current_dir/clean.sh -source $current_dir/build.sh -if [ $? -ne 0 ]; then +source "$current_dir/clean.sh" +if ! bash "$current_dir/build.sh"; then echo "Error: Build failed. Aborting." exit 1 fi diff --git a/source/databaseConnection.cpp b/source/databaseConnection.cpp index ef15d38..4a34e2a 100644 --- a/source/databaseConnection.cpp +++ b/source/databaseConnection.cpp @@ -9,12 +9,15 @@ #include #include #include -#include -#include +#include static std::chrono::system_clock::time_point fastParseTimestamp(const char* ts) { - int year, month, day, hour, min, sec, usec = 0; - std::sscanf(ts, "%4d-%2d-%2d %2d:%2d:%2d.%d", &year, &month, &day, &hour, &min, &sec, &usec); + int year = 0, month = 0, day = 0, hour = 0, min = 0, sec = 0, usec = 0; + 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"); + } // Cache timegm per date — tick data is time-ordered so date changes rarely static char cachedDate[11] = {}; @@ -53,7 +56,7 @@ std::vector DatabaseConnection::streamQuery(const std::string& query) std::vector results(result.size()); - for (int i = 0; i < (int)result.size(); ++i) { + for (std::size_t i = 0; i < result.size(); ++i) { const auto& row = result[i]; double ask, bid; auto symbol = row[0].view(); diff --git a/source/main.cpp b/source/main.cpp index ec8d98a..3934e78 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -32,6 +32,10 @@ using json = nlohmann::json; // argv[2] — Base64-encoded JSON strategy configuration int main(int argc, const char * argv[]) { + if (argc < 3) { + std::cerr << "Usage: " << argv[0] << " " << std::endl; + return 1; + } DatabaseConnection db(argv[1], 8812, "qdb", "admin", "quest"); JsonParser::parseConfigurationFromBase64(argv[2]); diff --git a/source/operations.cpp b/source/operations.cpp index e3cf9cd..7aae2d1 100644 --- a/source/operations.cpp +++ b/source/operations.cpp @@ -11,11 +11,13 @@ #include #include #include +#include +#include #include "tradeManager.hpp" void Operations::run(const std::vector& ticks) { - // Loop aroudn every tick + // Loop around every tick // Example output: // symbol=AUSIDXAUD, ask=8602.4000, bid=8599.4000 timestamp=2026-03-12 18:39:01.076 // symbol=AUSIDXAUD, ask=8602.9000, bid=8599.9000 timestamp=2026-03-12 18:39:01.584 diff --git a/source/sqlManager.cpp b/source/sqlManager.cpp index 37542f9..9de55ce 100644 --- a/source/sqlManager.cpp +++ b/source/sqlManager.cpp @@ -4,6 +4,7 @@ // This code is licensed under MIT license (see LICENSE.txt for details) // --------------------------------------- #include "sqlManager.hpp" +#include #include #include From 48a9a99d5c5b0eeb435639ac4b8f77d5e2f1fbcd Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sun, 12 Apr 2026 09:47:21 +0100 Subject: [PATCH 06/39] Refactoring --- .../project.pbxproj | 18 +++++++++++++++--- include/models/priceData.hpp | 2 +- include/sqlManager.hpp | 2 +- source/databaseConnection.cpp | 2 +- source/utilities/jsonParser.cpp | 2 +- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/backtesting-engine-cpp.xcodeproj/project.pbxproj b/backtesting-engine-cpp.xcodeproj/project.pbxproj index e6f732a..8e066cd 100644 --- a/backtesting-engine-cpp.xcodeproj/project.pbxproj +++ b/backtesting-engine-cpp.xcodeproj/project.pbxproj @@ -29,6 +29,8 @@ 94674B8E2D533E7800973137 /* trade.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94674B8B2D533E7800973137 /* trade.cpp */; }; 9470B5A42C8C5AD0007D9CC6 /* main.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9470B5A32C8C5AD0007D9CC6 /* main.cpp */; }; 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 */; }; 94CD8B992D2DCDD800041BBA /* libpqxx-7.10.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 94CD8B982D2DCDD800041BBA /* libpqxx-7.10.a */; }; 94CD8B9C2D2DD02A00041BBA /* libpqxx-7.10.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 94CD8B9A2D2DCF6E00041BBA /* libpqxx-7.10.a */; }; 94CD8BA02D2E8CE500041BBA /* databaseConnection.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94CD8B9F2D2E8CE500041BBA /* databaseConnection.cpp */; }; @@ -89,6 +91,8 @@ 9470B5A12C8C5AD0007D9CC6 /* source */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = source; sourceTree = BUILT_PRODUCTS_DIR; }; 9470B5A32C8C5AD0007D9CC6 /* main.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = main.cpp; sourceTree = ""; }; 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 = ""; }; 948A9CCD2C906A5600E23669 /* CONVENTIONS.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONVENTIONS.md; sourceTree = ""; }; 948A9CED2C906AFE00E23669 /* 2020.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = 2020.csv; sourceTree = ""; }; 94BBA4512D2EA2640010E04D /* build.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = build.sh; sourceTree = ""; }; @@ -1302,6 +1306,7 @@ isa = PBXGroup; children = ( 94280BA22D2FC00200F1CF56 /* base64.cpp */, + 943398232D57E53400287A2D /* jsonParser.cpp */, ); path = utilities; sourceTree = ""; @@ -1408,8 +1413,8 @@ 940A61112C92CE210083FEB8 /* configManager.cpp */, 940A61152C92CE960083FEB8 /* serviceA.cpp */, 94CD8B9F2D2E8CE500041BBA /* databaseConnection.cpp */, - 943398232D57E53400287A2D /* jsonParser.cpp */, 941408AD2D59F93F000ED1F9 /* sqlManager.cpp */, + 94724A822F8B92C10029B940 /* operations.cpp */, ); path = source; sourceTree = ""; @@ -3515,15 +3520,16 @@ 94DE4F772C8C3E7C00FE48FF /* include */ = { isa = PBXGroup; children = ( - 941408B02D59F954000ED1F9 /* sqlManager.hpp */, - 943398222D57E52900287A2D /* jsonParser.hpp */, 94674B842D533B2F00973137 /* trading */, 942966D72D48E84100532862 /* models */, 94B8C7932D3D770800E17EB6 /* utilities */, 941B548F2D3BBA3B00E3BF64 /* trading_definitions */, 941B549C2D3BBFB900E3BF64 /* trading_definitions.hpp */, 940A61162C92CE960083FEB8 /* serviceA.hpp */, + 943398222D57E52900287A2D /* jsonParser.hpp */, 940A61122C92CE210083FEB8 /* configManager.hpp */, + 941408B02D59F954000ED1F9 /* sqlManager.hpp */, + 94724A852F8B92E30029B940 /* operations.hpp */, 94CD8B9E2D2E8CE500041BBA /* databaseConnection.hpp */, ); path = include; @@ -3631,6 +3637,7 @@ 94674B872D533B4000973137 /* tradeManager.cpp in Sources */, 94CD8BA02D2E8CE500041BBA /* databaseConnection.cpp in Sources */, 940A61132C92CE210083FEB8 /* configManager.cpp in Sources */, + 94724A842F8B92C10029B940 /* operations.cpp in Sources */, 940A61172C92CE960083FEB8 /* serviceA.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3646,6 +3653,7 @@ 94674B8D2D533E7800973137 /* trade.cpp in Sources */, 941B549A2D3BBADE00E3BF64 /* trading_definitions_json.cpp in Sources */, 94674B8A2D533BDA00973137 /* tradeManager.mm in Sources */, + 94724A832F8B92C10029B940 /* operations.cpp in Sources */, 940A61182C92CE960083FEB8 /* serviceA.cpp in Sources */, 94674B882D533B4000973137 /* tradeManager.cpp in Sources */, 943398272D57E54000287A2D /* jsonParser.mm in Sources */, @@ -3788,6 +3796,7 @@ "\"$(SRCROOT)/external/libpqxx/build/src\"", "/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql", ); + MACOSX_DEPLOYMENT_TARGET = 26.0; OTHER_LDFLAGS = ""; OTHER_LIBTOOLFLAGS = ""; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3810,6 +3819,7 @@ "\"$(SRCROOT)/external/libpqxx/build/src\"", "/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql", ); + MACOSX_DEPLOYMENT_TARGET = 26.0; OTHER_LDFLAGS = ""; OTHER_LIBTOOLFLAGS = ""; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3832,6 +3842,7 @@ "\"$(SRCROOT)/external/libpqxx/build/src\"", "/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql", ); + MACOSX_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.mccaffers.tests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3854,6 +3865,7 @@ "\"$(SRCROOT)/external/libpqxx/build/src\"", "/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql", ); + MACOSX_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.mccaffers.tests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/include/models/priceData.hpp b/include/models/priceData.hpp index 23fe3a6..c57a2ff 100644 --- a/include/models/priceData.hpp +++ b/include/models/priceData.hpp @@ -1,6 +1,6 @@ // Backtesting Engine in C++ // -// (c) 2025 Ryan McCaffery | https://mccaffers.com +// (c) 2026 Ryan McCaffery | https://mccaffers.com // This code is licensed under MIT license (see LICENSE.txt for details) // --------------------------------------- #pragma once diff --git a/include/sqlManager.hpp b/include/sqlManager.hpp index 24b70d0..0eee8d9 100644 --- a/include/sqlManager.hpp +++ b/include/sqlManager.hpp @@ -1,6 +1,6 @@ // Backtesting Engine in C++ // -// (c) 2025 Ryan McCaffery | https://mccaffers.com +// (c) 2026 Ryan McCaffery | https://mccaffers.com // This code is licensed under MIT license (see LICENSE.txt for details) // --------------------------------------- #pragma once diff --git a/source/databaseConnection.cpp b/source/databaseConnection.cpp index ef15d38..d382e90 100644 --- a/source/databaseConnection.cpp +++ b/source/databaseConnection.cpp @@ -1,6 +1,6 @@ // Backtesting Engine in C++ // -// (c) 2025 Ryan McCaffery | https://mccaffers.com +// (c) 2026 Ryan McCaffery | https://mccaffers.com // This code is licensed under MIT license (see LICENSE.txt for details) // --------------------------------------- diff --git a/source/utilities/jsonParser.cpp b/source/utilities/jsonParser.cpp index 648ab06..5c6c630 100644 --- a/source/utilities/jsonParser.cpp +++ b/source/utilities/jsonParser.cpp @@ -1,6 +1,6 @@ // Backtesting Engine in C++ // -// (c) 2025 Ryan McCaffery | https://mccaffers.com +// (c) 2026 Ryan McCaffery | https://mccaffers.com // This code is licensed under MIT license (see LICENSE.txt for details) // --------------------------------------- From 85be15be1681042453cfd3c9bd4cfdd2421eb128 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sun, 12 Apr 2026 09:53:58 +0100 Subject: [PATCH 07/39] fix run script, shell scopping behaviour was putting the build script in it's own child process --- scripts/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/run.sh b/scripts/run.sh index c92878e..cf0baf1 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -6,7 +6,7 @@ current_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # Build the source code # source $current_dir/environment.sh - no longer necessary source "$current_dir/clean.sh" -if ! bash "$current_dir/build.sh"; then +if ! source "$current_dir/build.sh"; then echo "Error: Build failed. Aborting." exit 1 fi From cbc033d489523f2b591d40234da5a66f41253222 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:16:51 +0100 Subject: [PATCH 08/39] Update build os to target macos-latest --- .github/workflows/sonarcloud.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 6897994..fdbe154 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -17,7 +17,7 @@ jobs: build: name: Build & Test - runs-on: macos-14 # Use macOS 14 (Sonoma) runner + runs-on: macos-latest steps: # Step 1: Check out the repository code From bb24f067a8cf0038da2b3e3c57cab345fffb3e3a Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sun, 12 Apr 2026 10:23:18 +0100 Subject: [PATCH 09/39] Update build os to target macos-latest --- .github/workflows/sonarcloud.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index fdbe154..b219e3f 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -17,7 +17,7 @@ jobs: build: name: Build & Test - runs-on: macos-latest + runs-on: macos-26.2 steps: # Step 1: Check out the repository code @@ -40,7 +40,7 @@ jobs: # Step 5: Configure Xcode version - name: Select Xcode version run: | - sudo xcode-select -switch /Applications/Xcode_15.2.app + sudo xcode-select -switch /Applications/Xcode_26.2.app /usr/bin/xcodebuild -version # Run XCode tests with specific configurations: # - Builds and runs the test suite From 27f82f1fe7d217c080eff44ab50f71858be9be7f Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:22:59 +0100 Subject: [PATCH 10/39] Updating github workflow os --- .github/workflows/sonarcloud.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index b219e3f..4004505 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -17,7 +17,7 @@ jobs: build: name: Build & Test - runs-on: macos-26.2 + runs-on: macos-26 steps: # Step 1: Check out the repository code From e4e8e1334f0442c837e3f616147bba5b65109004 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:01:02 +0100 Subject: [PATCH 11/39] Refactoring build pipeline --- .github/workflows/build.sh | 8 ++++++-- .github/workflows/sonarcloud.yml | 5 +++-- .vscode/c_cpp_properties.json | 12 ++++++++++++ scripts/build.sh | 13 +++++++++++-- scripts/build_dep.sh | 8 +++++++- source/databaseConnection.cpp | 2 +- source/main.cpp | 6 ++++++ 7 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 .vscode/c_cpp_properties.json diff --git a/.github/workflows/build.sh b/.github/workflows/build.sh index 822e713..769d583 100644 --- a/.github/workflows/build.sh +++ b/.github/workflows/build.sh @@ -17,7 +17,11 @@ export PKG_CONFIG_PATH="$(brew --prefix libpq)/lib/pkgconfig:$PKG_CONFIG_PATH" export PostgreSQL_ROOT="$(brew --prefix libpq)" # 1. Generate build files (Passing your CXX flags directly to CMake instead of configure) -cmake .. -DCMAKE_CXX_STANDARD=20 -DCMAKE_BUILD_TYPE=Release +cmake .. \ + -DCMAKE_CXX_STANDARD=20 \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_OSX_SYSROOT=$(xcrun --show-sdk-path) \ + -DSKIP_BUILD_TEST=ON # 2. Compile libpqxx -make \ No newline at end of file +make -j$(sysctl -n hw.logicalcpu) \ No newline at end of file diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 4004505..09ec2a4 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -35,7 +35,8 @@ jobs: # Step 4: Build project external libraries - name: Build C++ Libraries - run: bash ./.github/workflows/build.sh + run: bash ./scripts/build_dep.sh + # run: bash ./.github/workflows/build.sh # Step 5: Configure Xcode version - name: Select Xcode version @@ -92,7 +93,7 @@ jobs: cmake --version - name: Build C++ Libraries run: > - bash ./.github/workflows/build.sh + sh ./scripts/build.sh - name: Install Python 3.12 for gcovr uses: actions/setup-python@v5 with: diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..0863e6f --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,12 @@ +{ + "configurations": [ + { + "name": "Mac", + "compileCommands": "${workspaceFolder}/build/compile_commands.json", + "includePath": [ + "${workspaceFolder}/external/libpqxx/include" + ] + } + ], + "version": 4 +} \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh index 3a17995..1a3a1ec 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -12,8 +12,17 @@ fi # Step 2: Navigate to the build directory cd "$BUILD_DIR" || exit -# Step 3: Run CMake to configure the project -cmake .. +# Expose paths so CMake finds libpq +export PATH="$(brew --prefix libpq)/bin:$PATH" +export PKG_CONFIG_PATH="$(brew --prefix libpq)/lib/pkgconfig:$PKG_CONFIG_PATH" +export PostgreSQL_ROOT="$(brew --prefix libpq)" + +# 1. Generate build files (Passing your CXX flags directly to CMake instead of configure) +cmake .. \ + -DCMAKE_CXX_STANDARD=20 \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_OSX_SYSROOT=$(xcrun --show-sdk-path) \ + -DSKIP_BUILD_TEST=ON # Step 4: Compile the project cmake --build . diff --git a/scripts/build_dep.sh b/scripts/build_dep.sh index 9afd5e6..24143dc 100644 --- a/scripts/build_dep.sh +++ b/scripts/build_dep.sh @@ -3,9 +3,15 @@ cd ./external/libpqxx mkdir -p build cd ./build +# Expose paths so CMake finds libpq export PATH="$(brew --prefix libpq)/bin:$PATH" export PKG_CONFIG_PATH="$(brew --prefix libpq)/lib/pkgconfig:$PKG_CONFIG_PATH" export PostgreSQL_ROOT="$(brew --prefix libpq)" -cmake .. -DCMAKE_CXX_STANDARD=20 -DCMAKE_BUILD_TYPE=Release +# 1. Generate build files (Passing your CXX flags directly to CMake instead of configure) +cmake .. \ + -DCMAKE_CXX_STANDARD=20 \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_OSX_SYSROOT=$(xcrun --show-sdk-path) \ + -DSKIP_BUILD_TEST=ON make \ No newline at end of file diff --git a/source/databaseConnection.cpp b/source/databaseConnection.cpp index 7f050bd..5f78ed1 100644 --- a/source/databaseConnection.cpp +++ b/source/databaseConnection.cpp @@ -57,7 +57,7 @@ std::vector DatabaseConnection::streamQuery(const std::string& query) std::vector results(result.size()); for (std::size_t i = 0; i < result.size(); ++i) { - const auto& row = result[i]; + const auto& row = result[static_cast(i)]; double ask, bid; auto symbol = row[0].view(); auto sv1 = row[1].view(); diff --git a/source/main.cpp b/source/main.cpp index 3934e78..ea9fbaa 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -32,18 +32,24 @@ using json = nlohmann::json; // argv[2] — Base64-encoded JSON strategy configuration int main(int argc, const char * argv[]) { + // Validate required command-line arguments before proceeding if (argc < 3) { std::cerr << "Usage: " << argv[0] << " " << std::endl; return 1; } + + // Connect to QuestDB on the default port (8812) using default credentials DatabaseConnection db(argv[1], 8812, "qdb", "admin", "quest"); + // Decode and apply the strategy configuration from Base64-encoded JSON JsonParser::parseConfigurationFromBase64(argv[2]); + // Define the instruments to backtest and stream their historical tick data std::vector symbols = {"AUSIDXAUD", "EURUSD"}; std::vector ticks = SqlManager::streamPriceData(db, symbols, 1); printf("Total ticks streamed: %zu\n", ticks.size()); + // Execute the backtest by replaying all ticks through the strategy logic Operations::run(ticks); return 0; From 0b6d40eff702b8323440de738e5a4f19992bbeec Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sat, 25 Apr 2026 20:17:32 +0100 Subject: [PATCH 12/39] fixing workflow --- scripts/build_dep.sh | 2 ++ source/main.cpp | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/build_dep.sh b/scripts/build_dep.sh index 24143dc..fc1aeca 100644 --- a/scripts/build_dep.sh +++ b/scripts/build_dep.sh @@ -1,3 +1,5 @@ +git submodule update --init --recursive + cd ./external/libpqxx mkdir -p build diff --git a/source/main.cpp b/source/main.cpp index ea9fbaa..4be3340 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -46,7 +46,7 @@ int main(int argc, const char * argv[]) { // Define the instruments to backtest and stream their historical tick data std::vector symbols = {"AUSIDXAUD", "EURUSD"}; - std::vector ticks = SqlManager::streamPriceData(db, symbols, 1); + std::vector ticks = SqlManager::streamPriceData(db, symbols, 3); printf("Total ticks streamed: %zu\n", ticks.size()); // Execute the backtest by replaying all ticks through the strategy logic From 8a0a710327336e7935f639cbcc62f34dea061464 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sun, 26 Apr 2026 10:28:48 +0100 Subject: [PATCH 13/39] Updating github workflow os --- scripts/build.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/build.sh b/scripts/build.sh index 1a3a1ec..8180c47 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,5 +1,7 @@ #!/bin/bash +git submodule update --init --recursive + BUILD_DIR="build" # Variables EXECUTABLE_NAME="BacktestingEngine" From 801c846e57c741ab08e7c99a6387a621e1cf0fa1 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sun, 26 Apr 2026 10:40:43 +0100 Subject: [PATCH 14/39] Updating github workflow os --- .github/workflows/sonarcloud.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 09ec2a4..e475070 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -11,7 +11,7 @@ name: Build env: # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) BUILD_TYPE: Release - BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed + BUILD_WRAPPER_OUT_DIR: build # Directory where build-wrapper output will be placed jobs: From b57db36785875c27d89066776a78f7d2b2791359 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sun, 26 Apr 2026 10:45:43 +0100 Subject: [PATCH 15/39] Updating github workflow os --- .github/workflows/sonarcloud.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index e475070..2228866 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -11,7 +11,7 @@ name: Build env: # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) BUILD_TYPE: Release - BUILD_WRAPPER_OUT_DIR: build # Directory where build-wrapper output will be placed + BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed jobs: @@ -119,12 +119,12 @@ jobs: # Configures the CMake build system, specifying the source directory and build directory, and setting the build type - name: Configure CMake - run: cmake -S ${{github.workspace}} -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} + run: cmake -S ${{github.workspace}} -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON # Runs the build wrapper to capture build commands and outputs them to the specified directory. Then builds the project using CMake - name: Run build-wrapper run: | - build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} # Performs the SonarQube scan using the scan action. Uses captured build commands for analysis and requires GitHub and SonarQube tokens for authentication - name: SonarQube Scan From 9a60a278a53b52a1046b1ef0a3ff00411a99ca3f Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sun, 26 Apr 2026 10:50:36 +0100 Subject: [PATCH 16/39] Updating github workflow os --- .github/workflows/sonarcloud.yml | 2 +- scripts/build.sh | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 2228866..65bfaba 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -124,7 +124,7 @@ jobs: # Runs the build wrapper to capture build commands and outputs them to the specified directory. Then builds the project using CMake - name: Run build-wrapper run: | - build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --clean-first # Performs the SonarQube scan using the scan action. Uses captured build commands for analysis and requires GitHub and SonarQube tokens for authentication - name: SonarQube Scan diff --git a/scripts/build.sh b/scripts/build.sh index 8180c47..85a295e 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -15,9 +15,11 @@ fi cd "$BUILD_DIR" || exit # Expose paths so CMake finds libpq -export PATH="$(brew --prefix libpq)/bin:$PATH" -export PKG_CONFIG_PATH="$(brew --prefix libpq)/lib/pkgconfig:$PKG_CONFIG_PATH" -export PostgreSQL_ROOT="$(brew --prefix libpq)" +if command -v brew &>/dev/null; then + export PATH="$(brew --prefix libpq)/bin:$PATH" + export PKG_CONFIG_PATH="$(brew --prefix libpq)/lib/pkgconfig:$PKG_CONFIG_PATH" + export PostgreSQL_ROOT="$(brew --prefix libpq)" +fi # 1. Generate build files (Passing your CXX flags directly to CMake instead of configure) cmake .. \ From edb253f432182b92af52f0ac57bd23ce9a5c4c6a Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:46:00 +0100 Subject: [PATCH 17/39] Updating main flow, extracting the strategy to query questdB --- .github/workflows/build.sh | 27 --------------------------- include/jsonParser.hpp | 2 +- scripts/build_dep.sh | 8 +++++--- scripts/run.sh | 2 +- source/main.cpp | 13 +++++++++---- source/operations.cpp | 4 ++-- source/utilities/jsonParser.cpp | 4 ++-- tests/jsonParser.mm | 4 ++-- 8 files changed, 22 insertions(+), 42 deletions(-) delete mode 100644 .github/workflows/build.sh diff --git a/.github/workflows/build.sh b/.github/workflows/build.sh deleted file mode 100644 index 769d583..0000000 --- a/.github/workflows/build.sh +++ /dev/null @@ -1,27 +0,0 @@ -# Update and initialize the libpqxx submodule and any nested submodules it may have -# --init: Initialize submodules if they haven't been already -# --recursive: Update nested submodules recursively -git submodule update --init --recursive - -# Navigate into the libpqxx submodule directory -cd ./external/libpqxx - -# Create a build directory for out-of-source build -# -p ensures parent directories are created if they don't exist -mkdir -p build -cd ./build - -# Expose paths so CMake finds libpq -export PATH="$(brew --prefix libpq)/bin:$PATH" -export PKG_CONFIG_PATH="$(brew --prefix libpq)/lib/pkgconfig:$PKG_CONFIG_PATH" -export PostgreSQL_ROOT="$(brew --prefix libpq)" - -# 1. Generate build files (Passing your CXX flags directly to CMake instead of configure) -cmake .. \ - -DCMAKE_CXX_STANDARD=20 \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_OSX_SYSROOT=$(xcrun --show-sdk-path) \ - -DSKIP_BUILD_TEST=ON - -# 2. Compile libpqxx -make -j$(sysctl -n hw.logicalcpu) \ No newline at end of file diff --git a/include/jsonParser.hpp b/include/jsonParser.hpp index 779fb4b..330da7a 100644 --- a/include/jsonParser.hpp +++ b/include/jsonParser.hpp @@ -12,5 +12,5 @@ class JsonParser { public: - static int parseConfigurationFromBase64(const std::string& input); + static trading_definitions::Configuration parseConfigurationFromBase64(const std::string& input); }; diff --git a/scripts/build_dep.sh b/scripts/build_dep.sh index fc1aeca..79daa4b 100644 --- a/scripts/build_dep.sh +++ b/scripts/build_dep.sh @@ -6,9 +6,11 @@ mkdir -p build cd ./build # Expose paths so CMake finds libpq -export PATH="$(brew --prefix libpq)/bin:$PATH" -export PKG_CONFIG_PATH="$(brew --prefix libpq)/lib/pkgconfig:$PKG_CONFIG_PATH" -export PostgreSQL_ROOT="$(brew --prefix libpq)" +if command -v brew &>/dev/null; then + export PATH="$(brew --prefix libpq)/bin:$PATH" + export PKG_CONFIG_PATH="$(brew --prefix libpq)/lib/pkgconfig:$PKG_CONFIG_PATH" + export PostgreSQL_ROOT="$(brew --prefix libpq)" +fi # 1. Generate build files (Passing your CXX flags directly to CMake instead of configure) cmake .. \ diff --git a/scripts/run.sh b/scripts/run.sh index cf0baf1..211e53f 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -22,7 +22,7 @@ fi json='{ "RUN_ID": "UNIQUE_IDENTIFER", - "SYMBOLS": "EURUSD", + "SYMBOLS": "EURUSD,AUSIDXAUD", "LAST_MONTHS": 6, "STRATEGY": { "UUID": "", diff --git a/source/main.cpp b/source/main.cpp index 4be3340..02db63d 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -10,6 +10,7 @@ #include #include #include +#include // external headers #include @@ -42,10 +43,14 @@ int main(int argc, const char * argv[]) { DatabaseConnection db(argv[1], 8812, "qdb", "admin", "quest"); // Decode and apply the strategy configuration from Base64-encoded JSON - JsonParser::parseConfigurationFromBase64(argv[2]); - - // Define the instruments to backtest and stream their historical tick data - std::vector symbols = {"AUSIDXAUD", "EURUSD"}; + auto config = JsonParser::parseConfigurationFromBase64(argv[2]); + + // Split config.SYMBOLS (comma-separated) into a vector + std::vector symbols; + std::istringstream ss(config.SYMBOLS); + for (std::string token; std::getline(ss, token, ',');) { + symbols.push_back(token); + } std::vector ticks = SqlManager::streamPriceData(db, symbols, 3); printf("Total ticks streamed: %zu\n", ticks.size()); diff --git a/source/operations.cpp b/source/operations.cpp index 7aae2d1..0318174 100644 --- a/source/operations.cpp +++ b/source/operations.cpp @@ -37,8 +37,8 @@ void Operations::run(const std::vector& ticks) { char buffer[20]; std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm); auto ms = std::chrono::duration_cast(tick.timestamp.time_since_epoch()) % 1000; - printf("symbol=%s, ask=%.4f, bid=%.4f timestamp=%s.%03lld\n", tick.symbol.c_str(), tick.ask, tick.bid, buffer, - static_cast(ms.count())); + // printf("symbol=%s, ask=%.4f, bid=%.4f timestamp=%s.%03lld\n", tick.symbol.c_str(), tick.ask, tick.bid, buffer, + // static_cast(ms.count())); } auto tradeManager = TradeManager::getInstance(); diff --git a/source/utilities/jsonParser.cpp b/source/utilities/jsonParser.cpp index 5c6c630..f7affcd 100644 --- a/source/utilities/jsonParser.cpp +++ b/source/utilities/jsonParser.cpp @@ -10,7 +10,7 @@ using json = nlohmann::json; -int JsonParser::parseConfigurationFromBase64(const std::string& input) { +trading_definitions::Configuration JsonParser::parseConfigurationFromBase64(const std::string& input) { // Ingest parameters std::string output = Base64::b64decode(input); @@ -28,5 +28,5 @@ int JsonParser::parseConfigurationFromBase64(const std::string& input) { auto config = j.get(); std::cout << config.RUN_ID << std::endl; - return 0; + return config; } diff --git a/tests/jsonParser.mm b/tests/jsonParser.mm index 88158b8..4829238 100644 --- a/tests/jsonParser.mm +++ b/tests/jsonParser.mm @@ -54,8 +54,8 @@ - (void)testValidJsonParsing { std::string base64Input = Base64::b64encode(validJson); // Test parsing - int result = JsonParser::parseConfigurationFromBase64(base64Input); - XCTAssertEqual(result, 0, "Parsing should succeed with valid JSON"); + trading_definitions::Configuration result = JsonParser::parseConfigurationFromBase64(base64Input); + XCTAssertFalse(result.RUN_ID.empty(), "Parsing should succeed with valid JSON"); } From 657b5e255eaedf1f4e4a1c6d03d89709ce87d662 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Tue, 28 Apr 2026 08:55:35 +0100 Subject: [PATCH 18/39] Updating trade management operations --- include/models/trade.hpp | 34 ++++++++++++----- include/trading/tradeManager.hpp | 16 ++++---- scripts/run.sh | 3 +- source/models/trade.cpp | 3 -- source/operations.cpp | 64 ++++++++++++++++++++++++-------- source/trading/tradeManager.cpp | 46 ++++++++++++----------- tests/tradeManager.mm | 22 +++++++---- 7 files changed, 120 insertions(+), 68 deletions(-) diff --git a/include/models/trade.hpp b/include/models/trade.hpp index 29eb4f5..6317596 100644 --- a/include/models/trade.hpp +++ b/include/models/trade.hpp @@ -8,31 +8,45 @@ #include #include +enum class Direction { + LONG, + SHORT +}; + struct Trade { - static int idCounter; std::string id; double entryPrice; double size; std::chrono::system_clock::time_point openTime; - bool isLong; + Direction direction; + + std::string dealReference; + std::string symbol; + double scalingFactor; + double stopDistancePips; + double limitDistancePips; + std::string strategyId; + std::string strategyName; + + double closePrice; + std::chrono::system_clock::time_point closeTime; // Default constructor - Trade() : entryPrice(0), size(0), isLong(false), + Trade() : entryPrice(0), size(0), direction(Direction::LONG), + scalingFactor(0), stopDistancePips(0), limitDistancePips(0), + closePrice(0), openTime(std::chrono::system_clock::now()) {} // Copy constructor Trade(const Trade& other) = default; - Trade(double price, double quantity, bool long_position) + Trade(double price, double quantity, Direction dir) : entryPrice(price), size(quantity), - isLong(long_position), + direction(dir), + scalingFactor(0), stopDistancePips(0), limitDistancePips(0), openTime(std::chrono::system_clock::now()) { - // Generate unique ID using counter - id = std::to_string(++idCounter); + } - static void resetCounter() { - idCounter = 0; - } }; diff --git a/include/trading/tradeManager.hpp b/include/trading/tradeManager.hpp index e884cc6..105e14c 100644 --- a/include/trading/tradeManager.hpp +++ b/include/trading/tradeManager.hpp @@ -6,22 +6,22 @@ #pragma once #include +#include #include #include "trade.hpp" +#include "priceData.hpp" class TradeManager { private: - static TradeManager* instance; std::unordered_map activeTrades; - - TradeManager() = default; + std::vector closedTrades; public: - static TradeManager* getInstance(); - static void reset(); - void clearAllTrades(); - std::string openTrade(double price, double size, bool isLong); + TradeManager() = default; + std::string openTrade(const PriceData& tick, double size, Direction direction); size_t reviewAccount() const; - bool closeTrade(const std::string& tradeId); + bool closeTrade(const std::string& tradeId, double closePrice); const std::unordered_map& getActiveTrades() const; + const std::vector& getClosedTrades() const; + double calculatePnl() const; }; diff --git a/scripts/run.sh b/scripts/run.sh index 211e53f..0ea94c8 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -20,9 +20,10 @@ else exit 1 fi +# "SYMBOLS": "EURUSD,AUSIDXAUD", json='{ "RUN_ID": "UNIQUE_IDENTIFER", - "SYMBOLS": "EURUSD,AUSIDXAUD", + "SYMBOLS": "EURUSD", "LAST_MONTHS": 6, "STRATEGY": { "UUID": "", diff --git a/source/models/trade.cpp b/source/models/trade.cpp index 1ff900e..c34cad9 100644 --- a/source/models/trade.cpp +++ b/source/models/trade.cpp @@ -5,6 +5,3 @@ // --------------------------------------- #include "trade.hpp" - -// Definition of static member -int Trade::idCounter = 0; diff --git a/source/operations.cpp b/source/operations.cpp index 0318174..99b4873 100644 --- a/source/operations.cpp +++ b/source/operations.cpp @@ -24,30 +24,62 @@ void Operations::run(const std::vector& ticks) { // symbol=EURUSD, ask=1.1513, bid=1.1512 timestamp=2026-03-12 18:39:01.644 // symbol=AUSIDXAUD, ask=8602.4000, bid=8599.4000 timestamp=2026-03-12 18:39:01.770 // symbol=AUSIDXAUD, ask=8601.9000, bid=8598.9000 timestamp=2026-03-12 18:39:01.982 - + + auto tradeManager = new TradeManager(); + + + + for (const auto& tick : ticks) { // (void)tick; // print first tick - auto time_t = std::chrono::system_clock::to_time_t(tick.timestamp); - struct tm tm = {}; - if (localtime_r(&time_t, &tm) == nullptr) { - std::cerr << "Error: failed to convert timestamp" << std::endl; + // auto time_t = std::chrono::system_clock::to_time_t(tick.timestamp); + // struct tm tm = {}; + // if (localtime_r(&time_t, &tm) == nullptr) { + // std::cerr << "Error: failed to convert timestamp" << std::endl; + // } + // char buffer[20]; + // std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm); + // auto ms = std::chrono::duration_cast(tick.timestamp.time_since_epoch()) % 1000; + // // printf("symbol=%s, ask=%.4f, bid=%.4f timestamp=%s.%03lld\n", tick.symbol.c_str(), tick.ask, tick.bid, buffer, + // // static_cast(ms.count())); + + size_t openTrades = tradeManager->reviewAccount(); + + if (openTrades == 0) { + std::string tradeId = tradeManager->openTrade(tick, 100000, Direction::LONG); + std::cout << "Opened trade: " << tradeId << std::endl; } - char buffer[20]; - std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm); - auto ms = std::chrono::duration_cast(tick.timestamp.time_since_epoch()) % 1000; - // printf("symbol=%s, ask=%.4f, bid=%.4f timestamp=%s.%03lld\n", tick.symbol.c_str(), tick.ask, tick.bid, buffer, - // static_cast(ms.count())); - } - auto tradeManager = TradeManager::getInstance(); + // randomly check account status every 100 ticks + if (openTrades > 0 && (std::rand() % 100) == 0) { + std::cout << "Reviewing account at tick timestamp: " << tick.timestamp.time_since_epoch().count() << std::endl; + std::cout << "Number of open trades: " << openTrades << std::endl; + for (const auto& [id, trade] : tradeManager->getActiveTrades()) { + std::cout << "Trade ID: " << id + << " | Entry: " << trade.entryPrice + << " | Size: " << trade.size + << " | Direction: " << (trade.direction == Direction::LONG ? "LONG" : "SHORT") + << std::endl; + } + } - // std::string tradeId = tradeManager->openTrade(ticks[0].ask, 100000, true); - // std::cout << "Opened trade: " << tradeId << std::endl; + + // randomly close trades every 200 ticks + if (openTrades > 0 && (std::rand() % 200) == 0) { + std::vector idsToClose; + for (const auto& [id, trade] : tradeManager->getActiveTrades()) { + idsToClose.push_back(id); + } + for (const auto& id : idsToClose) { + bool closed = tradeManager->closeTrade(id, tick.bid); + std::cout << "Closed trade ID: " << id << " - " << (closed ? "success" : "failure") << std::endl; + } + } + } - // size_t openTrades = tradeManager->reviewAccount(); - // std::cout << "Number of open trades: " << openTrades << std::endl; + std::cout << "Final PnL: " << std::fixed << std::setprecision(2) << tradeManager->calculatePnl() << std::endl; // bool closed = tradeManager->closeTrade(tradeId); // std::cout << "Trade closed: " << (closed ? "yes" : "no") << std::endl; diff --git a/source/trading/tradeManager.cpp b/source/trading/tradeManager.cpp index d3cb308..5c82f43 100644 --- a/source/trading/tradeManager.cpp +++ b/source/trading/tradeManager.cpp @@ -6,27 +6,9 @@ #include "tradeManager.hpp" -TradeManager* TradeManager::instance = nullptr; - -TradeManager* TradeManager::getInstance() { - if (instance == nullptr) { - instance = new TradeManager(); - } - return instance; -} - -void TradeManager::reset() { - delete instance; - instance = nullptr; - Trade::resetCounter(); -} - -void TradeManager::clearAllTrades() { - activeTrades.clear(); -} - -std::string TradeManager::openTrade(double price, double size, bool isLong) { - Trade trade(price, size, isLong); +std::string TradeManager::openTrade(const PriceData& tick, double size, Direction direction) { + double price = (direction == Direction::LONG) ? tick.ask : tick.bid; + Trade trade(price, size, direction); activeTrades[trade.id] = trade; return trade.id; } @@ -35,9 +17,13 @@ size_t TradeManager::reviewAccount() const { return activeTrades.size(); } -bool TradeManager::closeTrade(const std::string& tradeId) { +bool TradeManager::closeTrade(const std::string& tradeId, double closePrice) { auto it = activeTrades.find(tradeId); if (it != activeTrades.end()) { + Trade closed = it->second; + closed.closePrice = closePrice; + closed.closeTime = std::chrono::system_clock::now(); + closedTrades.push_back(closed); activeTrades.erase(it); return true; } @@ -47,3 +33,19 @@ bool TradeManager::closeTrade(const std::string& tradeId) { const std::unordered_map& TradeManager::getActiveTrades() const { return activeTrades; } + +const std::vector& TradeManager::getClosedTrades() const { + return closedTrades; +} + +double TradeManager::calculatePnl() const { + double pnl = 0.0; + for (const auto& trade : closedTrades) { + double diff = trade.closePrice - trade.entryPrice; + if (trade.direction == Direction::SHORT) diff = -diff; + pnl += diff * trade.size; + } + return pnl; +} + + diff --git a/tests/tradeManager.mm b/tests/tradeManager.mm index 1907d50..f57b2fc 100644 --- a/tests/tradeManager.mm +++ b/tests/tradeManager.mm @@ -24,35 +24,41 @@ - (void)tearDown { } - (void)testOpenTrade { - std::string tradeId = self.manager->openTrade(100.0, 1.0, true); + PriceData tick(100.0, 99.0, std::chrono::system_clock::now(), "EURUSD"); + std::string tradeId = self.manager->openTrade(tick, 1.0, Direction::LONG); XCTAssertFalse(tradeId.empty(), "Trade ID should not be empty"); XCTAssertEqual(self.manager->reviewAccount(), 1, "Should have 1 active trade"); } - (void)testCloseTrade { - std::string tradeId = self.manager->openTrade(100.0, 1.0, true); - bool closed = self.manager->closeTrade(tradeId); + PriceData tick(100.0, 99.0, std::chrono::system_clock::now(), "EURUSD"); + std::string tradeId = self.manager->openTrade(tick, 1.0, Direction::LONG); + bool closed = self.manager->closeTrade(tradeId, 110.0); XCTAssertTrue(closed, "Trade should be closed successfully"); XCTAssertEqual(self.manager->reviewAccount(), 0, "Should have 0 active trades"); } - (void)testMultipleTrades { - self.manager->openTrade(100.0, 1.0, true); - self.manager->openTrade(200.0, 2.0, false); - self.manager->openTrade(300.0, 3.0, true); + PriceData tick1(100.0, 99.0, std::chrono::system_clock::now(), "EURUSD"); + PriceData tick2(200.0, 199.0, std::chrono::system_clock::now(), "EURUSD"); + PriceData tick3(300.0, 299.0, std::chrono::system_clock::now(), "EURUSD"); + self.manager->openTrade(tick1, 1.0, Direction::LONG); + self.manager->openTrade(tick2, 2.0, Direction::SHORT); + self.manager->openTrade(tick3, 3.0, Direction::LONG); XCTAssertEqual(self.manager->reviewAccount(), 3, "Should have 3 active trades"); } - (void)testTradeDetails { - std::string tradeId = self.manager->openTrade(100.0, 1.0, true); + PriceData tick(100.0, 99.0, std::chrono::system_clock::now(), "EURUSD"); + std::string tradeId = self.manager->openTrade(tick, 1.0, Direction::LONG); auto trades = self.manager->getActiveTrades(); auto trade = trades.find(tradeId); XCTAssertNotEqual(trade, trades.end(), "Trade should exist"); XCTAssertEqual(trade->second.entryPrice, 100.0, "Entry price should match"); XCTAssertEqual(trade->second.size, 1.0, "Size should match"); - XCTAssertTrue(trade->second.isLong, "Trade should be long"); + XCTAssertTrue(trade->second.direction == Direction::LONG, "Trade should be long"); } @end From 87bc95e3e54bfb4f8fc4987f572c9427b768792c Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Wed, 29 Apr 2026 22:14:48 +0100 Subject: [PATCH 19/39] Refactoring operations flow --- include/sqlManager.hpp | 2 -- scripts/build.sh | 3 ++- scripts/run.sh | 2 +- source/main.cpp | 2 +- source/operations.cpp | 25 +------------------------ source/sqlManager.cpp | 25 +++++++++++-------------- source/trading/tradeManager.cpp | 9 +++++++++ tests/tradeManager.mm | 7 +++---- 8 files changed, 28 insertions(+), 47 deletions(-) diff --git a/include/sqlManager.hpp b/include/sqlManager.hpp index 0eee8d9..04ff5a8 100644 --- a/include/sqlManager.hpp +++ b/include/sqlManager.hpp @@ -12,6 +12,4 @@ class SqlManager { public: static std::vector streamPriceData(const DatabaseConnection& db, const std::vector& symbols, int LAST_MONTHS = 1); - static std::string getBaseQuery(const std::vector& symbols, int LAST_MONTHS = 1); - }; diff --git a/scripts/build.sh b/scripts/build.sh index 85a295e..ab170e4 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -26,7 +26,8 @@ cmake .. \ -DCMAKE_CXX_STANDARD=20 \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_OSX_SYSROOT=$(xcrun --show-sdk-path) \ - -DSKIP_BUILD_TEST=ON + -DSKIP_BUILD_TEST=ON \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON # Step 4: Compile the project cmake --build . diff --git a/scripts/run.sh b/scripts/run.sh index 0ea94c8..e1146cd 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -24,7 +24,7 @@ fi json='{ "RUN_ID": "UNIQUE_IDENTIFER", "SYMBOLS": "EURUSD", - "LAST_MONTHS": 6, + "LAST_MONTHS": 3, "STRATEGY": { "UUID": "", "TRADING_VARIABLES": { diff --git a/source/main.cpp b/source/main.cpp index 02db63d..782e0f9 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -51,7 +51,7 @@ int main(int argc, const char * argv[]) { for (std::string token; std::getline(ss, token, ',');) { symbols.push_back(token); } - std::vector ticks = SqlManager::streamPriceData(db, symbols, 3); + std::vector ticks = SqlManager::streamPriceData(db, symbols, config.LAST_MONTHS); printf("Total ticks streamed: %zu\n", ticks.size()); // Execute the backtest by replaying all ticks through the strategy logic diff --git a/source/operations.cpp b/source/operations.cpp index 99b4873..9da2fe5 100644 --- a/source/operations.cpp +++ b/source/operations.cpp @@ -17,33 +17,10 @@ void Operations::run(const std::vector& ticks) { - // Loop around every tick - // Example output: - // symbol=AUSIDXAUD, ask=8602.4000, bid=8599.4000 timestamp=2026-03-12 18:39:01.076 - // symbol=AUSIDXAUD, ask=8602.9000, bid=8599.9000 timestamp=2026-03-12 18:39:01.584 - // symbol=EURUSD, ask=1.1513, bid=1.1512 timestamp=2026-03-12 18:39:01.644 - // symbol=AUSIDXAUD, ask=8602.4000, bid=8599.4000 timestamp=2026-03-12 18:39:01.770 - // symbol=AUSIDXAUD, ask=8601.9000, bid=8598.9000 timestamp=2026-03-12 18:39:01.982 - + // Create auto tradeManager = new TradeManager(); - - - for (const auto& tick : ticks) { - // (void)tick; - - // print first tick - // auto time_t = std::chrono::system_clock::to_time_t(tick.timestamp); - // struct tm tm = {}; - // if (localtime_r(&time_t, &tm) == nullptr) { - // std::cerr << "Error: failed to convert timestamp" << std::endl; - // } - // char buffer[20]; - // std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm); - // auto ms = std::chrono::duration_cast(tick.timestamp.time_since_epoch()) % 1000; - // // printf("symbol=%s, ask=%.4f, bid=%.4f timestamp=%s.%03lld\n", tick.symbol.c_str(), tick.ask, tick.bid, buffer, - // // static_cast(ms.count())); size_t openTrades = tradeManager->reviewAccount(); diff --git a/source/sqlManager.cpp b/source/sqlManager.cpp index 9de55ce..29da1b0 100644 --- a/source/sqlManager.cpp +++ b/source/sqlManager.cpp @@ -5,28 +5,25 @@ // --------------------------------------- #include "sqlManager.hpp" #include +#include #include #include -std::string SqlManager::getBaseQuery(const std::vector& symbols, int LAST_MONTHS) { +std::vector SqlManager::streamPriceData(const DatabaseConnection& db, const std::vector& symbols, int LAST_MONTHS) { if (symbols.empty()) { - return ""; + return {}; } - - std::string query; + + std::ostringstream query; for (size_t i = 0; i < symbols.size(); ++i) { if (i > 0) { - query += " UNION ALL "; + query << " UNION ALL "; } - query += "SELECT '" + symbols[i] + "' as symbol, * FROM '" + symbols[i] + "' WHERE timestamp >= dateadd('M', -" + std::to_string(LAST_MONTHS) + ", now())"; + query << "SELECT '" << symbols[i] << "' as symbol, * FROM '" << symbols[i] + << "' WHERE timestamp >= dateadd('M', -" << LAST_MONTHS << ", now())"; } - query += " ORDER BY timestamp"; - - return query; -} + query << " ORDER BY timestamp"; -std::vector SqlManager::streamPriceData(const DatabaseConnection& db, const std::vector& symbols, int LAST_MONTHS) { - std::string query = getBaseQuery(symbols, LAST_MONTHS); - std::cout << "Executing query: " << query << std::endl; - return db.streamQuery(query); + std::cout << "Executing query: " << query.str() << std::endl; + return db.streamQuery(query.str()); } diff --git a/source/trading/tradeManager.cpp b/source/trading/tradeManager.cpp index 5c82f43..25b8b08 100644 --- a/source/trading/tradeManager.cpp +++ b/source/trading/tradeManager.cpp @@ -5,10 +5,19 @@ // --------------------------------------- #include "tradeManager.hpp" +#include + +namespace { +std::string nextTradeId() { + static std::atomic counter{0}; + return "T" + std::to_string(counter.fetch_add(1, std::memory_order_relaxed)); +} +} std::string TradeManager::openTrade(const PriceData& tick, double size, Direction direction) { double price = (direction == Direction::LONG) ? tick.ask : tick.bid; Trade trade(price, size, direction); + trade.id = nextTradeId(); activeTrades[trade.id] = trade; return trade.id; } diff --git a/tests/tradeManager.mm b/tests/tradeManager.mm index f57b2fc..d719199 100644 --- a/tests/tradeManager.mm +++ b/tests/tradeManager.mm @@ -14,13 +14,12 @@ @interface TradeManagerTests : XCTestCase @implementation TradeManagerTests - (void)setUp { - TradeManager::reset(); // Reset the singleton instance - self.manager = TradeManager::getInstance(); + self.manager = new TradeManager(); } - (void)tearDown { - self.manager->clearAllTrades(); - TradeManager::reset(); + delete self.manager; + self.manager = nullptr; } - (void)testOpenTrade { From 83d838f8cf798dad61aec088b2ab08e61ad455c8 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sat, 2 May 2026 10:52:48 +0100 Subject: [PATCH 20/39] updated workflows and optimised build & test --- .github/workflows/build.yml | 79 ++++++++++ .github/workflows/{ => scripts}/brew.sh | 0 .../workflows/{ => scripts}/cpp_coverage.sh | 0 .../xccov-to-sonarqube-generic.sh | 0 .github/workflows/sonar.yml | 86 +++++++++++ .github/workflows/sonarcloud.yml | 138 ------------------ scripts/build.sh | 4 +- scripts/build_dep.sh | 5 +- scripts/environment.sh | 42 ------ scripts/local_test_coverage.sh | 2 +- scripts/run.sh | 3 +- scripts/test.sh | 25 +++- 12 files changed, 189 insertions(+), 195 deletions(-) create mode 100644 .github/workflows/build.yml rename .github/workflows/{ => scripts}/brew.sh (100%) rename .github/workflows/{ => scripts}/cpp_coverage.sh (100%) rename .github/workflows/{ => scripts}/xccov-to-sonarqube-generic.sh (100%) create mode 100644 .github/workflows/sonar.yml delete mode 100644 .github/workflows/sonarcloud.yml delete mode 100644 scripts/environment.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..114b39a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,79 @@ +on: + # Trigger analysis when pushing in master or pull requests, and when creating + # a pull request. + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] +name: Build + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Release + +jobs: + + build: + name: Build & Test + runs-on: macos-26 + + steps: + # Step 1: Check out the repository code + - name: Checkout repository + uses: actions/checkout@v4 + + # Step 2: Set up Homebrew package manager + - name: Set up Homebrew + id: set-up-homebrew + uses: Homebrew/actions/setup-homebrew@master + + # Step 3: Install required dependencies using Homebrew + - name: Install dependencies + run: bash ./.github/workflows/scripts/brew.sh + + # Step 4: Build project external libraries + - name: Build C++ Libraries + run: bash ./scripts/build_dep.sh + + # Step 5: Configure Xcode version + - name: Select Xcode version + run: | + sudo xcode-select -switch /Applications/Xcode_26.2.app + /usr/bin/xcodebuild -version + # Run XCode tests with specific configurations: + # - Builds and runs the test suite + # - Generates code coverage reports + # - Uses PostgreSQL and libpqxx external dependencies + # - Outputs results in JUnit format + - name: Run tests + run: > + OTHER_CFLAGS="-fprofile-instr-generate -fcoverage-mapping" + OTHER_CPLUSPLUSFLAGS="-fprofile-instr-generate -fcoverage-mapping" + OTHER_SWIFT_FLAGS="-profile-generate -profile-coverage-mapping" + LLVM_PROFILE_FILE="/tmp/coverage.profraw" + CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO + xcodebuild + -scheme tests + -destination 'platform=macOS' + -resultBundlePath TestResult/ + -enableCodeCoverage YES + -derivedDataPath "${RUNNER_TEMP}/Build/DerivedData" + -parallelizeTargets + -jobs "$(sysctl -n hw.logicalcpu)" + HEADER_SEARCH_PATHS="./external/libpqxx/include/pqxx/internal ./external/libpqxx/include/ ./external/libpqxx/build/include/ ./external/" + LIBRARY_SEARCH_PATHS="./external/libpqxx/src/ ./external/libpqxx/build/src/" + OTHER_LDFLAGS="-L./external/libpqxx/build/src -lpqxx -lpq -L$(brew --prefix pkgconf)/lib -L$(brew --prefix pkgconf)/lib/pkgconfig -L$(brew --prefix postgresql@18)/lib/postgresql -L$(brew --prefix postgresql@18)/lib/postgresql/pgxs -L$(brew --prefix postgresql@18)/lib/postgresql/pkgconfig" + clean build test + | xcpretty -r junit && exit ${PIPESTATUS[0]} + - name: Convert coverage report to sonarqube format + run: > + bash ./.github/workflows/scripts/xccov-to-sonarqube-generic.sh *.xcresult/ > sonarqube-generic-coverage.xml + # Artifact is consumed by the SonarCloud Scan workflow, which is triggered + # on completion of this workflow via the workflow_run event. + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: sonarqube-coverage + path: sonarqube-generic-coverage.xml + retention-days: 1 \ No newline at end of file diff --git a/.github/workflows/brew.sh b/.github/workflows/scripts/brew.sh similarity index 100% rename from .github/workflows/brew.sh rename to .github/workflows/scripts/brew.sh diff --git a/.github/workflows/cpp_coverage.sh b/.github/workflows/scripts/cpp_coverage.sh similarity index 100% rename from .github/workflows/cpp_coverage.sh rename to .github/workflows/scripts/cpp_coverage.sh diff --git a/.github/workflows/xccov-to-sonarqube-generic.sh b/.github/workflows/scripts/xccov-to-sonarqube-generic.sh similarity index 100% rename from .github/workflows/xccov-to-sonarqube-generic.sh rename to .github/workflows/scripts/xccov-to-sonarqube-generic.sh diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml new file mode 100644 index 0000000..e655327 --- /dev/null +++ b/.github/workflows/sonar.yml @@ -0,0 +1,86 @@ +on: + # Triggered on completion of the Build workflow so we can consume its + # coverage artifact. workflow_run fires regardless of whether the upstream + # was triggered by push or pull_request. + workflow_run: + workflows: ["Build"] + types: [completed] + +name: SonarCloud Scan + +env: + BUILD_TYPE: Release + BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed + +jobs: + + sonar-scan: + name: SonarCloud Scan + runs-on: ubuntu-latest + # Only run if the upstream Build workflow succeeded. + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + # Check out the same commit the Build workflow ran against. workflow_run + # otherwise defaults to the default branch. + - name: Checkout repository on branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_sha }} + fetch-depth: 0 + - name: Check compiler version, for debugging + run: | + g++ --version + cmake --version + - name: Build C++ Libraries + run: > + sh ./scripts/build.sh + - name: Install Python 3.12 for gcovr + uses: actions/setup-python@v5 + with: + python-version: 3.12 + # Gcovr provides a utility for managing the use of the GNU gcov utility and generating + # summarized code coverage results. This command is inspired by the Python coverage.py + # package, which provides a similar utility for Python. + # https://pypi.org/project/gcovr/ + - name: Install gcovr + run: | + pip install gcovr==8.3 + # SonarQube Server and Cloud (formerly SonarQube and SonarCloud) is a widely used static + # analysis solution for continuous code quality and security inspection. + # This action now supports and is the official entrypoint for scanning C++ projects via GitHub actions. + # https://github.com/SonarSource/sonarqube-scan-action + - name: Install Build Wrapper + uses: SonarSource/sonarqube-scan-action/install-build-wrapper@v4.2.1 + # This step installs the SonarQube build wrapper, which is necessary for analyzing C/C++ projects. + + # Cross-workflow artifact download. v4 requires run-id and github-token + # when fetching from a different workflow run. The artifact lands at + # ./artifact/sonarqube-generic-coverage.xml so the existing + # sonar.coverageReportPaths argument keeps working unchanged. + - name: Download coverage artifact from Build workflow + uses: actions/download-artifact@v4 + with: + name: sonarqube-coverage + path: artifact + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + # Configures the CMake build system, specifying the source directory and build directory, and setting the build type + - name: Configure CMake + run: cmake -S ${{github.workspace}} -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + + # Runs the build wrapper to capture build commands and outputs them to the specified directory. Then builds the project using CMake + - name: Run build-wrapper + run: | + build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --clean-first + + # Performs the SonarQube scan using the scan action. Uses captured build commands for analysis and requires GitHub and SonarQube tokens for authentication + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@v4.2.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + args: > + --define sonar.cfamily.compile-commands="${{ env.BUILD_WRAPPER_OUT_DIR }}/compile_commands.json" + --define sonar.coverageReportPaths=artifact/sonarqube-generic-coverage.xml diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml deleted file mode 100644 index 65bfaba..0000000 --- a/.github/workflows/sonarcloud.yml +++ /dev/null @@ -1,138 +0,0 @@ -on: - # Trigger analysis when pushing in master or pull requests, and when creating - # a pull request. - push: - branches: - - main - pull_request: - types: [opened, synchronize, reopened] -name: Build - -env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) - BUILD_TYPE: Release - BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed - -jobs: - - build: - name: Build & Test - runs-on: macos-26 - - steps: - # Step 1: Check out the repository code - - name: Checkout repository - uses: actions/checkout@v4 - - # Step 2: Set up Homebrew package manager - - name: Set up Homebrew - id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@master - - # Step 3: Install required dependencies using Homebrew - - name: Install dependencies - run: bash ./.github/workflows/brew.sh - - # Step 4: Build project external libraries - - name: Build C++ Libraries - run: bash ./scripts/build_dep.sh - # run: bash ./.github/workflows/build.sh - - # Step 5: Configure Xcode version - - name: Select Xcode version - run: | - sudo xcode-select -switch /Applications/Xcode_26.2.app - /usr/bin/xcodebuild -version - # Run XCode tests with specific configurations: - # - Builds and runs the test suite - # - Generates code coverage reports - # - Uses PostgreSQL and libpqxx external dependencies - # - Outputs results in JUnit format - - name: Run tests - run: > - OTHER_CFLAGS="-fprofile-instr-generate -fcoverage-mapping" - OTHER_CPLUSPLUSFLAGS="-fprofile-instr-generate -fcoverage-mapping" - OTHER_SWIFT_FLAGS="-profile-generate -profile-coverage-mapping" - LLVM_PROFILE_FILE="/tmp/coverage.profraw" - CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO - xcodebuild - -scheme tests - -destination 'platform=macOS' - -resultBundlePath TestResult/ - -enableCodeCoverage YES - -derivedDataPath "${RUNNER_TEMP}/Build/DerivedData" - HEADER_SEARCH_PATHS="./external/libpqxx/include/pqxx/internal ./external/libpqxx/include/ ./external/libpqxx/build/include/ ./external/" - LIBRARY_SEARCH_PATHS="./external/libpqxx/src/ ./external/libpqxx/build/src/" - OTHER_LDFLAGS="-L./external/libpqxx/build/src -lpqxx -lpq -L$(brew --prefix pkgconf)/lib -L$(brew --prefix pkgconf)/lib/pkgconfig -L$(brew --prefix postgresql@18)/lib/postgresql -L$(brew --prefix postgresql@18)/lib/postgresql/pgxs -L$(brew --prefix postgresql@18)/lib/postgresql/pkgconfig" - clean build test - | xcpretty -r junit && exit ${PIPESTATUS[0]} - - name: Convert coverage report to sonarqube format - run: > - bash ./.github/workflows/xccov-to-sonarqube-generic.sh *.xcresult/ > sonarqube-generic-coverage.xml - # Artifact will be available only for 1 day, this is because - # it's only used to pass test data to SonarCloud only - - name: Upload coverage report - uses: actions/upload-artifact@v4 - with: - path: sonarqube-generic-coverage.xml - retention-days: 1 # Artifact will be available only for 5 days. - - sonar-scan: - name: SonarCloud Scan - runs-on: ubuntu-latest - needs: build - steps: - - name: Checkout repository on branch - uses: actions/checkout@v4 - with: - ref: ${{ github.HEAD_REF }} - fetch-depth: 0 - - name: Check compiler version, for debugging - run: | - g++ --version - cmake --version - - name: Build C++ Libraries - run: > - sh ./scripts/build.sh - - name: Install Python 3.12 for gcovr - uses: actions/setup-python@v5 - with: - python-version: 3.12 - # Gcovr provides a utility for managing the use of the GNU gcov utility and generating - # summarized code coverage results. This command is inspired by the Python coverage.py - # package, which provides a similar utility for Python. - # https://pypi.org/project/gcovr/ - - name: Install gcovr - run: | - pip install gcovr==8.3 - # SonarQube Server and Cloud (formerly SonarQube and SonarCloud) is a widely used static - # analysis solution for continuous code quality and security inspection. - # This action now supports and is the official entrypoint for scanning C++ projects via GitHub actions. - # https://github.com/SonarSource/sonarqube-scan-action - - name: Install Build Wrapper - uses: SonarSource/sonarqube-scan-action/install-build-wrapper@v4.2.1 - # This step installs the SonarQube build wrapper, which is necessary for analyzing C/C++ projects. - - # Downloads all artifacts generated by previous steps in the workflow - - name: Download all workflow run artifacts - uses: actions/download-artifact@v4 - - # Configures the CMake build system, specifying the source directory and build directory, and setting the build type - - name: Configure CMake - run: cmake -S ${{github.workspace}} -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - - # Runs the build wrapper to capture build commands and outputs them to the specified directory. Then builds the project using CMake - - name: Run build-wrapper - run: | - build-wrapper-linux-x86-64 --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} --clean-first - - # Performs the SonarQube scan using the scan action. Uses captured build commands for analysis and requires GitHub and SonarQube tokens for authentication - - name: SonarQube Scan - uses: SonarSource/sonarqube-scan-action@v4.2.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - with: - args: > - --define sonar.cfamily.compile-commands="${{ env.BUILD_WRAPPER_OUT_DIR }}/compile_commands.json" - --define sonar.coverageReportPaths=artifact/sonarqube-generic-coverage.xml \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh index ab170e4..90e2198 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -25,12 +25,12 @@ fi cmake .. \ -DCMAKE_CXX_STANDARD=20 \ -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_OSX_SYSROOT=$(xcrun --show-sdk-path) \ -DSKIP_BUILD_TEST=ON \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON # Step 4: Compile the project -cmake --build . +JOBS=$(sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4) +cmake --build . --parallel "$JOBS" # Step 5: Navigate back to the root directory cd .. diff --git a/scripts/build_dep.sh b/scripts/build_dep.sh index 79daa4b..610687d 100644 --- a/scripts/build_dep.sh +++ b/scripts/build_dep.sh @@ -16,6 +16,7 @@ fi cmake .. \ -DCMAKE_CXX_STANDARD=20 \ -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_OSX_SYSROOT=$(xcrun --show-sdk-path) \ -DSKIP_BUILD_TEST=ON -make \ No newline at end of file + +JOBS=$(sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4) +make -j"$JOBS" \ No newline at end of file diff --git a/scripts/environment.sh b/scripts/environment.sh deleted file mode 100644 index 26e5cf0..0000000 --- a/scripts/environment.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -# This file sets the environment variables for execution - -# Get the current execution directory -EXEC_DIR="$(pwd)" - -# Initialize a flag for demo mode -USE_DEMO=false - -# Parse command line arguments -for arg in "$@" -do - case $arg in - --demo) - USE_DEMO=true - shift # Remove --demo from processing - ;; - esac -done - -# Determine which environment file to use -if [ "$USE_DEMO" = true ] || [ ! -f "$EXEC_DIR/.env/prod.env" ]; then - echo "Using Demo Environment Variables" - ENV_FILE="$EXEC_DIR/.env/demo.env" -else - echo "Using Production Environment Variables" - ENV_FILE="$EXEC_DIR/.env/prod.env" -fi - -# You can now use $ENV_FILE which contains the full path to the environment file -echo "Full path to env file: $ENV_FILE" - -# Now import the selected .env file -set -a -while IFS= read -r line || [[ -n "$line" ]]; do - # Skip comments and empty lines - [[ $line =~ ^[[:space:]]*#.*$ ]] && continue - [[ -z "$line" ]] && continue - # Export the variable - export "$line" -done < "$ENV_FILE" -set +a \ No newline at end of file diff --git a/scripts/local_test_coverage.sh b/scripts/local_test_coverage.sh index 2dd7eeb..a122e30 100644 --- a/scripts/local_test_coverage.sh +++ b/scripts/local_test_coverage.sh @@ -18,4 +18,4 @@ LIBRARY_SEARCH_PATHS="./external/libpqxx/src/ ./external/libpqxx/build/src/" \ OTHER_LDFLAGS="-L./external/libpqxx/build/src -lpqxx -lpq -L/opt/homebrew/Cellar/pkgconf/2.3.0_1/lib -L/opt/homebrew/Cellar/pkgconf/2.3.0_1/lib/pkgconfig -L/opt/homebrew/Cellar/postgresql@14/14.15/lib/postgresql@14 -L/opt/homebrew/Cellar/postgresql@14/14.15/lib/postgresql@14/pgxs -L/opt/homebrew/Cellar/postgresql@14/14.15/lib/postgresql@14/pkgconfig" \ clean build test - bash ./.github/workflows/xccov-to-sonarqube-generic.sh *.xcresult/ > sonarqube-generic-coverage.xml \ No newline at end of file +bash ./.github/workflows/xccov-to-sonarqube-generic.sh *.xcresult/ > sonarqube-generic-coverage.xml \ No newline at end of file diff --git a/scripts/run.sh b/scripts/run.sh index e1146cd..e6231e4 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -4,7 +4,6 @@ current_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # Build the source code -# source $current_dir/environment.sh - no longer necessary source "$current_dir/clean.sh" if ! source "$current_dir/build.sh"; then echo "Error: Build failed. Aborting." @@ -24,7 +23,7 @@ fi json='{ "RUN_ID": "UNIQUE_IDENTIFER", "SYMBOLS": "EURUSD", - "LAST_MONTHS": 3, + "LAST_MONTHS": 1, "STRATEGY": { "UUID": "", "TRADING_VARIABLES": { diff --git a/scripts/test.sh b/scripts/test.sh index 72b2448..1480ccf 100644 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,12 +1,21 @@ -# #!/bin/bash +#!/bin/bash + +if [[ "$(uname)" != "Darwin" ]]; then + echo "Testing is done via Xcode build — C++ methods are wrapped in Objective-C for inline debugging and testing in Xcode." + exit 0 +fi + +# Pass CLEAN=1 to force a clean build: CLEAN=1 ./scripts/test.sh +CLEAN_ACTION="" +if [[ "${CLEAN:-0}" == "1" ]]; then + CLEAN_ACTION="clean" +fi xcodebuild \ -project backtesting-engine-cpp.xcodeproj \ -scheme tests \ - clean build test - # HEADER_SEARCH_PATHS="./external/libpqxx/include/pqxx/internal ./external/libpqxx/include/ ./external/libpqxx/build/include/ ./external/" \ - # LIBRARY_SEARCH_PATHS="./external/libpqxx/src/ ./external/libpqxx/build/src/" \ - # OTHER_LDFLAGS="-L./external/libpqxx/build/src -lpqxx -lpq -L$(brew --prefix pkgconf)/lib -L$(brew --prefix pkgconf)/lib/pkgconfig -L$(brew --prefix postgresql@18)/lib/postgresql -L$(brew --prefix postgresql@18)/lib/postgresql/pgxs -L$(brew --prefix postgresql@18)/lib/postgresql/pkgconfig" \ - - - \ No newline at end of file + -parallelizeTargets \ + -jobs "$(sysctl -n hw.logicalcpu)" \ + CODE_SIGN_IDENTITY="-" \ + ENABLE_TESTABILITY=YES \ + ${CLEAN_ACTION} build test 2>&1 | xcpretty \ No newline at end of file From e1ddeaa740cd3ce8c138186b3e9b12bb179f4ffd Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sat, 2 May 2026 11:03:12 +0100 Subject: [PATCH 21/39] updated workflow references --- .github/workflows/build.yml | 4 +++- .github/workflows/sonar.yml | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 114b39a..a7cc3da 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,6 +11,8 @@ name: Build env: # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) BUILD_TYPE: Release + # Opt into Node.js 24 for JavaScript actions ahead of the June 2026 default switch. + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: @@ -26,7 +28,7 @@ jobs: # Step 2: Set up Homebrew package manager - name: Set up Homebrew id: set-up-homebrew - uses: Homebrew/actions/setup-homebrew@master + uses: Homebrew/actions/setup-homebrew@main # Step 3: Install required dependencies using Homebrew - name: Install dependencies diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index e655327..799d682 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -11,6 +11,8 @@ name: SonarCloud Scan env: BUILD_TYPE: Release BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed + # Opt into Node.js 24 for JavaScript actions ahead of the June 2026 default switch. + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: From 28ce0186e6f21355402caba6cbe2a1690be44a03 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sat, 2 May 2026 11:19:45 +0100 Subject: [PATCH 22/39] updated workflow references --- .github/workflows/build.yml | 7 ++----- .github/workflows/sonar.yml | 6 +++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a7cc3da..c16f3dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,11 +11,8 @@ name: Build env: # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) BUILD_TYPE: Release - # Opt into Node.js 24 for JavaScript actions ahead of the June 2026 default switch. - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: - build: name: Build & Test runs-on: macos-26 @@ -23,7 +20,7 @@ jobs: steps: # Step 1: Check out the repository code - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 # Step 2: Set up Homebrew package manager - name: Set up Homebrew @@ -74,7 +71,7 @@ jobs: # Artifact is consumed by the SonarCloud Scan workflow, which is triggered # on completion of this workflow via the workflow_run event. - name: Upload coverage report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: sonarqube-coverage path: sonarqube-generic-coverage.xml diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 799d682..ed40ff2 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -25,7 +25,7 @@ jobs: # Check out the same commit the Build workflow ran against. workflow_run # otherwise defaults to the default branch. - name: Checkout repository on branch - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{ github.event.workflow_run.head_sha }} fetch-depth: 0 @@ -37,7 +37,7 @@ jobs: run: > sh ./scripts/build.sh - name: Install Python 3.12 for gcovr - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.12 # Gcovr provides a utility for managing the use of the GNU gcov utility and generating @@ -60,7 +60,7 @@ jobs: # ./artifact/sonarqube-generic-coverage.xml so the existing # sonar.coverageReportPaths argument keeps working unchanged. - name: Download coverage artifact from Build workflow - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: sonarqube-coverage path: artifact From cb840fdb92e031ed6c293121b2591b98efe09ef2 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sat, 2 May 2026 11:24:41 +0100 Subject: [PATCH 23/39] updated workflow references --- .github/workflows/build.yml | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c16f3dd..cb752d9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,16 +1,14 @@ on: - # Trigger analysis when pushing in master or pull requests, and when creating - # a pull request. push: branches: - main pull_request: types: [opened, synchronize, reopened] + name: Build env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) - BUILD_TYPE: Release + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: build: @@ -18,33 +16,22 @@ jobs: runs-on: macos-26 steps: - # Step 1: Check out the repository code - name: Checkout repository uses: actions/checkout@v5 - # Step 2: Set up Homebrew package manager - name: Set up Homebrew id: set-up-homebrew uses: Homebrew/actions/setup-homebrew@main - # Step 3: Install required dependencies using Homebrew - name: Install dependencies run: bash ./.github/workflows/scripts/brew.sh - # Step 4: Build project external libraries - name: Build C++ Libraries run: bash ./scripts/build_dep.sh - # Step 5: Configure Xcode version - name: Select Xcode version - run: | - sudo xcode-select -switch /Applications/Xcode_26.2.app - /usr/bin/xcodebuild -version - # Run XCode tests with specific configurations: - # - Builds and runs the test suite - # - Generates code coverage reports - # - Uses PostgreSQL and libpqxx external dependencies - # - Outputs results in JUnit format + run: sudo xcode-select -switch /Applications/Xcode.app + - name: Run tests run: > OTHER_CFLAGS="-fprofile-instr-generate -fcoverage-mapping" @@ -65,11 +52,11 @@ jobs: OTHER_LDFLAGS="-L./external/libpqxx/build/src -lpqxx -lpq -L$(brew --prefix pkgconf)/lib -L$(brew --prefix pkgconf)/lib/pkgconfig -L$(brew --prefix postgresql@18)/lib/postgresql -L$(brew --prefix postgresql@18)/lib/postgresql/pgxs -L$(brew --prefix postgresql@18)/lib/postgresql/pkgconfig" clean build test | xcpretty -r junit && exit ${PIPESTATUS[0]} + - name: Convert coverage report to sonarqube format run: > bash ./.github/workflows/scripts/xccov-to-sonarqube-generic.sh *.xcresult/ > sonarqube-generic-coverage.xml - # Artifact is consumed by the SonarCloud Scan workflow, which is triggered - # on completion of this workflow via the workflow_run event. + - name: Upload coverage report uses: actions/upload-artifact@v5 with: From 3f52047e7cb277518b31edec8bb618c4fed50baf Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sat, 2 May 2026 11:27:47 +0100 Subject: [PATCH 24/39] updated workflow references --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cb752d9..eb3aa55 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -58,7 +58,7 @@ jobs: bash ./.github/workflows/scripts/xccov-to-sonarqube-generic.sh *.xcresult/ > sonarqube-generic-coverage.xml - name: Upload coverage report - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: sonarqube-coverage path: sonarqube-generic-coverage.xml From 5dfc6054340ef3496326564bb07f3f2ce1639496 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sat, 2 May 2026 11:33:49 +0100 Subject: [PATCH 25/39] update readme with new workflow badges --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 363022d..bc19327 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Feel free to explore, but this code base is usuable at the moment. I'm developing a high-performance C++ backtesting engine designed to analyze financial data and evaluate multiple trading strategies at scale. -[![Build](https://github.com/mccaffers/backtesting-engine-cpp/actions/workflows/sonarcloud.yml/badge.svg)](https://github.com/mccaffers/backtesting-engine-cpp/actions/workflows/sonarcloud.yml) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=mccaffers_backtesting-engine-cpp&metric=bugs)](https://sonarcloud.io/summary/new_code?id=mccaffers_backtesting-engine-cpp) [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=mccaffers_backtesting-engine-cpp&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=mccaffers_backtesting-engine-cpp) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=mccaffers_backtesting-engine-cpp&metric=coverage)](https://sonarcloud.io/summary/new_code?id=mccaffers_backtesting-engine-cpp) +[![SonarCloud Scan](https://github.com/mccaffers/backtesting-engine-cpp/actions/workflows/sonar.yml/badge.svg)](https://github.com/mccaffers/backtesting-engine-cpp/actions/workflows/sonar.yml) [![Build](https://github.com/mccaffers/backtesting-engine-cpp/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/mccaffers/backtesting-engine-cpp/actions/workflows/build.yml) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=mccaffers_backtesting-engine-cpp&metric=bugs)](https://sonarcloud.io/summary/new_code?id=mccaffers_backtesting-engine-cpp) [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=mccaffers_backtesting-engine-cpp&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=mccaffers_backtesting-engine-cpp) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=mccaffers_backtesting-engine-cpp&metric=coverage)](https://sonarcloud.io/summary/new_code?id=mccaffers_backtesting-engine-cpp) I'm extracting results and creating various graphs for trend analyses using SciPy for calculations and Plotly for visualization. From 21dd5f32553f6764b1f0250f7d9ce398c9ef723c Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sat, 2 May 2026 11:37:17 +0100 Subject: [PATCH 26/39] ignoring cpp:S2245 as it's just for testing --- source/operations.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/operations.cpp b/source/operations.cpp index 9da2fe5..18b1ff4 100644 --- a/source/operations.cpp +++ b/source/operations.cpp @@ -30,7 +30,7 @@ void Operations::run(const std::vector& ticks) { } // randomly check account status every 100 ticks - if (openTrades > 0 && (std::rand() % 100) == 0) { + if (openTrades > 0 && (std::rand() % 100) == 0) { // NOSONAR(cpp:S2245) experimentation only, not security-sensitive std::cout << "Reviewing account at tick timestamp: " << tick.timestamp.time_since_epoch().count() << std::endl; std::cout << "Number of open trades: " << openTrades << std::endl; for (const auto& [id, trade] : tradeManager->getActiveTrades()) { @@ -44,7 +44,7 @@ void Operations::run(const std::vector& ticks) { // randomly close trades every 200 ticks - if (openTrades > 0 && (std::rand() % 200) == 0) { + if (openTrades > 0 && (std::rand() % 200) == 0) { // NOSONAR(cpp:S2245) experimentation only, not security-sensitive std::vector idsToClose; for (const auto& [id, trade] : tradeManager->getActiveTrades()) { idsToClose.push_back(id); From a3f77c42bc80c741680382c83890ecfe801ddcaf Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sun, 3 May 2026 10:47:13 +0100 Subject: [PATCH 27/39] Refactoring to add in pip scalling --- .gitmodules | 3 + CMakeLists.txt | 4 + .../project.pbxproj | 24 ++- external/boost-decimal | 1 + include/models/symbolScale.hpp | 156 ++++++++++++++++++ include/models/trade.hpp | 34 ++-- scripts/build_dep.sh | 48 ++++-- source/CMakeLists.txt | 70 -------- source/operations.cpp | 6 +- source/trading/tradeManager.cpp | 9 +- tests/symbolScale.mm | 66 ++++++++ 11 files changed, 315 insertions(+), 106 deletions(-) create mode 160000 external/boost-decimal create mode 100644 include/models/symbolScale.hpp delete mode 100644 source/CMakeLists.txt create mode 100644 tests/symbolScale.mm diff --git a/.gitmodules b/.gitmodules index cbc89a1..3ed4a98 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "external/libpqxx"] path = external/libpqxx url = https://github.com/jtv/libpqxx.git +[submodule "external/boost-decimal"] + path = external/boost-decimal + url = https://github.com/boostorg/decimal diff --git a/CMakeLists.txt b/CMakeLists.txt index ad2c316..c0f074a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,9 +57,13 @@ if(APPLE) set(OpenMP_C_LIB_NAMES "omp") set(OpenMP_CXX_LIB_NAMES "omp") set(OpenMP_omp_LIBRARY /opt/homebrew/opt/libomp/lib/libomp.dylib) + find_package(OpenMP REQUIRED) target_include_directories(BacktestingEngineLib PRIVATE /opt/homebrew/opt/libomp/include) endif() +add_subdirectory(external/boost-decimal) +target_link_libraries(BacktestingEngineLib PUBLIC Boost::decimal) + find_package(OpenMP REQUIRED) target_link_libraries(BacktestingEngineLib PUBLIC pqxx OpenMP::OpenMP_CXX) diff --git a/backtesting-engine-cpp.xcodeproj/project.pbxproj b/backtesting-engine-cpp.xcodeproj/project.pbxproj index 8e066cd..35899cb 100644 --- a/backtesting-engine-cpp.xcodeproj/project.pbxproj +++ b/backtesting-engine-cpp.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 943398272D57E54000287A2D /* jsonParser.mm in Sources */ = {isa = PBXBuildFile; fileRef = 943398262D57E54000287A2D /* jsonParser.mm */; }; 94364CB62D416D8D00F35B55 /* db.mm in Sources */ = {isa = PBXBuildFile; fileRef = 94364CB52D416D8000F35B55 /* db.mm */; }; 944698852D3A545B0070E30F /* libpq.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 94CD8A972D2D34A100041BBA /* libpq.a */; }; + 9464E5F12FA7467200D82BAD /* symbolScale.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9464E5F02FA7467200D82BAD /* symbolScale.mm */; }; 94674B872D533B4000973137 /* tradeManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94674B852D533B4000973137 /* tradeManager.cpp */; }; 94674B882D533B4000973137 /* tradeManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94674B852D533B4000973137 /* tradeManager.cpp */; }; 94674B8A2D533BDA00973137 /* tradeManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 94674B892D533BDA00973137 /* tradeManager.mm */; }; @@ -75,13 +76,14 @@ 944D0DC82C8C3704004DD0FC /* LICENSE.MD */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.MD; sourceTree = ""; }; 944D0DC92C8C3704004DD0FC /* build.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = build.sh; sourceTree = ""; }; 944D0DCA2C8C3704004DD0FC /* clean.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = clean.sh; sourceTree = ""; }; - 944D0DCB2C8C3704004DD0FC /* environment.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = environment.sh; sourceTree = ""; }; 944D0DCC2C8C3704004DD0FC /* run.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = run.sh; sourceTree = ""; }; 944D0DCD2C8C3704004DD0FC /* test.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = test.sh; sourceTree = ""; }; 944D0DCF2C8C3704004DD0FC /* sonar-project.properties */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "sonar-project.properties"; sourceTree = ""; }; 944D0DD02C8C3704004DD0FC /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 944D0DD12C8C3704004DD0FC /* random-indices-sp500-variable.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "random-indices-sp500-variable.svg"; sourceTree = ""; }; 944D0DD32C8C3704004DD0FC /* CMakeLists.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = CMakeLists.txt; sourceTree = ""; }; + 9464E5EF2FA7466900D82BAD /* symbolScale.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = symbolScale.hpp; sourceTree = ""; }; + 9464E5F02FA7467200D82BAD /* symbolScale.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = symbolScale.mm; sourceTree = ""; }; 94674B822D533B1D00973137 /* trade.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = trade.hpp; sourceTree = ""; }; 94674B832D533B2F00973137 /* tradeManager.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = tradeManager.hpp; sourceTree = ""; }; 94674B852D533B4000973137 /* tradeManager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = tradeManager.cpp; sourceTree = ""; }; @@ -1314,6 +1316,7 @@ 942966D72D48E84100532862 /* models */ = { isa = PBXGroup; children = ( + 9464E5EF2FA7466900D82BAD /* symbolScale.hpp */, 94674B822D533B1D00973137 /* trade.hpp */, 942966D82D48E84A00532862 /* priceData.hpp */, ); @@ -1355,7 +1358,6 @@ 94BBA4502D2EA2570010E04D /* arguments */, 944D0DC92C8C3704004DD0FC /* build.sh */, 944D0DCA2C8C3704004DD0FC /* clean.sh */, - 944D0DCB2C8C3704004DD0FC /* environment.sh */, 944D0DCC2C8C3704004DD0FC /* run.sh */, 944D0DCD2C8C3704004DD0FC /* test.sh */, ); @@ -1422,6 +1424,7 @@ 9470B5AD2C8C5B99007D9CC6 /* tests */ = { isa = PBXGroup; children = ( + 9464E5F02FA7467200D82BAD /* symbolScale.mm */, 943398262D57E54000287A2D /* jsonParser.mm */, 94674B892D533BDA00973137 /* tradeManager.mm */, 94364CB52D416D8000F35B55 /* db.mm */, @@ -3648,6 +3651,7 @@ files = ( 94CD8BA12D2E8CE500041BBA /* databaseConnection.cpp in Sources */, 941408AF2D59F93F000ED1F9 /* sqlManager.cpp in Sources */, + 9464E5F12FA7467200D82BAD /* symbolScale.mm in Sources */, 943398242D57E53400287A2D /* jsonParser.cpp in Sources */, 94280BA42D2FC00200F1CF56 /* base64.cpp in Sources */, 94674B8D2D533E7800973137 /* trade.cpp in Sources */, @@ -3789,12 +3793,14 @@ HEADER_SEARCH_PATHS = ( "\"$(SRCROOT)/external/libpqxx/include\"", "\"$(SRCROOT)/external/libpqxx/include/pqxx/internal\"", - "\"$(SRCROOT)/external/", + "\"$(SRCROOT)/external/\"", + "\"$(SRCROOT)/external/boost-decimal/include\"", ); INCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES = ""; LIBRARY_SEARCH_PATHS = ( "\"$(SRCROOT)/external/libpqxx/build/src\"", "/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql", + "\"$(SRCROOT)/external/boost-decimal/build\"", ); MACOSX_DEPLOYMENT_TARGET = 26.0; OTHER_LDFLAGS = ""; @@ -3812,12 +3818,14 @@ HEADER_SEARCH_PATHS = ( "\"$(SRCROOT)/external/libpqxx/include\"", "\"$(SRCROOT)/external/libpqxx/include/pqxx/internal\"", - "\"$(SRCROOT)/external/", + "\"$(SRCROOT)/external/\"", + "\"$(SRCROOT)/external/boost-decimal/include\"", ); INCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES = ""; LIBRARY_SEARCH_PATHS = ( "\"$(SRCROOT)/external/libpqxx/build/src\"", "/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql", + "\"$(SRCROOT)/external/boost-decimal/build\"", ); MACOSX_DEPLOYMENT_TARGET = 26.0; OTHER_LDFLAGS = ""; @@ -3836,11 +3844,13 @@ HEADER_SEARCH_PATHS = ( "\"$(SRCROOT)/external/libpqxx/include\"", "\"$(SRCROOT)/external/libpqxx/include/pqxx/internal\"", - "\"$(SRCROOT)/external/", + "\"$(SRCROOT)/external/\"", + "\"$(SRCROOT)/external/boost-decimal/include\"", ); LIBRARY_SEARCH_PATHS = ( "\"$(SRCROOT)/external/libpqxx/build/src\"", "/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql", + "\"$(SRCROOT)/external/boost-decimal/build\"", ); MACOSX_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; @@ -3859,11 +3869,13 @@ HEADER_SEARCH_PATHS = ( "\"$(SRCROOT)/external/libpqxx/include\"", "\"$(SRCROOT)/external/libpqxx/include/pqxx/internal\"", - "\"$(SRCROOT)/external/", + "\"$(SRCROOT)/external/\"", + "\"$(SRCROOT)/external/boost-decimal/include\"", ); LIBRARY_SEARCH_PATHS = ( "\"$(SRCROOT)/external/libpqxx/build/src\"", "/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql", + "\"$(SRCROOT)/external/boost-decimal/build\"", ); MACOSX_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; diff --git a/external/boost-decimal b/external/boost-decimal new file mode 160000 index 0000000..e995162 --- /dev/null +++ b/external/boost-decimal @@ -0,0 +1 @@ +Subproject commit e9951622837f63cf0020b22834ad96b01293fd07 diff --git a/include/models/symbolScale.hpp b/include/models/symbolScale.hpp new file mode 100644 index 0000000..207dd2f --- /dev/null +++ b/include/models/symbolScale.hpp @@ -0,0 +1,156 @@ +// Backtesting Engine in C++ +// +// (c) 2026 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- +// +// symbolScale.hpp — fast symbol -> pip-scale lookup. +// +// Given a symbol like "EURUSD" or "USDJPY", returns the integer scale +// used to convert price differences into pips (e.g. 10000 for most FX +// pairs, 100 for JPY pairs, 1 for indices/metals). +// +// Design goals (this is on the hot path of the backtester): +// - No heap allocation, no hashing, no string parsing at runtime. +// - The whole table is `constexpr`, so when the caller passes a string +// literal the compiler can fold the lookup down to a single `mov`. +// - Header-only on purpose: every translation unit sees the body of +// `get()` and can inline it. +// +// C# analogy: think of this as a `static readonly Dictionary`, +// except the lookup is resolved at compile time when possible. +#pragma once + +// — std::array is a fixed-size, stack-allocated array +// with an STL-style interface. Like a C# fixed-size buffer +// but type-safe and bounds-checkable. +// — std::string_view is a non-owning view over an existing +// string's characters: a (const char* + length) pair. It +// does NOT allocate or copy. Closest C# equivalent is +// ReadOnlySpan. Use it for parameters that read but +// don't store the string. +#include +#include + +// `namespace` is C++'s scoping construct. `symbol_scale::get(...)` is +// the fully qualified name from outside this namespace. +namespace symbol_scale { + +// A plain "POD" (plain old data) record. No constructors needed — we use +// brace-initialization below. `string_view` is safe to store here because +// the strings it points to are static string literals with program-long +// lifetime. +struct Entry { + std::string_view symbol; + int scale; +}; + +// Three keywords doing three different jobs on this one declaration: +// - `inline` : tells the linker "if you see this defined in multiple +// .cpp files (because it's in a header), that's fine — +// they're all the same thing." Required for variables +// defined in headers since C++17. +// - `constexpr` : the value is computable at compile time. The whole +// table lives in read-only program memory and the +// compiler can use it during constant evaluation. +// - `std::array` : 29 Entry objects laid out contiguously +// in memory — great for CPU cache locality during the +// binary search below. +// +// IMPORTANT: this table MUST stay sorted ascending by symbol. The +// `static_assert` below enforces that at compile time. +inline constexpr std::array kTable{{ + {"AUDNZD", 10000}, + {"AUDUSD", 10000}, + {"AUSIDXAUD", 1}, + {"BRENTCMDUSD", 1}, + {"COPPERCMDUSD", 1}, + {"DEUIDXEUR", 1}, + {"EURAUD", 10000}, + {"EURCHF", 10000}, + {"EURGBP", 10000}, + {"EURJPY", 100}, + {"EURNOK", 10000}, + {"EURUSD", 10000}, + {"FRAIDXEUR", 1}, + {"GBPJPY", 100}, + {"GBPUSD", 10000}, + {"GBRIDXGBP", 1}, + {"HKGIDXHKD", 1}, + {"JPNIDXJPY", 1}, + {"LIGHTCMDUSD", 1}, + {"NZDUSD", 10000}, + {"USA30IDXUSD", 1}, + {"USA500IDXUSD", 1}, + {"USATECHIDXUSD", 1}, + {"USDCAD", 10000}, + {"USDCHF", 10000}, + {"USDJPY", 100}, + {"USDSEK", 10000}, + {"XAGUSD", 1}, + {"XAUUSD", 1}, +}}; + +// Compile-time invariant check. The pattern is an IIFE — Immediately +// Invoked Function Expression. We declare a lambda `[]{ ... }` and +// then invoke it with `()`, all in one expression. This lets us run +// real logic (a loop) inside `static_assert`, which only accepts a +// boolean expression. +// +// If a future maintainer adds an entry in the wrong place, compilation +// fails with the message below — far better than a silently broken +// binary search. +static_assert([] { + for (std::size_t i = 1; i < kTable.size(); ++i) { + if (!(kTable[i - 1].symbol < kTable[i].symbol)) return false; + } + return true; +}(), "symbol_scale::kTable must be sorted ascending by symbol — binary search depends on it"); + +// Sentinel returned for unknown symbols. Multiplying a real price by 0 +// will produce an obviously-wrong result, which fails loudly rather +// than corrupting silently. +inline constexpr int kUnknown = 0; + +// `[[nodiscard]]` : compiler warning if the caller ignores the returned +// value (this function has no other purpose, so +// ignoring the result is almost always a bug). +// `constexpr` : callable at compile time. When the symbol is a +// literal known to the compiler, the entire binary +// search is folded away and the result becomes a +// constant in the generated assembly. +// `noexcept` : promises this function will not throw. Lets the +// compiler skip exception-handling bookkeeping at +// call sites. +// Parameter is `std::string_view` (by value — it's just a pointer + +// length, cheap to copy) so callers can pass `std::string`, string +// literals, or `const char*` without converting or allocating. +[[nodiscard]] constexpr int get(std::string_view symbol) noexcept { + // Standard binary search over the sorted table. + // `lo` and `hi` are the half-open range [lo, hi) of indices still + // in play. Each iteration halves the range, so for 29 entries we + // do at most 5 iterations. + std::size_t lo = 0; + std::size_t hi = kTable.size(); + while (lo < hi) { + // `lo + ((hi - lo) >> 1)` is the overflow-safe way to compute + // the midpoint. `(lo + hi) / 2` would be wrong if the indices + // were near std::size_t's max; not a real risk here, but it's + // the canonical idiom worth learning. `>> 1` is just `/ 2`. + const std::size_t mid = lo + ((hi - lo) >> 1); + const auto& entry = kTable[mid]; + + // Three-way compare on the symbol. `string_view::operator<` + // does a lexicographic comparison (essentially memcmp). + if (entry.symbol < symbol) { + lo = mid + 1; // target is in the upper half + } else if (symbol < entry.symbol) { + hi = mid; // target is in the lower half + } else { + return entry.scale; // exact match + } + } + return kUnknown; +} + +} // namespace symbol_scale diff --git a/include/models/trade.hpp b/include/models/trade.hpp index 6317596..84b323d 100644 --- a/include/models/trade.hpp +++ b/include/models/trade.hpp @@ -6,7 +6,10 @@ #pragma once #include +#include #include +#include "symbolScale.hpp" +#include enum class Direction { LONG, @@ -22,7 +25,7 @@ struct Trade { std::string dealReference; std::string symbol; - double scalingFactor; + int scalingFactor; double stopDistancePips; double limitDistancePips; std::string strategyId; @@ -30,23 +33,32 @@ struct Trade { double closePrice; std::chrono::system_clock::time_point closeTime; - + // Realised profit/loss for this trade in pip-points, populated on close. + // Pip-PnL = price difference * scalingFactor * size (sign flipped for SHORT). + double pnl; + // Default constructor Trade() : entryPrice(0), size(0), direction(Direction::LONG), scalingFactor(0), stopDistancePips(0), limitDistancePips(0), - closePrice(0), + closePrice(0), pnl(0), openTime(std::chrono::system_clock::now()) {} - + // Copy constructor Trade(const Trade& other) = default; - - Trade(double price, double quantity, Direction dir) - : entryPrice(price), - size(quantity), + + // Member initializers run in declaration order, not the order written + // here, so it's safe to derive `scalingFactor` from `tradeSymbol` + // regardless of where these appear in the list. + Trade(double price, double quantity, Direction dir, std::string_view tradeSymbol) + : entryPrice(price), + size(quantity), + openTime(std::chrono::system_clock::now()), direction(dir), - scalingFactor(0), stopDistancePips(0), limitDistancePips(0), - openTime(std::chrono::system_clock::now()) { - + symbol(tradeSymbol), + scalingFactor(symbol_scale::get(tradeSymbol)), + stopDistancePips(0), + limitDistancePips(0), + pnl(0) { } }; diff --git a/scripts/build_dep.sh b/scripts/build_dep.sh index 610687d..b5447c6 100644 --- a/scripts/build_dep.sh +++ b/scripts/build_dep.sh @@ -1,22 +1,44 @@ +#!/bin/bash +# Fail fast: -e exits on any error, -u errors on undefined vars, +# pipefail propagates failures through pipes (e.g. `foo | grep`). +set -euo pipefail + git submodule update --init --recursive -cd ./external/libpqxx +# Resolve paths relative to this script, not the caller's working directory, +# so the script works no matter where it's invoked from. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EXTERNAL_DIR="$SCRIPT_DIR/../external" + +JOBS=$(sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4) -mkdir -p build -cd ./build +# Build a CMake dependency. The body runs in a subshell `( ... )`, +# so the `cd` only affects this build — control returns to the caller +# in its original directory automatically. +build_dep() { + local dep_dir="$1" + ( + cd "$dep_dir" + mkdir -p build + cd build + cmake .. \ + -DCMAKE_CXX_STANDARD=20 \ + -DCMAKE_BUILD_TYPE=Release \ + -DSKIP_BUILD_TEST=ON + cmake --build . --parallel "$JOBS" + ) +} -# Expose paths so CMake finds libpq +# libpqxx needs libpq's headers/pkg-config; expose the Homebrew install. +# Setting these once at the top is fine — boost-decimal ignores them. if command -v brew &>/dev/null; then export PATH="$(brew --prefix libpq)/bin:$PATH" - export PKG_CONFIG_PATH="$(brew --prefix libpq)/lib/pkgconfig:$PKG_CONFIG_PATH" + export PKG_CONFIG_PATH="$(brew --prefix libpq)/lib/pkgconfig:${PKG_CONFIG_PATH:-}" export PostgreSQL_ROOT="$(brew --prefix libpq)" fi -# 1. Generate build files (Passing your CXX flags directly to CMake instead of configure) -cmake .. \ - -DCMAKE_CXX_STANDARD=20 \ - -DCMAKE_BUILD_TYPE=Release \ - -DSKIP_BUILD_TEST=ON - -JOBS=$(sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4) -make -j"$JOBS" \ No newline at end of file +# boost-decimal is header-only (an INTERFACE CMake target) — there's nothing +# to compile. The parent CMakeLists pulls it in via add_subdirectory(), +# which is all a header-only lib needs. We just need the submodule fetched +# above; no build step. +build_dep "$EXTERNAL_DIR/libpqxx" diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt deleted file mode 100644 index ce98442..0000000 --- a/source/CMakeLists.txt +++ /dev/null @@ -1,70 +0,0 @@ -cmake_minimum_required(VERSION 3.30) - -# CMAKE_OSX_SYSROOT is a macOS-specific setting that specifies the SDK path. -# Only query and set it on Apple platforms, where xcrun is expected to exist. -if(APPLE) - execute_process( - COMMAND xcrun --show-sdk-path - OUTPUT_VARIABLE CMAKE_OSX_SYSROOT - OUTPUT_STRIP_TRAILING_WHITESPACE - ) -endif() - -project(BacktestingEngine) - -# Set the C++ standard -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# Configure libpqxx build -set(PQXX_LIBRARIES_INSTALL ON) -set(SKIP_BUILD_TEST ON) -set(SKIP_CONFIGURE_LIBPQXX OFF) - -# Disable warningsfor external libraries -set(PREV_CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS}) -if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -w") -elseif(MSVC) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /w") -endif() - -# Quiet CMAKE output -set(CMAKE_INSTALL_MESSAGE NEVER) -set(CMAKE_MESSAGE_LOG_LEVEL "WARNING") - -# Build libpqxx from source -add_subdirectory(external/libpqxx EXCLUDE_FROM_ALL) - -# Include directories -include_directories( - ${CMAKE_SOURCE_DIR}/include - ${CMAKE_SOURCE_DIR}/include/utilities - ${CMAKE_SOURCE_DIR}/include/models - ${CMAKE_SOURCE_DIR}/include/trading - ${CMAKE_SOURCE_DIR}/include/trading_definitions - ${CMAKE_SOURCE_DIR}/external -) - -# Collect all .cpp files in the src directory -file(GLOB_RECURSE SOURCES "source/*.cpp") - -# Create a library of your project's code -add_library(BacktestingEngineLib STATIC ${SOURCES}) - -# Replace find_package(OpenMP REQUIRED) with this: -if(APPLE) - set(OpenMP_C_FLAGS "-Xclang -fopenmp") - set(OpenMP_CXX_FLAGS "-Xclang -fopenmp") - set(OpenMP_C_LIB_NAMES "omp") - set(OpenMP_CXX_LIB_NAMES "omp") - set(OpenMP_omp_LIBRARY /opt/homebrew/opt/libomp/lib/libomp.dylib) - find_package(OpenMP REQUIRED) - target_include_directories(BacktestingEngineLib PRIVATE /opt/homebrew/opt/libomp/include) -endif() - -target_link_libraries(BacktestingEngineLib PUBLIC pqxx OpenMP::OpenMP_CXX) - -# Main executable -add_executable(BacktestingEngine source/main.cpp) -target_link_libraries(BacktestingEngine BacktestingEngineLib) diff --git a/source/operations.cpp b/source/operations.cpp index 18b1ff4..4eaf016 100644 --- a/source/operations.cpp +++ b/source/operations.cpp @@ -24,11 +24,13 @@ void Operations::run(const std::vector& ticks) { size_t openTrades = tradeManager->reviewAccount(); + // this would be strategy invoke point if (openTrades == 0) { - std::string tradeId = tradeManager->openTrade(tick, 100000, Direction::LONG); + std::string tradeId = tradeManager->openTrade(tick, 1, Direction::LONG); std::cout << "Opened trade: " << tradeId << std::endl; } + // this would be a position manager review point // randomly check account status every 100 ticks if (openTrades > 0 && (std::rand() % 100) == 0) { // NOSONAR(cpp:S2245) experimentation only, not security-sensitive std::cout << "Reviewing account at tick timestamp: " << tick.timestamp.time_since_epoch().count() << std::endl; @@ -42,7 +44,7 @@ void Operations::run(const std::vector& ticks) { } } - + // strategy review point // randomly close trades every 200 ticks if (openTrades > 0 && (std::rand() % 200) == 0) { // NOSONAR(cpp:S2245) experimentation only, not security-sensitive std::vector idsToClose; diff --git a/source/trading/tradeManager.cpp b/source/trading/tradeManager.cpp index 25b8b08..41609fb 100644 --- a/source/trading/tradeManager.cpp +++ b/source/trading/tradeManager.cpp @@ -16,7 +16,7 @@ std::string nextTradeId() { std::string TradeManager::openTrade(const PriceData& tick, double size, Direction direction) { double price = (direction == Direction::LONG) ? tick.ask : tick.bid; - Trade trade(price, size, direction); + Trade trade(price, size, direction, tick.symbol); trade.id = nextTradeId(); activeTrades[trade.id] = trade; return trade.id; @@ -32,6 +32,9 @@ bool TradeManager::closeTrade(const std::string& tradeId, double closePrice) { Trade closed = it->second; closed.closePrice = closePrice; closed.closeTime = std::chrono::system_clock::now(); + double diff = closePrice - closed.entryPrice; + if (closed.direction == Direction::SHORT) diff = -diff; + closed.pnl = diff * closed.scalingFactor * closed.size; closedTrades.push_back(closed); activeTrades.erase(it); return true; @@ -50,9 +53,7 @@ const std::vector& TradeManager::getClosedTrades() const { double TradeManager::calculatePnl() const { double pnl = 0.0; for (const auto& trade : closedTrades) { - double diff = trade.closePrice - trade.entryPrice; - if (trade.direction == Direction::SHORT) diff = -diff; - pnl += diff * trade.size; + pnl += trade.pnl; } return pnl; } diff --git a/tests/symbolScale.mm b/tests/symbolScale.mm new file mode 100644 index 0000000..4e583d1 --- /dev/null +++ b/tests/symbolScale.mm @@ -0,0 +1,66 @@ +// Backtesting Engine in C++ +// +// (c) 2026 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- + +#import +#import +#import "symbolScale.hpp" + +@interface SymbolScaleTests : XCTestCase +@end + +@implementation SymbolScaleTests + +- (void)testFourDigitFxPairs { + XCTAssertEqual(symbol_scale::get("EURUSD"), 10000); + XCTAssertEqual(symbol_scale::get("AUDUSD"), 10000); + XCTAssertEqual(symbol_scale::get("GBPUSD"), 10000); + XCTAssertEqual(symbol_scale::get("USDCAD"), 10000); + XCTAssertEqual(symbol_scale::get("EURNOK"), 10000); +} + +- (void)testJpyPairs { + XCTAssertEqual(symbol_scale::get("USDJPY"), 100); + XCTAssertEqual(symbol_scale::get("GBPJPY"), 100); + XCTAssertEqual(symbol_scale::get("EURJPY"), 100); +} + +- (void)testIndicesAndCommodities { + XCTAssertEqual(symbol_scale::get("USA500IDXUSD"), 1); + XCTAssertEqual(symbol_scale::get("USATECHIDXUSD"), 1); + XCTAssertEqual(symbol_scale::get("XAUUSD"), 1); + XCTAssertEqual(symbol_scale::get("BRENTCMDUSD"), 1); +} + +- (void)testBoundaryEntries { + XCTAssertEqual(symbol_scale::get("AUDNZD"), 10000); + XCTAssertEqual(symbol_scale::get("XAGUSD"), 1); +} + +- (void)testUnknownSymbolReturnsSentinel { + XCTAssertEqual(symbol_scale::get("NOPE"), symbol_scale::kUnknown); + XCTAssertEqual(symbol_scale::get(""), symbol_scale::kUnknown); + XCTAssertEqual(symbol_scale::get("EURUSDX"), symbol_scale::kUnknown); +} + +- (void)testCaseSensitive { + XCTAssertEqual(symbol_scale::get("eurusd"), symbol_scale::kUnknown); +} + +- (void)testAcceptsStdString { + std::string s = "USDCHF"; + XCTAssertEqual(symbol_scale::get(s), 10000); +} + +- (void)testCompileTimeFolding { + constexpr int eurusd = symbol_scale::get("EURUSD"); + constexpr int usdjpy = symbol_scale::get("USDJPY"); + static_assert(eurusd == 10000); + static_assert(usdjpy == 100); + XCTAssertEqual(eurusd, 10000); + XCTAssertEqual(usdjpy, 100); +} + +@end From afeefae606bc4833f4603136b65fd411e92c97e2 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sun, 3 May 2026 10:51:25 +0100 Subject: [PATCH 28/39] updated build to reference boost-decimal --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eb3aa55..04bd431 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: -derivedDataPath "${RUNNER_TEMP}/Build/DerivedData" -parallelizeTargets -jobs "$(sysctl -n hw.logicalcpu)" - HEADER_SEARCH_PATHS="./external/libpqxx/include/pqxx/internal ./external/libpqxx/include/ ./external/libpqxx/build/include/ ./external/" + HEADER_SEARCH_PATHS="./external/libpqxx/include/pqxx/internal ./external/libpqxx/include/ ./external/libpqxx/build/include/ ./external/boost-decimal/include/ ./external/boost-decimal/include/decimal/ ./external/" LIBRARY_SEARCH_PATHS="./external/libpqxx/src/ ./external/libpqxx/build/src/" OTHER_LDFLAGS="-L./external/libpqxx/build/src -lpqxx -lpq -L$(brew --prefix pkgconf)/lib -L$(brew --prefix pkgconf)/lib/pkgconfig -L$(brew --prefix postgresql@18)/lib/postgresql -L$(brew --prefix postgresql@18)/lib/postgresql/pgxs -L$(brew --prefix postgresql@18)/lib/postgresql/pkgconfig" clean build test From 9988f84ff0ca16e59139710e5098f812d2d0c777 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sun, 3 May 2026 16:29:38 +0100 Subject: [PATCH 29/39] refactoring to use decimal --- README.md | 2 +- .../images}/development_active.svg | 0 .../images}/random-indices-sp500-variable.svg | 0 include/models/priceData.hpp | 14 +++++++++----- include/models/trade.hpp | 10 +++++----- include/trading/tradeManager.hpp | 7 ++++--- source/databaseConnection.cpp | 11 +++++++---- source/operations.cpp | 6 +++++- source/trading/tradeManager.cpp | 14 ++++++++------ 9 files changed, 39 insertions(+), 25 deletions(-) rename {images => documents/images}/development_active.svg (100%) rename {images => documents/images}/random-indices-sp500-variable.svg (100%) diff --git a/README.md b/README.md index bc19327..0d36f5f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ I'm developing a high-performance C++ backtesting engine designed to analyze fin I'm extracting results and creating various graphs for trend analyses using SciPy for calculations and Plotly for visualization. -![alt text](images/random-indices-sp500-variable.svg) +![alt text](documents/images/random-indices-sp500-variable.svg) *Read more results on https://mccaffers.com/randomly_trading/* diff --git a/images/development_active.svg b/documents/images/development_active.svg similarity index 100% rename from images/development_active.svg rename to documents/images/development_active.svg diff --git a/images/random-indices-sp500-variable.svg b/documents/images/random-indices-sp500-variable.svg similarity index 100% rename from images/random-indices-sp500-variable.svg rename to documents/images/random-indices-sp500-variable.svg diff --git a/include/models/priceData.hpp b/include/models/priceData.hpp index c57a2ff..38589c8 100644 --- a/include/models/priceData.hpp +++ b/include/models/priceData.hpp @@ -6,16 +6,20 @@ #pragma once #include #include +#include +// decimal64_t: 16 significant base-10 digits, no binary floating-point drift on +// values like 1.23456. Closest C# analogue is System.Decimal (though decimal64_t +// is 64-bit IEEE 754-2008 vs C#'s 128-bit type). struct PriceData { - double ask; - double bid; + boost::decimal::decimal64_t ask; + boost::decimal::decimal64_t bid; std::chrono::system_clock::time_point timestamp; std::string symbol; - // Constructor for easy creation - PriceData(double ask, double bid, const std::chrono::system_clock::time_point& ts, const std::string& symbol) + PriceData(boost::decimal::decimal64_t ask, boost::decimal::decimal64_t bid, + const std::chrono::system_clock::time_point& ts, const std::string& symbol) : ask(ask), bid(bid), timestamp(ts), symbol(symbol) {} - PriceData() : ask(0.0), bid(0.0), timestamp{}, symbol("") {} + PriceData() : ask(0), bid(0), timestamp{}, symbol("") {} }; diff --git a/include/models/trade.hpp b/include/models/trade.hpp index 84b323d..4d4336d 100644 --- a/include/models/trade.hpp +++ b/include/models/trade.hpp @@ -18,8 +18,8 @@ enum class Direction { struct Trade { std::string id; - double entryPrice; - double size; + boost::decimal::decimal64_t entryPrice; + boost::decimal::decimal64_t size; std::chrono::system_clock::time_point openTime; Direction direction; @@ -31,11 +31,11 @@ struct Trade { std::string strategyId; std::string strategyName; - double closePrice; + boost::decimal::decimal64_t closePrice; std::chrono::system_clock::time_point closeTime; // Realised profit/loss for this trade in pip-points, populated on close. // Pip-PnL = price difference * scalingFactor * size (sign flipped for SHORT). - double pnl; + boost::decimal::decimal64_t pnl; // Default constructor Trade() : entryPrice(0), size(0), direction(Direction::LONG), @@ -49,7 +49,7 @@ struct Trade { // Member initializers run in declaration order, not the order written // here, so it's safe to derive `scalingFactor` from `tradeSymbol` // regardless of where these appear in the list. - Trade(double price, double quantity, Direction dir, std::string_view tradeSymbol) + Trade(boost::decimal::decimal64_t price, boost::decimal::decimal64_t quantity, Direction dir, std::string_view tradeSymbol) : entryPrice(price), size(quantity), openTime(std::chrono::system_clock::now()), diff --git a/include/trading/tradeManager.hpp b/include/trading/tradeManager.hpp index 105e14c..960ba81 100644 --- a/include/trading/tradeManager.hpp +++ b/include/trading/tradeManager.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "trade.hpp" #include "priceData.hpp" @@ -18,10 +19,10 @@ class TradeManager { public: TradeManager() = default; - std::string openTrade(const PriceData& tick, double size, Direction direction); + std::string openTrade(const PriceData& tick, boost::decimal::decimal64_t size, Direction direction); size_t reviewAccount() const; - bool closeTrade(const std::string& tradeId, double closePrice); + bool closeTrade(const std::string& tradeId, boost::decimal::decimal64_t closePrice); const std::unordered_map& getActiveTrades() const; const std::vector& getClosedTrades() const; - double calculatePnl() const; + boost::decimal::decimal64_t calculatePnl() const; }; diff --git a/source/databaseConnection.cpp b/source/databaseConnection.cpp index 5f78ed1..e98488c 100644 --- a/source/databaseConnection.cpp +++ b/source/databaseConnection.cpp @@ -10,6 +10,7 @@ #include #include #include +#include static std::chrono::system_clock::time_point fastParseTimestamp(const char* ts) { int year = 0, month = 0, day = 0, hour = 0, min = 0, sec = 0, usec = 0; @@ -58,14 +59,16 @@ std::vector DatabaseConnection::streamQuery(const std::string& query) for (std::size_t i = 0; i < result.size(); ++i) { const auto& row = result[static_cast(i)]; - double ask, bid; + boost::decimal::decimal64_t ask, bid; auto symbol = row[0].view(); auto sv1 = row[1].view(); auto sv2 = row[2].view(); - std::from_chars(sv1.data(), sv1.data() + sv1.size(), ask); - std::from_chars(sv2.data(), sv2.data() + sv2.size(), bid); + // boost::decimal ships its own from_chars overload — std::from_chars + // doesn't know about decimal64_t. + boost::decimal::from_chars(sv1.data(), sv1.data() + sv1.size(), ask); + boost::decimal::from_chars(sv2.data(), sv2.data() + sv2.size(), bid); results[i] = PriceData(ask, bid, fastParseTimestamp(row[3].c_str()), std::string(symbol)); - } + } return results; } diff --git a/source/operations.cpp b/source/operations.cpp index 4eaf016..b35a22a 100644 --- a/source/operations.cpp +++ b/source/operations.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include "tradeManager.hpp" void Operations::run(const std::vector& ticks) { @@ -26,7 +27,10 @@ void Operations::run(const std::vector& ticks) { // this would be strategy invoke point if (openTrades == 0) { - std::string tradeId = tradeManager->openTrade(tick, 1, Direction::LONG); + // decimal64_t's int constructor is `explicit`, so the literal `1` + // can't implicitly convert — unlike C# where `1m` produces a + // decimal directly. Construct it explicitly via braced init. + std::string tradeId = tradeManager->openTrade(tick, boost::decimal::decimal64_t{1}, Direction::LONG); std::cout << "Opened trade: " << tradeId << std::endl; } diff --git a/source/trading/tradeManager.cpp b/source/trading/tradeManager.cpp index 41609fb..9c44689 100644 --- a/source/trading/tradeManager.cpp +++ b/source/trading/tradeManager.cpp @@ -14,8 +14,8 @@ std::string nextTradeId() { } } -std::string TradeManager::openTrade(const PriceData& tick, double size, Direction direction) { - double price = (direction == Direction::LONG) ? tick.ask : tick.bid; +std::string TradeManager::openTrade(const PriceData& tick, boost::decimal::decimal64_t size, Direction direction) { + auto price = (direction == Direction::LONG) ? tick.ask : tick.bid; Trade trade(price, size, direction, tick.symbol); trade.id = nextTradeId(); activeTrades[trade.id] = trade; @@ -26,14 +26,16 @@ size_t TradeManager::reviewAccount() const { return activeTrades.size(); } -bool TradeManager::closeTrade(const std::string& tradeId, double closePrice) { +bool TradeManager::closeTrade(const std::string& tradeId, boost::decimal::decimal64_t closePrice) { auto it = activeTrades.find(tradeId); if (it != activeTrades.end()) { Trade closed = it->second; closed.closePrice = closePrice; closed.closeTime = std::chrono::system_clock::now(); - double diff = closePrice - closed.entryPrice; + auto diff = closePrice - closed.entryPrice; if (closed.direction == Direction::SHORT) diff = -diff; + // scalingFactor stays an int — boost::decimal has overloads for builtin + // integer types, so no conversion is needed there. closed.pnl = diff * closed.scalingFactor * closed.size; closedTrades.push_back(closed); activeTrades.erase(it); @@ -50,8 +52,8 @@ const std::vector& TradeManager::getClosedTrades() const { return closedTrades; } -double TradeManager::calculatePnl() const { - double pnl = 0.0; +boost::decimal::decimal64_t TradeManager::calculatePnl() const { + boost::decimal::decimal64_t pnl{0}; for (const auto& trade : closedTrades) { pnl += trade.pnl; } From 59831502f3a5f246623f4600c4e695b8aa625be3 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Sun, 3 May 2026 16:41:08 +0100 Subject: [PATCH 30/39] Fixing test dependencies --- .../project.pbxproj | 10 ----- tests/tradeManager.mm | 40 +++++++++++-------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/backtesting-engine-cpp.xcodeproj/project.pbxproj b/backtesting-engine-cpp.xcodeproj/project.pbxproj index 35899cb..84dc9c9 100644 --- a/backtesting-engine-cpp.xcodeproj/project.pbxproj +++ b/backtesting-engine-cpp.xcodeproj/project.pbxproj @@ -80,7 +80,6 @@ 944D0DCD2C8C3704004DD0FC /* test.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = test.sh; sourceTree = ""; }; 944D0DCF2C8C3704004DD0FC /* sonar-project.properties */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "sonar-project.properties"; sourceTree = ""; }; 944D0DD02C8C3704004DD0FC /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; - 944D0DD12C8C3704004DD0FC /* random-indices-sp500-variable.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "random-indices-sp500-variable.svg"; sourceTree = ""; }; 944D0DD32C8C3704004DD0FC /* CMakeLists.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = CMakeLists.txt; sourceTree = ""; }; 9464E5EF2FA7466900D82BAD /* symbolScale.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = symbolScale.hpp; sourceTree = ""; }; 9464E5F02FA7467200D82BAD /* symbolScale.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = symbolScale.mm; sourceTree = ""; }; @@ -1327,7 +1326,6 @@ isa = PBXGroup; children = ( 94CD849E2D2D22C900041BBA /* external */, - 944D0DD22C8C3704004DD0FC /* images */, 944D0DCE2C8C3704004DD0FC /* scripts */, 94DE4F772C8C3E7C00FE48FF /* include */, 9470B5A22C8C5AD0007D9CC6 /* source */, @@ -1364,14 +1362,6 @@ path = scripts; sourceTree = ""; }; - 944D0DD22C8C3704004DD0FC /* images */ = { - isa = PBXGroup; - children = ( - 944D0DD12C8C3704004DD0FC /* random-indices-sp500-variable.svg */, - ); - path = images; - sourceTree = ""; - }; 94674B842D533B2F00973137 /* trading */ = { isa = PBXGroup; children = ( diff --git a/tests/tradeManager.mm b/tests/tradeManager.mm index d719199..cf76ad7 100644 --- a/tests/tradeManager.mm +++ b/tests/tradeManager.mm @@ -5,8 +5,14 @@ // --------------------------------------- #import +#import #import "tradeManager.hpp" +// Pulls in the _dd user-defined literal so "1.23"_dd produces a decimal64_t +// directly. decimal64_t has no implicit conversion from double — the closest +// C# analogue is having to write `1.23m` instead of `1.23` for a decimal. +using namespace boost::decimal::literals; + @interface TradeManagerTests : XCTestCase @property (nonatomic) TradeManager* manager; @end @@ -23,40 +29,40 @@ - (void)tearDown { } - (void)testOpenTrade { - PriceData tick(100.0, 99.0, std::chrono::system_clock::now(), "EURUSD"); - std::string tradeId = self.manager->openTrade(tick, 1.0, Direction::LONG); + PriceData tick("100.0"_dd, "99.0"_dd, std::chrono::system_clock::now(), "EURUSD"); + std::string tradeId = self.manager->openTrade(tick, "1.0"_dd, Direction::LONG); XCTAssertFalse(tradeId.empty(), "Trade ID should not be empty"); XCTAssertEqual(self.manager->reviewAccount(), 1, "Should have 1 active trade"); } - (void)testCloseTrade { - PriceData tick(100.0, 99.0, std::chrono::system_clock::now(), "EURUSD"); - std::string tradeId = self.manager->openTrade(tick, 1.0, Direction::LONG); - bool closed = self.manager->closeTrade(tradeId, 110.0); + PriceData tick("100.0"_dd, "99.0"_dd, std::chrono::system_clock::now(), "EURUSD"); + std::string tradeId = self.manager->openTrade(tick, "1.0"_dd, Direction::LONG); + bool closed = self.manager->closeTrade(tradeId, "110.0"_dd); XCTAssertTrue(closed, "Trade should be closed successfully"); XCTAssertEqual(self.manager->reviewAccount(), 0, "Should have 0 active trades"); } - (void)testMultipleTrades { - PriceData tick1(100.0, 99.0, std::chrono::system_clock::now(), "EURUSD"); - PriceData tick2(200.0, 199.0, std::chrono::system_clock::now(), "EURUSD"); - PriceData tick3(300.0, 299.0, std::chrono::system_clock::now(), "EURUSD"); - self.manager->openTrade(tick1, 1.0, Direction::LONG); - self.manager->openTrade(tick2, 2.0, Direction::SHORT); - self.manager->openTrade(tick3, 3.0, Direction::LONG); - + PriceData tick1("100.0"_dd, "99.0"_dd, std::chrono::system_clock::now(), "EURUSD"); + PriceData tick2("200.0"_dd, "199.0"_dd, std::chrono::system_clock::now(), "EURUSD"); + PriceData tick3("300.0"_dd, "299.0"_dd, std::chrono::system_clock::now(), "EURUSD"); + self.manager->openTrade(tick1, "1.0"_dd, Direction::LONG); + self.manager->openTrade(tick2, "2.0"_dd, Direction::SHORT); + self.manager->openTrade(tick3, "3.0"_dd, Direction::LONG); + XCTAssertEqual(self.manager->reviewAccount(), 3, "Should have 3 active trades"); } - (void)testTradeDetails { - PriceData tick(100.0, 99.0, std::chrono::system_clock::now(), "EURUSD"); - std::string tradeId = self.manager->openTrade(tick, 1.0, Direction::LONG); + PriceData tick("100.0"_dd, "99.0"_dd, std::chrono::system_clock::now(), "EURUSD"); + std::string tradeId = self.manager->openTrade(tick, "1.0"_dd, Direction::LONG); auto trades = self.manager->getActiveTrades(); auto trade = trades.find(tradeId); - + XCTAssertNotEqual(trade, trades.end(), "Trade should exist"); - XCTAssertEqual(trade->second.entryPrice, 100.0, "Entry price should match"); - XCTAssertEqual(trade->second.size, 1.0, "Size should match"); + XCTAssertEqual(trade->second.entryPrice, "100.0"_dd, "Entry price should match"); + XCTAssertEqual(trade->second.size, "1.0"_dd, "Size should match"); XCTAssertTrue(trade->second.direction == Direction::LONG, "Trade should be long"); } From 4590430cd3af37ffb42c9a8fb5d0a76658114280 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Mon, 4 May 2026 10:04:17 +0100 Subject: [PATCH 31/39] Added decimal support for json strings --- include/operations.hpp | 6 ++- .../trading_definitions/trading_variables.hpp | 8 +-- include/utilities/decimal_json.hpp | 51 +++++++++++++++++++ scripts/run.sh | 10 ++-- source/main.cpp | 2 +- source/operations.cpp | 8 ++- 6 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 include/utilities/decimal_json.hpp diff --git a/include/operations.hpp b/include/operations.hpp index cdb7cec..0a65f1d 100644 --- a/include/operations.hpp +++ b/include/operations.hpp @@ -7,9 +7,11 @@ #pragma once #include #include "models/priceData.hpp" +#include "trading_definitions/configuration.hpp" class Operations { - + public: - static void run(const std::vector& priceData); + static void run(const std::vector& priceData, + const trading_definitions::Configuration& config); }; diff --git a/include/trading_definitions/trading_variables.hpp b/include/trading_definitions/trading_variables.hpp index 08b6dd7..5a9fa8c 100644 --- a/include/trading_definitions/trading_variables.hpp +++ b/include/trading_definitions/trading_variables.hpp @@ -6,14 +6,16 @@ #pragma once #include +#include #include +#include "utilities/decimal_json.hpp" namespace trading_definitions { struct TradingVariables { std::string STRATEGY; - double STOP_DISTANCE_IN_PIPS; - double LIMIT_DISTANCE_IN_PIPS; - double TRADING_SIZE; + boost::decimal::decimal64_t STOP_DISTANCE_IN_PIPS; + boost::decimal::decimal64_t LIMIT_DISTANCE_IN_PIPS; + boost::decimal::decimal64_t TRADING_SIZE; }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(TradingVariables, STRATEGY, diff --git a/include/utilities/decimal_json.hpp b/include/utilities/decimal_json.hpp new file mode 100644 index 0000000..2c56de3 --- /dev/null +++ b/include/utilities/decimal_json.hpp @@ -0,0 +1,51 @@ +// 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 + +// Bridge boost::decimal::decimal64_t into nlohmann::json. Without this, +// NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE on structs containing decimal64_t fields +// fails to compile because the library can't find a (de)serializer for the type. +// +// Values move through JSON as strings (e.g. "0.0001") and are parsed with +// boost::decimal::from_chars. Going via a string skips the binary-float +// detour, so a literal like 0.1 in the source JSON survives the round-trip +// without snapping to the nearest IEEE-754 double. +// +// nlohmann's recommended way to add support for a third-party type is to +// specialize adl_serializer rather than injecting free functions into someone +// else's namespace. The C# analogue would be writing a custom JsonConverter +// and registering it on the serializer options. +namespace nlohmann { +template <> +struct adl_serializer { + static void to_json(json& j, const boost::decimal::decimal64_t& value) { + // 64 chars is comfortably above the longest possible decimal64_t + // representation (16 significant digits + sign + point + exponent). + char buffer[64]; + const auto result = boost::decimal::to_chars(buffer, buffer + sizeof(buffer), value); + if (result.ec != std::errc{}) { + throw std::runtime_error("decimal_json: to_chars failed serializing decimal64_t"); + } + j = std::string(buffer, result.ptr); + } + + static void from_json(const json& j, boost::decimal::decimal64_t& value) { + const auto& str = j.get_ref(); + const auto result = boost::decimal::from_chars(str.data(), str.data() + str.size(), value); + if (result.ec != std::errc{}) { + throw std::runtime_error("decimal_json: from_chars failed parsing '" + str + "'"); + } + } +}; +} diff --git a/scripts/run.sh b/scripts/run.sh index e6231e4..70fe50b 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -21,16 +21,16 @@ fi # "SYMBOLS": "EURUSD,AUSIDXAUD", json='{ - "RUN_ID": "UNIQUE_IDENTIFER", + "RUN_ID": "UNIQUE_IDENTIFIER", "SYMBOLS": "EURUSD", "LAST_MONTHS": 1, "STRATEGY": { "UUID": "", "TRADING_VARIABLES": { "STRATEGY": "OHLC_RSI", - "STOP_DISTANCE_IN_PIPS": 1, - "LIMIT_DISTANCE_IN_PIPS": 1, - "TRADING_SIZE": 1 + "STOP_DISTANCE_IN_PIPS": "1.5", + "LIMIT_DISTANCE_IN_PIPS": "1.5", + "TRADING_SIZE": "0.01" }, "OHLC_VARIABLES": [ { @@ -38,7 +38,7 @@ json='{ "OHLC_MINUTES": 100 } ], - "STRATEGY_VARIABLES" : { + "STRATEGY_VARIABLES": { "OHLC_RSI_VARIABLES": { "RSI_LONG": 60, "RSI_SHORT": 40 diff --git a/source/main.cpp b/source/main.cpp index 782e0f9..2062969 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -55,7 +55,7 @@ int main(int argc, const char * argv[]) { printf("Total ticks streamed: %zu\n", ticks.size()); // Execute the backtest by replaying all ticks through the strategy logic - Operations::run(ticks); + Operations::run(ticks, config); return 0; diff --git a/source/operations.cpp b/source/operations.cpp index b35a22a..0a9b8ef 100644 --- a/source/operations.cpp +++ b/source/operations.cpp @@ -16,7 +16,8 @@ #include #include "tradeManager.hpp" -void Operations::run(const std::vector& ticks) { +void Operations::run(const std::vector& ticks, + const trading_definitions::Configuration& config) { // Create auto tradeManager = new TradeManager(); @@ -27,10 +28,7 @@ void Operations::run(const std::vector& ticks) { // this would be strategy invoke point if (openTrades == 0) { - // decimal64_t's int constructor is `explicit`, so the literal `1` - // can't implicitly convert — unlike C# where `1m` produces a - // decimal directly. Construct it explicitly via braced init. - std::string tradeId = tradeManager->openTrade(tick, boost::decimal::decimal64_t{1}, Direction::LONG); + std::string tradeId = tradeManager->openTrade(tick, config.STRATEGY.TRADING_VARIABLES.TRADING_SIZE, Direction::LONG); std::cout << "Opened trade: " << tradeId << std::endl; } From 6543044acab40c72e3061317815b102ab2abb505 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Mon, 4 May 2026 10:23:35 +0100 Subject: [PATCH 32/39] update the build script --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 04bd431..7569a83 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,6 @@ jobs: -derivedDataPath "${RUNNER_TEMP}/Build/DerivedData" -parallelizeTargets -jobs "$(sysctl -n hw.logicalcpu)" - HEADER_SEARCH_PATHS="./external/libpqxx/include/pqxx/internal ./external/libpqxx/include/ ./external/libpqxx/build/include/ ./external/boost-decimal/include/ ./external/boost-decimal/include/decimal/ ./external/" LIBRARY_SEARCH_PATHS="./external/libpqxx/src/ ./external/libpqxx/build/src/" OTHER_LDFLAGS="-L./external/libpqxx/build/src -lpqxx -lpq -L$(brew --prefix pkgconf)/lib -L$(brew --prefix pkgconf)/lib/pkgconfig -L$(brew --prefix postgresql@18)/lib/postgresql -L$(brew --prefix postgresql@18)/lib/postgresql/pgxs -L$(brew --prefix postgresql@18)/lib/postgresql/pkgconfig" clean build test From c94a2eec9f250e7ff7a064edcff8ece7e4ece421 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Mon, 4 May 2026 10:44:43 +0100 Subject: [PATCH 33/39] Updating workflows --- .github/workflows/build.yml | 2 -- .../project.pbxproj | 30 +++++++++---------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7569a83..15774a6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,8 +47,6 @@ jobs: -derivedDataPath "${RUNNER_TEMP}/Build/DerivedData" -parallelizeTargets -jobs "$(sysctl -n hw.logicalcpu)" - LIBRARY_SEARCH_PATHS="./external/libpqxx/src/ ./external/libpqxx/build/src/" - OTHER_LDFLAGS="-L./external/libpqxx/build/src -lpqxx -lpq -L$(brew --prefix pkgconf)/lib -L$(brew --prefix pkgconf)/lib/pkgconfig -L$(brew --prefix postgresql@18)/lib/postgresql -L$(brew --prefix postgresql@18)/lib/postgresql/pgxs -L$(brew --prefix postgresql@18)/lib/postgresql/pkgconfig" clean build test | xcpretty -r junit && exit ${PIPESTATUS[0]} diff --git a/backtesting-engine-cpp.xcodeproj/project.pbxproj b/backtesting-engine-cpp.xcodeproj/project.pbxproj index 76be030..fb3d371 100644 --- a/backtesting-engine-cpp.xcodeproj/project.pbxproj +++ b/backtesting-engine-cpp.xcodeproj/project.pbxproj @@ -21,7 +21,6 @@ 943398252D57E53400287A2D /* jsonParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 943398232D57E53400287A2D /* jsonParser.cpp */; }; 943398272D57E54000287A2D /* jsonParser.mm in Sources */ = {isa = PBXBuildFile; fileRef = 943398262D57E54000287A2D /* jsonParser.mm */; }; 94364CB62D416D8D00F35B55 /* db.mm in Sources */ = {isa = PBXBuildFile; fileRef = 94364CB52D416D8000F35B55 /* db.mm */; }; - 944698852D3A545B0070E30F /* libpq.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 94CD8A972D2D34A100041BBA /* libpq.a */; }; 9464E5F12FA7467200D82BAD /* symbolScale.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9464E5F02FA7467200D82BAD /* symbolScale.mm */; }; 94674B872D533B4000973137 /* tradeManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94674B852D533B4000973137 /* tradeManager.cpp */; }; 94674B882D533B4000973137 /* tradeManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94674B852D533B4000973137 /* tradeManager.cpp */; }; @@ -32,11 +31,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 */; }; - 94CD8B992D2DCDD800041BBA /* libpqxx-7.10.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 94CD8B982D2DCDD800041BBA /* libpqxx-7.10.a */; }; - 94CD8B9C2D2DD02A00041BBA /* libpqxx-7.10.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 94CD8B9A2D2DCF6E00041BBA /* libpqxx-7.10.a */; }; 94CD8BA02D2E8CE500041BBA /* databaseConnection.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94CD8B9F2D2E8CE500041BBA /* databaseConnection.cpp */; }; 94CD8BA12D2E8CE500041BBA /* databaseConnection.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94CD8B9F2D2E8CE500041BBA /* databaseConnection.cpp */; }; - 94CD8BA22D2E8FC600041BBA /* libpq.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 94CD8A972D2D34A100041BBA /* libpq.a */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -1265,8 +1261,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 94CD8BA22D2E8FC600041BBA /* libpq.a in Frameworks */, - 94CD8B992D2DCDD800041BBA /* libpqxx-7.10.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1274,8 +1268,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 944698852D3A545B0070E30F /* libpq.a in Frameworks */, - 94CD8B9C2D2DD02A00041BBA /* libpqxx-7.10.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3782,11 +3774,14 @@ INCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES = ""; LIBRARY_SEARCH_PATHS = ( "\"$(SRCROOT)/external/libpqxx/build/src\"", - "/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql", + "/opt/homebrew/opt/postgresql@18/lib/postgresql", "\"$(SRCROOT)/external/boost-decimal/build\"", ); MACOSX_DEPLOYMENT_TARGET = 26.0; - OTHER_LDFLAGS = ""; + OTHER_LDFLAGS = ( + "-lpq", + "-lpqxx", + ); OTHER_LIBTOOLFLAGS = ""; PRODUCT_NAME = "$(TARGET_NAME)"; SECTORDER_FLAGS = ""; @@ -3808,11 +3803,14 @@ INCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES = ""; LIBRARY_SEARCH_PATHS = ( "\"$(SRCROOT)/external/libpqxx/build/src\"", - "/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql", + "/opt/homebrew/opt/postgresql@18/lib/postgresql", "\"$(SRCROOT)/external/boost-decimal/build\"", ); MACOSX_DEPLOYMENT_TARGET = 26.0; - OTHER_LDFLAGS = ""; + OTHER_LDFLAGS = ( + "-lpq", + "-lpqxx", + ); OTHER_LIBTOOLFLAGS = ""; PRODUCT_NAME = "$(TARGET_NAME)"; SECTORDER_FLAGS = ""; @@ -3827,14 +3825,14 @@ GENERATE_INFOPLIST_FILE = YES; HEADER_SEARCH_PATHS = ( "\"$(SRCROOT)/include\"", + "\"$(SRCROOT)/external/\"", "\"$(SRCROOT)/external/libpqxx/include\"", "\"$(SRCROOT)/external/libpqxx/include/pqxx/internal\"", - "\"$(SRCROOT)/external/\"", "\"$(SRCROOT)/external/boost-decimal/include\"", ); LIBRARY_SEARCH_PATHS = ( "\"$(SRCROOT)/external/libpqxx/build/src\"", - "/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql", + "/opt/homebrew/opt/postgresql@18/lib/postgresql", "\"$(SRCROOT)/external/boost-decimal/build\"", ); MACOSX_DEPLOYMENT_TARGET = 26.0; @@ -3853,14 +3851,14 @@ GENERATE_INFOPLIST_FILE = YES; HEADER_SEARCH_PATHS = ( "\"$(SRCROOT)/include\"", + "\"$(SRCROOT)/external/\"", "\"$(SRCROOT)/external/libpqxx/include\"", "\"$(SRCROOT)/external/libpqxx/include/pqxx/internal\"", - "\"$(SRCROOT)/external/\"", "\"$(SRCROOT)/external/boost-decimal/include\"", ); LIBRARY_SEARCH_PATHS = ( "\"$(SRCROOT)/external/libpqxx/build/src\"", - "/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql", + "/opt/homebrew/opt/postgresql@18/lib/postgresql", "\"$(SRCROOT)/external/boost-decimal/build\"", ); MACOSX_DEPLOYMENT_TARGET = 26.0; From 6491098eca138d2ec7f419414fe83058d3de2355 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Mon, 4 May 2026 10:48:21 +0100 Subject: [PATCH 34/39] Updating workflows --- backtesting-engine-cpp.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backtesting-engine-cpp.xcodeproj/project.pbxproj b/backtesting-engine-cpp.xcodeproj/project.pbxproj index fb3d371..c4d96a1 100644 --- a/backtesting-engine-cpp.xcodeproj/project.pbxproj +++ b/backtesting-engine-cpp.xcodeproj/project.pbxproj @@ -3768,6 +3768,7 @@ "\"$(SRCROOT)/include\"", "\"$(SRCROOT)/external/libpqxx/include\"", "\"$(SRCROOT)/external/libpqxx/include/pqxx/internal\"", + "\"$(SRCROOT)/external/libpqxx/build/include\"", "\"$(SRCROOT)/external/\"", "\"$(SRCROOT)/external/boost-decimal/include\"", ); @@ -3797,6 +3798,7 @@ "\"$(SRCROOT)/include\"", "\"$(SRCROOT)/external/libpqxx/include\"", "\"$(SRCROOT)/external/libpqxx/include/pqxx/internal\"", + "\"$(SRCROOT)/external/libpqxx/build/include\"", "\"$(SRCROOT)/external/\"", "\"$(SRCROOT)/external/boost-decimal/include\"", ); @@ -3828,6 +3830,7 @@ "\"$(SRCROOT)/external/\"", "\"$(SRCROOT)/external/libpqxx/include\"", "\"$(SRCROOT)/external/libpqxx/include/pqxx/internal\"", + "\"$(SRCROOT)/external/libpqxx/build/include\"", "\"$(SRCROOT)/external/boost-decimal/include\"", ); LIBRARY_SEARCH_PATHS = ( @@ -3854,6 +3857,7 @@ "\"$(SRCROOT)/external/\"", "\"$(SRCROOT)/external/libpqxx/include\"", "\"$(SRCROOT)/external/libpqxx/include/pqxx/internal\"", + "\"$(SRCROOT)/external/libpqxx/build/include\"", "\"$(SRCROOT)/external/boost-decimal/include\"", ); LIBRARY_SEARCH_PATHS = ( From 724754b63d5f2463e802f9436b4b267531b6ff77 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Mon, 4 May 2026 10:53:52 +0100 Subject: [PATCH 35/39] updating test references --- 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 c4d96a1..28a6cfe 100644 --- a/backtesting-engine-cpp.xcodeproj/project.pbxproj +++ b/backtesting-engine-cpp.xcodeproj/project.pbxproj @@ -3840,6 +3840,10 @@ ); MACOSX_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "-lpq", + "-lpqxx", + ); PRODUCT_BUNDLE_IDENTIFIER = com.mccaffers.tests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -3867,6 +3871,10 @@ ); MACOSX_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "-lpq", + "-lpqxx", + ); PRODUCT_BUNDLE_IDENTIFIER = com.mccaffers.tests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; From 4511b80999b9c22c4b8ea9056088cd260536581d Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Mon, 4 May 2026 12:37:00 +0100 Subject: [PATCH 36/39] Updating references --- .github/workflows/scripts/brew.sh | 2 +- backtesting-engine-cpp.xcodeproj/project.pbxproj | 2 -- source/main.cpp | 6 +++++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/scripts/brew.sh b/.github/workflows/scripts/brew.sh index 2c7376f..6e60614 100644 --- a/.github/workflows/scripts/brew.sh +++ b/.github/workflows/scripts/brew.sh @@ -11,5 +11,5 @@ check_and_install() { } # Install packages if they don't exist -check_and_install postgresql # Check and install PostgreSQL (which includes libpq) +check_and_install postgresql@18 # Check and install PostgreSQL (which includes libpq) check_and_install pkg-config # Check and install pkg-config \ No newline at end of file diff --git a/backtesting-engine-cpp.xcodeproj/project.pbxproj b/backtesting-engine-cpp.xcodeproj/project.pbxproj index 28a6cfe..7b96798 100644 --- a/backtesting-engine-cpp.xcodeproj/project.pbxproj +++ b/backtesting-engine-cpp.xcodeproj/project.pbxproj @@ -3776,7 +3776,6 @@ LIBRARY_SEARCH_PATHS = ( "\"$(SRCROOT)/external/libpqxx/build/src\"", "/opt/homebrew/opt/postgresql@18/lib/postgresql", - "\"$(SRCROOT)/external/boost-decimal/build\"", ); MACOSX_DEPLOYMENT_TARGET = 26.0; OTHER_LDFLAGS = ( @@ -3806,7 +3805,6 @@ LIBRARY_SEARCH_PATHS = ( "\"$(SRCROOT)/external/libpqxx/build/src\"", "/opt/homebrew/opt/postgresql@18/lib/postgresql", - "\"$(SRCROOT)/external/boost-decimal/build\"", ); MACOSX_DEPLOYMENT_TARGET = 26.0; OTHER_LDFLAGS = ( diff --git a/source/main.cpp b/source/main.cpp index 2062969..e1277dc 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -20,7 +20,7 @@ #include "serviceA.hpp" #include "databaseConnection.hpp" #include "base64.hpp" -#include "trading_definitions.hpp" // For everything +#include "trading_definitions.hpp" #include "tradeManager.hpp" #include "jsonParser.hpp" #include "sqlManager.hpp" @@ -51,7 +51,11 @@ int main(int argc, const char * argv[]) { for (std::string token; std::getline(ss, token, ',');) { symbols.push_back(token); } + + // Stream all the tick data into a vector std::vector ticks = SqlManager::streamPriceData(db, symbols, config.LAST_MONTHS); + + // TODO add a condition, if tick.size == zero printf("Total ticks streamed: %zu\n", ticks.size()); // Execute the backtest by replaying all ticks through the strategy logic From c4c702f1814c16d47aceda8764c9de731baf93a5 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Mon, 4 May 2026 13:43:19 +0100 Subject: [PATCH 37/39] fixing workflow, referenced build folder that didn't exist --- .github/workflows/build.yml | 2 +- backtesting-engine-cpp.xcodeproj/project.pbxproj | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 15774a6..77be3d6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,7 +41,7 @@ jobs: CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO xcodebuild -scheme tests - -destination 'platform=macOS' + -destination 'platform=macOS,arch=arm64' -resultBundlePath TestResult/ -enableCodeCoverage YES -derivedDataPath "${RUNNER_TEMP}/Build/DerivedData" diff --git a/backtesting-engine-cpp.xcodeproj/project.pbxproj b/backtesting-engine-cpp.xcodeproj/project.pbxproj index 7b96798..fbdaccb 100644 --- a/backtesting-engine-cpp.xcodeproj/project.pbxproj +++ b/backtesting-engine-cpp.xcodeproj/project.pbxproj @@ -3834,7 +3834,6 @@ LIBRARY_SEARCH_PATHS = ( "\"$(SRCROOT)/external/libpqxx/build/src\"", "/opt/homebrew/opt/postgresql@18/lib/postgresql", - "\"$(SRCROOT)/external/boost-decimal/build\"", ); MACOSX_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; @@ -3865,7 +3864,6 @@ LIBRARY_SEARCH_PATHS = ( "\"$(SRCROOT)/external/libpqxx/build/src\"", "/opt/homebrew/opt/postgresql@18/lib/postgresql", - "\"$(SRCROOT)/external/boost-decimal/build\"", ); MACOSX_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; From 6dffe6f9748e49e67977bdf6c3ae2455b6fd4761 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Mon, 4 May 2026 13:52:47 +0100 Subject: [PATCH 38/39] fixing the tests for the json ingest of a strategy --- tests/jsonParser.mm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/jsonParser.mm b/tests/jsonParser.mm index 4829238..d7dadc9 100644 --- a/tests/jsonParser.mm +++ b/tests/jsonParser.mm @@ -31,9 +31,9 @@ - (void)testValidJsonParsing { "UUID": "", "TRADING_VARIABLES": { "STRATEGY": "OHLC_RSI", - "STOP_DISTANCE_IN_PIPS": 1, - "LIMIT_DISTANCE_IN_PIPS": 1, - "TRADING_SIZE": 1 + "STOP_DISTANCE_IN_PIPS": "1", + "LIMIT_DISTANCE_IN_PIPS": "1", + "TRADING_SIZE": "1" }, "OHLC_VARIABLES": [ { From 6a137245c5f3a82b8bd235790afdbbc3a8087774 Mon Sep 17 00:00:00 2001 From: Ryan <16667079+mccaffers@users.noreply.github.com> Date: Mon, 4 May 2026 17:09:08 +0100 Subject: [PATCH 39/39] Updated test coverage script and added auto pr response --- .github/workflows/auto-close-external-prs.yml | 58 +++++++++++++++++++ scripts/local_test_coverage.sh | 3 - 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/auto-close-external-prs.yml diff --git a/.github/workflows/auto-close-external-prs.yml b/.github/workflows/auto-close-external-prs.yml new file mode 100644 index 0000000..42a3926 --- /dev/null +++ b/.github/workflows/auto-close-external-prs.yml @@ -0,0 +1,58 @@ +name: "Auto-Close External PRs" + +on: + pull_request_target: + types: [opened] + workflow_dispatch: + +jobs: + close-on-open: + if: github.event_name == 'pull_request_target' && github.event.pull_request.user.login != github.repository_owner + runs-on: ubuntu-latest + permissions: + pull-requests: write + issues: write + steps: + - uses: actions/github-script@v7 + with: + script: | + const pr = context.issue.number; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr, + body: "Thanks for the interest! This repository isn't currently accepting external contributions as I am actively experimenting with different approaches and want to avoid merge conflicts. Therefore, this PR is being closed automatically by Github Bot." + }); + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr, + state: "closed" + }); + + sweep-existing: + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + pull-requests: write + issues: write + steps: + - uses: actions/github-script@v7 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const prs = await github.paginate(github.rest.pulls.list, { + owner, repo, state: 'open', per_page: 100 + }); + for (const pr of prs) { + if (pr.user.login === owner) continue; + core.info(`Closing PR #${pr.number} by ${pr.user.login}`); + await github.rest.issues.createComment({ + owner, repo, issue_number: pr.number, + body: "Thanks for the interest! This repository isn't currently accepting external contributions as I am actively experimenting with different approaches and want to avoid merge conflicts. Therefore, this PR is being closed automatically by Github Bot." + }); + await github.rest.pulls.update({ + owner, repo, pull_number: pr.number, state: "closed" + }); + } diff --git a/scripts/local_test_coverage.sh b/scripts/local_test_coverage.sh index a122e30..91d2e25 100644 --- a/scripts/local_test_coverage.sh +++ b/scripts/local_test_coverage.sh @@ -13,9 +13,6 @@ xcodebuild \ -resultBundlePath TestResult/ \ -enableCodeCoverage YES \ -derivedDataPath "/tmp" \ -HEADER_SEARCH_PATHS="./external/libpqxx/include/pqxx/internal ./external/libpqxx/include/ ./external/libpqxx/build/include/ ./external/" \ -LIBRARY_SEARCH_PATHS="./external/libpqxx/src/ ./external/libpqxx/build/src/" \ -OTHER_LDFLAGS="-L./external/libpqxx/build/src -lpqxx -lpq -L/opt/homebrew/Cellar/pkgconf/2.3.0_1/lib -L/opt/homebrew/Cellar/pkgconf/2.3.0_1/lib/pkgconfig -L/opt/homebrew/Cellar/postgresql@14/14.15/lib/postgresql@14 -L/opt/homebrew/Cellar/postgresql@14/14.15/lib/postgresql@14/pgxs -L/opt/homebrew/Cellar/postgresql@14/14.15/lib/postgresql@14/pkgconfig" \ clean build test bash ./.github/workflows/xccov-to-sonarqube-generic.sh *.xcresult/ > sonarqube-generic-coverage.xml \ No newline at end of file