Combining SMA and MACD Crossovers Across Two Backtest Strategies
Summary
This Backtrader example combines two entry and exit signals on a selected data feed: a short and long simple moving average crossover, and a MACD line crossing its signal line. Each bullish crossover adds half of the strategy's target stake, while bearish crossovers close positions; when both entries have accumulated the full target, each exit is sized to close half. It prints position and execution details and can run two strategy instances with different indicator periods and stake sizes, using either the same feed or a copied feed.
The document provides implementation structure and configurable parameters, but no backtest results or evidence that the signals are profitable. The sample uses historical Yahoo Finance CSV data and offers optional plotting and date filters. Its mechanics also leave practical questions: overlapping signals can issue multiple orders, and the code does not present risk controls, transaction-cost assumptions, or a performance evaluation. It is best read as an example of combining and comparing crossover rules in a backtesting framework, rather than as a validated trading system.
Key ideas
- A moving average crossover and a MACD crossover can serve as separate entry and exit signals.
- Each bullish signal targets half the strategy's configured stake.
- When the full target stake is held, a bearish signal closes half of the position.
- Two strategy instances can use different parameters on the same or copied data feed.
- The example supplies no evidence of profitability or a full risk and cost analysis.
Tags
Full text
# multi-copy.py
```py
#!/usr/bin/env python
# -*- coding: utf-8; py-indent-offset:4 -*-
###############################################################################
#
# Copyright (C) 2015-2023 Daniel Rodriguez
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import argparse
import datetime
import random
import backtrader as bt
class TheStrategy(bt.Strategy):
'''
This strategy is capable of:
- Going Long with a Moving Average upwards CrossOver
- Going Long again with a MACD upwards CrossOver
- Closing the aforementioned longs with the corresponding downwards
crossovers
'''
params = (
('myname', None),
('dtarget', None),
('stake', 100),
('macd1', 12),
('macd2', 26),
('macdsig', 9),
('sma1', 10),
('sma2', 30),
)
def notify_order(self, order):
if not order.alive():
if not order.isbuy(): # going flat
self.order = 0
if order.status == order.Completed:
tfields = [self.p.myname,
len(self),
order.data.datetime.date(),
order.data._name,
'BUY' * order.isbuy() or 'SELL',
order.executed.size, order.executed.price]
print(','.join(str(x) for x in tfields))
def __init__(self):
# Choose data to buy from
self.dtarget = self.getdatabyname(self.p.dtarget)
# Create indicators
sma1 = bt.ind.SMA(self.dtarget, period=self.p.sma1)
sma2 = bt.ind.SMA(self.dtarget, period=self.p.sma2)
self.smasig = bt.ind.CrossOver(sma1, sma2)
macd = bt.ind.MACD(self.dtarget,
period_me1=self.p.macd1,
period_me2=self.p.macd2,
period_signal=self.p.macdsig)
# Cross of macd.macd and macd.signal
self.macdsig = bt.ind.CrossOver(macd.macd, macd.signal)
def start(self):
self.order = 0 # sentinel to avoid operrations on pending order
tfields = ['Name', 'Length', 'Datetime', 'Operation/Names',
'Position1.Size', 'Position2.Size']
print(','.join(str(x) for x in tfields))
def next(self):
tfields = [self.p.myname,
len(self),
self.data.datetime.date(),
self.getposition(self.data0).size]
if len(self.datas) > 1:
tfields.append(self.getposition(self.data1).size)
print(','.join(str(x) for x in tfields))
buysize = self.p.stake // 2 # let each signal buy half
if self.macdsig[0] > 0.0:
self.buy(data=self.dtarget, size=buysize)
if self.smasig[0] > 0.0:
self.buy(data=self.dtarget, size=buysize)
size = self.getposition(self.dtarget).size
# if 2x in the market, let each potential close ... close 1/2
if size == self.p.stake:
size //= 2
if self.macdsig[0] < 0.0:
self.close(data=self.dtarget, size=size)
if self.smasig[0] < 0.0:
self.close(data=self.dtarget, size=size)
class TheStrategy2(TheStrategy):
'''
Subclass of TheStrategy to simply change the parameters
'''
params = (
('stake', 200),
('macd1', 15),
('macd2', 22),
('macdsig', 7),
('sma1', 15),
('sma2', 50),
)
def runstrat(args=None):
args = parse_args(args)
cerebro = bt.Cerebro()
cerebro.broker.set_cash(args.cash)
dkwargs = dict()
if args.fromdate is not None:
fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d')
dkwargs['fromdate'] = fromdate
if args.todate is not None:
todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d')
dkwargs['todate'] = todate
# if dataset is None, args.data has been given
data0 = bt.feeds.YahooFinanceCSVData(dataname=args.data0, **dkwargs)
cerebro.adddata(data0, name='MyData0')
st0kwargs = dict()
if args.st0 is not None:
tmpdict = eval('dict(' + args.st0 + ')') # args were passed
st0kwargs.update(tmpdict)
cerebro.addstrategy(TheStrategy,
myname='St1', dtarget='MyData0', **st0kwargs)
if args.copydata:
data1 = data0.copyas('MyData1')
cerebro.adddata(data1)
dtarget = 'MyData1'
else: # use same target
dtarget = 'MyData0'
st1kwargs = dict()
if args.st1 is not None:
tmpdict = eval('dict(' + args.st1 + ')') # args were passed
st1kwargs.update(tmpdict)
cerebro.addstrategy(TheStrategy2,
myname='St2', dtarget=dtarget, **st1kwargs)
results = cerebro.run()
if args.plot:
pkwargs = dict(style='bar')
if args.plot is not True: # evals to True but is not True
npkwargs = eval('dict(' + args.plot + ')') # args were passed
pkwargs.update(npkwargs)
cerebro.plot(**pkwargs)
def parse_args(pargs=None):
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description='Sample for Tharp example with MACD')
# pgroup = parser.add_mutually_exclusive_group(required=True)
parser.add_argument('--data0', required=False,
default='../../datas/yhoo-1996-2014.txt',
help='Specific data0 to be read in')
parser.add_argument('--fromdate', required=False,
default='2005-01-01',
help='Starting date in YYYY-MM-DD format')
parser.add_argument('--todate', required=False,
default='2006-12-31',
help='Ending date in YYYY-MM-DD format')
parser.add_argument('--cash', required=False, action='store',
type=float, default=50000,
help=('Cash to start with'))
parser.add_argument('--copydata', required=False, action='store_true',
help=('Copy Data for 2nd strategy'))
parser.add_argument('--st0', required=False, action='store',
default=None,
help=('Params for 1st strategy: as a list of comma '
'separated name=value pairs like: '
'stake=100,macd1=12,macd2=26,macdsig=9,'
'sma1=10,sma2=30'))
parser.add_argument('--st1', required=False, action='store',
default=None,
help=('Params for 1st strategy: as a list of comma '
'separated name=value pairs like: '
'stake=200,macd1=15,macd2=22,macdsig=7,'
'sma1=15,sma2=50'))
# Plot options
parser.add_argument('--plot', '-p', nargs='?', required=False,
metavar='kwargs', const=True,
help=('Plot the read data applying any kwargs passed\n'
'\n'
'For example:\n'
'\n'
' --plot style="candle" (to plot candles)\n'))
if pargs is not None:
return parser.parse_args(pargs)
return parser.parse_args()
if __name__ == '__main__':
runstrat()
```Shown in full with attribution under the source's licence. Licence: GPL-3.0
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.