Back to Community
First attempt at pairs trading

I'm assuming my results are mostly luck, and have very little to do with anything fancy I did (although 250% return in 5 years is not too shabby).

Clone Algorithm
Backtest from to with initial capital
Total Returns
Max Drawdown
Benchmark Returns
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
import datetime as dt
import statsmodels.tsa.stattools as ts
import statsmodels.api as sm
import numpy as np

def initialize(context):
    # Initialize stock universe with the following stocks:

    # Coca-Cola (KO) and Pepsi (PEP)
    # Wal-Mart (WMT) and Target Corporation (TGT)
    # Exxon Mobil (XOM) and Chevron Corporation (CVX)
    # BHP Billiton Limited (BHP) and BHP Billiton plc (BBL)
    #1. Identification of security (stocks, bonds, futures etc.)  pairs.  Essentially, first create a short list of related securities (we’ll be using equities via Quantopian).
    context.stocks = [[sid(4283), sid(5885)],
                      [sid(8229), sid(21090)],
                      [sid(8347), sid(23112)],
                      [sid(863), sid(25165)]]

    # Declare lag value/flag
    context.warmupDays = 60
    context.warmedUp = False

    # Initialize ratio (for the current ratio), historical (historical prices), and current holdings
    lenCon = len(context.stocks)
    context.ratio = [[]] * lenCon
    context.historical = [[[],[]]] * lenCon
    context.currDays = 0
    # Used later to test for cointegration
    context.cointegrated = []
    # The amount of standard deviations that causes a buy
    context.SDDiff = 1
    # Mean the stock was last bought at
    context.limits = [False] * lenCon
    context.spread = [[]] * lenCon
    context.amountStock = [[0,0]] * lenCon
def handle_data(context, data):
    # Check bool flag for 60 day lag
    if context.warmedUp == False:
        for pair in range(len(context.stocks)):
            currPair = context.stocks[pair]
            context.spread[pair].append(data[currPair[0]].price - data[currPair[1]].price)

        if len(context.ratio[0]) >= 60:
            context.warmedUp = True
            for pair in range(len(context.stocks)):
            #2. Relationship testing.  There are many ways to test of this relationship.  We will be using cointegration.  Two time series are cointegrated if they share the same stochastic drift (the change of average value over time).  There are a couple of video on cointegration, and we’ll go over some examples.
            if False in context.cointegrated:
                print("First pair that is not cointegrated:")
                # This could be built out to iterate if we are searching for pairs, but all of the pairs I have chosen cointegrate
                print(context.stocks[np.where([not i for i in context.cointegrated])[0][0]])
        #3. Building the trade.  Using the historical data we have available, establish baseline values that create rules for buying and selling the securities.  At the core, this is simple.  When the pair is out of line, we buy one and sell the other (other variations exist).  When the pair comes back in line, we exit the trade and capture the profit.
        for pair in range(len(context.stocks)):
            currPair = context.stocks[pair]
            currX = currPair[0]
            currY = currPair[1]
            currXPrice = data[currX].price
            currYPrice = data[currY].price
            spreadMean = np.mean(context.spread[pair])
            spreadSD = np.std(context.spread[pair])
            currSpread = currXPrice - currYPrice
            currRatio = currXPrice/currYPrice

            #figure out how many stocks
            stocksToOrderX = 1000 * currRatio
            stocksToOrderY = 1000
            # amount of stocks owned
            currOwnedX = context.portfolio.positions[currPair[0]]['amount']
            currOwnedY = context.portfolio.positions[currPair[1]]['amount']
            # Attempting to resolve initial state
            # if not all(i == False for i in context.limits):
            toCheck = [i for i, j in enumerate(context.limits) if j != False]
            if pair in toCheck:
                #pair, spreadMean, 'long'/'short'
                lim = context.limits[pair]
                if lim[2] == 'long':
                    if currSpread <= spreadMean:
                        order(currX, -currOwnedX)
                        order(currY, -currOwnedY)
                        context.limits[pair] = False
                    if currSpread >= spreadMean:
                        order(currX, -currOwnedX)
                        order(currY, -currOwnedY)
                        context.limits[pair] = False
            # Initial Trade
            lowerLim = -100000
            # currOwnedX < upperLim and and currOwnedY < upperLim
            if currOwnedX > lowerLim  and currOwnedY > lowerLim:          
                if currSpread > spreadMean + context.SDDiff * spreadSD:
                        order(currX, -stocksToOrderX)
                        order(currY, stocksToOrderY)
                        print("Bought " + str(stocksToOrderY) + " stocks of " + str(currY))
                        print("Shorted " + str(stocksToOrderX) + " stocks of " + str(currX))
                        context.limits[pair] = [pair, spreadMean, 'long']
                elif currSpread < spreadMean - context.SDDiff * spreadSD:
                        order(currX, stocksToOrderX)
                        order(currY, -stocksToOrderY)
                        print("Bought " + str(stocksToOrderX) + " stocks of " + str(currX))
                        print("Shorted " + str(stocksToOrderY) + " stocks of " + str(currY))
                        context.limits[pair] = [pair, spreadMean, 'long']

            # Logging data

def test_coint(pair):
    result = sm.OLS(pair[1], pair[0]).fit()   
    dfResult =  ts.adfuller(result.resid)
    return dfResult[0] >= dfResult[4]['10%']
There was a runtime error.
6 responses

Nice algo! I'm curious, could you explain why you have a warm-up period? What data is the algo accumulating here?

If you need a trailing window of prices, use the history() function. This will have the price data available on the first bar the algo starts trading.
So you could do:

prices = history(60, '1d', 'price')  

Then to get a specific stock's price history you can index into the dataframe by doing,

stock_price = history(60, '1d', 'price')[stock]  

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.

I was completely unaware of the history() function. With that, the warmup period is completely unnecessary.
Thank you!

Thanks for sharing this. Just started reading and this explained a lot to me.

I'm definitely new to a lot of these concepts, but I'm enjoying experimenting on quantopian to see if I can apply what I'm learning.

Just stumbled on this what is interesting is how well it does start of 2015 on.

I tried to backtest this with 30k amount but it was making orders worth around 300k. I'm not sure if I'm missing something?