diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 77be3d6..2ae2973 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,12 +3,18 @@ on: branches: - main pull_request: - types: [opened, synchronize, reopened] + types: [opened, synchronize, reopened] name: Build +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + BUILD_TYPE: Release + BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory # Directory where build-wrapper output will be placed jobs: build: @@ -59,4 +65,57 @@ jobs: with: name: sonarqube-coverage path: sonarqube-generic-coverage.xml - retention-days: 1 \ No newline at end of file + retention-days: 1 + + sonar-scan: + name: SonarCloud Scan + needs: build + runs-on: ubuntu-latest + + steps: + # fetch-depth: 0 gives Sonar full git history for blame-based new-code analysis. + - name: Checkout repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + - name: Check compiler version, for debugging + run: | + g++ --version + cmake --version + - name: Build C++ Libraries + run: bash ./scripts/build_dep.sh + # 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. + + # Lands at ./artifact/sonarqube-generic-coverage.xml so the existing + # sonar.coverageReportPaths argument keeps working unchanged. + - name: Download coverage artifact from build job + uses: actions/download-artifact@v5 + with: + name: sonarqube-coverage + path: artifact + + # 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/sonar.yml b/.github/workflows/sonar.yml deleted file mode 100644 index 81c930a..0000000 --- a/.github/workflows/sonar.yml +++ /dev/null @@ -1,89 +0,0 @@ -on: - # Triggered on completion of the Build workflow so we can consume its - # coverage artifact. Limited to runs against main so we only scan the - # mainline; PR builds don't trigger Sonar. - workflow_run: - workflows: ["Build"] - types: [completed] - branches: [main] - -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/CMakeLists.txt b/CMakeLists.txt index c0f074a..7d02d5b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,7 @@ include_directories( ${CMAKE_SOURCE_DIR}/include/models ${CMAKE_SOURCE_DIR}/include/trading ${CMAKE_SOURCE_DIR}/include/trading_definitions + ${CMAKE_SOURCE_DIR}/include/strategies ${CMAKE_SOURCE_DIR}/external ) diff --git a/README.md b/README.md index 0d36f5f..ea1060b 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. -[![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) +[![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. @@ -20,7 +20,17 @@ I'm extracting results and creating various graphs for trend analyses using SciP This backtesting engine can pull tick data from local files or from a Postgres database. I'm using QuestDB. -### Postgres Setup - Requires libpq-dev or its equivalent for your OS: +### Clone with submodules + +The project depends on two vendored libraries (`libpqxx` and `boost-decimal`) tracked as git submodules under `external/`. If you didn't clone with `--recurse-submodules`, run: + +``` +git submodule update --init --recursive +``` + +`scripts/build_dep.sh` does this for you on first run. + +### Install libpq (required by libpqxx) ``` For Ubuntu/Debian systems: sudo apt-get install libpq-dev @@ -30,15 +40,12 @@ For OpenSuse: zypper in postgresql-devel For ArchLinux: pacman -S postgresql-libs ``` -### Postgres Setup (using C++20) +### Build dependencies + +`libpqxx` is built once via CMake. `boost-decimal` is header-only and pulled in via `add_subdirectory` from the top-level `CMakeLists.txt` — nothing to build. The script below handles the libpqxx build: ``` -cd ./external/libpqxx -mkdir -p build -cd ./build -cmake .. -./configure CXXFLAGS="-std=c++20 -O3" -make +bash ./scripts/build_dep.sh ``` Xcode - Link Binary with Libraries (Source & Test) @@ -63,17 +70,17 @@ Xcode - Library Path "/opt/homebrew/Cellar/postgresql@14/14.15/lib/postgresql@14" ``` -### Test the build +### Build the project -`sh ./scripts/build.sh` +`bash ./scripts/build.sh` ### Run via terminal -`sh ./scripts/run.sh` +`bash ./scripts/run.sh` -### Run via test via terminal +### Run tests via terminal -`sh ./scripts/test.sh` +`bash ./scripts/test.sh` ### License [MIT](https://choosealicense.com/licenses/mit/) diff --git a/backtesting-engine-cpp.xcodeproj/project.pbxproj b/backtesting-engine-cpp.xcodeproj/project.pbxproj index fbdaccb..af02243 100644 --- a/backtesting-engine-cpp.xcodeproj/project.pbxproj +++ b/backtesting-engine-cpp.xcodeproj/project.pbxproj @@ -33,6 +33,8 @@ 94724A842F8B92C10029B940 /* operations.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94724A822F8B92C10029B940 /* operations.cpp */; }; 94CD8BA02D2E8CE500041BBA /* databaseConnection.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94CD8B9F2D2E8CE500041BBA /* databaseConnection.cpp */; }; 94CD8BA12D2E8CE500041BBA /* databaseConnection.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94CD8B9F2D2E8CE500041BBA /* databaseConnection.cpp */; }; + 94D601102FA9CD700066F51A /* randomStrategy.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94D6010E2FA9CD700066F51A /* randomStrategy.cpp */; }; + 94D601112FA9CD700066F51A /* randomStrategy.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94D6010E2FA9CD700066F51A /* randomStrategy.cpp */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -48,6 +50,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 9409A61B2FAA6411002C30FF /* strategy.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = strategy.hpp; sourceTree = ""; }; 940A61112C92CE210083FEB8 /* configManager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = configManager.cpp; sourceTree = ""; }; 940A61122C92CE210083FEB8 /* configManager.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = configManager.hpp; sourceTree = ""; }; 940A61152C92CE960083FEB8 /* serviceA.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = serviceA.cpp; sourceTree = ""; }; @@ -1254,6 +1257,8 @@ 94CD8B9A2D2DCF6E00041BBA /* libpqxx-7.10.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libpqxx-7.10.a"; path = "build/external/libpqxx/src/libpqxx-7.10.a"; sourceTree = ""; }; 94CD8B9E2D2E8CE500041BBA /* databaseConnection.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = databaseConnection.hpp; sourceTree = ""; }; 94CD8B9F2D2E8CE500041BBA /* databaseConnection.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = databaseConnection.cpp; sourceTree = ""; }; + 94D6010E2FA9CD700066F51A /* randomStrategy.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = randomStrategy.cpp; sourceTree = ""; }; + 94D601122FA9CD890066F51A /* randomStrategy.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = randomStrategy.hpp; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1388,6 +1393,7 @@ 9470B5A22C8C5AD0007D9CC6 /* source */ = { isa = PBXGroup; children = ( + 94D6010F2FA9CD700066F51A /* strategies */, 94674B8C2D533E7800973137 /* models */, 94674B862D533B4000973137 /* trading */, 941B54982D3BBAD800E3BF64 /* trading_definitions */, @@ -3494,9 +3500,27 @@ path = "../../../../../opt/homebrew/Cellar/postgresql@14/14.15/lib/postgresql@14/pgxs"; sourceTree = ""; }; + 94D6010F2FA9CD700066F51A /* strategies */ = { + isa = PBXGroup; + children = ( + 94D6010E2FA9CD700066F51A /* randomStrategy.cpp */, + ); + path = strategies; + sourceTree = ""; + }; + 94D601132FA9CD890066F51A /* strategies */ = { + isa = PBXGroup; + children = ( + 9409A61B2FAA6411002C30FF /* strategy.hpp */, + 94D601122FA9CD890066F51A /* randomStrategy.hpp */, + ); + path = strategies; + sourceTree = ""; + }; 94DE4F772C8C3E7C00FE48FF /* include */ = { isa = PBXGroup; children = ( + 94D601132FA9CD890066F51A /* strategies */, 94674B842D533B2F00973137 /* trading */, 942966D72D48E84100532862 /* models */, 94B8C7932D3D770800E17EB6 /* utilities */, @@ -3616,6 +3640,7 @@ 940A61132C92CE210083FEB8 /* configManager.cpp in Sources */, 94724A842F8B92C10029B940 /* operations.cpp in Sources */, 940A61172C92CE960083FEB8 /* serviceA.cpp in Sources */, + 94D601112FA9CD700066F51A /* randomStrategy.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3630,6 +3655,7 @@ 94280BA42D2FC00200F1CF56 /* base64.cpp in Sources */, 94674B8D2D533E7800973137 /* trade.cpp in Sources */, 941B549A2D3BBADE00E3BF64 /* trading_definitions_json.cpp in Sources */, + 94D601102FA9CD700066F51A /* randomStrategy.cpp in Sources */, 94674B8A2D533BDA00973137 /* tradeManager.mm in Sources */, 94724A832F8B92C10029B940 /* operations.cpp in Sources */, 940A61182C92CE960083FEB8 /* serviceA.cpp in Sources */, diff --git a/include/strategies/randomStrategy.hpp b/include/strategies/randomStrategy.hpp new file mode 100644 index 0000000..3ee1742 --- /dev/null +++ b/include/strategies/randomStrategy.hpp @@ -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) +// --------------------------------------- + +#pragma once +#include +#include +#include +#include "models/priceData.hpp" +#include "models/trade.hpp" // for Direction enum +#include "strategies/strategy.hpp" // IStrategy base class +#include "trading_definitions/strategy.hpp" + +class TradeManager; // forward declared in strategy.hpp; redeclaring is harmless + +// A trivial strategy: on every tick it flips a fair coin and returns +// LONG or SHORT. While trades are open it has a small per-tick chance +// of closing every active position. Intended as scaffolding for the +// strategy interface, not as a real trading approach. +// +// C# parallels for readers from a C# background: +// - `class` here is a value/owning type managed via stack or +// std::unique_ptr — there is no GC. Lifetime is explicit. +// - `explicit` on a single-arg constructor disables implicit +// conversion (C# constructors are always explicit, so this is +// just C++ catching up to the default C# behaviour). +// - `std::mt19937` is the modern C++ RNG engine; it replaces the +// globally-shared `std::rand()` used elsewhere in this codebase +// and gives us the option of seeding for reproducible backtests. +// - `: public IStrategy` is C++ inheritance syntax. `public` means +// the inheritance preserves access — outside code can use a +// `RandomStrategy` anywhere an `IStrategy` is expected. The C# +// equivalent is `: IStrategy`; C# has no concept of private +// inheritance, so the `public` keyword has no analogue there. +class RandomStrategy : public IStrategy { +public: + explicit RandomStrategy(const trading_definitions::Strategy& strategyConfig); + + // Returns `std::nullopt` to mean "no signal — don't trade". The + // random strategy always returns a direction, but the interface + // matches future strategies that only fire on certain conditions. + // `std::optional` is roughly C#'s `Nullable` / `T?` — a + // value type that may or may not hold a T, with no heap allocation. + // + // Not const because the RNG engine mutates its internal state on + // each call. The `tick` parameter is unused today but keeps the + // interface stable for strategies that will look at price. + std::optional decide(const PriceData& tick) override; + + // Per-tick management hook. With a small probability per tick + // (see `closeProb`), closes every currently-open trade at the + // tick's bid price. `tradeManager` is passed by mutable reference + // because the strategy needs to call mutating methods on it + // (`closeTrade`); `getActiveTrades()` is still safely const. + void during(std::size_t tickValue, + const PriceData& price, + TradeManager& tradeManager) override; + +private: + trading_definitions::Strategy config; + std::mt19937 rng; + std::bernoulli_distribution coin; // fair coin flip for entry direction + std::bernoulli_distribution closeProb; // per-tick probability of closing all trades +}; diff --git a/include/strategies/strategy.hpp b/include/strategies/strategy.hpp new file mode 100644 index 0000000..1b67f12 --- /dev/null +++ b/include/strategies/strategy.hpp @@ -0,0 +1,62 @@ +// 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 "models/priceData.hpp" +#include "models/trade.hpp" + +// Forward declaration. We only refer to `TradeManager` by reference in +// this header, so the compiler doesn't need its full definition here — +// just to know "it's a class". Pulling in the full header would drag +// every TradeManager dependency into every strategy. C# doesn't really +// have an equivalent because it resolves types at the assembly level +// rather than per file, but in C++ this is a standard way to keep +// header dependencies (and compile times) under control. +class TradeManager; + +// Abstract base class that every concrete strategy implements. +// +// C# parallels for readers from a C# background: +// - This is the C++ equivalent of a C# `interface`. C++ has no +// dedicated `interface` keyword, so you express it as a class +// whose methods are all pure virtual (the `= 0` suffix). The +// `I` prefix is borrowed from C# — C++ has no fixed convention, +// but it's a useful hint because C++ classes routinely mix +// virtual and concrete methods, so "is this an interface?" isn't +// always obvious from the keyword alone. +// - `virtual ~IStrategy() = default;` is mandatory. If you ever +// delete a derived strategy through an `IStrategy*`, a non-virtual +// destructor would skip the derived destructor and leak resources. +// C# handles this for you; in C++ you opt in. +// - `= 0` makes a method pure virtual, which makes the class +// abstract — you cannot instantiate `IStrategy` directly, only +// concrete subclasses. Same behaviour as a C# interface. +// - Derived classes annotate their implementations with `override` +// (see `RandomStrategy`). It's optional in C++ but catches +// signature typos at compile time, exactly like C#'s `override`. +class IStrategy { +public: + virtual ~IStrategy() = default; + + // Entry signal. Returns `std::nullopt` to mean "no trade". + // Non-const because some implementations (e.g. RandomStrategy) + // mutate internal RNG state on each call. + virtual std::optional decide(const PriceData& tick) = 0; + + // Called every tick. Receives the TradeManager by mutable + // reference so strategies can both inspect open positions + // (`tradeManager.getActiveTrades()`) and act on them + // (`closeTrade`, future scale/adjust hooks). A reference signals + // "borrow, don't own" — the strategy must not delete it. The C# + // analogue is just passing the manager as a parameter; C# has no + // distinction between reference and pointer so the by-ref nature + // is implicit there. + virtual void during(std::size_t tickValue, + const PriceData& price, + TradeManager& tradeManager) = 0; +}; diff --git a/source/operations.cpp b/source/operations.cpp index 0a9b8ef..1fd8c73 100644 --- a/source/operations.cpp +++ b/source/operations.cpp @@ -15,21 +15,27 @@ #include #include #include "tradeManager.hpp" +#include "strategies/randomStrategy.hpp" void Operations::run(const std::vector& ticks, const trading_definitions::Configuration& config) { // Create auto tradeManager = new TradeManager(); - + RandomStrategy strategy(config.STRATEGY); + + std::size_t tickIndex = 0; for (const auto& tick : ticks) { size_t openTrades = tradeManager->reviewAccount(); - - // this would be strategy invoke point + + // only open a trade if there is zero if (openTrades == 0) { - std::string tradeId = tradeManager->openTrade(tick, config.STRATEGY.TRADING_VARIABLES.TRADING_SIZE, Direction::LONG); - std::cout << "Opened trade: " << tradeId << std::endl; + // optional is false + if (auto signal = strategy.decide(tick)) { + std::string tradeId = tradeManager->openTrade(tick, config.STRATEGY.TRADING_VARIABLES.TRADING_SIZE, *signal); + std::cout << "Opened trade: " << tradeId << std::endl; + } } // this would be a position manager review point @@ -46,18 +52,15 @@ 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; - 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; - } - } + // Strategy-driven management hook. The strategy itself decides + // whether/when to close positions; the random strategy currently + // closes every open trade with a small per-tick probability. + // Dereferencing `tradeManager` (a raw pointer) yields a reference + // — the parameter type is `TradeManager&`, so `*tradeManager` + // is what we hand in. + strategy.during(tickIndex, tick, *tradeManager); + + ++tickIndex; } std::cout << "Final PnL: " << std::fixed << std::setprecision(2) << tradeManager->calculatePnl() << std::endl; diff --git a/source/strategies/randomStrategy.cpp b/source/strategies/randomStrategy.cpp new file mode 100644 index 0000000..365c113 --- /dev/null +++ b/source/strategies/randomStrategy.cpp @@ -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) +// --------------------------------------- + +#include "strategies/randomStrategy.hpp" + +#include +#include +#include + +#include "tradeManager.hpp" + +// Member initialiser list (the bit after the `:`) runs in declaration +// order, not the order written here — same caveat called out in +// trade.hpp. The C# equivalent would be field initialisers plus a +// constructor body, but C++ prefers the initialiser list because it +// constructs members directly rather than default-then-assign. +RandomStrategy::RandomStrategy(const trading_definitions::Strategy& strategyConfig) + : config(strategyConfig), + rng(std::random_device{}()), // seed once from the OS entropy source + coin(0.5), + closeProb(1.0 / 200.0) {} // ~0.5% chance per tick — same odds as the + // old `std::rand() % 200 == 0` in operations.cpp + +std::optional RandomStrategy::decide(const PriceData& /*tick*/) { + return coin(rng) ? Direction::LONG : Direction::SHORT; +} + +void RandomStrategy::during(std::size_t /*tickValue*/, + const PriceData& price, + TradeManager& tradeManager) { + const auto& openTrades = tradeManager.getActiveTrades(); + if (openTrades.empty()) return; + if (!closeProb(rng)) return; + + // Two-phase pattern: collect IDs first, then close. Closing inside + // the range-for loop would invalidate the iterator we're walking, + // because `closeTrade` erases the entry from the underlying map. + // Same trap as mutating a C# Dictionary while iterating it. + std::vector idsToClose; + idsToClose.reserve(openTrades.size()); + for (const auto& [id, trade] : openTrades) { + idsToClose.push_back(id); + } + for (const auto& id : idsToClose) { + bool closed = tradeManager.closeTrade(id, price.bid); + std::cout << "Closed trade ID: " << id << " - " << (closed ? "success" : "failure") << std::endl; + } +}