Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(CURL REQUIRED)

add_subdirectory(src)
add_subdirectory(strategies)

option(BARCOLA_BUILD_TESTS "Build tests" ON)
if(BARCOLA_BUILD_TESTS)
Expand Down
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ add_library(barcola STATIC
risk/slippage_model.cpp
risk/dynamic_hedging.cpp
simulation/monte_carlo.cpp
backtest/performance.cpp
backtest/backtest_engine.cpp
)

target_include_directories(barcola PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
Expand Down
140 changes: 140 additions & 0 deletions src/backtest/backtest_engine.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#include "backtest/backtest_engine.h"

#include <cmath>
#include <stdexcept>

namespace barcola {

BacktestEngine::BacktestEngine(BacktestConfig config)
: config_(std::move(config)) {}

BacktestResult BacktestEngine::run(Strategy& strategy,
const PriceHistory& data) const {
const auto& points = data.getDataPoints();
if (points.empty()) {
throw std::invalid_argument("Cannot backtest on empty price data");
}

BacktestResult result;
double cash = config_.initialCapital;
double shares = 0.0;
double entryPrice = 0.0;
std::string entryDate;
bool inPosition = false;

result.equityCurve.reserve(points.size());

for (size_t i = 0; i < points.size(); ++i) {
const double price = points[i].getClosing();
const Signal signal = strategy.evaluate(points, i);

if (signal == Signal::BUY && !inPosition) {
// Enter long position
const double execPrice = applySlippage(price, true);
shares = calculateShares(cash, execPrice);
if (shares > 0.0) {
const double cost = shares * execPrice;
const double commission = applyCommission(cost);
cash -= (cost + commission);
entryPrice = execPrice;
entryDate = points[i].getDateString();
inPosition = true;
}
} else if (signal == Signal::SELL && inPosition) {
// Exit long position
const double execPrice = applySlippage(price, false);
const double proceeds = shares * execPrice;
const double commission = applyCommission(proceeds);
cash += (proceeds - commission);

Trade trade;
trade.symbol = data.getAssetSymbol();
trade.entryPrice = entryPrice;
trade.exitPrice = execPrice;
trade.entryDate = entryDate;
trade.exitDate = points[i].getDateString();
trade.shares = shares;
trade.pnl = (execPrice - entryPrice) * shares -
applyCommission(shares * entryPrice) -
applyCommission(proceeds);
trade.returnPct = (execPrice - entryPrice) / entryPrice;
result.trades.push_back(trade);

shares = 0.0;
inPosition = false;
}

// Record equity: cash + mark-to-market position value
const double portfolioValue = cash + shares * price;
result.equityCurve.push_back(portfolioValue);
}

// Close any open position at the last price
if (inPosition && !points.empty()) {
const double lastPrice = points.back().getClosing();
const double execPrice = applySlippage(lastPrice, false);
const double proceeds = shares * execPrice;
const double commission = applyCommission(proceeds);
cash += (proceeds - commission);

Trade trade;
trade.symbol = data.getAssetSymbol();
trade.entryPrice = entryPrice;
trade.exitPrice = execPrice;
trade.entryDate = entryDate;
trade.exitDate = points.back().getDateString();
trade.shares = shares;
trade.pnl = (execPrice - entryPrice) * shares -
applyCommission(shares * entryPrice) -
applyCommission(proceeds);
trade.returnPct = (execPrice - entryPrice) / entryPrice;
result.trades.push_back(trade);

shares = 0.0;
result.equityCurve.back() = cash;
}

// Compute summary metrics
const double finalEquity = result.equityCurve.back();
result.totalReturn = (finalEquity / config_.initialCapital) - 1.0;

const double years = static_cast<double>(points.size()) /
TRADING_DAYS_PER_YEAR;
if (years > 1e-12) {
result.annualizedReturn =
std::pow(1.0 + result.totalReturn, 1.0 / years) - 1.0;
}

result.sharpeRatio = calculateSharpeRatio(result.equityCurve);
result.sortinoRatio = calculateSortinoRatio(result.equityCurve);
result.maxDrawdown = calculateMaxDrawdown(result.equityCurve);
result.calmarRatio = calculateCalmarRatio(result.equityCurve);

const TradeStatistics stats = analyzeTradeHistory(result.trades);
result.winRate = stats.winRate;
result.profitFactor = stats.profitFactor;
result.totalTrades = stats.totalTrades;
result.avgHoldDays = stats.avgHoldDays;

return result;
}

double BacktestEngine::applySlippage(double price, bool isBuy) const {
const double slippageFraction = config_.slippageBps / 10000.0;
return isBuy ? price * (1.0 + slippageFraction)
: price * (1.0 - slippageFraction);
}

double BacktestEngine::applyCommission(double tradeValue) const {
return tradeValue * config_.commissionRate;
}

double BacktestEngine::calculateShares(double capital, double price) const {
if (price <= 0.0) {
return 0.0;
}
const double riskCapital = capital * config_.riskPerTrade;
return std::floor(riskCapital / price);
}

} // namespace barcola
47 changes: 47 additions & 0 deletions src/backtest/backtest_engine.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#pragma once

#include <vector>
#include "backtest/strategy.h"
#include "backtest/performance.h"
#include "core/price_history.h"

namespace barcola {

struct BacktestConfig {
double initialCapital = 100000.0;
double commissionRate = 0.001; // 0.1% per trade
double slippageBps = 5.0; // 5 basis points
double riskPerTrade = 0.02; // 2% risk per trade
};

struct BacktestResult {
std::vector<Trade> trades;
std::vector<double> equityCurve;
double totalReturn = 0.0;
double annualizedReturn = 0.0;
double sharpeRatio = 0.0;
double sortinoRatio = 0.0;
double maxDrawdown = 0.0;
double calmarRatio = 0.0;
double winRate = 0.0;
double profitFactor = 0.0;
size_t totalTrades = 0;
double avgHoldDays = 0.0;
};

class BacktestEngine {
public:
explicit BacktestEngine(BacktestConfig config);

[[nodiscard]] BacktestResult run(Strategy& strategy,
const PriceHistory& data) const;

private:
BacktestConfig config_;

[[nodiscard]] double applySlippage(double price, bool isBuy) const;
[[nodiscard]] double applyCommission(double tradeValue) const;
[[nodiscard]] double calculateShares(double capital, double price) const;
};

} // namespace barcola
Loading