Back to Community
Tips for improvement on my mean reversion pairing algo

The theory behind my algo is that if you have two competing companies in the same market sector, then their regression is a more accurate estimate of the future stock price. The algo trades when the stock prices of the two stocks become over or under valued relative to the average simple moving average of the two.

I also added in my commissions, and added controls so that it would not buy or sell on margin or make orders larger than 25% of my initial capital base.

Please let me know your thoughts on flaws and improvements.

Thanks,
BSM

Clone Algorithm
73
Loading...
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
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
def initialize(context):
    
    context.pfe = sid(5923)
    context.mrk = sid(5029)    
    context.portfolio.positions_value
    set_commission(commission.PerTrade(cost=7.00))

def handle_data(context, data):
    
    posvalue = context.portfolio.positions_value
    
    cash = context.portfolio.cash
    record(cash = context.portfolio.cash)           
    
    pfeprice = data[context.pfe].price
    mrkprice = data[context.mrk].price
    
    pfemultiple = mrkprice / pfeprice
    mrkmultiple = pfeprice / mrkprice
    
    adjpfemavg = data[context.pfe].mavg(30) * pfemultiple
    adjmrkmavg = data[context.mrk].mavg(30) * mrkmultiple 
    pfemavg = data[context.pfe].mavg(30) 
    mrkmavg = data[context.mrk].mavg(30)        
    
    mrklinreg = (adjpfemavg + mrkmavg) / 2
    pfelinreg = (pfemavg + adjmrkmavg) / 2
    
    pfeshares = ((context.portfolio.starting_cash * .25) / pfeprice) 
    mrkshares = ((context.portfolio.starting_cash * .25) / mrkprice)

           
    if pfeprice > pfelinreg * 1.1 and posvalue > context.portfolio.starting_cash * .25:
         order(context.pfe,-pfeshares)
         log.info("Selling %s" % (context.pfe))
            
    elif pfeprice < pfelinreg * .9 and pfeprice * pfeshares < cash:
         order(context.pfe,+pfeshares)
         log.info("Buying %s" % (context.pfe))
     
    elif mrkprice > mrklinreg * 1.1 and posvalue > context.portfolio.starting_cash * .25: 
         order(context.mrk,-mrkshares)
         log.info("Selling %s" % (context.mrk))     
     
    elif mrkprice < mrklinreg * .9 and mrkprice * mrkshares < cash:
         order(context.mrk,+mrkshares)
         log.info("Buying %s" % (context.mrk))   
                        


              


        
       
      
There was a runtime error.
6 responses

Better comparison using the two underlying stocks as the benchmark.

Thanks for adding that feature!

Clone Algorithm
73
Loading...
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
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
def initialize(context):
    
    context.pfe = sid(5923)
    context.mrk = sid(5029)    
    context.portfolio.positions_value
    set_commission(commission.PerTrade(cost=7.00))
    set_benchmark(sid(5029))
    set_benchmark(sid(5923))
    
def handle_data(context, data):
    
    posvalue = context.portfolio.positions_value
    
    cash = context.portfolio.cash
    record(cash = context.portfolio.cash)           
    
    pfeprice = data[context.pfe].price
    mrkprice = data[context.mrk].price
    
    pfemultiple = mrkprice / pfeprice
    mrkmultiple = pfeprice / mrkprice
    
    adjpfemavg = data[context.pfe].mavg(30) * pfemultiple
    adjmrkmavg = data[context.mrk].mavg(30) * mrkmultiple 
    pfemavg = data[context.pfe].mavg(30) 
    mrkmavg = data[context.mrk].mavg(30)        
    
    mrklinreg = (adjpfemavg + mrkmavg) / 2
    pfelinreg = (pfemavg + adjmrkmavg) / 2
    
    pfeshares = ((context.portfolio.starting_cash * .25) / pfeprice) 
    mrkshares = ((context.portfolio.starting_cash * .25) / mrkprice)

           
    if pfeprice > pfelinreg * 1.1 and posvalue > context.portfolio.starting_cash * .25:
         order(context.pfe,-pfeshares)
         log.info("Selling %s" % (context.pfe))
            
    elif pfeprice < pfelinreg * .9 and pfeprice * pfeshares < cash:
         order(context.pfe,+pfeshares)
         log.info("Buying %s" % (context.pfe))
     
    elif mrkprice > mrklinreg * 1.1 and posvalue > context.portfolio.starting_cash * .25: 
         order(context.mrk,-mrkshares)
         log.info("Selling %s" % (context.mrk))     
     
    elif mrkprice < mrklinreg * .9 and mrkprice * mrkshares < cash:
         order(context.mrk,+mrkshares)
         log.info("Buying %s" % (context.mrk))   
                        


              


        
       
      
