Finance20 min read·

Backtrader Python: Complete Tutorial & First Strategy 2026

A hands-on tutorial for Backtrader - Python's most popular backtesting framework. Build your first strategy from scratch with complete code examples and best practices.

What Is Backtrader?

Backtrader is an open-source Python backtesting framework that lets you test trading strategies against historical data before risking real money. It's event-driven, supports multiple data feeds simultaneously, ships with over 120 built-in indicators, and can connect to live brokers for paper or real trading.

If you're getting started with algorithmic trading, you've probably already hit the question: "How do I actually test whether my strategy works?" You could write everything from scratch - load data, loop through bars, track positions, calculate performance - but that's weeks of work before you even start testing ideas. Backtrader handles all of that plumbing for you.

The library was created by Daniel Rodriguez and has been one of the most popular Python backtesting frameworks since its release. While development has slowed in recent years (the last major commit was in 2020), the codebase remains stable and the community is still active in 2026. For learning how backtesting works and prototyping strategies quickly, it's hard to beat.

What makes Backtrader different from simpler backtesting approaches is its event-driven architecture. Rather than vectorising your entire strategy across a dataframe (the way you might with pandas), Backtrader simulates the market bar by bar. Your strategy receives each new price bar as it would in live trading, makes decisions, and submits orders. This means your backtest logic translates almost directly to live trading code - there's no gap between "what I tested" and "what I deployed."

Here's what Backtrader gives you out of the box:

  • Multiple data feeds - test strategies that trade across several instruments simultaneously
  • Built-in indicators - SMA, EMA, RSI, MACD, Bollinger Bands, and over 100 more
  • Order types - market, limit, stop, stop-limit, and bracket orders
  • Position sizing - percentage of equity, fixed size, or custom sizers
  • Commission schemes - percentage-based, fixed per trade, or custom models
  • Performance analysers - Sharpe ratio, drawdown, trade statistics, and more
  • Plotting - built-in matplotlib integration for visualising results

Installing and Setting Up Backtrader

Installing Backtrader takes one command. You'll need Python 3.6 or later (Python 3.10+ works fine in 2026, though you may see deprecation warnings). It's best practice to use a virtual environment so Backtrader's dependencies don't interfere with your other projects.

# Create and activate a virtual environment python -m venv backtrader-env source backtrader-env/bin/activate # On Windows: backtrader-env\Scripts\activate # Install backtrader pip install backtrader # Optional but recommended: install matplotlib for plotting pip install matplotlib

Once installed, verify it works:

import backtrader as bt print(bt.__version__) # Should print something like 1.9.78.123

Project Structure

For anything beyond a quick experiment, organise your project like this:

backtrader-project/
    data/
        AAPL.csv
        MSFT.csv
    strategies/
        sma_crossover.py
        mean_reversion.py
    analysers/
        custom_metrics.py
    main.py
    requirements.txt

Your requirements.txt should pin versions:

backtrader==1.9.78.123
matplotlib>=3.5.0
pandas>=1.4.0

This structure keeps strategies modular and testable. You can swap data files, try different strategies, and compare results without everything living in one giant script.


Your First Backtrader Strategy: SMA Crossover

Let's build a complete strategy from scratch - a simple moving average (SMA) crossover. This is the "hello world" of algorithmic trading: buy when a short-term moving average crosses above a long-term one, and sell when it crosses below. It's not going to make you rich, but it teaches you how Backtrader works.

If you're new to trading strategies in general, the SMA crossover is a momentum-following approach. The idea is that when the short-term average of prices rises above the long-term average, the asset has upward momentum, and vice versa.

Step 1: Prepare Your Data

Backtrader can read CSV files directly. You'll need OHLCV data (Open, High, Low, Close, Volume) with dates. Yahoo Finance format works out of the box. Save a CSV file to your data/ folder - you can download historical data from Yahoo Finance, Alpha Vantage, or any provider you prefer.

Your CSV should look something like this:

Date,Open,High,Low,Close,Adj Close,Volume
2020-01-02,296.24,300.60,295.19,300.35,296.77,33870100
2020-01-03,297.15,300.58,296.50,297.43,293.88,36580700
...

Step 2: Define the Strategy

Every Backtrader strategy is a class that inherits from bt.Strategy. You define your logic by overriding specific methods. Here's the complete SMA crossover:

