Quantopian: RSI Strategy Backtest

Relative Strength Index (RSI) Strategy Backtest

Contact: andrewshamlet@gmail.com // @andrewshamlet

Download the IPython Notebook that accompanies this Tutorial from Github. 

View the Quantopian Backtest here. 

Summary

  • The Relative Strength Index (RSI) is a momentum indicator that compares the magnitude of recent gains and losses over a specified time period. RSI values range from 0 to 100.
  • For this strategy, we buy $FB when the RSI is less than 30, and we will sell $FB when the RSI is greater than 70. The RSI will be calculated at a minutely frequency, as opposed to a daily frequency.
  • During 01/01/16 – 12/31/16,
    • The RSI Strategy produces 32.2% return, resulting in $3,220 pre-tax return.
    • FB Buy & Hold produces 10.0% return, resulting in $1,000 pre-tax return.
    • SPY Buy & Hold produces 12.0% return, resulting in $1,200 pre-tax return.
    • Compared to the SPY Buy & Hold, the RSI Strategy produces $2,220 Alpha whereas FB Buy & Hold produces ($200) Alpha, both on $10,000, principal.
  • During 05/19/12 – 12/31/16,
    • The RSI Strategy produces 147.4% return, resulting in $14,740 pre-tax return.
    • FB Buy & Hold produces 238.5% return, resulting in $23,850 pre-tax return.
    • SPY Buy & Hold produces 89.6% return, resulting in $8,960 pre-tax return.
    • Compared to SPY Buy & Hold, the RSI Strategy produces $5,780 Alpha whereas FB Buy & Hold produces $14,890 Alpha, both on $10,000 principal.
  • Thus, on the broader time horizon, FB Buy & Hold outperforms the RSI Strategy.
  • The question still stands: what about 2016 makes the RSI Strategy superior in performance to FB Buy & Hold?

 

Introduction 

In this post, we use Quantopian to build and backtest a Relative Strength Index (RSI) trading strategy.

 

Quantopian

About Quantopian:

Quantopian provides capital, education, data, a research environment, and a development platform to algorithm authors (quants). Quantopian provides everything a quant needs to create a strategy and profit from it.

Quantopian’s members include finance professionals, scientists, developers, and students from more than 180 countries from around the world. The members collaborate in our forums and in person at regional meetups, workshops, and QuantCon, Quantopian’s flagship annual event.”

In other words, Quantopian is a website where one can build, test, and deploy trading strategies, using Python.

 

Relative Strength Index

To review, the Relative Strength Index (RSI) is a momentum indicator 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 identify overbought or oversold conditions in the trading of an asset.

RSI values range from 0 to 100.

The Relative Strength Index (RSI) is calculated as follows:

RSI = 100 - 100 / (1 + RS)

RS = Average gain of last 14 trading days / Average loss of last 14 trading days

 

Strategy

For this strategy, we buy $FB when the RSI is less than 30, and we will sell $FB when the RSI is greater than 70. The RSI will be calculated at a minutely frequency, as opposed to a daily frequency.

Trading Strategy

Buy - RSI < 30

Sell - RSI > 70

 

Code

Here is the Python code for the RSI Strategy.

import talib
import numpy as np
import pandas as pd

def initialize(context):
    context.stocks = symbols('FB')
    context.pct_per_stock = 1.0 / len(context.stocks)
    context.LOW_RSI = 30
    context.HIGH_RSI = 70
    
    set_benchmark(sid(42950))  
    
def handle_data(context, data):
    prices = data.history(context.stocks, 'price', 40, '1d')

    rsis = {}
    
    for stock in context.stocks:
        rsi = talib.RSI(prices[stock], timeperiod=14)[-1]
        rsis[stock] = rsi
        
        current_position = context.portfolio.positions[stock].amount
        
        if rsi > context.HIGH_RSI and current_position > 0 and data.can_trade(stock):
            order_target(stock, 0)

        elif rsi < context.LOW_RSI and current_position == 0 and data.can_trade(stock):
            order_target_percent(stock, context.pct_per_stock)

    record(FB_rsi=rsis[symbol('FB')])

At its foundation, Quantopian code is made up of three chunks: import modules, initialize, and handle_data.

1.) First we import the Talib, Numpy, and Pandas modules. As we’ll see, Talib streamlines the calculation of Technical Indicators.

import talib 
import numpy as np 
import pandas as pd

2.)  The initialize function:

