Algorithmic Trading (Part 1): Backtesting an RSI Strategy

exxonmobil-algo-results

This post is part of a series.
Part 2 can be found here.

The above chart was generated in Python. It’s the result of backtesting a basic algorithmic trading strategy that makes use of the Relative Strength Index (RSI). In this tutorial I’ll walk through implementing and graphing a simple strategy. The tutorial should provide a framework that will allow coders to swap out code segments to include strategies and indicators of their preference. It’s meant more to model the beginnings of a customizable backtesting platform than a successful strategy.

 Libraries

There is a small set of backtesting libraries to choose from, but I used Zipline as the backbone for backtesting strategies. Zipline also underpins Quantopian, an algorithmic trading platform and community that allows traders to plug their algorithms into a polished interface. I chose not to use Quantopian because I wanted the freedom to work outside of their framework, but everything I’ve done thus far could likely be done in Quantopian with ease.

The python wrapper for TA-Lib allows users to painlessly calculate technical indicators.

All of the plotting was done with the old standard: Matplotlib.

 A Strategy-Independent Dashboard

Creating a separate class to run these strategies allows you to swap and compare algorithms easily. This “dashboard” class is used primarily to import libraries, establish a timeframe, and select a security. The security used as an example is ExxonMobil, which has had its fair share of ups and downs in the given timeframe.

Creating the ‘algo’ object involves references to a second class where the strategy is actually held. The details of that class are discussed later.

#All the necessary libraries
import talib as ta
import pandas.io.data as web
import matplotlib.pyplot as plt
from matplotlib.dates import date2num
from matplotlib.patches import Rectangle
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import numpy
import warnings
from datetime import datetime
import logbook
from logbook import Logger
log = Logger('Algorithm')
import pytz
from zipline.algorithm import TradingAlgorithm
from zipline.utils.factory import load_from_yahoo
from zipline.api import order, symbol, get_order, record
import pylab as pl

warnings.filterwarnings("ignore")

#Choosing a security and a time horizon
logbook.StderrHandler().push_application()
start = datetime(2012, 9, 1, 0, 0, 0, 0, pytz.utc)
end = datetime(2016, 1, 1, 0, 0, 0, 0, pytz.utc)
sec = 'XOM'
data = load_from_yahoo(stocks= [sec], indexes={}, start=start,
                        end=end, adjusted = True)

data = data.dropna()
algo = TradingAlgorithm(initialize= RSI_Strategy.initialize,
                            handle_data= RSI_Strategy.handle_data,
                            analyze= RSI_Strategy.analyze)
results = algo.run(data)

Implementing a Strategy

Before I go through the individual functions, here’s the class in its entirety:

from datetime import datetime, timedelta

