Rajandran R Creator of OpenAlgo - OpenSource Algo Trading framework for Indian Traders. Building GenAI Applications. Telecom Engineer turned Full-time Derivative Trader. Mostly Trading Nifty, Banknifty, High Liquid Stock Derivatives. Trading the Markets Since 2006 onwards. Using Market Profile and Orderflow for more than a decade. Designed and published 100+ open source trading systems on various trading tools. Strongly believe that market understanding and robust trading frameworks are the key to the trading success. Building Algo Platforms, Writing about Markets, Trading System Design, Market Sentiment, Trading Softwares & Trading Nuances since 2007 onwards. Author of Marketcalls.in

Introduction to Backtrader – Creating your First Trading Strategy – Python Trading Tutorial

4 min read

Backtrader is a powerful Python library designed to facilitate the backtesting of trading strategies. It allows traders to test their strategies against historical market data to simulate how they would have performed in the past. Backtrader’s flexibility in handling various asset classes, integration with different data sources, and ability to simulate broker environments makes it a go-to tool for both retail and institutional traders.

Stock Professional Performing Backtesting

Used Python Version – Python 3.11.7

How to Install Backtrader?

open your command line and use the following command. Ensure that pip is already installed.

pip install backtrader
pip install yfinance

Backtrader library is used for backtesting purpose and yfinance python library to get the End of the day data from NSE

Why is Backtrader Powerful?

  1. Flexibility: You can create a variety of strategies, use multiple data feeds, and perform both live and backtesting.
  2. Built-in Indicators: Backtrader comes with numerous pre-built technical indicators such as moving averages, RSI, MACD, etc.
  3. Performance: The framework is optimized for large datasets and fast execution, even when running complex simulations.
  4. Broker Simulation: It simulates broker behavior, including commission, slippage, and capital management, which helps to accurately assess real-world trading performance.
  5. Data Integration: Backtrader supports multiple data sources like Yahoo Finance, Quandl, and others, making it easier to obtain historical data for testing.

Cerebro: The Engine of Backtrader

At the core of Backtrader is the Cerebro engine, responsible for managing data feeds, strategies, and orders. For traders, Cerebro is vital for backtesting as it:

  • Loads historical data feeds.
  • Executes strategies based on the loaded data.
  • Manages capital and order executions.
  • Simulates trading conditions, including commission and slippage.
  • Generates reports on performance metrics such as profit, drawdown, etc.

Without Cerebro, you wouldn’t be able to perform backtesting, as it essentially manages the entire trading environment.

Example: EMA Crossover Strategy with RELIANCE

In this example, we will implement a simple Exponential Moving Average (EMA) crossover strategy using two EMAs (10-day and 20-day). The strategy works as follows:

  • Buy when the 10-day EMA crosses above the 20-day EMA.
  • Sell when the 10-day EMA crosses below the 20-day EMA.

We will test this strategy using RELIANCE.NS data from Yahoo Finance, set an initial capital of ₹3,00,000, and apply a trading commission of 0.002%.

Python Code to Implement a Simple Ema Crossover Strategy with Buy/Sell Signals Plot.

import backtrader as bt
import datetime
import yfinance as yf
import matplotlib.pyplot as plt

# Fetch data using yfinance
data_df = yf.download('RELIANCE.NS', start='2020-01-01', end='2023-01-01')

# Create a custom pandas feed to use with Backtrader
class PandasData(bt.feeds.PandasData):
    params = (
        ('fromdate', datetime.datetime(2020, 1, 1)),
        ('todate', datetime.datetime(2023, 1, 1)),
        ('open', 'Open'),
        ('high', 'High'),
        ('low', 'Low'),
        ('close', 'Close'),
        ('volume', 'Volume'),
        ('openinterest', None),  # No open interest in Yahoo data
    )

