VVibeTrading
Back to Blog
Also available in中文
TutorialsJuly 13, 202618 min read

Backtesting Basics with backtrader: Validate Before Adding AI

Learn how to backtest a simple SMA crossover strategy in backtrader, plot equity curves, and run walk-forward validation before layering on machine learning.

#backtrader#backtesting#python#basics
Risk Disclaimer: This content is for educational purposes only. Trading involves significant risk of loss. Past performance does not guarantee future results. Always do your own research before using any trading tool or strategy.

Every trader has watched a chart and thought, "If I had just bought here and sold there, I would be rich." That hindsight is seductive, and it is the reason backtesting exists. Backtesting lets you replace wishful thinking with a structured simulation. Instead of trusting a hunch, you run your rules against historical price data and see what would have happened, trade by trade, day by day.

This matters even more when you start thinking about artificial intelligence. Machine learning can spot patterns that humans miss, but it can also memorize noise, chase random correlations, and produce models that look brilliant on paper and collapse in live trading. Before you add neural networks, gradient boosters, or large language models to your workflow, you need a solid baseline: a simple, rule-based strategy tested honestly on clean historical data.

In this tutorial, we will use backtrader, a popular open-source Python backtesting engine, to build and evaluate a simple moving-average crossover strategy. We will install the library, load market data, write the strategy code, plot the equity curve, and introduce walk-forward validation. Along the way, we will highlight the common mistakes that turn a beautiful backtest into a live-trading disaster.

Why Backtest at All?

Backtesting is not about proving that a strategy will make money. It is about disqualifying strategies that are unlikely to survive contact with reality. A good backtest answers several questions before you risk capital:

QuestionWhat It Reveals
Does the signal produce trades?A strategy that barely trades is hard to evaluate and may not fit your broker or market.
How deep are the drawdowns?A 50% drawdown can force you to stop trading before the strategy recovers.
What is the win rate and payoff?High win rate with small losses is different from low win rate with large winners.
Is the result driven by a few outliers?One lucky trade can make a terrible strategy look great.
How sensitive is it to costs?Commissions, slippage, and spreads can erase paper profits quickly.

A backtest also gives you a language for talking about risk. Return alone is meaningless without drawdown, volatility, and exposure. When you later add an AI layer, you can compare the AI version against the simple rule-based version on the same data, with the same metrics. If the AI cannot beat a couple of moving averages after costs, you know the model is not adding value.

A positive backtest does not predict future profits. It only shows that the rules would have worked in the past. Markets change, regimes shift, and every edge decays.

Backtesting also protects you from yourself. It forces you to define exact entry, exit, and sizing rules. Vague plans like "buy the dip" become precise conditions: buy when the 20-day simple moving average crosses above the 50-day simple moving average, invest 95% of available cash, and sell when the cross reverses. That precision is the foundation of reproducible research.

What Is backtrader?

backtrader is an event-driven backtesting library written in Python. It was created by Daniel Rodriguez and gained a large following because it is flexible, object-oriented, and close to the mental model most traders use. You create a Cerebro engine, attach one or more data feeds, add one or more strategies, and run the simulation. The engine steps through each bar, calls your strategy logic, executes simulated orders, and tracks performance.

Key concepts in backtrader:

ConceptRole
CerebroThe engine that orchestrates data, strategy, broker, and analyzers.
Data feedA source of OHLCV bars, usually from a CSV or a library like yfinance.
StrategyA Python class where you define indicators, signals, and order logic.
IndicatorA computed time series, such as a moving average or RSI.
BrokerSimulated cash, position size, commissions, and order execution.
AnalyzerA performance reporter, such as Sharpe ratio or maximum drawdown.

backtrader is not the newest backtesting framework on the market, and the original repository is not under heavy development. However, it is stable, well-documented, and extensive enough to teach the fundamentals that transfer to any engine. Once you understand event-driven backtesting in backtrader, moving to Zipline, QuantConnect, VectorBT, or a custom C++ engine is much easier.

Installing backtrader

Start with a clean Python environment. backtrader works with Python 3.7 and newer, but for the best experience use a recent 3.10 or 3.11 environment. If you do not have a virtual environment, create one:

python -m venv vbt-env
source vbt-env/bin/activate  # On Windows: vbt-env\Scripts\activate
pip install --upgrade pip

Then install the core packages for this tutorial:

pip install backtrader yfinance pandas matplotlib

