Cyptocurrency Trading Bot Part 3 - Adding Backtesting and Support for RSI Indicator

in #cryptocurrency2 years ago (edited)

Step 1 – Code Clean Up

After the previous part, we have some functional code to give us a look at the MACD indicator for a particular trading pair, but the code is not particularly useful for a number of reasons. If we wanted to add additional indicators or strategies, the duplicate code would stack up very quickly, not practical. We’ll instead create a new Strategy class which will handle all of the indicator calculation, strategy calculation (finding buy and sell points), and plotting. With this change our complete code now becomes:

from binance.client import Client
import talib as ta
import matplotlib.pyplot as plt
import numpy as np
from datetime import datetime

class Trader:
    def __init__(self, file):
        self.connect(file)

    """ Creates Binance client """
    def connect(self,file):
        lines = [line.rstrip('\n') for line in open(file)]
        key = lines[0]
        secret = lines[1]
        self.client = Client(key, secret)

    """ Gets all account balances """
    def getBalances(self):
        prices = self.client.get_withdraw_history()
        return prices

class Strategy:

    def __init__(self, indicator_name, strategy_name, pair, interval, klines):
        self.indicator = indicator_name
        self.strategy = strategy_name
        self.pair = pair
        self.interval = interval
        self.klines = klines
        self.indicator_result = self.calculateIndicator()
        self.strategy_result = self.calculateStrategy()


    def calculateIndicator(self):
        if self.indicator == 'MACD':
            close = [float(entry[4]) for entry in self.klines]
            close_array = np.asarray(close)

            macd, macdsignal, macdhist = ta.MACD(close_array, fastperiod=12, slowperiod=26, signalperiod=9)
            return [macd, macdsignal, macdhist]

        else:
            return None


    def calculateStrategy(self):
        if self.indicator == 'MACD':
            if self.strategy == 'CROSS':
                open_time = [int(entry[0]) for entry in self.klines]
                new_time = [datetime.fromtimestamp(time / 1000) for time in open_time]
                crosses = []
                macdabove = False
                for i in range(len(self.indicator_result[0])):
                    if np.isnan(self.indicator_result[0][i]) or np.isnan(self.indicator_result[1][i]):
                        pass
                    else:
                        if self.indicator_result[0][i] > self.indicator_result[1][i]:
                            if macdabove == False:
                                macdabove = True
                                cross = [new_time[i],self.indicator_result[0][i] , 'go']
                                crosses.append(cross)
                        else:
                            if macdabove == True:
                                macdabove = False
                                cross = [new_time[i], self.indicator_result[0][i], 'ro']
                                crosses.append(cross)
                return crosses

            else:
                return None
        else:
            return None



    def plotIndicator(self):
        if self.indicator == 'MACD':
            open_time = [int(entry[0]) for entry in klines]
            new_time = [datetime.fromtimestamp(time / 1000) for time in open_time]
            plt.style.use('dark_background')
            plt.plot(new_time, self.indicator_result[0], label='MACD')
            plt.plot(new_time, self.indicator_result[1], label='MACD Signal')
            plt.plot(new_time, self.indicator_result[2], label='MACD Histogram')
            for entry in self.strategy_result:
                plt.plot(entry[0], entry[1], entry[2])
            title = "MACD Plot for " + self.pair + " on " + self.interval
            plt.title(title)
            plt.xlabel("Open Time")
            plt.ylabel("Value")
            plt.legend()
            plt.show()

        else:
            pass




filename = 'credentials.txt'
trader = Trader(filename)
trading_pair = 'BTCUSDT'
interval = '1d'
klines = trader.client.get_klines(symbol=trading_pair,interval=interval)
macd_strategy = Strategy('MACD','CROSS',trading_pair,interval,klines)
macd_strategy.plotIndicator()


This is much more efficient, and will for us to expand the functionality of our script with ease in the future.

Step 2 – Backtesting

