Skip to content

Commit 9fdd89a

Browse files
committed
Updated trade tracking to post to elasticsearch and switched to using env variables with Infisical
1 parent 257fa17 commit 9fdd89a

14 files changed

Lines changed: 155 additions & 36 deletions

File tree

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,28 @@ Xcode - Library Path
8787

8888
`bash ./scripts/build.sh`
8989

90+
### Environment variables
91+
92+
The engine reads its connection configuration from the environment. The following variables are **required**`scripts/run.sh` validates them up front and aborts if any are missing or empty:
93+
94+
| Variable | Used for |
95+
| --- | --- |
96+
| `ELASTIC_HOST` | Elasticsearch base URL that trading results are PUT to (e.g. `https://elastic.example.com:9200`) |
97+
| `ELASTIC_USER` | Elasticsearch HTTP basic-auth username |
98+
| `ELASTIC_USER_PASSWORD` | Elasticsearch HTTP basic-auth password |
99+
| `REDIS_HOST` | Redis host for the `strategy_queue` list |
100+
101+
I manage these secrets with [Infisical](https://infisical.com/), which injects them into the process environment at runtime, so I run the engine with:
102+
103+
```
104+
infisical run -- sh ./scripts/run.sh
105+
```
106+
107+
If you're not using Infisical, export the variables yourself (e.g. via your shell profile or a sourced `.env`) before invoking the script.
108+
90109
### Run via terminal
91110

92-
`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.
111+
`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 script requires the [environment variables](#environment-variables) listed above; with Infisical that becomes `infisical run -- sh ./scripts/run.sh`.
93112

94113
The `BacktestingEngine` binary exposes a subcommand CLI:
95114

include/elasticClient.hpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
#include "tradingResults.hpp"
1010

1111
// 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.
12+
// Host is read from $ELASTIC_HOST (default http://localhost:9200) with optional
13+
// HTTP basic auth from $ELASTIC_USER / $ELASTIC_USER_PASSWORD; docs land in
14+
// index "trading_results" with a freshly generated UUID per put.
1415
class ElasticClient {
1516
public:
1617
static int putTradingResults(const TradingResults& results);

include/strategies/strategy.hpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
#include "models/trade.hpp"
1212

1313
// Forward declaration. We only refer to `TradeManager` by reference in
14-
// this header, so the compiler doesn't need its full definition here
14+
// this header, so the compiler doesn't need its full definition here,
1515
// just to know "it's a class". Pulling in the full header would drag
1616
// every TradeManager dependency into every strategy. C# doesn't really
1717
// have an equivalent because it resolves types at the assembly level
@@ -25,7 +25,7 @@ class TradeManager;
2525
// - This is the C++ equivalent of a C# `interface`. C++ has no
2626
// dedicated `interface` keyword, so you express it as a class
2727
// whose methods are all pure virtual (the `= 0` suffix). The
28-
// `I` prefix is borrowed from C# C++ has no fixed convention,
28+
// `I` prefix is borrowed from C#, C++ has no fixed convention,
2929
// but it's a useful hint because C++ classes routinely mix
3030
// virtual and concrete methods, so "is this an interface?" isn't
3131
// always obvious from the keyword alone.
@@ -34,7 +34,7 @@ class TradeManager;
3434
// destructor would skip the derived destructor and leak resources.
3535
// C# handles this for you; in C++ you opt in.
3636
// - `= 0` makes a method pure virtual, which makes the class
37-
// abstract you cannot instantiate `IStrategy` directly, only
37+
// abstract, you cannot instantiate `IStrategy` directly, only
3838
// concrete subclasses. Same behaviour as a C# interface.
3939
// - Derived classes annotate their implementations with `override`
4040
// (see `RandomStrategy`). It's optional in C++ but catches
@@ -52,7 +52,7 @@ class IStrategy {
5252
// reference so strategies can both inspect open positions
5353
// (`tradeManager.getActiveTrades()`) and act on them
5454
// (`closeTrade`, future scale/adjust hooks). A reference signals
55-
// "borrow, don't own" the strategy must not delete it. The C#
55+
// "borrow, don't own", the strategy must not delete it. The C#
5656
// analogue is just passing the manager as a parameter; C# has no
5757
// distinction between reference and pointer so the by-ref nature
5858
// is implicit there.

include/trading/reporting.hpp

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

77
#pragma once
88
#include "tradeManager.hpp"
9+
#include "tradingResults.hpp"
910

1011
class Reporting {
1112

1213
public:
14+
static TradingResultsStats collect(const TradeManager& tradeManager);
1315
static void summarise(const TradeManager& tradeManager);
1416
};

include/utilities/env.hpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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 <string>
10+
11+
namespace env {
12+
13+
// Returns $name, or `fallback` when the variable is unset or empty.
14+
std::string getOr(const char* name, std::string fallback);
15+
16+
} // namespace env

scripts/run.sh

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
#!/bin/bash
22
# This executes the run script
33

4+
required_vars=(ELASTIC_HOST ELASTIC_USER ELASTIC_USER_PASSWORD REDIS_HOST)
5+
missing=()
6+
for var in "${required_vars[@]}"; do
7+
[[ -z "${!var}" ]] && missing+=("$var")
8+
done
9+
if [[ ${#missing[@]} -gt 0 ]]; then
10+
echo "Error: missing required environment variables: ${missing[*]}"
11+
exit 1
12+
fi
13+
414
current_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
515

616
# Build the source code
@@ -24,10 +34,6 @@ if ! redis-cli -h localhost ping >/dev/null 2>&1; then
2434
exit 0
2535
fi
2636

27-
if ! ./"$BUILD_DIR/$EXECUTABLE_NAME" load; then
28-
exit 1
29-
fi
30-
3137
start_time=$(date +%s%N)
3238
# Invoke the `run` subcommand: BacktestingEngine pops a Base64-encoded
3339
# strategy off the Redis `strategy_queue` and executes it against the

source/backtestRunner.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@
1717

1818
int runBacktest(const std::string& questdbHost,
1919
const trading_definitions::Configuration& config) {
20+
2021
DatabaseConnection db(questdbHost, 8812, "qdb", "admin", "quest");
2122

23+
// Get a list of symbols
2224
std::vector<std::string> symbols;
2325
std::istringstream ss(config.SYMBOLS);
2426
for (std::string token; std::getline(ss, token, ',');) {
2527
symbols.push_back(token);
2628
}
2729

30+
// Get all the tick data out of QuestDB for these symbols
2831
std::vector<PriceData> ticks =
2932
SqlManager::streamPriceData(db, symbols, config.LAST_MONTHS);
3033

source/commands/loadCommand.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <boost/decimal/literals.hpp>
1010
#include <nlohmann/json.hpp>
1111

12+
#include "env.hpp"
1213
#include "redisLoader.hpp"
1314
#include "trading_definitions.hpp"
1415

@@ -38,5 +39,5 @@ int LoadCommand::run() {
3839
};
3940

4041
const nlohmann::json j = config;
41-
return RedisLoader::load(j.dump());
42+
return RedisLoader::load(j.dump(), env::getOr("REDIS_HOST", "127.0.0.1"));
4243
}

source/commands/runCommand.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <string>
1111

1212
#include "backtestRunner.hpp"
13+
#include "env.hpp"
1314
#include "jsonParser.hpp"
1415
#include "redisRunner.hpp"
1516

@@ -31,7 +32,7 @@ int RunCommand::run(int argc, const char* argv[]) {
3132
return 1;
3233
}
3334
if (argc == 3) {
34-
return RedisRunner::run(argv[2]);
35+
return RedisRunner::run(argv[2], env::getOr("REDIS_HOST", "127.0.0.1"));
3536
}
3637
return runBacktestFromBase64(argv[2], argv[3]);
3738
}

source/elasticClient.cpp

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

77
#include "elasticClient.hpp"
88

9-
#include <cstdlib>
109
#include <iostream>
1110
#include <string>
1211

@@ -15,6 +14,8 @@
1514
#include <boost/uuid/uuid_io.hpp>
1615
#include <nlohmann/json.hpp>
1716

17+
#include "env.hpp"
18+
1819
namespace {
1920

2021
void ensureCurlInit() {
@@ -25,11 +26,6 @@ void ensureCurlInit() {
2526
(void)guard;
2627
}
2728

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-
3329
std::string generateUuid() {
3430
static thread_local boost::uuids::random_generator gen;
3531
return boost::uuids::to_string(gen());
@@ -40,7 +36,9 @@ std::string generateUuid() {
4036
int ElasticClient::putTradingResults(const TradingResults& results) {
4137
ensureCurlInit();
4238

43-
const std::string host = envOr("ELASTICSEARCH_URL", "http://localhost:9200");
39+
const std::string host = env::getOr("ELASTIC_HOST", "http://localhost:9200");
40+
const std::string user = env::getOr("ELASTIC_USER", "");
41+
const std::string password = env::getOr("ELASTIC_USER_PASSWORD", "");
4442
const std::string url = host + "/trading_results/_doc/" + generateUuid();
4543
const std::string body = nlohmann::json(results).dump();
4644

@@ -55,6 +53,13 @@ int ElasticClient::putTradingResults(const TradingResults& results) {
5553

5654
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
5755
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
56+
57+
// HTTP basic auth when credentials are supplied via the environment.
58+
if (!user.empty()) {
59+
curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
60+
curl_easy_setopt(curl, CURLOPT_USERNAME, user.c_str());
61+
curl_easy_setopt(curl, CURLOPT_PASSWORD, password.c_str());
62+
}
5863
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
5964
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
6065
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast<long>(body.size()));

0 commit comments

Comments
 (0)