diff --git a/.github/workflows/build.sh b/.github/workflows/build.sh deleted file mode 100644 index 822e713..0000000 --- a/.github/workflows/build.sh +++ /dev/null @@ -1,23 +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 - -# 2. Compile libpqxx -make \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..eb3aa55 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,65 @@ +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + +name: Build + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + build: + name: Build & Test + runs-on: macos-26 + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up Homebrew + id: set-up-homebrew + uses: Homebrew/actions/setup-homebrew@main + + - name: Install dependencies + run: bash ./.github/workflows/scripts/brew.sh + + - name: Build C++ Libraries + run: bash ./scripts/build_dep.sh + + - name: Select Xcode version + run: sudo xcode-select -switch /Applications/Xcode.app + + - 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 + + - name: Upload coverage report + uses: actions/upload-artifact@v7 + 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..ed40ff2 --- /dev/null +++ b/.github/workflows/sonar.yml @@ -0,0 +1,88 @@ +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 + # Opt into Node.js 24 for JavaScript actions ahead of the June 2026 default switch. + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +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@v5 + 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@v6 + 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@v5 + 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 6897994..0000000 --- a/.github/workflows/sonarcloud.yml +++ /dev/null @@ -1,137 +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-14 # Use macOS 14 (Sonoma) runner - - 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 ./.github/workflows/build.sh - - # Step 5: Configure Xcode version - - name: Select Xcode version - run: | - sudo xcode-select -switch /Applications/Xcode_15.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: > - bash ./.github/workflows/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}} - - # 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}} - - # 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/.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/CMakeLists.txt b/CMakeLists.txt index b6c5634..ad2c316 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) +# 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) + 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 add_executable(BacktestingEngine source/main.cpp) 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/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..1f81d45 100644 --- a/include/databaseConnection.hpp +++ b/include/databaseConnection.hpp @@ -20,7 +20,7 @@ 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 { return connection_string; 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/include/models/priceData.hpp b/include/models/priceData.hpp index 4047ffd..c57a2ff 100644 --- a/include/models/priceData.hpp +++ b/include/models/priceData.hpp @@ -1,17 +1,21 @@ // 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 #include +#include struct PriceData { - double value1; - double value2; + double ask; + double bid; std::chrono::system_clock::time_point timestamp; + std::string symbol; // 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, const std::string& symbol) + : ask(ask), bid(bid), timestamp(ts), symbol(symbol) {} + + PriceData() : ask(0.0), bid(0.0), timestamp{}, symbol("") {} }; 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/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 1f65287..04ff5a8 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 @@ -11,8 +11,5 @@ class SqlManager { public: - static std::vector getInitialPriceData(const DatabaseConnection& db); - static std::string getBaseQuery(); -private: - static constexpr int DEFAULT_LIMIT = 1000; + static std::vector streamPriceData(const DatabaseConnection& db, const std::vector& symbols, int LAST_MONTHS = 1); }; 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/build.sh b/scripts/build.sh index 3a17995..90e2198 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" @@ -12,11 +14,23 @@ 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 +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 .. \ + -DCMAKE_CXX_STANDARD=20 \ + -DCMAKE_BUILD_TYPE=Release \ + -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 new file mode 100644 index 0000000..610687d --- /dev/null +++ b/scripts/build_dep.sh @@ -0,0 +1,22 @@ +git submodule update --init --recursive + +cd ./external/libpqxx + +mkdir -p build +cd ./build + +# Expose paths so CMake finds 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 .. \ + -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 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 fa3cd8d..e6231e4 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -4,9 +4,11 @@ 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 +source "$current_dir/clean.sh" +if ! source "$current_dir/build.sh"; then + echo "Error: Build failed. Aborting." + exit 1 +fi # Debug: Check if the executable exists if [ -f "$BUILD_DIR/$EXECUTABLE_NAME" ]; then @@ -17,10 +19,11 @@ else exit 1 fi +# "SYMBOLS": "EURUSD,AUSIDXAUD", json='{ "RUN_ID": "UNIQUE_IDENTIFER", "SYMBOLS": "EURUSD", - "LAST_MONTHS": 6, + "LAST_MONTHS": 1, "STRATEGY": { "UUID": "", "TRADING_VARIABLES": { @@ -49,5 +52,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/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 diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt new file mode 100644 index 0000000..ce98442 --- /dev/null +++ b/source/CMakeLists.txt @@ -0,0 +1,70 @@ +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/databaseConnection.cpp b/source/databaseConnection.cpp index 292da31..5f78ed1 100644 --- a/source/databaseConnection.cpp +++ b/source/databaseConnection.cpp @@ -1,12 +1,40 @@ // 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 "databaseConnection.hpp" #include "base64.hpp" #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; + 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] = {}; + 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, @@ -21,37 +49,23 @@ 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); +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); - if (!conn.is_open()) { - throw std::invalid_argument("Failed to open database connection"); - } + std::vector results(result.size()); - 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 value1 = row[0].as(); - double value2 = row[1].as(); - std::string timestamp_str = row[2].as(); - - auto timestamp = Utilities::parseTimestamp(timestamp_str); - - results.emplace_back(value1, value2, timestamp); - } - - txn.commit(); - - } catch (const std::exception& e) { - std::cerr << "Error: " << e.what() << std::endl; - } + for (std::size_t i = 0; i < result.size(); ++i) { + const auto& row = result[static_cast(i)]; + double 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); + results[i] = PriceData(ask, bid, fastParseTimestamp(row[3].c_str()), std::string(symbol)); + } return results; } @@ -70,8 +84,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 9fb01da..782e0f9 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) // --------------------------------------- @@ -10,6 +10,7 @@ #include #include #include +#include // external headers #include @@ -23,36 +24,38 @@ #include "tradeManager.hpp" #include "jsonParser.hpp" #include "sqlManager.hpp" +#include "operations.hpp" 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"); + // Validate required command-line arguments before proceeding + if (argc < 3) { + std::cerr << "Usage: " << argv[0] << " " << std::endl; + return 1; + } - // Load strategy from Base64 argv[2] - JsonParser::parseConfigurationFromBase64(argv[2]); + // Connect to QuestDB on the default port (8812) using default credentials + DatabaseConnection db(argv[1], 8812, "qdb", "admin", "quest"); - std::vector priceData = SqlManager::getInitialPriceData(db); + // Decode and apply the strategy configuration from Base64-encoded JSON + auto config = JsonParser::parseConfigurationFromBase64(argv[2]); - // 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; - - auto tradeManager = TradeManager::getInstance(); - - // Open a trade - 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; - - // Close trade - bool closed = tradeManager->closeTrade(tradeId); - std::cout << "Trade closed: " << (closed ? "yes" : "no") << std::endl; + // 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, config.LAST_MONTHS); + printf("Total ticks streamed: %zu\n", ticks.size()); + + // Execute the backtest by replaying all ticks through the strategy logic + Operations::run(ticks); return 0; 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 new file mode 100644 index 0000000..9da2fe5 --- /dev/null +++ b/source/operations.cpp @@ -0,0 +1,63 @@ +// 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 +#include +#include "tradeManager.hpp" + +void Operations::run(const std::vector& ticks) { + + // Create + auto tradeManager = new TradeManager(); + + for (const auto& tick : ticks) { + + size_t openTrades = tradeManager->reviewAccount(); + + if (openTrades == 0) { + std::string tradeId = tradeManager->openTrade(tick, 100000, Direction::LONG); + std::cout << "Opened trade: " << tradeId << std::endl; + } + + // 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; + } + } + + + // 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; + } + } + } + + 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/sqlManager.cpp b/source/sqlManager.cpp index 12152cd..29da1b0 100644 --- a/source/sqlManager.cpp +++ b/source/sqlManager.cpp @@ -1,14 +1,29 @@ // 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 +#include +#include -std::string SqlManager::getBaseQuery() { - return "SELECT * FROM EURUSD LIMIT " + std::to_string(DEFAULT_LIMIT) + ";"; -} +std::vector SqlManager::streamPriceData(const DatabaseConnection& db, const std::vector& symbols, int LAST_MONTHS) { + if (symbols.empty()) { + return {}; + } + + std::ostringstream 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', -" << LAST_MONTHS << ", now())"; + } + query << " ORDER BY timestamp"; -std::vector SqlManager::getInitialPriceData(const DatabaseConnection& db) { - return db.executeQuery(getBaseQuery()); + 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 d3cb308..25b8b08 100644 --- a/source/trading/tradeManager.cpp +++ b/source/trading/tradeManager.cpp @@ -5,28 +5,19 @@ // --------------------------------------- #include "tradeManager.hpp" +#include -TradeManager* TradeManager::instance = nullptr; - -TradeManager* TradeManager::getInstance() { - if (instance == nullptr) { - instance = new TradeManager(); - } - return instance; -} - -void TradeManager::reset() { - delete instance; - instance = nullptr; - Trade::resetCounter(); +namespace { +std::string nextTradeId() { + static std::atomic counter{0}; + return "T" + std::to_string(counter.fetch_add(1, std::memory_order_relaxed)); } - -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); + trade.id = nextTradeId(); activeTrades[trade.id] = trade; return trade.id; } @@ -35,9 +26,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 +42,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/source/jsonParser.cpp b/source/utilities/jsonParser.cpp similarity index 79% rename from source/jsonParser.cpp rename to source/utilities/jsonParser.cpp index 648ab06..f7affcd 100644 --- a/source/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) // --------------------------------------- @@ -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"); } diff --git a/tests/tradeManager.mm b/tests/tradeManager.mm index 1907d50..d719199 100644 --- a/tests/tradeManager.mm +++ b/tests/tradeManager.mm @@ -14,45 +14,50 @@ @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 { - 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