Moving forward, backtesting will be a crucial part of this project, and it starts right here. We’re going to create a simple backtesting class with init parameters for starting amount and a strategy object, and an output with ending amount, percent change, number of trades, percentage of profitable trades, and a detailed report of each trade executed. After adding this functionality (and some more comments for readability), our code looks like this:

from binance.client import Client
import talib as ta
import matplotlib.pyplot as plt
import numpy as np
from datetime import datetime

class Trader:
    def __init__(self, file):
        self.connect(file)

    """ Creates Binance client """
    def connect(self,file):
        lines = [line.rstrip('\n') for line in open(file)]
        key = lines[0]
        secret = lines[1]
        self.client = Client(key, secret)

    """ Gets all account balances """
    def getBalances(self):
        prices = self.client.get_withdraw_history()
        return prices

class Strategy:

    def __init__(self, indicator_name, strategy_name, pair, interval, klines):
        #Name of indicator
        self.indicator = indicator_name
        #Name of strategy being used
        self.strategy = strategy_name
        #Trading pair
        self.pair = pair
        #Trading interval
        self.interval = interval
        #Kline data for the pair on given interval
        self.klines = klines
        #Calculates the indicator
        self.indicator_result = self.calculateIndicator()
        #Uses the indicator to run strategy
        self.strategy_result = self.calculateStrategy()


    '''
    Calculates the desired indicator given the init parameters
    '''
    def calculateIndicator(self):
        if self.indicator == 'MACD':
            close = [float(entry[4]) for entry in self.klines]
            close_array = np.asarray(close)

            macd, macdsignal, macdhist = ta.MACD(close_array, fastperiod=12, slowperiod=26, signalperiod=9)
            return [macd, macdsignal, macdhist]

        else:
            return None


    '''
    Runs the desired strategy given the indicator results
    '''
    def calculateStrategy(self):
        if self.indicator == 'MACD':

            if self.strategy == 'CROSS':
                open_time = [int(entry[0]) for entry in self.klines]
                new_time = [datetime.fromtimestamp(time / 1000) for time in open_time]
                self.time = new_time
                crosses = []
                macdabove = False
                #Runs through each timestamp in order
                for i in range(len(self.indicator_result[0])):
                    if np.isnan(self.indicator_result[0][i]) or np.isnan(self.indicator_result[1][i]):
                        pass
                    #If both the MACD and signal are well defined, we compare the 2 and decide if a cross has occured
                    else:
                        if self.indicator_result[0][i] > self.indicator_result[1][i]:
                            if macdabove == False:
                                macdabove = True
                                #Appends the timestamp, MACD value at the timestamp, color of dot, buy signal, and the buy price
                                cross = [new_time[i],self.indicator_result[0][i] , 'go', 'BUY', self.klines[i][4]]
                                crosses.append(cross)
                        else:
                            if macdabove == True:
                                macdabove = False
                                #Appends the timestamp, MACD value at the timestamp, color of dot, sell signal, and the sell price
                                cross = [new_time[i], self.indicator_result[0][i], 'ro', 'SELL', self.klines[i][4]]
                                crosses.append(cross)
                return crosses

            else:
                return None
        else:
            return None

    '''
    Getter for the strategy result
    '''
    def getStrategyResult(self):
        return self.strategy_result

    '''
    Getter for the klines
    '''
    def getKlines(self):
        return self.klines

    '''
    Getter for the trading pair
    '''
    def getPair(self):
        return self.pair

    '''
    Getter for the trading interval
    '''
    def getInterval(self):
        return self.interval

    '''
    Getter for the time list
    '''
    def getTime(self):
        return self.time

    '''
    Plots the desired indicator with strategy buy and sell points
    '''
    def plotIndicator(self):
        if self.indicator == 'MACD':
            open_time = [int(entry[0]) for entry in klines]
            new_time = [datetime.fromtimestamp(time / 1000) for time in open_time]
            plt.style.use('dark_background')
            plt.plot(new_time, self.indicator_result[0], label='MACD')
            plt.plot(new_time, self.indicator_result[1], label='MACD Signal')
            plt.plot(new_time, self.indicator_result[2], label='MACD Histogram')
            for entry in self.strategy_result:
                plt.plot(entry[0], entry[1], entry[2])
            title = "MACD Plot for " + self.pair + " on " + self.interval
            plt.title(title)
            plt.xlabel("Open Time")
            plt.ylabel("Value")
            plt.legend()
            plt.show()

        else:
            pass

