Master algorithmic trading with this comprehensive backtrader guide. From basic setup to advanced optimization techniques, learn everything you need to build, test, and deploy profitable trading strategies in Python.

This comprehensive cheatsheet covers everything you need to know about backtrader, the powerful Python framework for algorithmic trading. From basic setup to advanced optimization techniques, you'll learn how to build, test, and deploy profitable trading strategies with confidence.
Educational Disclaimer: This content is for educational purposes only and does not constitute investment advice. Trading involves substantial risk and may not be suitable for all investors. Past performance does not guarantee future results.
backtrader is a feature-rich, open-source Python framework designed for backtesting, optimizing, and deploying algorithmic trading strategies. Its fundamental purpose is to allow developers and quantitative analysts to focus on crafting and refining reusable trading logic, indicators, and performance analyzers, rather than expending resources on building the underlying infrastructure from scratch. The platform is self-contained, written in pure Python, and supports a wide array of functionalities, from handling multiple data feeds to simulating complex order types and connecting to live brokers.
The architecture is built upon distinct components:
The central engine that orchestrates the entire process.
Conduits for market data (CSV, Yahoo Finance, Pandas).
User-defined class containing the core trading logic.
Reusable technical analysis calculations (SMA, RSI, etc.).
Simulates a real-world brokerage, managing cash, positions, and costs.
Tools for performance evaluation (Sharpe Ratio, Drawdown).
Components for automated position sizing.
Setting up a functional backtrader environment is a straightforward process, requiring only Python and the pip package manager.
Installation
pip install backtrader
# For plotting capabilities
pip install backtrader[plotting]Anatomy of a Minimal Script
import backtrader as bt
import datetime
# 1. Create a Strategy class
class MyFirstStrategy(bt.Strategy):
def __init__(self):
self.dataclose = self.datas[0].close
def log(self, txt, dt=None):
dt = dt or self.datas[0].datetime.date(0)
print(f'{dt.isoformat()}, {txt}')
def next(self):
self.log(f'Close, {self.dataclose[0]:.2f}')
# 2. Instantiate the Cerebro engine
cerebro = bt.Cerebro()
# 3. Add the Strategy to Cerebro
cerebro.addstrategy(MyFirstStrategy)
# 4. Create and Add a Data Feed
data = bt.feeds.GenericCSVData(
dataname='your_data.csv', # Replace with your data file
fromdate=datetime.datetime(2000, 1, 1),
todate=datetime.datetime(2000, 12, 31),
dtformat=('%Y-%m-%d'),
openinterest=-1
)
cerebro.adddata(data)
# 5. Set the initial cash
cerebro.broker.setcash(100000.0)
# 6. Run the backtest
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())The Cerebro engine is the central controller. Its typical workflow involves configuring an instance through its various methods before calling run().
Adds a data feed.
Adds a strategy class.
Sets initial capital.
Configures trading costs.
Attaches a position sizing algorithm.
Adds a performance analyzer.
Initiates the backtest.
Generates a visual chart of the results.
Accessing data from lines within a strategy's next method follows a strict 0-based indexing convention to prevent look-ahead bias: self.data.close[0] for the current bar, self.data.close[-1] for the previous bar.
Loading a Pandas DataFrame
import pandas as pd
# Assume 'my_dataframe' is a Pandas DataFrame with a DatetimeIndex
# and columns named 'open', 'high', 'low', 'close', 'volume'
data = bt.feeds.PandasData(dataname=my_dataframe)The bt.Strategy class behavior is defined by a series of methods called by Cerebro at different points in the backtest lifecycle. Understanding these provides greater control.
Called once. Used to set up indicators and one-time configurations.
Called once at the very beginning of data processing.
Called for each bar during the indicator warm-up period.
Called once on the first bar after the warm-up period.
The primary workhorse. Called for every bar after warm-up for the main trading logic.
Called once at the end of the backtest for final calculations.
backtrader uses a notification system to communicate the status of asynchronous events, like order executions, back to the strategy.
Handling Order Notifications
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
# Buy/Sell order submitted/accepted to/by broker - Nothing to do
return
# Check if an order has been completed
if order.status in [order.Completed]:
if order.isbuy():
self.log(f'BUY EXECUTED, Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Comm: {order.executed.comm:.2f}')
elif order.issell():
self.log(f'SELL EXECUTED, Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Comm: {order.executed.comm:.2f}')
self.bar_executed = len(self)
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('Order Canceled/Margin/Rejected')
# Write down: no pending order
self.order = NoneHandling Trade Notifications
def notify_trade(self, trade):
if not trade.isclosed:
return
self.log(f'OPERATION PROFIT, GROSS {trade.pnl:.2f}, NET {trade.pnlcomm:.2f}')backtrader offers multiple ways to create orders, from direct calls to target-based allocation.
Creates market orders with a size determined by the active Sizer.
Adjusts the position to a target size of N shares.
Adjusts the position to a target monetary value V.
Adjusts the position to a target of P percent of portfolio value.
| Indicator Name | backtrader Class | Key Parameters |
|---|---|---|
| Simple Moving Average | bt.indicators.SimpleMovingAverage | period |
| Exponential Moving Average | bt.indicators.ExponentialMovingAverage | period |
| Moving Average Crossover | bt.indicators.CrossOver | line1, line2 |
| Relative Strength Index | bt.indicators.RSI | period |
| MACD | bt.indicators.MACD | period_me1, period_me2, period_signal |
| Bollinger Bands® | bt.indicators.BollingerBands | period, devfactor |
| Average True Range | bt.indicators.AverageTrueRange | period |
| Stochastic Oscillator | bt.indicators.Stochastic | period, period_dfast, period_dslow |
SmaCrossStrategy
class SmaCrossStrategy(bt.Strategy):
params = dict(pfast=10, pslow=30)
def __init__(self):
sma_fast = bt.indicators.SMA(period=self.p.pfast)
sma_slow = bt.indicators.SMA(period=self.p.pslow)
self.crossover = bt.indicators.CrossOver(sma_fast, sma_slow)
def next(self):
if not self.position:
if self.crossover > 0:
self.buy()
elif self.crossover < 0:
self.close()RsiStrategy
class RsiStrategy(bt.Strategy):
params = (("rsi_period", 14), ("rsi_overbought", 70), ("rsi_oversold", 30))
def __init__(self):
self.rsi = bt.indicators.RSI(self.data.close, period=self.params.rsi_period)
def next(self):
if not self.position and self.rsi < self.params.rsi_oversold:
self.buy()
elif self.position and self.rsi > self.params.rsi_overbought:
self.sell()While backtrader offers a rich library of built-in indicators, developers often need to implement proprietary or non-standard indicators. The process involves subclassing bt.Indicator and defining its lines, params, and calculation logic.
Custom Stochastic Indicator
import backtrader as bt
class CustomStochastic(bt.Indicator):
lines = ('k', 'd',) # Declare the output lines
params = (
('k_period', 14), # Lookback period for HighestHigh/LowestLow
('d_period', 3), # Smoothing period for the %D line
)
def __init__(self):
# Use built-in indicators for the components
highest = bt.indicators.Highest(self.data.high, period=self.p.k_period)
lowest = bt.indicators.Lowest(self.data.low, period=self.p.k_period)
# Calculate and assign the %K line
self.lines.k = 100 * (self.data.close - lowest) / (highest - lowest)
# Calculate and assign the %D line by smoothing %K
self.lines.d = bt.indicators.SimpleMovingAverage(self.lines.k, period=self.p.d_period)Analyzers are added to Cerebro before the run and produce a dictionary of results afterward. They are crucial for quantitatively evaluating strategy performance.
| Metric / Question | backtrader Analyzer | Key Output(s) |
|---|---|---|
| Risk-adjusted return? | bt.analyzers.SharpeRatio | sharperatio |
| Largest peak-to-trough loss? | bt.analyzers.DrawDown | max.drawdown (%) |
| Win rate and average P/L? | bt.analyzers.TradeAnalyzer | pnl.net.average |
| Annualized returns? | bt.analyzers.Returns | rnorm100 (annualized %) |
| System Quality Number? | bt.analyzers.SQN | sqn |
Accessing Analyzer Results
# 1. Add analyzers to Cerebro with unique names
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='mysharpe')
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='mytrade')
# 2. Run the backtest
results = cerebro.run()
strategy_instance = results[0]
# 3. Retrieve and print analysis from each analyzer
sharpe_analysis = strategy_instance.analyzers.mysharpe.get_analysis()
trade_analysis = strategy_instance.analyzers.mytrade.get_analysis()
print(f"Sharpe Ratio: {sharpe_analysis['sharperatio']}")
print(f"Total Trades: {trade_analysis.total.total}")
print(f"Winning Trades: {trade_analysis.won.total}")
print(f"Losing Trades: {trade_analysis.lost.total}")Use cerebro.optstrategy to test a strategy with various parameter combinations. This helps in finding the most robust parameter set but must be used carefully to avoid overfitting.
Optimization Example
cerebro.optstrategy(
SmaCrossStrategy,
pfast=range(10, 21, 5), # Test with pfast = 10, 15, 20
pslow=range(30, 51, 10) # Test with pslow = 30, 40, 50
)Processing Optimization Results
# Run the optimization
optimized_runs = cerebro.run()
final_results_list = []
for run in optimized_runs:
for strategy in run:
final_results_list.append({
'pfast': strategy.p.pfast,
'pslow': strategy.p.pslow,
'pnl': strategy.broker.getvalue()
})
# Sort the results by final portfolio value
by_pnl = sorted(final_results_list, key=lambda x: x['pnl'], reverse=True)
# Print the best result
print("Best performing parameters:")
print(by_pnl[0])A backtest that ignores transaction costs is fundamentally flawed. Use setcommission and set_slippage_perc to simulate real-world conditions.
Setting Costs
# Percentage-based commission for stocks (0.1%)
cerebro.broker.setcommission(commission=0.001)
# Fixed-based commission for futures
# cerebro.broker.setcommission(commission=2.0, mult=10.0, margin=2000.0)
# Percentage-based slippage (0.1%)
cerebro.broker.set_slippage_perc(perc=0.001, slip_open=True)Sizers decouple the position sizing decision from the signal generation logic. This allows for modular and reusable risk management components.
Trades a fixed number of shares/contracts.
Allocates a percentage of available cash.
Allocates almost all available cash.
Using PercentSizer
# Add a sizer to Cerebro to risk 20% of cash on each trade
cerebro.addsizer(bt.sizers.PercentSizer, percents=20)A significant advantage of backtrader's architecture is that the same strategy code can often be deployed for live trading by replacing the backtesting components with live equivalents.
Conceptual Live Trading Setup
# NOTE: This is a conceptual example and requires a running IB TWS/Gateway
# and the appropriate API libraries installed.
# 1. Create an IBStore instance with connection details
ibstore = bt.stores.IBStore(host='127.0.0.1', port=7497, clientId=10)
# 2. Get a live data feed from the store
data = ibstore.getdata(dataname='EUR.USD-CASH-IDEALPRO')
# 3. Get a live broker instance from the store
broker = ibstore.getbroker()
# 4. Configure Cerebro with live components
cerebro = bt.Cerebro(runonce=False) # Use runonce=False for live data
cerebro.adddata(data)
cerebro.setbroker(broker)
# 5. Add the SAME strategy used for backtesting
cerebro.addstrategy(MyStrategy)
# 6. Run Cerebro for live trading
cerebro.run()This cheatsheet provides the foundation for mastering backtrader. Start building your own algorithmic trading strategies and take your quantitative analysis to the next level.