import backtrader as bt class SmaCrossover(bt.Strategy): params = ( ("fast_period", 10), ("slow_period", 30), ) def __init__(self): self.fast_sma = bt.indicators.SimpleMovingAverage( self.data.close, period=self.params.fast_period ) self.slow_sma = bt.indicators.SimpleMovingAverage( self.data.close, period=self.params.slow_period ) self.crossover = bt.indicators.CrossOver(self.fast_sma, self.slow_sma) self.order = None def next(self): if self.order: return if not self.position: if self.crossover > 0: self.order = self.buy() else: if self.crossover < 0: self.order = self.sell() def notify_order(self, order): if order.status in [order.Completed]: if order.isbuy(): print( f"BUY executed at {order.executed.price:.2f}, " f"cost: {order.executed.value:.2f}, " f"commission: {order.executed.comm:.2f}" ) else: print( f"SELL executed at {order.executed.price:.2f}, " f"cost: {order.executed.value:.2f}, " f"commission: {order.executed.comm:.2f}" ) elif order.status in [order.Canceled, order.Margin, order.Rejected]: print("Order cancelled/margin/rejected") self.order = None

Let's break this down:

  • params - a tuple of tuples defining strategy parameters. This makes them easy to optimise later.
  • __init__ - runs once at the start. You declare your indicators here. Backtrader calculates them automatically on each bar.
  • next - runs on every new bar. This is where your trading logic lives. It only fires after your indicators have enough data (after 30 bars, in this case, because the slow SMA needs 30 periods).
  • notify_order - called whenever an order's status changes. We use it to reset the self.order flag and log executions.
  • self.crossover - Backtrader's built-in CrossOver indicator returns +1 when the fast line crosses above the slow line, and -1 when it crosses below.

Step 3: Run the Backtest

Now wire everything together in your main.py:

import backtrader as bt from strategies.sma_crossover import SmaCrossover def run_backtest(): cerebro = bt.Cerebro() cerebro.addstrategy(SmaCrossover) data = bt.feeds.YahooFinanceCSVData( dataname="data/AAPL.csv", fromdate=datetime.datetime(2020, 1, 1), todate=datetime.datetime(2025, 12, 31), ) cerebro.adddata(data) cerebro.broker.setcash(100000.0) cerebro.broker.setcommission(commission=0.001) print(f"Starting portfolio value: {cerebro.broker.getvalue():.2f}") cerebro.run() print(f"Final portfolio value: {cerebro.broker.getvalue():.2f}") cerebro.plot() if __name__ == "__main__": import datetime run_backtest()

The Cerebro engine is Backtrader's central orchestrator. You add strategies, data feeds, and configuration to it, then call run(). It handles synchronising the data, calling your strategy's next() method on each bar, processing orders, and tracking your portfolio.

Running this will print each trade as it executes and finish with your final portfolio value. The cerebro.plot() call opens a matplotlib chart showing price, indicators, and buy/sell markers.


Understanding Backtrader's Architecture

Backtrader's architecture has six core components. Understanding how they fit together saves you hours of confusion when you start building more complex strategies.

ComponentRoleExample
CerebroCentral engine that orchestrates everythingbt.Cerebro()
StrategyYour trading logicclass MyStrategy(bt.Strategy)
Data FeedProvides OHLCV bars to the strategybt.feeds.YahooFinanceCSVData
IndicatorComputes derived values from databt.indicators.RSI
ObserverTracks portfolio metrics during the runbt.observers.DrawDown
AnalyserComputes performance statistics after the runbt.analyzers.SharpeRatio

Cerebro

Cerebro (Spanish for "brain") is the main engine. It manages the event loop, synchronises multiple data feeds, and routes orders between your strategy and the broker simulator. You configure everything through Cerebro before calling run().

Data Feeds

Backtrader supports multiple data feed formats out of the box:

  • Yahoo Finance CSV - bt.feeds.YahooFinanceCSVData
  • Generic CSV - bt.feeds.GenericCSVData (fully configurable column mapping)
  • Pandas DataFrame - bt.feeds.PandasData
  • Interactive Brokers - bt.stores.IBStore (for live trading)

You can also create custom data feeds by subclassing bt.feeds.DataBase. This is useful when your data has non-standard columns or comes from a database.