class Backtest:
    def __init__(self, starting_amount, start_datetime, end_datetime, strategy):
        #Starting amount
        self.start = starting_amount
        #Number of trades
        self.num_trades = 0
        #Number of profitable trades
        self.profitable_trades = 0
        #Running amount
        self.amount = self.start
        #Start of desired interval
        self.startTime = start_datetime
        #End of desired interval
        self.endTime = end_datetime
        #Strategy object
        self.strategy = strategy
        #Trading pair
        self.pair = self.strategy.getPair()
        #Trading interval
        self.interval = self.strategy.getInterval()
        #Outputs the trades exectued
        self.trades = []
        #Runs the backtest
        self.results = self.runBacktest()
        #Prints the results
        self.printResults()


    def runBacktest(self):
        amount = self.start
        klines = self.strategy.getKlines()
        time = self.strategy.getTime()
        point_finder = 0
        strategy_result = self.strategy.getStrategyResult()
        #Finds the first cross point within the desired backtest interval
        while strategy_result[point_finder][0] < self.startTime:
            point_finder += 1
        #Initialize to not buy
        active_buy = False
        buy_price = 0
        #Runs through each kline
        for i in range(len(klines)):
            if point_finder > len(strategy_result)-1:
                break
            #If timestamp is in the interval, check if strategy has triggered a buy or sell
            if time[i] > self.startTime and time[i] < self.endTime:
                if(time[i] == strategy_result[point_finder][0]):
                    if strategy_result[point_finder][3] == 'BUY':
                        active_buy = True
                        buy_price = float(strategy_result[point_finder][4])
                        self.trades.append(['BUY', buy_price])
                    if strategy_result[point_finder][3] == 'SELL' and active_buy == True:
                        active_buy = False
                        bought_amount = amount / buy_price
                        self.num_trades += 1
                        if(float(strategy_result[point_finder][4]) > buy_price):
                            self.profitable_trades += 1
                        amount = bought_amount * float(strategy_result[point_finder][4])
                        self.trades.append(['SELL', float(strategy_result[point_finder][4])])
                    point_finder += 1
        self.amount = amount

    '''
    Prints the results of the backtest
    '''
    def printResults(self):
        print("Trading Pair: " + self.pair)
        print("Interval: " + self.interval)
        print("Ending amount: " + str(self.amount))
        print("Number of Trades: " + str(self.num_trades))
        profitable = self.profitable_trades / self.num_trades * 100
        print("Percentage of Profitable Trades: " + str(profitable) + "%")
        percent = self.amount / self.start * 100
        print(str(percent) + "% of starting amount")
        for entry in self.trades:
            print(entry[0] + " at " + str(entry[1]))



filename = 'credentials.txt'
trader = Trader(filename)
trading_pair = 'BTCUSDT'
interval = '1d'
klines = trader.client.get_klines(symbol=trading_pair,interval=interval)
macd_strategy = Strategy('MACD','CROSS',trading_pair,interval,klines)
#macd_strategy.plotIndicator()
time = macd_strategy.getTime()
macd_backtest = Backtest(10000, time[0], time[len(time)-1], macd_strategy)


And the output of running the program becomes:

