Skip to content
Merged
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
15 changes: 15 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ jobs:
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install dependencies
run: sudo apt install -y libssl-dev libpq-dev
# Ubuntu's apt Boost (1.83 on 24.04) predates Boost.Redis, which was added
# in Boost 1.84, so <boost/redis.hpp> is absent. Install a newer Boost.
# Boost.Redis/Asio are header-only, so we only need the headers plus the
# CMake package config — building just Boost.System generates both quickly.
- name: Install Boost >= 1.84 (for Boost.Redis)
run: |
BOOST_VERSION=1.86.0
BOOST_DIR="boost_${BOOST_VERSION//./_}"
wget -q "https://archives.boost.io/release/${BOOST_VERSION}/source/${BOOST_DIR}.tar.gz"
tar xzf "${BOOST_DIR}.tar.gz"
cd "${BOOST_DIR}"
./bootstrap.sh
sudo ./b2 install --prefix=/usr/local --with-system -j"$(nproc)"
- name: Check compiler version, for debugging
run: |
g++ --version
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/scripts/brew.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ check_and_install() {

# Install packages if they don't exist
check_and_install postgresql@18 # Check and install PostgreSQL (which includes libpq)
check_and_install pkg-config # Check and install pkg-config
check_and_install pkg-config # Check and install pkg-config
check_and_install boost # Check and install pkg-config
13 changes: 13 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ if(APPLE)
target_include_directories(BacktestingEngineLib PRIVATE /opt/homebrew/opt/libomp/include)
endif()

# Boost (Homebrew) — Boost.Redis is header-only but its implementation is
# compiled into one TU via <boost/redis/src.hpp> (included in
# source/redisRunner.cpp). It pulls in Boost.Asio's SSL layer, which needs
# OpenSSL.
find_package(Boost REQUIRED CONFIG)
find_package(OpenSSL REQUIRED)
find_package(Threads REQUIRED)
target_link_libraries(BacktestingEngineLib PUBLIC
Boost::headers
OpenSSL::SSL
OpenSSL::Crypto
Threads::Threads)

add_subdirectory(external/boost-decimal)
target_link_libraries(BacktestingEngineLib PUBLIC Boost::decimal)

Expand Down
44 changes: 39 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ I'm extracting results and creating various graphs for trend analyses using SciP

![alt text](documents/images/random-indices-sp500-variable.svg)

*Read more results on https://mccaffers.com/randomly_trading/*
*Read more results on https://mccaffers.com/quantitative_analysis/randomly_trading/*

## Setup

This backtesting engine can pull tick data from local files or from a Postgres database. I'm using QuestDB.
This backtesting engine can pull tick data from local files or from a Postgres database (I'm using QuestDB). Strategy execution is dispatched via a Redis list called `strategy_queue`, with each entry a Base64-encoded JSON payload, the `load` subcommand enqueues strategies (LPUSH) and the `run` subcommand dequeues and executes them (RPOP). The default workflow expects a local `redis-server` listening on `127.0.0.1:6379`.

### Clone with submodules

Expand All @@ -40,9 +40,22 @@ For OpenSuse: zypper in postgresql-devel
For ArchLinux: pacman -S postgresql-libs
```

### Install Boost, OpenSSL, and Redis

Boost.Redis is header-only but its single translation unit (compiled via `<boost/redis/src.hpp>` from `source/redisRunner.cpp`) pulls in Boost.Asio's SSL layer, so OpenSSL is a transitive requirement. A local `redis-server` on `127.0.0.1:6379` is also needed for the default `load`/`run` workflow.

```
For Mac Homebrew: brew install boost openssl redis
For Ubuntu/Debian systems: sudo apt-get install libboost-all-dev libssl-dev redis-server
```

The canonical CI prerequisite list lives in `.github/workflows/scripts/brew.sh` (`postgresql`, `pkg-config`, `boost`).

![alt text](documents/flow.png)

### 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:
`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:

```
bash ./scripts/build_dep.sh
Expand Down Expand Up @@ -76,15 +89,36 @@ Xcode - Library Path

### Run via terminal

`bash ./scripts/run.sh`
`bash ./scripts/run.sh` builds the project, then, if `redis-cli ping` reaches a local Redis, enqueues an inline JSON strategy via `load` and executes it via `run localhost`. If Redis is unreachable the script prints a message and exits cleanly (see `scripts/run.sh:22-25`), so first-time users without Redis still get a clear signal.

The `BacktestingEngine` binary exposes a subcommand CLI:

```
BacktestingEngine load <path> [path...]
Read each file as raw JSON, Base64-encode it, and LPUSH onto the Redis
`strategy_queue` list.

BacktestingEngine run <questdb-host>
RPOP one Base64-encoded strategy from `strategy_queue` and execute it
against the supplied QuestDB host.

BacktestingEngine run <questdb-host> <base64-config>
Decode the supplied Base64 strategy and execute it directly, bypassing
Redis.

BacktestingEngine -h | --help
Show usage.
```

Defaults are `127.0.0.1:6379` for the Redis endpoint and `strategy_queue` for the list key (see `include/redisRunner.hpp` and `include/redisLoader.hpp`).

### Run tests via terminal

`bash ./scripts/test.sh`

### Contributing

This is an active solo experiment, so I'm not accepting pull requests right now but please fork freely and use [GitHub Issues](https://github.com/mccaffers/backtesting-engine-cpp/issues) for bugs, questions, and ideas. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
This is an active solo experiment, so I'm not accepting pull requests right now, but please fork freely and use [GitHub Issues](https://github.com/mccaffers/backtesting-engine-cpp/issues) for bugs, questions, and ideas. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.

### License
[MIT](https://choosealicense.com/licenses/mit/)
56 changes: 56 additions & 0 deletions backtesting-engine-cpp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
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 */; };
942EC5622FBEF94700CCBB5D /* redisConnection.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942EC5612FBEF94700CCBB5D /* redisConnection.cpp */; };
942EC5632FBEF94700CCBB5D /* redisConnection.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942EC5612FBEF94700CCBB5D /* redisConnection.cpp */; };
942EC5672FBEF95000CCBB5D /* backtestRunner.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942EC5642FBEF95000CCBB5D /* backtestRunner.cpp */; };
942EC5682FBEF95000CCBB5D /* redisLoader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942EC5652FBEF95000CCBB5D /* redisLoader.cpp */; };
942EC5692FBEF95000CCBB5D /* redisRunner.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942EC5662FBEF95000CCBB5D /* redisRunner.cpp */; };
942EC56A2FBEF95000CCBB5D /* backtestRunner.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942EC5642FBEF95000CCBB5D /* backtestRunner.cpp */; };
942EC56B2FBEF95000CCBB5D /* redisLoader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942EC5652FBEF95000CCBB5D /* redisLoader.cpp */; };
942EC56C2FBEF95000CCBB5D /* redisRunner.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942EC5662FBEF95000CCBB5D /* redisRunner.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 */; };
Expand Down Expand Up @@ -70,6 +78,14 @@
94280BA12D2FC00200F1CF56 /* base64.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = base64.hpp; sourceTree = "<group>"; };
94280BA22D2FC00200F1CF56 /* base64.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = base64.cpp; sourceTree = "<group>"; };
942966D82D48E84A00532862 /* priceData.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = priceData.hpp; sourceTree = "<group>"; };
942EC55D2FBEF92F00CCBB5D /* redisConnection.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = redisConnection.hpp; sourceTree = "<group>"; };
942EC55E2FBEF93A00CCBB5D /* backtestRunner.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = backtestRunner.hpp; sourceTree = "<group>"; };
942EC55F2FBEF93A00CCBB5D /* redisLoader.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = redisLoader.hpp; sourceTree = "<group>"; };
942EC5602FBEF93A00CCBB5D /* redisRunner.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = redisRunner.hpp; sourceTree = "<group>"; };
942EC5612FBEF94700CCBB5D /* redisConnection.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = redisConnection.cpp; sourceTree = "<group>"; };
942EC5642FBEF95000CCBB5D /* backtestRunner.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = backtestRunner.cpp; sourceTree = "<group>"; };
942EC5652FBEF95000CCBB5D /* redisLoader.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = redisLoader.cpp; sourceTree = "<group>"; };
942EC5662FBEF95000CCBB5D /* redisRunner.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = redisRunner.cpp; sourceTree = "<group>"; };
943398222D57E52900287A2D /* jsonParser.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = jsonParser.hpp; sourceTree = "<group>"; };
943398232D57E53400287A2D /* jsonParser.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = jsonParser.cpp; sourceTree = "<group>"; };
943398262D57E54000287A2D /* jsonParser.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = jsonParser.mm; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1309,6 +1325,7 @@
94280BA72D2FC29F00F1CF56 /* utilities */ = {
isa = PBXGroup;
children = (
942EC5612FBEF94700CCBB5D /* redisConnection.cpp */,
94280BA22D2FC00200F1CF56 /* base64.cpp */,
943398232D57E53400287A2D /* jsonParser.cpp */,
);
Expand Down Expand Up @@ -1403,6 +1420,9 @@
9470B5A22C8C5AD0007D9CC6 /* source */ = {
isa = PBXGroup;
children = (
942EC5642FBEF95000CCBB5D /* backtestRunner.cpp */,
942EC5652FBEF95000CCBB5D /* redisLoader.cpp */,
942EC5662FBEF95000CCBB5D /* redisRunner.cpp */,
94D6010F2FA9CD700066F51A /* strategies */,
94674B8C2D533E7800973137 /* models */,
94674B862D533B4000973137 /* trading */,
Expand Down Expand Up @@ -1432,6 +1452,7 @@
94B8C7932D3D770800E17EB6 /* utilities */ = {
isa = PBXGroup;
children = (
942EC55D2FBEF92F00CCBB5D /* redisConnection.hpp */,
94C331A02FA899A8006BD690 /* decimal_json.hpp */,
94280BA12D2FC00200F1CF56 /* base64.hpp */,
);
Expand Down Expand Up @@ -3530,6 +3551,9 @@
94DE4F772C8C3E7C00FE48FF /* include */ = {
isa = PBXGroup;
children = (
942EC55E2FBEF93A00CCBB5D /* backtestRunner.hpp */,
942EC55F2FBEF93A00CCBB5D /* redisLoader.hpp */,
942EC5602FBEF93A00CCBB5D /* redisRunner.hpp */,
94D601132FA9CD890066F51A /* strategies */,
94674B842D533B2F00973137 /* trading */,
942966D72D48E84100532862 /* models */,
Expand Down Expand Up @@ -3642,12 +3666,16 @@
941408AE2D59F93F000ED1F9 /* sqlManager.cpp in Sources */,
9470B5A42C8C5AD0007D9CC6 /* main.cpp in Sources */,
943398252D57E53400287A2D /* jsonParser.cpp in Sources */,
942EC56A2FBEF95000CCBB5D /* backtestRunner.cpp in Sources */,
942EC56B2FBEF95000CCBB5D /* redisLoader.cpp in Sources */,
942EC56C2FBEF95000CCBB5D /* redisRunner.cpp in Sources */,
94280BA32D2FC00200F1CF56 /* base64.cpp in Sources */,
94674B8E2D533E7800973137 /* trade.cpp in Sources */,
941B549B2D3BBADE00E3BF64 /* trading_definitions_json.cpp in Sources */,
946EFF7E2FB9F44E008D9647 /* reporting.cpp in Sources */,
94674B872D533B4000973137 /* tradeManager.cpp in Sources */,
94CD8BA02D2E8CE500041BBA /* databaseConnection.cpp in Sources */,
942EC5622FBEF94700CCBB5D /* redisConnection.cpp in Sources */,
940A61132C92CE210083FEB8 /* configManager.cpp in Sources */,
94724A842F8B92C10029B940 /* operations.cpp in Sources */,
940A61172C92CE960083FEB8 /* serviceA.cpp in Sources */,
Expand All @@ -3662,7 +3690,11 @@
94CD8BA12D2E8CE500041BBA /* databaseConnection.cpp in Sources */,
941408AF2D59F93F000ED1F9 /* sqlManager.cpp in Sources */,
9464E5F12FA7467200D82BAD /* symbolScale.mm in Sources */,
942EC5632FBEF94700CCBB5D /* redisConnection.cpp in Sources */,
943398242D57E53400287A2D /* jsonParser.cpp in Sources */,
942EC5672FBEF95000CCBB5D /* backtestRunner.cpp in Sources */,
942EC5682FBEF95000CCBB5D /* redisLoader.cpp in Sources */,
942EC5692FBEF95000CCBB5D /* redisRunner.cpp in Sources */,
94280BA42D2FC00200F1CF56 /* base64.cpp in Sources */,
94674B8D2D533E7800973137 /* trade.cpp in Sources */,
941B549A2D3BBADE00E3BF64 /* trading_definitions_json.cpp in Sources */,
Expand Down Expand Up @@ -3809,16 +3841,22 @@
"\"$(SRCROOT)/external/libpqxx/build/include\"",
"\"$(SRCROOT)/external/\"",
"\"$(SRCROOT)/external/boost-decimal/include\"",
/opt/homebrew/include,
/opt/homebrew/opt/openssl/include,
);
INCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES = "";
LIBRARY_SEARCH_PATHS = (
"\"$(SRCROOT)/external/libpqxx/build/src\"",
"/opt/homebrew/opt/postgresql@18/lib/postgresql",
/opt/homebrew/opt/openssl/lib,
);
MACOSX_DEPLOYMENT_TARGET = 26.0;
OTHER_LDFLAGS = (
"-lpq",
"-lpqxx",
"-lssl",
"-lcrypto",
"-pthread",
);
OTHER_LIBTOOLFLAGS = "";
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -3838,16 +3876,22 @@
"\"$(SRCROOT)/external/libpqxx/build/include\"",
"\"$(SRCROOT)/external/\"",
"\"$(SRCROOT)/external/boost-decimal/include\"",
/opt/homebrew/include,
/opt/homebrew/opt/openssl/include,
);
INCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES = "";
LIBRARY_SEARCH_PATHS = (
"\"$(SRCROOT)/external/libpqxx/build/src\"",
"/opt/homebrew/opt/postgresql@18/lib/postgresql",
/opt/homebrew/opt/openssl/lib,
);
MACOSX_DEPLOYMENT_TARGET = 26.0;
OTHER_LDFLAGS = (
"-lpq",
"-lpqxx",
"-lssl",
"-lcrypto",
"-pthread",
);
OTHER_LIBTOOLFLAGS = "";
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -3868,16 +3912,22 @@
"\"$(SRCROOT)/external/libpqxx/include/pqxx/internal\"",
"\"$(SRCROOT)/external/libpqxx/build/include\"",
"\"$(SRCROOT)/external/boost-decimal/include\"",
/opt/homebrew/include,
/opt/homebrew/opt/openssl/include,
);
LIBRARY_SEARCH_PATHS = (
"\"$(SRCROOT)/external/libpqxx/build/src\"",
"/opt/homebrew/opt/postgresql@18/lib/postgresql",
/opt/homebrew/opt/openssl/lib,
);
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"-lpq",
"-lpqxx",
"-lssl",
"-lcrypto",
"-pthread",
);
PRODUCT_BUNDLE_IDENTIFIER = com.mccaffers.tests;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -3898,16 +3948,22 @@
"\"$(SRCROOT)/external/libpqxx/include/pqxx/internal\"",
"\"$(SRCROOT)/external/libpqxx/build/include\"",
"\"$(SRCROOT)/external/boost-decimal/include\"",
/opt/homebrew/include,
/opt/homebrew/opt/openssl/include,
);
LIBRARY_SEARCH_PATHS = (
"\"$(SRCROOT)/external/libpqxx/build/src\"",
"/opt/homebrew/opt/postgresql@18/lib/postgresql",
/opt/homebrew/opt/openssl/lib,
);
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"-lpq",
"-lpqxx",
"-lssl",
"-lcrypto",
"-pthread",
);
PRODUCT_BUNDLE_IDENTIFIER = com.mccaffers.tests;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down
Binary file added documents/flow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions include/backtestRunner.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// 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 <string>
#include "trading_definitions/configuration.hpp"

int runBacktest(const std::string& questdbHost,
const trading_definitions::Configuration& config);
23 changes: 23 additions & 0 deletions include/redisLoader.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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 <string>

// LPUSH pairs with RedisRunner's RPOP so consumers observe FIFO ordering.
class RedisLoader {
public:
static int load(const std::string& rawJson,
const std::string& redisHost = "127.0.0.1",
int redisPort = 6379,
const std::string& queueKey = "strategy_queue");

static int loadPayload(const std::string& redisHost,
int redisPort,
const std::string& queueKey,
const std::string& rawJson);
};
16 changes: 16 additions & 0 deletions include/redisRunner.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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 <string>

class RedisRunner {
public:
static int run(const std::string& questdbHost,
const std::string& redisHost = "127.0.0.1",
int redisPort = 6379,
const std::string& queueKey = "strategy_queue");
};
28 changes: 28 additions & 0 deletions include/strategies/strategyErrors.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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 <stdexcept>
#include <string>
#include <utility>

// Thrown by the strategy factory when the configured strategy name does not
// match any known strategy. Derives from std::runtime_error so existing
// catch(const std::runtime_error&) / catch(const std::exception&) sites still
// match, while giving new callers a dedicated type to discriminate a
// configuration / domain error from any other runtime failure.
class UnknownStrategyError : public std::runtime_error {
public:
explicit UnknownStrategyError(std::string name)
: std::runtime_error("Unknown strategy: '" + name + "'"),
name_(std::move(name)) {}

const std::string& name() const noexcept { return name_; }

private:
std::string name_;
};
Loading
Loading