Here is what each package does:

PackagePurpose
backtraderThe backtesting engine.
yfinanceDownloads historical end-of-day data from Yahoo Finance.
pandasHandles tabular data and is required by yfinance.
matplotlibRenders strategy and equity-curve plots.

If you plan to run many backtests, you may also want numpy and scipy for statistical analysis. backtrader itself does not require them, but they are useful for walk-forward optimization and performance measurement.

Use a virtual environment for every trading project. Mixing libraries in your global Python installation leads to version conflicts that are painful to debug.

Test the installation by opening a Python prompt and running:

import backtrader as bt
print(bt.__version__)

If you see a version number and no errors, you are ready to build your first strategy.

Coding a Simple SMA Crossover Strategy

The moving-average crossover is the "hello world" of systematic trading. It buys when a short-term trend crosses above a long-term trend and sells when it crosses back below. It is simple, easy to understand, and a surprisingly useful benchmark. Many machine-learning strategies fail to outperform a well-parameterized trend-following model, so never skip this baseline.

Create a file named sma_cross.py and add the following code:

import backtrader as bt
import yfinance as yf
 
 
class SmaCross(bt.Strategy):
    params = (
        ('fast', 20),
        ('slow', 50),
    )
 
    def __init__(self):
        # Keep a reference to the close price for logging
        self.dataclose = self.datas[0].close
 
        # Create the moving averages and crossover indicator
        self.fast_sma = bt.indicators.SMA(self.data.close, period=self.p.fast)
        self.slow_sma = bt.indicators.SMA(self.data.close, period=self.p.slow)
        self.crossover = bt.indicators.CrossOver(self.fast_sma, self.slow_sma)
 
        # Track pending orders so we do not issue multiple orders per bar
        self.order = None
 
    def log(self, txt, dt=None):
        dt = dt or self.datas[0].datetime.date(0)
        print(f'{dt.isoformat()} {txt}')
 
    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            return
 
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(f'BUY EXECUTED, {order.executed.size} @ {order.executed.price:.2f}')
            elif order.issell():
                self.log(f'SELL EXECUTED, {order.executed.size} @ {order.executed.price:.2f}')
 
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('ORDER CANCELED/MARGIN/REJECTED')
 
        # Reset the pending order tracker
        self.order = None
 
    def next(self):
        # Wait for an open order to complete before issuing a new one
        if self.order:
            return
 
        # No position: check for a buy signal
        if not self.position:
            if self.crossover > 0:
                self.log(f'BUY SIGNAL, {self.dataclose[0]:.2f}')
                self.order = self.buy()
        # In a position: check for a sell signal
        else:
            if self.crossover < 0:
                self.log(f'SELL SIGNAL, {self.dataclose[0]:.2f}')
                self.order = self.sell()
 
 
if __name__ == '__main__':
    cerebro = bt.Cerebro()
 
    # Download SPY data from Yahoo Finance
    df = yf.download('SPY', start='2018-01-01', end='2023-12-31', auto_adjust=True)
    data = bt.feeds.PandasData(dataname=df)
    cerebro.adddata(data)
 
    # Add the strategy
    cerebro.addstrategy(SmaCross)
 
    # Set starting cash and commission
    cerebro.broker.setcash(10000.0)
    cerebro.broker.setcommission(commission=0.001)  # 0.1% per trade
 
    # Invest 95% of available cash on each signal
    cerebro.addsizer(bt.sizers.PercentSizer, percents=95)
 
    # Attach analyzers
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe', riskfreerate=0.0)
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
    cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')
 
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
    results = cerebro.run()
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
 
    # Print analyzer results
    strat = results[0]
    print(f"Sharpe: {strat.analyzers.sharpe.get_analysis()['sharperatio']:.3f}")
    print(f"Max Drawdown: {strat.analyzers.drawdown.get_analysis()['max']['drawdown']:.2f}%")
 
    cerebro.plot()

Let us walk through the important parts of this script so you can adapt it later.

Strategy parameters

The params tuple defines fast and slow windows. By exposing them as parameters, you can later optimize them without rewriting the strategy. A 20/50 combination is a common starting point, but it is not sacred.

ParameterDefaultMeaning
fast20Short-term simple moving average in bars.
slow50Long-term simple moving average in bars.

Indicators