Trading Pair: BTCUSDT
Interval: 1d
Ending amount: 27000.907633194856
Number of Trades: 8
Percentage of Profitable Trades: 62.5%
270.00907633194856% of starting amount
BUY at 4193.0
SELL at 5477.03
BUY at 6463.0
SELL at 6506.98
BUY at 7699.19
SELL at 16488.98
BUY at 17069.79
SELL at 14400.0
BUY at 11879.95
SELL at 10237.51
BUY at 8063.88
SELL at 9271.64
BUY at 8898.03
SELL at 7795.51
BUY at 7018.0
SELL at 9187.56

Not bad for a beginner strategy, but then again, it was pretty difficult to lose money when investing in Bitcoin back in October, and a 62.5% profitable trade rate is far from desirable. Let’s experiment with a few different trading pairs, trading intervals, and date ranges and look at the results:

Trading Pair: BTCUSDT
Interval: 1h
Ending amount: 9587.746001592644
Number of Trades: 41
Percentage of Profitable Trades: 36.58536585365854
95.87746001592645% of starting amount
BUY at 6887.99
SELL at 6923.99
BUY at 7099.85
SELL at 6980.0
BUY at 7120.0
SELL at 6782.0
BUY at 6740.11
SELL at 6820.03
BUY at 6851.97
SELL at 6776.4
BUY at 7690.0
SELL at 7832.99
BUY at 8193.49
SELL at 8096.0
BUY at 8077.96
SELL at 7999.01
BUY at 8067.87
SELL at 8295.99
BUY at 8350.0
SELL at 8321.37
BUY at 8017.0
SELL at 7945.17
BUY at 8125.0
SELL at 8166.97
BUY at 8230.89
SELL at 8239.02
BUY at 8259.08
SELL at 8249.95
BUY at 8350.56
SELL at 8240.99
BUY at 8488.01
SELL at 8705.01
BUY at 8940.03
SELL at 8853.97
BUY at 8942.39
SELL at 8911.54
BUY at 8934.01
SELL at 9347.99
BUY at 8841.0
SELL at 8720.5
BUY at 8845.02
SELL at 9288.88
BUY at 9163.03
SELL at 9285.47
BUY at 9405.45
SELL at 9275.0
BUY at 9318.06
SELL at 9218.39
BUY at 9039.0
SELL at 8896.63
BUY at 8999.99
SELL at 9210.09
BUY at 9471.05
SELL at 9649.12
BUY at 9839.99
SELL at 9811.84
BUY at 9862.04
SELL at 9902.0
BUY at 9652.75
SELL at 9350.0
BUY at 9362.01
SELL at 9295.0
BUY at 9190.0
SELL at 9066.0
BUY at 9112.24
SELL at 9292.17
BUY at 9369.0
SELL at 9340.0
BUY at 8606.93
SELL at 8422.99
BUY at 8450.01
SELL at 8264.68
BUY at 8311.0
SELL at 8230.0
BUY at 8501.83
SELL at 8614.99
BUY at 8680.0
SELL at 8657.31
BUY at 8762.18
SELL at 8740.75
BUY at 8350.26
SELL at 8243.51
Trading Pair: ETHUSDT
Interval: 15m
Ending amount: 10063.292422919483
Number of Trades: 36
Percentage of Profitable Trades: 38.88888888888889%
100.63292422919483% of starting amount
BUY at 732.88
SELL at 751.42
BUY at 762.15
SELL at 769.15
BUY at 739.92
SELL at 745.67
BUY at 724.0
SELL at 742.5
BUY at 758.5
SELL at 753.33
BUY at 753.0
SELL at 750.91
BUY at 755.3
SELL at 754.75
BUY at 759.95
SELL at 762.01
BUY at 765.55
SELL at 762.0
BUY at 764.95
SELL at 762.0
BUY at 765.9
SELL at 762.43
BUY at 764.33
SELL at 752.82
BUY at 737.65
SELL at 729.58
BUY at 728.95
SELL at 723.98
BUY at 679.46
SELL at 672.03
BUY at 677.74
SELL at 677.78
BUY at 648.53
SELL at 640.05
BUY at 648.6
SELL at 666.55
BUY at 665.83
SELL at 672.77
BUY at 679.0
SELL at 680.0
BUY at 677.21
SELL at 697.79
BUY at 702.7
SELL at 718.0
BUY at 704.99
SELL at 698.37
BUY at 725.91
SELL at 731.95
BUY at 731.02
SELL at 727.38
BUY at 727.94
SELL at 729.19
BUY at 735.82
SELL at 727.38
BUY at 725.43
SELL at 716.93
BUY at 713.48
SELL at 703.33
BUY at 706.95
SELL at 692.15
BUY at 687.63
SELL at 686.57
BUY at 699.67
SELL at 694.42
BUY at 697.6
SELL at 694.05
BUY at 698.68
SELL at 710.6
BUY at 700.54
SELL at 696.84
BUY at 700.55
SELL at 695.1