def initialize(context): 
     context.stocks = symbols('FB') 
     context.pct_per_stock = 1.0 / len(context.stocks) 
     context.LOW_RSI = 30 
     context.HIGH_RSI = 70 

     set_benchmark(sid(42950)) 

2.a.) Define the security to trade, $FB.

context.stocks = symbols('FB') 

2.b.) Define the weight of each security. Since the RSI Strategy trades one security, the weight is 1.0. If there were two securities, the weight would be 0.5.

context.pct_per_stock = 1.0 / len(context.stocks) 

2.c.) Define the LOW_RSI value as 30

context.LOW_RSI = 30 

2.d.) Define the HIGH_RSI value as 70

context.HIGH_RSI = 70 

2.e.) Define the benchmark to which we will compare our strategy. In the example, the benchmark is set to $FB, essentially a buy and hold strategy. Remove ‘set_benchmark()’ to set the benchmark to the standard, ‘SPY’, or market rate.

set_benchmark(sid(42950)) 

3.)  The handle_data function:

def handle_data(context, data): 
     prices = data.history(context.stocks, 'price', 40, '1d') 

     rsis = {} 

     for stock in context.stocks: 
          rsi = talib.RSI(prices[stock], timeperiod=14)[-1] 
          rsis[stock] = rsi 

          current_position = context.portfolio.positions[stock].amount 

          if rsi > context.HIGH_RSI and current_position > 0 and data.can_trade(stock): 
               order_target(stock, 0) 

          elif rsi < context.LOW_RSI and current_position == 0 and data.can_trade(stock): 
               order_target_percent(stock, context.pct_per_stock) 

     record(FB_rsi=rsis[symbol('FB')])

3.a.) Query the ‘FB’ historical price data for the past 40 trading days.

prices = data.history(context.stocks, 'price', 40, '1d') 

3.b.) Create dictionary of RSI values.

rsis = {} 

3.c.) Create for loop for RSI calculation and order logic.

for stock in context.stocks: 

3.d.) Use Talib to calculate Relative Strength Index.

rsi = talib.RSI(prices[stock], timeperiod=14)[-1]

3.e.) Save Talib output to dictionary.

rsis[stock] = rsi 

3.f.) Save current portfolio positions in order to not execute too many/few orders.

current_position = context.portfolio.positions[stock].amount 

3.g.) Order logic: if RSI is greater than 70 and positions are greater than 0, then sell all positions.

if rsi > context.HIGH_RSI and current_position > 0 and data.can_trade(stock): 
               order_target(stock, 0) 

3.h.) Order logic: if RSI is less than 30 and positions are equal to 0, then buy positions equal to weight defined in initialize function.

elif rsi < context.LOW_RSI and current_position == 0 and data.can_trade(stock): 
               order_target_percent(stock, context.pct_per_stock) 

3.i.) Chart RSI data for $FB.

record(FB_rsi=rsis[symbol('FB')])

1 Year Performance

For the time period, 01/01/16 – 12/31/16

% Return Principal Pre-Tax Return Alpha
RSI Strategy 32.2% $10,000 $3,220 $2,220
FB Buy & Hold 10.0% $10,000 $1,000 ($200)
SPY Buy & Hold 12.0% $10,000 $1,200 N/A

 

We backtest the RSI Strategy with a $10,000 principal for the time period, 01/01/16 – 12/31/16. 

During 01/01/16 – 12/31/16,

  • The RSI Strategy produces 32.2% return, resulting in $3,220 pre-tax return.
  • FB Buy & Hold produces 10.0% return, resulting in $1,000 pre-tax return.
  • SPY Buy & Hold produces 12.0% return, resulting in $1,200 pre-tax return.
  • Compared to the SPY Buy & Hold, the RSI Strategy produces $2,220 Alpha whereas FB Buy & Hold produces ($200) Alpha, both on $10,000, principal.

 

 

Beyond 1 Year Performance

Yes, $2,220 Alpha on $10,000 principal is impressive.

Before we go and bet the farm, let’s see how the RSI Strategy performs over a longer time period.

Since the ‘FB’ IPO occurred on 05/18/12, we will backtest for the period 05/19/12 – 12/31/16.

For the time period, 05/19/12 – 12/31/16

% Return Principal Pre-Tax Return Alpha
RSI Strategy 147.4% $10,000 $14,740 $5,780
FB Buy & Hold 238.5% $10,000 $23,850 $14,890
SPY Buy & Hold 89.6% 10,000 $8,960 N/A

