diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db02427..4d86dd3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -91,8 +91,10 @@ jobs: wget -q "https://archives.boost.io/release/${BOOST_VERSION}/source/${BOOST_DIR}.tar.gz" tar xzf "${BOOST_DIR}.tar.gz" cd "${BOOST_DIR}" - ./bootstrap.sh - sudo ./b2 install --prefix=/usr/local --with-system -j"$(nproc)" + ./bootstrap.sh > /dev/null + # -d0 silences per-action output (otherwise b2 prints one line per + # copied header — ~15k lines just for the header install). + sudo ./b2 install -d0 --prefix=/usr/local --with-system -j"$(nproc)" - name: Check compiler version, for debugging run: | g++ --version diff --git a/CMakeLists.txt b/CMakeLists.txt index 8302a54..6e2f4b1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ add_subdirectory(external/libpqxx EXCLUDE_FROM_ALL) # Include directories include_directories( ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/include/commands ${CMAKE_SOURCE_DIR}/include/utilities ${CMAKE_SOURCE_DIR}/include/models ${CMAKE_SOURCE_DIR}/include/trading diff --git a/backtesting-engine-cpp.xcodeproj/project.pbxproj b/backtesting-engine-cpp.xcodeproj/project.pbxproj index f31cf65..a04d080 100644 --- a/backtesting-engine-cpp.xcodeproj/project.pbxproj +++ b/backtesting-engine-cpp.xcodeproj/project.pbxproj @@ -43,6 +43,10 @@ 94724A842F8B92C10029B940 /* operations.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94724A822F8B92C10029B940 /* operations.cpp */; }; 94CD8BA02D2E8CE500041BBA /* databaseConnection.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94CD8B9F2D2E8CE500041BBA /* databaseConnection.cpp */; }; 94CD8BA12D2E8CE500041BBA /* databaseConnection.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94CD8B9F2D2E8CE500041BBA /* databaseConnection.cpp */; }; + 94D3A7262FC1B3AD00EBEA32 /* loadCommand.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94D3A7242FC1B3AD00EBEA32 /* loadCommand.cpp */; }; + 94D3A7272FC1B3AD00EBEA32 /* loadCommand.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94D3A7242FC1B3AD00EBEA32 /* loadCommand.cpp */; }; + 94D3A7292FC1B41500EBEA32 /* runCommand.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94D3A7282FC1B41500EBEA32 /* runCommand.cpp */; }; + 94D3A72A2FC1B41500EBEA32 /* runCommand.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94D3A7282FC1B41500EBEA32 /* runCommand.cpp */; }; 94D601102FA9CD700066F51A /* randomStrategy.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94D6010E2FA9CD700066F51A /* randomStrategy.cpp */; }; 94D601112FA9CD700066F51A /* randomStrategy.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94D6010E2FA9CD700066F51A /* randomStrategy.cpp */; }; /* End PBXBuildFile section */ @@ -1279,6 +1283,10 @@ 94CD8B9A2D2DCF6E00041BBA /* libpqxx-7.10.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libpqxx-7.10.a"; path = "build/external/libpqxx/src/libpqxx-7.10.a"; sourceTree = ""; }; 94CD8B9E2D2E8CE500041BBA /* databaseConnection.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = databaseConnection.hpp; sourceTree = ""; }; 94CD8B9F2D2E8CE500041BBA /* databaseConnection.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = databaseConnection.cpp; sourceTree = ""; }; + 94D3A7222FC1B3A600EBEA32 /* loadCommand.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = loadCommand.hpp; sourceTree = ""; }; + 94D3A7242FC1B3AD00EBEA32 /* loadCommand.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = loadCommand.cpp; sourceTree = ""; }; + 94D3A7282FC1B41500EBEA32 /* runCommand.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = runCommand.cpp; sourceTree = ""; }; + 94D3A72B2FC1B41D00EBEA32 /* runCommand.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = runCommand.hpp; sourceTree = ""; }; 94D6010E2FA9CD700066F51A /* randomStrategy.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = randomStrategy.cpp; sourceTree = ""; }; 94D601122FA9CD890066F51A /* randomStrategy.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = randomStrategy.hpp; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1420,14 +1428,15 @@ 9470B5A22C8C5AD0007D9CC6 /* source */ = { isa = PBXGroup; children = ( + 941B54982D3BBAD800E3BF64 /* trading_definitions */, + 94674B862D533B4000973137 /* trading */, + 94674B8C2D533E7800973137 /* models */, + 94D6010F2FA9CD700066F51A /* strategies */, + 94D3A7252FC1B3AD00EBEA32 /* commands */, + 94280BA72D2FC29F00F1CF56 /* utilities */, 942EC5642FBEF95000CCBB5D /* backtestRunner.cpp */, 942EC5652FBEF95000CCBB5D /* redisLoader.cpp */, 942EC5662FBEF95000CCBB5D /* redisRunner.cpp */, - 94D6010F2FA9CD700066F51A /* strategies */, - 94674B8C2D533E7800973137 /* models */, - 94674B862D533B4000973137 /* trading */, - 941B54982D3BBAD800E3BF64 /* trading_definitions */, - 94280BA72D2FC29F00F1CF56 /* utilities */, 9470B5A32C8C5AD0007D9CC6 /* main.cpp */, 940A61112C92CE210083FEB8 /* configManager.cpp */, 940A61152C92CE960083FEB8 /* serviceA.cpp */, @@ -3531,6 +3540,24 @@ path = "../../../../../opt/homebrew/Cellar/postgresql@14/14.15/lib/postgresql@14/pgxs"; sourceTree = ""; }; + 94D3A7232FC1B3A600EBEA32 /* commands */ = { + isa = PBXGroup; + children = ( + 94D3A72B2FC1B41D00EBEA32 /* runCommand.hpp */, + 94D3A7222FC1B3A600EBEA32 /* loadCommand.hpp */, + ); + path = commands; + sourceTree = ""; + }; + 94D3A7252FC1B3AD00EBEA32 /* commands */ = { + isa = PBXGroup; + children = ( + 94D3A7282FC1B41500EBEA32 /* runCommand.cpp */, + 94D3A7242FC1B3AD00EBEA32 /* loadCommand.cpp */, + ); + path = commands; + sourceTree = ""; + }; 94D6010F2FA9CD700066F51A /* strategies */ = { isa = PBXGroup; children = ( @@ -3551,6 +3578,7 @@ 94DE4F772C8C3E7C00FE48FF /* include */ = { isa = PBXGroup; children = ( + 94D3A7232FC1B3A600EBEA32 /* commands */, 942EC55E2FBEF93A00CCBB5D /* backtestRunner.hpp */, 942EC55F2FBEF93A00CCBB5D /* redisLoader.hpp */, 942EC5602FBEF93A00CCBB5D /* redisRunner.hpp */, @@ -3664,8 +3692,10 @@ buildActionMask = 2147483647; files = ( 941408AE2D59F93F000ED1F9 /* sqlManager.cpp in Sources */, + 94D3A7272FC1B3AD00EBEA32 /* loadCommand.cpp in Sources */, 9470B5A42C8C5AD0007D9CC6 /* main.cpp in Sources */, 943398252D57E53400287A2D /* jsonParser.cpp in Sources */, + 94D3A72A2FC1B41500EBEA32 /* runCommand.cpp in Sources */, 942EC56A2FBEF95000CCBB5D /* backtestRunner.cpp in Sources */, 942EC56B2FBEF95000CCBB5D /* redisLoader.cpp in Sources */, 942EC56C2FBEF95000CCBB5D /* redisRunner.cpp in Sources */, @@ -3694,10 +3724,12 @@ 943398242D57E53400287A2D /* jsonParser.cpp in Sources */, 942EC5672FBEF95000CCBB5D /* backtestRunner.cpp in Sources */, 942EC5682FBEF95000CCBB5D /* redisLoader.cpp in Sources */, + 94D3A7262FC1B3AD00EBEA32 /* loadCommand.cpp in Sources */, 942EC5692FBEF95000CCBB5D /* redisRunner.cpp in Sources */, 94280BA42D2FC00200F1CF56 /* base64.cpp in Sources */, 94674B8D2D533E7800973137 /* trade.cpp in Sources */, 941B549A2D3BBADE00E3BF64 /* trading_definitions_json.cpp in Sources */, + 94D3A7292FC1B41500EBEA32 /* runCommand.cpp in Sources */, 94D601102FA9CD700066F51A /* randomStrategy.cpp in Sources */, 94674B8A2D533BDA00973137 /* tradeManager.mm in Sources */, 94724A832F8B92C10029B940 /* operations.cpp in Sources */, diff --git a/include/commands/loadCommand.hpp b/include/commands/loadCommand.hpp new file mode 100644 index 0000000..400f552 --- /dev/null +++ b/include/commands/loadCommand.hpp @@ -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 + +// Backs the `load` subcommand: reads each strategy file from disk and LPUSHes +// it onto the Redis `strategy_queue` via RedisLoader. +class LoadCommand { +public: + static int run(int argc, const char* argv[]); +}; diff --git a/include/commands/runCommand.hpp b/include/commands/runCommand.hpp new file mode 100644 index 0000000..dd4fb07 --- /dev/null +++ b/include/commands/runCommand.hpp @@ -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 + +// Backs the `run` subcommand: executes one strategy popped from the Redis +// `strategy_queue`, or a Base64 config supplied directly on the command line. +class RunCommand { +public: + static int run(int argc, const char* argv[]); +}; diff --git a/source/commands/loadCommand.cpp b/source/commands/loadCommand.cpp new file mode 100644 index 0000000..ab303a6 --- /dev/null +++ b/source/commands/loadCommand.cpp @@ -0,0 +1,44 @@ +// Backtesting Engine in C++ +// +// (c) 2026 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- + +#include "loadCommand.hpp" + +#include +#include +#include +#include + +#include "redisLoader.hpp" + +int LoadCommand::run(int argc, const char* argv[]) { + if (argc < 3) { + std::cerr << "Usage: " << argv[0] << " load [path...]" + << std::endl; + return 1; + } + for (int i = 2; i < argc; ++i) { + const std::string path = argv[i]; + std::ifstream ifs(path); + if (!ifs) { + std::cerr << "BacktestingEngine: failed to open file: " << path + << std::endl; + return 1; + } + std::ostringstream buffer; + buffer << ifs.rdbuf(); + if (ifs.bad()) { + std::cerr << "BacktestingEngine: failed to read file: " << path + << std::endl; + return 1; + } + const int rc = RedisLoader::load(buffer.str(), "127.0.0.1", 6379, + "strategy_queue"); + if (rc != 0) { + return rc; + } + } + return 0; +} \ No newline at end of file diff --git a/source/commands/runCommand.cpp b/source/commands/runCommand.cpp new file mode 100644 index 0000000..caca9b9 --- /dev/null +++ b/source/commands/runCommand.cpp @@ -0,0 +1,37 @@ +// Backtesting Engine in C++ +// +// (c) 2026 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- + +#include "runCommand.hpp" + +#include +#include + +#include "backtestRunner.hpp" +#include "jsonParser.hpp" +#include "redisRunner.hpp" + +namespace { + +int runBacktestFromBase64(const std::string& questdbHost, + const std::string& base64Config) { + auto config = JsonParser::parseConfigurationFromBase64(base64Config); + return runBacktest(questdbHost, config); +} + +} // namespace + +int RunCommand::run(int argc, const char* argv[]) { + if (argc < 3) { + std::cerr << "Usage: BacktestingEngine run \n" + << " BacktestingEngine run " + << std::endl; + return 1; + } + if (argc == 3) { + return RedisRunner::run(argv[2]); + } + return runBacktestFromBase64(argv[2], argv[3]); +} diff --git a/source/main.cpp b/source/main.cpp index f7e49a2..78c2129 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -5,103 +5,26 @@ // --------------------------------------- // std headers -#include #include -#include -#include +#include // backtesting engine headers -#include "backtestRunner.hpp" -#include "jsonParser.hpp" -#include "redisLoader.hpp" -#include "redisRunner.hpp" +#include "loadCommand.hpp" +#include "runCommand.hpp" -static int runBacktest(const std::string& questdbHost, - const std::string& base64Config) { - auto config = JsonParser::parseConfigurationFromBase64(base64Config); - return runBacktest(questdbHost, config); -} - -static void printUsage(std::ostream& out) { - out << "Usage: BacktestingEngine [args...]\n" - << "\n" - << "Subcommands:\n" - << " load [path...] Push Base64-encoded JSON strategies\n" - << " from path(s) onto Redis\n" - << " `strategy_queue`.\n" - << " run Pop one Base64 strategy from the\n" - << " Redis `strategy_queue` and execute\n" - << " it.\n" - << " run \n" - << " Decode the supplied Base64 strategy\n" - << " and execute it.\n" - << " -h, --help Show this help message." - << std::endl; -} - -// Entry point. Dispatches on argv[1] to one of the BacktestingEngine -// subcommands: `load ` is the Redis enqueue path — it LPUSHes a -// base64-encoded JSON payload onto `strategy_queue` via RedisLoader, pairing -// with the dequeue side handled by `RedisRunner`; `run ` RPOPs -// the next strategy from `strategy_queue` and executes it against QuestDB, -// while `run ` skips Redis and executes the -// supplied Base64 strategy directly; `-h`/`--help` prints usage. -int main(int argc, const char * argv[]) { +int main(int argc, const char* argv[]) { if (argc < 2) { - printUsage(std::cerr); + std::cerr << "BacktestingEngine: missing subcommand. See README.md for usage." + << std::endl; return 1; } - const std::string subcommand = argv[1]; + const std::string_view subcommand = argv[1]; - if (subcommand == "-h" || subcommand == "--help") { - printUsage(std::cout); - return 0; - } - - if (subcommand == "load") { - if (argc < 3) { - std::cerr << "Usage: " << argv[0] << " load [path...]" - << std::endl; - return 1; - } - for (int i = 2; i < argc; ++i) { - const std::string path = argv[i]; - std::ifstream ifs(path); - if (!ifs) { - std::cerr << "BacktestingEngine: failed to open file: " << path - << std::endl; - return 1; - } - std::ostringstream buffer; - buffer << ifs.rdbuf(); - if (ifs.bad()) { - std::cerr << "BacktestingEngine: failed to read file: " << path - << std::endl; - return 1; - } - const int rc = RedisLoader::load(buffer.str(), "127.0.0.1", 6379, - "strategy_queue"); - if (rc != 0) { - return rc; - } - } - return 0; - } - - if (subcommand == "run") { - if (argc < 3) { - std::cerr << "Usage: BacktestingEngine run \n" - << " BacktestingEngine run " - << std::endl; - return 1; - } - if (argc == 3) { - return RedisRunner::run(argv[2]); - } - return runBacktest(argv[2], argv[3]); - } + if (subcommand == "load") return LoadCommand::run(argc, argv); + if (subcommand == "run") return RunCommand::run(argc, argv); - printUsage(std::cerr); + std::cerr << "BacktestingEngine: unknown subcommand. See README.md for usage." + << std::endl; return 1; }