class RSI_Strategy:

    def initialize(context):
        context.test = 1
        context.stock = symbol(sec)
        context.RSI_upper = 65
        context.RSI_lower = 35
        #Flag for when the RSI is in the overbought region
        context.RSI_OB = False
        #Flag for when the RSI is in the oversold region
        context.RSI_OS = False
        context.buy = False
        context.sell = False

    def handle_data(context, data):
        try:
            trailing_window = data.history(context.stock, 'price', 35, '1d')
        except:
            return

        crossover_flag = False
        RSI = ta.RSI(trailing_window.values, 14)

        #Setting flags
        if(RSI[-1] > context.RSI_upper):
            context.RSI_OB = True

        elif(RSI[-1] < context.RSI_lower):
            context.RSI_OS = True

        if(RSI[-1] < context.RSI_upper and context.RSI_OB):
            context.RSI_OB = False
            crossover_flag = True

        elif(RSI[-1] > context.RSI_lower and context.RSI_OS):
            context.RSI_OS = False
            crossover_flag = True

        #Trading Logic
        if(crossover_flag and RSI[-1] < 50 and not context.buy):
            context.order_target(context.stock, 100)
            context.buy = True

        if(crossover_flag and RSI[-1] > 50 and not context.sell):
            context.order_target(context.stock, -100)
            context.sell = True

        if(context.buy and RSI[-1] >= 50):
            context.order_target(context.stock, 0)
            context.buy = False
            context.sell = False

        if(context.sell and RSI[-1] <= 50):
            context.order_target(context.stock, 0)
            context.buy = False
            context.sell = False

         #Recording Results
        record(security=data[symbol(sec)].price,
            RSI = RSI[-1],
            buy = context.buy,
            sell = context.sell) 

    def analyze(context, perf):
        fig = plt.figure()

        #Set up a plot of the portfolio value
        ax1 = plt.subplot2grid((8,1), (0,0), rowspan=3, colspan=1)
        perf.portfolio_value.plot(ax=ax1)
        ax1.set_ylabel('Portfolio Value ($)')

        #Set up a plot of the security value
        ax2 = plt.subplot2grid((8,1), (3,0), rowspan=3, colspan=1, sharex=ax1)
        data.plot(ax=ax2)
        ax2.set_ylabel(sec + ' Value ($)')

        #Find transaction points
        perf_trans = perf.ix[[t != [] for t in perf.transactions]]
        buys = perf_trans.ix[[t[0]['amount'] > 0 for t in perf_trans.transactions]]
        sells = perf_trans.ix[[t[0]['amount'] < 0 for t in perf_trans.transactions]]

        #Plot the colored box of the final transaction if the time period ends with an open position
        if(len(buys) != len(sells)):
            if(len(buys) > len(sells)):
                upper_lim = len(sells)
                last_point = mdates.date2num(buys.index[upper_lim])
                col = 'g'
            elif(len(sells) < len(buys)):
                upper_lim = len(buys)
                last_point = mdates.date2num(sells.index[upper_lim])
                col = 'r'
            end_d = mdates.date2num(end)
            width = mdates.date2num(end) - last_point
            rect = Rectangle((last_point, 60), width, 40, color=col, alpha = 0.3)
            ax2.add_patch(rect)
        else:
            upper_lim= len(buys)

        short_patch = mpatches.Patch(color='r', alpha = 0.3, label='Short Holdings')
        long_patch = mpatches.Patch(color='g', alpha = 0.3, label='Long Holdings')
        plt.legend(handles=[long_patch, short_patch])
        plt.setp(plt.gca().get_legend().get_texts(), fontsize='8')        

        #Plot the colored box of all other transactions
        for i in range(0,upper_lim):
            buy_d = mdates.date2num(buys.index[i])
            sell_d = mdates.date2num(sells.index[i])
            if(buy_d < sell_d):
                col = 'g'
                width = sell_d - buy_d
                rect = Rectangle((buy_d, 60), width, 40, color=col, alpha = 0.3)
            else:
                col = 'r'
                width = buy_d - sell_d
                rect = Rectangle((sell_d, 60), width, 40, color=col, alpha = 0.3)
            ax2.add_patch(rect)

        #Plot the RSI with proper lines and shading
        ax3 = plt.subplot2grid((8,1), (6,0), rowspan=2, colspan=1, sharex=ax1)
        ax3.plot(perf['RSI'])
        ax3.fill_between(perf.index,perf['RSI'],context.RSI_upper,
                         where = perf['RSI'] >= context.RSI_upper, alpha = 0.5, color='r')
        ax3.fill_between(perf.index,perf['RSI'],context.RSI_lower,
                         where = perf['RSI'] <= context.RSI_lower, alpha = 0.5, color = 'g')
        ax3.plot((date2num(perf.index[0]),date2num(perf.index[-1])),
                           (context.RSI_upper,context.RSI_upper), color='r', alpha = 0.5)
        ax3.plot((date2num(perf.index[0]),date2num(perf.index[-1])),
                           (context.RSI_lower,context.RSI_lower),color='g', alpha = 0.5)
        ax3.grid(True)
        ax3.set_ylabel('RSI')

        OB_patch = mpatches.Patch(color='r', alpha = 0.5, label='Overbought')
        OS_patch = mpatches.Patch(color='g', alpha = 0.5, label='Oversold')
        plt.legend(loc = 'upper left',handles=[OB_patch, OS_patch])
        plt.setp(plt.gca().get_legend().get_texts(), fontsize='8')

        plt.tight_layout()
        plt.show()

