Skip to content

BacktestResult

BacktestResult collects fills during backtest execution and computes performance statistics, including an equity curve and a range of risk/reward metrics.

BacktestConfig

struct BacktestConfig
{
  double initialCapital{100000.0};
  double feeRate{0.0001};            // 0.01% per trade
  bool usePercentageFee{true};
  double fixedFeePerTrade{0.0};

  SlippageProfile defaultSlippage{};
  std::vector<std::pair<SymbolId, SlippageProfile>> perSymbolSlippage{};

  QueueModel queueModel{QueueModel::NONE};
  size_t queueDepth{8};

  double riskFreeRate{0.0};
  double metricsAnnualizationFactor{252.0};
};
Field Default Description
initialCapital 100000.0 Starting capital
feeRate 0.0001 Fee as fraction of notional (0.01%)
usePercentageFee true Use percentage fee vs fixed
fixedFeePerTrade 0.0 Fixed fee per trade when percentage is off
defaultSlippage {NONE} Slippage profile applied when no per-symbol override exists. See slippage.md.
perSymbolSlippage {} Per-symbol overrides of the default profile
queueModel NONE Queue simulation mode for limit orders. See queue_simulation.md.
queueDepth 8 Levels tracked in FULL mode
riskFreeRate 0.0 Per-period risk-free rate subtracted from each trade return before Sharpe/Sortino
metricsAnnualizationFactor 252.0 Scaling factor used for annualized Sharpe/Sortino/Calmar (sqrt applied)

BacktestStats

struct BacktestStats
{
  size_t totalTrades;
  size_t winningTrades;
  size_t losingTrades;

  double initialCapital;
  double finalCapital;
  double totalPnl;
  double totalFees;
  double netPnl;
  double grossProfit;
  double grossLoss;

  double maxDrawdown;
  double maxDrawdownPct;

  double winRate;
  double profitFactor;
  double avgWin;
  double avgLoss;
  double avgWinLossRatio;

  size_t maxConsecutiveWins;
  size_t maxConsecutiveLosses;

  double avgTradeDurationNs;
  double medianTradeDurationNs;
  double maxTradeDurationNs;

  double sharpeRatio;
  double sortinoRatio;
  double calmarRatio;
  double timeWeightedReturn;
  double returnPct;

  UnixNanos startTimeNs;
  UnixNanos endTimeNs;
};

New metrics

Metric Description
avgWinLossRatio avgWin / avgLoss (undefined when no losses)
maxConsecutiveWins Longest streak of profitable trades
maxConsecutiveLosses Longest streak of losing trades
avgTradeDurationNs Mean time from entry to exit in nanoseconds
medianTradeDurationNs Median trade duration in nanoseconds
maxTradeDurationNs Longest trade duration in nanoseconds
timeWeightedReturn Cumulative product of per-trade returns minus 1

Formula changes

Sharpe, Sortino and Calmar are computed from the equity curve. A per-period return is the relative change from the previous equity point (or from initialCapital for the first point), with riskFreeRate subtracted:

r_i = (equity[i] - equity[i-1]) / equity[i-1] - riskFreeRate

Sharpe and Sortino annualize the series with sqrt(metricsAnnualizationFactor). Calmar is annualizedReturn / maxDrawdownPct, where annualizedReturn = (1 + TWR)^(metricsAnnualizationFactor / n) - 1 and n is the number of observed periods (closed trades).

TradeRecord.fee now includes both the entry-fill fee and the exit-fill fee pro-rated by the quantity closed in the trade. Partial closes keep the residual entry fee accrued to the remaining open portion of the position.

EquityPoint

struct EquityPoint
{
  UnixNanos timestampNs;
  double equity;
  double drawdownPct;
};

One point is appended to the equity curve on every closed trade. The curve is accessible via BacktestResult::equityCurve() and can be written to a CSV file with header timestamp_ns,equity,drawdown_pct via writeEquityCurveCsv(path).

BacktestResult

class BacktestResult
{
public:
  explicit BacktestResult(const BacktestConfig& config = {}, size_t expectedFills = 0);

  void recordFill(const Fill& fill);
  BacktestStats computeStats() const;

  const std::vector<Fill>& fills() const;
  const std::vector<TradeRecord>& trades() const;
  const std::vector<EquityPoint>& equityCurve() const;
  double totalPnl() const;

  bool writeEquityCurveCsv(const std::string& path) const;
};

Each TradeRecord now carries both the entry and exit price, the closed quantity, and the entry/exit timestamps. These fields drive the duration statistics and enable equity-curve analysis per trade.

Usage

BacktestConfig cfg;
cfg.defaultSlippage = {SlippageModel::FIXED_BPS, 0, 2.0, 0.0};  // 2 bps
cfg.queueModel = QueueModel::TOB;
cfg.riskFreeRate = 0.0;

BacktestResult result(cfg);
// feed fills via result.recordFill(...) or by running a BacktestRunner
auto stats = result.computeStats();

std::cout << "Return: " << stats.returnPct << "%\n";
std::cout << "Sharpe: " << stats.sharpeRatio << "\n";
std::cout << "Sortino: " << stats.sortinoRatio << "\n";
std::cout << "Calmar: " << stats.calmarRatio << "\n";
std::cout << "Max consecutive wins: " << stats.maxConsecutiveWins << "\n";
std::cout << "Avg duration (s): " << stats.avgTradeDurationNs / 1e9 << "\n";

result.writeEquityCurveCsv("equity.csv");