# Define the EMA Crossover Strategy
class EmaCrossStrategy(bt.Strategy):
    params = (('short_period', 10), ('long_period', 20),)

    def __init__(self):
        # Initialize the 10-day and 20-day EMA indicators
        self.ema_short = bt.indicators.EMA(self.data.close, period=self.params.short_period)
        self.ema_long = bt.indicators.EMA(self.data.close, period=self.params.long_period)
        self.trade_list = []  # To keep track of trades

    def next(self):
        if not self.position:  # Check if we are not in a position
            if self.ema_short > self.ema_long:  # Buy if short EMA crosses above long EMA
                self.buy()
        else:
            if self.ema_short < self.ema_long:  # Sell if short EMA crosses below long EMA
                self.sell()

    def notify_trade(self, trade):
        if trade.isclosed:
            exit_price = None
            if trade.size != 0:
                exit_price = trade.price + trade.pnlcomm / trade.size  # Calculate exit price
            else:
                exit_price = trade.price  # Set exit price to entry price if size is zero (or handle as needed)

            # Log the trade details when a trade is closed
            trade_details = {
                'Entry Price': trade.price,  # Entry price of the trade
                'Exit Price': exit_price,  # Calculated exit price
                'Size': trade.size,  # Size of the trade
                'Profit/Loss': trade.pnlcomm  # Net PnL after commission
            }
            self.trade_list.append(trade_details)


    def notify_order(self, order):
        if order.status in [order.Completed]:
            if order.isbuy():
                print(f"Buy Executed: Price: {order.executed.price}, Size: {order.executed.size}")
            elif order.issell():
                print(f"Sell Executed: Price: {order.executed.price}, Size: {order.executed.size}")
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            print("Order Failed")

    def stop(self):
        # Print list of trades
        print("List of Trades:")
        for trade in self.trade_list:
            print(trade)
        print("\n")

# Create an instance of Cerebro engine
cerebro = bt.Cerebro()

# Add the strategy to Cerebro
cerebro.addstrategy(EmaCrossStrategy)

# Convert the DataFrame into a Backtrader-compatible data feed
data_feed = PandasData(dataname=data_df)

# Add the data feed to Cerebro
cerebro.adddata(data_feed)

# Set initial capital and commissions
cerebro.broker.setcash(300000)  # Rs 3,00,000 starting capital
cerebro.broker.setcommission(commission=0.002 / 100)  # 0.002% commission on turnover

# Print the starting portfolio value
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())

# Run the backtest
results = cerebro.run()

# Print the final portfolio value
final_value = cerebro.broker.getvalue()
print('Final Portfolio Value: %.2f' % final_value)

# Calculate Profit/Loss and other metrics
profit_loss = final_value - 300000
print('Net Profit/Loss: %.2f' % profit_loss)

# Plot the results
fig = cerebro.plot(iplot=False)[0][0]
plt.show()

Output

