diff --git a/.github/workflows/brew.sh b/.github/workflows/brew.sh index 5e03be3..c0f9188 100644 --- a/.github/workflows/brew.sh +++ b/.github/workflows/brew.sh @@ -1,15 +1,15 @@ #!/bin/bash -# Check if a package is already installed via Homebrew, then skip +# Function to check if a package is already installed via Homebrew, then skip installation if it is check_and_install() { if ! brew list $1 &>/dev/null; then - echo "Installing $1..." - brew install $1 + echo "Installing $1..." # Inform the user that the package is being installed + brew install $1 # Install the package using Homebrew else - echo "$1 is already installed" + echo "$1 is already installed" # Inform the user that the package is already installed fi } # Install packages if they don't exist -check_and_install postgresql -check_and_install pkg-config \ No newline at end of file +check_and_install postgresql # Check and install PostgreSQL +check_and_install pkg-config # Check and install pkg-config \ No newline at end of file diff --git a/.github/workflows/cpp_coverage.sh b/.github/workflows/cpp_coverage.sh new file mode 100644 index 0000000..70b6c2b --- /dev/null +++ b/.github/workflows/cpp_coverage.sh @@ -0,0 +1,6 @@ +PROFILE_DIR=$(ls -t /tmp/Build/ProfileData | head -n 1); +xcrun llvm-cov show \ + /tmp/Build/Products/Debug/tests.xctest/Contents/MacOS/tests \ + -instr-profile="/tmp/Build/ProfileData/${PROFILE_DIR}/Coverage.profdata" \ + -format=text \ + > coverage.txt \ No newline at end of file diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index b84ab97..f5255b7 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -1,23 +1,20 @@ -# Backtesting C++ Workflow -# To build, test and perform SonarCloud analysis -name: Build - on: + # Trigger analysis when pushing in master or pull requests, and when creating + # a pull request. push: branches: - - main # Trigger on pushes to main branch + - main pull_request: - types: - - opened # When PR is first created - - synchronize # When new commits are pushed to the PR - - reopened # When PR is reopened after being closed + types: [opened, synchronize, reopened] +name: Build -# Environment variables used across jobs env: - BUILD_TYPE: Release # Set CMake build configuration - BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Output directory for build wrapper + # 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 @@ -52,17 +49,21 @@ jobs: # - 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' + 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/" \ + 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 + clean build test | xcpretty -r junit && exit ${PIPESTATUS[0]} - name: Convert coverage report to sonarqube format run: > @@ -73,8 +74,8 @@ jobs: uses: actions/upload-artifact@v4 with: path: sonarqube-generic-coverage.xml - retention-days: 1 - + retention-days: 1 # Artifact will be available only for 5 days. + sonar-scan: name: SonarCloud Scan runs-on: ubuntu-latest @@ -109,13 +110,22 @@ jobs: # 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: @@ -123,4 +133,5 @@ jobs: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} with: args: > - --define sonar.cfamily.compile-commands="${{ env.BUILD_WRAPPER_OUT_DIR }}/compile_commands.json" \ \ No newline at end of file + --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/.gitignore b/.gitignore index 5bba247..3846e40 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,6 @@ TestResult .DS_Store xcuserdata/ -xcuserstate/ \ No newline at end of file +xcuserstate/ +TestResult.xcresult/ +sonarqube-generic-coverage.xml \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..487a355 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,67 @@ +{ + "files.associations": { + "unordered_map": "cpp", + "__bit_reference": "cpp", + "__hash_table": "cpp", + "__locale": "cpp", + "__node_handle": "cpp", + "__split_buffer": "cpp", + "__threading_support": "cpp", + "__tree": "cpp", + "__verbose_abort": "cpp", + "any": "cpp", + "array": "cpp", + "bitset": "cpp", + "cctype": "cpp", + "charconv": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "complex": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "execution": "cpp", + "memory": "cpp", + "forward_list": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "ios": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "limits": "cpp", + "list": "cpp", + "locale": "cpp", + "map": "cpp", + "mutex": "cpp", + "new": "cpp", + "optional": "cpp", + "ostream": "cpp", + "print": "cpp", + "queue": "cpp", + "ratio": "cpp", + "regex": "cpp", + "set": "cpp", + "source_location": "cpp", + "span": "cpp", + "sstream": "cpp", + "stack": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "string": "cpp", + "string_view": "cpp", + "tuple": "cpp", + "typeinfo": "cpp", + "valarray": "cpp", + "variant": "cpp", + "vector": "cpp", + "algorithm": "cpp" + } +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ddb6c7..29cd38c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,8 @@ add_subdirectory(external/libpqxx EXCLUDE_FROM_ALL) include_directories( ${CMAKE_SOURCE_DIR}/source/include ${CMAKE_SOURCE_DIR}/source/include/utilities + ${CMAKE_SOURCE_DIR}/source/include/models + ${CMAKE_SOURCE_DIR}/source/include/trading ${CMAKE_SOURCE_DIR}/source/include/trading_definitions ${CMAKE_SOURCE_DIR}/external ) diff --git a/README.md b/README.md index 216339d..363022d 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. -![alt](images/development_active.svg) [![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) +[![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) I'm extracting results and creating various graphs for trend analyses using SciPy for calculations and Plotly for visualization. @@ -63,7 +63,7 @@ Xcode - Library Path "/opt/homebrew/Cellar/postgresql@14/14.15/lib/postgresql@14" ``` -### Test build with cmake +### Test the build `sh ./scripts/build.sh` diff --git a/backtesting-engine-cpp.xcodeproj/project.pbxproj b/backtesting-engine-cpp.xcodeproj/project.pbxproj index 466e57a..8c6bf5a 100644 --- a/backtesting-engine-cpp.xcodeproj/project.pbxproj +++ b/backtesting-engine-cpp.xcodeproj/project.pbxproj @@ -15,8 +15,16 @@ 941B549B2D3BBADE00E3BF64 /* trading_definitions_json.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 941B54992D3BBADD00E3BF64 /* trading_definitions_json.cpp */; }; 94280BA32D2FC00200F1CF56 /* base64.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94280BA22D2FC00200F1CF56 /* base64.cpp */; }; 94280BA42D2FC00200F1CF56 /* base64.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94280BA22D2FC00200F1CF56 /* base64.cpp */; }; + 943398242D57E53400287A2D /* jsonParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 943398232D57E53400287A2D /* jsonParser.cpp */; }; + 943398252D57E53400287A2D /* jsonParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 943398232D57E53400287A2D /* jsonParser.cpp */; }; + 943398272D57E54000287A2D /* jsonParser.mm in Sources */ = {isa = PBXBuildFile; fileRef = 943398262D57E54000287A2D /* jsonParser.mm */; }; 94364CB62D416D8D00F35B55 /* db.mm in Sources */ = {isa = PBXBuildFile; fileRef = 94364CB52D416D8000F35B55 /* db.mm */; }; 944698852D3A545B0070E30F /* libpq.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 94CD8A972D2D34A100041BBA /* libpq.a */; }; + 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 */; }; + 94674B8D2D533E7800973137 /* trade.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94674B8B2D533E7800973137 /* trade.cpp */; }; + 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 */; }; 94CD8B992D2DCDD800041BBA /* libpqxx-7.10.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 94CD8B982D2DCDD800041BBA /* libpqxx-7.10.a */; }; @@ -54,6 +62,9 @@ 94280BA12D2FC00200F1CF56 /* base64.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = base64.hpp; sourceTree = ""; }; 94280BA22D2FC00200F1CF56 /* base64.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = base64.cpp; sourceTree = ""; }; 942966D82D48E84A00532862 /* priceData.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = priceData.hpp; sourceTree = ""; }; + 943398222D57E52900287A2D /* jsonParser.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = jsonParser.hpp; sourceTree = ""; }; + 943398232D57E53400287A2D /* jsonParser.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = jsonParser.cpp; sourceTree = ""; }; + 943398262D57E54000287A2D /* jsonParser.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = jsonParser.mm; sourceTree = ""; }; 94364CB52D416D8000F35B55 /* db.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = db.mm; sourceTree = ""; }; 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 = ""; }; @@ -65,6 +76,11 @@ 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 = ""; }; + 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 = ""; }; + 94674B892D533BDA00973137 /* tradeManager.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = tradeManager.mm; sourceTree = ""; }; + 94674B8B2D533E7800973137 /* trade.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = trade.cpp; sourceTree = ""; }; 94685CCE2D384A8B00863D04 /* json.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = json.hpp; sourceTree = ""; }; 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 = ""; }; @@ -1289,6 +1305,7 @@ 942966D72D48E84100532862 /* models */ = { isa = PBXGroup; children = ( + 94674B822D533B1D00973137 /* trade.hpp */, 942966D82D48E84A00532862 /* priceData.hpp */, ); path = models; @@ -1343,6 +1360,30 @@ path = images; sourceTree = ""; }; + 94674B842D533B2F00973137 /* trading */ = { + isa = PBXGroup; + children = ( + 94674B832D533B2F00973137 /* tradeManager.hpp */, + ); + path = trading; + sourceTree = ""; + }; + 94674B862D533B4000973137 /* trading */ = { + isa = PBXGroup; + children = ( + 94674B852D533B4000973137 /* tradeManager.cpp */, + ); + path = trading; + sourceTree = ""; + }; + 94674B8C2D533E7800973137 /* models */ = { + isa = PBXGroup; + children = ( + 94674B8B2D533E7800973137 /* trade.cpp */, + ); + path = models; + sourceTree = ""; + }; 94685CCF2D384A8B00863D04 /* nlohmann */ = { isa = PBXGroup; children = ( @@ -1354,6 +1395,9 @@ 9470B5A22C8C5AD0007D9CC6 /* source */ = { isa = PBXGroup; children = ( + 943398232D57E53400287A2D /* jsonParser.cpp */, + 94674B8C2D533E7800973137 /* models */, + 94674B862D533B4000973137 /* trading */, 94DE4F772C8C3E7C00FE48FF /* include */, 941B54982D3BBAD800E3BF64 /* trading_definitions */, 94280BA72D2FC29F00F1CF56 /* utilities */, @@ -1368,6 +1412,8 @@ 9470B5AD2C8C5B99007D9CC6 /* tests */ = { isa = PBXGroup; children = ( + 943398262D57E54000287A2D /* jsonParser.mm */, + 94674B892D533BDA00973137 /* tradeManager.mm */, 94364CB52D416D8000F35B55 /* db.mm */, ); path = tests; @@ -3464,6 +3510,8 @@ 94DE4F772C8C3E7C00FE48FF /* include */ = { isa = PBXGroup; children = ( + 943398222D57E52900287A2D /* jsonParser.hpp */, + 94674B842D533B2F00973137 /* trading */, 942966D72D48E84100532862 /* models */, 94B8C7932D3D770800E17EB6 /* utilities */, 941B548F2D3BBA3B00E3BF64 /* trading_definitions */, @@ -3569,8 +3617,11 @@ buildActionMask = 2147483647; files = ( 9470B5A42C8C5AD0007D9CC6 /* main.cpp in Sources */, + 943398252D57E53400287A2D /* jsonParser.cpp in Sources */, 94280BA32D2FC00200F1CF56 /* base64.cpp in Sources */, + 94674B8E2D533E7800973137 /* trade.cpp in Sources */, 941B549B2D3BBADE00E3BF64 /* trading_definitions_json.cpp in Sources */, + 94674B872D533B4000973137 /* tradeManager.cpp in Sources */, 94CD8BA02D2E8CE500041BBA /* databaseConnection.cpp in Sources */, 940A61132C92CE210083FEB8 /* configManager.cpp in Sources */, 940A61172C92CE960083FEB8 /* serviceA.cpp in Sources */, @@ -3582,9 +3633,14 @@ buildActionMask = 2147483647; files = ( 94CD8BA12D2E8CE500041BBA /* databaseConnection.cpp in Sources */, + 943398242D57E53400287A2D /* jsonParser.cpp in Sources */, 94280BA42D2FC00200F1CF56 /* base64.cpp in Sources */, + 94674B8D2D533E7800973137 /* trade.cpp in Sources */, 941B549A2D3BBADE00E3BF64 /* trading_definitions_json.cpp in Sources */, + 94674B8A2D533BDA00973137 /* tradeManager.mm in Sources */, 940A61182C92CE960083FEB8 /* serviceA.cpp in Sources */, + 94674B882D533B4000973137 /* tradeManager.cpp in Sources */, + 943398272D57E54000287A2D /* jsonParser.mm in Sources */, 9470B5B62C8C5BFD007D9CC6 /* main.cpp in Sources */, 94364CB62D416D8D00F35B55 /* db.mm in Sources */, 940A61142C92CE210083FEB8 /* configManager.cpp in Sources */, diff --git a/backtesting-engine-cpp.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/backtesting-engine-cpp.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/backtesting-engine-cpp.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/scripts/local_test_coverage.sh b/scripts/local_test_coverage.sh new file mode 100644 index 0000000..2dd7eeb --- /dev/null +++ b/scripts/local_test_coverage.sh @@ -0,0 +1,21 @@ +rm -rf ./TestResult; +rm -rf ./TestResult.xcresult; +rm -rf ./sonarqube-generic-coverage.xml + +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 "/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 diff --git a/scripts/run.sh b/scripts/run.sh index 5a82e40..79e8dd8 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -17,7 +17,37 @@ else exit 1 fi +json='{ + "RUN_ID": "UNIQUE_IDENTIFER", + "SYMBOLS": "EURUSD", + "LAST_MONTHS": 6, + "STRATEGY": { + "UUID": "", + "TRADING_VARIABLES": { + "STRATEGY": "OHLC_RSI", + "STOP_DISTANCE_IN_PIPS": 1, + "LIMIT_DISTANCE_IN_PIPS": 1, + "TRADING_SIZE": 1 + }, + "OHLC_VARIABLES": [ + { + "OHLC_COUNT": 60, + "OHLC_MINUTES": 100 + } + ], + "STRATEGY_VARIABLES" : { + "OHLC_RSI_VARIABLES": { + "RSI_LONG": 60, + "RSI_SHORT": 40 + } + } + } +}' + +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) -./"$BUILD_DIR/$EXECUTABLE_NAME" localhost ewogICJSVU5fSUQiOiAiaWQiLAogICJTWU1CT0xTIjogIkVVUlVTRCIsCiAgIkxBU1RfTU9OVEhTIjogNiwKICAiU1RSQVRFR1kiOiB7CiAgICAgICJVVUlEIjogIiIsCiAgICAgICJUUkFESU5HX1ZBUklBQkxFUyI6IHsKICAgICAgICAgICJTVFJBVEVHWSI6ICJPSExDX1JTSSIsCiAgICAgICAgICAiU1RPUF9ESVNUQU5DRV9JTl9QSVBTIjogMSwKICAgICAgICAgICJMSU1JVF9ESVNUQU5DRV9JTl9QSVBTIjogMSwKICAgICAgICAgICJUUkFESU5HX1NJWkUiOiAxCiAgICAgIH0sCiAgICAgICJPSExDX1ZBUklBQkxFUyI6IFsKICAgICAgICAgIHsKICAgICAgICAgICAgICAiT0hMQ19DT1VOVCI6IDYwLAogICAgICAgICAgICAgICJPSExDX01JTlVURVMiOiAxMDAKICAgICAgICAgIH0KICAgICAgXSwKICAgICAgIk9ITENfUlNJX1ZBUklBQkxFUyI6IHsKICAgICAgICAgICJSU0lfTE9ORyI6IDYwLAogICAgICAgICAgIlJTSV9TSE9SVCI6IDQwCiAgICAgIH0KICB9Cn0K +./"$BUILD_DIR/$EXECUTABLE_NAME" localhost "$output" diff --git a/scripts/test.sh b/scripts/test.sh index 2f008de..dcd6609 100644 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,6 +1,9 @@ # #!/bin/bash xcodebuild \ - test \ - -project backtesting-engine-cpp.xcodeproj \ - -scheme tests + -project backtesting-engine-cpp.xcodeproj \ + -scheme tests \ + 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 \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties index 7ed5b0f..a958544 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,8 +1,8 @@ sonar.projectKey=mccaffers_backtesting-engine-cpp sonar.organization=mccaffers sonar.python.version=3.8 -sonar.language=c -sonar.c.file.suffixes=.c,.h -sonar.cpp.file.suffixes=.cc,.cpp,.cxx,.c++,.hh,.hpp,.hxx,.h++,.ipp -sonar.coverage.exclusions=tests/**, source/utilities/base64.cpp +sonar.language=cpp +sonar.cpp.file.suffixes=.cpp,.hpp,.mm +sonar.coverage.exclusions=tests/**, external/**/*, source/utilities/base64.cpp sonar.sources=source/ +sonar.tests=tests/ diff --git a/source/include/jsonParser.hpp b/source/include/jsonParser.hpp new file mode 100644 index 0000000..779fb4b --- /dev/null +++ b/source/include/jsonParser.hpp @@ -0,0 +1,16 @@ +// Backtesting Engine in C++ +// +// (c) 2025 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- + +#pragma once + +#include +#include +#include "trading_definitions.hpp" + +class JsonParser { +public: + static int parseConfigurationFromBase64(const std::string& input); +}; diff --git a/source/include/models/trade.hpp b/source/include/models/trade.hpp new file mode 100644 index 0000000..29eb4f5 --- /dev/null +++ b/source/include/models/trade.hpp @@ -0,0 +1,38 @@ +// Backtesting Engine in C++ +// +// (c) 2025 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- + +#pragma once +#include +#include + +struct Trade { + static int idCounter; + std::string id; + double entryPrice; + double size; + std::chrono::system_clock::time_point openTime; + bool isLong; + + // Default constructor + Trade() : entryPrice(0), size(0), isLong(false), + openTime(std::chrono::system_clock::now()) {} + + // Copy constructor + Trade(const Trade& other) = default; + + Trade(double price, double quantity, bool long_position) + : entryPrice(price), + size(quantity), + isLong(long_position), + openTime(std::chrono::system_clock::now()) { + // Generate unique ID using counter + id = std::to_string(++idCounter); + } + + static void resetCounter() { + idCounter = 0; + } +}; diff --git a/source/include/trading/tradeManager.hpp b/source/include/trading/tradeManager.hpp new file mode 100644 index 0000000..e884cc6 --- /dev/null +++ b/source/include/trading/tradeManager.hpp @@ -0,0 +1,27 @@ +// Backtesting Engine in C++ +// +// (c) 2025 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- + +#pragma once +#include +#include +#include "trade.hpp" + +class TradeManager { +private: + static TradeManager* instance; + std::unordered_map activeTrades; + + TradeManager() = default; + +public: + static TradeManager* getInstance(); + static void reset(); + void clearAllTrades(); + std::string openTrade(double price, double size, bool isLong); + size_t reviewAccount() const; + bool closeTrade(const std::string& tradeId); + const std::unordered_map& getActiveTrades() const; +}; diff --git a/source/jsonParser.cpp b/source/jsonParser.cpp new file mode 100644 index 0000000..37cffb7 --- /dev/null +++ b/source/jsonParser.cpp @@ -0,0 +1,26 @@ +#include "jsonParser.hpp" +#include "base64.hpp" +#include + +using json = nlohmann::json; + +int JsonParser::parseConfigurationFromBase64(const std::string& input) { + // Ingest parameters + std::string output = Base64::b64decode(input); + + // Debug, console print out + std::cout << output; + + json j; + try { + j = json::parse(output); + } + catch (json::parse_error& ex) { + std::cerr << "parse error at byte " << ex.byte << std::endl; + } + + auto config = j.get(); + std::cout << config.RUN_ID << std::endl; + + return 0; +} diff --git a/source/main.cpp b/source/main.cpp index fc498b9..9f6ecea 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -9,6 +9,7 @@ #include #include #include +#include // external headers #include @@ -19,46 +20,42 @@ #include "databaseConnection.hpp" #include "base64.hpp" #include "trading_definitions.hpp" // For everything +#include "tradeManager.hpp" +#include "jsonParser.hpp" using json = nlohmann::json; -int parseJson(const std::string& input) { - - // Ingest parameters - std::string output = Base64::b64decode(input); - - // Debug, console print out - std::cout << output; - - json j; - try - { - j = json::parse(output); - } - catch (json::parse_error& ex) - { - std::cerr << "parse error at byte " << ex.byte << std::endl; - } - - auto config = j.get(); - std::cout << config.RUN_ID << std::endl; - - return 0; -} - 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] - parseJson(argv[2]); + JsonParser::parseConfigurationFromBase64(argv[2]); // Example query - replace with your actual query std::string query = "SELECT * FROM EURUSD LIMIT 5;"; - db.executeQuery(query); + std::vector priceData = db.executeQuery(query); + + // 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; -return 0; + // 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; + + return 0; } diff --git a/source/models/trade.cpp b/source/models/trade.cpp new file mode 100644 index 0000000..1ff900e --- /dev/null +++ b/source/models/trade.cpp @@ -0,0 +1,10 @@ +// Backtesting Engine in C++ +// +// (c) 2025 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- + +#include "trade.hpp" + +// Definition of static member +int Trade::idCounter = 0; diff --git a/source/trading/tradeManager.cpp b/source/trading/tradeManager.cpp new file mode 100644 index 0000000..d3cb308 --- /dev/null +++ b/source/trading/tradeManager.cpp @@ -0,0 +1,49 @@ +// Backtesting Engine in C++ +// +// (c) 2025 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- + +#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); + activeTrades[trade.id] = trade; + return trade.id; +} + +size_t TradeManager::reviewAccount() const { + return activeTrades.size(); +} + +bool TradeManager::closeTrade(const std::string& tradeId) { + auto it = activeTrades.find(tradeId); + if (it != activeTrades.end()) { + activeTrades.erase(it); + return true; + } + return false; +} + +const std::unordered_map& TradeManager::getActiveTrades() const { + return activeTrades; +} diff --git a/tests/jsonParser.mm b/tests/jsonParser.mm new file mode 100644 index 0000000..88158b8 --- /dev/null +++ b/tests/jsonParser.mm @@ -0,0 +1,62 @@ +// Backtesting Engine in C++ +// +// (c) 2025 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- + +#import +#import "jsonParser.hpp" +#include "base64.hpp" + +@interface jsonParserTests : XCTestCase +@end + +@implementation jsonParserTests + +- (void)setUp { + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. +} + +- (void)testValidJsonParsing { + // Create a sample configuration JSON with all required fields + std::string validJson = R"({ + "RUN_ID": "UNIQUE_IDENTIFER", + "SYMBOLS": "EURUSD", + "LAST_MONTHS": 6, + "STRATEGY": { + "UUID": "", + "TRADING_VARIABLES": { + "STRATEGY": "OHLC_RSI", + "STOP_DISTANCE_IN_PIPS": 1, + "LIMIT_DISTANCE_IN_PIPS": 1, + "TRADING_SIZE": 1 + }, + "OHLC_VARIABLES": [ + { + "OHLC_COUNT": 60, + "OHLC_MINUTES": 100 + } + ], + "STRATEGY_VARIABLES" : { + "OHLC_RSI_VARIABLES": { + "RSI_LONG": 60, + "RSI_SHORT": 40 + } + } + } +})"; + + // Convert to base64 + std::string base64Input = Base64::b64encode(validJson); + + // Test parsing + int result = JsonParser::parseConfigurationFromBase64(base64Input); + XCTAssertEqual(result, 0, "Parsing should succeed with valid JSON"); +} + + +@end diff --git a/tests/tradeManager.mm b/tests/tradeManager.mm new file mode 100644 index 0000000..1907d50 --- /dev/null +++ b/tests/tradeManager.mm @@ -0,0 +1,58 @@ +// Backtesting Engine in C++ +// +// (c) 2025 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- + +#import +#import "tradeManager.hpp" + +@interface TradeManagerTests : XCTestCase +@property (nonatomic) TradeManager* manager; +@end + +@implementation TradeManagerTests + +- (void)setUp { + TradeManager::reset(); // Reset the singleton instance + self.manager = TradeManager::getInstance(); +} + +- (void)tearDown { + self.manager->clearAllTrades(); + TradeManager::reset(); +} + +- (void)testOpenTrade { + std::string tradeId = self.manager->openTrade(100.0, 1.0, true); + 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); + 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); + + XCTAssertEqual(self.manager->reviewAccount(), 3, "Should have 3 active trades"); +} + +- (void)testTradeDetails { + std::string tradeId = self.manager->openTrade(100.0, 1.0, true); + 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"); +} + +@end