diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 6897994..4004505 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -17,7 +17,7 @@ jobs: build: name: Build & Test - runs-on: macos-14 # Use macOS 14 (Sonoma) runner + runs-on: macos-26 steps: # Step 1: Check out the repository code @@ -40,7 +40,7 @@ jobs: # Step 5: Configure Xcode version - name: Select Xcode version run: | - sudo xcode-select -switch /Applications/Xcode_15.2.app + sudo xcode-select -switch /Applications/Xcode_26.2.app /usr/bin/xcodebuild -version # Run XCode tests with specific configurations: # - Builds and runs the test suite diff --git a/CMakeLists.txt b/CMakeLists.txt index b6c5634..ad2c316 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,6 @@ project(BacktestingEngine) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) - # Configure libpqxx build set(PQXX_LIBRARIES_INSTALL ON) set(SKIP_BUILD_TEST ON) @@ -51,8 +50,18 @@ file(GLOB_RECURSE SOURCES "source/*.cpp") # Create a library of your project's code add_library(BacktestingEngineLib STATIC ${SOURCES}) -# Link against pqxx -target_link_libraries(BacktestingEngineLib pqxx) +# Configure OpenMP. On Apple, provide Homebrew libomp hints before discovery. +if(APPLE) + set(OpenMP_C_FLAGS "-Xclang -fopenmp") + set(OpenMP_CXX_FLAGS "-Xclang -fopenmp") + set(OpenMP_C_LIB_NAMES "omp") + set(OpenMP_CXX_LIB_NAMES "omp") + set(OpenMP_omp_LIBRARY /opt/homebrew/opt/libomp/lib/libomp.dylib) + target_include_directories(BacktestingEngineLib PRIVATE /opt/homebrew/opt/libomp/include) +endif() + +find_package(OpenMP REQUIRED) +target_link_libraries(BacktestingEngineLib PUBLIC pqxx OpenMP::OpenMP_CXX) # Main executable add_executable(BacktestingEngine source/main.cpp) diff --git a/backtesting-engine-cpp.xcodeproj/project.pbxproj b/backtesting-engine-cpp.xcodeproj/project.pbxproj index e6f732a..8e066cd 100644 --- a/backtesting-engine-cpp.xcodeproj/project.pbxproj +++ b/backtesting-engine-cpp.xcodeproj/project.pbxproj @@ -29,6 +29,8 @@ 94674B8E2D533E7800973137 /* trade.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94674B8B2D533E7800973137 /* trade.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 */; }; + 94724A842F8B92C10029B940 /* operations.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94724A822F8B92C10029B940 /* operations.cpp */; }; 94CD8B992D2DCDD800041BBA /* libpqxx-7.10.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 94CD8B982D2DCDD800041BBA /* libpqxx-7.10.a */; }; 94CD8B9C2D2DD02A00041BBA /* libpqxx-7.10.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 94CD8B9A2D2DCF6E00041BBA /* libpqxx-7.10.a */; }; 94CD8BA02D2E8CE500041BBA /* databaseConnection.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 94CD8B9F2D2E8CE500041BBA /* databaseConnection.cpp */; }; @@ -89,6 +91,8 @@ 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 = ""; }; 9470B5AC2C8C5B99007D9CC6 /* tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 94724A822F8B92C10029B940 /* operations.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = operations.cpp; sourceTree = ""; }; + 94724A852F8B92E30029B940 /* operations.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = operations.hpp; sourceTree = ""; }; 948A9CCD2C906A5600E23669 /* CONVENTIONS.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONVENTIONS.md; sourceTree = ""; }; 948A9CED2C906AFE00E23669 /* 2020.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = 2020.csv; sourceTree = ""; }; 94BBA4512D2EA2640010E04D /* build.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = build.sh; sourceTree = ""; }; @@ -1302,6 +1306,7 @@ isa = PBXGroup; children = ( 94280BA22D2FC00200F1CF56 /* base64.cpp */, + 943398232D57E53400287A2D /* jsonParser.cpp */, ); path = utilities; sourceTree = ""; @@ -1408,8 +1413,8 @@ 940A61112C92CE210083FEB8 /* configManager.cpp */, 940A61152C92CE960083FEB8 /* serviceA.cpp */, 94CD8B9F2D2E8CE500041BBA /* databaseConnection.cpp */, - 943398232D57E53400287A2D /* jsonParser.cpp */, 941408AD2D59F93F000ED1F9 /* sqlManager.cpp */, + 94724A822F8B92C10029B940 /* operations.cpp */, ); path = source; sourceTree = ""; @@ -3515,15 +3520,16 @@ 94DE4F772C8C3E7C00FE48FF /* include */ = { isa = PBXGroup; children = ( - 941408B02D59F954000ED1F9 /* sqlManager.hpp */, - 943398222D57E52900287A2D /* jsonParser.hpp */, 94674B842D533B2F00973137 /* trading */, 942966D72D48E84100532862 /* models */, 94B8C7932D3D770800E17EB6 /* utilities */, 941B548F2D3BBA3B00E3BF64 /* trading_definitions */, 941B549C2D3BBFB900E3BF64 /* trading_definitions.hpp */, 940A61162C92CE960083FEB8 /* serviceA.hpp */, + 943398222D57E52900287A2D /* jsonParser.hpp */, 940A61122C92CE210083FEB8 /* configManager.hpp */, + 941408B02D59F954000ED1F9 /* sqlManager.hpp */, + 94724A852F8B92E30029B940 /* operations.hpp */, 94CD8B9E2D2E8CE500041BBA /* databaseConnection.hpp */, ); path = include; @@ -3631,6 +3637,7 @@ 94674B872D533B4000973137 /* tradeManager.cpp in Sources */, 94CD8BA02D2E8CE500041BBA /* databaseConnection.cpp in Sources */, 940A61132C92CE210083FEB8 /* configManager.cpp in Sources */, + 94724A842F8B92C10029B940 /* operations.cpp in Sources */, 940A61172C92CE960083FEB8 /* serviceA.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3646,6 +3653,7 @@ 94674B8D2D533E7800973137 /* trade.cpp in Sources */, 941B549A2D3BBADE00E3BF64 /* trading_definitions_json.cpp in Sources */, 94674B8A2D533BDA00973137 /* tradeManager.mm in Sources */, + 94724A832F8B92C10029B940 /* operations.cpp in Sources */, 940A61182C92CE960083FEB8 /* serviceA.cpp in Sources */, 94674B882D533B4000973137 /* tradeManager.cpp in Sources */, 943398272D57E54000287A2D /* jsonParser.mm in Sources */, @@ -3788,6 +3796,7 @@ "\"$(SRCROOT)/external/libpqxx/build/src\"", "/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql", ); + MACOSX_DEPLOYMENT_TARGET = 26.0; OTHER_LDFLAGS = ""; OTHER_LIBTOOLFLAGS = ""; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3810,6 +3819,7 @@ "\"$(SRCROOT)/external/libpqxx/build/src\"", "/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql", ); + MACOSX_DEPLOYMENT_TARGET = 26.0; OTHER_LDFLAGS = ""; OTHER_LIBTOOLFLAGS = ""; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3832,6 +3842,7 @@ "\"$(SRCROOT)/external/libpqxx/build/src\"", "/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql", ); + MACOSX_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.mccaffers.tests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -3854,6 +3865,7 @@ "\"$(SRCROOT)/external/libpqxx/build/src\"", "/opt/homebrew/Cellar/postgresql@18/18.3/lib/postgresql", ); + MACOSX_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.mccaffers.tests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/documents/questdb.md b/documents/questdb.md new file mode 100644 index 0000000..015d25c --- /dev/null +++ b/documents/questdb.md @@ -0,0 +1,5 @@ +Start QuestDB (on macOS) + +``` +JAVA_HOME="/opt/homebrew/opt/openjdk@17" sh $HOME/dev/questdb/questdb.sh start -d $HOME/dev/questdb/data +``` \ No newline at end of file diff --git a/include/databaseConnection.hpp b/include/databaseConnection.hpp index 1502ec2..1f81d45 100644 --- a/include/databaseConnection.hpp +++ b/include/databaseConnection.hpp @@ -20,7 +20,7 @@ class DatabaseConnection { const std::string& password = ""); void printResults(const std::vector& results) const; - std::vector executeQuery(const std::string& query) const; + std::vector streamQuery(const std::string& query) const; const std::string& getConnectionString() const { return connection_string; diff --git a/include/models/priceData.hpp b/include/models/priceData.hpp index 4047ffd..c57a2ff 100644 --- a/include/models/priceData.hpp +++ b/include/models/priceData.hpp @@ -1,17 +1,21 @@ // Backtesting Engine in C++ // -// (c) 2025 Ryan McCaffery | https://mccaffers.com +// (c) 2026 Ryan McCaffery | https://mccaffers.com // This code is licensed under MIT license (see LICENSE.txt for details) // --------------------------------------- #pragma once #include +#include struct PriceData { - double value1; - double value2; + double ask; + double bid; std::chrono::system_clock::time_point timestamp; + std::string symbol; // Constructor for easy creation - PriceData(double v1, double v2, const std::chrono::system_clock::time_point& ts) - : value1(v1), value2(v2), timestamp(ts) {} + PriceData(double ask, double bid, const std::chrono::system_clock::time_point& ts, const std::string& symbol) + : ask(ask), bid(bid), timestamp(ts), symbol(symbol) {} + + PriceData() : ask(0.0), bid(0.0), timestamp{}, symbol("") {} }; diff --git a/include/operations.hpp b/include/operations.hpp new file mode 100644 index 0000000..cdb7cec --- /dev/null +++ b/include/operations.hpp @@ -0,0 +1,15 @@ +// 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 +#include "models/priceData.hpp" + +class Operations { + +public: + static void run(const std::vector& priceData); +}; diff --git a/include/sqlManager.hpp b/include/sqlManager.hpp index 1f65287..0eee8d9 100644 --- a/include/sqlManager.hpp +++ b/include/sqlManager.hpp @@ -1,6 +1,6 @@ // Backtesting Engine in C++ // -// (c) 2025 Ryan McCaffery | https://mccaffers.com +// (c) 2026 Ryan McCaffery | https://mccaffers.com // This code is licensed under MIT license (see LICENSE.txt for details) // --------------------------------------- #pragma once @@ -11,8 +11,7 @@ class SqlManager { public: - static std::vector getInitialPriceData(const DatabaseConnection& db); - static std::string getBaseQuery(); -private: - static constexpr int DEFAULT_LIMIT = 1000; + static std::vector streamPriceData(const DatabaseConnection& db, const std::vector& symbols, int LAST_MONTHS = 1); + static std::string getBaseQuery(const std::vector& symbols, int LAST_MONTHS = 1); + }; diff --git a/scripts/build_dep.sh b/scripts/build_dep.sh new file mode 100644 index 0000000..9afd5e6 --- /dev/null +++ b/scripts/build_dep.sh @@ -0,0 +1,11 @@ +cd ./external/libpqxx + +mkdir -p build +cd ./build + +export PATH="$(brew --prefix libpq)/bin:$PATH" +export PKG_CONFIG_PATH="$(brew --prefix libpq)/lib/pkgconfig:$PKG_CONFIG_PATH" +export PostgreSQL_ROOT="$(brew --prefix libpq)" + +cmake .. -DCMAKE_CXX_STANDARD=20 -DCMAKE_BUILD_TYPE=Release +make \ No newline at end of file diff --git a/scripts/run.sh b/scripts/run.sh index fa3cd8d..cf0baf1 100644 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -5,8 +5,11 @@ current_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # Build the source code # source $current_dir/environment.sh - no longer necessary -source $current_dir/clean.sh -source $current_dir/build.sh +source "$current_dir/clean.sh" +if ! source "$current_dir/build.sh"; then + echo "Error: Build failed. Aborting." + exit 1 +fi # Debug: Check if the executable exists if [ -f "$BUILD_DIR/$EXECUTABLE_NAME" ]; then @@ -49,5 +52,9 @@ output=$(echo "$json" | base64) # Step 6: Run the tests for now (/executable) from the root directory # Passing two arguements, the destination of the QuestDB and the Strategy JSON (in base64) +start_time=$(date +%s%N) ./"$BUILD_DIR/$EXECUTABLE_NAME" localhost "$output" +end_time=$(date +%s%N) +elapsed=$(( (end_time - start_time) / 1000000 )) +echo "Execution time: ${elapsed}ms" diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt new file mode 100644 index 0000000..ce98442 --- /dev/null +++ b/source/CMakeLists.txt @@ -0,0 +1,70 @@ +cmake_minimum_required(VERSION 3.30) + +# CMAKE_OSX_SYSROOT is a macOS-specific setting that specifies the SDK path. +# Only query and set it on Apple platforms, where xcrun is expected to exist. +if(APPLE) + execute_process( + COMMAND xcrun --show-sdk-path + OUTPUT_VARIABLE CMAKE_OSX_SYSROOT + OUTPUT_STRIP_TRAILING_WHITESPACE + ) +endif() + +project(BacktestingEngine) + +# Set the C++ standard +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Configure libpqxx build +set(PQXX_LIBRARIES_INSTALL ON) +set(SKIP_BUILD_TEST ON) +set(SKIP_CONFIGURE_LIBPQXX OFF) + +# Disable warningsfor external libraries +set(PREV_CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS}) +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -w") +elseif(MSVC) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /w") +endif() + +# Quiet CMAKE output +set(CMAKE_INSTALL_MESSAGE NEVER) +set(CMAKE_MESSAGE_LOG_LEVEL "WARNING") + +# Build libpqxx from source +add_subdirectory(external/libpqxx EXCLUDE_FROM_ALL) + +# Include directories +include_directories( + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/include/utilities + ${CMAKE_SOURCE_DIR}/include/models + ${CMAKE_SOURCE_DIR}/include/trading + ${CMAKE_SOURCE_DIR}/include/trading_definitions + ${CMAKE_SOURCE_DIR}/external +) + +# Collect all .cpp files in the src directory +file(GLOB_RECURSE SOURCES "source/*.cpp") + +# Create a library of your project's code +add_library(BacktestingEngineLib STATIC ${SOURCES}) + +# Replace find_package(OpenMP REQUIRED) with this: +if(APPLE) + set(OpenMP_C_FLAGS "-Xclang -fopenmp") + set(OpenMP_CXX_FLAGS "-Xclang -fopenmp") + set(OpenMP_C_LIB_NAMES "omp") + set(OpenMP_CXX_LIB_NAMES "omp") + set(OpenMP_omp_LIBRARY /opt/homebrew/opt/libomp/lib/libomp.dylib) + find_package(OpenMP REQUIRED) + target_include_directories(BacktestingEngineLib PRIVATE /opt/homebrew/opt/libomp/include) +endif() + +target_link_libraries(BacktestingEngineLib PUBLIC pqxx OpenMP::OpenMP_CXX) + +# Main executable +add_executable(BacktestingEngine source/main.cpp) +target_link_libraries(BacktestingEngine BacktestingEngineLib) diff --git a/source/databaseConnection.cpp b/source/databaseConnection.cpp index 292da31..7f050bd 100644 --- a/source/databaseConnection.cpp +++ b/source/databaseConnection.cpp @@ -1,12 +1,40 @@ // Backtesting Engine in C++ // -// (c) 2025 Ryan McCaffery | https://mccaffers.com +// (c) 2026 Ryan McCaffery | https://mccaffers.com // This code is licensed under MIT license (see LICENSE.txt for details) // --------------------------------------- #include "databaseConnection.hpp" #include "base64.hpp" #include +#include +#include +#include + +static std::chrono::system_clock::time_point fastParseTimestamp(const char* ts) { + int year = 0, month = 0, day = 0, hour = 0, min = 0, sec = 0, usec = 0; + const int parsedFields = + std::sscanf(ts, "%4d-%2d-%2d %2d:%2d:%2d.%d", &year, &month, &day, &hour, &min, &sec, &usec); + if (parsedFields != 6 && parsedFields != 7) { + throw std::runtime_error("Invalid timestamp format"); + } + + // Cache timegm per date — tick data is time-ordered so date changes rarely + static char cachedDate[11] = {}; + static time_t cachedEpoch = 0; + if (std::memcmp(ts, cachedDate, 10) != 0) { + std::memcpy(cachedDate, ts, 10); + std::tm tm = {}; + tm.tm_year = year - 1900; + tm.tm_mon = month - 1; + tm.tm_mday = day; + tm.tm_isdst = 0; + cachedEpoch = timegm(&tm); + } + + time_t t = cachedEpoch + hour * 3600 + min * 60 + sec; + return std::chrono::system_clock::from_time_t(t) + std::chrono::microseconds(usec); +} DatabaseConnection::DatabaseConnection(const std::string& endpoint, int port, const std::string& dbname, const std::string& user, @@ -21,37 +49,23 @@ DatabaseConnection::DatabaseConnection(const std::string& endpoint, int port, } -std::vector DatabaseConnection::executeQuery(const std::string& query) const { - std::vector results; - - try { - pqxx::connection conn(this->connection_string); +std::vector DatabaseConnection::streamQuery(const std::string& query) const { + pqxx::connection conn(this->connection_string); + pqxx::nontransaction txn(conn); + pqxx::result result = txn.exec(query); - if (!conn.is_open()) { - throw std::invalid_argument("Failed to open database connection"); - } + std::vector results(result.size()); - std::cout << "Connected to database successfully!" << std::endl; - - pqxx::work txn(conn); - pqxx::result result = txn.exec(query); - - // Convert results to PriceData objects - for (const auto& row : result) { - double value1 = row[0].as(); - double value2 = row[1].as(); - std::string timestamp_str = row[2].as(); - - auto timestamp = Utilities::parseTimestamp(timestamp_str); - - results.emplace_back(value1, value2, timestamp); - } - - txn.commit(); - - } catch (const std::exception& e) { - std::cerr << "Error: " << e.what() << std::endl; - } + for (std::size_t i = 0; i < result.size(); ++i) { + const auto& row = result[i]; + double ask, bid; + auto symbol = row[0].view(); + auto sv1 = row[1].view(); + auto sv2 = row[2].view(); + std::from_chars(sv1.data(), sv1.data() + sv1.size(), ask); + std::from_chars(sv2.data(), sv2.data() + sv2.size(), bid); + results[i] = PriceData(ask, bid, fastParseTimestamp(row[3].c_str()), std::string(symbol)); + } return results; } @@ -70,8 +84,8 @@ void DatabaseConnection::printResults(const std::vector& results) con ss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S"); std::cout << std::fixed << std::setprecision(4) - << data.value1 << "\t" - << data.value2 << "\t" + << data.ask << "\t" + << data.bid << "\t" << ss.str() << std::endl; } } diff --git a/source/main.cpp b/source/main.cpp index 9fb01da..3934e78 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -1,6 +1,6 @@ // Backtesting Engine in C++ // -// (c) 2025 Ryan McCaffery | https://mccaffers.com +// (c) 2026 Ryan McCaffery | https://mccaffers.com // This code is licensed under MIT license (see LICENSE.txt for details) // --------------------------------------- @@ -23,36 +23,28 @@ #include "tradeManager.hpp" #include "jsonParser.hpp" #include "sqlManager.hpp" +#include "operations.hpp" using json = nlohmann::json; +// Entry point. Expects two command-line arguments: +// argv[1] — hostname/IP of the QuestDB instance +// argv[2] — Base64-encoded JSON strategy configuration int main(int argc, const char * argv[]) { - // Connect to QuestDb argv[1] + if (argc < 3) { + std::cerr << "Usage: " << argv[0] << " " << std::endl; + return 1; + } DatabaseConnection db(argv[1], 8812, "qdb", "admin", "quest"); - // Load strategy from Base64 argv[2] JsonParser::parseConfigurationFromBase64(argv[2]); - std::vector priceData = SqlManager::getInitialPriceData(db); - - // Convert timestamp to readable format for debugging - auto timeT = std::chrono::system_clock::to_time_t(priceData[0].timestamp); - std::cout << "Timestamp: " << std::put_time(std::localtime(&timeT), "%Y-%m-%d %H:%M:%S") << std::endl; - - auto tradeManager = TradeManager::getInstance(); - - // Open a trade - std::string tradeId = tradeManager->openTrade(1.2345, 100000, true); - std::cout << "Opened trade: " << tradeId << std::endl; - - // Review account - size_t openTrades = tradeManager->reviewAccount(); - std::cout << "Number of open trades: " << openTrades << std::endl; + std::vector symbols = {"AUSIDXAUD", "EURUSD"}; + std::vector ticks = SqlManager::streamPriceData(db, symbols, 1); + printf("Total ticks streamed: %zu\n", ticks.size()); - // Close trade - bool closed = tradeManager->closeTrade(tradeId); - std::cout << "Trade closed: " << (closed ? "yes" : "no") << std::endl; + Operations::run(ticks); return 0; diff --git a/source/operations.cpp b/source/operations.cpp new file mode 100644 index 0000000..7aae2d1 --- /dev/null +++ b/source/operations.cpp @@ -0,0 +1,54 @@ +// Backtesting Engine in C++ +// +// (c) 2026 Ryan McCaffery | https://mccaffers.com +// This code is licensed under MIT license (see LICENSE.txt for details) +// --------------------------------------- + +#include "operations.hpp" +// std headers +#include +#include +#include +#include +#include +#include +#include +#include "tradeManager.hpp" + +void Operations::run(const std::vector& ticks) { + + // Loop around every tick + // Example output: + // symbol=AUSIDXAUD, ask=8602.4000, bid=8599.4000 timestamp=2026-03-12 18:39:01.076 + // symbol=AUSIDXAUD, ask=8602.9000, bid=8599.9000 timestamp=2026-03-12 18:39:01.584 + // symbol=EURUSD, ask=1.1513, bid=1.1512 timestamp=2026-03-12 18:39:01.644 + // symbol=AUSIDXAUD, ask=8602.4000, bid=8599.4000 timestamp=2026-03-12 18:39:01.770 + // symbol=AUSIDXAUD, ask=8601.9000, bid=8598.9000 timestamp=2026-03-12 18:39:01.982 + + for (const auto& tick : ticks) { + // (void)tick; + + // print first tick + auto time_t = std::chrono::system_clock::to_time_t(tick.timestamp); + struct tm tm = {}; + if (localtime_r(&time_t, &tm) == nullptr) { + std::cerr << "Error: failed to convert timestamp" << std::endl; + } + char buffer[20]; + std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm); + auto ms = std::chrono::duration_cast(tick.timestamp.time_since_epoch()) % 1000; + printf("symbol=%s, ask=%.4f, bid=%.4f timestamp=%s.%03lld\n", tick.symbol.c_str(), tick.ask, tick.bid, buffer, + static_cast(ms.count())); + } + + auto tradeManager = TradeManager::getInstance(); + + // std::string tradeId = tradeManager->openTrade(ticks[0].ask, 100000, true); + // std::cout << "Opened trade: " << tradeId << std::endl; + + // size_t openTrades = tradeManager->reviewAccount(); + // std::cout << "Number of open trades: " << openTrades << std::endl; + + // bool closed = tradeManager->closeTrade(tradeId); + // std::cout << "Trade closed: " << (closed ? "yes" : "no") << std::endl; +} diff --git a/source/sqlManager.cpp b/source/sqlManager.cpp index 12152cd..9de55ce 100644 --- a/source/sqlManager.cpp +++ b/source/sqlManager.cpp @@ -1,14 +1,32 @@ // Backtesting Engine in C++ // -// (c) 2025 Ryan McCaffery | https://mccaffers.com +// (c) 2026 Ryan McCaffery | https://mccaffers.com // This code is licensed under MIT license (see LICENSE.txt for details) // --------------------------------------- #include "sqlManager.hpp" +#include +#include +#include -std::string SqlManager::getBaseQuery() { - return "SELECT * FROM EURUSD LIMIT " + std::to_string(DEFAULT_LIMIT) + ";"; +std::string SqlManager::getBaseQuery(const std::vector& symbols, int LAST_MONTHS) { + if (symbols.empty()) { + return ""; + } + + std::string query; + for (size_t i = 0; i < symbols.size(); ++i) { + if (i > 0) { + query += " UNION ALL "; + } + query += "SELECT '" + symbols[i] + "' as symbol, * FROM '" + symbols[i] + "' WHERE timestamp >= dateadd('M', -" + std::to_string(LAST_MONTHS) + ", now())"; + } + query += " ORDER BY timestamp"; + + return query; } -std::vector SqlManager::getInitialPriceData(const DatabaseConnection& db) { - return db.executeQuery(getBaseQuery()); +std::vector SqlManager::streamPriceData(const DatabaseConnection& db, const std::vector& symbols, int LAST_MONTHS) { + std::string query = getBaseQuery(symbols, LAST_MONTHS); + std::cout << "Executing query: " << query << std::endl; + return db.streamQuery(query); } diff --git a/source/jsonParser.cpp b/source/utilities/jsonParser.cpp similarity index 93% rename from source/jsonParser.cpp rename to source/utilities/jsonParser.cpp index 648ab06..5c6c630 100644 --- a/source/jsonParser.cpp +++ b/source/utilities/jsonParser.cpp @@ -1,6 +1,6 @@ // Backtesting Engine in C++ // -// (c) 2025 Ryan McCaffery | https://mccaffers.com +// (c) 2026 Ryan McCaffery | https://mccaffers.com // This code is licensed under MIT license (see LICENSE.txt for details) // ---------------------------------------