Here's how to load data from a pandas DataFrame - useful if you're already doing data analysis with pandas:

import pandas as pd import backtrader as bt df = pd.read_csv("data/AAPL.csv", parse_dates=["Date"], index_col="Date") data = bt.feeds.PandasData( dataname=df, fromdate=datetime.datetime(2022, 1, 1), todate=datetime.datetime(2025, 12, 31), )

The Strategy Lifecycle

When Cerebro runs, your strategy goes through a specific lifecycle:

  1. __init__ - Declare indicators and variables. Indicators declared here are automatically computed.
  2. prenext - Called on bars where indicators don't have enough data yet. By default, it does nothing.
  3. nextstart - Called exactly once on the first bar where all indicators are ready. By default, it calls next().
  4. next - Called on every subsequent bar. This is where your trading logic goes.
  5. stop - Called once after the last bar. Use it for final calculations or cleanup.

Understanding this lifecycle matters because a common mistake is wondering why next() doesn't fire on the first 30 bars when you're using a 30-period SMA. Backtrader is waiting for the indicator to have enough data - that's by design.


Adding Indicators

Backtrader ships with over 120 built-in indicators. Here's how to use the most common ones and how to build your own.

Built-in Indicators

class MultiIndicatorStrategy(bt.Strategy): def __init__(self): # Simple Moving Average self.sma = bt.indicators.SimpleMovingAverage(self.data.close, period=20) # Relative Strength Index self.rsi = bt.indicators.RelativeStrengthIndex(self.data.close, period=14) # MACD self.macd = bt.indicators.MACD(self.data.close) self.macd_signal = self.macd.signal self.macd_histogram = self.macd.macd - self.macd.signal # Bollinger Bands self.bbands = bt.indicators.BollingerBands(self.data.close, period=20) # Average True Range self.atr = bt.indicators.AverageTrueRange(self.data, period=14) # Exponential Moving Average self.ema = bt.indicators.ExponentialMovingAverage(self.data.close, period=12) def next(self): # Access indicator values on the current bar current_rsi = self.rsi[0] previous_rsi = self.rsi[-1] # Previous bar's RSI if current_rsi < 30 and not self.position: self.buy() elif current_rsi > 70 and self.position: self.sell()

A few things to note about indicators in Backtrader:

  • Indexing - self.rsi[0] gives the current value, self.rsi[-1] gives the previous bar's value, self.rsi[-2] the one before that. Positive indices look into the future and will raise errors (which is exactly the behaviour you want to avoid look-ahead bias).
  • Automatic calculation - indicators declared in __init__ are computed automatically before next() is called. You never need to manually update them.
  • Minimum period - Backtrader tracks the minimum number of bars needed for all your indicators and won't call next() until that requirement is met.

Creating Custom Indicators

When the built-ins aren't enough, you can create your own. Here's a custom indicator that computes the percentage distance from a moving average:

class DistanceFromMA(bt.Indicator): lines = ("distance",) params = ( ("period", 20), ) def __init__(self): ma = bt.indicators.SimpleMovingAverage(self.data, period=self.params.period) self.lines.distance = (self.data - ma) / ma * 100 class DistanceStrategy(bt.Strategy): def __init__(self): self.dist = DistanceFromMA(self.data.close, period=20) def next(self): if self.dist[0] < -5.0 and not self.position: self.buy() elif self.dist[0] > 5.0 and self.position: self.sell()

Custom indicators use the lines declaration to define their output values. You compute them in __init__ using Backtrader's built-in arithmetic operations, which are automatically applied bar-by-bar. This declarative approach means Backtrader can optimise the calculations and handle the minimum period logic for you.


Analysing Results

Running a backtest without measuring performance is like running an experiment without recording the results. Backtrader's built-in analysers give you the statistics you need to evaluate whether a strategy is actually worth trading.

Adding Analysers

