Skip to content

Commit 9b586c4

Browse files
committed
creating a strategy interface
1 parent e4c07aa commit 9b586c4

6 files changed

Lines changed: 154 additions & 35 deletions

File tree

README.md

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Feel free to explore, but this code base is usuable at the moment.
88

99
I'm developing a high-performance C++ backtesting engine designed to analyze financial data and evaluate multiple trading strategies at scale.
1010

11-
[![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)
11+
[![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)
1212

1313
I'm extracting results and creating various graphs for trend analyses using SciPy for calculations and Plotly for visualization.
1414

@@ -20,7 +20,17 @@ I'm extracting results and creating various graphs for trend analyses using SciP
2020

2121
This backtesting engine can pull tick data from local files or from a Postgres database. I'm using QuestDB.
2222

23-
### Postgres Setup - Requires libpq-dev or its equivalent for your OS:
23+
### Clone with submodules
24+
25+
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:
26+
27+
```
28+
git submodule update --init --recursive
29+
```
30+
31+
`scripts/build_dep.sh` does this for you on first run.
32+
33+
### Install libpq (required by libpqxx)
2434

2535
```
2636
For Ubuntu/Debian systems: sudo apt-get install libpq-dev
@@ -30,15 +40,12 @@ For OpenSuse: zypper in postgresql-devel
3040
For ArchLinux: pacman -S postgresql-libs
3141
```
3242

33-
### Postgres Setup (using C++20)
43+
### Build dependencies
44+
45+
`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:
3446

3547
```
36-
cd ./external/libpqxx
37-
mkdir -p build
38-
cd ./build
39-
cmake ..
40-
./configure CXXFLAGS="-std=c++20 -O3"
41-
make
48+
bash ./scripts/build_dep.sh
4249
```
4350

4451
Xcode - Link Binary with Libraries (Source & Test)
@@ -63,17 +70,17 @@ Xcode - Library Path
6370
"/opt/homebrew/Cellar/postgresql@14/14.15/lib/postgresql@14"
6471
```
6572

66-
### Test the build
73+
### Build the project
6774

68-
`sh ./scripts/build.sh`
75+
`bash ./scripts/build.sh`
6976

7077
### Run via terminal
7178

72-
`sh ./scripts/run.sh`
79+
`bash ./scripts/run.sh`
7380

74-
### Run via test via terminal
81+
### Run tests via terminal
7582

76-
`sh ./scripts/test.sh`
83+
`bash ./scripts/test.sh`
7784

7885
### License
7986
[MIT](https://choosealicense.com/licenses/mit/)

backtesting-engine-cpp.xcodeproj/project.pbxproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
/* End PBXCopyFilesBuildPhase section */
5151

5252
/* Begin PBXFileReference section */
53+
9409A61B2FAA6411002C30FF /* strategy.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = strategy.hpp; sourceTree = "<group>"; };
5354
940A61112C92CE210083FEB8 /* configManager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = configManager.cpp; sourceTree = "<group>"; };
5455
940A61122C92CE210083FEB8 /* configManager.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = configManager.hpp; sourceTree = "<group>"; };
5556
940A61152C92CE960083FEB8 /* serviceA.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = serviceA.cpp; sourceTree = "<group>"; };
@@ -3510,6 +3511,7 @@
35103511
94D601132FA9CD890066F51A /* strategies */ = {
35113512
isa = PBXGroup;
35123513
children = (
3514+
9409A61B2FAA6411002C30FF /* strategy.hpp */,
35133515
94D601122FA9CD890066F51A /* randomStrategy.hpp */,
35143516
);
35153517
path = strategies;

include/strategies/randomStrategy.hpp

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@
55
// ---------------------------------------
66

77
#pragma once
8+
#include <cstddef>
89
#include <optional>
910
#include <random>
1011
#include "models/priceData.hpp"
1112
#include "models/trade.hpp" // for Direction enum
13+
#include "strategies/strategy.hpp" // IStrategy base class
1214
#include "trading_definitions/strategy.hpp"
1315

16+
class TradeManager; // forward declared in strategy.hpp; redeclaring is harmless
17+
1418
// A trivial strategy: on every tick it flips a fair coin and returns
15-
// LONG or SHORT. Intended as scaffolding for the strategy interface,
16-
// not as a real trading approach.
19+
// LONG or SHORT. While trades are open it has a small per-tick chance
20+
// of closing every active position. Intended as scaffolding for the
21+
// strategy interface, not as a real trading approach.
1722
//
1823
// C# parallels for readers from a C# background:
1924
// - `class` here is a value/owning type managed via stack or
@@ -24,7 +29,12 @@
2429
// - `std::mt19937` is the modern C++ RNG engine; it replaces the
2530
// globally-shared `std::rand()` used elsewhere in this codebase
2631
// and gives us the option of seeding for reproducible backtests.
27-
class RandomStrategy {
32+
// - `: public IStrategy` is C++ inheritance syntax. `public` means
33+
// the inheritance preserves access — outside code can use a
34+
// `RandomStrategy` anywhere an `IStrategy` is expected. The C#
35+
// equivalent is `: IStrategy`; C# has no concept of private
36+
// inheritance, so the `public` keyword has no analogue there.
37+
class RandomStrategy : public IStrategy {
2838
public:
2939
explicit RandomStrategy(const trading_definitions::Strategy& strategyConfig);
3040

@@ -37,10 +47,20 @@ class RandomStrategy {
3747
// Not const because the RNG engine mutates its internal state on
3848
// each call. The `tick` parameter is unused today but keeps the
3949
// interface stable for strategies that will look at price.
40-
std::optional<Direction> decide(const PriceData& tick);
50+
std::optional<Direction> decide(const PriceData& tick) override;
51+
52+
// Per-tick management hook. With a small probability per tick
53+
// (see `closeProb`), closes every currently-open trade at the
54+
// tick's bid price. `tradeManager` is passed by mutable reference
55+
// because the strategy needs to call mutating methods on it
56+
// (`closeTrade`); `getActiveTrades()` is still safely const.
57+
void during(std::size_t tickValue,
58+
const PriceData& price,
59+
TradeManager& tradeManager) override;
4160

4261
private:
4362
trading_definitions::Strategy config;
4463
std::mt19937 rng;
45-
std::bernoulli_distribution coin;
64+
std::bernoulli_distribution coin; // fair coin flip for entry direction
65+
std::bernoulli_distribution closeProb; // per-tick probability of closing all trades
4666
};

include/strategies/strategy.hpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Backtesting Engine in C++
2+
//
3+
// (c) 2026 Ryan McCaffery | https://mccaffers.com
4+
// This code is licensed under MIT license (see LICENSE.txt for details)
5+
// ---------------------------------------
6+
7+
#pragma once
8+
#include <cstddef>
9+
#include <optional>
10+
#include "models/priceData.hpp"
11+
#include "models/trade.hpp"
12+
13+
// Forward declaration. We only refer to `TradeManager` by reference in
14+
// this header, so the compiler doesn't need its full definition here —
15+
// just to know "it's a class". Pulling in the full header would drag
16+
// every TradeManager dependency into every strategy. C# doesn't really
17+
// have an equivalent because it resolves types at the assembly level
18+
// rather than per file, but in C++ this is a standard way to keep
19+
// header dependencies (and compile times) under control.
20+
class TradeManager;
21+
22+
// Abstract base class that every concrete strategy implements.
23+
//
24+
// C# parallels for readers from a C# background:
25+
// - This is the C++ equivalent of a C# `interface`. C++ has no
26+
// dedicated `interface` keyword, so you express it as a class
27+
// whose methods are all pure virtual (the `= 0` suffix). The
28+
// `I` prefix is borrowed from C# — C++ has no fixed convention,
29+
// but it's a useful hint because C++ classes routinely mix
30+
// virtual and concrete methods, so "is this an interface?" isn't
31+
// always obvious from the keyword alone.
32+
// - `virtual ~IStrategy() = default;` is mandatory. If you ever
33+
// delete a derived strategy through an `IStrategy*`, a non-virtual
34+
// destructor would skip the derived destructor and leak resources.
35+
// C# handles this for you; in C++ you opt in.
36+
// - `= 0` makes a method pure virtual, which makes the class
37+
// abstract — you cannot instantiate `IStrategy` directly, only
38+
// concrete subclasses. Same behaviour as a C# interface.
39+
// - Derived classes annotate their implementations with `override`
40+
// (see `RandomStrategy`). It's optional in C++ but catches
41+
// signature typos at compile time, exactly like C#'s `override`.
42+
class IStrategy {
43+
public:
44+
virtual ~IStrategy() = default;
45+
46+
// Entry signal. Returns `std::nullopt` to mean "no trade".
47+
// Non-const because some implementations (e.g. RandomStrategy)
48+
// mutate internal RNG state on each call.
49+
virtual std::optional<Direction> decide(const PriceData& tick) = 0;
50+
51+
// Called every tick. Receives the TradeManager by mutable
52+
// reference so strategies can both inspect open positions
53+
// (`tradeManager.getActiveTrades()`) and act on them
54+
// (`closeTrade`, future scale/adjust hooks). A reference signals
55+
// "borrow, don't own" — the strategy must not delete it. The C#
56+
// analogue is just passing the manager as a parameter; C# has no
57+
// distinction between reference and pointer so the by-ref nature
58+
// is implicit there.
59+
virtual void during(std::size_t tickValue,
60+
const PriceData& price,
61+
TradeManager& tradeManager) = 0;
62+
};

source/operations.cpp

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ void Operations::run(const std::vector<PriceData>& ticks,
2424
auto tradeManager = new TradeManager();
2525
RandomStrategy strategy(config.STRATEGY);
2626

27+
std::size_t tickIndex = 0;
2728
for (const auto& tick : ticks) {
2829

2930
size_t openTrades = tradeManager->reviewAccount();
3031

31-
// strategy invoke point: ask the strategy for a signal. If it
32-
// returns std::nullopt the strategy is saying "no trade".
32+
// only open a trade if there is zero
3333
if (openTrades == 0) {
34+
// optional is false
3435
if (auto signal = strategy.decide(tick)) {
3536
std::string tradeId = tradeManager->openTrade(tick, config.STRATEGY.TRADING_VARIABLES.TRADING_SIZE, *signal);
3637
std::cout << "Opened trade: " << tradeId << std::endl;
@@ -51,18 +52,15 @@ void Operations::run(const std::vector<PriceData>& ticks,
5152
}
5253
}
5354

54-
// strategy review point
55-
// randomly close trades every 200 ticks
56-
if (openTrades > 0 && (std::rand() % 200) == 0) { // NOSONAR(cpp:S2245) experimentation only, not security-sensitive
57-
std::vector<std::string> idsToClose;
58-
for (const auto& [id, trade] : tradeManager->getActiveTrades()) {
59-
idsToClose.push_back(id);
60-
}
61-
for (const auto& id : idsToClose) {
62-
bool closed = tradeManager->closeTrade(id, tick.bid);
63-
std::cout << "Closed trade ID: " << id << " - " << (closed ? "success" : "failure") << std::endl;
64-
}
65-
}
55+
// Strategy-driven management hook. The strategy itself decides
56+
// whether/when to close positions; the random strategy currently
57+
// closes every open trade with a small per-tick probability.
58+
// Dereferencing `tradeManager` (a raw pointer) yields a reference
59+
// — the parameter type is `TradeManager&`, so `*tradeManager`
60+
// is what we hand in.
61+
strategy.during(tickIndex, tick, *tradeManager);
62+
63+
++tickIndex;
6664
}
6765

6866
std::cout << "Final PnL: " << std::fixed << std::setprecision(2) << tradeManager->calculatePnl() << std::endl;

source/strategies/randomStrategy.cpp

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,46 @@
66

77
#include "strategies/randomStrategy.hpp"
88

9+
#include <iostream>
10+
#include <string>
11+
#include <vector>
12+
13+
#include "tradeManager.hpp"
14+
915
// Member initialiser list (the bit after the `:`) runs in declaration
1016
// order, not the order written here — same caveat called out in
1117
// trade.hpp. The C# equivalent would be field initialisers plus a
1218
// constructor body, but C++ prefers the initialiser list because it
1319
// constructs members directly rather than default-then-assign.
1420
RandomStrategy::RandomStrategy(const trading_definitions::Strategy& strategyConfig)
1521
: config(strategyConfig),
16-
rng(std::random_device{}()), // seed once from the OS entropy source
17-
coin(0.5) {}
22+
rng(std::random_device{}()), // seed once from the OS entropy source
23+
coin(0.5),
24+
closeProb(1.0 / 200.0) {} // ~0.5% chance per tick — same odds as the
25+
// old `std::rand() % 200 == 0` in operations.cpp
1826

1927
std::optional<Direction> RandomStrategy::decide(const PriceData& /*tick*/) {
2028
return coin(rng) ? Direction::LONG : Direction::SHORT;
2129
}
30+
31+
void RandomStrategy::during(std::size_t /*tickValue*/,
32+
const PriceData& price,
33+
TradeManager& tradeManager) {
34+
const auto& openTrades = tradeManager.getActiveTrades();
35+
if (openTrades.empty()) return;
36+
if (!closeProb(rng)) return;
37+
38+
// Two-phase pattern: collect IDs first, then close. Closing inside
39+
// the range-for loop would invalidate the iterator we're walking,
40+
// because `closeTrade` erases the entry from the underlying map.
41+
// Same trap as mutating a C# Dictionary while iterating it.
42+
std::vector<std::string> idsToClose;
43+
idsToClose.reserve(openTrades.size());
44+
for (const auto& [id, trade] : openTrades) {
45+
idsToClose.push_back(id);
46+
}
47+
for (const auto& id : idsToClose) {
48+
bool closed = tradeManager.closeTrade(id, price.bid);
49+
std::cout << "Closed trade ID: " << id << " - " << (closed ? "success" : "failure") << std::endl;
50+
}
51+
}

0 commit comments

Comments
 (0)