During 05/19/12 – 12/31/16,

  • The RSI Strategy produces 147.4% return, resulting in $14,740 pre-tax return.
  • FB Buy & Hold produces 238.5% return, resulting in $23,850 pre-tax return.
  • SPY Buy & Hold produces 89.6% return, resulting in $8,960 pre-tax return.
  • Compared to SPY Buy & Hold, the RSI Strategy produces $5,780 Alpha whereas FB Buy & Hold produces $14,890 Alpha, both on $10,000 principal.

Thus, on the broader time horizon, FB Buy & Hold outperforms the RSI Strategy.

 

Concluding Thought

Over the long term, money would go further with the FB Buy & Hold strategy.

The question still stands: what about 2016 makes the RSI Strategy superior in performance to FB Buy & Hold?

Until next time!

Python Tutorial: Price Channels

Download the accompanying IPython Notebook for this Tutorial from Github. 

 

Python streamlines tasks requiring multiple steps in a single block of code. For this reason, it is a great tool for querying and performing analysis on stock ticker data.

Last post, we outlined steps for calculating Bollinger Bands.

In this post, we introduce a new technical indicator,  Price Channels.

Price Channels

Price Channels are lines set above and below the price of a security. The upper channel is set at the x-period high and the lower channel is set at the x-period low. For a 20-day Price Channel, the upper channel would equal the 20-day high and the lower channel would equal the 20-day low.

Price Channels can be used to identify upward thrusts that signal the start of an uptrend or downward plunges that signal the start of a downtrend. Price Channels can also be used to identify overbought or oversold levels within a bigger downtrend or uptrend.

Price Channels are calculated as follows:

Upper Channel: 20-day high
Lower Channel: 20-day low

Let’s use Python to compute Price Channels.

1. Import modules.

import pandas as pd
import pandas.io.data as web
import matplotlib.pyplot as plt
%matplotlib inline

2. Define function for querying the daily high.

def get_high(stock, start, end): 
     return web.get_data_yahoo(stock, start, end)['High']

3. Define function for querying the daily low.

def get_low(stock, start, end): 
     return web.get_data_yahoo(stock, start, end)['Low']

4. Define function for querying daily close.

def get_close(stock, start, end): 
     return web.get_data_yahoo(stock, start, end)['Adj Close']

5. Query daily high, daily low, and daily close for ‘FB’ during 2016.

x = pd.DataFrame(get_high('FB', '1/1/2016', '12/31/2016'))
x['Low'] = pd.DataFrame(get_low('FB', '1/1/2016', '12/31/2016'))
x['Close'] = pd.DataFrame(get_close('FB', '1/1/2016', '12/31/2016'))

6. Compute 4 week high and 4 week low using rolling max/min. Add 50 day simple moving average for good measure.

x['4WH'] = pd.rolling_max(x['High'], 20)
x['4WL'] = pd.rolling_min(x['Low'], 20)
x['50 sma'] = pd.rolling_mean(x['Close'], 50)

7. Plot 4WH, 4WL, 50 sma, and daily close.

x.plot(y=['4WH', '4WL', '50 sma', 'Close'])

There you have it! We created our Price Channels. Here’s the full code:

import pandas as pd 
import pandas.io.data as web 
import matplotlib.pyplot as plt
%matplotlib inline

def get_high(stock, start, end): 
     return web.get_data_yahoo(stock, start, end)['High']
def get_low(stock, start, end): 
     return web.get_data_yahoo(stock, start, end)['Low']
def get_close(stock, start, end): 
     return web.get_data_yahoo(stock, start, end)['Adj Close']

x = pd.DataFrame(get_high('FB', '1/1/2016', '12/31/2016'))
x['Low'] = pd.DataFrame(get_low('FB', '1/1/2016', '12/31/2016'))
x['Close'] = pd.DataFrame(get_close('FB', '1/1/2016', '12/31/2016'))

x['4WH'] = pd.rolling_max(x['High'], 20)
x['4WL'] = pd.rolling_min(x['Low'], 20)
x['50 sma'] = pd.rolling_mean(x['Close'], 50)

x.plot(y=['4WH', '4WL', '50 sma', 'Close'])

In celebration of completing this tutorial, let’s watch Ed Seykota sing ‘The Whipsaw Song’.

Python Tutorial: Bollinger Bands

Download the accompanying IPython Notebook for this Tutorial from Github. 

 

Python streamlines tasks requiring multiple steps in a single block of code. For this reason, it is a great tool for querying and performing analysis on data.