import backtrader as bt import datetime def run_with_analysers(): cerebro = bt.Cerebro() cerebro.addstrategy(SmaCrossover) data = bt.feeds.YahooFinanceCSVData( dataname="data/AAPL.csv", fromdate=datetime.datetime(2020, 1, 1), todate=datetime.datetime(2025, 12, 31), ) cerebro.adddata(data) cerebro.broker.setcash(100000.0) cerebro.broker.setcommission(commission=0.001) # Add performance analysers cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe", riskfreerate=0.04) cerebro.addanalyzer(bt.analyzers.DrawDown, _name="drawdown") cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="trades") cerebro.addanalyzer(bt.analyzers.Returns, _name="returns") cerebro.addanalyzer(bt.analyzers.SQN, _name="sqn") results = cerebro.run() strategy = results[0] # Extract and display metrics sharpe = strategy.analyzers.sharpe.get_analysis() drawdown = strategy.analyzers.drawdown.get_analysis() trades = strategy.analyzers.trades.get_analysis() returns = strategy.analyzers.returns.get_analysis() print("=" * 50) print("PERFORMANCE SUMMARY") print("=" * 50) print(f"Sharpe Ratio: {sharpe.get('sharperatio', 'N/A')}") print(f"Max Drawdown: {drawdown.max.drawdown:.2f}%") print(f"Max Drawdown $: {drawdown.max.moneydown:.2f}") print(f"Total Return: {returns.get('rtot', 0) * 100:.2f}%") print(f"Total Trades: {trades.total.total}") print(f"Won: {trades.won.total}") print(f"Lost: {trades.lost.total}") if trades.won.total > 0: win_rate = trades.won.total / trades.total.total * 100 print(f"Win Rate: {win_rate:.1f}%") if __name__ == "__main__": run_with_analysers()

Key Metrics to Track

MetricWhat It Tells YouGood Benchmark
Sharpe RatioRisk-adjusted return (excess return per unit of volatility)> 1.0 is decent, > 2.0 is strong
Max DrawdownLargest peak-to-trough decline< 20% for most strategies
Win RatePercentage of profitable tradesDepends on profit factor
Profit FactorGross profit / Gross loss> 1.5 is respectable
SQNSystem Quality Number (Van Tharp metric)> 2.0 is tradeable
Total ReturnOverall percentage gain/lossMust beat benchmark

A common trap is focusing only on total return. A strategy that returns 40% with a 60% drawdown is far worse than one that returns 20% with a 10% drawdown. The Sharpe ratio and max drawdown together give you a much clearer picture of whether a strategy is actually viable. For more on building strategies that account for risk, see our quantitative trading strategies guide.


A More Advanced Strategy: RSI Mean Reversion with Position Sizing

Now let's build something more realistic - a mean reversion strategy that uses RSI, incorporates ATR-based position sizing, and includes stop losses. This is closer to what you'd actually test in a real workflow.

The idea: when RSI drops below 30, the asset is oversold and likely to revert upward. We buy with a position size scaled by the Average True Range (so we take smaller positions in volatile markets) and place a stop loss 2x ATR below our entry.

import backtrader as bt class RsiMeanReversion(bt.Strategy): params = ( ("rsi_period", 14), ("rsi_oversold", 30), ("rsi_overbought", 70), ("atr_period", 14), ("atr_stop_mult", 2.0), ("risk_per_trade", 0.02), ) def __init__(self): self.rsi = bt.indicators.RelativeStrengthIndex( self.data.close, period=self.params.rsi_period ) self.atr = bt.indicators.AverageTrueRange( self.data, period=self.params.atr_period ) self.sma_200 = bt.indicators.SimpleMovingAverage( self.data.close, period=200 ) self.order = None self.stop_order = None self.entry_price = None def calculate_position_size(self): risk_amount = self.broker.getvalue() * self.params.risk_per_trade stop_distance = self.atr[0] * self.params.atr_stop_mult if stop_distance <= 0: return 0 size = int(risk_amount / stop_distance) return max(size, 1) def next(self): if self.order: return if not self.position: # Only buy if price is above 200-day SMA (trend filter) if self.data.close[0] > self.sma_200[0]: if self.rsi[0] < self.params.rsi_oversold: size = self.calculate_position_size() self.order = self.buy(size=size) self.entry_price = self.data.close[0] else: # Exit on RSI overbought if self.rsi[0] > self.params.rsi_overbought: self.close() def notify_order(self, order): if order.status in [order.Completed]: if order.isbuy(): # Place stop loss after entry stop_price = order.executed.price - ( self.atr[0] * self.params.atr_stop_mult ) self.stop_order = self.sell( exectype=bt.Order.Stop, price=stop_price ) elif order.status in [order.Canceled, order.Margin, order.Rejected]: pass if order.status in [order.Completed, order.Canceled, order.Margin, order.Rejected]: self.order = None def notify_trade(self, trade): if trade.isclosed: print( f"Trade PnL: Gross={trade.pnl:.2f}, Net={trade.pnlcomm:.2f}" ) def stop(self): print( f"RSI({self.params.rsi_period}) " f"Oversold={self.params.rsi_oversold} " f"Overbought={self.params.rsi_overbought} " f"Final Value: {self.broker.getvalue():.2f}" )

