Skip to content

Commit abbb9e5

Browse files
committed
Adding elasticsearch client
1 parent 3be1db3 commit abbb9e5

6 files changed

Lines changed: 225 additions & 6 deletions

File tree

CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,13 @@ endif()
7070
find_package(Boost REQUIRED CONFIG)
7171
find_package(OpenSSL REQUIRED)
7272
find_package(Threads REQUIRED)
73+
find_package(CURL REQUIRED)
7374
target_link_libraries(BacktestingEngineLib PUBLIC
7475
Boost::headers
7576
OpenSSL::SSL
7677
OpenSSL::Crypto
77-
Threads::Threads)
78+
Threads::Threads
79+
CURL::libcurl)
7880

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

backtesting-engine-cpp.xcodeproj/project.pbxproj

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@
2525
942EC56A2FBEF95000CCBB5D /* backtestRunner.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942EC5642FBEF95000CCBB5D /* backtestRunner.cpp */; };
2626
942EC56B2FBEF95000CCBB5D /* redisLoader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942EC5652FBEF95000CCBB5D /* redisLoader.cpp */; };
2727
942EC56C2FBEF95000CCBB5D /* redisRunner.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942EC5662FBEF95000CCBB5D /* redisRunner.cpp */; };
28+
942FDDE02FC5C8B20096F318 /* tradingResults.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942FDDDF2FC5C8B20096F318 /* tradingResults.cpp */; };
29+
942FDDE12FC5C8B20096F318 /* elasticClient.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942FDDDE2FC5C8B20096F318 /* elasticClient.cpp */; };
30+
942FDDE22FC5C8B20096F318 /* tradingResults.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942FDDDF2FC5C8B20096F318 /* tradingResults.cpp */; };
31+
942FDDE32FC5C8B20096F318 /* elasticClient.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 942FDDDE2FC5C8B20096F318 /* elasticClient.cpp */; };
32+
942FDDE52FC5C9D30096F318 /* libcurl.4.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 942FDDE42FC5C9D30096F318 /* libcurl.4.tbd */; };
33+
942FDDE62FC5C9DB0096F318 /* libcurl.4.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 942FDDE42FC5C9D30096F318 /* libcurl.4.tbd */; };
2834
943398242D57E53400287A2D /* jsonParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 943398232D57E53400287A2D /* jsonParser.cpp */; };
2935
943398252D57E53400287A2D /* jsonParser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 943398232D57E53400287A2D /* jsonParser.cpp */; };
3036
943398272D57E54000287A2D /* jsonParser.mm in Sources */ = {isa = PBXBuildFile; fileRef = 943398262D57E54000287A2D /* jsonParser.mm */; };
@@ -90,6 +96,11 @@
9096
942EC5642FBEF95000CCBB5D /* backtestRunner.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = backtestRunner.cpp; sourceTree = "<group>"; };
9197
942EC5652FBEF95000CCBB5D /* redisLoader.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = redisLoader.cpp; sourceTree = "<group>"; };
9298
942EC5662FBEF95000CCBB5D /* redisRunner.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = redisRunner.cpp; sourceTree = "<group>"; };
99+
942FDDDC2FC5C8950096F318 /* elasticClient.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = elasticClient.hpp; sourceTree = "<group>"; };
100+
942FDDDD2FC5C8A30096F318 /* tradingResults.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = tradingResults.hpp; sourceTree = "<group>"; };
101+
942FDDDE2FC5C8B20096F318 /* elasticClient.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = elasticClient.cpp; sourceTree = "<group>"; };
102+
942FDDDF2FC5C8B20096F318 /* tradingResults.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = tradingResults.cpp; sourceTree = "<group>"; };
103+
942FDDE42FC5C9D30096F318 /* libcurl.4.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libcurl.4.tbd; path = usr/lib/libcurl.4.tbd; sourceTree = SDKROOT; };
93104
943398222D57E52900287A2D /* jsonParser.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = jsonParser.hpp; sourceTree = "<group>"; };
94105
943398232D57E53400287A2D /* jsonParser.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = jsonParser.cpp; sourceTree = "<group>"; };
95106
943398262D57E54000287A2D /* jsonParser.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = jsonParser.mm; sourceTree = "<group>"; };
@@ -1296,13 +1307,15 @@
12961307
isa = PBXFrameworksBuildPhase;
12971308
buildActionMask = 2147483647;
12981309
files = (
1310+
942FDDE62FC5C9DB0096F318 /* libcurl.4.tbd in Frameworks */,
12991311
);
13001312
runOnlyForDeploymentPostprocessing = 0;
13011313
};
13021314
9470B5A92C8C5B99007D9CC6 /* Frameworks */ = {
13031315
isa = PBXFrameworksBuildPhase;
13041316
buildActionMask = 2147483647;
13051317
files = (
1318+
942FDDE52FC5C9D30096F318 /* libcurl.4.tbd in Frameworks */,
13061319
);
13071320
runOnlyForDeploymentPostprocessing = 0;
13081321
};
@@ -1436,6 +1449,8 @@
14361449
94280BA72D2FC29F00F1CF56 /* utilities */,
14371450
942EC5642FBEF95000CCBB5D /* backtestRunner.cpp */,
14381451
942EC5652FBEF95000CCBB5D /* redisLoader.cpp */,
1452+
942FDDDE2FC5C8B20096F318 /* elasticClient.cpp */,
1453+
942FDDDF2FC5C8B20096F318 /* tradingResults.cpp */,
14391454
942EC5662FBEF95000CCBB5D /* redisRunner.cpp */,
14401455
9470B5A32C8C5AD0007D9CC6 /* main.cpp */,
14411456
940A61112C92CE210083FEB8 /* configManager.cpp */,
@@ -2218,6 +2233,7 @@
22182233
94CD87342D2D2EE100041BBA /* Frameworks */ = {
22192234
isa = PBXGroup;
22202235
children = (
2236+
942FDDE42FC5C9D30096F318 /* libcurl.4.tbd */,
22212237
94CD8B982D2DCDD800041BBA /* libpqxx-7.10.a */,
22222238
94CD8B9A2D2DCF6E00041BBA /* libpqxx-7.10.a */,
22232239
94CD8B712D2D34C800041BBA /* config */,
@@ -3579,17 +3595,19 @@
35793595
isa = PBXGroup;
35803596
children = (
35813597
94D3A7232FC1B3A600EBEA32 /* commands */,
3582-
942EC55E2FBEF93A00CCBB5D /* backtestRunner.hpp */,
3583-
942EC55F2FBEF93A00CCBB5D /* redisLoader.hpp */,
3584-
942EC5602FBEF93A00CCBB5D /* redisRunner.hpp */,
3585-
94D601132FA9CD890066F51A /* strategies */,
3586-
94674B842D533B2F00973137 /* trading */,
35873598
942966D72D48E84100532862 /* models */,
3599+
94674B842D533B2F00973137 /* trading */,
3600+
94D601132FA9CD890066F51A /* strategies */,
35883601
94B8C7932D3D770800E17EB6 /* utilities */,
35893602
941B548F2D3BBA3B00E3BF64 /* trading_definitions */,
35903603
941B549C2D3BBFB900E3BF64 /* trading_definitions.hpp */,
3604+
942EC55E2FBEF93A00CCBB5D /* backtestRunner.hpp */,
3605+
942EC55F2FBEF93A00CCBB5D /* redisLoader.hpp */,
3606+
942EC5602FBEF93A00CCBB5D /* redisRunner.hpp */,
35913607
940A61162C92CE960083FEB8 /* serviceA.hpp */,
3608+
942FDDDC2FC5C8950096F318 /* elasticClient.hpp */,
35923609
943398222D57E52900287A2D /* jsonParser.hpp */,
3610+
942FDDDD2FC5C8A30096F318 /* tradingResults.hpp */,
35933611
940A61122C92CE210083FEB8 /* configManager.hpp */,
35943612
941408B02D59F954000ED1F9 /* sqlManager.hpp */,
35953613
94724A852F8B92E30029B940 /* operations.hpp */,
@@ -3695,6 +3713,8 @@
36953713
94D3A7272FC1B3AD00EBEA32 /* loadCommand.cpp in Sources */,
36963714
9470B5A42C8C5AD0007D9CC6 /* main.cpp in Sources */,
36973715
943398252D57E53400287A2D /* jsonParser.cpp in Sources */,
3716+
942FDDE02FC5C8B20096F318 /* tradingResults.cpp in Sources */,
3717+
942FDDE12FC5C8B20096F318 /* elasticClient.cpp in Sources */,
36983718
94D3A72A2FC1B41500EBEA32 /* runCommand.cpp in Sources */,
36993719
942EC56A2FBEF95000CCBB5D /* backtestRunner.cpp in Sources */,
37003720
942EC56B2FBEF95000CCBB5D /* redisLoader.cpp in Sources */,
@@ -3737,6 +3757,8 @@
37373757
94674B882D533B4000973137 /* tradeManager.cpp in Sources */,
37383758
946EFF7F2FB9F44E008D9647 /* reporting.cpp in Sources */,
37393759
943398272D57E54000287A2D /* jsonParser.mm in Sources */,
3760+
942FDDE22FC5C8B20096F318 /* tradingResults.cpp in Sources */,
3761+
942FDDE32FC5C8B20096F318 /* elasticClient.cpp in Sources */,
37403762
9470B5B62C8C5BFD007D9CC6 /* main.cpp in Sources */,
37413763
94364CB62D416D8D00F35B55 /* db.mm in Sources */,
37423764
940A61142C92CE210083FEB8 /* configManager.cpp in Sources */,

include/elasticClient.hpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
9+
#include "tradingResults.hpp"
10+
11+
// Minimal Elasticsearch HTTP client — PUT-only, for indexing TradingResults.
12+
// Host is read from $ELASTICSEARCH_URL (default http://localhost:9200);
13+
// docs land in index "trading_results" with a freshly generated UUID per put.
14+
class ElasticClient {
15+
public:
16+
static int putTradingResults(const TradingResults& results);
17+
};

include/tradingResults.hpp

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
9+
#include <cstddef>
10+
#include <optional>
11+
#include <string>
12+
13+
#include <boost/decimal.hpp>
14+
#include <nlohmann/json.hpp>
15+
16+
#include "utilities/decimal_json.hpp"
17+
#include "trading_definitions.hpp"
18+
19+
// Per-run summary mirroring what Reporting::summarise prints today.
20+
struct TradingResultsStats {
21+
boost::decimal::decimal64_t finalPnl;
22+
std::size_t tradesOpened;
23+
std::size_t tradesClosed;
24+
std::size_t openedLong;
25+
std::size_t openedShort;
26+
std::size_t closedLong;
27+
std::size_t closedShort;
28+
std::size_t winners;
29+
std::size_t losers;
30+
std::size_t breakeven;
31+
std::optional<boost::decimal::decimal64_t> avgPnl;
32+
};
33+
34+
// Wire shape pushed to Elasticsearch: the input Configuration plus the run's
35+
// output stats. Serialises the timestamp as `@timestamp` for Kibana.
36+
struct TradingResults {
37+
std::string RUN_ID;
38+
std::string timestamp;
39+
trading_definitions::Configuration config;
40+
TradingResultsStats results;
41+
42+
static std::string nowIsoUtc();
43+
};
44+
45+
void to_json(nlohmann::json& j, const TradingResultsStats& s);
46+
void to_json(nlohmann::json& j, const TradingResults& r);

source/elasticClient.cpp

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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 "elasticClient.hpp"
8+
9+
#include <cstdlib>
10+
#include <iostream>
11+
#include <string>
12+
13+
#include <curl/curl.h>
14+
#include <boost/uuid/random_generator.hpp>
15+
#include <boost/uuid/uuid_io.hpp>
16+
#include <nlohmann/json.hpp>
17+
18+
namespace {
19+
20+
void ensureCurlInit() {
21+
static const struct CurlGlobal {
22+
CurlGlobal() { curl_global_init(CURL_GLOBAL_ALL); }
23+
~CurlGlobal() { curl_global_cleanup(); }
24+
} guard;
25+
(void)guard;
26+
}
27+
28+
std::string envOr(const char* name, std::string fallback) {
29+
const char* val = std::getenv(name);
30+
return val ? std::string{val} : std::move(fallback);
31+
}
32+
33+
std::string generateUuid() {
34+
static thread_local boost::uuids::random_generator gen;
35+
return boost::uuids::to_string(gen());
36+
}
37+
38+
} // namespace
39+
40+
int ElasticClient::putTradingResults(const TradingResults& results) {
41+
ensureCurlInit();
42+
43+
const std::string host = envOr("ELASTICSEARCH_URL", "http://localhost:9200");
44+
const std::string url = host + "/trading_results/_doc/" + generateUuid();
45+
const std::string body = nlohmann::json(results).dump();
46+
47+
CURL* curl = curl_easy_init();
48+
if (!curl) {
49+
std::cerr << "ElasticClient: curl_easy_init failed" << std::endl;
50+
return 1;
51+
}
52+
53+
struct curl_slist* headers = nullptr;
54+
headers = curl_slist_append(headers, "Content-Type: application/json");
55+
56+
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
57+
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
58+
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
59+
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
60+
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(body.size()));
61+
62+
const CURLcode rc = curl_easy_perform(curl);
63+
64+
long httpStatus = 0;
65+
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpStatus);
66+
67+
curl_slist_free_all(headers);
68+
curl_easy_cleanup(curl);
69+
70+
if (rc != CURLE_OK) {
71+
std::cerr << "ElasticClient: PUT failed: " << curl_easy_strerror(rc)
72+
<< std::endl;
73+
return 2;
74+
}
75+
if (httpStatus < 200 || httpStatus >= 300) {
76+
std::cerr << "ElasticClient: HTTP " << httpStatus << " from " << url
77+
<< std::endl;
78+
return 3;
79+
}
80+
std::cout << "ElasticClient: PUT " << url << " (HTTP " << httpStatus << ")"
81+
<< std::endl;
82+
return 0;
83+
}

source/tradingResults.cpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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 "tradingResults.hpp"
8+
9+
#include <chrono>
10+
#include <ctime>
11+
12+
std::string TradingResults::nowIsoUtc() {
13+
const auto now = std::chrono::system_clock::to_time_t(
14+
std::chrono::system_clock::now());
15+
std::tm tm_buf{};
16+
gmtime_r(&now, &tm_buf);
17+
char buf[32];
18+
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &tm_buf);
19+
return std::string{buf};
20+
}
21+
22+
void to_json(nlohmann::json& j, const TradingResultsStats& s) {
23+
j = nlohmann::json{
24+
{"finalPnl", s.finalPnl},
25+
{"tradesOpened", s.tradesOpened},
26+
{"tradesClosed", s.tradesClosed},
27+
{"openedLong", s.openedLong},
28+
{"openedShort", s.openedShort},
29+
{"closedLong", s.closedLong},
30+
{"closedShort", s.closedShort},
31+
{"winners", s.winners},
32+
{"losers", s.losers},
33+
{"breakeven", s.breakeven},
34+
};
35+
if (s.avgPnl) {
36+
j["avgPnl"] = *s.avgPnl;
37+
} else {
38+
j["avgPnl"] = nullptr;
39+
}
40+
}
41+
42+
void to_json(nlohmann::json& j, const TradingResults& r) {
43+
j = nlohmann::json{
44+
{"RUN_ID", r.RUN_ID},
45+
{"@timestamp", r.timestamp},
46+
{"config", r.config},
47+
{"results", r.results},
48+
};
49+
}

0 commit comments

Comments
 (0)