backtrader's bt.indicators.SMA handles the rolling calculation for you. CrossOver returns 1 when the fast line crosses above the slow line, -1 when it crosses below, and 0 otherwise. This makes the signal logic clean and readable.

Order management

In event-driven backtesting, the next() method runs once per bar. If you issue an order on bar t, it may not fill until bar t+1. The notify_order() callback tells you when an order is submitted, accepted, completed, or rejected. We store a pending order in self.order and refuse to issue new signals until the broker reports completion. Without this guard, you can accidentally pile up multiple orders on the same bar.

Broker and sizing

setcash gives the account starting capital. setcommission simulates a 0.1% round-trip cost, which is realistic for liquid US ETFs but may be too low for small-cap stocks or crypto. PercentSizer invests 95% of available cash on each buy signal. We leave 5% in cash to absorb small price movements and avoid margin issues.

Commission and slippage assumptions have a massive impact on results. A strategy that looks great with zero costs often becomes unprofitable once realistic costs are added.

Analyzers

We attach three analyzers: Sharpe ratio to measure risk-adjusted return, maximum drawdown to measure peak-to-trough loss, and trade analyzer to inspect individual trade statistics. These numbers are more important than the final equity value because they describe how you would have experienced the strategy while it was running.

Plotting the Equity Curve

When the script finishes, cerebro.plot() opens a matplotlib window showing the price chart, the moving averages, buy and sell markers, and a panel with the portfolio value over time. On a headless server, you may need to save the figure instead of displaying it:

import matplotlib.pyplot as plt
 
figs = cerebro.plot(style='candlestick')
plt.savefig('sma_cross_equity.png')

The equity curve tells a richer story than the final PnL. Look for these patterns:

PatternInterpretation
Smooth upward slopeThe strategy is capturing a persistent trend.
Long flat periodsThe strategy is out of the market, which may be healthy in choppy conditions.
Sharp vertical dropsA single trade or a few trades caused large losses; check position sizing.
Recovery after a deep dipHigh drawdown tests your ability to stick with the system.

A common mistake is to focus only on the ending number. A strategy that turns $10,000 into $18,000 but spends six months at $6,000 along the way is not the same as one that reaches $17,000 smoothly. The first path may cause you to abandon the strategy at the worst possible time.

When reviewing the plot, compare the equity curve against a buy-and-hold benchmark. If the strategy underperforms simply holding SPY, you need a strong reason to trade it. Maybe it reduces drawdown, or maybe it is just a complicated way to get mediocre returns. The chart will make that obvious.

Walk-Forward Validation

A single backtest on the entire dataset is useful, but it can be misleading. If you optimize the SMA windows on the same data you test, you are effectively peeking at the answer key. This is called overfitting, and it produces parameters that look perfect historically but fail in live trading.

Walk-forward analysis solves this by simulating the real process of research and deployment. You split the data into windows. For each window, you optimize the parameters on the in-sample portion, then test them on the out-of-sample portion. You move the window forward and repeat.

WindowIn-Sample (train)Out-of-Sample (test)Action
12018-01 to 2020-122021-01 to 2021-06Optimize, then test.
22018-07 to 2021-062021-07 to 2021-12Optimize, then test.
32019-01 to 2021-122022-01 to 2022-06Optimize, then test.
42019-07 to 2022-062022-07 to 2022-12Optimize, then test.

There are two main variants:

  1. Rolling walk-forward: The training window stays the same length and slides forward. Older data drops out. This is appropriate when you believe recent data is more relevant.
  2. Anchored walk-forward: The training window grows over time, starting from the earliest data. This uses all available history, but the model is retrained as new data arrives.

The pseudo-code below shows the rolling approach:

from itertools import product
 
windows = [
    ('2018-01-01', '2020-12-31', '2021-01-01', '2021-06-30'),
    ('2018-07-01', '2021-06-30', '2021-07-01', '2021-12-31'),
    ('2019-01-01', '2021-12-31', '2022-01-01', '2022-06-30'),
]
 
best_out_of_sample_results = []
 
for train_start, train_end, test_start, test_end in windows:
    best_sharpe = -1e9
    best_params = None
 
    # In-sample optimization
    for fast, slow in product(range(10, 31, 5), range(40, 91, 10)):
        if fast >= slow:
            continue
        df_train = yf.download('SPY', start=train_start, end=train_end, auto_adjust=True)
        # Run cerebro on df_train with (fast, slow) and record Sharpe
        # ...
        if sharpe > best_sharpe:
            best_sharpe = sharpe
            best_params = (fast, slow)
 
    # Out-of-sample test with best_params
    df_test = yf.download('SPY', start=test_start, end=test_end, auto_adjust=True)
    # Run cerebro on df_test with best_params
    # Append result to best_out_of_sample_results