This strategy introduces several concepts beyond the basic SMA crossover:

  • ATR-based position sizing - instead of buying a fixed number of shares, we calculate how many shares to buy based on the current volatility. In volatile markets, we buy fewer shares. This keeps our risk consistent across different market conditions.
  • Trend filter - we only take mean-reversion longs when the price is above the 200-day SMA. This avoids catching falling knives in bear markets.
  • Stop loss orders - after each entry, we immediately place a stop loss order at 2x ATR below our entry price. If the trade goes against us, we're out automatically.
  • Risk per trade - we risk only 2% of our portfolio on each trade. This is a standard risk management approach.

Running with Parameter Optimisation

One of Backtrader's strengths is built-in parameter optimisation. Instead of testing one set of parameters, you can test many combinations:

def run_optimisation(): cerebro = bt.Cerebro() cerebro.optstrategy( RsiMeanReversion, rsi_period=[10, 14, 21], rsi_oversold=[25, 30, 35], rsi_overbought=[65, 70, 75], ) data = bt.feeds.YahooFinanceCSVData( dataname="data/AAPL.csv", fromdate=datetime.datetime(2020, 1, 1), todate=datetime.datetime(2025, 12, 31), ) cerebro.adddata(data) cerebro.broker.setcash(100000.0) cerebro.broker.setcommission(commission=0.001) cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe") results = cerebro.run(maxcpus=1) # Find the best parameters best_sharpe = float("-inf") best_params = None for run in results: for strategy in run: sharpe = strategy.analyzers.sharpe.get_analysis() ratio = sharpe.get("sharperatio", None) if ratio is not None and ratio > best_sharpe: best_sharpe = ratio best_params = strategy.params if best_params: print(f"Best Sharpe: {best_sharpe:.3f}") print(f"RSI Period: {best_params.rsi_period}") print(f"Oversold: {best_params.rsi_oversold}") print(f"Overbought: {best_params.rsi_overbought}")

A word of caution: optimisation is useful for understanding how sensitive your strategy is to parameters, but it's dangerously easy to overfit. If your strategy only works with RSI period 13 and oversold threshold 28, it's probably curve-fitted to noise. Good strategies should work across a range of reasonable parameters.


Backtrader vs Other Python Backtesting Frameworks

Backtrader isn't the only option in 2026. Here's how it compares to the other major Python backtesting frameworks available.

FeatureBacktraderZiplinevectorbtQuantConnect (Lean)
ArchitectureEvent-drivenEvent-drivenVectorisedEvent-driven
SpeedModerateModerateVery fastModerate
Learning curveModerateSteepGentleSteep
Built-in indicators120+Limited30+ (via TA-Lib)100+
Live tradingYes (IB, Oanda)NoNoYes (many brokers)
Multi-assetYesYesYesYes
LanguagePythonPythonPythonC# / Python
Active developmentMinimalMinimalActiveActive
DocumentationGoodFairGoodExcellent
CommunityLargeLargeGrowingLarge
Free/Open sourceYesYesYesYes (cloud paid)

Backtrader vs Zipline

Zipline was originally built by Quantopian and became the de facto standard for a while. It has a clean API and solid performance, but it's tightly coupled to US equity data and the Quantopian data bundle format. Since Quantopian shut down in 2020, maintenance has been community-driven and sporadic. Backtrader is more flexible with data sources and easier to get running.

Backtrader vs vectorbt

vectorbt takes a fundamentally different approach - it's vectorised, meaning it operates on entire arrays at once rather than bar-by-bar. This makes it dramatically faster for simple strategies and parameter sweeps. If you need to test 10,000 parameter combinations across 20 years of data, vectorbt will finish in seconds where Backtrader might take hours. The trade-off is that complex strategies with conditional logic, position sizing rules, and stop losses are harder to express in a vectorised framework. vectorbt is best for rapid prototyping and exploration; Backtrader is better for realistic simulation of strategies you'd actually trade.

