AlgoDaily

Backtesting and Strategy Analysis

Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.

Test Before Live

Why this lesson matters

In Implementing Trading Algorithms, you built a strategy layer that can emit trading intent. In Optimization and Performance Tuning, you learned that milliseconds matter and that performance must be measured, not guessed.

Now we connect both ideas: does the strategy actually work on historical data?

Backtesting answers questions like:

  • Would this SMA crossover strategy have made money on past prices?
  • How often did it trade?
  • Did costs erase the edge?
  • Was the strategy stable, or did it only work on one lucky period?

A backtest is not a prediction machine. It is a debugger for trading logic and assumptions before you connect real capital.

Test Before Live

Backtest Pipeline

The core loop

A backtester replays historical market data as if your application were running in the past.

Historical bars/ticks
        ↓
Strategy signal
        ↓
Execution simulation
        ↓
Portfolio update
        ↓
Metrics + trade log

The important part is time order. Your strategy must only see data that would have been available at that moment.

What we simulate today

We will keep the scope focused:

  • Historical close prices
  • A simple moving-average crossover strategy from the earlier lesson
  • Buy/sell execution simulation
  • Basic performance statistics

We will not deeply cover position sizing limits, capital-at-risk rules, or account security yet. Those belong to the next lesson: Risk Management and Security.

Backtest Pipeline

Minimal Backtester

A small event-style simulation

Below is a standalone C++ example that replays close prices, calculates short and long SMAs using only prior data, executes trades, and prints a final report.

Notice the key anti-cheat rule:

At index i, the strategy computes averages from prices before i, then executes at price i.

That prevents the strategy from using the current bar's closing price to decide a trade at that same close.

```cpp
#include <iostream>
#include <vector>
#include <iomanip>
#include <string>

struct Trade {
    int index;
    std::string side;
    double price;
    int quantity;
};

double average(const std::vector<double>& prices, int begin, int end) {
    double sum = 0.0;
    for (int i = begin; i < end; ++i) sum += prices[i];
    return sum / (end - begin);
}

int main() {
    std::vector<double> close = {
        100, 101, 102, 103, 104, 103, 102, 101,
        103, 105, 107, 109, 108, 110, 112, 111,
        113, 115, 114, 116
    };

    const int fastN = 3;
    const int slowN = 5;
    const double feePerTrade = 1.00;

    double cash = 10000.0;
    int shares = 0;
    bool inPosition = false;
    std::vector<Trade> trades;

    for (int i = slowN; i < static_cast<int>(close.size()); ++i) {
        // Use only completed historical data: [i - N, i)
        double fast = average(close, i - fastN, i);
        double slow = average(close, i - slowN, i);
        double price = close[i];

        if (!inPosition && fast > slow) {
            int quantity = static_cast<int>((cash - feePerTrade) / price);
            if (quantity > 0) {
                cash -= quantity * price + feePerTrade;
                shares += quantity;
                inPosition = true;
                trades.push_back({i, "BUY", price, quantity});
            }
        } else if (inPosition && fast < slow) {
            cash += shares * price - feePerTrade;
            trades.push_back({i, "SELL", price, shares});
            shares = 0;
            inPosition = false;
        }
    }

    double finalEquity = cash + shares * close.back();

    std::cout << std::fixed << std::setprecision(2);
    std::cout << "Trades:\n";
    for (const auto& t : trades) {
        std::cout << "  bar=" << t.index
                  << " side=" << t.side
                  << " price=" << t.price
                  << " qty=" << t.quantity << "\n";
    }

    std::cout << "\nFinal cash: " << cash << "\n";
    std::cout << "Final shares: " << shares << "\n";
    std::cout << "Final equity: " << finalEquity << "\n";
    std::cout << "Total return: " << ((finalEquity / 10000.0) - 1.0) * 100.0 << "%\n";

    return 0;
}
```

Avoiding Backtest Cheating

A strategy calculates today's closing price, decides to buy, and assumes the buy also happened at that same closing price.

What is the main issue?

Measure The Strategy

Profit is not enough

