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
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
dsh{
"files.associations": {
"unordered_map": "cpp",
"__bit_reference": "cpp",
Expand Down
12 changes: 11 additions & 1 deletion backtesting-engine-cpp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
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 */; };
946EFF7E2FB9F44E008D9647 /* reporting.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 946EFF7D2FB9F44E008D9647 /* reporting.cpp */; };
946EFF7F2FB9F44E008D9647 /* reporting.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 946EFF7D2FB9F44E008D9647 /* reporting.cpp */; };
9470B5A42C8C5AD0007D9CC6 /* main.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9470B5A32C8C5AD0007D9CC6 /* main.cpp */; };
9470B5B62C8C5BFD007D9CC6 /* main.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9470B5A32C8C5AD0007D9CC6 /* main.cpp */; };
94724A832F8B92C10029B940 /* operations.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94724A822F8B92C10029B940 /* operations.cpp */; };
Expand Down Expand Up @@ -84,11 +86,14 @@
9464E5F02FA7467200D82BAD /* symbolScale.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = symbolScale.mm; sourceTree = "<group>"; };
94674B822D533B1D00973137 /* trade.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = trade.hpp; sourceTree = "<group>"; };
94674B832D533B2F00973137 /* tradeManager.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = tradeManager.hpp; sourceTree = "<group>"; };
94674BA02D533B2F00973137 /* exitRules.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = exitRules.hpp; sourceTree = "<group>"; };
94674B852D533B4000973137 /* tradeManager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = tradeManager.cpp; sourceTree = "<group>"; };
94674B892D533BDA00973137 /* tradeManager.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = tradeManager.mm; sourceTree = "<group>"; };
94674B8B2D533E7800973137 /* trade.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = trade.cpp; sourceTree = "<group>"; };
94674BA02D533B2F00973137 /* exitRules.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = exitRules.hpp; sourceTree = "<group>"; };
94674BA12F8B92C10029B940 /* reviewStopAndLimit.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = reviewStopAndLimit.hpp; sourceTree = "<group>"; };
94685CCE2D384A8B00863D04 /* json.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = json.hpp; sourceTree = "<group>"; };
946EFF7D2FB9F44E008D9647 /* reporting.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = reporting.cpp; sourceTree = "<group>"; };
946EFF802FB9F457008D9647 /* reporting.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = reporting.hpp; sourceTree = "<group>"; };
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 = "<group>"; };
9470B5AC2C8C5B99007D9CC6 /* tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -1362,15 +1367,18 @@
94674B842D533B2F00973137 /* trading */ = {
isa = PBXGroup;
children = (
946EFF802FB9F457008D9647 /* reporting.hpp */,
94674B832D533B2F00973137 /* tradeManager.hpp */,
94674BA02D533B2F00973137 /* exitRules.hpp */,
94674BA12F8B92C10029B940 /* reviewStopAndLimit.hpp */,
);
path = trading;
sourceTree = "<group>";
};
94674B862D533B4000973137 /* trading */ = {
isa = PBXGroup;
children = (
946EFF7D2FB9F44E008D9647 /* reporting.cpp */,
94674B852D533B4000973137 /* tradeManager.cpp */,
);
path = trading;
Expand Down Expand Up @@ -3637,6 +3645,7 @@
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 */,
940A61132C92CE210083FEB8 /* configManager.cpp in Sources */,
Expand All @@ -3662,6 +3671,7 @@
94724A832F8B92C10029B940 /* operations.cpp in Sources */,
940A61182C92CE960083FEB8 /* serviceA.cpp in Sources */,
94674B882D533B4000973137 /* tradeManager.cpp in Sources */,
946EFF7F2FB9F44E008D9647 /* reporting.cpp in Sources */,
943398272D57E54000287A2D /* jsonParser.mm in Sources */,
9470B5B62C8C5BFD007D9CC6 /* main.cpp in Sources */,
94364CB62D416D8D00F35B55 /* db.mm in Sources */,
Expand Down
14 changes: 14 additions & 0 deletions include/trading/reporting.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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 "tradeManager.hpp"

class Reporting {

public:
static void summarise(const TradeManager& tradeManager);
};
43 changes: 43 additions & 0 deletions include/trading/reviewStopAndLimit.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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 <utility>
#include <vector>
#include <boost/decimal.hpp>
#include "tradeManager.hpp"
#include "exitRules.hpp"
#include "models/priceData.hpp"

namespace trading {

// Walk every active trade, close any whose SL/TP has been hit on this
// tick. Two-phase to avoid invalidating the map iterator while erasing.
//
// Symbol-aware: a tick for one instrument must never trigger an exit on
// a trade opened for a different instrument. The per-trade filter below
// is the production fix — `exit_rules::checkExit` is symbol-agnostic
// and would otherwise compare e.g. an AUSIDXAUD price (thousands)
// against a EURUSD stop level (~1.10) and spuriously close the trade.
inline void reviewStopAndLimit(TradeManager& tradeManager, const PriceData& tick) {
const auto& openTrades = tradeManager.getActiveTrades();
if (openTrades.empty()) return;

std::vector<std::pair<std::string, boost::decimal::decimal64_t>> toClose;
toClose.reserve(openTrades.size());
for (const auto& [id, trade] : openTrades) {
if (trade.symbol != tick.symbol) continue;
if (auto exitPrice = trading::exit_rules::checkExit(trade, tick)) {
toClose.emplace_back(id, *exitPrice);
}
}
for (const auto& [id, exitPrice] : toClose) {
tradeManager.closeTrade(id, exitPrice, tick);
}
}

} // namespace trading
2 changes: 2 additions & 0 deletions include/trading/tradeManager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#pragma once
#include <unordered_map>
#include <string_view>
#include <vector>
#include <memory>
#include <boost/decimal.hpp>
Expand All @@ -25,6 +26,7 @@ class TradeManager {
boost::decimal::decimal64_t stopDistancePips = boost::decimal::decimal64_t{0},
boost::decimal::decimal64_t limitDistancePips = boost::decimal::decimal64_t{0});
size_t reviewAccount() const;
bool hasActiveTradeForSymbol(std::string_view symbol) const;
bool closeTrade(const std::string& tradeId,
boost::decimal::decimal64_t closePrice,
const PriceData& tick);
Expand Down
4 changes: 4 additions & 0 deletions scripts/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

git submodule update --init --recursive

if [ -z "$(ls -A ./external/boost-decimal 2>/dev/null)" ] || [ -z "$(ls -A ./external/libpqxx 2>/dev/null)" ]; then
./scripts/build_dep.sh
fi

BUILD_DIR="build"
# Variables
EXECUTABLE_NAME="BacktestingEngine"
Expand Down
3 changes: 1 addition & 2 deletions scripts/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ else
exit 1
fi

# "SYMBOLS": "EURUSD,AUSIDXAUD",
json='{
"RUN_ID": "UNIQUE_IDENTIFIER",
"SYMBOLS": "EURUSD",
"SYMBOLS": "EURUSD,AUDUSD",
"LAST_MONTHS": 2,
"STRATEGY": {
"UUID": "",
Expand Down
86 changes: 5 additions & 81 deletions source/operations.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,18 @@
// ---------------------------------------

#include "operations.hpp"
// std headers
#include <iostream>
#include <vector>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>
#include <iomanip>
#include <cstdio>
#include <ctime>
#include <boost/decimal.hpp>
#include "tradeManager.hpp"
#include "exitRules.hpp"
#include "models/symbolScale.hpp"
#include "reviewStopAndLimit.hpp"
#include "reporting.hpp"
#include "strategies/strategy.hpp"
#include "strategies/randomStrategy.hpp"

namespace {

// Walk every active trade, close any whose SL/TP has been hit on this
// tick. Two-phase to avoid invalidating the map iterator while erasing.
void reviewStopAndLimit(TradeManager& tradeManager, const PriceData& tick) {
const auto& openTrades = tradeManager.getActiveTrades();
if (openTrades.empty()) return;

std::vector<std::pair<std::string, boost::decimal::decimal64_t>> toClose;
toClose.reserve(openTrades.size());
for (const auto& [id, trade] : openTrades) {
if (auto exitPrice = trading::exit_rules::checkExit(trade, tick)) {
toClose.emplace_back(id, *exitPrice);
}
}
for (const auto& [id, exitPrice] : toClose) {
tradeManager.closeTrade(id, exitPrice, tick);
}
}

// Adding a new strategy means adding one branch here; nothing else in
// Operations needs to know about the concrete type.
std::unique_ptr<IStrategy>
Expand All @@ -69,13 +43,9 @@ void Operations::run(const std::vector<PriceData>& ticks,
// Close any trade whose stop-loss or take-profit fired on this tick
// before we consider opening a new one — otherwise an exit and an
// entry could race within the same tick.
reviewStopAndLimit(*tradeManager, tick);
trading::reviewStopAndLimit(*tradeManager, tick);

size_t openTrades = tradeManager->reviewAccount();

// only open a trade if there is zero
if (openTrades == 0) {
// optional is false
if (!tradeManager->hasActiveTradeForSymbol(tick.symbol)) {
if (auto signal = strategy->decide(tick)) {
tradeManager->openTrade(tick,
tradingVars.TRADING_SIZE,
Expand All @@ -92,51 +62,5 @@ void Operations::run(const std::vector<PriceData>& ticks,
strategy->during(tick, *tradeManager);
}

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

const auto& activeTrades = tradeManager->getActiveTrades();
const auto& closedTrades = tradeManager->getClosedTrades();

const std::size_t openedCount = activeTrades.size() + closedTrades.size();
const std::size_t closedCount = closedTrades.size();

std::size_t openedLong = 0;
std::size_t openedShort = 0;
for (const auto& [id, trade] : activeTrades) {
if (trade.direction == Direction::LONG) ++openedLong;
else ++openedShort;
}

std::size_t closedLong = 0;
std::size_t closedShort = 0;
std::size_t winners = 0;
std::size_t losers = 0;
std::size_t breakeven = 0;
boost::decimal::decimal64_t pnlSum{0};
const boost::decimal::decimal64_t zero{0};
for (const auto& trade : closedTrades) {
if (trade.direction == Direction::LONG) ++closedLong;
else ++closedShort;
if (trade.pnl > zero) ++winners;
else if (trade.pnl < zero) ++losers;
else ++breakeven;
pnlSum += trade.pnl;
}
openedLong += closedLong;
openedShort += closedShort;

std::cout << "Trades opened: " << openedCount
<< " (LONG: " << openedLong << ", SHORT: " << openedShort << ")" << std::endl;
std::cout << "Trades closed: " << closedCount
<< " (LONG: " << closedLong << ", SHORT: " << closedShort << ")" << std::endl;
std::cout << "Winners: " << winners
<< " Losers: " << losers
<< " Breakeven: " << breakeven << std::endl;
if (closedCount == 0) {
std::cout << "Average PnL per closed trade: n/a (0 closed)" << std::endl;
} else {
const auto avgPnl = pnlSum / boost::decimal::decimal64_t{static_cast<long long>(closedCount)};
std::cout << "Average PnL per closed trade: "
<< std::fixed << std::setprecision(2) << avgPnl << std::endl;
}
Reporting::summarise(*tradeManager);
}
62 changes: 62 additions & 0 deletions source/trading/reporting.cpp
Original file line number Diff line number Diff line change
@@ -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)
// ---------------------------------------

#include "reporting.hpp"
#include <iostream>
#include <iomanip>
#include <cstddef>
#include <boost/decimal.hpp>
#include "trade.hpp"

void Reporting::summarise(const TradeManager& tradeManager) {
std::cout << "Final PnL: " << std::fixed << std::setprecision(2) << tradeManager.calculatePnl() << std::endl;

const auto& activeTrades = tradeManager.getActiveTrades();
const auto& closedTrades = tradeManager.getClosedTrades();

const std::size_t openedCount = activeTrades.size() + closedTrades.size();
const std::size_t closedCount = closedTrades.size();

std::size_t openedLong = 0;
std::size_t openedShort = 0;
for (const auto& [id, trade] : activeTrades) {
if (trade.direction == Direction::LONG) ++openedLong;
else ++openedShort;
}

std::size_t closedLong = 0;
std::size_t closedShort = 0;
std::size_t winners = 0;
std::size_t losers = 0;
std::size_t breakeven = 0;
boost::decimal::decimal64_t pnlSum{0};
const boost::decimal::decimal64_t zero{0};
for (const auto& trade : closedTrades) {
if (trade.direction == Direction::LONG) ++closedLong;
else ++closedShort;
if (trade.pnl > zero) ++winners;
else if (trade.pnl < zero) ++losers;
else ++breakeven;
pnlSum += trade.pnl;
}
openedLong += closedLong;
openedShort += closedShort;

std::cout << "Trades opened: " << openedCount
<< " (LONG: " << openedLong << ", SHORT: " << openedShort << ")" << std::endl;
std::cout << "Trades closed: " << closedCount
<< " (LONG: " << closedLong << ", SHORT: " << closedShort << ")" << std::endl;
std::cout << "Winners: " << winners
<< " Losers: " << losers
<< " Breakeven: " << breakeven << std::endl;
if (closedCount == 0) {
std::cout << "Average PnL per closed trade: n/a (0 closed)" << std::endl;
} else {
const auto avgPnl = pnlSum / boost::decimal::decimal64_t{static_cast<long long>(closedCount)};
std::cout << "Average PnL per closed trade: "
<< std::fixed << std::setprecision(2) << avgPnl << std::endl;
}
}
8 changes: 8 additions & 0 deletions source/trading/tradeManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// ---------------------------------------

#include "tradeManager.hpp"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <ctime>
Expand Down Expand Up @@ -40,6 +41,13 @@
return activeTrades.size();
}

bool TradeManager::hasActiveTradeForSymbol(std::string_view symbol) const {
return std::any_of(activeTrades.begin(), activeTrades.end(),
[symbol](const auto& pair) {
return pair.second.symbol == symbol;
});

Check warning on line 48 in source/trading/tradeManager.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace with the version of "std::ranges::any_of" that takes a range.

See more on https://sonarcloud.io/project/issues?id=mccaffers_backtesting-engine-cpp&issues=AZ42Fj6p2fnSISr3vRxC&open=AZ42Fj6p2fnSISr3vRxC&pullRequest=34
}

bool TradeManager::closeTrade(const std::string& tradeId,
boost::decimal::decimal64_t closePrice,
const PriceData& tick) {
Expand Down
Loading
Loading