Backtrader vs QuantConnect (Lean)

QuantConnect's Lean engine is the most fully-featured option. It supports multiple languages, has extensive data libraries, handles live trading across many brokers, and has a cloud platform for running backtests. The downside is complexity - there's a steep learning curve, and you're somewhat locked into their ecosystem. Backtrader is simpler, lighter, and better for learning. If you're building production trading systems, QuantConnect is worth the investment. If you're learning or prototyping, Backtrader gets you there faster.

When to Choose Backtrader

Pick Backtrader when:

  • You're learning how backtesting works and want a framework that's straightforward to understand
  • You need event-driven simulation where your backtest mirrors how live trading actually works
  • You want to prototype strategies quickly without a lot of boilerplate
  • You need built-in indicators and don't want to implement everything from scratch

Pick something else when:

  • You need maximum speed for large-scale parameter optimisation (use vectorbt)
  • You need a production-grade system with multi-broker live trading (use QuantConnect)
  • You're working exclusively with pandas DataFrames and want a vectorised approach (use vectorbt)

Common Mistakes and Best Practices

Building backtests that produce reliable results is harder than it looks. Here are the mistakes that catch almost everyone, and how to avoid them.

Look-Ahead Bias

Look-ahead bias happens when your strategy uses information that wouldn't have been available at the time of the trade. It's the most common and most dangerous backtesting error.

# BAD - look-ahead bias! Using tomorrow's close to make today's decision def next(self): if self.data.close[1] > self.data.close[0]: # [1] is TOMORROW self.buy() # GOOD - only using current and past data def next(self): if self.data.close[0] > self.data.close[-1]: # [-1] is yesterday self.buy()

In Backtrader, positive indices look into the future and negative indices look into the past. This is actually helpful because Backtrader will raise an error if you try to access future data (in most configurations). But look-ahead bias can also creep in through your data - for example, using adjusted closing prices that were adjusted using future information, or using a stock universe that only includes companies that survived to the present day (survivorship bias).

Survivorship Bias

If you test your strategy on today's S&P 500 constituents, you're only testing on companies that survived. The ones that went bankrupt, were delisted, or were acquired aren't in your dataset. This systematically inflates your returns because you're excluding the worst performers.

The fix: use point-in-time data that reflects which stocks were actually in the index on each historical date. This is harder to get and more expensive, but it's essential for any serious research.

Ignoring Transaction Costs

A strategy that trades 50 times a day looks great with zero commission. Add realistic transaction costs and slippage, and it might be deeply unprofitable.

# Always set realistic commission and slippage cerebro.broker.setcommission(commission=0.001) # 0.1% per trade cerebro.broker.set_slippage_perc(perc=0.001) # 0.1% slippage

Slippage models the fact that you rarely get the exact price you see. When you send a market order, the price moves slightly before your order is filled - especially for larger orders or less liquid instruments. Backtrader's built-in slippage model is basic but sufficient for initial testing.

Overfitting

If you optimise your strategy across 50 parameters using 5 years of data, you'll almost certainly find a parameter set that looks amazing - on that specific data. It'll likely perform terribly on new data.

Best practices to combat overfitting:

  • Out-of-sample testing - split your data into in-sample (for development) and out-of-sample (for validation). Never touch the out-of-sample data until you're ready for a final test.
  • Walk-forward analysis - repeatedly optimise on a rolling window and test on the next period.
  • Keep it simple - strategies with fewer parameters are less likely to be overfitted. If your strategy needs 10 parameters to work, it's probably fragile.
  • Economic rationale - every strategy should have a reason for why it works that goes beyond "the numbers look good." If you can't explain the edge in plain English, it probably doesn't exist.

Not Accounting for Market Impact

For small accounts, this doesn't matter. But if you're testing a strategy that trades large volumes relative to the market's liquidity, your own orders would move the price. Backtrader doesn't model market impact by default. For strategies that trade frequently in less liquid instruments, you need to be aware of this limitation.

Complete Best Practices Checklist

PracticeWhy It Matters
Set realistic commissionsPrevents false profitability signals
Add slippage modellingAccounts for execution reality
Use out-of-sample dataDetects overfitting
Check for look-ahead biasEnsures results are achievable
Use point-in-time dataEliminates survivorship bias
Test across multiple time periodsVerifies strategy stability
Start with a simple baselineGives you something to beat
Log every tradeHelps debug unexpected behaviour
Use prenext for debuggingTracks what happens before indicators are ready
Keep parameters fewReduces overfitting risk

