Comparing Strategies for Detecting Buy Signal with BackTrader

In the post Bitcoin trading with Python — Bollinger Bands strategy analysis the author reported 34% returns over the initial investment on back test. Does it work all the time? Is it the best strategy?

Let us use backtrader platform and compare several strategies just for buy signal. Backtrader is a feature-rich Python framework for backtesting and trading. Backtrader allows you to focus on writing reusable trading strategies, indicators and analyzers instead of having to spend time building infrastructure.

We will use the following strategies:

Crossover – based on two simple moving averages (SMA) with periods 10 and 30 days. Buy if fast SMA line crosses slow to the upside.

Consecutive 2 prices (Simple1) – Buy if two consecutive prices are decreasing – if current price is less than previous price and previous price is also less than previous price

1
2
3
4
if self.dataclose[0] < self.dataclose[-1]:
                  if self.dataclose[-1] < self.dataclose[-2]:
                       self.log('BUY CREATE {0:8.2f}'.format(self.dataclose[0]))
                       self.order = self.buy()

Consecutive 4 prices (Simple2) – Buy if within 4 price windows we have decrease more than 5% of original price between any two prices from this window:

1
2
3
4
5
6
7
if tr_str == "simple2":
         # since there is no order pending, are we in the market?   
         if not self.position: # not in the market
             if (self.dataclose[0] - self.dataclose[-1]) < -0.05*self.dataclose[0] or (self.dataclose[0] - self.dataclose[-2]) < -0.05*self.dataclose[0] or (self.dataclose[0] - self.dataclose[-3]) < -0.05*self.dataclose[0] or (self.dataclose[0] - self.dataclose[-4]) < -0.05*self.dataclose[0]:
                 #if self.dataclose[-1] < self.dataclose[-2]:
                     self.log('BUY CREATE {0:8.2f}'.format(self.dataclose[0]))
                     self.order = self.buy()

Bollinger Bands – Buy when the price cross the bottom band.

1
2
3
if self.data.close < self.boll.lines.bot:
              self.log('BUY CREATE {0:8.2f}'.format(self.dataclose[0]))
              self.order = self.buy()    

Our starting asset value is 10000. We use daily prices.
After running above strategies we get results like below:

Final Vaues for Strategies

1
2
3
4
cross 9999.06 
simple1 9999.31 
simple2 9999.91 
BB 10011.099999999999 

Thus we see that Bollinger Band looks more promising comparing with other strategies that we tried. However we did not do any optimization – using different parameters to improve performance of strategy. We learned how to set different strategies with backtrader and got understanding how to issue buy signal. Below you can find full source code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# -*- coding: utf-8 -*-
import matplotlib
import matplotlib.pyplot as plt
 
from datetime import datetime
import backtrader as bt
 
matplotlib.use('Qt5Agg')
plt.switch_backend('Qt5Agg')
 
# Create a subclass of Strategy to define the indicators and logic
class SmaCross(bt.Strategy):
    # parameters which are configurable for the strategy
    params = dict(
        pfast=10# period for the fast moving average
        pslow=30,   # period for the slow moving average
    )
     params['tr_strategy'] = None
 
    def __init__(self):
           
        self.boll = bt.indicators.BollingerBands(period=50, devfactor=2)
        self.dataclose= self.datas[0].close    # Keep a reference to
        self.sma1 = bt.ind.SMA(period=self.p.pfast)  # fast moving average
        self.sma2 = bt.ind.SMA(period=self.p.pslow)  # slow moving average
        self.crossover = bt.ind.CrossOver(self.sma1, self.sma2)  # crossover signal
        self.tr_strategy = self.params.tr_strategy
 
    def next(self, strategy_type=""):
        tr_str = self.tr_strategy
        print (self.tr_strategy)
        
        # Log the closing prices of the series
        self.log("Close, {0:8.2f} ".format(self.dataclose[0]))
        self.log('sma1, {0:8.2f}'.format(self.sma1[0]))
         
        if tr_str == "cross":
            if not self.position:  # not in the market
                if self.crossover > 0# if fast crosses slow to the upside
                    self.buy()  # enter long
 
               
        if tr_str == "simple1":
           
            if not self.position: # not in the market
                if self.dataclose[0] < self.dataclose[-1]:
                    if self.dataclose[-1] < self.dataclose[-2]:
                        self.log('BUY CREATE {0:8.2f}'.format(self.dataclose[0]))
                        self.order = self.buy()
                         
        if tr_str == "simple2":
            
            if not self.position: # not in the market
                if (self.dataclose[0] - self.dataclose[-1]) < -0.05*self.dataclose[0] or (self.dataclose[0] - self.dataclose[-2]) < -0.05*self.dataclose[0] or (self.dataclose[0] - self.dataclose[-3]) < -0.05*self.dataclose[0] or (self.dataclose[0] - self.dataclose[-4]) < -0.05*self.dataclose[0]:
                   
                        self.log('BUY CREATE {0:8.2f}'.format(self.dataclose[0]))
                        self.order = self.buy()               
                         
        if tr_str == "BB":
            #if self.data.close > self.boll.lines.top:
            #self.sell(exectype=bt.Order.Stop, price=self.boll.lines.top[0], size=self.p.size)
            if self.data.close < self.boll.lines.bot:
                self.log('BUY CREATE {0:8.2f}'.format(self.dataclose[0]))
                self.order = self.buy()    
                 
        print('Current Portfolio Value: %.2f' % cerebro.broker.getvalue())           
         
    def log(self, txt, dt=None):
        # Logging function for the strategy.  'txt' is the statement and 'dt' can be used to specify a specific datetime
        dt = dt or self.datas[0].datetime.date(0)
        print('{0},{1}'.format(dt.isoformat(),txt))
      
         
    def notify_trade(self,trade):
        if not trade.isclosed:
            return
         
        self.log('OPERATION PROFIT, GROSS {0:8.2f}, NET {1:8.2f}'.format(
            trade.pnl, trade.pnlcomm))   
  
         
strategy_final_values=[0,0,0,0]
strategies = ["cross", "simple1", "simple2", "BB"]
 
 
for tr_strategy in strategies:        
 
    cerebro = bt.Cerebro()  # create a "Cerebro" engine instance
        
    data = bt.feeds.GenericCSVData(
        dataname='GE.csv',
     
        fromdate=datetime(2019, 1, 1),
        todate=datetime(2019, 9, 13),
   
        nullvalue=0.0,
     
        dtformat=('%Y-%m-%d'),
   
        datetime=0,
        high=2,
        low=3,
        open=1,
        close=4,
        adjclose=5,
        volume=6,
        openinterest=-1
       
    )
     
 
    print ("data")
    print (data )
    cerebro.adddata(data)  # Add the data feed
 
    # Print out the starting conditions
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
     
     
    cerebro.addstrategy(SmaCross, tr_strategy=tr_strategy)  # Add the trading strategy
    result=cerebro.run()  # run it all
    figure=cerebro.plot(iplot=False)[0][0
    figure.savefig('example.png')
     
    # Print out the final result
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
    ind=strategies.index(tr_strategy)
    strategy_final_values[ind] = cerebro.broker.getvalue()
     
print ("Final Vaues for Strategies")
for tr_strategy in strategies:
    ind=strategies.index(tr_strategy)
    print ("{} {}  ". format(tr_strategy, strategy_final_values[ind]))    

References
1. Backtrader
2. BackTrader Documentation – Quickstart
3. Backtrader: Bollinger Mean Reversion Strategy
4. Bitcoin trading with Python — Bollinger Bands strategy analysis