These look a little more as expected from a beginner strategy. No need to panic just yet though, we are far from being done with our trading strategy development.

Moving forward it would be nice to have a stable backtesting metric to compare strategies against each other. The metric that I’ll most commonly be using to compare strategies is avg. daily percent gain for a strategy on the 15 minute interval over the last 100 days. To do this we’ll need to pull some older data, which can be done using the get_historical_klines function in the Binance API. After we add our RSI indicator, we will test it using this metric in the next issue.

Step 4 – Adding RSI Strategy

The relative strength index (RSI) is a momentum indicator developed by noted technical analyst Welles Wilder, that compares the magnitude of recent gains and losses over a specified time period to measure speed and change of price movements of a security. It is primarily used to attempt to identify overbought or oversold conditions in the trading of an asset (definition from Investopedia. After our earlier code cleanup, adding a function for this new indicator is quite simple, the calculateIndicator function becomes:

if self.indicator == 'MACD':
    close = [float(entry[4]) for entry in self.klines]
    close_array = np.asarray(close)

    macd, macdsignal, macdhist = ta.MACD(close_array, fastperiod=12, slowperiod=26, signalperiod=9)
    return [macd, macdsignal, macdhist]

elif self.indicator == 'RSI':
    close = [float(entry[4]) for entry in self.klines]
    close_array = np.asarray(close)

    rsi = ta.RSI(close_array, timeperiod=14)
    return rsi

else:
    return None

calculateStrategy is:

if self.indicator == 'MACD':

    if self.strategy == 'CROSS':
        open_time = [int(entry[0]) for entry in self.klines]
        new_time = [datetime.fromtimestamp(time / 1000) for time in open_time]
        self.time = new_time
        crosses = []
        macdabove = False
        #Runs through each timestamp in order
        for i in range(len(self.indicator_result[0])):
            if np.isnan(self.indicator_result[0][i]) or np.isnan(self.indicator_result[1][i]):
                pass
            #If both the MACD and signal are well defined, we compare the 2 and decide if a cross has occured
            else:
                if self.indicator_result[0][i] > self.indicator_result[1][i]:
                    if macdabove == False:
                        macdabove = True
                        #Appends the timestamp, MACD value at the timestamp, color of dot, buy signal, and the buy price
                        cross = [new_time[i],self.indicator_result[0][i] , 'go', 'BUY', self.klines[i][4]]
                        crosses.append(cross)
                else:
                    if macdabove == True:
                        macdabove = False
                        #Appends the timestamp, MACD value at the timestamp, color of dot, sell signal, and the sell price
                        cross = [new_time[i], self.indicator_result[0][i], 'ro', 'SELL', self.klines[i][4]]
                        crosses.append(cross)
        return crosses

    else:
        return None
elif self.indicator == 'RSI':
    if self.strategy == '7030':
        open_time = [int(entry[0]) for entry in self.klines]
        new_time = [datetime.fromtimestamp(time / 1000) for time in open_time]
        self.time = new_time
        result = []
        active_buy = False
        # Runs through each timestamp in order
        for i in range(len(self.indicator_result)):
            if np.isnan(self.indicator_result[i]):
                pass
            # If the RSI is well defined, check if over 70 or under 30
            else:
                if float(self.indicator_result[i]) < 30 and active_buy == False:
                    # Appends the timestamp, RSI value at the timestamp, color of dot, buy signal, and the buy price
                    entry = [new_time[i], self.indicator_result[i], 'go', 'BUY', self.klines[i][4]]
                    result.append(entry)
                    active_buy = True
                elif float(self.indicator_result[i]) > 70 and active_buy == True:
                    # Appends the timestamp, RSI value at the timestamp, color of dot, sell signal, and the sell price
                    entry = [new_time[i], self.indicator_result[i], 'ro', 'SELL', self.klines[i][4]]
                    result.append(entry)
                    active_buy = False
        return result
    elif self.strategy == '8020':
        open_time = [int(entry[0]) for entry in self.klines]
        new_time = [datetime.fromtimestamp(time / 1000) for time in open_time]
        self.time = new_time
        result = []
        active_buy = False
        # Runs through each timestamp in order
        for i in range(len(self.indicator_result)):
            if np.isnan(self.indicator_result[i]):
                pass
            # If the RSI is well defined, check if over 80 or under 20
            else:
                if float(self.indicator_result[i]) < 20 and active_buy == False:
                    # Appends the timestamp, RSI value at the timestamp, color of dot, buy signal, and the buy price
                    entry = [new_time[i], self.indicator_result[i], 'go', 'BUY', self.klines[i][4]]
                    result.append(entry)
                    active_buy = True
                elif float(self.indicator_result[i]) > 80 and active_buy == True:
                    # Appends the timestamp, RSI value at the timestamp, color of dot, sell signal, and the sell price
                    entry = [new_time[i], self.indicator_result[i], 'ro', 'SELL', self.klines[i][4]]
                    result.append(entry)
                    active_buy = False
        return result


else:
    return None

And plotIndicator is:

open_time = [int(entry[0]) for entry in klines]
new_time = [datetime.fromtimestamp(time / 1000) for time in open_time]
plt.style.use('dark_background')
for entry in self.strategy_result:
    plt.plot(entry[0], entry[1], entry[2])
if self.indicator == 'MACD':
    plt.plot(new_time, self.indicator_result[0], label='MACD')
    plt.plot(new_time, self.indicator_result[1], label='MACD Signal')
    plt.plot(new_time, self.indicator_result[2], label='MACD Histogram')

elif self.indicator == 'RSI':
    plt.plot(new_time, self.indicator_result, label='RSI')

else:
    pass

title = self.indicator + " Plot for " + self.pair + " on " + self.interval
plt.title(title)
plt.xlabel("Open Time")
plt.ylabel("Value")
plt.legend()
plt.show()

We have implemented 2 RSI trading strategies, one with the bounds of 70 and 30, and the other with bounds of 80 and 20. There are different situations where each of these bounds are useful so we have implemented both. Running a simple plot with the DGDBTC pair on 15m we get the following:
dgdbtcrsi.png
Figure 1: Sample RSI Plot

This plot looks accurate compared to TradingView, but is a little messy and may not give the best trading strategy right off the bat. Due to the length of this post, we will be stopping here for now. In the next installment we’ll be testing out the RSI strategy making adjustments if necessary, combining the MACD and RSI strategies, adding stop losses, and setting up our bot to run in real time.

As always, any questions, comments, concerns, general feedback, or statements of well being are always appreciated.

Thanks for reading,
Andrew

Sort:  

Great Work. I built a screener using CCXT and will be studying your work. I will post details when I am done here.

Congratulations @genesiscrypto! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 1 year!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Do not miss the last post from @steemitboard:

New japanese speaking community Steem Meetup badge
Vote for @Steemitboard as a witness to get one more award and increased upvotes!