A strategy can make money and still be unattractive if it suffers deep drawdowns or unstable returns.

Common backtest metrics include:

  • Total return: final equity compared with starting equity
  • Maximum drawdown: worst peak-to-trough equity decline
  • Sharpe ratio: return consistency relative to volatility
  • Trade count: too many trades can mean costs dominate

In a real trading system, these metrics become part of your decision loop: keep, adjust, or reject the strategy.

```cpp
#include <iostream>
#include <vector>
#include <cmath>
#include <iomanip>
#include <algorithm>

int main() {
    // Example equity curve produced by a backtest.
    std::vector<double> equity = {
        10000, 10080, 10040, 10120, 10250,
        10100, 10300, 10420, 10350, 10500
    };

    std::vector<double> returns;
    for (size_t i = 1; i < equity.size(); ++i) {
        returns.push_back((equity[i] / equity[i - 1]) - 1.0);
    }

    double totalReturn = (equity.back() / equity.front()) - 1.0;

    double peak = equity.front();
    double maxDrawdown = 0.0;
    for (double value : equity) {
        peak = std::max(peak, value);
        double drawdown = (peak - value) / peak;
        maxDrawdown = std::max(maxDrawdown, drawdown);
    }

    double mean = 0.0;
    for (double r : returns) mean += r;
    mean /= returns.size();

    double variance = 0.0;
    for (double r : returns) variance += (r - mean) * (r - mean);
    variance /= returns.size();

    double volatility = std::sqrt(variance);
    double sharpe = volatility == 0.0 ? 0.0 : (mean / volatility) * std::sqrt(252.0);

    std::cout << std::fixed << std::setprecision(4);
    std::cout << "Total return: " << totalReturn * 100.0 << "%\n";
    std::cout << "Max drawdown: " << maxDrawdown * 100.0 << "%\n";
    std::cout << "Sharpe ratio estimate: " << sharpe << "\n";

    return 0;
}
```

One Good Metric

True or false:

If a backtest has a positive total return, the strategy is automatically ready for live trading.

Costs And Slippage

The market is not frictionless

A beginner backtest often assumes:

  • Every order fills instantly
  • The fill happens exactly at the displayed price
  • Brokerage and exchange costs are zero
  • Large orders do not move the market

Those assumptions can make weak strategies look strong.

Add realism gradually

A simple model can include:

buyFill  = signalPrice + slippage
sellFill = signalPrice - slippage
netPnL   = grossPnL - commissions - fees

For fast C++ systems, this matters because a high-frequency strategy may produce many small trades. If each trade has a tiny expected edge, fees and slippage can completely erase it.

Costs And Slippage

Fill The Metric

Fill in the blank:

The worst peak-to-trough decline in an equity curve is called maximum ________.

Out Of Sample

Do not optimize on the answer key

After the previous lesson, you know how tempting it is to tune parameters for speed and performance. In strategy analysis, there is another danger: tuning parameters until the backtest looks perfect.

That is called overfitting.

A simple defense is splitting historical data:

Training / development period  → choose strategy rules
Validation period              → compare parameter choices
Out-of-sample period           → final reality check

If a strategy only works on the exact dates used for tuning, it may not have a real edge. Strong strategies usually behave reasonably across multiple market conditions, not just one hand-picked window.

Out Of Sample

Analysis Checklist

Before trusting a backtest

Use this checklist when analyzing a strategy:

  • No look-ahead bias: decisions use only available historical data
  • Clear execution rule: define when and at what simulated price orders fill
  • Costs included: commissions, fees, spread, and slippage assumptions
  • Trade log inspected: verify buys and sells match your intent
  • Equity curve reviewed: look for unstable jumps or long painful declines
  • Multiple metrics checked: return, drawdown, Sharpe, trade count
  • Out-of-sample tested: avoid tuning only to the past

Backtesting is where your C++ trading application becomes more than fast code. It becomes a system you can question, measure, and improve.

Next, we will add stronger guardrails around capital, operational safety, and security.

Analysis Checklist

Open interactive view →