Last post, we outlined steps for calculating MACD Signal Line & Centerline Crossovers.

In this post, we introduce a new technical indicator,  Bollinger Bands.

Bollinger Bands

Developed by John Bollinger, Bollinger Bands® are volatility bands placed above and below a moving average. Volatility is based on the standard deviation, which changes as volatility increases and decreases. The bands automatically widen when volatility increases and narrow when volatility decreases. This dynamic nature of Bollinger Bands also means they can be used on different securities with the standard settings. For signals, Bollinger Bands can be used to identify Tops and Bottoms or to determine the strength of the trend.

Bollinger Bands reflect direction with the 20-period SMA and volatility with the upper/lower bands. As such, they can be used to determine if prices are relatively high or low. According to Bollinger, the bands should contain 88-89% of price action, which makes a move outside the bands significant. Technically, prices are relatively high when above the upper band and relatively low when below the lower band. However, relatively high should not be regarded as bearish or as a sell signal. Likewise, relatively low should not be considered bullish or as a buy signal. Prices are high or low for a reason. As with other indicators, Bollinger Bands are not meant to be used as a stand alone tool. Chartists should combine Bollinger Bands with basic trend analysis and other indicators for confirmation.

Bollinger Bands are calculated as follows:

Middle Band = 20 day moving average
Upper Band = 20 day moving average + (20 Day standard deviation of price x 2) 
Lower Band = 20 day moving average - (20 Day standard deviation of price x 2)

Bollinger Bands consist of a middle band with two outer bands. The middle band is a simple moving average that is usually set at 20 periods. A simple moving average is used because the standard deviation formula also uses a simple moving average. The look-back period for the standard deviation is the same as for the simple moving average. The outer bands are usually set 2 standard deviations above and below the middle band.

Let’s use Python to compute Bollinger Bands.

1. Start with the 30 Day Moving Average Tutorial code.

import pandas as pd
import pandas.io.data as web
%matplotlib inline
import matplotlib.pyplot as plt

stocks = ['FB']
def get_stock(stock, start, end):
     return web.get_data_yahoo(stock, start, end)['Adj Close']
px = pd.DataFrame({n: get_px(n, '1/1/2016', '12/31/2016') for n in names})
px

2. Compute the 20 Day Moving Average.

px['20 ma'] = pd.stats.moments.rolling_mean(px['FB'],20)

3. Compute 20 Day Standard Deviation. 

px['20 sd'] = pd.stats.moments.rolling_std(px['FB'],20)

4. Create Upper Band.

px['Upper Band'] = px['20 ma'] + (px['20 sd']*2)

5. Create Lower Band.

px['Lower Band'] = px['20 ma'] - (px['20 sd']*2)

6. Plot Bollinger Bands.

px.plot(y=['FB','20 ma', 'Upper Band', 'Lower Band'], title='Bollinger Bands')

There you have it! We created our Bollinger Bands. Here’s the full code:

import pandas as pd
import pandas.io.data as web
%matplotlib inline
import matplotlib.pyplot as plt

stocks = ['FB']
def get_stock(stock, start, end):
     return web.get_data_yahoo(stock, start, end)['Adj Close']
px = pd.DataFrame({n: get_px(n, '1/1/2016', '12/31/2016') for n in names})
px['20 ma'] = pd.stats.moments.rolling_mean(px['FB'],20)
px['20 sd'] = pd.stats.moments.rolling_std(px['FB'],20)
px['Upper Band'] = px['20 ma'] + (px['20 sd']*2)
px['Lower Band'] = px['20 ma'] - (px['20 sd']*2)
px.plot(y=['FB','20 ma', 'Upper Band', 'Lower Band'], title='Bollinger Bands')

Python Tutorial: Query Stock Data, Calculate 30 Day Moving Average

Python streamlines tasks requiring multiple steps in a single block of code. For this reason, it is a great tool for querying and performing analysis on data. In this post we will use Python to pull ticker data for a specific stock and then calculate its 30 day moving average. Here are the steps:

1. Import the pandas modules.

import pandas as pd
import pandas.io.data as web

2. Create a list of the stocks for which you would like to query ticker data. For this example, we will pull ticker data for Facebook, ‘FB’. If you would like to add other stocks, simply add the symbols to the list separated by commas.

stocks = ['FB']

Or

stocks = ['FB','AAPL','GOOG','AMZN']

3. Write function to query data from yahoo finance. The function takes three arguments: the stock, the start date, and the end date. It returns the daily ‘Adj Close’. If you would like to pull a different value, simply switch it for ‘Adj Close’ without the brackets.

