For code/output blocks: Use ``` (aka backtick or grave accent) in a single line before and after the block. See: http://commonmark.org/help/

Help with Simple BBand Strategy



  • Hi,

    I am trying to work on a simple Bband strategy, where when close up cross the bband bottom line, I buy; when close down cross the bband top line, I sell.

    Here is my code:

    class BBand_CrossOver(bt.Strategy):
      params = (('period', 20),)
    
      def __init__(self):
        self.bband = bt.indicators.BollingerBands(self.data0, period=self.params.period)
        self.buysig = bt.indicators.CrossOver(self.data0, self.bband.lines.bot)
        self.sellsig = bt.indicators.CrossOver(self.data0, self.bband.lines.top)
    
      def next(self):
        if self.position.size:
          if self.sellsig < 0:
            self.sell()
    
        elif self.buysig > 0:
          self.buy()
    

    I tried symbol 'BABA' on it for 2016 data, code:

      cerebro = bt.Cerebro()
      data = bt.feeds.YahooFinanceData(dataname='BABA', fromdate=datetime.datetime(2016, 1, 1),
                                       todate=bt.datetime.datetime(2016, 12, 31), timeframe=bt.TimeFrame.Days,
                                       compression=1)
      cerebro.adddata(data)
      cerebro.addstrategy(BBand_CrossOver, period=20)
    
      cerebro.broker.setcash(10000.0)
      cerebro.addsizer(bt.sizers.AllInSizer)
    
      cerebro.addwriter(bt.WriterFile, csv=True, out='test.csv')
    
      cerebro.run(optreturn=False)
      cerebro.plot()
    

    Interestingly, it did execute some orders, but it did not trigger on all of them.
    alt text

    link: https://www.dropbox.com/s/ysrsbwzde4f00eh/BABA.png?dl=0

    You can see it triggered the buy order around Feb 5th, and Sell order around March 20th, which are expected.

    However, around June 28th, there should be a up-cross. but the buy order is not triggered.

    Is there a good way to debug this?
    Can I output the self.bband.lines and the signals in the writer as well?

    Thanks very much!



  • @xisisu said in Help with Simple BBand Strategy:

      cerebro.broker.setcash(10000.0)
      cerebro.addsizer(bt.sizers.AllInSizer)
    

    Your orders are getting for sure rejected due to the AllIn strategy. There is plenty of literature in the community itself.

    • Your strategy sees the signal and issues the order. The only available price for size calculation is the current close (to avoid the lookahead bias which was a question yesterday)
    • The next open price is greater (opening gap) than the close
    • Because the order is sent as a size (like in a normal broker), the current size * open > size * close and there is not enough money in your account to execute the operation

    See (quick googling, I know there are more because I read them)

    Your alternatives:

    I would personally avoid cheating but ...



  • @Paska-Houso

    Thank you so much for the detailed reply!

    I have modified my code with the cheat_on_open option, but it seems using the current open price in next_open(), not the next day open price.

    class BBand_CrossOver(bt.Strategy):
      params = (('period', 20),)
    
      def log(self, txt, dt=None):
        dt = dt or self.datas[0].datetime.date(0)
        print('{}, {}'.format(dt.isoformat(), txt))
    
      def __init__(self):
        self.dataclose = self.data0.close
        self.dataopen = self.data0.open
    
        self.bband = bt.indicators.BollingerBands(self.data0, period=self.params.period)
        self.buysig = bt.indicators.CrossOver(self.data0, self.bband.lines.bot)
        self.sellsig = bt.indicators.CrossOver(self.data0, self.bband.lines.top)
    
      def next_open(self):
        self.log('Close: %.2f, Open: %.2f' % (self.dataclose[0], self.dataopen[0]))
    
        if self.position.size:
          if self.sellsig < 0:
            self.log('SELL CREATE, size {}'.format(self.position.size))
            self.sell(size=self.position.size)
    
        elif self.buysig > 0:
          size = int(self.broker.getcash() / self.data0.open)
          self.log('BUY CREATE, cash {}, size {}, open {}, close {}'.format(self.broker.getcash(), size, self.dataopen[0], self.dataclose[0]))
          self.buy(size=size)
    
      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 enougth cash
        if order.status in [order.Completed]:
          if order.isbuy():
            self.log('BUY EXECUTED, price {}, cost {}, comm {}'.format(order.executed.price, order.executed.value,
                                                                       order.executed.comm))
          elif order.issell():
            self.log('SELL EXECUTED, price {}, cost {}, comm {}'.format(order.executed.price, order.executed.value,
                                                                        order.executed.comm))
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
          self.log('Order Canceled/Margin/Rejected')
    
        # Write down: no pending order
        self.order = None
    
      def notify_trade(self, trade):
        if not trade.isclosed:
          return
        self.log('OPERATION PROFIT, GROSS {}, NET {}'.format(trade.pnl, trade.pnlcomm))
    

    And here is the main program:

      cerebro = bt.Cerebro(cheat_on_open=True)
      data = bt.feeds.YahooFinanceData(dataname='BABA', fromdate=datetime.datetime(2016, 1, 1),
                                       todate=bt.datetime.datetime(2016, 12, 31), timeframe=bt.TimeFrame.Days,
                                       compression=1)
      cerebro.adddata(data)
      cerebro.addstrategy(BBand_CrossOver, period=20)
    
      cerebro.broker.setcash(10000.0)
      # cerebro.addsizer(bt.sizers.AllInSizer)
      # cerebro.broker.setcommission(commission=0.001)
    
      cerebro.addwriter(bt.WriterFile, csv=True, out='test.csv')
    
      print('start: {}'.format(cerebro.broker.getvalue()))
      cerebro.run(optreturn=False)
      print('finish: {}'.format(cerebro.broker.getvalue()))
      cerebro.plot()
    

    To be more specific, in the output, I have:

    2016-06-29, Close: 78.04, Open: 76.98
    2016-06-29, BUY CREATE, cash 12071.300000000001, size 156, open 76.98, close 78.04
    2016-06-29, Order Canceled/Margin/Rejected
    2016-06-30, Close: 79.53, Open: 78.31
    
    • if we use close price on 0629, size is 12071.3 / 78.04 = 154
    • if we use open price on 0629, size is 12071.3 / 76.98 = 156
    • if we use open price on 0630, size is 12071.3 / 78.31 = 154

    Seems here it is using the open price on current day, not next day.

    Any suggestions?



  • Another general question:

    If we know that the AllInSizer have this price gap problem, and we cannot use it with the cheat-on-open option, what would be the correct use case for the AllInSizer?

    Thanks.



  • @Paska-Houso

    Digging around, I saw there is also the option of Order.Partial, link: https://www.backtrader.com/docu/order.html

    However, seems it is not triggered automatically.

    is there a way to

    • still use the AllInSizer
    • if price gap happens, partially execute the order, and cancel the rest that cannot be filled?

    Thanks.



  • @xisisu said in Help with Simple BBand Strategy:

    However, seems it is not triggered automatically.
    is there a way to

    • still use the AllInSizer
    • if price gap happens, partially execute the order, and cancel the rest that cannot be filled?

    I believe you misunderstand Order.Partial. It's not an option to execute partially. It's telling you that your order executed partially (probably because the volume at your price was not enough to fill your entire request)

    But your order CANNOT be partially executed because it's not even accepted.

    @xisisu said in Help with Simple BBand Strategy:

    • if we use close price on 0629, size is 12071.3 / 78.04 = 154
    • if we use open price on 0629, size is 12071.3 / 76.98 = 156
    • if we use open price on 0630, size is 12071.3 / 78.31 = 154

    Let me formulate a theory:

    • The close price on 2016-06-29 is larger than the open.
    • Your size has been calculated against the opening price
    • And here comes the theory ... the broker is using the close price to check the validity.
    • And your order is rejected because it doesn't pass the submission criteria

    (The theory would have to be validated by looking at the sources)

    Deactivate order submission checking: https://www.backtrader.com/docu/broker.html

    • checksubmit (there is a method to do it, find it in the doc)


  • @Paska-Houso

    Thanks for the detailed reply!

      cerebro.broker.set_checksubmit(checksubmit=False)
    

    Fixed the problem. Thank you so much! :)