PS C:\Users\Dell\OneDrive\Documents\Python\backtester strategies> python .\emacross_backtrader.py
[*********************100%***********************]  1 of 1 completed
Starting Portfolio Value: 300000.00
Buy Executed: Price: 1094.501953125, Size: 1
Sell Executed: Price: 2011.21630859375, Size: -1
Buy Executed: Price: 1853.337158203125, Size: 1
Sell Executed: Price: 1834.92333984375, Size: -1
Buy Executed: Price: 1839.9998779296875, Size: 1
Sell Executed: Price: 1772.620849609375, Size: -1
Buy Executed: Price: 1865.7515869140625, Size: 1
Sell Executed: Price: 1748.438232421875, Size: -1
Buy Executed: Price: 1897.225830078125, Size: 1
Sell Executed: Price: 1903.17919921875, Size: -1
Buy Executed: Price: 1844.06103515625, Size: 1
Sell Executed: Price: 1934.8380126953125, Size: -1
Buy Executed: Price: 1954.2672119140625, Size: 1
Sell Executed: Price: 2313.083251953125, Size: -1
Buy Executed: Price: 2262.456787109375, Size: 1
Sell Executed: Price: 2182.894287109375, Size: -1
Buy Executed: Price: 2226.275146484375, Size: 1
Sell Executed: Price: 2127.51416015625, Size: -1
Buy Executed: Price: 2245.427490234375, Size: 1
Sell Executed: Price: 2282.255126953125, Size: -1
Buy Executed: Price: 2438.01123046875, Size: 1
Sell Executed: Price: 2404.4140625, Size: -1
Buy Executed: Price: 2374.416748046875, Size: 1
Sell Executed: Price: 2306.7607421875, Size: -1
Buy Executed: Price: 2270.5791015625, Size: 1
Sell Executed: Price: 2382.26220703125, Size: -1
List of Trades:
{'Entry Price': 1094.501953125, 'Exit Price': 1094.501953125, 'Size': 0, 'Profit/Loss': 916.6522411035156}
{'Entry Price': 1853.337158203125, 'Exit Price': 1853.337158203125, 'Size': 0, 'Profit/Loss': -18.48758356933594}
{'Entry Price': 1839.9998779296875, 'Exit Price': 1839.9998779296875, 'Size': 0, 'Profit/Loss': -67.45128073486327}
{'Entry Price': 1865.7515869140625, 'Exit Price': 1865.7515869140625, 'Size': 0, 'Profit/Loss': -117.38563828857421}
{'Entry Price': 1897.225830078125, 'Exit Price': 1897.225830078125, 'Size': 0, 'Profit/Loss': 5.877361040039062}
{'Entry Price': 1844.06103515625, 'Exit Price': 1844.06103515625, 'Size': 0, 'Profit/Loss': 90.70139955810546}
{'Entry Price': 1954.2672119140625, 'Exit Price': 1954.2672119140625, 'Size': 0, 'Profit/Loss': 358.73069302978513}
{'Entry Price': 2262.456787109375, 'Exit Price': 2262.456787109375, 'Size': 0, 'Profit/Loss': -79.65140702148437}
{'Entry Price': 2226.275146484375, 'Exit Price': 2226.275146484375, 'Size': 0, 'Profit/Loss': -98.84806211425781}
{'Entry Price': 2245.427490234375, 'Exit Price': 2245.427490234375, 'Size': 0, 'Profit/Loss': 36.73708306640625}
{'Entry Price': 2438.01123046875, 'Exit Price': 2438.01123046875, 'Size': 0, 'Profit/Loss': -33.69401647460938}
{'Entry Price': 2374.416748046875, 'Exit Price': 2374.416748046875, 'Size': 0, 'Profit/Loss': -67.74962940917969}
{'Entry Price': 2270.5791015625, 'Exit Price': 2270.5791015625, 'Size': 0, 'Profit/Loss': 111.59004864257813}


Final Portfolio Value: 301037.02
Net Profit/Loss: 1037.02

Key Points:

Backtesting Metrics: We print the starting and final portfolio values, along with the net profit/loss after running the backtest.

EMA Crossover Strategy: We define a strategy that uses the 10-day and 20-day EMAs to signal buy/sell points.

Trade Tracking: The notify_trade method tracks each trade, including entry/exit prices, trade size, and profit/loss. It calculates the exit price based on the profit/loss and trade size.

Order Execution: The notify_order method logs the order details (entry/exit prices and size) when orders are executed.

Backtrader’s flexibility and powerful Cerebro engine make it easy to backtest trading strategies with historical data. In this tutorial, we implemented an EMA crossover strategy using Yahoo Finance data, and tracked detailed trade information, such as entry/exit prices, trade sizes, and profit/loss. You can further extend this by adding more complex strategies, additional metrics, or real-time trading.

Rajandran R Creator of OpenAlgo - OpenSource Algo Trading framework for Indian Traders. Building GenAI Applications. Telecom Engineer turned Full-time Derivative Trader. Mostly Trading Nifty, Banknifty, High Liquid Stock Derivatives. Trading the Markets Since 2006 onwards. Using Market Profile and Orderflow for more than a decade. Designed and published 100+ open source trading systems on various trading tools. Strongly believe that market understanding and robust trading frameworks are the key to the trading success. Building Algo Platforms, Writing about Markets, Trading System Design, Market Sentiment, Trading Softwares & Trading Nuances since 2007 onwards. Author of Marketcalls.in

Leave a Reply

Get Notifications, Alerts on Market Updates, Trading Tools, Automation & More