Zipline provides a series of helpful backtesting functions, three of which are utilized above: ‘context’, ‘handle_data’, and ‘analyze’. The ‘context’ function initializes the algorithm. It also serves as a good place to store any variables you want to be local to the algorithm. Initializing them in ‘handle_data’ instead means they’d be re-initialized at each iteration your algorithm goes through. This is particularly important for flag variables like “RSI_OB” or “RSI_OS”

    def initialize(context):
        context.test = 1
        context.stock = symbol(sec)
        context.RSI_upper = 65
        context.RSI_lower = 35
        #Flag for when the RSI is in the overbought region
        context.RSI_OB = False
        #Flag for when the RSI is in the oversold region
        context.RSI_OS = False
        context.buy = False
        context.sell = False

The ‘handle_data’ function is where the strategy is really built. To provide a brief overview: the algorithm makes a trade when the RSI leaves either of the extreme regions (overbought or oversold, as indicated by the shaded regions of the RSI). It buys the security when the RSI exits the ‘oversold’ period (defined in the ‘context’ function as anything under 35), and shorts the security when the RSI exits the ‘overbought’ period (anything over 65). As soon as the RSI hits 50, the algorithm closes whichever position is took and moves to cash.

The biggest flaw of the strategy is that it doesn’t recognize when the security has re-entered the overbought or oversold regions. You’ll notice it takes its biggest losses when the RSI re-enters those regions before hitting 50. A simple solution for this would be to move to cash if the the RSI enters into an extreme region while a position is open. Since this algorithm is meant to be more demonstrative than actionable, adding in this extra layers would really just serve to make the code more confusing and uninterpretable.

The first chunk of the ‘handle_data’ function determines whether the function is in extreme territory, or if it has just crossed back into the “normal” 35-65 ranged. The second chunk takes a position if the security has moved out of an extreme, or moves to cash if the RSI has settled back to 50.

    def handle_data(context, data):
        try:
            trailing_window = data.history(context.stock, 'price', 35, '1d')
        except:
            return

        crossover_flag = False
        RSI = ta.RSI(trailing_window.values, 14)

        #Setting flags
        if(RSI[-1] > context.RSI_upper):
            context.RSI_OB = True

        elif(RSI[-1] < context.RSI_lower):
            context.RSI_OS = True

        if(RSI[-1] < context.RSI_upper and context.RSI_OB):
            context.RSI_OB = False
            crossover_flag = True

        elif(RSI[-1] > context.RSI_lower and context.RSI_OS):
            context.RSI_OS = False
            crossover_flag = True

        #Trading Logic
        if(crossover_flag and RSI[-1] < 50 and not context.buy):
            context.order_target(context.stock, 100)
            context.buy = True

        if(crossover_flag and RSI[-1] > 50 and not context.sell):
            context.order_target(context.stock, -100)
            context.sell = True

        if(context.buy and RSI[-1] >= 50):
            context.order_target(context.stock, 0)
            context.buy = False
            context.sell = False

        if(context.sell and RSI[-1] <= 50):
            context.order_target(context.stock, 0)
            context.buy = False
            context.sell = False

         #Recording Results
        record(security=data[symbol(sec)].price,
            RSI = RSI[-1],
            buy = context.buy,
            sell = context.sell)