There was a runtime error.

Nice algo.
I generalized the code a bit.

Clone Algorithm
26
Loading...
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
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
def initialize(context):
    
    context.sec1 = sid(5923)
    context.sec2 = sid(5029)    
    set_commission(commission.PerTrade(cost=7.00))

def algo(sec, price, pair_price, mavg, pair_mavg, portfolio):
    """
    Core of the algo
    :param sec: security object
    :param price: current security price
    :param pari_price: current price of paired security
    :param mavg: moving average of the security
    :param pair_mavg: moving average of the paired security
    :param portfolio: portfolio object

    :returns: order id or None if no orders created
    """
    adj = pair_mavg * price / pair_price
    linreg = (adj + mavg) / 2.

    shares = portfolio.starting_cash * .25 / price
           
    if price > linreg * 1.1 and portfolio.positions_value > portfolio.starting_cash * .25:
         log.info("Selling %d shares of %s" % (shares, sec.symbol))
         return order(sec, -shares)
            
    elif price < linreg * .9 and price * shares < portfolio.cash:
         log.info("Buying %d shares of %s" % (shares, sec.symbol))
         return order(sec, shares)


def handle_data(context, data):
    """
    The main proccessing function.
    Called whenever a market event occurs for any of algorithm's securities.

    :param context: context object
    :param data: Object contains all the market data for algorithm securities
                 keyed by security id. It represents a snapshot of algorithm's
                 universe as of when this method is called.
    :returns: None
    """
       
    record(cash=context.portfolio.cash)

    data1 = data[context.sec1]
    data2 = data[context.sec2]
    price1 = data1.price
    price2 = data2.price
    mavg1 = data1.mavg(30)
    mavg2 = data2.mavg(30)
    
    order_id = algo(context.sec1, price1, price2, mavg1, mavg2, context.portfolio)
    if not order_id:
        algo(context.sec2, price2, price1, mavg2, mavg1, context.portfolio)
There was a runtime error.

If you start it a year earlier, you get a more realistic view. Because the algorithm starts late, in accidentally benefits from a bit of market timing, missing out on the early 2008 drop. If you re-write using the history function, you should be able to avoid this, although you won't be able to run it in daily mode anymore.

Clone Algorithm
10
Loading...
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
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
def initialize(context):
    
    context.sec1 = sid(5923)
    context.sec2 = sid(5029)    
    set_commission(commission.PerTrade(cost=7.00))

def algo(sec, price, pair_price, mavg, pair_mavg, portfolio):
    """
    Core of the algo
    :param sec: security object
    :param price: current security price
    :param pari_price: current price of paired security
    :param mavg: moving average of the security
    :param pair_mavg: moving average of the paired security
    :param portfolio: portfolio object

    :returns: order id or None if no orders created
    """
    adj = pair_mavg * price / pair_price
    linreg = (adj + mavg) / 2.

    shares = portfolio.starting_cash * .25 / price
           
    if price > linreg * 1.1 and portfolio.positions_value > portfolio.starting_cash * .25:
         log.info("Selling %d shares of %s" % (shares, sec.symbol))
         return order(sec, -shares)
            
    elif price < linreg * .9 and price * shares < portfolio.cash:
         log.info("Buying %d shares of %s" % (shares, sec.symbol))
         return order(sec, shares)


