BBand Strategy



  • Hello, I am excited to see that Backtrader now has a community section and I want to share a simple strategy with you all. This strategy uses Backtrader's BBand indicator and buys after the market dips into the lower band and sells on the moving average after the market hits the top band. This works great in sideways/bull markets. The idea is to buy during a low period and sell if the market dips below a moving average. Also note I am new to algotrading and programming in general so don't laugh to hard at this idea/strategy

    Example

    I actually use this strategy in the Bitcoin world and have had great results trading it live in the past 3 months. I use Tradewave to run it live with a small porting of my holdings.

    Tradewave

    You can use talib to create a BBands in Tradewave or you can use a simple math formula to recreate the bbands used in Backtrader....

    std = data(interval=INTERVAL).btc_usd.std(SETTING1)
    
    ma = data(interval=INTERVAL).btc_usd.ma(SETTING1)  
    
    upperband = ma + (Decimal(2) * std)
    
    lowerband = ma - (Decimal(2) * std)
    

    Note, I changed a lot of the code to work better such as not trading in low volatile markets, using a stop when the market turns and other things you can use to help make better, safer decisions. :wink:

    So here is the code you can copy and paste....

    import datetime  # For datetime objects
    import os.path  # To manage paths
    import sys  # To find out the script name (in argv[0])
    
    # Import the backtrader platform
    import backtrader as bt
    
    # Create a Stratey
    class TestStrategy(bt.Strategy):
        params = (('BBandsperiod', 20),)
    
        def log(self, txt, dt=None):
            ''' Logging function fot 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
            self.redline = None
            self.blueline = None
    
            # Add a BBand indicator
            self.bband = bt.indicators.BBands(self.datas[0], period=self.params.BBandsperiod)
    
        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, order.Canceled, order.Margin]:
                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)
    
            # Write down: no pending order
            self.order = None
    
        def notify_trade(self, trade):
            if not trade.isclosed:
                return
    
            self.log('OPERATION PROFIT, 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
    
            if self.dataclose < self.bband.lines.bot and not self.position:
            	self.redline = True
    
            if self.dataclose > self.bband.lines.top and self.position:
            	self.blueline = True
    
            if self.dataclose > self.bband.lines.mid and not self.position and self.redline:        	
            	# BUY, BUY, BUY!!! (with all possible default parameters)
                self.log('BUY CREATE, %.2f' % self.dataclose[0])
                # Keep track of the created order to avoid a 2nd order
                self.order = self.buy()
    
            if self.dataclose > self.bband.lines.top and not self.position:
                # BUY, BUY, BUY!!! (with all possible default parameters)
                self.log('BUY CREATE, %.2f' % self.dataclose[0])
                # Keep track of the created order to avoid a 2nd order
                self.order = self.buy()
    
            if self.dataclose < self.bband.lines.mid and self.position and self.blueline:
                # SELL, SELL, SELL!!! (with all possible default parameters)
                self.log('SELL CREATE, %.2f' % self.dataclose[0])
                self.blueline = False
                self.redline = False
                # Keep track of the created order to avoid a 2nd order
                self.order = self.sell()
    
    if __name__ == '__main__':
    # Create a cerebro entity
    cerebro = bt.Cerebro()
    
    # Add a strategy
    cerebro.addstrategy(TestStrategy)
    
    # 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(sys.argv[0]))
    datapath = os.path.join(modpath, 'TSLA-USD.csv')
    
    # Create a Data Feed
    data = bt.feeds.GenericCSVData(
        dataname=datapath,
        # Do not pass values before this date
        fromdate=datetime.datetime(2008, 4, 4),
        # Do not pass values before this date
        todate=datetime.datetime(2016, 12, 2),
    
    	nullvalue=0.0,
    
    	dtformat=('%m/%d/%Y'),
    
    	datetime=0,
    	high=2,
    	low=3,
    	open=1,
    	close=4,
    	volume=5,
    	openinterest=-1)
    
    # Add the Data Feed to Cerebro
    cerebro.adddata(data)
    
    # Set our desired cash start
    cerebro.broker.setcash(10000.0)
    
    # Add a FixedSize sizer according to the stake
    cerebro.addsizer(bt.sizers.FixedSize, stake=5)
    
    # Set the commission
    cerebro.broker.setcommission(commission=0.002)
    
    # 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()
    

    Also, you can just use this script and paste it into your browsers to grab quick market cvs data from yahoo.

    http://ichart.finance.yahoo.com/table.csv?s=ENTERSTOCKHERE
    

    I will be posting more simple examples and hope others do as well. I found backtesting ideas with Backtrader can be quick and when compared to using online backtesters and more powerful. Using the right parameters without oversampling is still a difficult thing to master. Hope to keep learning and trying new things!


  • administrators

    Thx for sharing. Really impressive.



  • Thanks for the post. You used period of 20 for Bband indicator. How did you come to this value?



  • @ab_trader I just wanted to take the past 30 days of data and use that to find the average low and highs for the BBand and trade on 1 day ticks. When backtesting, 20 days of data seemed to produce better results. I also found this to be true on Quantopian when I added the strategy there and tried many different stocks on 1 day ticks with 20 days of data.



  • Thanks for sharing,
    I tried executing this, the Buy orders are getting created but not executed.
    The log says
    2016-09-08, Close, 8952.50
    2016-09-08, BUY CREATE, 8952.50
    2016-09-08, BUY CREATE, 8952.50
    2016-09-09, BUY EXECUTED, Price: 0.00, Cost: 0.00, Comm 0.00
    2016-09-09, BUY EXECUTED, Price: 0.00, Cost: 0.00, Comm 0.00
    2016-09-09, Close, 8866.70

    0_1483957434236_upload-a56ff59d-2c7b-47e6-b68e-282573b88df6
    Any idea why this might be happening


  • administrators

    The code from @tim3lord will print EXECUTED even when the order has been canceled (or rejected because it cannot meet cash requirements). See the following lines

            if order.status in [order.Completed, order.Canceled, order.Margin]:
                if order.isbuy():
                    self.log(
                        'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                        (order.executed.price,
                         order.executed.value,
                         order.executed.comm))
    

    Of course this is purely cosmetic.

    The code also contains

    # Set our desired cash start
    cerebro.broker.setcash(10000.0)
    
    # Add a FixedSize sizer according to the stake
    cerebro.addsizer(bt.sizers.FixedSize, stake=5)
    

    Your asset is in the 8000s range and for a stocklike behavior you would need at least 5 * 8000 = 40,000 monetary units.

    The orders are obviously being rejected due to lack of enough cash.



  • Awesome, I changed the setcash parameter and it works now, in fact showing some profits by changing the BB parameter.
    I will be sharing something soon, still trying to get my strategy coded in backtrader framework (as I am new to python)


Log in to reply
 

Looks like your connection to Backtrader Community was lost, please wait while we try to reconnect.