Walk-forward analysis is slower than a single backtest, but it is the closest you can get to an honest out-of-sample test without waiting for live market data.

When you aggregate the out-of-sample results, you get a performance curve that reflects what the strategy would have done if you had optimized and deployed it repeatedly. If the out-of-sample performance is poor while the in-sample performance is great, the strategy is overfit. This is exactly the kind of trap that AI can amplify, because machine-learning models have far more parameters than two moving-average windows.

Common Backtesting Mistakes

Even a correctly installed backtest can lie to you if the design is sloppy. Here are the most common mistakes and how to avoid them.

MistakeWhy It HurtsHow to Avoid
Look-ahead biasUsing information that would not have been available at the decision bar.Calculate indicators with data up to the current bar only. Avoid future functions.
OverfittingOptimizing parameters until they fit random noise.Use walk-forward analysis and keep parameter grids small.
Ignoring costsCommissions, slippage, spreads, and borrow fees reduce returns.Model realistic costs and rerun the backtest.
Survivorship biasTesting only stocks that still exist today.Use point-in-time universes and delisted symbols.
Data snoopingReusing the same dataset for many experiments.Hold out a final validation set and limit tweaks.
Wrong time frameIntraday signals tested on daily bars, or vice versa.Match the strategy frequency to the data frequency.
OverconfidenceTreating a backtest as a trading license.Treat it as evidence, not proof, and start with paper trading.

Look-ahead bias is especially subtle. It can creep in when you use adjusted close prices without realizing that adjustments are applied retroactively, or when you rebalance a portfolio using the closing price you could not have known until after the market closed. In backtrader, decisions inside next() can only see data up to and including the current bar, which helps. Still, you must be careful with external data such as earnings calendars, analyst ratings, or macroeconomic releases.

Overfitting is the silent killer of AI trading projects. A model with hundreds of features can almost always find some pattern in historical data. The question is whether that pattern repeats. A simple way to guard against overfitting is to demand economic or behavioral rationale for every feature. If you cannot explain why a variable should predict returns, it is probably noise.

The best backtest in the world cannot predict a market regime change. Always keep position sizing conservative and never deploy capital you cannot afford to lose.

Frequently Asked Questions

Why should I backtest before adding AI to a trading strategy?

Backtesting shows how a strategy would have behaved on historical data. It exposes curve fitting, survivorship bias, and unrealistic assumptions before you spend time on machine learning.

Do I need to know Python to use backtrader?

Yes, basic Python is required. You write strategies as classes, feed data from CSVs or APIs, and run them with a few lines of code.

Can backtrader guarantee profitable results?

No. A backtest is a simulation. Past performance never guarantees future returns, and many backtests fail in live trading because of slippage, costs, and overfitting.

What is walk-forward analysis and why does it matter?

Walk-forward analysis repeatedly trains parameters on an in-sample window and tests them on the following out-of-sample window. It mimics real-life optimization better than a single static backtest.

What are the most common backtesting mistakes?

Look-ahead bias, overfitting to historical noise, ignoring transaction costs and slippage, survivorship bias, and data snooping are the most frequent errors.

Is backtrader still maintained?

The original backtrader repository is stable but not actively updated. The community continues to use and extend it, and it remains a solid teaching and research tool.

Bottom Line

Backtesting is the cheapest laboratory in trading. It lets you test ideas, measure risk, and discard bad strategies before they cost real money. In this tutorial, we installed backtrader, built a simple SMA crossover strategy, plotted the equity curve, and introduced walk-forward validation as a defense against overfitting.

The real lesson is humility. A backtest can tell you that a strategy did well in the past. It cannot tell you that the same conditions will continue. Use backtests to set realistic expectations, not to chase promises of easy profits. Once you have an honest, rule-based baseline, you can evaluate whether AI genuinely improves your edge or merely adds complexity.

If you want to go deeper, try modifying the strategy: add a volatility filter, test different markets, or introduce a stop-loss. Every change should be justified by logic and revalidated with walk-forward testing. Build the discipline now, and your future AI experiments will stand on solid ground.