MA Crossover Strategy: Flip position and max risk sizer
-
Hi everyone,
I am fairly new to Python and Backtrader. I am testing out a simple SMA cross over strategy. I am finding the results i would like to obtain are not what they should be. The idea is i want to flip position from long short and vice versa when the SMA crosses. I also haven't been successful figuring out how to make the risk sizer work for the short position. Once i get this working, i intend to incorporate margin. Any assistance or feedback would be helpful.
# Create a Stratey class MA_CrossOver(bt.Strategy): alias = ('SMA_CrossOver',) params = ( # period for the fast Moving Average ('fast', 20), # period for the slow moving average ('slow', 200), # Exit Bar entry delay for flipped position ('exitbars', 1), # moving average to use ('_movav', bt.ind.MovAv.SMA) ) def log(self, txt, dt=None): ''' Logging function for this strategy''' dt = dt or self.datas[0].datetime.date(0) print('%s, %s' % (dt.isoformat(), txt)) def __init__(self): # Keep a reference to the "close" line in the data[0] dataseries self.dataclose = self.datas[0].close # To keep track of pending orders and buy price/commission self.order = None self.buyprice = None self.buycomm = None # Indicators sma_fast = self.p._movav(period=self.p.fast) sma_slow = self.p._movav(period=self.p.slow) self.buysig = bt.ind.CrossOver(sma_fast, sma_slow) self.sellsig = bt.ind.CrossOver(sma_slow, sma_fast) 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 # Attention: broker could reject order if not enough cash if order.status in [order.Completed]: if order.isbuy(): self.log( 'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % (order.executed.price, order.executed.value, order.executed.comm)) self.buyprice = order.executed.price self.buycomm = order.executed.comm else: # Sell self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % (order.executed.price, order.executed.value, order.executed.comm)) self.bar_executed = len(self) elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log('Order Canceled/Margin/Rejected') self.order = None def notify_trade(self, trade): if not trade.isclosed: return self.log('GROSS %.2f, NET %.2f' % (trade.pnl, trade.pnlcomm)) def next(self): # Simply log the closing price of the series from the reference self.log('Close, %.2f' % self.dataclose[0]) # Check if an order is pending ... if yes, we cannot send a 2nd one if self.order: return # Check if we are in the market if not self.position: # buy signal if self.buysig > 0: # BUY self.log('BUY CREATE, %.2f' % self.dataclose[0]) # Keep track of the created order to avoid a 2nd order self.order = self.buy() elif self.sellsig > 0: #if len(self) >= (self.bar_executed + self.params.exitbars): # Sell self.log('SELL CREATE, %.2f' % self.dataclose[0]) # Keep track of the created order to avoid a 2nd order self.order = self.sell(size=1000) #else: #return else: return else: # in a position if self.buysig < 0: # Buy Closed self.log('BUY CLOSE, %.2f' % self.dataclose[0]) self.order = self.close() self.order = self.sell(size=1000) # elif ignore zero case elif self.sellsig < 0: # Sell Closed self.log('SELL CLOSE, %.2f' % self.dataclose[0]) self.order = self.close() self.order = self.buy() else: return class maxRiskSizer(bt.Sizer): ''' Returns the number of shares rounded down that can be purchased for the max rish tolerance ''' params = (('risk', 0.98),) def __init__(self): if self.p.risk > 1 or self.p.risk < 0: raise ValueError('The risk parameter is a percentage which must be' 'entered as a float. e.g. 0.5') def _getsizing(self, comminfo, cash, data, isbuy): if isbuy == True: size = math.floor((cash * self.p.risk) / data[0]) return size if __name__ == '__main__': # Create a cerebro entity cerebro = bt.Cerebro() # Add a strategy cerebro.addstrategy(MA_CrossOver) # Datas are in a subfolder of the samples. Need to find where the script is # because it could have been called from anywhere modpath = os.path.dirname(os.path.abspath('C:\\Users\\Cad\\Documents\\Python Scripts\\Strategy Development\\Data\\')) datapath = os.path.join(modpath, 'Data\\SPY-TIME_SERIES_DAILY_ADJUSTED.csv') # Create a Data Feed data = bt.feeds.GenericCSVData( dataname=datapath, # Do not pass values before this date fromdate=datetime.datetime(1994, 1, 1), # Do not pass values before this date todate=datetime.datetime(2018, 12, 31), # Set empty values to 0 nullvalue=0.0, # Date formatting dtformat=('%Y-%m-%d'), # set headers for data feed to match csv datetime=0, close=6, high=7, low=8, open=9, volume=10, openinterest=-1, # Do not pass values after this date reverse=False) # Add the Data Feed to Cerebro cerebro.adddata(data) # Set our desired cash start cerebro.broker.setcash(30000.0) # Add sizer cerebro.addsizer(maxRiskSizer) # Set the commission cerebro.broker.setcommission(commission=0.0003) # Add Analyzer #Cerebro.addanalyzer(bt.analyzers.Benchmark) # Print out the starting conditions print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) # Run over everything cerebro.run() # Print out the final result print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue()) # Plot the result cerebro.plot()
-
When you open short position with 'size=1000', the sizer is not participated. Maybe you need to skip size in the 'sell' order.
-
https://www.backtrader.com/docu/strategy-reference.html
It's alo in the sources.
-
@ab_trader I added size = 1000 as if i don't, it will throw an error citing the max risk sizer that i suspect is being caused by the current code not closing the open position at the cross over before trying to take the opposite position.
-
@backtrader Yes, i have seen this, but i am not entirely sure how to go about implementing it. I will take a stab and if i hit a wall, i'll repost here for further guidance.
-
Two things concerns me in your sizer:
- you don't have provision for size definition in case of short position
- I don't think that this line should work, but I might be wrong
size = math.floor((cash * self.p.risk) / data[0])
I don't think that
data[0]
is a price you want.So reworking one of the built in sizers I would try the following (not tested):
class maxRiskSizer(bt.Sizer): params = (('risk', 20),) def __init__(self): if self.p.risk > 1 or self.p.risk < 0: raise ValueError('The risk parameter is a percentage which must be entered as a float. e.g. 0.5') def _getsizing(self, comminfo, cash, data, isbuy): position = self.broker.getposition(data) if not position: size = math.floor((cash * self.p.risk) /data.close[0]) else: size = position.size return size
-
I am not able to edit the post, but script i shifted a bit. I think you can fix it.
-
@ab_trader Thank you! I tested it out and it seems to work, but i am unsure if it is taking a 0.99 risk size on the short side since i am having errors thrown for something else. See new post below.
-
I have updated the code taking @backtrader suggestion and also incorporating @ab_trader suggestion for the max risk sizer. It's throwing the following error which it didn't before when i was testing a simpler version before putting it into this one.
AttributeError Traceback (most recent call last)
<ipython-input-8-c89fb8c08f73> in <module>()
141
142 # Run over everything
--> 143 cerebro.run()
144
145 # Print out the final result~\Anaconda3\lib\site-packages\backtrader\cerebro.py in run(self, **kwargs)
1125 # let's skip process "spawning"
1126 for iterstrat in iterstrats:
-> 1127 runstrat = self.runstrategies(iterstrat)
1128 self.runstrats.append(runstrat)
1129 if self._dooptimize:~\Anaconda3\lib\site-packages\backtrader\cerebro.py in runstrategies(self, iterstrat, predata)
1215 sargs = self.datas + list(sargs)
1216 try:
-> 1217 strat = stratcls(*sargs, **skwargs)
1218 except bt.errors.StrategySkipError:
1219 continue # do not add strategy to the mix~\Anaconda3\lib\site-packages\backtrader\metabase.py in call(cls, *args, **kwargs)
86 _obj, args, kwargs = cls.donew(*args, **kwargs)
87 _obj, args, kwargs = cls.dopreinit(_obj, *args, **kwargs)
---> 88 _obj, args, kwargs = cls.doinit(_obj, *args, **kwargs)
89 _obj, args, kwargs = cls.dopostinit(_obj, *args, **kwargs)
90 return _obj~\Anaconda3\lib\site-packages\backtrader\metabase.py in doinit(cls, _obj, *args, **kwargs)
76
77 def doinit(cls, _obj, *args, **kwargs):
---> 78 _obj.init(*args, **kwargs)
79 return _obj, args, kwargs
80<ipython-input-8-c89fb8c08f73> in init(self)
26 # Indicators
27 sma1, sma2 = bt.ind.SMA(period=self.p.pfast), bt.ind.SMA(period=self.p.pslow)
---> 28 self.signal_add(bt.SIGNAL_LONGSHORT, bt.ind.CrossOver(sma1, sma2))
29
30 def notify_order(self, order):~\Anaconda3\lib\site-packages\backtrader\lineseries.py in getattr(self, name)
459 # in this object if we set an attribute in this object it will be
460 # found before we end up here
--> 461 return getattr(self.lines, name)
462
463 def len(self):AttributeError: 'Lines_LineSeries_LineIterator_DataAccessor_Strateg' object has no attribute 'signal_add'
# Create a Stratey class MA_CrossOver(bt.Strategy): alias = ('SMA_CrossOver',) params = ( # period for the fast Moving Average ('pfast', 20), # period for the slow moving average ('pslow', 200) ) def log(self, txt, dt=None): ''' Logging function for this strategy''' dt = dt or self.datas[0].datetime.date(0) print('%s, %s' % (dt.isoformat(), txt)) def __init__(self): # Keep a reference to the "close" line in the data[0] dataseries self.dataclose = self.datas[0].close # To keep track of pending orders and buy price/commission self.order = None self.buyprice = None self.buycomm = None # Indicators sma1, sma2 = bt.ind.SMA(period=self.p.pfast), bt.ind.SMA(period=self.p.pslow) self.signal_add(bt.SIGNAL_LONGSHORT, bt.ind.CrossOver(sma1, sma2)) 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 # Attention: broker could reject order if not enough cash if order.status in [order.Completed]: if order.isbuy(): self.log( 'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % (order.executed.price, order.executed.value, order.executed.comm)) self.buyprice = order.executed.price self.buycomm = order.executed.comm else: # Sell self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % (order.executed.price, order.executed.value, order.executed.comm)) self.bar_executed = len(self) elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log('Order Canceled/Margin/Rejected') self.order = None def notify_trade(self, trade): if not trade.isclosed: return self.log('GROSS %.2f, NET %.2f' % (trade.pnl, trade.pnlcomm)) def next(self): # Simply log the closing price of the series from the reference self.log('Close, %.2f' % self.dataclose[0]) # Check if an order is pending ... if yes, we cannot send a 2nd one if self.order: return class maxRiskSizer(bt.Sizer): params = (('risk', 0.99),) def __init__(self): if self.p.risk > 1 or self.p.risk < 0: raise ValueError('The risk parameter is a percentage which must be entered as a float. e.g. 0.5') def _getsizing(self, comminfo, cash, data, isbuy): position = self.broker.getposition(data) if not position: size = math.floor((cash * self.p.risk) /data.close[0]) else: size = position.size return size if __name__ == '__main__': # Create a cerebro entity cerebro = bt.Cerebro() # Add a strategy cerebro.addstrategy(MA_CrossOver) # Datas are in a subfolder of the samples. Need to find where the script is # because it could have been called from anywhere modpath = os.path.dirname(os.path.abspath('C:\\Users\\Cad\\Documents\\Python Scripts\\Strategy Development\\Data\\')) datapath = os.path.join(modpath, 'Data\\SPY-TIME_SERIES_DAILY_ADJUSTED.csv') # Create a Data Feed data = bt.feeds.GenericCSVData( dataname=datapath, # Do not pass values before this date fromdate=datetime.datetime(1994, 1, 1), # Do not pass values before this date todate=datetime.datetime(2018, 12, 31), # Set empty values to 0 nullvalue=0.0, # Date formatting dtformat=('%Y-%m-%d'), # set headers for data feed to match csv datetime=0, close=6, high=7, low=8, open=9, volume=10, openinterest=-1, # Do not pass values after this date reverse=False) # Add the Data Feed to Cerebro cerebro.adddata(data) # Set our desired cash start cerebro.broker.setcash(30000.0) # Add sizer cerebro.addsizer(maxRiskSizer) # Set the commission cerebro.broker.setcommission(commission=0.00035) # Add Analyzer #Cerebro.addanalyzer(bt.analyzers.Benchmark) # Print out the starting conditions print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) # Run over everything cerebro.run() # Print out the final result print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue()) # Plot the result cerebro.plot()
-
-
@ab_trader that did the trick. so silly i missed that. I will do some more testing as i need to verify it's working the way i intend.