diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..fb12896 --- /dev/null +++ b/.clang-format @@ -0,0 +1,11 @@ +BasedOnStyle: Google +IndentWidth: 4 +ColumnLimit: 100 +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +BreakBeforeBraces: Attach +PointerAlignment: Left +DerivePointerAlignment: false +SortIncludes: CaseInsensitive +IncludeBlocks: Regroup diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3bd66cb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI +on: + push: + branches: [main, "phase-*"] + pull_request: + branches: [main] + +jobs: + build-and-test: + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies (Ubuntu) + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev + + - name: Configure + run: cmake -B build -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build --parallel + + - name: Test + run: ctest --test-dir build --output-on-failure diff --git a/.gitignore b/.gitignore index 259148f..29dc6ac 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,14 @@ *.exe *.out *.app + +# Build directories +build/ +cmake-build-*/ +.cache/ +compile_commands.json + +# IDE +.vscode/ +.idea/ +*.swp diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..95bc1ab --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,72 @@ +# Barcola — Project Conventions + +## What This Is +A C++17 quantitative trading toolkit being transformed from a learning project into a +quant-grade portfolio piece. The end goal: a fully backtested mean reversion strategy +on real market data, with verified math, professional engineering, and documented results. + +## Architecture +- `src/core/` — Foundational types: PricePoint, PriceHistory, time utilities +- `src/indicators/` — Technical indicators: SMA, EMA, RSI, Bollinger Bands +- `src/analysis/` — Market analysis: mean reversion, correlation, market data analysis +- `src/risk/` — Risk management: position sizing, slippage modeling, dynamic hedging +- `src/simulation/` — Monte Carlo simulation +- `src/backtest/` — (Phase 2) Backtesting engine, strategy interface, performance metrics +- `tests/` — Google Test suite, one test file per module +- `data/` — Static CSV files for reproducible testing (never depend on network for tests) + +## Build +``` +cmake -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --parallel +ctest --test-dir build --output-on-failure +``` + +## C++ Standards & Idioms +- **C++17 required.** Use `std::optional`, `std::string_view`, structured bindings, + `[[nodiscard]]`, `if constexpr` where appropriate. +- **No raw `new`/`delete`.** Stack-allocate by default. Use `std::unique_ptr` only when + polymorphism or heap allocation is genuinely needed. +- **Const correctness.** All getters are `const`. Pass by `const&` unless mutation is needed. + Prefer `const` local variables. +- **RAII everywhere.** Resources (CURL handles, file handles) are managed by constructors/ + destructors or scope guards, never manual cleanup. +- **No `using namespace std;` in headers.** Allowed in .cpp files at function scope only. +- **`#pragma once`** for all headers. +- **Error handling:** Throw `std::runtime_error` or `std::invalid_argument` for unrecoverable + errors. Return `std::optional` for expected-empty cases. Never silently swallow failures. +- **No magic numbers.** Use named constants (`constexpr double RISK_FREE_RATE = 0.05;`). + +## Math Correctness (Critical) +- Every numerical function must handle edge cases: division by zero, empty inputs, + insufficient data points, NaN/infinity propagation. +- All formulas must be verifiable against hand-calculated known values in tests. +- When in doubt, return a safe sentinel (0.0 for undefined correlation, 100.0 for RSI + with no losses) and document the choice. + +## Testing Conventions +- One test file per module: `tests/test_.cpp` +- Use `EXPECT_DOUBLE_EQ` for exact floating-point matches, `EXPECT_NEAR(a, b, 1e-10)` + for computed values with rounding. +- Every bug fix gets a regression test that would have failed before the fix. +- Test data comes from CSV files in `data/`, path injected via `BARCOLA_TEST_DATA_DIR` + compile definition. Tests must NEVER hit the network. +- Name tests descriptively: `TEST(RSITest, AllGains_Returns100)`, not `TEST(RSI, Test1)`. + +## Git Conventions +- Feature branches: `phase-N/description` (e.g., `phase-1/foundation-hardening`) +- Commit messages: imperative mood, concise. +- Each commit should compile and pass tests. + +## What NOT to Do +- Do not add external trading libraries. Everything is built from scratch. +- Do not depend on Yahoo Finance API for tests or CI. Use CSV files. +- Do not optimize prematurely. Correctness first, then clarity, then performance. +- Do not add features from later phases during Phase 1. + +## Roadmap Reference +Phase 1: Foundation hardening (CMake, tests, CI, bug fixes, CSV support) +Phase 2: Backtesting engine (strategy interface, backtest engine, performance metrics) +Phase 3: Run mean reversion backtest + document results +Phase 4: Second strategy (dual MA crossover) + comparison +Phase 5: Polish, visualizations, README rewrite diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..744c4dd --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.16) +project(Barcola VERSION 0.1.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +find_package(CURL REQUIRED) + +add_subdirectory(src) + +option(BARCOLA_BUILD_TESTS "Build tests" ON) +if(BARCOLA_BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/Main/.gitattributes b/Main/.gitattributes deleted file mode 100644 index dfe0770..0000000 --- a/Main/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto diff --git a/Main/.vscode/settings.json b/Main/.vscode/settings.json deleted file mode 100644 index 522cbe3..0000000 --- a/Main/.vscode/settings.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "files.associations": { - "iosfwd": "cpp", - "__bit_reference": "cpp", - "__bits": "cpp", - "__config": "cpp", - "__debug": "cpp", - "__errc": "cpp", - "__hash_table": "cpp", - "__locale": "cpp", - "__mutex_base": "cpp", - "__node_handle": "cpp", - "__split_buffer": "cpp", - "__threading_support": "cpp", - "__tuple": "cpp", - "__verbose_abort": "cpp", - "array": "cpp", - "atomic": "cpp", - "bit": "cpp", - "bitset": "cpp", - "cctype": "cpp", - "clocale": "cpp", - "cmath": "cpp", - "complex": "cpp", - "cstdarg": "cpp", - "cstddef": "cpp", - "cstdint": "cpp", - "cstdio": "cpp", - "cstdlib": "cpp", - "cstring": "cpp", - "ctime": "cpp", - "cwchar": "cpp", - "cwctype": "cpp", - "exception": "cpp", - "initializer_list": "cpp", - "ios": "cpp", - "iostream": "cpp", - "istream": "cpp", - "limits": "cpp", - "locale": "cpp", - "memory": "cpp", - "mutex": "cpp", - "new": "cpp", - "optional": "cpp", - "ostream": "cpp", - "ratio": "cpp", - "sstream": "cpp", - "stdexcept": "cpp", - "streambuf": "cpp", - "string": "cpp", - "string_view": "cpp", - "system_error": "cpp", - "tuple": "cpp", - "type_traits": "cpp", - "typeinfo": "cpp", - "unordered_map": "cpp", - "variant": "cpp", - "vector": "cpp", - "__nullptr": "cpp", - "__string": "cpp", - "chrono": "cpp", - "compare": "cpp", - "concepts": "cpp", - "algorithm": "cpp", - "numeric": "cpp", - "random": "cpp" - } -} \ No newline at end of file diff --git a/Main/README.md b/Main/README.md deleted file mode 100644 index 1a49a5a..0000000 --- a/Main/README.md +++ /dev/null @@ -1,251 +0,0 @@ -# QuantX - -QuantX encompasses modules for real-time stock data collection, analysis, AI-driven position sizing, and data insights, providing traders with informed decision-making capabilities. Highlighting dynamic hedging strategies, advanced simulations, and slippage modeling, QuantX facilitates risk reduction and strategy enhancement. With its technical analysis tools and API integration, the project offers a robust platform for quantitative trading and in-depth market analysis. - - -## Features - - -- **Position Sizing Module:** - - Calculates position sizes based on risk percentage and stop loss percentage. - - Supports calculation of position size with a specified maximum loss amount. - -- **Moving Average Module:** - - Computes simple moving averages (SMA) and exponential moving averages (EMA) for a given window size. - - Tracks and updates moving averages as new data points are added. - -- **Mean Reversion Analysis Module:** - - Analyzes mean reversion using historical price data. - - Provides methods to calculate mean reversion values, average mean reversion, and identify extreme values. - -- **Market Data Analysis Module:** - - Performs various market data analyses on historical price data. - - Calculates average price, volatility, upward and downward trends, and the relative strength index (RSI). - -- **Dynamic Hedging Module:** - - Implements dynamic hedging strategies based on moving averages. - - Determines short and long moving averages windows for analysis. - - Outputs moving average values and their changes over time. - -- **Correlation Analysis Module:** - - Conducts correlation analysis between two sets of historical price data. - - Calculates the correlation coefficient to measure the relationship between asset prices. - -- **Slippage Modeling Module:** - - Models slippage effects on trading. - - Considers slippage due to market conditions and trading volume. - - Adjusts slippage based on trade volume and market volatility. - -- **Price History Management:** - - Manages historical price data for various assets. - - Fetches historical price data using Yahoo Finance API. - - Stores and organizes data points for analysis. - -- **Advanced Simulation:** - - Performs Monte Carlo simulations to assess trading strategy performance. - - Simulates multiple scenarios with varying initial investments. - -- **Time and Date Utilities:** - - Converts between different time representations (POSIX timestamps and human-readable dates). - - Compares dates and checks their order. - - - -```cpp -//sys.cpp -int main() { - - PriceHistory *snp500History = new PriceHistory("AAPL"); - PriceHistory *eurusdHistory = new PriceHistory("EURUSD=X"); - PriceHistory *euraudHistory = new PriceHistory("EURAUD=X"); - - - snp500History->fetchHistoricalData("2017-12-01", "2017-12-31", "1d"); - eurusdHistory->fetchHistoricalData("2018-01-01", "2018-01-10", "1d"); - euraudHistory->fetchHistoricalData("2018-01-01", "2018-01-10", "1d"); - - - cout << "Historical data for S&P 500 (Dec 2017):" << endl; - snp500History->printDataPoints(); - - - try { - PricePoint dataPoint = snp500History->getDataPoint("2017-12-01"); - cout << "Data point at 2017-12-01:" << endl; - dataPoint.printPricePoint(); - } catch (const exception &e) { - cerr << e.what() << endl; - } - - - cout << "EUR/USD rates (Jan 2018):" << endl; - eurusdHistory->printDataPoints(); - - - cout << "EUR/AUD rates (Jan 2018):" << endl; - euraudHistory->printDataPoints(); - - - delete snp500History; - delete eurusdHistory; - delete euraudHistory; -} - -``` -```cpp -//position_sizing.cpp -int main() { - // Create an instance of the PositionSizing class with a risk percentage and stop loss percentage - PositionSizing positionSizer(2.5, 0.02); // Example risk: 2.5%, stop loss: 2% - - // Calculate position size based on risk percentage and stop loss - double portfolioSize = 100000; // Example portfolio size: $100,000 - double entryPrice = 50; // Example entry price: $50 per share - double positionSize = positionSizer.calculatePositionSize(portfolioSize, entryPrice); - - std::cout << "Calculated Position Size: " << positionSize << " shares" << std::endl; - - // Modify risk and stop loss percentages - positionSizer.setRiskPercentage(3.0); // Change risk percentage to 3% - positionSizer.setStopLossPercentage(0.03); // Change stop loss percentage to 3% - - // Calculate position size with a maximum loss amount - double maxLossAmount = 2500; // Example maximum allowable loss: $2,500 - double positionSizeWithMaxLoss = positionSizer.calculatePositionSizeWithMaxLoss(portfolioSize, entryPrice, maxLossAmount); - - std::cout << "Position Size with Max Loss: " << positionSizeWithMaxLoss << " shares" << std::endl; - - return 0; -} - -``` - -```cpp -//moving_averages.cpp -int main() { - // Create a PriceHistory instance for the "AAPL" asset - PriceHistory priceHistory("AAPL"); - - // Fetch historical data for the specified date range and interval - priceHistory.fetchHistoricalData("2023-01-01", "2023-08-31", "1d"); - - // Define window size for moving averages and EMA smoothing factor - size_t windowSize = 5; - double emaSmoothingFactor = 0.2; - - // Calculate and display moving averages for the fetched data - priceHistory.calculateMovingAverages(windowSize, emaSmoothingFactor); - - return 0; -} -``` - -```cpp -//mean_reversion.cpp -int main() { - PriceHistory priceHistory("AAPL"); // Create a PriceHistory instance for the "AAPL" asset - priceHistory.fetchHistoricalData("2023-01-01", "2023-08-31", "1d"); // Fetch historical data - - int movingWindowSize = 10; // Define the moving window size for mean reversion calculation - MeanReversion meanReversion(priceHistory, movingWindowSize); // Create a MeanReversion instance - - meanReversion.calculateMeanReversion(); // Calculate mean reversion values - meanReversion.printMeanReversionResults(); // Print mean reversion results - - // Get statistics about mean reversion values - std::cout << "Average Mean Reversion: " << meanReversion.getAverageMeanReversion() << std::endl; - std::cout << "Max Mean Reversion Index: " << meanReversion.getMaxMeanReversionIndex() << std::endl; - std::cout << "Min Mean Reversion Index: " << meanReversion.getMinMeanReversionIndex() << std::endl; - - return 0; -} -``` - -```cpp -//market_data_analysis.cpp -int main() { - MarketDataAnalysis marketAnalysis("AAPL"); // Create a MarketDataAnalysis instance for "AAPL" asset - marketAnalysis.priceHistory_.fetchHistoricalData("2023-01-01", "2023-08-31", "1d"); // Fetch historical data - - double avgPrice = marketAnalysis.calculateAveragePrice(); - double volatility = marketAnalysis.calculateVolatility(); - cout << "Average Price: " << avgPrice << endl; - cout << "Volatility: " << volatility << endl; - - vector upwardTrends = marketAnalysis.getUpwardTrends(); - vector downwardTrends = marketAnalysis.getDownwardTrends(); - cout << "Number of Upward Trends: " << upwardTrends.size() << endl; - cout << "Number of Downward Trends: " << downwardTrends.size() << endl; - - size_t rsiPeriod = 14; // RSI calculation period - double rsiValue = marketAnalysis.calculateRelativeStrengthIndex(rsiPeriod); - cout << "RSI for period " << rsiPeriod << ": " << rsiValue << endl; - - return 0; -} - -``` - -```cpp -//dynamic_hedging.cpp -int main() { - string assetSymbol = "AAPL"; - const char* startDate = "2023-01-01"; - const char* endDate = "2023-08-31"; - const char* interval = "1d"; - - DynamicHedging dynamicHedging(assetSymbol, startDate, endDate, interval); - dynamicHedging.performDynamicHedging(); - - return 0; -} -```cpp -//correlation_analysis.cpp -int main() { - string assetSymbolA = "AAPL"; // Asset symbol for the first historical data - string assetSymbolB = "MSFT"; // Asset symbol for the second historical data - - const char* startDate = "2023-01-01"; - const char* endDate = "2023-08-31"; - const char* interval = "1d"; - - // Create PriceHistory instances for both assets and fetch historical data - PriceHistory historyA(assetSymbolA); - historyA.fetchHistoricalData(startDate, endDate, interval); - - PriceHistory historyB(assetSymbolB); - historyB.fetchHistoricalData(startDate, endDate, interval); - - // Create a CorrelationAnalysis instance and calculate correlation - CorrelationAnalysis correlationAnalysis(historyA, historyB); - double correlation = correlationAnalysis.calculateCorrelation(); - - std::cout << "Correlation between " << assetSymbolA << " and " << assetSymbolB << ": " << correlation << std::endl; - - return 0; -} - -``` - -```cpp -//slippage_model -int main() { - PriceHistory priceHistory("AAPL"); - priceHistory.fetchHistoricalData("2023-01-01", "2023-08-01", "1d"); - - SlippageModel slippageModel(priceHistory, 0.01); - - // Convert date strings to time_t using a function like convertDateToEpoch() - - time_t entryDate = convertDateToEpoch("2023-03-15"); - double entryPrice = 150.0; - time_t exitDate = convertDateToEpoch("2023-06-15"); - double exitPrice = 170.0; - double tradeVolume = 10000.0; - - double calculatedSlippage = slippageModel.calculateSlippage(entryDate, entryPrice, exitDate, exitPrice, tradeVolume); - std::cout << "Calculated slippage: " << calculatedSlippage << std::endl; - - return 0; -} -``` diff --git a/Main/correlation_analysis.cpp b/Main/correlation_analysis.cpp deleted file mode 100644 index da9234f..0000000 --- a/Main/correlation_analysis.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include -#include -#include "./includes/correlation_analysis.hpp" - -CorrelationAnalysis::CorrelationAnalysis(PriceHistory &historyA, PriceHistory &historyB) - : historyA(historyA), historyB(historyB) {} - -double CorrelationAnalysis::calculateCorrelation() { - size_t minDataPoints = std::min(historyA.dataPointsCount(), historyB.dataPointsCount()); - - std::vector pricesA; - std::vector pricesB; - - for (size_t i = 0; i < minDataPoints; ++i) { - pricesA.push_back(historyA.getDataPoint(i).getClosing()); - pricesB.push_back(historyB.getDataPoint(i).getClosing()); - } - - double meanA = std::accumulate(pricesA.begin(), pricesA.end(), 0.0) / pricesA.size(); - double meanB = std::accumulate(pricesB.begin(), pricesB.end(), 0.0) / pricesB.size(); - - double covariance = 0.0; - double varianceA = 0.0; - double varianceB = 0.0; - - for (size_t i = 0; i < minDataPoints; ++i) { - double deviationA = pricesA[i] - meanA; - double deviationB = pricesB[i] - meanB; - - covariance += deviationA * deviationB; - varianceA += deviationA * deviationA; - varianceB += deviationB * deviationB; - } - - covariance /= minDataPoints; - varianceA /= minDataPoints; - varianceB /= minDataPoints; - - double correlation = covariance / (std::sqrt(varianceA) * std::sqrt(varianceB)); - return correlation; -} diff --git a/Main/dynamic_hedging.cpp b/Main/dynamic_hedging.cpp deleted file mode 100644 index 45107ec..0000000 --- a/Main/dynamic_hedging.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#include -#include -#include -#include "./includes/data_sys.hpp" -class DynamicHedging { -public: - DynamicHedging(const string& assetSymbol, const char* startDate, const char* endDate, const char* interval) - : priceHistory(assetSymbol) { - priceHistory.fetchHistoricalData(startDate, endDate, interval); - } - -void performDynamicHedging() { - const size_t shortWindow = 20; - const size_t longWindow = 50; - - size_t dataSize = priceHistory.dataPointsCount(); - if (dataSize < longWindow) { - cout << "Insufficient data for hedging." << endl; - return; - } - - vector shortMA(dataSize - shortWindow + 1, 0.0); - vector longMA(dataSize - longWindow + 1, 0.0); - for (size_t i = 0; i < dataSize - shortWindow + 1; ++i) { - double sumShort = 0.0; - double sumLong = 0.0; - for (size_t j = 0; j < shortWindow; ++j) { - sumShort += priceHistory.getDataPoint(i + j).getClosing(); - if (j < longWindow) { - sumLong += priceHistory.getDataPoint(i + j).getClosing(); - } - } - shortMA[i] = sumShort / shortWindow; - longMA[i] = sumLong / longWindow; - } - cout << "Moving Averages:" << endl; - for (size_t i = 0; i < shortMA.size(); ++i) { - PricePoint currentDataPoint = priceHistory.getDataPoint(i + shortWindow - 1); - cout << "Date: " << currentDataPoint.getDateString() - << " Short MA: " << shortMA[i] - << " Long MA: " << longMA[i] << endl; - } -} - - -private: - PriceHistory priceHistory; -}; - diff --git a/Main/includes/correlation_analysis.hpp b/Main/includes/correlation_analysis.hpp deleted file mode 100644 index ec5a233..0000000 --- a/Main/includes/correlation_analysis.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef CORRELATION_ANALYSIS_HPP -#define CORRELATION_ANALYSIS_HPP - -#include "data_sys.hpp" - -class CorrelationAnalysis { -public: - CorrelationAnalysis(PriceHistory &historyA, PriceHistory &historyB); - double calculateCorrelation(); - -private: - PriceHistory &historyA; - PriceHistory &historyB; -}; - -#endif diff --git a/Main/includes/data_sys.hpp b/Main/includes/data_sys.hpp deleted file mode 100644 index f59fee3..0000000 --- a/Main/includes/data_sys.hpp +++ /dev/null @@ -1,83 +0,0 @@ -#include -#include -#include -#include -#include -#include -using namespace std; -time_t getCurrentPosixTimestamp(); -time_t convertDateToEpoch(const char *date); -string convertEpochToDate(const time_t epoch); -bool isDateBeforeOrEqual(const char *dateA, const char *dateB); - -class PricePoint { - -public: - - PricePoint(time_t date, double opening, double highest, double lowest, double closing); - PricePoint(string date, double opening, double highest, double lowest, double closing); - PricePoint(time_t date, double price); - PricePoint(string date, double price); - ~PricePoint(); - time_t getDate(); - string getDateString(); - - double getOpening(); - double getHighest(); - double getLowest(); - double getClosing(); - - string toString(); - void printPricePoint(); -private: - time_t date; - double opening; - double highest; - double lowest; - double closing; -}; - -size_t responseCallback(char *data, size_t itemSize, size_t itemCount, void *userData); -string fetchYahooCsvData( - string stockSymbol, - time_t startTime, - time_t endTime, - string intervalType -); -class PriceHistory { - -public: - PriceHistory(string assetSymbol); - ~PriceHistory(); - size_t dataPointsCount(); - PricePoint getDataPoint(size_t index); - PricePoint getDataPoint(time_t date); - PricePoint getDataPoint(string date); - void printDataPoints(); - void clearDataPoints(); - string getHistoricalCsv(time_t startDate, - time_t endDate, - const char *interval); - void fetchHistoricalData(time_t startDate, - time_t endDate, - const char *interval); - void fetchHistoricalData(const char *startDate, - const char *endDate, - const char *interval); - void calculateMovingAverages(size_t windowSize, double emaSmoothingFactor); - void calculatePositionSizeForTrade( - size_t dataIndex, - double portfolioSize, - double riskPercentage, - double stopLossPercentage - ); - pair, vector> calculateSMA(size_t windowSize); - pair, vector> calculateEMA(size_t windowSize, double emaSmoothingFactor); - - vector calculateBollingerBands(size_t period, double stdDevFactor); - double PriceHistory::performMonteCarloSimulationAdvanced(size_t numSimulations, double initialInvestment); -private: - string assetSymbol; - vector dataPoints; -}; - \ No newline at end of file diff --git a/Main/market_data_analysis.cpp b/Main/market_data_analysis.cpp deleted file mode 100644 index f49b300..0000000 --- a/Main/market_data_analysis.cpp +++ /dev/null @@ -1,112 +0,0 @@ - - -#include -#include -#include -#include "sys.cpp" -using namespace std; -class MarketDataAnalysis { -public: - MarketDataAnalysis(string assetSymbol); - - double calculateAveragePrice(); - double calculateVolatility(); - vector getUpwardTrends(); - vector getDownwardTrends(); - double calculateRelativeStrengthIndex(size_t period); - - - PriceHistory priceHistory_; -}; - - -MarketDataAnalysis::MarketDataAnalysis(string assetSymbol) : priceHistory_(assetSymbol) {} - -double MarketDataAnalysis::calculateAveragePrice() { - double total = 0.0; - size_t count = priceHistory_.dataPointsCount(); - - for (size_t i = 0; i < count; ++i) { - total += priceHistory_.getDataPoint(i).getClosing(); - } - - return total / count; -} - -double MarketDataAnalysis::calculateVolatility() { - size_t count = priceHistory_.dataPointsCount(); - if (count < 2) { - return 0.0; - } - - double sumSquaredDifferences = 0.0; - double averagePrice = calculateAveragePrice(); - - for (size_t i = 0; i < count; ++i) { - double price = priceHistory_.getDataPoint(i).getClosing(); - sumSquaredDifferences += (price - averagePrice) * (price - averagePrice); - } - - return sqrt(sumSquaredDifferences / (count - 1)); -} - -vector MarketDataAnalysis::getUpwardTrends() { - vector upwardTrends; - size_t count = priceHistory_.dataPointsCount(); - - for (size_t i = 1; i < count; ++i) { - PricePoint prevPoint = priceHistory_.getDataPoint(i - 1); - PricePoint currentPoint = priceHistory_.getDataPoint(i); - - if (currentPoint.getClosing() > prevPoint.getClosing()) { - upwardTrends.push_back(currentPoint); - } - } - - return upwardTrends; -} - -vector MarketDataAnalysis::getDownwardTrends() { - vector downwardTrends; - size_t count = priceHistory_.dataPointsCount(); - - for (size_t i = 1; i < count; ++i) { - PricePoint prevPoint = priceHistory_.getDataPoint(i - 1); - PricePoint currentPoint = priceHistory_.getDataPoint(i); - - if (currentPoint.getClosing() < prevPoint.getClosing()) { - downwardTrends.push_back(currentPoint); - } - } - - return downwardTrends; -} - - double MarketDataAnalysis::calculateRelativeStrengthIndex(size_t period) { - size_t count = priceHistory_.dataPointsCount(); - if (count <= period) { - return 0.0; - } - double avgGain = 0.0; - double avgLoss = 0.0; - for (size_t i = 1; i < period; ++i) { - double diff = priceHistory_.getDataPoint(i).getClosing() - priceHistory_.getDataPoint(i - 1).getClosing(); - if (diff > 0) { - avgGain += diff; - } else { - avgLoss -= diff; - } - } - avgGain /= period; - avgLoss /= period; - double relativeStrength = avgGain / avgLoss; - double rsi = 100.0 - (100.0 / (1.0 + relativeStrength)); - - return rsi; -} - - - - - - diff --git a/Main/mean_reversion.cpp b/Main/mean_reversion.cpp deleted file mode 100644 index 67d7b99..0000000 --- a/Main/mean_reversion.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#include -#include - -#include "./includes/data_sys.hpp" - -class MeanReversion { -public: - MeanReversion(const PriceHistory& history, int windowSize); - void calculateMeanReversion(); - void printMeanReversionResults(); - double getMeanReversionValue(size_t index) const; - double getAverageMeanReversion() const; - size_t getMaxMeanReversionIndex() const; - size_t getMinMeanReversionIndex() const; - -private: - const PriceHistory& priceHistory; - int movingWindowSize; - std::vector meanReversionValues; - - double calculateMovingAverage(size_t startIndex) const; -}; - - - -double MeanReversion::getMeanReversionValue(size_t index) const { - if (index < meanReversionValues.size()) { - return meanReversionValues[index]; - } - throw std::out_of_range("Index out of range."); -} - -double MeanReversion::getAverageMeanReversion() const { - double sum = 0.0; - for (double value : meanReversionValues) { - sum += value; - } - return sum / meanReversionValues.size(); -} - -size_t MeanReversion::getMaxMeanReversionIndex() const { - double maxValue = *std::max_element(meanReversionValues.begin(), meanReversionValues.end()); - return std::distance(meanReversionValues.begin(), std::find(meanReversionValues.begin(), meanReversionValues.end(), maxValue)); -} - -size_t MeanReversion::getMinMeanReversionIndex() const { - double minValue = *std::min_element(meanReversionValues.begin(), meanReversionValues.end()); - return std::distance(meanReversionValues.begin(), std::find(meanReversionValues.begin(), meanReversionValues.end(), minValue)); -} diff --git a/Main/moving_average.cpp b/Main/moving_average.cpp deleted file mode 100644 index d9a4ffe..0000000 --- a/Main/moving_average.cpp +++ /dev/null @@ -1,68 +0,0 @@ -#include -#include -#include -#include "./includes/data_sys.hpp" - -class MovingAverage { -public: - MovingAverage(size_t windowSize); - void addData(double value); - double getSMA(); - double getEMA(double smoothingFactor); - -private: - size_t windowSize; - std::vector data; -}; - -MovingAverage::MovingAverage(size_t windowSize) : windowSize(windowSize) {} - -void MovingAverage::addData(double value) { - data.push_back(value); - if (data.size() > windowSize) { - data.erase(data.begin()); - } -} - -double MovingAverage::getSMA() { - double sum = 0.0; - for (double value : data) { - sum += value; - } - return sum / data.size(); -} - -double MovingAverage::getEMA(double smoothingFactor) { - double ema = data.back(); - - for (size_t i = data.size() - 2; i < data.size(); --i) { - ema = (data[i] - ema) * smoothingFactor + ema; - } - -return ema; -} - -void PriceHistory::calculateMovingAverages(size_t windowSize, double emaSmoothingFactor) { - MovingAverage sma(windowSize); - MovingAverage ema(windowSize); - - for (size_t i = 0; i < dataPoints.size(); ++i) { - double closingPrice = dataPoints[i].getClosing(); - sma.addData(closingPrice); - ema.addData(closingPrice); - - std::cout << "Date: " << dataPoints[i].getDateString() - << " Closing Price: " << closingPrice - << " SMA: " << sma.getSMA() - << " EMA: " << ema.getEMA(emaSmoothingFactor) << std::endl; -} -} - -int main() { - PriceHistory priceHistory("AAPL"); - priceHistory.fetchHistoricalData("2023-01-01", "2023-08-31", "1d"); - size_t windowSize = 5; - double emaSmoothingFactor = 0.2; - priceHistory.calculateMovingAverages(windowSize, emaSmoothingFactor); - return 0; -} diff --git a/Main/position_sizing.cpp b/Main/position_sizing.cpp deleted file mode 100644 index a50072d..0000000 --- a/Main/position_sizing.cpp +++ /dev/null @@ -1,30 +0,0 @@ - -#include -#include -#include - -class PositionSizing { -public: - PositionSizing(double riskPercentage, double stopLossPercentage) - : riskPercentage(riskPercentage), stopLossPercentage(stopLossPercentage) {} - void setRiskPercentage(double percentage) { - riskPercentage = percentage; - } - void setStopLossPercentage(double percentage) { - stopLossPercentage = percentage; - } - double calculatePositionSize(double portfolioSize, double entryPrice) { - double riskAmount = portfolioSize * riskPercentage; - double stopLossAmount = entryPrice * stopLossPercentage; - double positionSize = riskAmount / stopLossAmount; - return positionSize; - } - double calculatePositionSizeWithMaxLoss(double portfolioSize, double entryPrice, double maxLossAmount) { - double positionSize = maxLossAmount / (entryPrice * stopLossPercentage); - return positionSize; - } - -private: - double riskPercentage; - double stopLossPercentage; -}; diff --git a/Main/slippage_model.cpp b/Main/slippage_model.cpp deleted file mode 100644 index 4825899..0000000 --- a/Main/slippage_model.cpp +++ /dev/null @@ -1,134 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include "./includes/data_sys.hpp" -using namespace std; - -class SlippageModel { -public: - SlippageModel(PriceHistory& priceHistory, double slippagePercentage) - : priceHistory(priceHistory), slippagePercentage(slippagePercentage) {} - - double calculateSlippage(time_t entryDate, double entryPrice, time_t exitDate, double exitPrice, double tradeVolume) { - PricePoint entryDataPoint = priceHistory.getDataPoint(entryDate); - PricePoint exitDataPoint = priceHistory.getDataPoint(exitDate); - - double entryActualPrice = calculateActualPrice(entryDataPoint.getClosing(), entryPrice); - double exitActualPrice = calculateActualPrice(exitDataPoint.getClosing(), exitPrice); - - double slippage = entryActualPrice - exitActualPrice; - double volumeFactor = calculateVolumeFactor(tradeVolume); - - return slippage * volumeFactor; - } - -private: - PriceHistory& priceHistory; - double slippagePercentage; - - double calculateActualPrice(double referencePrice, double requestedPrice) { - double maxSlippage = referencePrice * slippagePercentage; - double actualPrice = requestedPrice; - - if (actualPrice > referencePrice) { - actualPrice = min(referencePrice + maxSlippage, actualPrice); - } else if (actualPrice < referencePrice) { - actualPrice = max(referencePrice - maxSlippage, actualPrice); - } - - return actualPrice; - } - - double calculateVolumeFactor(double tradeVolume) { - - double volumeFactor = 1.0; - - - const vector> volumeTiers = { - {1000.0, 1.0}, - {10000.0, 0.95}, - {50000.0, 0.9}, - {100000.0, 0.85} - }; - - - for (const auto& tier : volumeTiers) { - if (tradeVolume >= tier.first) { - volumeFactor = tier.second; - } else { - break; - } - } - - - volumeFactor = max(volumeFactor, 0.5); - - - double marketVolatility = calculateMarketVolatility(); - volumeFactor *= calculateVolatilityAdjustment(marketVolatility); - - return volumeFactor; - } - double calculateMarketVolatility() { - vector priceChanges = generateRandomPriceChanges(30); - double averageChange = calculateAverage(priceChanges); - double squaredDifferencesSum = calculateSquaredDifferencesSum(priceChanges, averageChange); - double volatility = sqrt(squaredDifferencesSum / priceChanges.size()); - return volatility; - } - double calculateVolatilityAdjustment(double marketVolatility) { - double volatilityFactor = 1.0 - 0.5 * tanh(marketVolatility); - return max(volatilityFactor, 0.5); - } - - vector generateRandomPriceChanges(size_t numDays) { - vector priceChanges; - random_device rd; - mt19937 generator(rd()); - normal_distribution distribution(0.0, 0.02); - for (size_t i = 0; i < numDays; ++i) { - priceChanges.push_back(distribution(generator)); - } - return priceChanges; - } - double calculateAverage(const vector& values) { - double sum = 0.0; - for (double value : values) { - sum += value; - } - return sum / values.size(); - } - double calculateSquaredDifferencesSum(const vector& values, double mean) { - double sum = 0.0; - for (double value : values) { - double difference = value - mean; - sum += difference * difference; - } - return sum; - } -}; - -int main() { - - PriceHistory priceHistory("AAPL"); - priceHistory.fetchHistoricalData("2023-01-01", "2023-08-01", "1d"); - - - SlippageModel slippageModel(priceHistory, 0.01); - - - time_t entryDate = convertDateToEpoch("2023-03-15"); - double entryPrice = 150.0; - time_t exitDate = convertDateToEpoch("2023-06-15"); - double exitPrice = 170.0; - double tradeVolume = 10000.0; - - double calculatedSlippage = slippageModel.calculateSlippage(entryDate, entryPrice, exitDate, exitPrice, tradeVolume); - cout << "Calculated slippage: " << calculatedSlippage << endl; - - return 0; -} diff --git a/Main/sys.cpp b/Main/sys.cpp deleted file mode 100644 index 2cf2bd8..0000000 --- a/Main/sys.cpp +++ /dev/null @@ -1,386 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include "./includes/data_sys.hpp" -#include "position_sizing.cpp" -using namespace std; - - - -size_t onDataReceived(char *data, size_t size, size_t nmemb, void *userData) { - ((string*)userData)->append(data, size * nmemb); - return size * nmemb; -} -string fetchYahooCsvData( - string stockSymbol, - time_t startTime, - time_t endTime, - string interval -) { - stringstream ssStart; - ssStart << startTime; - stringstream ssEnd; - ssEnd << endTime; - string url = "https://query1.finance.yahoo.com/v7/finance/download/" - + stockSymbol - + "?period1=" + ssStart.str() - + "&period2=" + ssEnd.str() - + "&interval=" + interval - + "&events=history"; - - CURL* curlHandle = curl_easy_init(); - string responseData; - if (curlHandle) { - curl_easy_setopt(curlHandle, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curlHandle, CURLOPT_WRITEFUNCTION, onDataReceived); - curl_easy_setopt(curlHandle, CURLOPT_WRITEDATA, &responseData); - CURLcode result = curl_easy_perform(curlHandle); - curl_easy_cleanup(curlHandle); - } - - return responseData; -} - -time_t getCurrentEpoch() { - return time(nullptr); -} - -time_t convertDateToEpoch(const char *date) { - char yearSegment[5] = {0}; - strncpy(yearSegment, date, 4); - - char monthSegment[3] = {0}; - strncpy(monthSegment, date + 5, 2); - - char daySegment[3] = {0}; - strncpy(daySegment, date + 8, 2); - - struct tm timeStruct = {0}; - timeStruct.tm_year = atoi(yearSegment) - 1900; - timeStruct.tm_mon = atoi(monthSegment) - 1; - timeStruct.tm_mday = atoi(daySegment); - - return timegm(&timeStruct); -} - -string convertEpochToDate(const time_t epoch) { - struct tm *timeInfo = gmtime(&epoch); - stringstream yearStream; - yearStream << timeInfo->tm_year + 1900; - - stringstream monthStream; - if (timeInfo->tm_mon < 9) { - monthStream << 0 << timeInfo->tm_mon + 1; - } else { - monthStream << timeInfo->tm_mon + 1; - } - - stringstream dayStream; - if (timeInfo->tm_mday < 10) { - dayStream << 0 << timeInfo->tm_mday; - } else { - dayStream << timeInfo->tm_mday; - } - - string dateStr = yearStream.str() + "-" + monthStream.str() + "-" + dayStream.str(); - return dateStr; -} - -bool isDateEarlierOrEqual(const char *dateA, const char *dateB) { - time_t epochA = convertDateToEpoch(dateA); - time_t epochB = convertDateToEpoch(dateB); - - return epochA <= epochB; -} - - -PricePoint::PricePoint(time_t date, double opening, double highest, double lowest, double closing) { - this->date = date; - this->opening = opening; - this->highest = highest; - this->lowest = lowest; - this->closing = closing; -} - -PricePoint::PricePoint(string date, double opening, double highest, double lowest, double closing) { - this->date = convertDateToEpoch(date.c_str()); - this->opening = opening; - this->highest = highest; - this->lowest = lowest; - this->closing = closing; -} - -PricePoint::PricePoint(time_t date, double price) { - this->date = date; - this->closing = price; - this->opening = price; - this->highest = price; - this->lowest = price; -} - -PricePoint::PricePoint(string date, double price) { - this->date = convertDateToEpoch(date.c_str()); - this->closing = price; - this->opening = price; - this->highest = price; - this->lowest = price; -} - -PricePoint::~PricePoint() {} - -time_t PricePoint::getDate() { - return this->date; -} - -string PricePoint::getDateString() { - return convertEpochToDate(this->date); -} - -double PricePoint::getOpening() { - return this->opening; -} - -double PricePoint::getHighest() { - return this->highest; -} - -double PricePoint::getLowest() { - return this->lowest; -} - -double PricePoint::getClosing() { - return this->closing; -} - -string PricePoint::toString() { - ostringstream osOpening; - osOpening << this->opening; - ostringstream osHighest; - osHighest << this->highest; - ostringstream osLowest; - osLowest << this->lowest; - ostringstream osClosing; - osClosing << this->closing; - return "{ date: " + this->getDateString() - + " opening: " + osOpening.str() - + " highest: " + osHighest.str() - + " lowest: " + osLowest.str() - + " closing: " + osClosing.str() - + " }"; -} - -void PricePoint::printPricePoint() { - cout << this->toString() << endl; -} -PriceHistory::PriceHistory(string assetSymbol) { - this->assetSymbol = assetSymbol; -} - -PriceHistory::~PriceHistory() {} - -size_t PriceHistory::dataPointsCount() { - return this->dataPoints.size(); -} - -PricePoint PriceHistory::getDataPoint(size_t index) { - if (index < this->dataPoints.size()) { - return this->dataPoints[index]; - } - stringstream ss; - ss << this->dataPoints.size(); - - string error = "ERROR: getDataPoint(index) - Index must be less than " - + ss.str(); - throw invalid_argument(error); -} - -PricePoint PriceHistory::getDataPoint(time_t date) { - for (auto it = this->dataPoints.begin(); it != this->dataPoints.end(); ++it) { - if (it->getDate() == date) { - return *it; - } - } - string error = "ERROR: getDataPoint(date) - No data point at " + to_string(date); - throw invalid_argument(error); -} - -PricePoint PriceHistory::getDataPoint(string date) { - for (auto it = this->dataPoints.begin(); it != this->dataPoints.end(); ++it) { - if (it->getDateString() == date) { - return *it; - } - } - string error = "ERROR: getDataPoint(date) - No data point at " + date; - throw invalid_argument(error); -} - -void PriceHistory::printDataPoints() { - for (auto it = this->dataPoints.begin(); it != this->dataPoints.end(); ++it) { - it->printPricePoint(); - } -} - -void PriceHistory::clearDataPoints() { - this->dataPoints.clear(); -} - -void PriceHistory::calculatePositionSizeForTrade( - size_t dataIndex, - double portfolioSize, - double riskPercentage, - double stopLossPercentage - ) { - if (dataIndex >= dataPoints.size()) { - cout << "Invalid data index." << endl; - return; - } - - PositionSizing positionSizer(riskPercentage, stopLossPercentage); - double entryPrice = dataPoints[dataIndex].getClosing(); - - double positionSize = positionSizer.calculatePositionSize(portfolioSize, entryPrice); - - cout << "Position Size for trade at index " << dataIndex << ": " << positionSize << endl; - } -string PriceHistory::getHistoricalCsv( - time_t startDate, - time_t endDate, - const char *interval -) { - return fetchYahooCsvData(this->assetSymbol, startDate, endDate, interval); -} -vector PriceHistory::calculateBollingerBands(size_t period, double stdDevFactor) { - vector bollingerUpper, bollingerLower; - size_t dataSize = dataPoints.size(); - - for (size_t i = period - 1; i < dataSize; ++i) { - double sum = 0.0; - for (size_t j = i - period + 1; j <= i; ++j) { - sum += dataPoints[j].getClosing(); - } - double movingAvg = sum / period; - - double squaredDiffSum = 0.0; - for (size_t j = i - period + 1; j <= i; ++j) { - double diff = dataPoints[j].getClosing() - movingAvg; - squaredDiffSum += diff * diff; - } - double stdDev = sqrt(squaredDiffSum / period); - - bollingerUpper.push_back(movingAvg + stdDevFactor * stdDev); - bollingerLower.push_back(movingAvg - stdDevFactor * stdDev); - } - - return bollingerUpper; -} - - pair, vector> PriceHistory::calculateSMA(size_t windowSize) { - size_t dataSize = dataPoints.size(); - vector smaValues(dataSize, 0.0); - for (size_t i = windowSize - 1; i < dataSize; ++i) { - double sum = 0.0; - for (size_t j = i - windowSize + 1; j <= i; ++j) { - sum += dataPoints[j].getClosing(); - } - smaValues[i] = sum / windowSize; - } - - return make_pair(smaValues, vector()); -} - -pair, vector> PriceHistory::calculateEMA(size_t windowSize, double emaSmoothingFactor) { - size_t dataSize = dataPoints.size(); - vector emaValues(dataSize, 0.0); - - emaValues[windowSize - 1] = dataPoints[windowSize - 1].getClosing(); - for (size_t i = windowSize; i < dataSize; ++i) { - emaValues[i] = emaSmoothingFactor * dataPoints[i].getClosing() + (1 - emaSmoothingFactor) * emaValues[i - 1]; - } - - return make_pair(vector(), emaValues); -} - -double PriceHistory::performMonteCarloSimulationAdvanced(size_t numSimulations, double initialInvestment) { - size_t dataSize = dataPoints.size(); - double totalProfit = 0.0; - - for (size_t simulation = 0; simulation < numSimulations; ++simulation) { - double investment = initialInvestment; - double cash = investment; - double sharesOwned = 0.0; - size_t currentDay = 0; - - vector movingAveragesShort = calculateSMA(20); - vector movingAveragesLong = calculateSMA(50); - - for (size_t day = 51; day < dataSize; ++day) { - - bool shouldBuy = movingAveragesShort[day - 1] > movingAveragesLong[day - 1] && - movingAveragesShort[day] <= movingAveragesLong[day]; - - bool shouldSell = movingAveragesShort[day - 1] < movingAveragesLong[day - 1] && - movingAveragesShort[day] >= movingAveragesLong[day]; - - if (shouldBuy) { - double amountToInvest = cash * 0.2; // Invest 20% of available funds - double sharesToBuy = amountToInvest / dataPoints[day].getClosing(); - sharesOwned += sharesToBuy; - cash -= amountToInvest; - } else if (shouldSell && sharesOwned > 0.0) { - double amountToSell = sharesOwned * dataPoints[day].getClosing(); - cash += amountToSell; - sharesOwned = 0.0; - } - } - - totalProfit += (cash + sharesOwned * dataPoints[dataSize - 1].getClosing() - initialInvestment); - } - - return totalProfit / numSimulations; -} - -void PriceHistory::fetchHistoricalData( - time_t startDate, - time_t endDate, - const char *interval -) { - string csvData = this->getHistoricalCsv(startDate, endDate, interval); - istringstream csvStream(csvData); - string line; - getline(csvStream, line); - - while (getline(csvStream, line)) { - vector dataFields; - stringstream iss(line); - string field; - while (getline(iss, field, ',')) { - dataFields.push_back(field); - } - - if (dataFields.size() >= 5 && dataFields[0] != "null" && dataFields[4] != "null") { - PricePoint dataPoint( - dataFields[0], // date - stod(dataFields[1]), // open - stod(dataFields[2]), // high - stod(dataFields[3]), // low - stod(dataFields[4]) // close - ); - this->dataPoints.push_back(dataPoint); - } - } -} - -void PriceHistory::fetchHistoricalData( - const char *startDate, - const char *endDate, - const char *interval -) { - time_t startTimestamp = convertDateToEpoch(startDate); - time_t endTimestamp = convertDateToEpoch(endDate); - - this->fetchHistoricalData(startTimestamp, endTimestamp, interval); -} - diff --git a/Main/test.cpp b/Main/test.cpp deleted file mode 100644 index 2d03f38..0000000 --- a/Main/test.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include -#include "sys.cpp" -using namespace std; - -int main() { - - PriceHistory *snp500History = new PriceHistory("AAPL"); - PriceHistory *eurusdHistory = new PriceHistory("EURUSD=X"); - PriceHistory *euraudHistory = new PriceHistory("EURAUD=X"); - - - snp500History->fetchHistoricalData("2017-12-01", "2017-12-31", "1d"); - eurusdHistory->fetchHistoricalData("2018-01-01", "2018-01-10", "1d"); - euraudHistory->fetchHistoricalData("2018-01-01", "2018-01-10", "1d"); - - - cout << "Historical data for S&P 500 (Dec 2017):" << endl; - snp500History->printDataPoints(); - - - try { - PricePoint dataPoint = snp500History->getDataPoint("2017-12-01"); - cout << "Data point at 2017-12-01:" << endl; - dataPoint.printPricePoint(); - } catch (const exception &e) { - cerr << e.what() << endl; - } - - - cout << "EUR/USD rates (Jan 2018):" << endl; - eurusdHistory->printDataPoints(); - - - cout << "EUR/AUD rates (Jan 2018):" << endl; - euraudHistory->printDataPoints(); - - - delete snp500History; - delete eurusdHistory; - delete euraudHistory; -} diff --git a/Main/test_corr.cpp b/Main/test_corr.cpp deleted file mode 100644 index b189ebc..0000000 --- a/Main/test_corr.cpp +++ /dev/null @@ -1,21 +0,0 @@ - -#include "sys.cpp" -#include -#include -#include "correlation_analysis.cpp" - -int main() { - PriceHistory historyA("AAPL"); - PriceHistory historyB("TSLA"); - - - historyA.fetchHistoricalData("2022-01-01", "2022-12-31", "1d"); - historyB.fetchHistoricalData("2022-01-01", "2022-12-31", "1d"); - - CorrelationAnalysis correlationAnalysis(historyA, historyB); - double correlation = correlationAnalysis.calculateCorrelation(); - - cout << "Correlation between AAPL and MSFT: " << correlation << endl; - - return 0; -} diff --git a/Main/test_data.cpp b/Main/test_data.cpp deleted file mode 100644 index 9a5f50a..0000000 --- a/Main/test_data.cpp +++ /dev/null @@ -1,19 +0,0 @@ - -#include "market_data_analysis.cpp" -#include -using namespace std; -int main() { - MarketDataAnalysis analysis("AAPL"); - analysis.priceHistory_.fetchHistoricalData("2023-01-01", "2023-08-31", "1d"); - double averagePrice = analysis.calculateAveragePrice(); - cout << "Average Price: " << averagePrice << endl; - double volatility = analysis.calculateVolatility(); - cout << "Volatility: " << volatility << endl; - vector upwardTrends = analysis.getUpwardTrends(); - cout << "Upward Trends: " << upwardTrends.size() << " data points" << endl; - vector downwardTrends = analysis.getDownwardTrends(); - cout << "Downward Trends: " << downwardTrends.size() << " data points" << endl; - - - return 0; -} diff --git a/Main/tree.xt b/Main/tree.xt deleted file mode 100644 index 305ee75..0000000 --- a/Main/tree.xt +++ /dev/null @@ -1,19 +0,0 @@ -. -├── README.md -├── correlation_analysis.cpp -├── dynamic_hedging.cpp -├── include -│   ├── correlation_analysis.hpp -│   └── data_sys.hpp -├── market_data_analysis.cpp -├── mean_reversion.cpp -├── moving_average.cpp -├── position_sizing.cpp -├── slippage_model.cpp -├── sys.cpp -├── test.cpp -├── test_corr.cpp -├── test_data.cpp -└── tree.xt - -2 directories, 15 files diff --git a/README.md b/README.md index de45c2c..de0b4c9 100644 --- a/README.md +++ b/README.md @@ -1,251 +1,87 @@ -# Barcola +# Barcola — Quantitative Trading Toolkit in C++17 -Barcola encompasses modules for real-time stock data collection, analysis, AI-driven position sizing, and data insights, providing traders with informed decision-making capabilities. Highlighting dynamic hedging strategies, advanced simulations, and slippage modeling, Barcola facilitates risk reduction and strategy enhancement. With its technical analysis tools and API integration, the project offers a robust platform for quantitative trading and in-depth market analysis. +A modular quantitative analysis and backtesting framework built from scratch in modern C++. No external trading library dependencies. +## Building -## Features - - -- **Position Sizing Module:** - - Calculates position sizes based on risk percentage and stop loss percentage. - - Supports calculation of position size with a specified maximum loss amount. - -- **Moving Average Module:** - - Computes simple moving averages (SMA) and exponential moving averages (EMA) for a given window size. - - Tracks and updates moving averages as new data points are added. - -- **Mean Reversion Analysis Module:** - - Analyzes mean reversion using historical price data. - - Provides methods to calculate mean reversion values, average mean reversion, and identify extreme values. - -- **Market Data Analysis Module:** - - Performs various market data analyses on historical price data. - - Calculates average price, volatility, upward and downward trends, and the relative strength index (RSI). - -- **Dynamic Hedging Module:** - - Implements dynamic hedging strategies based on moving averages. - - Determines short and long moving averages windows for analysis. - - Outputs moving average values and their changes over time. - -- **Correlation Analysis Module:** - - Conducts correlation analysis between two sets of historical price data. - - Calculates the correlation coefficient to measure the relationship between asset prices. - -- **Slippage Modeling Module:** - - Models slippage effects on trading. - - Considers slippage due to market conditions and trading volume. - - Adjusts slippage based on trade volume and market volatility. - -- **Price History Management:** - - Manages historical price data for various assets. - - Fetches historical price data using Yahoo Finance API. - - Stores and organizes data points for analysis. - -- **Advanced Simulation:** - - Performs Monte Carlo simulations to assess trading strategy performance. - - Simulates multiple scenarios with varying initial investments. - -- **Time and Date Utilities:** - - Converts between different time representations (POSIX timestamps and human-readable dates). - - Compares dates and checks their order. - +```bash +cmake -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --parallel +ctest --test-dir build --output-on-failure +``` +**Requirements:** C++17 compiler, CMake 3.16+, libcurl -```cpp -//sys.cpp -int main() { - - PriceHistory *snp500History = new PriceHistory("AAPL"); - PriceHistory *eurusdHistory = new PriceHistory("EURUSD=X"); - PriceHistory *euraudHistory = new PriceHistory("EURAUD=X"); - - - snp500History->fetchHistoricalData("2017-12-01", "2017-12-31", "1d"); - eurusdHistory->fetchHistoricalData("2018-01-01", "2018-01-10", "1d"); - euraudHistory->fetchHistoricalData("2018-01-01", "2018-01-10", "1d"); - - - cout << "Historical data for S&P 500 (Dec 2017):" << endl; - snp500History->printDataPoints(); - - - try { - PricePoint dataPoint = snp500History->getDataPoint("2017-12-01"); - cout << "Data point at 2017-12-01:" << endl; - dataPoint.printPricePoint(); - } catch (const exception &e) { - cerr << e.what() << endl; - } - - - cout << "EUR/USD rates (Jan 2018):" << endl; - eurusdHistory->printDataPoints(); - - - cout << "EUR/AUD rates (Jan 2018):" << endl; - euraudHistory->printDataPoints(); - - - delete snp500History; - delete eurusdHistory; - delete euraudHistory; -} +## Project Structure ``` -```cpp -//position_sizing.cpp -int main() { - // Create an instance of the PositionSizing class with a risk percentage and stop loss percentage - PositionSizing positionSizer(2.5, 0.02); // Example risk: 2.5%, stop loss: 2% - - // Calculate position size based on risk percentage and stop loss - double portfolioSize = 100000; // Example portfolio size: $100,000 - double entryPrice = 50; // Example entry price: $50 per share - double positionSize = positionSizer.calculatePositionSize(portfolioSize, entryPrice); - - std::cout << "Calculated Position Size: " << positionSize << " shares" << std::endl; - - // Modify risk and stop loss percentages - positionSizer.setRiskPercentage(3.0); // Change risk percentage to 3% - positionSizer.setStopLossPercentage(0.03); // Change stop loss percentage to 3% - - // Calculate position size with a maximum loss amount - double maxLossAmount = 2500; // Example maximum allowable loss: $2,500 - double positionSizeWithMaxLoss = positionSizer.calculatePositionSizeWithMaxLoss(portfolioSize, entryPrice, maxLossAmount); - - std::cout << "Position Size with Max Loss: " << positionSizeWithMaxLoss << " shares" << std::endl; - - return 0; -} - +src/ +├── core/ PricePoint, PriceHistory (CSV + Yahoo Finance), time utilities +├── indicators/ SMA, EMA, RSI, Bollinger Bands +├── analysis/ Mean reversion, correlation, market data analysis +├── risk/ Position sizing, slippage modeling, dynamic hedging +└── simulation/ Monte Carlo simulation +tests/ Google Test suite (14 test files, 60+ test cases) +data/ Static CSV fixtures for reproducible testing ``` -```cpp -//moving_averages.cpp -int main() { - // Create a PriceHistory instance for the "AAPL" asset - PriceHistory priceHistory("AAPL"); - - // Fetch historical data for the specified date range and interval - priceHistory.fetchHistoricalData("2023-01-01", "2023-08-31", "1d"); +## Modules - // Define window size for moving averages and EMA smoothing factor - size_t windowSize = 5; - double emaSmoothingFactor = 0.2; +| Module | Description | +|--------|-------------| +| **Core** | OHLCV data structures, Yahoo Finance API integration, CSV import/export, date utilities | +| **Indicators** | Simple & exponential moving averages, RSI, Bollinger Bands | +| **Analysis** | Mean reversion detection, Pearson correlation, volatility & trend analysis | +| **Risk** | Position sizing (risk-based & max-loss), slippage modeling with volume tiers, dynamic hedging | +| **Simulation** | Monte Carlo simulation with dual-SMA crossover strategy | - // Calculate and display moving averages for the fetched data - priceHistory.calculateMovingAverages(windowSize, emaSmoothingFactor); - - return 0; -} -``` +## Data Loading ```cpp -//mean_reversion.cpp -int main() { - PriceHistory priceHistory("AAPL"); // Create a PriceHistory instance for the "AAPL" asset - priceHistory.fetchHistoricalData("2023-01-01", "2023-08-31", "1d"); // Fetch historical data - - int movingWindowSize = 10; // Define the moving window size for mean reversion calculation - MeanReversion meanReversion(priceHistory, movingWindowSize); // Create a MeanReversion instance - - meanReversion.calculateMeanReversion(); // Calculate mean reversion values - meanReversion.printMeanReversionResults(); // Print mean reversion results +#include "core/price_history.h" - // Get statistics about mean reversion values - std::cout << "Average Mean Reversion: " << meanReversion.getAverageMeanReversion() << std::endl; - std::cout << "Max Mean Reversion Index: " << meanReversion.getMaxMeanReversionIndex() << std::endl; - std::cout << "Min Mean Reversion Index: " << meanReversion.getMinMeanReversionIndex() << std::endl; +barcola::PriceHistory history("AAPL"); - return 0; -} -``` +// From CSV (for reproducible testing and backtesting) +history.loadFromCsv("data/AAPL_2023_sample.csv"); -```cpp -//market_data_analysis.cpp -int main() { - MarketDataAnalysis marketAnalysis("AAPL"); // Create a MarketDataAnalysis instance for "AAPL" asset - marketAnalysis.priceHistory_.fetchHistoricalData("2023-01-01", "2023-08-31", "1d"); // Fetch historical data - - double avgPrice = marketAnalysis.calculateAveragePrice(); - double volatility = marketAnalysis.calculateVolatility(); - cout << "Average Price: " << avgPrice << endl; - cout << "Volatility: " << volatility << endl; - - vector upwardTrends = marketAnalysis.getUpwardTrends(); - vector downwardTrends = marketAnalysis.getDownwardTrends(); - cout << "Number of Upward Trends: " << upwardTrends.size() << endl; - cout << "Number of Downward Trends: " << downwardTrends.size() << endl; - - size_t rsiPeriod = 14; // RSI calculation period - double rsiValue = marketAnalysis.calculateRelativeStrengthIndex(rsiPeriod); - cout << "RSI for period " << rsiPeriod << ": " << rsiValue << endl; - - return 0; -} +// From Yahoo Finance API (live data) +history.fetchHistoricalData("2023-01-01", "2023-12-31", "1d"); +// Save fetched data for later use +history.saveToCsv("data/aapl_2023.csv"); ``` -```cpp -//dynamic_hedging.cpp -int main() { - string assetSymbol = "AAPL"; - const char* startDate = "2023-01-01"; - const char* endDate = "2023-08-31"; - const char* interval = "1d"; +## Quick Example - DynamicHedging dynamicHedging(assetSymbol, startDate, endDate, interval); - dynamicHedging.performDynamicHedging(); - - return 0; -} ```cpp -//correlation_analysis.cpp -int main() { - string assetSymbolA = "AAPL"; // Asset symbol for the first historical data - string assetSymbolB = "MSFT"; // Asset symbol for the second historical data +#include "core/price_history.h" +#include "indicators/rsi.h" +#include "analysis/correlation.h" - const char* startDate = "2023-01-01"; - const char* endDate = "2023-08-31"; - const char* interval = "1d"; +using namespace barcola; - // Create PriceHistory instances for both assets and fetch historical data - PriceHistory historyA(assetSymbolA); - historyA.fetchHistoricalData(startDate, endDate, interval); +PriceHistory aapl("AAPL"); +aapl.loadFromCsv("data/AAPL_2023_sample.csv"); - PriceHistory historyB(assetSymbolB); - historyB.fetchHistoricalData(startDate, endDate, interval); - - // Create a CorrelationAnalysis instance and calculate correlation - CorrelationAnalysis correlationAnalysis(historyA, historyB); - double correlation = correlationAnalysis.calculateCorrelation(); - - std::cout << "Correlation between " << assetSymbolA << " and " << assetSymbolB << ": " << correlation << std::endl; - - return 0; +// Calculate RSI +std::vector prices; +for (const auto& dp : aapl.getDataPoints()) { + prices.push_back(dp.getClosing()); } +double rsi = calculateRSI(prices, 14); +// Correlation between two assets +PriceHistory msft("MSFT"); +msft.loadFromCsv("data/MSFT_2023_sample.csv"); +CorrelationAnalysis corr(aapl, msft); +double correlation = corr.calculateCorrelation(); ``` -```cpp -//slippage_model -int main() { - PriceHistory priceHistory("AAPL"); - priceHistory.fetchHistoricalData("2023-01-01", "2023-08-01", "1d"); - - SlippageModel slippageModel(priceHistory, 0.01); +## Roadmap - // Convert date strings to time_t using a function like convertDateToEpoch() - - time_t entryDate = convertDateToEpoch("2023-03-15"); - double entryPrice = 150.0; - time_t exitDate = convertDateToEpoch("2023-06-15"); - double exitPrice = 170.0; - double tradeVolume = 10000.0; - - double calculatedSlippage = slippageModel.calculateSlippage(entryDate, entryPrice, exitDate, exitPrice, tradeVolume); - std::cout << "Calculated slippage: " << calculatedSlippage << std::endl; - - return 0; -} -``` +- [x] **Phase 1:** Foundation hardening (CMake, tests, CI, bug fixes, CSV support) +- [ ] **Phase 2:** Backtesting engine (strategy interface, engine, performance metrics) +- [ ] **Phase 3:** Mean reversion backtest with documented results +- [ ] **Phase 4:** Second strategy (dual MA crossover) + comparison +- [ ] **Phase 5:** Polish, visualizations, results-first README diff --git a/data/AAPL_2023_sample.csv b/data/AAPL_2023_sample.csv new file mode 100644 index 0000000..4585a8d --- /dev/null +++ b/data/AAPL_2023_sample.csv @@ -0,0 +1,31 @@ +Date,Open,High,Low,Close +2023-01-03,130.28,130.90,124.17,125.07 +2023-01-04,126.89,128.66,125.08,126.36 +2023-01-05,127.13,127.77,124.76,125.02 +2023-01-06,126.01,130.29,124.89,129.62 +2023-01-09,130.47,133.41,129.89,130.15 +2023-01-10,130.26,131.26,128.12,130.73 +2023-01-11,131.25,133.51,130.06,133.49 +2023-01-12,133.88,134.26,131.44,133.41 +2023-01-13,132.03,134.92,131.66,134.76 +2023-01-17,134.83,137.29,134.13,135.94 +2023-01-18,136.82,138.61,135.03,135.21 +2023-01-19,134.08,136.25,133.77,135.27 +2023-01-20,135.28,138.02,134.22,137.87 +2023-01-23,138.12,143.32,137.90,141.11 +2023-01-24,140.31,143.16,140.30,142.53 +2023-01-25,140.89,142.43,138.81,141.86 +2023-01-26,143.17,144.25,141.90,143.96 +2023-01-27,143.16,147.23,143.08,145.93 +2023-01-30,144.96,145.55,142.85,143.00 +2023-01-31,142.70,144.34,142.28,144.29 +2023-02-01,143.97,146.61,141.32,145.43 +2023-02-02,148.90,151.18,148.17,150.82 +2023-02-03,148.03,157.38,147.83,154.50 +2023-02-06,152.57,153.10,150.78,151.73 +2023-02-07,150.64,155.23,150.64,154.65 +2023-02-08,153.88,154.58,151.17,151.92 +2023-02-09,153.78,154.33,150.42,150.87 +2023-02-10,149.46,151.34,149.22,151.01 +2023-02-13,150.95,154.26,150.92,153.85 +2023-02-14,152.12,153.77,150.86,153.20 diff --git a/data/MSFT_2023_sample.csv b/data/MSFT_2023_sample.csv new file mode 100644 index 0000000..fec10e5 --- /dev/null +++ b/data/MSFT_2023_sample.csv @@ -0,0 +1,31 @@ +Date,Open,High,Low,Close +2023-01-03,243.08,245.75,237.40,239.58 +2023-01-04,242.00,245.45,238.97,229.10 +2023-01-05,227.20,227.55,221.76,222.31 +2023-01-06,223.00,225.76,219.35,224.93 +2023-01-09,226.45,231.24,226.41,227.12 +2023-01-10,227.76,230.33,225.36,228.85 +2023-01-11,229.59,233.87,228.84,232.87 +2023-01-12,233.53,234.67,231.17,233.94 +2023-01-13,231.92,234.76,231.42,234.51 +2023-01-17,237.51,239.90,234.50,235.49 +2023-01-18,238.41,240.97,235.82,235.81 +2023-01-19,233.97,236.47,232.80,235.28 +2023-01-20,234.87,238.07,233.40,240.22 +2023-01-23,238.49,243.30,237.97,242.58 +2023-01-24,242.50,244.40,240.54,242.04 +2023-01-25,237.67,241.78,236.58,240.61 +2023-01-26,244.07,246.49,240.62,248.16 +2023-01-27,246.32,249.81,246.06,248.93 +2023-01-30,246.25,247.60,243.22,244.03 +2023-01-31,243.15,247.95,242.97,247.81 +2023-02-01,246.39,249.97,245.73,252.75 +2023-02-02,258.82,264.69,257.11,264.60 +2023-02-03,259.53,264.20,257.37,258.35 +2023-02-06,256.77,258.31,253.14,256.77 +2023-02-07,254.76,261.41,254.76,267.56 +2023-02-08,263.64,267.35,260.20,266.73 +2023-02-09,268.05,269.60,262.09,263.62 +2023-02-10,261.38,263.92,260.06,263.68 +2023-02-13,263.17,267.89,262.16,265.15 +2023-02-14,264.56,268.14,262.71,272.05 diff --git a/data/constant_price.csv b/data/constant_price.csv new file mode 100644 index 0000000..dfa20bc --- /dev/null +++ b/data/constant_price.csv @@ -0,0 +1,11 @@ +Date,Open,High,Low,Close +2023-01-03,100.00,100.00,100.00,100.00 +2023-01-04,100.00,100.00,100.00,100.00 +2023-01-05,100.00,100.00,100.00,100.00 +2023-01-06,100.00,100.00,100.00,100.00 +2023-01-09,100.00,100.00,100.00,100.00 +2023-01-10,100.00,100.00,100.00,100.00 +2023-01-11,100.00,100.00,100.00,100.00 +2023-01-12,100.00,100.00,100.00,100.00 +2023-01-13,100.00,100.00,100.00,100.00 +2023-01-17,100.00,100.00,100.00,100.00 diff --git a/data/empty.csv b/data/empty.csv new file mode 100644 index 0000000..47dfd9c --- /dev/null +++ b/data/empty.csv @@ -0,0 +1 @@ +Date,Open,High,Low,Close diff --git a/data/monotonic_down.csv b/data/monotonic_down.csv new file mode 100644 index 0000000..e217b01 --- /dev/null +++ b/data/monotonic_down.csv @@ -0,0 +1,17 @@ +Date,Open,High,Low,Close +2023-01-03,25.00,25.50,24.50,25.00 +2023-01-04,24.00,24.50,23.50,24.00 +2023-01-05,23.00,23.50,22.50,23.00 +2023-01-06,22.00,22.50,21.50,22.00 +2023-01-09,21.00,21.50,20.50,21.00 +2023-01-10,20.00,20.50,19.50,20.00 +2023-01-11,19.00,19.50,18.50,19.00 +2023-01-12,18.00,18.50,17.50,18.00 +2023-01-13,17.00,17.50,16.50,17.00 +2023-01-17,16.00,16.50,15.50,16.00 +2023-01-18,15.00,15.50,14.50,15.00 +2023-01-19,14.00,14.50,13.50,14.00 +2023-01-20,13.00,13.50,12.50,13.00 +2023-01-23,12.00,12.50,11.50,12.00 +2023-01-24,11.00,11.50,10.50,11.00 +2023-01-25,10.00,10.50,9.50,10.00 diff --git a/data/monotonic_up.csv b/data/monotonic_up.csv new file mode 100644 index 0000000..b38f0f9 --- /dev/null +++ b/data/monotonic_up.csv @@ -0,0 +1,17 @@ +Date,Open,High,Low,Close +2023-01-03,10.00,10.50,9.50,10.00 +2023-01-04,11.00,11.50,10.50,11.00 +2023-01-05,12.00,12.50,11.50,12.00 +2023-01-06,13.00,13.50,12.50,13.00 +2023-01-09,14.00,14.50,13.50,14.00 +2023-01-10,15.00,15.50,14.50,15.00 +2023-01-11,16.00,16.50,15.50,16.00 +2023-01-12,17.00,17.50,16.50,17.00 +2023-01-13,18.00,18.50,17.50,18.00 +2023-01-17,19.00,19.50,18.50,19.00 +2023-01-18,20.00,20.50,19.50,20.00 +2023-01-19,21.00,21.50,20.50,21.00 +2023-01-20,22.00,22.50,21.50,22.00 +2023-01-23,23.00,23.50,22.50,23.00 +2023-01-24,24.00,24.50,23.50,24.00 +2023-01-25,25.00,25.50,24.50,25.00 diff --git a/data/single_point.csv b/data/single_point.csv new file mode 100644 index 0000000..cb2f0b3 --- /dev/null +++ b/data/single_point.csv @@ -0,0 +1,2 @@ +Date,Open,High,Low,Close +2023-01-03,150.00,155.00,148.00,152.00 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..b83b678 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,18 @@ +add_library(barcola STATIC + core/time_utils.cpp + core/price_point.cpp + core/price_history.cpp + indicators/moving_average.cpp + indicators/rsi.cpp + indicators/bollinger_bands.cpp + analysis/market_data_analysis.cpp + analysis/correlation.cpp + analysis/mean_reversion.cpp + risk/position_sizing.cpp + risk/slippage_model.cpp + risk/dynamic_hedging.cpp + simulation/monte_carlo.cpp +) + +target_include_directories(barcola PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(barcola PUBLIC CURL::libcurl) diff --git a/src/analysis/correlation.cpp b/src/analysis/correlation.cpp new file mode 100644 index 0000000..3eca825 --- /dev/null +++ b/src/analysis/correlation.cpp @@ -0,0 +1,70 @@ +#include "analysis/correlation.h" + +#include +#include +#include + +namespace barcola { + +CorrelationAnalysis::CorrelationAnalysis(const PriceHistory& historyA, + const PriceHistory& historyB) + : historyA_(historyA), historyB_(historyB) {} + +double CorrelationAnalysis::calculateCorrelation() const { + const size_t minDataPoints = + std::min(historyA_.dataPointsCount(), historyB_.dataPointsCount()); + + // FIX Bug 2: Guard against zero or one data point + if (minDataPoints < 2) { + return 0.0; + } + + std::vector pricesA; + std::vector pricesB; + pricesA.reserve(minDataPoints); + pricesB.reserve(minDataPoints); + + for (size_t i = 0; i < minDataPoints; ++i) { + pricesA.push_back(historyA_.getDataPoint(i).getClosing()); + pricesB.push_back(historyB_.getDataPoint(i).getClosing()); + } + + return barcola::calculateCorrelation(pricesA, pricesB); +} + +double calculateCorrelation(const std::vector& a, const std::vector& b) { + const size_t n = std::min(a.size(), b.size()); + if (n < 2) { + return 0.0; + } + + const double meanA = std::accumulate(a.begin(), a.begin() + n, 0.0) / static_cast(n); + const double meanB = std::accumulate(b.begin(), b.begin() + n, 0.0) / static_cast(n); + + double covariance = 0.0; + double varianceA = 0.0; + double varianceB = 0.0; + + for (size_t i = 0; i < n; ++i) { + const double deviationA = a[i] - meanA; + const double deviationB = b[i] - meanB; + + covariance += deviationA * deviationB; + varianceA += deviationA * deviationA; + varianceB += deviationB * deviationB; + } + + covariance /= static_cast(n); + varianceA /= static_cast(n); + varianceB /= static_cast(n); + + // FIX Bug 2: Guard against division by zero when variance is zero + const double denominator = std::sqrt(varianceA) * std::sqrt(varianceB); + if (denominator < 1e-10) { + return 0.0; + } + + return covariance / denominator; +} + +} // namespace barcola diff --git a/src/analysis/correlation.h b/src/analysis/correlation.h new file mode 100644 index 0000000..2976be6 --- /dev/null +++ b/src/analysis/correlation.h @@ -0,0 +1,24 @@ +#pragma once + +#include "core/price_history.h" + +namespace barcola { + +class CorrelationAnalysis { +public: + CorrelationAnalysis(const PriceHistory& historyA, const PriceHistory& historyB); + + // Pearson correlation coefficient between two price histories. + // Returns 0.0 if either series has zero variance or insufficient data. + [[nodiscard]] double calculateCorrelation() const; + +private: + const PriceHistory& historyA_; + const PriceHistory& historyB_; +}; + +// Free function: correlation between two price vectors directly. +[[nodiscard]] double calculateCorrelation(const std::vector& a, + const std::vector& b); + +} // namespace barcola diff --git a/src/analysis/market_data_analysis.cpp b/src/analysis/market_data_analysis.cpp new file mode 100644 index 0000000..4cda284 --- /dev/null +++ b/src/analysis/market_data_analysis.cpp @@ -0,0 +1,72 @@ +#include "analysis/market_data_analysis.h" + +#include + +namespace barcola { + +MarketDataAnalysis::MarketDataAnalysis(PriceHistory& priceHistory) + : priceHistory_(priceHistory) {} + +double MarketDataAnalysis::calculateAveragePrice() const { + const size_t count = priceHistory_.dataPointsCount(); + if (count == 0) { + return 0.0; + } + + double total = 0.0; + for (size_t i = 0; i < count; ++i) { + total += priceHistory_.getDataPoint(i).getClosing(); + } + return total / static_cast(count); +} + +double MarketDataAnalysis::calculateVolatility() const { + const size_t count = priceHistory_.dataPointsCount(); + if (count < 2) { + return 0.0; + } + + const double averagePrice = calculateAveragePrice(); + double sumSquaredDifferences = 0.0; + + for (size_t i = 0; i < count; ++i) { + const double price = priceHistory_.getDataPoint(i).getClosing(); + sumSquaredDifferences += (price - averagePrice) * (price - averagePrice); + } + + return std::sqrt(sumSquaredDifferences / static_cast(count - 1)); +} + +std::vector MarketDataAnalysis::getUpwardTrends() const { + std::vector upwardTrends; + const size_t count = priceHistory_.dataPointsCount(); + + for (size_t i = 1; i < count; ++i) { + const auto prevPoint = priceHistory_.getDataPoint(i - 1); + const auto currentPoint = priceHistory_.getDataPoint(i); + + if (currentPoint.getClosing() > prevPoint.getClosing()) { + upwardTrends.push_back(currentPoint); + } + } + + return upwardTrends; +} + +std::vector MarketDataAnalysis::getDownwardTrends() const { + std::vector downwardTrends; + const size_t count = priceHistory_.dataPointsCount(); + + for (size_t i = 1; i < count; ++i) { + const auto prevPoint = priceHistory_.getDataPoint(i - 1); + const auto currentPoint = priceHistory_.getDataPoint(i); + + if (currentPoint.getClosing() < prevPoint.getClosing()) { + downwardTrends.push_back(currentPoint); + } + } + + return downwardTrends; +} + +} // namespace barcola diff --git a/src/analysis/market_data_analysis.h b/src/analysis/market_data_analysis.h new file mode 100644 index 0000000..7dfe752 --- /dev/null +++ b/src/analysis/market_data_analysis.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include "core/price_history.h" + +namespace barcola { + +class MarketDataAnalysis { +public: + explicit MarketDataAnalysis(PriceHistory& priceHistory); + + [[nodiscard]] double calculateAveragePrice() const; + [[nodiscard]] double calculateVolatility() const; + [[nodiscard]] std::vector getUpwardTrends() const; + [[nodiscard]] std::vector getDownwardTrends() const; + +private: + PriceHistory& priceHistory_; +}; + +} // namespace barcola diff --git a/src/analysis/mean_reversion.cpp b/src/analysis/mean_reversion.cpp new file mode 100644 index 0000000..ac25778 --- /dev/null +++ b/src/analysis/mean_reversion.cpp @@ -0,0 +1,98 @@ +#include "analysis/mean_reversion.h" + +#include +#include +#include + +namespace barcola { + +MeanReversion::MeanReversion(const PriceHistory& history, int windowSize) + : priceHistory_(history), movingWindowSize_(windowSize) {} + +double MeanReversion::calculateMovingAverage(size_t startIndex) const { + const auto& dataPoints = priceHistory_.getDataPoints(); + const size_t endIndex = startIndex + static_cast(movingWindowSize_); + + if (endIndex > dataPoints.size()) { + throw std::out_of_range("Window extends beyond data range"); + } + + double sum = 0.0; + for (size_t i = startIndex; i < endIndex; ++i) { + sum += dataPoints[i].getClosing(); + } + return sum / static_cast(movingWindowSize_); +} + +void MeanReversion::calculateMeanReversion() { + const auto& dataPoints = priceHistory_.getDataPoints(); + const size_t dataSize = dataPoints.size(); + const size_t windowSize = static_cast(movingWindowSize_); + + meanReversionValues_.clear(); + + if (dataSize < windowSize) { + return; + } + + // For each point starting at windowSize-1, compute deviation from its local moving average + for (size_t i = windowSize - 1; i < dataSize; ++i) { + const size_t startIdx = i - windowSize + 1; + const double movingAvg = calculateMovingAverage(startIdx); + const double deviation = dataPoints[i].getClosing() - movingAvg; + meanReversionValues_.push_back(deviation); + } +} + +void MeanReversion::printMeanReversionResults() const { + const auto& dataPoints = priceHistory_.getDataPoints(); + const size_t windowSize = static_cast(movingWindowSize_); + + for (size_t i = 0; i < meanReversionValues_.size(); ++i) { + const size_t dataIdx = i + windowSize - 1; + std::cout << "Date: " << dataPoints[dataIdx].getDateString() + << " Deviation: " << meanReversionValues_[i] << std::endl; + } +} + +double MeanReversion::getMeanReversionValue(size_t index) const { + if (index < meanReversionValues_.size()) { + return meanReversionValues_[index]; + } + throw std::out_of_range("Index out of range"); +} + +double MeanReversion::getAverageMeanReversion() const { + if (meanReversionValues_.empty()) { + return 0.0; + } + double sum = 0.0; + for (const double value : meanReversionValues_) { + sum += value; + } + return sum / static_cast(meanReversionValues_.size()); +} + +size_t MeanReversion::getMaxMeanReversionIndex() const { + if (meanReversionValues_.empty()) { + throw std::out_of_range("No mean reversion values computed"); + } + return static_cast(std::distance( + meanReversionValues_.begin(), + std::max_element(meanReversionValues_.begin(), meanReversionValues_.end()))); +} + +size_t MeanReversion::getMinMeanReversionIndex() const { + if (meanReversionValues_.empty()) { + throw std::out_of_range("No mean reversion values computed"); + } + return static_cast(std::distance( + meanReversionValues_.begin(), + std::min_element(meanReversionValues_.begin(), meanReversionValues_.end()))); +} + +const std::vector& MeanReversion::getValues() const { + return meanReversionValues_; +} + +} // namespace barcola diff --git a/src/analysis/mean_reversion.h b/src/analysis/mean_reversion.h new file mode 100644 index 0000000..8a3bd2e --- /dev/null +++ b/src/analysis/mean_reversion.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "core/price_history.h" + +namespace barcola { + +class MeanReversion { +public: + MeanReversion(const PriceHistory& history, int windowSize); + + void calculateMeanReversion(); + void printMeanReversionResults() const; + [[nodiscard]] double getMeanReversionValue(size_t index) const; + [[nodiscard]] double getAverageMeanReversion() const; + [[nodiscard]] size_t getMaxMeanReversionIndex() const; + [[nodiscard]] size_t getMinMeanReversionIndex() const; + [[nodiscard]] const std::vector& getValues() const; + +private: + const PriceHistory& priceHistory_; + int movingWindowSize_; + std::vector meanReversionValues_; + + [[nodiscard]] double calculateMovingAverage(size_t startIndex) const; +}; + +} // namespace barcola diff --git a/src/core/price_history.cpp b/src/core/price_history.cpp new file mode 100644 index 0000000..24ca0ef --- /dev/null +++ b/src/core/price_history.cpp @@ -0,0 +1,241 @@ +#include "core/price_history.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "core/time_utils.h" + +namespace barcola { + +namespace { + +size_t onDataReceived(char* data, size_t size, size_t nmemb, void* userData) { + static_cast(userData)->append(data, size * nmemb); + return size * nmemb; +} + +} // namespace + +PriceHistory::PriceHistory(const std::string& assetSymbol) : assetSymbol_(assetSymbol) {} + +size_t PriceHistory::dataPointsCount() const { + return dataPoints_.size(); +} + +PricePoint PriceHistory::getDataPoint(size_t index) const { + if (index < dataPoints_.size()) { + return dataPoints_[index]; + } + throw std::invalid_argument("Index " + std::to_string(index) + + " out of range (size: " + std::to_string(dataPoints_.size()) + ")"); +} + +PricePoint PriceHistory::getDataPoint(time_t date) const { + for (const auto& dp : dataPoints_) { + if (dp.getDate() == date) { + return dp; + } + } + throw std::invalid_argument("No data point at epoch " + std::to_string(date)); +} + +PricePoint PriceHistory::getDataPoint(const std::string& date) const { + for (const auto& dp : dataPoints_) { + if (dp.getDateString() == date) { + return dp; + } + } + throw std::invalid_argument("No data point at date " + date); +} + +const std::vector& PriceHistory::getDataPoints() const { + return dataPoints_; +} + +const std::string& PriceHistory::getAssetSymbol() const { + return assetSymbol_; +} + +void PriceHistory::printDataPoints() const { + for (const auto& dp : dataPoints_) { + dp.printPricePoint(); + } +} + +void PriceHistory::clearDataPoints() { + dataPoints_.clear(); +} + +// --- Data loading --- + +std::string PriceHistory::fetchYahooCsvData(time_t startTime, time_t endTime, + const std::string& interval) const { + const std::string url = "https://query1.finance.yahoo.com/v7/finance/download/" + + assetSymbol_ + "?period1=" + std::to_string(startTime) + + "&period2=" + std::to_string(endTime) + "&interval=" + interval + + "&events=history"; + + CURL* curlHandle = curl_easy_init(); + if (!curlHandle) { + throw std::runtime_error("Failed to initialize CURL"); + } + + std::string responseData; + curl_easy_setopt(curlHandle, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curlHandle, CURLOPT_WRITEFUNCTION, onDataReceived); + curl_easy_setopt(curlHandle, CURLOPT_WRITEDATA, &responseData); + curl_easy_setopt(curlHandle, CURLOPT_TIMEOUT, 30L); + + CURLcode result = curl_easy_perform(curlHandle); + curl_easy_cleanup(curlHandle); + + if (result != CURLE_OK) { + throw std::runtime_error(std::string("CURL request failed: ") + + curl_easy_strerror(result)); + } + + return responseData; +} + +void PriceHistory::fetchHistoricalData(time_t startDate, time_t endDate, + const std::string& interval) { + const std::string csvData = fetchYahooCsvData(startDate, endDate, interval); + std::istringstream csvStream(csvData); + std::string line; + + // Skip header + std::getline(csvStream, line); + + while (std::getline(csvStream, line)) { + std::vector fields; + std::istringstream lineStream(line); + std::string field; + while (std::getline(lineStream, field, ',')) { + fields.push_back(field); + } + + if (fields.size() >= 5 && fields[0] != "null" && fields[4] != "null") { + dataPoints_.emplace_back(fields[0], std::stod(fields[1]), std::stod(fields[2]), + std::stod(fields[3]), std::stod(fields[4])); + } + } +} + +void PriceHistory::fetchHistoricalData(const std::string& startDate, const std::string& endDate, + const std::string& interval) { + const time_t startTimestamp = convertDateToEpoch(startDate); + const time_t endTimestamp = convertDateToEpoch(endDate); + fetchHistoricalData(startTimestamp, endTimestamp, interval); +} + +void PriceHistory::loadFromCsv(const std::string& filepath) { + std::ifstream file(filepath); + if (!file.is_open()) { + throw std::runtime_error("Failed to open CSV file: " + filepath); + } + + std::string line; + // Skip header + std::getline(file, line); + + while (std::getline(file, line)) { + if (line.empty()) continue; + + std::vector fields; + std::istringstream lineStream(line); + std::string field; + while (std::getline(lineStream, field, ',')) { + fields.push_back(field); + } + + if (fields.size() >= 5 && fields[0] != "null" && fields[4] != "null") { + dataPoints_.emplace_back(fields[0], std::stod(fields[1]), std::stod(fields[2]), + std::stod(fields[3]), std::stod(fields[4])); + } + } +} + +void PriceHistory::saveToCsv(const std::string& filepath) const { + std::ofstream file(filepath); + if (!file.is_open()) { + throw std::runtime_error("Failed to open CSV file for writing: " + filepath); + } + + file << "Date,Open,High,Low,Close\n"; + file << std::fixed << std::setprecision(6); + for (const auto& dp : dataPoints_) { + file << dp.getDateString() << "," << dp.getOpening() << "," << dp.getHighest() << "," + << dp.getLowest() << "," << dp.getClosing() << "\n"; + } +} + +// --- Technical calculations --- + +std::pair, std::vector> PriceHistory::calculateSMA( + size_t windowSize) const { + const size_t dataSize = dataPoints_.size(); + std::vector smaValues(dataSize, 0.0); + + for (size_t i = windowSize - 1; i < dataSize; ++i) { + double sum = 0.0; + for (size_t j = i - windowSize + 1; j <= i; ++j) { + sum += dataPoints_[j].getClosing(); + } + smaValues[i] = sum / static_cast(windowSize); + } + + return {smaValues, {}}; +} + +std::pair, std::vector> PriceHistory::calculateEMA( + size_t windowSize, double emaSmoothingFactor) const { + const size_t dataSize = dataPoints_.size(); + if (dataSize < windowSize) { + return {{}, {}}; + } + + std::vector emaValues(dataSize, 0.0); + emaValues[windowSize - 1] = dataPoints_[windowSize - 1].getClosing(); + + for (size_t i = windowSize; i < dataSize; ++i) { + emaValues[i] = emaSmoothingFactor * dataPoints_[i].getClosing() + + (1.0 - emaSmoothingFactor) * emaValues[i - 1]; + } + + return {{}, emaValues}; +} + +// FIX: Returns both upper AND lower bands (original only returned upper) +std::pair, std::vector> PriceHistory::calculateBollingerBands( + size_t period, double stdDevFactor) const { + std::vector bollingerUpper; + std::vector bollingerLower; + const size_t dataSize = dataPoints_.size(); + + for (size_t i = period - 1; i < dataSize; ++i) { + double sum = 0.0; + for (size_t j = i - period + 1; j <= i; ++j) { + sum += dataPoints_[j].getClosing(); + } + const double movingAvg = sum / static_cast(period); + + double squaredDiffSum = 0.0; + for (size_t j = i - period + 1; j <= i; ++j) { + const double diff = dataPoints_[j].getClosing() - movingAvg; + squaredDiffSum += diff * diff; + } + const double stdDev = std::sqrt(squaredDiffSum / static_cast(period)); + + bollingerUpper.push_back(movingAvg + stdDevFactor * stdDev); + bollingerLower.push_back(movingAvg - stdDevFactor * stdDev); + } + + return {bollingerUpper, bollingerLower}; +} + +} // namespace barcola diff --git a/src/core/price_history.h b/src/core/price_history.h new file mode 100644 index 0000000..66ae1c3 --- /dev/null +++ b/src/core/price_history.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include + +#include "core/price_point.h" + +namespace barcola { + +class PriceHistory { +public: + explicit PriceHistory(const std::string& assetSymbol); + ~PriceHistory() = default; + + // Data access + [[nodiscard]] size_t dataPointsCount() const; + [[nodiscard]] PricePoint getDataPoint(size_t index) const; + [[nodiscard]] PricePoint getDataPoint(time_t date) const; + [[nodiscard]] PricePoint getDataPoint(const std::string& date) const; + [[nodiscard]] const std::vector& getDataPoints() const; + [[nodiscard]] const std::string& getAssetSymbol() const; + void printDataPoints() const; + void clearDataPoints(); + + // Data loading + void fetchHistoricalData(time_t startDate, time_t endDate, const std::string& interval); + void fetchHistoricalData(const std::string& startDate, const std::string& endDate, + const std::string& interval); + void loadFromCsv(const std::string& filepath); + void saveToCsv(const std::string& filepath) const; + + // Technical calculations (kept on PriceHistory for backward compatibility) + [[nodiscard]] std::pair, std::vector> calculateSMA( + size_t windowSize) const; + [[nodiscard]] std::pair, std::vector> calculateEMA( + size_t windowSize, double emaSmoothingFactor) const; + [[nodiscard]] std::pair, std::vector> calculateBollingerBands( + size_t period, double stdDevFactor) const; + +private: + std::string assetSymbol_; + std::vector dataPoints_; + + [[nodiscard]] std::string fetchYahooCsvData(time_t startTime, time_t endTime, + const std::string& interval) const; +}; + +} // namespace barcola diff --git a/src/core/price_point.cpp b/src/core/price_point.cpp new file mode 100644 index 0000000..8275c2c --- /dev/null +++ b/src/core/price_point.cpp @@ -0,0 +1,66 @@ +#include "core/price_point.h" + +#include +#include + +#include "core/time_utils.h" + +namespace barcola { + +PricePoint::PricePoint(time_t date, double opening, double highest, double lowest, double closing) + : date_(date), opening_(opening), highest_(highest), lowest_(lowest), closing_(closing) {} + +PricePoint::PricePoint(const std::string& date, double opening, double highest, double lowest, + double closing) + : date_(convertDateToEpoch(date)), + opening_(opening), + highest_(highest), + lowest_(lowest), + closing_(closing) {} + +PricePoint::PricePoint(time_t date, double price) + : date_(date), opening_(price), highest_(price), lowest_(price), closing_(price) {} + +PricePoint::PricePoint(const std::string& date, double price) + : date_(convertDateToEpoch(date)), + opening_(price), + highest_(price), + lowest_(price), + closing_(price) {} + +time_t PricePoint::getDate() const { + return date_; +} + +std::string PricePoint::getDateString() const { + return convertEpochToDate(date_); +} + +double PricePoint::getOpening() const { + return opening_; +} + +double PricePoint::getHighest() const { + return highest_; +} + +double PricePoint::getLowest() const { + return lowest_; +} + +double PricePoint::getClosing() const { + return closing_; +} + +std::string PricePoint::toString() const { + std::ostringstream oss; + oss << "{ date: " << getDateString() << " opening: " << opening_ << " highest: " << highest_ + << " lowest: " << lowest_ << " closing: " << closing_ << " }"; + return oss.str(); +} + +void PricePoint::printPricePoint() const { + std::cout << toString() << std::endl; +} + +} // namespace barcola diff --git a/src/core/price_point.h b/src/core/price_point.h new file mode 100644 index 0000000..8467e57 --- /dev/null +++ b/src/core/price_point.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +namespace barcola { + +class PricePoint { +public: + PricePoint(time_t date, double opening, double highest, double lowest, double closing); + PricePoint(const std::string& date, double opening, double highest, double lowest, + double closing); + PricePoint(time_t date, double price); + PricePoint(const std::string& date, double price); + ~PricePoint() = default; + + [[nodiscard]] time_t getDate() const; + [[nodiscard]] std::string getDateString() const; + [[nodiscard]] double getOpening() const; + [[nodiscard]] double getHighest() const; + [[nodiscard]] double getLowest() const; + [[nodiscard]] double getClosing() const; + + [[nodiscard]] std::string toString() const; + void printPricePoint() const; + +private: + time_t date_; + double opening_; + double highest_; + double lowest_; + double closing_; +}; + +} // namespace barcola diff --git a/src/core/time_utils.cpp b/src/core/time_utils.cpp new file mode 100644 index 0000000..021b545 --- /dev/null +++ b/src/core/time_utils.cpp @@ -0,0 +1,55 @@ +#include "core/time_utils.h" + +#include +#include +#include + +namespace barcola { + +time_t getCurrentEpoch() { + return time(nullptr); +} + +time_t convertDateToEpoch(const std::string& date) { + if (date.size() < 10 || date[4] != '-' || date[7] != '-') { + throw std::invalid_argument("Date must be in YYYY-MM-DD format: " + date); + } + + int year = 0, month = 0, day = 0; + if (std::sscanf(date.c_str(), "%d-%d-%d", &year, &month, &day) != 3) { + throw std::invalid_argument("Failed to parse date: " + date); + } + + if (month < 1 || month > 12 || day < 1 || day > 31) { + throw std::invalid_argument("Invalid date components: " + date); + } + + struct tm timeStruct = {}; + timeStruct.tm_year = year - 1900; + timeStruct.tm_mon = month - 1; + timeStruct.tm_mday = day; + + return timegm(&timeStruct); +} + +std::string convertEpochToDate(time_t epoch) { + struct tm* timeInfo = gmtime(&epoch); + if (!timeInfo) { + throw std::runtime_error("Failed to convert epoch to date"); + } + + char buffer[32]; // "YYYY-MM-DD\0" with headroom for GCC format-truncation warning + std::snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d", + timeInfo->tm_year + 1900, + timeInfo->tm_mon + 1, + timeInfo->tm_mday); + return std::string(buffer); +} + +bool isDateEarlierOrEqual(const std::string& dateA, const std::string& dateB) { + const time_t epochA = convertDateToEpoch(dateA); + const time_t epochB = convertDateToEpoch(dateB); + return epochA <= epochB; +} + +} // namespace barcola diff --git a/src/core/time_utils.h b/src/core/time_utils.h new file mode 100644 index 0000000..2c5ee1c --- /dev/null +++ b/src/core/time_utils.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +namespace barcola { + +[[nodiscard]] time_t getCurrentEpoch(); +[[nodiscard]] time_t convertDateToEpoch(const std::string& date); +[[nodiscard]] std::string convertEpochToDate(time_t epoch); +[[nodiscard]] bool isDateEarlierOrEqual(const std::string& dateA, const std::string& dateB); + +} // namespace barcola diff --git a/src/indicators/bollinger_bands.cpp b/src/indicators/bollinger_bands.cpp new file mode 100644 index 0000000..a4a3d5e --- /dev/null +++ b/src/indicators/bollinger_bands.cpp @@ -0,0 +1,37 @@ +#include "indicators/bollinger_bands.h" + +#include + +namespace barcola { + +std::pair, std::vector> calculateBollingerBands( + const std::vector& prices, size_t period, double stdDevFactor) { + std::vector upper; + std::vector lower; + + if (prices.size() < period) { + return {upper, lower}; + } + + for (size_t i = period - 1; i < prices.size(); ++i) { + double sum = 0.0; + for (size_t j = i - period + 1; j <= i; ++j) { + sum += prices[j]; + } + const double movingAvg = sum / static_cast(period); + + double squaredDiffSum = 0.0; + for (size_t j = i - period + 1; j <= i; ++j) { + const double diff = prices[j] - movingAvg; + squaredDiffSum += diff * diff; + } + const double stdDev = std::sqrt(squaredDiffSum / static_cast(period)); + + upper.push_back(movingAvg + stdDevFactor * stdDev); + lower.push_back(movingAvg - stdDevFactor * stdDev); + } + + return {upper, lower}; +} + +} // namespace barcola diff --git a/src/indicators/bollinger_bands.h b/src/indicators/bollinger_bands.h new file mode 100644 index 0000000..507d379 --- /dev/null +++ b/src/indicators/bollinger_bands.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include +#include + +namespace barcola { + +// Calculate Bollinger Bands from closing prices. +// Returns {upper_bands, lower_bands}. +[[nodiscard]] std::pair, std::vector> calculateBollingerBands( + const std::vector& prices, size_t period, double stdDevFactor); + +} // namespace barcola diff --git a/src/indicators/moving_average.cpp b/src/indicators/moving_average.cpp new file mode 100644 index 0000000..f9f9620 --- /dev/null +++ b/src/indicators/moving_average.cpp @@ -0,0 +1,62 @@ +#include "indicators/moving_average.h" + +#include + +namespace barcola { + +MovingAverage::MovingAverage(size_t windowSize) : windowSize_(windowSize) {} + +void MovingAverage::addData(double value) { + data_.push_back(value); + if (data_.size() > windowSize_) { + data_.erase(data_.begin()); + } +} + +double MovingAverage::getSMA() const { + if (data_.empty()) { + return 0.0; + } + double sum = 0.0; + for (const double value : data_) { + sum += value; + } + return sum / static_cast(data_.size()); +} + +// FIX Bug 4: Original used backward size_t loop that could underflow. +// Now uses forward iteration which is mathematically equivalent for EMA. +double MovingAverage::getEMA(double smoothingFactor) const { + if (data_.empty()) { + return 0.0; + } + if (data_.size() == 1) { + return data_[0]; + } + + // Forward EMA: start from oldest, accumulate toward newest + double ema = data_[0]; + for (size_t i = 1; i < data_.size(); ++i) { + ema = smoothingFactor * data_[i] + (1.0 - smoothingFactor) * ema; + } + return ema; +} + +void printMovingAverages(const PriceHistory& history, size_t windowSize, + double emaSmoothingFactor) { + MovingAverage sma(windowSize); + MovingAverage ema(windowSize); + + const auto& dataPoints = history.getDataPoints(); + for (size_t i = 0; i < dataPoints.size(); ++i) { + const double closingPrice = dataPoints[i].getClosing(); + sma.addData(closingPrice); + ema.addData(closingPrice); + + std::cout << "Date: " << dataPoints[i].getDateString() + << " Closing Price: " << closingPrice << " SMA: " << sma.getSMA() + << " EMA: " << ema.getEMA(emaSmoothingFactor) << std::endl; + } +} + +} // namespace barcola diff --git a/src/indicators/moving_average.h b/src/indicators/moving_average.h new file mode 100644 index 0000000..d291819 --- /dev/null +++ b/src/indicators/moving_average.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include "core/price_history.h" + +namespace barcola { + +class MovingAverage { +public: + explicit MovingAverage(size_t windowSize); + + void addData(double value); + [[nodiscard]] double getSMA() const; + [[nodiscard]] double getEMA(double smoothingFactor) const; + +private: + size_t windowSize_; + std::vector data_; +}; + +// Prints SMA/EMA for each data point in the history +void printMovingAverages(const PriceHistory& history, size_t windowSize, + double emaSmoothingFactor); + +} // namespace barcola diff --git a/src/indicators/rsi.cpp b/src/indicators/rsi.cpp new file mode 100644 index 0000000..ae50e76 --- /dev/null +++ b/src/indicators/rsi.cpp @@ -0,0 +1,41 @@ +#include "indicators/rsi.h" + +#include + +namespace barcola { + +double calculateRSI(const std::vector& prices, size_t period) { + if (prices.size() <= period) { + return 0.0; // Insufficient data + } + + double avgGain = 0.0; + double avgLoss = 0.0; + + for (size_t i = 1; i <= period; ++i) { + const double diff = prices[i] - prices[i - 1]; + if (diff > 0.0) { + avgGain += diff; + } else { + avgLoss -= diff; // Make positive + } + } + + avgGain /= static_cast(period); + avgLoss /= static_cast(period); + + // FIX Bug 1: Guard against division by zero when avgLoss is zero (all gains) + if (avgLoss < 1e-10) { + return 100.0; + } + + // Also handle edge case where avgGain is zero (all losses) + if (avgGain < 1e-10) { + return 0.0; + } + + const double relativeStrength = avgGain / avgLoss; + return 100.0 - (100.0 / (1.0 + relativeStrength)); +} + +} // namespace barcola diff --git a/src/indicators/rsi.h b/src/indicators/rsi.h new file mode 100644 index 0000000..a197a9a --- /dev/null +++ b/src/indicators/rsi.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +namespace barcola { + +// Calculate RSI from a vector of closing prices. +// Returns 100.0 if all gains (no losses), 0.0 if all losses (no gains), +// 0.0 if insufficient data. +[[nodiscard]] double calculateRSI(const std::vector& prices, size_t period); + +} // namespace barcola diff --git a/src/risk/dynamic_hedging.cpp b/src/risk/dynamic_hedging.cpp new file mode 100644 index 0000000..02d7258 --- /dev/null +++ b/src/risk/dynamic_hedging.cpp @@ -0,0 +1,65 @@ +#include "risk/dynamic_hedging.h" + +#include +#include + +namespace barcola { + +DynamicHedging::DynamicHedging(const PriceHistory& priceHistory) : priceHistory_(priceHistory) {} + +DynamicHedging::MAResult DynamicHedging::computeMovingAverages(size_t shortWindow, + size_t longWindow) const { + const size_t dataSize = priceHistory_.dataPointsCount(); + if (dataSize < longWindow) { + return {{}, {}}; + } + + // Compute short MA + std::vector shortMA(dataSize - shortWindow + 1, 0.0); + for (size_t i = 0; i < dataSize - shortWindow + 1; ++i) { + double sum = 0.0; + for (size_t j = 0; j < shortWindow; ++j) { + sum += priceHistory_.getDataPoint(i + j).getClosing(); + } + shortMA[i] = sum / static_cast(shortWindow); + } + + // FIX Bug 3: longMA gets its OWN loop with correct longWindow bound. + // Original code shared the shortMA loop and only summed shortWindow elements for longMA. + std::vector longMA(dataSize - longWindow + 1, 0.0); + for (size_t i = 0; i < dataSize - longWindow + 1; ++i) { + double sum = 0.0; + for (size_t j = 0; j < longWindow; ++j) { + sum += priceHistory_.getDataPoint(i + j).getClosing(); + } + longMA[i] = sum / static_cast(longWindow); + } + + return {shortMA, longMA}; +} + +void DynamicHedging::performDynamicHedging(size_t shortWindow, size_t longWindow) const { + const size_t dataSize = priceHistory_.dataPointsCount(); + if (dataSize < longWindow) { + std::cout << "Insufficient data for hedging." << std::endl; + return; + } + + const auto [shortMA, longMA] = computeMovingAverages(shortWindow, longWindow); + + std::cout << "Moving Averages:" << std::endl; + // Only print where both MAs are valid. The longMA starts at index longWindow-1, + // shortMA starts at index shortWindow-1. We align by data index. + const size_t longOffset = longWindow - shortWindow; + const size_t printCount = std::min(shortMA.size() - longOffset, longMA.size()); + + for (size_t i = 0; i < printCount; ++i) { + const auto currentDataPoint = + priceHistory_.getDataPoint(i + longWindow - 1); + std::cout << "Date: " << currentDataPoint.getDateString() + << " Short MA: " << shortMA[i + longOffset] << " Long MA: " << longMA[i] + << std::endl; + } +} + +} // namespace barcola diff --git a/src/risk/dynamic_hedging.h b/src/risk/dynamic_hedging.h new file mode 100644 index 0000000..705a8d5 --- /dev/null +++ b/src/risk/dynamic_hedging.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +#include "core/price_history.h" + +namespace barcola { + +class DynamicHedging { +public: + DynamicHedging(const PriceHistory& priceHistory); + + void performDynamicHedging(size_t shortWindow = 20, size_t longWindow = 50) const; + + // Compute moving averages without printing (for testing) + struct MAResult { + std::vector shortMA; + std::vector longMA; + }; + [[nodiscard]] MAResult computeMovingAverages(size_t shortWindow, size_t longWindow) const; + +private: + const PriceHistory& priceHistory_; +}; + +} // namespace barcola diff --git a/src/risk/position_sizing.cpp b/src/risk/position_sizing.cpp new file mode 100644 index 0000000..81f1663 --- /dev/null +++ b/src/risk/position_sizing.cpp @@ -0,0 +1,44 @@ +#include "risk/position_sizing.h" + +#include + +namespace barcola { + +PositionSizing::PositionSizing(double riskPercentage, double stopLossPercentage) + : riskPercentage_(riskPercentage), stopLossPercentage_(stopLossPercentage) {} + +double PositionSizing::calculatePositionSize(double portfolioSize, double entryPrice) const { + const double stopLossAmount = entryPrice * stopLossPercentage_; + if (stopLossAmount < 1e-10) { + throw std::invalid_argument("Stop loss amount is effectively zero"); + } + const double riskAmount = portfolioSize * riskPercentage_; + return riskAmount / stopLossAmount; +} + +double PositionSizing::calculatePositionSizeWithMaxLoss(double portfolioSize, double entryPrice, + double maxLossAmount) const { + const double denominator = entryPrice * stopLossPercentage_; + if (denominator < 1e-10) { + throw std::invalid_argument("Stop loss amount is effectively zero"); + } + return maxLossAmount / denominator; +} + +void PositionSizing::setRiskPercentage(double percentage) { + riskPercentage_ = percentage; +} + +void PositionSizing::setStopLossPercentage(double percentage) { + stopLossPercentage_ = percentage; +} + +double PositionSizing::getRiskPercentage() const { + return riskPercentage_; +} + +double PositionSizing::getStopLossPercentage() const { + return stopLossPercentage_; +} + +} // namespace barcola diff --git a/src/risk/position_sizing.h b/src/risk/position_sizing.h new file mode 100644 index 0000000..871de7b --- /dev/null +++ b/src/risk/position_sizing.h @@ -0,0 +1,23 @@ +#pragma once + +namespace barcola { + +class PositionSizing { +public: + PositionSizing(double riskPercentage, double stopLossPercentage); + + [[nodiscard]] double calculatePositionSize(double portfolioSize, double entryPrice) const; + [[nodiscard]] double calculatePositionSizeWithMaxLoss(double portfolioSize, double entryPrice, + double maxLossAmount) const; + + void setRiskPercentage(double percentage); + void setStopLossPercentage(double percentage); + [[nodiscard]] double getRiskPercentage() const; + [[nodiscard]] double getStopLossPercentage() const; + +private: + double riskPercentage_; + double stopLossPercentage_; +}; + +} // namespace barcola diff --git a/src/risk/slippage_model.cpp b/src/risk/slippage_model.cpp new file mode 100644 index 0000000..0b3d299 --- /dev/null +++ b/src/risk/slippage_model.cpp @@ -0,0 +1,106 @@ +#include "risk/slippage_model.h" + +#include +#include +#include +#include + +namespace barcola { + +SlippageModel::SlippageModel(const PriceHistory& priceHistory, double slippagePercentage) + : priceHistory_(priceHistory), slippagePercentage_(slippagePercentage) {} + +double SlippageModel::calculateSlippage(time_t entryDate, double entryPrice, time_t exitDate, + double exitPrice, double tradeVolume) const { + const auto entryDataPoint = priceHistory_.getDataPoint(entryDate); + const auto exitDataPoint = priceHistory_.getDataPoint(exitDate); + + const double entryActualPrice = + calculateActualPrice(entryDataPoint.getClosing(), entryPrice); + const double exitActualPrice = + calculateActualPrice(exitDataPoint.getClosing(), exitPrice); + + const double slippage = entryActualPrice - exitActualPrice; + const double volumeFactor = calculateVolumeFactor(tradeVolume); + + return slippage * volumeFactor; +} + +double SlippageModel::calculateActualPrice(double referencePrice, double requestedPrice) const { + const double maxSlippage = referencePrice * slippagePercentage_; + double actualPrice = requestedPrice; + + if (actualPrice > referencePrice) { + actualPrice = std::min(referencePrice + maxSlippage, actualPrice); + } else if (actualPrice < referencePrice) { + actualPrice = std::max(referencePrice - maxSlippage, actualPrice); + } + + return actualPrice; +} + +double SlippageModel::calculateVolumeFactor(double tradeVolume) const { + double volumeFactor = 1.0; + + struct VolumeTier { + double threshold; + double factor; + }; + constexpr VolumeTier kVolumeTiers[] = { + {1000.0, 1.0}, + {10000.0, 0.95}, + {50000.0, 0.9}, + {100000.0, 0.85}, + }; + + for (const auto& tier : kVolumeTiers) { + if (tradeVolume >= tier.threshold) { + volumeFactor = tier.factor; + } else { + break; + } + } + + volumeFactor = std::max(volumeFactor, 0.5); + + const double marketVolatility = calculateMarketVolatility(); + volumeFactor *= calculateVolatilityAdjustment(marketVolatility); + + return volumeFactor; +} + +double SlippageModel::calculateMarketVolatility() const { + // Note: Uses random price changes for simulation. In production, this should use + // actual historical price changes. Preserved from original for backward compatibility. + constexpr size_t kNumDays = 30; + std::random_device rd; + std::mt19937 generator(rd()); + std::normal_distribution distribution(0.0, 0.02); + + std::vector priceChanges; + priceChanges.reserve(kNumDays); + for (size_t i = 0; i < kNumDays; ++i) { + priceChanges.push_back(distribution(generator)); + } + + double sum = 0.0; + for (const double val : priceChanges) { + sum += val; + } + const double mean = sum / static_cast(kNumDays); + + double squaredDiffSum = 0.0; + for (const double val : priceChanges) { + const double diff = val - mean; + squaredDiffSum += diff * diff; + } + + return std::sqrt(squaredDiffSum / static_cast(kNumDays)); +} + +double SlippageModel::calculateVolatilityAdjustment(double marketVolatility) const { + const double volatilityFactor = 1.0 - 0.5 * std::tanh(marketVolatility); + return std::max(volatilityFactor, 0.5); +} + +} // namespace barcola diff --git a/src/risk/slippage_model.h b/src/risk/slippage_model.h new file mode 100644 index 0000000..46b1e48 --- /dev/null +++ b/src/risk/slippage_model.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "core/price_history.h" + +namespace barcola { + +class SlippageModel { +public: + SlippageModel(const PriceHistory& priceHistory, double slippagePercentage); + + [[nodiscard]] double calculateSlippage(time_t entryDate, double entryPrice, time_t exitDate, + double exitPrice, double tradeVolume) const; + +private: + const PriceHistory& priceHistory_; + double slippagePercentage_; + + [[nodiscard]] double calculateActualPrice(double referencePrice, + double requestedPrice) const; + [[nodiscard]] double calculateVolumeFactor(double tradeVolume) const; + [[nodiscard]] double calculateMarketVolatility() const; + [[nodiscard]] double calculateVolatilityAdjustment(double marketVolatility) const; +}; + +} // namespace barcola diff --git a/src/simulation/monte_carlo.cpp b/src/simulation/monte_carlo.cpp new file mode 100644 index 0000000..b9898ed --- /dev/null +++ b/src/simulation/monte_carlo.cpp @@ -0,0 +1,52 @@ +#include "simulation/monte_carlo.h" + +namespace barcola { + +double performMonteCarloSimulation(const PriceHistory& history, size_t numSimulations, + double initialInvestment) { + const size_t dataSize = history.dataPointsCount(); + if (dataSize < 52) { + return 0.0; // Need at least 51 days for SMA(50) + 1 comparison day + } + + double totalProfit = 0.0; + + // Pre-compute SMAs once (they don't change between simulations in original code) + const auto smaShort = history.calculateSMA(20); + const auto smaLong = history.calculateSMA(50); + const auto& movingAveragesShort = smaShort.first; + const auto& movingAveragesLong = smaLong.first; + + const auto& dataPoints = history.getDataPoints(); + + for (size_t simulation = 0; simulation < numSimulations; ++simulation) { + double cash = initialInvestment; + double sharesOwned = 0.0; + + for (size_t day = 51; day < dataSize; ++day) { + const bool shouldBuy = movingAveragesShort[day - 1] > movingAveragesLong[day - 1] && + movingAveragesShort[day] <= movingAveragesLong[day]; + + const bool shouldSell = movingAveragesShort[day - 1] < movingAveragesLong[day - 1] && + movingAveragesShort[day] >= movingAveragesLong[day]; + + if (shouldBuy) { + constexpr double kInvestFraction = 0.2; + const double amountToInvest = cash * kInvestFraction; + const double sharesToBuy = amountToInvest / dataPoints[day].getClosing(); + sharesOwned += sharesToBuy; + cash -= amountToInvest; + } else if (shouldSell && sharesOwned > 0.0) { + cash += sharesOwned * dataPoints[day].getClosing(); + sharesOwned = 0.0; + } + } + + totalProfit += + (cash + sharesOwned * dataPoints[dataSize - 1].getClosing() - initialInvestment); + } + + return totalProfit / static_cast(numSimulations); +} + +} // namespace barcola diff --git a/src/simulation/monte_carlo.h b/src/simulation/monte_carlo.h new file mode 100644 index 0000000..7bc6f2a --- /dev/null +++ b/src/simulation/monte_carlo.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include "core/price_history.h" + +namespace barcola { + +// Run a Monte Carlo simulation using dual SMA crossover strategy. +// Returns average profit across all simulations. +[[nodiscard]] double performMonteCarloSimulation(const PriceHistory& history, + size_t numSimulations, + double initialInvestment); + +} // namespace barcola diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..2f7af1a --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,31 @@ +include(FetchContent) +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.14.0 +) +FetchContent_MakeAvailable(googletest) + +function(barcola_add_test name) + add_executable(${name} ${name}.cpp) + target_link_libraries(${name} PRIVATE barcola GTest::gtest_main) + target_include_directories(${name} PRIVATE ${PROJECT_SOURCE_DIR}/src) + target_compile_definitions(${name} PRIVATE + BARCOLA_TEST_DATA_DIR="${PROJECT_SOURCE_DIR}/data") + add_test(NAME ${name} COMMAND ${name}) +endfunction() + +barcola_add_test(test_time_utils) +barcola_add_test(test_price_point) +barcola_add_test(test_price_history) +barcola_add_test(test_moving_average) +barcola_add_test(test_rsi) +barcola_add_test(test_bollinger) +barcola_add_test(test_correlation) +barcola_add_test(test_mean_reversion) +barcola_add_test(test_market_data) +barcola_add_test(test_position_sizing) +barcola_add_test(test_slippage) +barcola_add_test(test_dynamic_hedging) +barcola_add_test(test_monte_carlo) +barcola_add_test(test_csv_io) diff --git a/tests/test_bollinger.cpp b/tests/test_bollinger.cpp new file mode 100644 index 0000000..fd88863 --- /dev/null +++ b/tests/test_bollinger.cpp @@ -0,0 +1,49 @@ +#include +#include "indicators/bollinger_bands.h" +#include "core/price_history.h" + +using namespace barcola; + +TEST(BollingerTest, ConstantPrice_BandsEqual) { + // Constant prices = zero std dev, so upper == lower == price + std::vector prices(10, 100.0); + auto [upper, lower] = calculateBollingerBands(prices, 5, 2.0); + EXPECT_EQ(upper.size(), 6u); // 10 - 5 + 1 = 6 + for (size_t i = 0; i < upper.size(); ++i) { + EXPECT_DOUBLE_EQ(upper[i], 100.0); + EXPECT_DOUBLE_EQ(lower[i], 100.0); + } +} + +TEST(BollingerTest, InsufficientData_ReturnsEmpty) { + std::vector prices = {100.0, 110.0}; + auto [upper, lower] = calculateBollingerBands(prices, 5, 2.0); + EXPECT_TRUE(upper.empty()); + EXPECT_TRUE(lower.empty()); +} + +TEST(BollingerTest, UpperAboveLower) { + // Non-constant prices should produce upper > lower + PriceHistory history("AAPL"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/AAPL_2023_sample.csv"); + + const auto& points = history.getDataPoints(); + std::vector prices; + for (const auto& p : points) { + prices.push_back(p.getClosing()); + } + + auto [upper, lower] = calculateBollingerBands(prices, 10, 2.0); + EXPECT_GT(upper.size(), 0u); + for (size_t i = 0; i < upper.size(); ++i) { + EXPECT_GE(upper[i], lower[i]); + } +} + +TEST(BollingerTest, PriceHistoryMethod_ReturnsBothBands) { + PriceHistory history("AAPL"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/AAPL_2023_sample.csv"); + auto [upper, lower] = history.calculateBollingerBands(10, 2.0); + EXPECT_EQ(upper.size(), lower.size()); + EXPECT_GT(upper.size(), 0u); +} diff --git a/tests/test_correlation.cpp b/tests/test_correlation.cpp new file mode 100644 index 0000000..b1360b4 --- /dev/null +++ b/tests/test_correlation.cpp @@ -0,0 +1,48 @@ +#include +#include "analysis/correlation.h" + +using namespace barcola; + +TEST(CorrelationTest, PerfectPositive) { + std::vector a = {1, 2, 3, 4, 5}; + std::vector b = {2, 4, 6, 8, 10}; + EXPECT_NEAR(calculateCorrelation(a, b), 1.0, 1e-10); +} + +TEST(CorrelationTest, PerfectNegative) { + std::vector a = {1, 2, 3, 4, 5}; + std::vector b = {10, 8, 6, 4, 2}; + EXPECT_NEAR(calculateCorrelation(a, b), -1.0, 1e-10); +} + +TEST(CorrelationTest, ZeroVariance_ReturnsZero) { + // Bug 2 regression test: constant series has zero variance + std::vector a = {100, 100, 100, 100, 100}; + std::vector b = {1, 2, 3, 4, 5}; + EXPECT_DOUBLE_EQ(calculateCorrelation(a, b), 0.0); +} + +TEST(CorrelationTest, BothConstant_ReturnsZero) { + std::vector a = {5, 5, 5}; + std::vector b = {10, 10, 10}; + EXPECT_DOUBLE_EQ(calculateCorrelation(a, b), 0.0); +} + +TEST(CorrelationTest, SinglePoint_ReturnsZero) { + std::vector a = {42}; + std::vector b = {99}; + EXPECT_DOUBLE_EQ(calculateCorrelation(a, b), 0.0); +} + +TEST(CorrelationTest, CorrelationAnalysisClass) { + PriceHistory histA("AAPL"); + PriceHistory histB("MSFT"); + histA.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/AAPL_2023_sample.csv"); + histB.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/MSFT_2023_sample.csv"); + + CorrelationAnalysis analysis(histA, histB); + double corr = analysis.calculateCorrelation(); + // AAPL and MSFT should be positively correlated + EXPECT_GT(corr, 0.0); + EXPECT_LE(corr, 1.0); +} diff --git a/tests/test_csv_io.cpp b/tests/test_csv_io.cpp new file mode 100644 index 0000000..8382952 --- /dev/null +++ b/tests/test_csv_io.cpp @@ -0,0 +1,68 @@ +#include +#include +#include "core/price_history.h" + +using namespace barcola; + +static std::string dataDir() { + return std::string(BARCOLA_TEST_DATA_DIR); +} + +TEST(CsvIOTest, LoadEmptyCsv_NoCrash) { + PriceHistory history("EMPTY"); + EXPECT_NO_THROW(history.loadFromCsv(dataDir() + "/empty.csv")); + EXPECT_EQ(history.dataPointsCount(), 0u); +} + +TEST(CsvIOTest, LoadSinglePointCsv) { + PriceHistory history("SINGLE"); + EXPECT_NO_THROW(history.loadFromCsv(dataDir() + "/single_point.csv")); + EXPECT_EQ(history.dataPointsCount(), 1u); +} + +TEST(CsvIOTest, LoadConstantPriceCsv) { + PriceHistory history("CONST"); + EXPECT_NO_THROW(history.loadFromCsv(dataDir() + "/constant_price.csv")); + EXPECT_EQ(history.dataPointsCount(), 10u); +} + +TEST(CsvIOTest, LoadMonotonicUpCsv) { + PriceHistory history("UP"); + EXPECT_NO_THROW(history.loadFromCsv(dataDir() + "/monotonic_up.csv")); + EXPECT_EQ(history.dataPointsCount(), 16u); +} + +TEST(CsvIOTest, LoadMonotonicDownCsv) { + PriceHistory history("DOWN"); + EXPECT_NO_THROW(history.loadFromCsv(dataDir() + "/monotonic_down.csv")); + EXPECT_EQ(history.dataPointsCount(), 16u); +} + +TEST(CsvIOTest, SaveAndReload_RoundTrip) { + PriceHistory original("AAPL"); + original.loadFromCsv(dataDir() + "/AAPL_2023_sample.csv"); + + const std::string tempPath = dataDir() + "/test_roundtrip_output.csv"; + original.saveToCsv(tempPath); + + PriceHistory reloaded("AAPL"); + reloaded.loadFromCsv(tempPath); + + EXPECT_EQ(original.dataPointsCount(), reloaded.dataPointsCount()); + + for (size_t i = 0; i < original.dataPointsCount(); ++i) { + auto orig = original.getDataPoint(i); + auto reload = reloaded.getDataPoint(i); + EXPECT_EQ(orig.getDateString(), reload.getDateString()); + EXPECT_NEAR(orig.getClosing(), reload.getClosing(), 0.01); + EXPECT_NEAR(orig.getOpening(), reload.getOpening(), 0.01); + } + + // Cleanup + std::filesystem::remove(tempPath); +} + +TEST(CsvIOTest, NonExistentFile_Throws) { + PriceHistory history("NONE"); + EXPECT_THROW(history.loadFromCsv("/nonexistent/path.csv"), std::runtime_error); +} diff --git a/tests/test_dynamic_hedging.cpp b/tests/test_dynamic_hedging.cpp new file mode 100644 index 0000000..4b338b8 --- /dev/null +++ b/tests/test_dynamic_hedging.cpp @@ -0,0 +1,52 @@ +#include +#include "risk/dynamic_hedging.h" + +using namespace barcola; + +TEST(DynamicHedgingTest, InsufficientData_EmptyResult) { + PriceHistory history("SINGLE"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/single_point.csv"); + + DynamicHedging hedging(history); + auto result = hedging.computeMovingAverages(20, 50); + EXPECT_TRUE(result.shortMA.empty()); + EXPECT_TRUE(result.longMA.empty()); +} + +TEST(DynamicHedgingTest, ShortMA_KnownValues) { + // Monotonic up: 10, 11, 12, ..., 25 (16 points) + // Short MA(3) at index 0: (10+11+12)/3 = 11.0 + PriceHistory history("UP"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/monotonic_up.csv"); + + DynamicHedging hedging(history); + auto result = hedging.computeMovingAverages(3, 5); + + EXPECT_GT(result.shortMA.size(), 0u); + EXPECT_NEAR(result.shortMA[0], 11.0, 1e-10); +} + +TEST(DynamicHedgingTest, LongMA_CorrectWindowSize) { + // Bug 3 regression test: longMA must use the full longWindow for calculation. + // Monotonic up: 10, 11, 12, 13, 14, ... (16 points) + // Long MA(5) at index 0: (10+11+12+13+14)/5 = 12.0 + PriceHistory history("UP"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/monotonic_up.csv"); + + DynamicHedging hedging(history); + auto result = hedging.computeMovingAverages(3, 5); + + EXPECT_GT(result.longMA.size(), 0u); + EXPECT_NEAR(result.longMA[0], 12.0, 1e-10); +} + +TEST(DynamicHedgingTest, ShortAndLongMA_DifferentSizes) { + PriceHistory history("AAPL"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/AAPL_2023_sample.csv"); + + DynamicHedging hedging(history); + auto result = hedging.computeMovingAverages(5, 10); + + // shortMA should be longer than longMA (more valid windows) + EXPECT_GT(result.shortMA.size(), result.longMA.size()); +} diff --git a/tests/test_market_data.cpp b/tests/test_market_data.cpp new file mode 100644 index 0000000..6496592 --- /dev/null +++ b/tests/test_market_data.cpp @@ -0,0 +1,57 @@ +#include +#include "analysis/market_data_analysis.h" + +using namespace barcola; + +TEST(MarketDataTest, AveragePrice_KnownValues) { + PriceHistory history("CONST"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/constant_price.csv"); + + MarketDataAnalysis analysis(history); + EXPECT_DOUBLE_EQ(analysis.calculateAveragePrice(), 100.0); +} + +TEST(MarketDataTest, Volatility_ConstantPrice_IsZero) { + PriceHistory history("CONST"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/constant_price.csv"); + + MarketDataAnalysis analysis(history); + EXPECT_DOUBLE_EQ(analysis.calculateVolatility(), 0.0); +} + +TEST(MarketDataTest, Volatility_NonConstant_IsPositive) { + PriceHistory history("AAPL"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/AAPL_2023_sample.csv"); + + MarketDataAnalysis analysis(history); + EXPECT_GT(analysis.calculateVolatility(), 0.0); +} + +TEST(MarketDataTest, UpwardTrends_MonotonicUp) { + PriceHistory history("UP"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/monotonic_up.csv"); + + MarketDataAnalysis analysis(history); + auto trends = analysis.getUpwardTrends(); + // All steps go up, so every point after the first is an upward trend + EXPECT_EQ(trends.size(), history.dataPointsCount() - 1); +} + +TEST(MarketDataTest, DownwardTrends_MonotonicDown) { + PriceHistory history("DOWN"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/monotonic_down.csv"); + + MarketDataAnalysis analysis(history); + auto trends = analysis.getDownwardTrends(); + EXPECT_EQ(trends.size(), history.dataPointsCount() - 1); +} + +TEST(MarketDataTest, EmptyData_NoCrash) { + PriceHistory history("EMPTY"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/empty.csv"); + + MarketDataAnalysis analysis(history); + EXPECT_DOUBLE_EQ(analysis.calculateAveragePrice(), 0.0); + EXPECT_DOUBLE_EQ(analysis.calculateVolatility(), 0.0); + EXPECT_TRUE(analysis.getUpwardTrends().empty()); +} diff --git a/tests/test_mean_reversion.cpp b/tests/test_mean_reversion.cpp new file mode 100644 index 0000000..3cfa998 --- /dev/null +++ b/tests/test_mean_reversion.cpp @@ -0,0 +1,57 @@ +#include +#include "analysis/mean_reversion.h" + +using namespace barcola; + +TEST(MeanReversionTest, ConstantPrices_ZeroDeviation) { + PriceHistory history("CONST"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/constant_price.csv"); + + MeanReversion mr(history, 5); + mr.calculateMeanReversion(); + + const auto& values = mr.getValues(); + EXPECT_GT(values.size(), 0u); + for (double val : values) { + EXPECT_NEAR(val, 0.0, 1e-10); + } +} + +TEST(MeanReversionTest, AverageDeviationOfConstant_IsZero) { + PriceHistory history("CONST"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/constant_price.csv"); + + MeanReversion mr(history, 3); + mr.calculateMeanReversion(); + EXPECT_NEAR(mr.getAverageMeanReversion(), 0.0, 1e-10); +} + +TEST(MeanReversionTest, InsufficientData_EmptyValues) { + PriceHistory history("SINGLE"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/single_point.csv"); + + MeanReversion mr(history, 5); + mr.calculateMeanReversion(); + EXPECT_EQ(mr.getValues().size(), 0u); +} + +TEST(MeanReversionTest, KnownValues_MonotonicUp) { + PriceHistory history("UP"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/monotonic_up.csv"); + + MeanReversion mr(history, 3); + mr.calculateMeanReversion(); + + // Prices: 10, 11, 12, 13, ... + // Window=3, first value at idx 2: MA=(10+11+12)/3=11.0, deviation=12-11=1.0 + EXPECT_NEAR(mr.getMeanReversionValue(0), 1.0, 1e-10); +} + +TEST(MeanReversionTest, GetValueOutOfRange_Throws) { + PriceHistory history("CONST"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/constant_price.csv"); + + MeanReversion mr(history, 3); + mr.calculateMeanReversion(); + EXPECT_THROW(mr.getMeanReversionValue(999), std::out_of_range); +} diff --git a/tests/test_monte_carlo.cpp b/tests/test_monte_carlo.cpp new file mode 100644 index 0000000..099df19 --- /dev/null +++ b/tests/test_monte_carlo.cpp @@ -0,0 +1,21 @@ +#include +#include "simulation/monte_carlo.h" + +using namespace barcola; + +TEST(MonteCarloTest, InsufficientData_ReturnsZero) { + PriceHistory history("UP"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/monotonic_up.csv"); + + // Only 16 data points, need 52+ for SMA(50) + double result = performMonteCarloSimulation(history, 10, 100000.0); + EXPECT_DOUBLE_EQ(result, 0.0); +} + +TEST(MonteCarloTest, EmptyData_ReturnsZero) { + PriceHistory history("EMPTY"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/empty.csv"); + + double result = performMonteCarloSimulation(history, 10, 100000.0); + EXPECT_DOUBLE_EQ(result, 0.0); +} diff --git a/tests/test_moving_average.cpp b/tests/test_moving_average.cpp new file mode 100644 index 0000000..98558b7 --- /dev/null +++ b/tests/test_moving_average.cpp @@ -0,0 +1,65 @@ +#include +#include "indicators/moving_average.h" + +using namespace barcola; + +TEST(MovingAverageTest, SMA_KnownValues) { + MovingAverage ma(3); + ma.addData(10.0); + ma.addData(20.0); + ma.addData(30.0); + EXPECT_DOUBLE_EQ(ma.getSMA(), 20.0); +} + +TEST(MovingAverageTest, SMA_WindowOverflow) { + MovingAverage ma(3); + ma.addData(10.0); + ma.addData(20.0); + ma.addData(30.0); + ma.addData(40.0); + // Window: [20, 30, 40] + EXPECT_DOUBLE_EQ(ma.getSMA(), 30.0); +} + +TEST(MovingAverageTest, SMA_Empty) { + MovingAverage ma(3); + EXPECT_DOUBLE_EQ(ma.getSMA(), 0.0); +} + +TEST(MovingAverageTest, EMA_SinglePoint) { + MovingAverage ma(3); + ma.addData(42.0); + EXPECT_DOUBLE_EQ(ma.getEMA(0.5), 42.0); +} + +TEST(MovingAverageTest, EMA_Empty) { + MovingAverage ma(3); + EXPECT_DOUBLE_EQ(ma.getEMA(0.5), 0.0); +} + +TEST(MovingAverageTest, EMA_KnownValues) { + // Forward EMA with smoothing=0.5: start at first value, blend forward + MovingAverage ma(5); + ma.addData(10.0); + ma.addData(20.0); + ma.addData(30.0); + double ema = ma.getEMA(0.5); + // ema[0]=10, ema[1]=0.5*20+0.5*10=15, ema[2]=0.5*30+0.5*15=22.5 + EXPECT_DOUBLE_EQ(ema, 22.5); +} + +TEST(MovingAverageTest, PriceHistorySMA_KnownValues) { + // SMA(3) for prices [10, 20, 30, 40, 50]: + // Index 2: (10+20+30)/3 = 20.0 + // Index 3: (20+30+40)/3 = 30.0 + // Index 4: (30+40+50)/3 = 40.0 + PriceHistory history("TEST"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/monotonic_up.csv"); + + auto [sma, _] = history.calculateSMA(3); + // First valid SMA is at index 2 (3rd element) + // Prices: 10, 11, 12, ... SMA(3) at idx 2 = (10+11+12)/3 = 11.0 + EXPECT_NEAR(sma[2], 11.0, 1e-10); + // SMA(3) at idx 3 = (11+12+13)/3 = 12.0 + EXPECT_NEAR(sma[3], 12.0, 1e-10); +} diff --git a/tests/test_position_sizing.cpp b/tests/test_position_sizing.cpp new file mode 100644 index 0000000..08ba54b --- /dev/null +++ b/tests/test_position_sizing.cpp @@ -0,0 +1,31 @@ +#include +#include "risk/position_sizing.h" + +using namespace barcola; + +TEST(PositionSizingTest, KnownValues) { + // risk=2.5%, stopLoss=2% + // positionSize = (100000 * 0.025) / (50 * 0.02) = 2500 / 1.0 = 2500 + PositionSizing ps(0.025, 0.02); + double result = ps.calculatePositionSize(100000.0, 50.0); + EXPECT_DOUBLE_EQ(result, 2500.0); +} + +TEST(PositionSizingTest, WithMaxLoss) { + // maxLoss=1000, entryPrice=50, stopLoss=2% + // positionSize = 1000 / (50 * 0.02) = 1000 / 1.0 = 1000 + PositionSizing ps(0.025, 0.02); + double result = ps.calculatePositionSizeWithMaxLoss(100000.0, 50.0, 1000.0); + EXPECT_DOUBLE_EQ(result, 1000.0); +} + +TEST(PositionSizingTest, SettersAndGetters) { + PositionSizing ps(0.01, 0.05); + EXPECT_DOUBLE_EQ(ps.getRiskPercentage(), 0.01); + EXPECT_DOUBLE_EQ(ps.getStopLossPercentage(), 0.05); + + ps.setRiskPercentage(0.03); + ps.setStopLossPercentage(0.10); + EXPECT_DOUBLE_EQ(ps.getRiskPercentage(), 0.03); + EXPECT_DOUBLE_EQ(ps.getStopLossPercentage(), 0.10); +} diff --git a/tests/test_price_history.cpp b/tests/test_price_history.cpp new file mode 100644 index 0000000..05b910e --- /dev/null +++ b/tests/test_price_history.cpp @@ -0,0 +1,75 @@ +#include +#include "core/price_history.h" + +using namespace barcola; + +static std::string dataDir() { + return std::string(BARCOLA_TEST_DATA_DIR); +} + +TEST(PriceHistoryTest, LoadFromCsv_CorrectCount) { + PriceHistory history("AAPL"); + history.loadFromCsv(dataDir() + "/AAPL_2023_sample.csv"); + EXPECT_EQ(history.dataPointsCount(), 30u); +} + +TEST(PriceHistoryTest, LoadFromCsv_FirstRow) { + PriceHistory history("AAPL"); + history.loadFromCsv(dataDir() + "/AAPL_2023_sample.csv"); + auto first = history.getDataPoint(static_cast(0)); + EXPECT_DOUBLE_EQ(first.getOpening(), 130.28); + EXPECT_DOUBLE_EQ(first.getClosing(), 125.07); + EXPECT_EQ(first.getDateString(), "2023-01-03"); +} + +TEST(PriceHistoryTest, GetDataPointByDate) { + PriceHistory history("AAPL"); + history.loadFromCsv(dataDir() + "/AAPL_2023_sample.csv"); + auto dp = history.getDataPoint(std::string("2023-01-06")); + EXPECT_DOUBLE_EQ(dp.getClosing(), 129.62); +} + +TEST(PriceHistoryTest, GetDataPoint_OutOfRange_Throws) { + PriceHistory history("AAPL"); + history.loadFromCsv(dataDir() + "/AAPL_2023_sample.csv"); + EXPECT_THROW(history.getDataPoint(static_cast(999)), std::invalid_argument); +} + +TEST(PriceHistoryTest, GetDataPoint_InvalidDate_Throws) { + PriceHistory history("AAPL"); + history.loadFromCsv(dataDir() + "/AAPL_2023_sample.csv"); + EXPECT_THROW(history.getDataPoint(std::string("2099-01-01")), std::invalid_argument); +} + +TEST(PriceHistoryTest, ClearDataPoints) { + PriceHistory history("AAPL"); + history.loadFromCsv(dataDir() + "/AAPL_2023_sample.csv"); + EXPECT_GT(history.dataPointsCount(), 0u); + history.clearDataPoints(); + EXPECT_EQ(history.dataPointsCount(), 0u); +} + +TEST(PriceHistoryTest, EmptyCsv_ZeroDataPoints) { + PriceHistory history("EMPTY"); + history.loadFromCsv(dataDir() + "/empty.csv"); + EXPECT_EQ(history.dataPointsCount(), 0u); +} + +TEST(PriceHistoryTest, SinglePointCsv) { + PriceHistory history("SINGLE"); + history.loadFromCsv(dataDir() + "/single_point.csv"); + EXPECT_EQ(history.dataPointsCount(), 1u); + EXPECT_DOUBLE_EQ(history.getDataPoint(static_cast(0)).getClosing(), 152.0); +} + +TEST(PriceHistoryTest, GetAssetSymbol) { + PriceHistory history("SPY"); + EXPECT_EQ(history.getAssetSymbol(), "SPY"); +} + +TEST(PriceHistoryTest, GetDataPoints_ReturnsConstRef) { + PriceHistory history("AAPL"); + history.loadFromCsv(dataDir() + "/AAPL_2023_sample.csv"); + const auto& points = history.getDataPoints(); + EXPECT_EQ(points.size(), 30u); +} diff --git a/tests/test_price_point.cpp b/tests/test_price_point.cpp new file mode 100644 index 0000000..defe2fa --- /dev/null +++ b/tests/test_price_point.cpp @@ -0,0 +1,37 @@ +#include +#include "core/price_point.h" +#include "core/time_utils.h" + +using namespace barcola; + +TEST(PricePointTest, ConstructWithOHLC) { + PricePoint pp("2023-01-03", 130.28, 130.90, 124.17, 125.07); + EXPECT_DOUBLE_EQ(pp.getOpening(), 130.28); + EXPECT_DOUBLE_EQ(pp.getHighest(), 130.90); + EXPECT_DOUBLE_EQ(pp.getLowest(), 124.17); + EXPECT_DOUBLE_EQ(pp.getClosing(), 125.07); + EXPECT_EQ(pp.getDateString(), "2023-01-03"); +} + +TEST(PricePointTest, ConstructWithSinglePrice) { + PricePoint pp("2023-01-03", 150.0); + EXPECT_DOUBLE_EQ(pp.getOpening(), 150.0); + EXPECT_DOUBLE_EQ(pp.getHighest(), 150.0); + EXPECT_DOUBLE_EQ(pp.getLowest(), 150.0); + EXPECT_DOUBLE_EQ(pp.getClosing(), 150.0); +} + +TEST(PricePointTest, ConstructWithEpoch) { + time_t epoch = convertDateToEpoch("2023-06-15"); + PricePoint pp(epoch, 100.0, 110.0, 90.0, 105.0); + EXPECT_EQ(pp.getDateString(), "2023-06-15"); + EXPECT_DOUBLE_EQ(pp.getClosing(), 105.0); +} + +TEST(PricePointTest, ToString_ContainsExpectedFields) { + PricePoint pp("2023-01-03", 130.28, 130.90, 124.17, 125.07); + std::string str = pp.toString(); + EXPECT_NE(str.find("2023-01-03"), std::string::npos); + EXPECT_NE(str.find("opening"), std::string::npos); + EXPECT_NE(str.find("closing"), std::string::npos); +} diff --git a/tests/test_rsi.cpp b/tests/test_rsi.cpp new file mode 100644 index 0000000..0f370a8 --- /dev/null +++ b/tests/test_rsi.cpp @@ -0,0 +1,49 @@ +#include +#include "indicators/rsi.h" +#include "core/price_history.h" + +using namespace barcola; + +TEST(RSITest, AllGains_Returns100) { + // Monotonically increasing: RSI should be 100 (Bug 1 regression test) + PriceHistory history("UP"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/monotonic_up.csv"); + + const auto& points = history.getDataPoints(); + std::vector prices; + for (const auto& p : points) { + prices.push_back(p.getClosing()); + } + + double rsi = calculateRSI(prices, 14); + EXPECT_DOUBLE_EQ(rsi, 100.0); +} + +TEST(RSITest, AllLosses_Returns0) { + PriceHistory history("DOWN"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/monotonic_down.csv"); + + const auto& points = history.getDataPoints(); + std::vector prices; + for (const auto& p : points) { + prices.push_back(p.getClosing()); + } + + double rsi = calculateRSI(prices, 14); + EXPECT_DOUBLE_EQ(rsi, 0.0); +} + +TEST(RSITest, InsufficientData_Returns0) { + std::vector prices = {10.0, 20.0, 30.0}; + EXPECT_DOUBLE_EQ(calculateRSI(prices, 14), 0.0); +} + +TEST(RSITest, KnownValues) { + // Hand-calculated: prices 100, 102, 101, 103, 104, 102 (period=5) + // Changes: +2, -1, +2, +1, -2 + // avgGain = (2+2+1)/5 = 1.0, avgLoss = (1+2)/5 = 0.6 + // RS = 1.0/0.6 = 1.667, RSI = 100 - 100/(1+1.667) = 62.5 + std::vector prices = {100.0, 102.0, 101.0, 103.0, 104.0, 102.0}; + double rsi = calculateRSI(prices, 5); + EXPECT_NEAR(rsi, 62.5, 0.1); +} diff --git a/tests/test_slippage.cpp b/tests/test_slippage.cpp new file mode 100644 index 0000000..3d97ed1 --- /dev/null +++ b/tests/test_slippage.cpp @@ -0,0 +1,22 @@ +#include +#include +#include "risk/slippage_model.h" + +using namespace barcola; + +TEST(SlippageTest, BasicCalculation_NoCrash) { + PriceHistory history("AAPL"); + history.loadFromCsv(std::string(BARCOLA_TEST_DATA_DIR) + "/AAPL_2023_sample.csv"); + + SlippageModel model(history, 0.01); + + // Use dates from the data + time_t entry = history.getDataPoint(std::string("2023-01-03")).getDate(); + time_t exit = history.getDataPoint(std::string("2023-01-06")).getDate(); + + // Should not throw + double slippage = model.calculateSlippage(entry, 126.0, exit, 130.0, 10000.0); + // Result is non-deterministic due to random volatility, just check it's finite + EXPECT_FALSE(std::isnan(slippage)); + EXPECT_FALSE(std::isinf(slippage)); +} diff --git a/tests/test_time_utils.cpp b/tests/test_time_utils.cpp new file mode 100644 index 0000000..24dcc4d --- /dev/null +++ b/tests/test_time_utils.cpp @@ -0,0 +1,45 @@ +#include +#include "core/time_utils.h" + +using namespace barcola; + +TEST(TimeUtilsTest, ConvertDateToEpoch_ValidDate) { + time_t epoch = convertDateToEpoch("2023-01-15"); + EXPECT_GT(epoch, 0); +} + +TEST(TimeUtilsTest, RoundTrip_DateToEpochAndBack) { + std::string original = "2023-06-15"; + time_t epoch = convertDateToEpoch(original); + std::string result = convertEpochToDate(epoch); + EXPECT_EQ(result, original); +} + +TEST(TimeUtilsTest, RoundTrip_MultipleFormats) { + std::vector dates = {"2020-01-01", "2023-12-31", "2000-06-15"}; + for (const auto& date : dates) { + EXPECT_EQ(convertEpochToDate(convertDateToEpoch(date)), date); + } +} + +TEST(TimeUtilsTest, IsDateEarlierOrEqual_Earlier) { + EXPECT_TRUE(isDateEarlierOrEqual("2023-01-01", "2023-06-15")); +} + +TEST(TimeUtilsTest, IsDateEarlierOrEqual_Equal) { + EXPECT_TRUE(isDateEarlierOrEqual("2023-06-15", "2023-06-15")); +} + +TEST(TimeUtilsTest, IsDateEarlierOrEqual_Later) { + EXPECT_FALSE(isDateEarlierOrEqual("2023-12-31", "2023-01-01")); +} + +TEST(TimeUtilsTest, InvalidDateFormat_Throws) { + EXPECT_THROW(convertDateToEpoch("not-a-date"), std::invalid_argument); + EXPECT_THROW(convertDateToEpoch("2023/01/15"), std::invalid_argument); + EXPECT_THROW(convertDateToEpoch(""), std::invalid_argument); +} + +TEST(TimeUtilsTest, GetCurrentEpoch_ReturnsPositive) { + EXPECT_GT(getCurrentEpoch(), 0); +}