The ‘analyze’ function is used after the algorithm has run. The “perf” variable in its function header stands for the performance of the algorithm. This function is used to report and plot the results of the algorithm. The three paned image shown at the top of the post was created here. Most of the plotting is relatively straight-forward, with the exception of creating those green and red long/short rectangles on the second graph. Since it’s possible that the algorithm ends its backtest with an open short or long position, the ‘analyze’ function has to account for that before plotting the rest of the rectangles.

    def analyze(context, perf):
        fig = plt.figure()

        #Set up a plot of the portfolio value
        ax1 = plt.subplot2grid((8,1), (0,0), rowspan=3, colspan=1)
        perf.portfolio_value.plot(ax=ax1)
        ax1.set_ylabel('Portfolio Value ($)')

        #Set up a plot of the security value
        ax2 = plt.subplot2grid((8,1), (3,0), rowspan=3, colspan=1, sharex=ax1)
        data.plot(ax=ax2)
        ax2.set_ylabel(sec + ' Value ($)')

        #Find transaction points
        perf_trans = perf.ix[[t != [] for t in perf.transactions]]
        buys = perf_trans.ix[[t[0]['amount'] > 0 for t in perf_trans.transactions]]
        sells = perf_trans.ix[[t[0]['amount'] < 0 for t in perf_trans.transactions]]

        #Plot the colored box of the final transaction if the time period ends with an open position
        if(len(buys) != len(sells)):
            if(len(buys) > len(sells)):
                upper_lim = len(sells)
                last_point = mdates.date2num(buys.index[upper_lim])
                col = 'g'
            elif(len(sells) > len(buys)):
                upper_lim = len(buys)
                last_point = mdates.date2num(sells.index[upper_lim])
                col = 'r'
            end_d = mdates.date2num(end)
            width = mdates.date2num(end) - last_point
            rect = Rectangle((last_point, 60), width, 40, color=col, alpha = 0.3)
            ax2.add_patch(rect)
        else:
            upper_lim= len(buys)

        short_patch = mpatches.Patch(color='r', alpha = 0.3, label='Short Holdings')
        long_patch = mpatches.Patch(color='g', alpha = 0.3, label='Long Holdings')
        plt.legend(handles=[long_patch, short_patch])
        plt.setp(plt.gca().get_legend().get_texts(), fontsize='8')        

        #Plot the colored box of all other transactions
        for i in range(0,upper_lim):
            buy_d = mdates.date2num(buys.index[i])
            sell_d = mdates.date2num(sells.index[i])
            if(buy_d < sell_d):
                col = 'g'
                width = sell_d - buy_d
                rect = Rectangle((buy_d, 60), width, 40, color=col, alpha = 0.3)
            else:
                col = 'r'
                width = buy_d - sell_d
                rect = Rectangle((sell_d, 60), width, 40, color=col, alpha = 0.3)
            ax2.add_patch(rect)

        #Plot the RSI with proper lines and shading
        ax3 = plt.subplot2grid((8,1), (6,0), rowspan=2, colspan=1, sharex=ax1)
        ax3.plot(perf['RSI'])
        ax3.fill_between(perf.index,perf['RSI'],context.RSI_upper,
                         where = perf['RSI'] >= context.RSI_upper, alpha = 0.5, color='r')
        ax3.fill_between(perf.index,perf['RSI'],context.RSI_lower,
                         where = perf['RSI'] <= context.RSI_lower, alpha = 0.5, color = 'g')
        ax3.plot((date2num(perf.index[0]),date2num(perf.index[-1])),
                           (context.RSI_upper,context.RSI_upper), color='r', alpha = 0.5)
        ax3.plot((date2num(perf.index[0]),date2num(perf.index[-1])),
                           (context.RSI_lower,context.RSI_lower),color='g', alpha = 0.5)
        ax3.grid(True)
        ax3.set_ylabel('RSI')

        OB_patch = mpatches.Patch(color='r', alpha = 0.5, label='Overbought')
        OS_patch = mpatches.Patch(color='g', alpha = 0.5, label='Oversold')
        plt.legend(loc = 'upper left',handles=[OB_patch, OS_patch])
        plt.setp(plt.gca().get_legend().get_texts(), fontsize='8')

        plt.tight_layout()
        plt.show()
Advertisements

2 thoughts on “Algorithmic Trading (Part 1): Backtesting an RSI Strategy

  1. Pingback: Machine Learning for Stock Market Prediction: Global Indices – Keith Selover

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s