Navigation

    Backtrader Community

    • Register
    • Login
    • Search
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Groups
    • Search
    For code/output blocks: Use ``` (aka backtick or grave accent) in a single line before and after the block. See: http://commonmark.org/help/

    MA Crossover Strategy: Flip position and max risk sizer

    General Code/Help
    3
    11
    2875
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • C
      CadTrader last edited by

      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()
      
      1 Reply Last reply Reply Quote 0
      • A
        ab_trader last edited by

        When you open short position with 'size=1000', the sizer is not participated. Maybe you need to skip size in the 'sell' order.

        • If my answer helped, hit reputation up arrow at lower right corner of the post.
        • Python Debugging With Pdb
        • New to python and bt - check this out
        C 1 Reply Last reply Reply Quote 0
        • B
          backtrader administrators last edited by

          https://www.backtrader.com/docu/strategy-reference.html

          It's alo in the sources.

          C 1 Reply Last reply Reply Quote 1
          • C
            CadTrader @ab_trader last edited by

            @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.

            1 Reply Last reply Reply Quote 0
            • C
              CadTrader @backtrader last edited by

              @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.

              1 Reply Last reply Reply Quote 0
              • A
                ab_trader last edited by

                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
                
                • If my answer helped, hit reputation up arrow at lower right corner of the post.
                • Python Debugging With Pdb
                • New to python and bt - check this out
                1 Reply Last reply Reply Quote 1
                • A
                  ab_trader last edited by

                  I am not able to edit the post, but script i shifted a bit. I think you can fix it.

                  • If my answer helped, hit reputation up arrow at lower right corner of the post.
                  • Python Debugging With Pdb
                  • New to python and bt - check this out
                  C 1 Reply Last reply Reply Quote 2
                  • C
                    CadTrader @ab_trader last edited by

                    @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.

                    1 Reply Last reply Reply Quote 0
                    • C
                      CadTrader last edited by

                      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()
                      
                      1 Reply Last reply Reply Quote 1
                      • A
                        ab_trader last edited by

                        @cadtrader use of

                        class MA_CrossOver(bt.SignalStrategy):
                        

                        should help.

                        • If my answer helped, hit reputation up arrow at lower right corner of the post.
                        • Python Debugging With Pdb
                        • New to python and bt - check this out
                        1 Reply Last reply Reply Quote 2
                        • C
                          CadTrader last edited by

                          @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.

                          1 Reply Last reply Reply Quote 0
                          • 1 / 1
                          • First post
                            Last post
                          Copyright © 2016, 2017, 2018, 2019, 2020, 2021 NodeBB Forums | Contributors