For a broader view of how to approach building and testing strategies properly, our statistics for quantitative trading guide covers the statistical foundations you need to get right.


Frequently Asked Questions

Is Backtrader still maintained in 2026?

Backtrader's last major update was in 2020, and the project's GitHub repository has limited new activity. However, the library is stable and widely used. It doesn't crash or produce incorrect results - it simply isn't receiving new features. The community remains active through forums and Stack Overflow. For learning and prototyping, Backtrader is perfectly fine in 2026. For production trading systems that need ongoing support and new broker integrations, you might want to consider QuantConnect's Lean engine or build on top of a more actively maintained framework. Many professional quants still use Backtrader as a rapid prototyping tool even if their production systems use something else.

Can Backtrader do live trading?

Yes. Backtrader supports live trading through broker stores, most notably Interactive Brokers via the IBStore class. You can also connect to Oanda for forex trading. The process involves replacing the simulated broker with a live broker connection, switching from historical data feeds to live data feeds, and keeping the rest of your strategy code largely unchanged. That said, live trading with Backtrader requires careful testing in paper trading mode first. The transition from backtest to live isn't quite as smooth as the documentation suggests - you'll need to handle connection drops, partial fills, and other real-world complications that don't exist in simulation.

How fast is Backtrader compared to other frameworks?

For single-strategy backtests on a few years of daily data, Backtrader is fast enough that speed isn't a concern. A typical backtest over 5 years of daily data finishes in under a second. The speed issue appears when you're running optimisation across many parameter combinations or testing on tick-level data. vectorbt is roughly 100-1000x faster for vectorisable strategies because it uses NumPy array operations rather than Python loops. If speed is your primary concern for large-scale parameter sweeps, vectorbt or a custom solution will serve you better. If you need realistic event-driven simulation where speed is secondary, Backtrader is fine.

How do I add multiple data feeds for pairs trading?

Adding multiple data feeds is straightforward. You add each feed to Cerebro separately and access them by index in your strategy:

cerebro = bt.Cerebro() data0 = bt.feeds.YahooFinanceCSVData(dataname="data/AAPL.csv") data1 = bt.feeds.YahooFinanceCSVData(dataname="data/MSFT.csv") cerebro.adddata(data0, name="AAPL") cerebro.adddata(data1, name="MSFT") # In your strategy: class PairsStrategy(bt.Strategy): def next(self): aapl_close = self.datas[0].close[0] msft_close = self.datas[1].close[0] spread = aapl_close - msft_close

When using multiple data feeds, Backtrader automatically synchronises them by date. If one instrument has data for a day that the other doesn't, Backtrader handles the alignment for you.

What Python version should I use with Backtrader?

Backtrader was written for Python 3.2+ and works with Python versions up to 3.10 without issues. Python 3.11 and 3.12 introduced some changes that generate deprecation warnings (related to datetime.utcnow() and some internal usage of removed features), but the library still runs correctly. Python 3.13+ may require patching a few lines. The safest bet in 2026 is Python 3.10, which is stable and fully compatible. If you're using a newer Python version and hit issues, check the Backtrader community forums - patches are usually available within days of a new Python release.

Should I learn Backtrader or go straight to a more modern framework?

If you're learning algorithmic trading for the first time, Backtrader is an excellent starting point. Its event-driven architecture teaches you how real backtesting works - concepts like order management, position tracking, and the difference between a signal and an execution. These concepts transfer directly to any other framework. Once you're comfortable with Backtrader, you'll understand what every backtesting framework is trying to do under the hood, and you'll be better equipped to choose (or build) the right tool for your specific needs. Think of Backtrader as learning to drive in a manual car - it teaches you fundamentals that automatic gearboxes hide from you.

Want to go deeper on Backtrader Python: Complete Tutorial & First Strategy 2026?

This article covers the essentials, but there's a lot more to learn. Inside Quantt, you'll find hands-on coding exercises, interactive quizzes, and structured lessons that take you from fundamentals to production-ready skills — across 50+ courses in technology, finance, and mathematics.

Free to get started · No credit card required