def get_stock(stock,start,end):
     return web.get_data_yahoo(stock,start,end)['Adj Close']

Or

def get_stock(stock,start,end):
     return web.get_data_yahoo(stock,start,end)['Volume']

4. Call function for the date range, 1/1/2016 – 12/31/2016. Use the ‘for n in stocks’ logic in case you have more than one stock for which you would like to pull data. Compile query in DataFrame, saved to variable ‘px’.

px = pd.DataFrame({n: get_stock(n, '1/1/2016', '12/31/2016') for n in stocks})

5. Add new column to DataFrame in which you calculate the 30 day moving average. Call DataFrame to view contents.

px['30 mavg'] = pd.rolling_mean(px, 30)
px

There you have it! We created a DataFrame containing the daily ticker data for a specific stock and then calculated its 30 day moving average. Here is the full code:

import pandas as pd
import pandas.io.data as web

stocks = ['FB']
def get_stock(stock, start, end):
     return web.get_data_yahoo(stock, start, end)['Adj Close']
px = pd.DataFrame({n: get_px(n, '1/1/2016', '12/31/2016') for n in names})
px['30 mavg'] = pd.rolling_mean(px, 30)
px

Dem Miscalculation

Since the election, many have voiced their perspectives on what led to the surprising results. One of whom is Bernie Sanders, who I believe has a clear view. As he states, Americans are in pain. Millions of workers make less in inflation-adjusted wages than they did 40 years ago. One half of older workers have nothing in the bank as they enter retirement. Young Americans who graduate from college with $100k in debt make $12/hour. The list goes on…

Donald Trump addressed the people’s anger, pain, and frustration. He did it directly, in an emotional way, amplified through social media. On the other hand, Hillary focused on social justice and equality issues, which for the record are important. However, how can one care about another’s needs if she does not feel her own are being addressed? As Maslow’s hierarchy indicates, one’s basic needs must be met before higher order needs, such as those of the collective, can be realized. Hillary’s campaign did not see that its messaging missed this tenet of human psychology.

screen-shot-2016-11-19-at-8-43-16-am

Yes, there is irony in a Billionaire relating to the common man or woman, but between the two, Trump did the better job resonating, and that’s why he won the election.

If they Google You, Do you Win?

In a way, this election is a referendum on “do actions speak louder than words”, is what people do in the privacy of their internet browsing more reflective of their future behavior than what they tell pollsters? And while I have focused on twitter as a barometer of public opinion, there are other data sources that could signal the private thoughts and future actions of voters. The linked NYT article, “If they Google you, Do you Win?”, mentions using the Google queries “Trump Clinton” vs. “Clinton Trump” as signals of voter interest, with the respective queries reflecting bias towards the candidate listed first, i.e. “Clinton Trump” would reflect bias towards Clinton. Using this methodology, I researched Google trends for Battleground states to see where public opinion may be. The data are displayed below.

screen-shot-2016-11-03-at-5-49-10-pm

For the month of October 2016, “Trump Clinton” leads “Clinton Trump” in every state with the exception of Nevada.

You might say Trump is a polarizing celebrity, and for that reason he may be top of mind even if the individual plans to vote for Clinton. Okay, well then let’s penalize Trump 10%. Even in that case, ‘Factored “Trump Clinton”‘ indicates that, with the exception of Nevada, the three states that are in play are Virginia, Iowa, and Florida.

So while it is unclear in which direction the election will result, I believe we may be surprised at how close the results turn out to be, and that one thing we may remember is the discrepancy between what was reported in the polls leading up to the election and what actually happened online. We only have 4 days left to see which source provides a clearer signal of truth, and until then….Good luck to both candidates!

Twitterwonk Model: Primary & General Election

​_Twitterwonk
TwitterwonkJune15-July16_Dashboard How does Twitterwonk work?

 1. Twitterwonk examines the leading candidates from both US political parties and conducts analyses using Twitter data for the following categories: Volume, Engagement, and Followers. 

 2. The categories are combined and weighted to form the Twitterwonk Score.

 3. The results are displayed by political party as monthly averages .

 Why Does Twitterwonk Matter?

1. Twitterwonk correctly predicted the Republican and Democrat Nominees.

2. The 2016 Presidential election continues to play out on Twitter. Some have called it the “celebrity” election. 

3. Twitter has the greatest number of users, 300 million, in the history of the platform.