This article demonstrates how to create a custom trading strategy using the backtrader library in Python for use with “Backtester” app. The strategy combines Moving Average (MA) crossover with MACD confirmation to generate buy and sell signals. We’ll break down the code, explain its components, and show how it can be used within the backtrader framework.
The strategy, named MACrossoverMACDConfirmStrategy,
leverages two popular technical indicators:
The strategy’s logic is as follows:
Let’s dissect the Python code, explaining each section:
1. Import Statements and Parameters:
Python
import backtrader as bt
from datetime import datetime
class MACrossoverMACDConfirmStrategy(bt.Strategy):
params = (
# MA Params
('short_ma_period', 50), # Period for the short-term MA
('long_ma_period', 200), # Period for the long-term MA
('ma_type', 'EMA'), # Type of Moving Average: 'SMA' or 'EMA'
# MACD Params
('macd_fast', 12), # Period for the fast EMA in MACD
('macd_slow', 26), # Period for the slow EMA in MACD
('macd_signal', 9), # Period for the signal line EMA in MACD
('printlog', True), # Enable/Disable logging
)
backtrader library as
bt and the datetime module.MACrossoverMACDConfirmStrategy class inherits from
bt.Strategy, which is the base class for all backtrader
strategies.params tuple defines the strategy’s configurable
parameters. These include the periods for the short-term and long-term
moving averages (short_ma_period,
long_ma_period), the type of moving average to use
(ma_type - either ‘SMA’ for Simple Moving Average or ‘EMA’
for Exponential Moving Average), and the parameters for the MACD
calculation (macd_fast, macd_slow,
macd_signal). The printlog parameter enables
or disables logging of trades and strategy actions. The image of the
backtester, for example, shows the parameters
short_ma_period at 50 and long_ma_period at
90.2. __init__ Method:
Python
def __init__(self):
# Keep references to data feeds
self.dataclose = self.datas[0].close
# To keep track of pending orders
self.order = None
self.buyprice = None
self.buycomm = None
# --- MA Indicators ---
ma_indicator = bt.indicators.SimpleMovingAverage
if self.p.ma_type == 'EMA':
ma_indicator = bt.indicators.ExponentialMovingAverage
self.short_ma = ma_indicator(self.datas[0], period=self.p.short_ma_period)
self.long_ma = ma_indicator(self.datas[0], period=self.p.long_ma_period)
self.ma_crossover = bt.indicators.CrossOver(self.short_ma, self.long_ma)
# --- MACD Indicator ---
self.macd = bt.indicators.MACD(
self.datas[0],
period_me1=self.p.macd_fast,
period_me2=self.p.macd_slow,
period_signal=self.p.macd_signal
)
__init__ method is the constructor for the
strategy. It’s called once when the strategy is initialized.self.dataclose = self.datas[0].close gets the closing
price data. self.datas is a list of data feeds provided to
the strategy.self.order, self.buyprice, and
self.buycomm are initialized to None and are
used to track orders and trade details.SimpleMovingAverage and
ExponentialMovingAverage based on the ma_type
parameter. The periods are set using short_ma_period and
long_ma_period from the parameters. The
bt.indicators.CrossOver indicator detects when the short MA
crosses the long MA.bt.indicators.MACD, with its parameters also taken from the
params tuple.3. log Method:
Python
def log(self, txt, dt=None, doprint=False):
''' Logging function for this strategy'''
if self.params.printlog or doprint:
try:
log_dt = bt.num2date(self.datas[0].datetime[0]).date()
except IndexError:
dt = dt or self.datas[0].datetime.date(0) if len(self.datas[0]) else datetime.now().date()
log_dt = dt
print(f'{log_dt.isoformat()} - {txt}')
printlog parameter to determine whether
to print the log message.4. notify_order Method:
Python
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, 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')
self.order = None
Submitted,
Accepted, Completed, Canceled,
Margin, and Rejected.5. notify_trade Method:
Python
def notify_trade(self, trade):
if not trade.isclosed:
return
self.log(f'OPERATION PROFIT, GROSS {trade.pnl:.2f}, NET {trade.pnlcomm:.2f}')
6. next Method:
Python
def next(self):
# Check if an order is pending
if self.order:
return
# Check if indicators are ready
if len(self) < self.p.long_ma_period or len(self) < (self.p.macd_slow + self.p.macd_signal):
return
It is easy to create custom strategies to backtest with backtrader that you can add to the “Backtester” app. You can start with my book to learn fast or check out the strategies instruction manual for Backtester: https://www.pyquantlab.com/apps/backtester_strategies_manual.pdf let’s see the results with Backtester:
I changed parameters as you can see to find a combination that works better. I am updating the app to include many strategies to give you ideas and a base to build upon. Don’t miss out!