Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 61 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -59,4 +65,57 @@ jobs:
with:
name: sonarqube-coverage
path: sonarqube-generic-coverage.xml
retention-days: 1
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
89 changes: 0 additions & 89 deletions .github/workflows/sonar.yml

This file was deleted.

1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down
35 changes: 21 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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/)
26 changes: 26 additions & 0 deletions backtesting-engine-cpp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -48,6 +50,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
9409A61B2FAA6411002C30FF /* strategy.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = strategy.hpp; sourceTree = "<group>"; };
940A61112C92CE210083FEB8 /* configManager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = configManager.cpp; sourceTree = "<group>"; };
940A61122C92CE210083FEB8 /* configManager.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = configManager.hpp; sourceTree = "<group>"; };
940A61152C92CE960083FEB8 /* serviceA.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = serviceA.cpp; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 = "<group>"; };
94CD8B9E2D2E8CE500041BBA /* databaseConnection.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = databaseConnection.hpp; sourceTree = "<group>"; };
94CD8B9F2D2E8CE500041BBA /* databaseConnection.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = databaseConnection.cpp; sourceTree = "<group>"; };
94D6010E2FA9CD700066F51A /* randomStrategy.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = randomStrategy.cpp; sourceTree = "<group>"; };
94D601122FA9CD890066F51A /* randomStrategy.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = randomStrategy.hpp; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -1388,6 +1393,7 @@
9470B5A22C8C5AD0007D9CC6 /* source */ = {
isa = PBXGroup;
children = (
94D6010F2FA9CD700066F51A /* strategies */,
94674B8C2D533E7800973137 /* models */,
94674B862D533B4000973137 /* trading */,
941B54982D3BBAD800E3BF64 /* trading_definitions */,
Expand Down Expand Up @@ -3494,9 +3500,27 @@
path = "../../../../../opt/homebrew/Cellar/postgresql@14/14.15/lib/postgresql@14/pgxs";
sourceTree = "<group>";
};
94D6010F2FA9CD700066F51A /* strategies */ = {
isa = PBXGroup;
children = (
94D6010E2FA9CD700066F51A /* randomStrategy.cpp */,
);
path = strategies;
sourceTree = "<group>";
};
94D601132FA9CD890066F51A /* strategies */ = {
isa = PBXGroup;
children = (
9409A61B2FAA6411002C30FF /* strategy.hpp */,
94D601122FA9CD890066F51A /* randomStrategy.hpp */,
);
path = strategies;
sourceTree = "<group>";
};
94DE4F772C8C3E7C00FE48FF /* include */ = {
isa = PBXGroup;
children = (
94D601132FA9CD890066F51A /* strategies */,
94674B842D533B2F00973137 /* trading */,
942966D72D48E84100532862 /* models */,
94B8C7932D3D770800E17EB6 /* utilities */,
Expand Down Expand Up @@ -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;
};
Expand All @@ -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 */,
Expand Down
66 changes: 66 additions & 0 deletions include/strategies/randomStrategy.hpp
Original file line number Diff line number Diff line change
@@ -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 <cstddef>
#include <optional>
#include <random>
#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<T>` is roughly C#'s `Nullable<T>` / `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<Direction> 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
};
Loading
Loading