Skip to content

Commit e5a82f6

Browse files
authored
Updated score to take into account the symbol inside the tick struct,… (#34)
* Updated score to take into account the symbol inside the tick struct, and lifted reporting out into it's own class * Updating references * Fixed the linked headers and source in xcode for unit tests
1 parent 955413d commit e5a82f6

11 files changed

Lines changed: 274 additions & 85 deletions

File tree

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{
1+
dsh{
22
"files.associations": {
33
"unordered_map": "cpp",
44
"__bit_reference": "cpp",

backtesting-engine-cpp.xcodeproj/project.pbxproj

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
94674B8A2D533BDA00973137 /* tradeManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 94674B892D533BDA00973137 /* tradeManager.mm */; };
2828
94674B8D2D533E7800973137 /* trade.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94674B8B2D533E7800973137 /* trade.cpp */; };
2929
94674B8E2D533E7800973137 /* trade.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94674B8B2D533E7800973137 /* trade.cpp */; };
30+
946EFF7E2FB9F44E008D9647 /* reporting.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 946EFF7D2FB9F44E008D9647 /* reporting.cpp */; };
31+
946EFF7F2FB9F44E008D9647 /* reporting.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 946EFF7D2FB9F44E008D9647 /* reporting.cpp */; };
3032
9470B5A42C8C5AD0007D9CC6 /* main.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9470B5A32C8C5AD0007D9CC6 /* main.cpp */; };
3133
9470B5B62C8C5BFD007D9CC6 /* main.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9470B5A32C8C5AD0007D9CC6 /* main.cpp */; };
3234
94724A832F8B92C10029B940 /* operations.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94724A822F8B92C10029B940 /* operations.cpp */; };
@@ -84,11 +86,14 @@
8486
9464E5F02FA7467200D82BAD /* symbolScale.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = symbolScale.mm; sourceTree = "<group>"; };
8587
94674B822D533B1D00973137 /* trade.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = trade.hpp; sourceTree = "<group>"; };
8688
94674B832D533B2F00973137 /* tradeManager.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = tradeManager.hpp; sourceTree = "<group>"; };
87-
94674BA02D533B2F00973137 /* exitRules.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = exitRules.hpp; sourceTree = "<group>"; };
8889
94674B852D533B4000973137 /* tradeManager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = tradeManager.cpp; sourceTree = "<group>"; };
8990
94674B892D533BDA00973137 /* tradeManager.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = tradeManager.mm; sourceTree = "<group>"; };
9091
94674B8B2D533E7800973137 /* trade.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = trade.cpp; sourceTree = "<group>"; };
92+
94674BA02D533B2F00973137 /* exitRules.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = exitRules.hpp; sourceTree = "<group>"; };
93+
94674BA12F8B92C10029B940 /* reviewStopAndLimit.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = reviewStopAndLimit.hpp; sourceTree = "<group>"; };
9194
94685CCE2D384A8B00863D04 /* json.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = json.hpp; sourceTree = "<group>"; };
95+
946EFF7D2FB9F44E008D9647 /* reporting.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = reporting.cpp; sourceTree = "<group>"; };
96+
946EFF802FB9F457008D9647 /* reporting.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = reporting.hpp; sourceTree = "<group>"; };
9297
9470B5A12C8C5AD0007D9CC6 /* source */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = source; sourceTree = BUILT_PRODUCTS_DIR; };
9398
9470B5A32C8C5AD0007D9CC6 /* main.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = main.cpp; sourceTree = "<group>"; };
9499
9470B5AC2C8C5B99007D9CC6 /* tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -1362,15 +1367,18 @@
13621367
94674B842D533B2F00973137 /* trading */ = {
13631368
isa = PBXGroup;
13641369
children = (
1370+
946EFF802FB9F457008D9647 /* reporting.hpp */,
13651371
94674B832D533B2F00973137 /* tradeManager.hpp */,
13661372
94674BA02D533B2F00973137 /* exitRules.hpp */,
1373+
94674BA12F8B92C10029B940 /* reviewStopAndLimit.hpp */,
13671374
);
13681375
path = trading;
13691376
sourceTree = "<group>";
13701377
};
13711378
94674B862D533B4000973137 /* trading */ = {
13721379
isa = PBXGroup;
13731380
children = (
1381+
946EFF7D2FB9F44E008D9647 /* reporting.cpp */,
13741382
94674B852D533B4000973137 /* tradeManager.cpp */,
13751383
);
13761384
path = trading;
@@ -3637,6 +3645,7 @@
36373645
94280BA32D2FC00200F1CF56 /* base64.cpp in Sources */,
36383646
94674B8E2D533E7800973137 /* trade.cpp in Sources */,
36393647
941B549B2D3BBADE00E3BF64 /* trading_definitions_json.cpp in Sources */,
3648+
946EFF7E2FB9F44E008D9647 /* reporting.cpp in Sources */,
36403649
94674B872D533B4000973137 /* tradeManager.cpp in Sources */,
36413650
94CD8BA02D2E8CE500041BBA /* databaseConnection.cpp in Sources */,
36423651
940A61132C92CE210083FEB8 /* configManager.cpp in Sources */,
@@ -3662,6 +3671,7 @@
36623671
94724A832F8B92C10029B940 /* operations.cpp in Sources */,
36633672
940A61182C92CE960083FEB8 /* serviceA.cpp in Sources */,
36643673
94674B882D533B4000973137 /* tradeManager.cpp in Sources */,
3674+
946EFF7F2FB9F44E008D9647 /* reporting.cpp in Sources */,
36653675
943398272D57E54000287A2D /* jsonParser.mm in Sources */,
36663676
9470B5B62C8C5BFD007D9CC6 /* main.cpp in Sources */,
36673677
94364CB62D416D8D00F35B55 /* db.mm in Sources */,

include/trading/reporting.hpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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 "tradeManager.hpp"
9+
10+
class Reporting {
11+
12+
public:
13+
static void summarise(const TradeManager& tradeManager);
14+
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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 <string>
9+
#include <utility>
10+
#include <vector>
11+
#include <boost/decimal.hpp>
12+
#include "tradeManager.hpp"
13+
#include "exitRules.hpp"
14+
#include "models/priceData.hpp"
15+
16+
namespace trading {
17+
18+
// Walk every active trade, close any whose SL/TP has been hit on this
19+
// tick. Two-phase to avoid invalidating the map iterator while erasing.
20+
//
21+
// Symbol-aware: a tick for one instrument must never trigger an exit on
22+
// a trade opened for a different instrument. The per-trade filter below
23+
// is the production fix — `exit_rules::checkExit` is symbol-agnostic
24+
// and would otherwise compare e.g. an AUSIDXAUD price (thousands)
25+
// against a EURUSD stop level (~1.10) and spuriously close the trade.
26+
inline void reviewStopAndLimit(TradeManager& tradeManager, const PriceData& tick) {
27+
const auto& openTrades = tradeManager.getActiveTrades();
28+
if (openTrades.empty()) return;
29+
30+
std::vector<std::pair<std::string, boost::decimal::decimal64_t>> toClose;
31+
toClose.reserve(openTrades.size());
32+
for (const auto& [id, trade] : openTrades) {
33+
if (trade.symbol != tick.symbol) continue;
34+
if (auto exitPrice = trading::exit_rules::checkExit(trade, tick)) {
35+
toClose.emplace_back(id, *exitPrice);
36+
}
37+
}
38+
for (const auto& [id, exitPrice] : toClose) {
39+
tradeManager.closeTrade(id, exitPrice, tick);
40+
}
41+
}
42+
43+
} // namespace trading

include/trading/tradeManager.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#pragma once
88
#include <unordered_map>
9+
#include <string_view>
910
#include <vector>
1011
#include <memory>
1112
#include <boost/decimal.hpp>
@@ -25,6 +26,7 @@ class TradeManager {
2526
boost::decimal::decimal64_t stopDistancePips = boost::decimal::decimal64_t{0},
2627
boost::decimal::decimal64_t limitDistancePips = boost::decimal::decimal64_t{0});
2728
size_t reviewAccount() const;
29+
bool hasActiveTradeForSymbol(std::string_view symbol) const;
2830
bool closeTrade(const std::string& tradeId,
2931
boost::decimal::decimal64_t closePrice,
3032
const PriceData& tick);

scripts/build.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
git submodule update --init --recursive
44

5+
if [ -z "$(ls -A ./external/boost-decimal 2>/dev/null)" ] || [ -z "$(ls -A ./external/libpqxx 2>/dev/null)" ]; then
6+
./scripts/build_dep.sh
7+
fi
8+
59
BUILD_DIR="build"
610
# Variables
711
EXECUTABLE_NAME="BacktestingEngine"

scripts/run.sh

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@ else
1919
exit 1
2020
fi
2121

22-
# "SYMBOLS": "EURUSD,AUSIDXAUD",
2322
json='{
2423
"RUN_ID": "UNIQUE_IDENTIFIER",
25-
"SYMBOLS": "EURUSD",
24+
"SYMBOLS": "EURUSD,AUDUSD",
2625
"LAST_MONTHS": 2,
2726
"STRATEGY": {
2827
"UUID": "",

source/operations.cpp

Lines changed: 5 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -5,44 +5,18 @@
55
// ---------------------------------------
66

77
#include "operations.hpp"
8-
// std headers
9-
#include <iostream>
108
#include <vector>
119
#include <memory>
12-
#include <optional>
1310
#include <stdexcept>
1411
#include <string>
15-
#include <utility>
16-
#include <iomanip>
17-
#include <cstdio>
18-
#include <ctime>
19-
#include <boost/decimal.hpp>
2012
#include "tradeManager.hpp"
21-
#include "exitRules.hpp"
22-
#include "models/symbolScale.hpp"
13+
#include "reviewStopAndLimit.hpp"
14+
#include "reporting.hpp"
2315
#include "strategies/strategy.hpp"
2416
#include "strategies/randomStrategy.hpp"
2517

2618
namespace {
2719

28-
// Walk every active trade, close any whose SL/TP has been hit on this
29-
// tick. Two-phase to avoid invalidating the map iterator while erasing.
30-
void reviewStopAndLimit(TradeManager& tradeManager, const PriceData& tick) {
31-
const auto& openTrades = tradeManager.getActiveTrades();
32-
if (openTrades.empty()) return;
33-
34-
std::vector<std::pair<std::string, boost::decimal::decimal64_t>> toClose;
35-
toClose.reserve(openTrades.size());
36-
for (const auto& [id, trade] : openTrades) {
37-
if (auto exitPrice = trading::exit_rules::checkExit(trade, tick)) {
38-
toClose.emplace_back(id, *exitPrice);
39-
}
40-
}
41-
for (const auto& [id, exitPrice] : toClose) {
42-
tradeManager.closeTrade(id, exitPrice, tick);
43-
}
44-
}
45-
4620
// Adding a new strategy means adding one branch here; nothing else in
4721
// Operations needs to know about the concrete type.
4822
std::unique_ptr<IStrategy>
@@ -69,13 +43,9 @@ void Operations::run(const std::vector<PriceData>& ticks,
6943
// Close any trade whose stop-loss or take-profit fired on this tick
7044
// before we consider opening a new one — otherwise an exit and an
7145
// entry could race within the same tick.
72-
reviewStopAndLimit(*tradeManager, tick);
46+
trading::reviewStopAndLimit(*tradeManager, tick);
7347

74-
size_t openTrades = tradeManager->reviewAccount();
75-
76-
// only open a trade if there is zero
77-
if (openTrades == 0) {
78-
// optional is false
48+
if (!tradeManager->hasActiveTradeForSymbol(tick.symbol)) {
7949
if (auto signal = strategy->decide(tick)) {
8050
tradeManager->openTrade(tick,
8151
tradingVars.TRADING_SIZE,
@@ -92,51 +62,5 @@ void Operations::run(const std::vector<PriceData>& ticks,
9262
strategy->during(tick, *tradeManager);
9363
}
9464

95-
std::cout << "Final PnL: " << std::fixed << std::setprecision(2) << tradeManager->calculatePnl() << std::endl;
96-
97-
const auto& activeTrades = tradeManager->getActiveTrades();
98-
const auto& closedTrades = tradeManager->getClosedTrades();
99-
100-
const std::size_t openedCount = activeTrades.size() + closedTrades.size();
101-
const std::size_t closedCount = closedTrades.size();
102-
103-
std::size_t openedLong = 0;
104-
std::size_t openedShort = 0;
105-
for (const auto& [id, trade] : activeTrades) {
106-
if (trade.direction == Direction::LONG) ++openedLong;
107-
else ++openedShort;
108-
}
109-
110-
std::size_t closedLong = 0;
111-
std::size_t closedShort = 0;
112-
std::size_t winners = 0;
113-
std::size_t losers = 0;
114-
std::size_t breakeven = 0;
115-
boost::decimal::decimal64_t pnlSum{0};
116-
const boost::decimal::decimal64_t zero{0};
117-
for (const auto& trade : closedTrades) {
118-
if (trade.direction == Direction::LONG) ++closedLong;
119-
else ++closedShort;
120-
if (trade.pnl > zero) ++winners;
121-
else if (trade.pnl < zero) ++losers;
122-
else ++breakeven;
123-
pnlSum += trade.pnl;
124-
}
125-
openedLong += closedLong;
126-
openedShort += closedShort;
127-
128-
std::cout << "Trades opened: " << openedCount
129-
<< " (LONG: " << openedLong << ", SHORT: " << openedShort << ")" << std::endl;
130-
std::cout << "Trades closed: " << closedCount
131-
<< " (LONG: " << closedLong << ", SHORT: " << closedShort << ")" << std::endl;
132-
std::cout << "Winners: " << winners
133-
<< " Losers: " << losers
134-
<< " Breakeven: " << breakeven << std::endl;
135-
if (closedCount == 0) {
136-
std::cout << "Average PnL per closed trade: n/a (0 closed)" << std::endl;
137-
} else {
138-
const auto avgPnl = pnlSum / boost::decimal::decimal64_t{static_cast<long long>(closedCount)};
139-
std::cout << "Average PnL per closed trade: "
140-
<< std::fixed << std::setprecision(2) << avgPnl << std::endl;
141-
}
65+
Reporting::summarise(*tradeManager);
14266
}

source/trading/reporting.cpp

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+
#include "reporting.hpp"
8+
#include <iostream>
9+
#include <iomanip>
10+
#include <cstddef>
11+
#include <boost/decimal.hpp>
12+
#include "trade.hpp"
13+
14+
void Reporting::summarise(const TradeManager& tradeManager) {
15+
std::cout << "Final PnL: " << std::fixed << std::setprecision(2) << tradeManager.calculatePnl() << std::endl;
16+
17+
const auto& activeTrades = tradeManager.getActiveTrades();
18+
const auto& closedTrades = tradeManager.getClosedTrades();
19+
20+
const std::size_t openedCount = activeTrades.size() + closedTrades.size();
21+
const std::size_t closedCount = closedTrades.size();
22+
23+
std::size_t openedLong = 0;
24+
std::size_t openedShort = 0;
25+
for (const auto& [id, trade] : activeTrades) {
26+
if (trade.direction == Direction::LONG) ++openedLong;
27+
else ++openedShort;
28+
}
29+
30+
std::size_t closedLong = 0;
31+
std::size_t closedShort = 0;
32+
std::size_t winners = 0;
33+
std::size_t losers = 0;
34+
std::size_t breakeven = 0;
35+
boost::decimal::decimal64_t pnlSum{0};
36+
const boost::decimal::decimal64_t zero{0};
37+
for (const auto& trade : closedTrades) {
38+
if (trade.direction == Direction::LONG) ++closedLong;
39+
else ++closedShort;
40+
if (trade.pnl > zero) ++winners;
41+
else if (trade.pnl < zero) ++losers;
42+
else ++breakeven;
43+
pnlSum += trade.pnl;
44+
}
45+
openedLong += closedLong;
46+
openedShort += closedShort;
47+
48+
std::cout << "Trades opened: " << openedCount
49+
<< " (LONG: " << openedLong << ", SHORT: " << openedShort << ")" << std::endl;
50+
std::cout << "Trades closed: " << closedCount
51+
<< " (LONG: " << closedLong << ", SHORT: " << closedShort << ")" << std::endl;
52+
std::cout << "Winners: " << winners
53+
<< " Losers: " << losers
54+
<< " Breakeven: " << breakeven << std::endl;
55+
if (closedCount == 0) {
56+
std::cout << "Average PnL per closed trade: n/a (0 closed)" << std::endl;
57+
} else {
58+
const auto avgPnl = pnlSum / boost::decimal::decimal64_t{static_cast<long long>(closedCount)};
59+
std::cout << "Average PnL per closed trade: "
60+
<< std::fixed << std::setprecision(2) << avgPnl << std::endl;
61+
}
62+
}

source/trading/tradeManager.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// ---------------------------------------
66

77
#include "tradeManager.hpp"
8+
#include <algorithm>
89
#include <atomic>
910
#include <chrono>
1011
#include <ctime>
@@ -40,6 +41,13 @@ size_t TradeManager::reviewAccount() const {
4041
return activeTrades.size();
4142
}
4243

44+
bool TradeManager::hasActiveTradeForSymbol(std::string_view symbol) const {
45+
return std::any_of(activeTrades.begin(), activeTrades.end(),
46+
[symbol](const auto& pair) {
47+
return pair.second.symbol == symbol;
48+
});
49+
}
50+
4351
bool TradeManager::closeTrade(const std::string& tradeId,
4452
boost::decimal::decimal64_t closePrice,
4553
const PriceData& tick) {

0 commit comments

Comments
 (0)