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!