def handle_data(context, data):
    """
    The main proccessing function.
    Called whenever a market event occurs for any of algorithm's securities.

    :param context: context object
    :param data: Object contains all the market data for algorithm securities
                 keyed by security id. It represents a snapshot of algorithm's
                 universe as of when this method is called.
    :returns: None
    """
       
    record(cash=context.portfolio.cash)

    data1 = data[context.sec1]
    data2 = data[context.sec2]
    price1 = data1.price
    price2 = data2.price
    mavg1 = data1.mavg(30)
    mavg2 = data2.mavg(30)
    
    order_id = algo(context.sec1, price1, price2, mavg1, mavg2, context.portfolio)
    if not order_id:
        algo(context.sec2, price2, price1, mavg2, mavg1, context.portfolio)
There was a runtime error.

Just for grins, I loaded this puppy up with TLT and SPY. Not ready for prime time yet, but it's worth further research.

Clone Algorithm
12
Loading...
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
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
def initialize(context):
    
   # context.pfe = sid(5923)
    #context.mrk = sid(5029)    
    context.pfe = sid(8554)
    context.mrk = sid(23921)  
    context.portfolio.positions_value
    set_commission(commission.PerTrade(cost=7.00))
    set_benchmark(context.pfe)
    set_benchmark(context.mrk)
    
def handle_data(context, data):
    
    posvalue = context.portfolio.positions_value
    
    cash = context.portfolio.cash
    record(cash = context.portfolio.cash)           
    
    pfeprice = data[context.pfe].price
    mrkprice = data[context.mrk].price
    
    pfemultiple = mrkprice / pfeprice
    mrkmultiple = pfeprice / mrkprice
    
    adjpfemavg = data[context.pfe].mavg(30) * pfemultiple
    adjmrkmavg = data[context.mrk].mavg(30) * mrkmultiple 
    pfemavg = data[context.pfe].mavg(30) 
    mrkmavg = data[context.mrk].mavg(30)        
    
    mrklinreg = (adjpfemavg + mrkmavg) / 2
    pfelinreg = (pfemavg + adjmrkmavg) / 2
    
    pfeshares = ((context.portfolio.starting_cash * .25) / pfeprice) 
    mrkshares = ((context.portfolio.starting_cash * .25) / mrkprice)

           
    if pfeprice > pfelinreg * 1.1 and posvalue > context.portfolio.starting_cash * .25:
         order(context.pfe,-pfeshares)
         log.info("Selling %s" % (context.pfe))
            
    elif pfeprice < pfelinreg * .9 and pfeprice * pfeshares < cash:
         order(context.pfe,+pfeshares)
         log.info("Buying %s" % (context.pfe))
     
    elif mrkprice > mrklinreg * 1.1 and posvalue > context.portfolio.starting_cash * .25: 
         order(context.mrk,-mrkshares)
         log.info("Selling %s" % (context.mrk))     
     
    elif mrkprice < mrklinreg * .9 and mrkprice * mrkshares < cash:
         order(context.mrk,+mrkshares)
         log.info("Buying %s" % (context.mrk))   
                        


              


        
       
      
There was a runtime error.

So say I want to verify that the spreads are not diverging and wanted to use the history of the past 3 months in order to compare the spread between the two securities now and 3 months ago. If they do diverge I want to be able to change the securities being traded to others that the algorithm has identified as cointegrated over the past 3 months.

Any thoughts on the code for how to do this?

A quick note about the new set_benchmark feature - you can only initialize one benchmark per algo. If its not initialized, the default is SPY. If you have a list of set_benchmark's, it will take the last item in the list. For example, in Richard's backtest, the benchmark is MRK.

Disclaimer

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.