Back to Community
Multiple pair trading strategy (Problems please help!)

Hello Everyone,

I have a few problems with my algorithm. I just recently started to code.

  1. I was trying to implement a hedge spread ratio using OLS regression, but no orders were executed.
  2. Often times the program has run time errors, such as nontype for context.slope[t], even in context.zscore[t].
  3. All I aim for is to get the zscore based on the hedge spread ratio and use it as a trading signal.
  4. Without the zscore part, the program would trade and cover position like it should do.

I am sure there are more problems in the codes, please help me to solve my problems!

Thank you all in advance,

Sincerely,
Terence.

Clone Algorithm
9
Loading...
Backtest from to with initial capital
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
import math
import numpy as np
import datetime as dt
import statsmodels.tsa.stattools as ts
import statsmodels.api as sm
import pytz
import pandas as pd

window_length=15

def initialize(context):
    context.std_multiplier = 2.5
    set_commission(commission.PerTrade(cost=1.00))
    context.Pair_list =  [
                          [sid(46216), sid(46220)],#last-1
                          [sid(14517), sid(14516)],#last
                  [sid(5484), sid(6119)],
                  [sid(24), sid(5773)],
                  [sid(24), sid(7671)],
                  [sid(20088), sid(1335)],
                  [sid(12691), sid(698)],
                  [sid(46631),sid(32146)],
                  [sid(46222), sid(46221)],
                  [sid(46217),sid(46223)],
                  [sid(46170), sid(46220)],
                  [sid(46170), sid(46222)],
                  [sid(46216), sid(46222)],
                  [sid(46216), sid(46220)]
                         ]
                
    context.last_trade=[None] * len(context.Pair_list)
    context.bounds=[None]*len(context.Pair_list)
    context.avg_ratio_data=[None]*len(context.Pair_list)
    context.current_ratio=[None]*len(context.Pair_list)
    context.high_bound=[None]*len(context.Pair_list)
    context.low_bound=[None]*len(context.Pair_list)
    context.vwap1= [None]*len(context.Pair_list)
    context.vwap2= [None]*len(context.Pair_list)
    context.mavg1= [None]*len(context.Pair_list)
    context.mavg2= [None]*len(context.Pair_list)
    context.slope= [None]*len(context.Pair_list)
    context.spread= [None]*len(context.Pair_list)
    context.zscore= [None]*len(context.Pair_list)
    context.spreads= [None]*len(context.Pair_list)

def compute_zscore(context, data, t):

    price_history = history(15, '1d', 'price')
    price_history = price_history.fillna(method='ffill')
    slope=ols_transform(price_history, context.Pair_list[t][0], context.Pair_list[t][1])
    spread = data[context.Pair_list[t][0]].price - (slope * data[context.Pair_list[t][1]].price)
    
    context.spreads.append(spread)
          
    zscore = (spread - np.mean(context.spreads[-window_length:])) / np.std(context.spreads[-window_length:])
    return zscore


# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    price_history = history(15, '1d', 'price')
    price_history = price_history.fillna(method='ffill')
   
    for t in range(0,(len(context.Pair_list))):
    #t=0
    #while(t<len(context.Pair_list)):
        if context.Pair_list[t][0] not in data or context.Pair_list[t][1] not in data:
            return 
        context.vwap1[t] = data[context.Pair_list[t][0]].vwap(5)
        context.vwap2[t] = data[context.Pair_list[t][1]].vwap(5)
        context.mavg1[t] = data[context.Pair_list[t][0]].mavg(5)
        context.mavg2[t] = data[context.Pair_list[t][1]].mavg(5)
        
        context.current_ratio[t] = data[context.Pair_list[t][0]].price / data[context.Pair_list[t][1]].price
        context.avg_ratio_data[t] = avg_ratio(price_history, context,context.Pair_list[t][0],context.Pair_list[t][1],t)
    
        if context.avg_ratio_data[t] is None:
            return
        context.bounds[t] = calc_trade_bounds(price_history,context,context.Pair_list[t][0],context.Pair_list[t][1],context.avg_ratio_data[t],t)
    
        if context.bounds[t] is None:
            return
        context.high_bound[t], context.low_bound[t] = context.bounds[t]
        
        #context.slope[t] = Ols(price_history, context, context.Pair_list[t][0], context.Pair_list[t][1], t)
        """
        if context.slope[t] is None:
            return
        context.slope[t] = ols_transform(price_history, context.Pair_list[t][0], context.Pair_list[t][1])

        if context.spread[t] is None:
            return
        context.spread[t] = (data[context.Pair_list[t][0]].price) -        ((context.slope[t])*data[context.Pair_list[t][1]].price)
        
        if context.spreads[t] is None:
            return
        context.spreads.append(context.spread[t])
        """
        if context.zscore[t] is None:
            return
        context.zscore[t]=compute_zscore(context,data,t)
       # context.zscore[t] = (context.spread[t] - np.mean(context.spreads)) / (np.std( context.spreads))

    #Buys the undervalued stock and sells the overvalues stock
        if (context.current_ratio[t] > context.high_bound[t] and context.last_trade[t] is not "high") or (context.zscore[t] > 1):
           # if context.vwap1[t] < data[context.Pair_list[t][0]].price and context.vwap2[t] < data[context.Pair_list[t][1]].price:
                order_value(context.Pair_list[t][1], +(context.portfolio.portfolio_value/(2*len(context.Pair_list))))
                order_value(context.Pair_list[t][0], -(context.portfolio.portfolio_value/(2*len(context.Pair_list))))
                context.last_trade[t] = "high"
        elif context.current_ratio[t] < context.low_bound[t] and context.last_trade[t] is not "low" or (context.zscore[t] < 1):
           # if context.vwap1[t] > data[context.Pair_list[t][0]].price and context.vwap2[t] > data[context.Pair_list[t][1]].price:
                order_value(context.Pair_list[t][0], +(context.portfolio.portfolio_value/(2*len(context.Pair_list))))
                order_value(context.Pair_list[t][1], -(context.portfolio.portfolio_value/(2*len(context.Pair_list))))
                context.last_trade[t] = "low"
        elif context.last_trade[t] is not "mid" and context.last_trade[t] is "high" and context.current_ratio[t] < context.avg_ratio_data[t]:
            order_target(context.Pair_list[t][1], 0)
            order_target(context.Pair_list[t][0], 0)
            context.last_trade[t] = "mid"
        elif context.last_trade[t] is not "mid" and context.last_trade[t] is "low" and context.current_ratio[t] > context.avg_ratio_data[t]:
            order_target(context.Pair_list[t][1], 0)
            order_target(context.Pair_list[t][0], 0)
            context.last_trade[t] = "mid"

        record(value=context.portfolio.positions_value)

#@batch_transform(window_length=15, refresh_period=1)
"""
def Ols(prices,context,sid0,sid1,t): 
    
        prices = prices.fillna(method='bfill')
        sid0=context.Pair_list[t][0]
        sid1=context.Pair_list[t][1]
    
        p0 = prices[sid0].values
        p1 = prices[sid1].values
        
        return sm.OLS(p0, p1).fit().params
    
#@batch_transform(window_length=15, refresh_period=1)
"""

def ols_transform(prices, sid1, sid2):
    """
    Computes regression coefficient (slope and intercept)
    via Ordinary Least Squares between two SIDs.
    """
    p0 = prices[sid1]
    p1 = sm.add_constant(prices[sid2], prepend=True)
    
    slope=sm.OLS(p0, p1).fit().params
    return slope
def avg_ratio(prices,context,sid0,sid1,t): 

        prices = prices.fillna(method='bfill')
        sid0=context.Pair_list[t][0]
        sid1=context.Pair_list[t][1]
    
        p0 = prices[sid0].values
        p1 = prices[sid1].values    
    
        p0_arr = np.array(p0)
        p1_arr = np.array(p1)

        if p0_arr is None or p1_arr is None:
            return None
        
        return (p0_arr/p1_arr).mean() 
    
    
#@batch_transform(window_length=15, refresh_period=1)
def calc_trade_bounds(prices,context,sid0,sid1,mean,t):  

        prices = prices.fillna(method='bfill')
        sid0=context.Pair_list[t][0]
        sid1=context.Pair_list[t][1]
    
        p0 = prices[sid0].values
        p1 = prices[sid1].values    
    
        p0_arr = np.array(p0)
        p1_arr = np.array(p1)

        if p0_arr is None or p1_arr is None:
            return None
    
        std_amt = (p0_arr/ p1_arr).std()*context.std_multiplier
    
        high_bound = mean + std_amt
        low_bound = mean - std_amt
        
        return (high_bound, low_bound)
    
       # elif context.vwap1 > data[context.Pair_list[t][0]] and context.last_trade[t] is "low"
       # and context.current_ratio[t] > context.avg_ratio_data[t]
        
       # elif context.vwap2 > data[context.Pair_list[t][1]] and context.last_trade[t] is "low"
       # and context.current_ratio[t] > context.avg_ratio_data[t]
        
       # elif context.vwap1 < data[context.Pair_list[t][0]] and context.last_trade[t] is "high"
       # and context.current_ratio[t] < context.avg_ratio_data[t]
        
       # elif context.vwap2 < data[context.Pair_list[t][1]] and context.last_trade[t] is "high"
       # and context.current_ratio[t] < context.avg_ratio_data[t]
        
There was a runtime error.
18 responses

I was trying to something similar but my Python knowledge got on the way. Will be also interested in the algorithm when done.

Partly experimental code:

import numpy as np  
import pandas as pd  
import collections  
import copy

from sklearn import linear_model

def initialize(context):  
    set_commission(commission.PerShare(0.01))  
    set_slippage(slippage.VolumeShareSlippage(volume_limit=0.25, price_impact=0.01))  
    c = context  
    c.SPDR = {}

    c.SPDR['index'] = [symbol('SPY')]  
    c.SPDR['sectors'] = [symbol('XLY'), symbol('XLP'), symbol('XLE'), symbol('XLF'), symbol('XLV'), symbol('XLI'), symbol('XLB'), symbol('XLK'), symbol('XLU')]

    c.secs = c.SPDR['index'] + c.SPDR['sectors']  
    c.to_200 = pd.DataFrame({i : np.linspace(1, 200, 200) for i in c.secs})  
    c.to_100 = pd.DataFrame({i : np.linspace(1, 100, 100) for i in c.secs})  
    c.to_050 = pd.DataFrame({i : np.linspace(1, 50, 50) for i in c.secs})  
    c.to_030 = pd.DataFrame({i : np.linspace(1, 30, 30) for i in c.secs})  
    c.to_015 = pd.DataFrame({i : np.linspace(1, 15, 15) for i in c.secs})  
    c.to_007 = pd.DataFrame({i : np.linspace(1, 7, 7) for i in c.secs})  
    c.regr = linear_model.LinearRegression()  
    c.numsecs = float(len(c.secs))  
    c.margine = 0.30  
def handle_data(context, data):  
    c = context  
    p200 = history(200, '1d', 'price', ffill=True)  
    p100 = p200.iloc[-100:, :]  
    p050 = p200.iloc[-50:, :]  
    p030 = p200.iloc[-30:, :]  
    p015 = p200.iloc[-15:, :]  
    p007 = p200.iloc[-7:, :]  
    if len(p200.columns) != len(c.secs) or \  
    len(p100.columns) != len(c.secs) or \  
    len(p050.columns) != len(c.secs) or \  
    len(p030.columns) != len(c.secs) or \  
    len(p015.columns) != len(c.secs) or \  
    len(p007.columns) != len(c.secs) or \  
    len(data) != len(c.secs):  
        log.warn('No complete data')  
        return  
    lnp200 = np.log(p200)  
    lnp100 = lnp200.iloc[-100:, :]  
    lnp050 = lnp200.iloc[-50:, :]  
    lnp030 = lnp200.iloc[-30:, :]  
    lnp015 = lnp200.iloc[-15:, :]  
    lnp007 = lnp200.iloc[-7:, :]  

    def sgn(x):  
        return 0.0 if x is None or x == 0.0 else np.copysign(1.0, x)  
    def initiate_trade(s, w, force = False):  
        poss = float(c.portfolio.positions[s].amount)  
        oo = get_open_orders(s)  
        if force and len(oo) == 0:  
            order_target_percent(s, w)  
        elif poss == 0.0 and w != 0.0:  
            [cancel_order(i) for i in oo]  
            order_target_percent(s, w)  
        elif sgn(poss) != sgn(w):  
            [cancel_order(i) for i in oo]  
            order_target_percent(s, w)  

    c.to_200.index = lnp200.index  
    c.to_100.index = lnp100.index  
    c.to_050.index = lnp050.index  
    c.to_030.index = lnp030.index  
    c.to_015.index = lnp015.index  
    c.to_007.index = lnp007.index  
    regc200 = { i : c.regr.fit(pd.DataFrame(c.to_200[i], columns = [i]), pd.DataFrame(lnp200[i], columns = [i]), -1).coef_[-1][-1] for i in c.secs }  
    regc100 = { i : c.regr.fit(pd.DataFrame(c.to_100[i], columns = [i]), pd.DataFrame(lnp100[i], columns = [i]), -1).coef_[-1][-1] for i in c.secs }  
    regc050 = { i : c.regr.fit(pd.DataFrame(c.to_050[i], columns = [i]), pd.DataFrame(lnp050[i], columns = [i]), -1).coef_[-1][-1] for i in c.secs }  
    regc030 = { i : c.regr.fit(pd.DataFrame(c.to_030[i], columns = [i]), pd.DataFrame(lnp030[i], columns = [i]), -1).coef_[-1][-1] for i in c.secs }  
    regc015 = { i : c.regr.fit(pd.DataFrame(c.to_015[i], columns = [i]), pd.DataFrame(lnp015[i], columns = [i]), -1).coef_[-1][-1] for i in c.secs }  
    regc007 = { i : c.regr.fit(pd.DataFrame(c.to_007[i], columns = [i]), pd.DataFrame(lnp007[i], columns = [i]), -1).coef_[-1][-1] for i in c.secs }  

    dsig200 = { i : regc200[i] for i in c.secs }  
    dsig100 = { i : regc100[i] for i in c.secs }  
    dsig050 = { i : regc050[i] for i in c.secs }  
    dsig030 = { i : regc030[i] for i in c.secs }  
    dsig015 = { i : regc015[i] for i in c.secs }  
    dsig007 = { i : regc007[i] for i in c.secs }  

    sig200 = { i : regc200[i] - regc200[c.SPDR['index'][-1]] for i in c.secs }  
    sig100 = { i : regc100[i] - regc100[c.SPDR['index'][-1]] for i in c.secs }  
    sig050 = { i : regc050[i] - regc050[c.SPDR['index'][-1]] for i in c.secs }  
    sig030 = { i : regc030[i] - regc030[c.SPDR['index'][-1]] for i in c.secs }  
    sig015 = { i : regc015[i] - regc015[c.SPDR['index'][-1]] for i in c.secs }  
    sig007 = { i : regc007[i] - regc007[c.SPDR['index'][-1]] for i in c.secs }  
    sig200[c.SPDR['index'][-1]] = 0.0  
    sig100[c.SPDR['index'][-1]] = 0.0  
    sig050[c.SPDR['index'][-1]] = 0.0  
    sig030[c.SPDR['index'][-1]] = 0.0  
    sig015[c.SPDR['index'][-1]] = 0.0  
    sig007[c.SPDR['index'][-1]] = 0.0  

    dw200 = { i : sgn(dsig200[i]) / c.numsecs for i in c.secs }  
    dw100 = { i : sgn(dsig100[i]) / c.numsecs for i in c.secs }  
    dw050 = { i : sgn(dsig050[i]) / c.numsecs for i in c.secs }  
    dw030 = { i : sgn(dsig030[i]) / c.numsecs for i in c.secs }  
    dw015 = { i : sgn(dsig015[i]) / c.numsecs for i in c.secs }  
    dw007 = { i : sgn(dsig007[i]) / c.numsecs for i in c.secs }  

    w200 = { i : sgn(sig200[i]) / c.numsecs for i in c.secs }  
    w100 = { i : sgn(sig100[i]) / c.numsecs for i in c.secs }  
    w050 = { i : sgn(sig050[i]) / c.numsecs for i in c.secs }  
    w030 = { i : sgn(sig030[i]) / c.numsecs for i in c.secs }  
    w015 = { i : sgn(sig015[i]) / c.numsecs for i in c.secs }  
    w007 = { i : sgn(sig007[i]) / c.numsecs for i in c.secs }  
    w200[c.SPDR['index'][-1]] = 0.0  
    w100[c.SPDR['index'][-1]] = 0.0  
    w050[c.SPDR['index'][-1]] = 0.0  
    w030[c.SPDR['index'][-1]] = 0.0  
    w015[c.SPDR['index'][-1]] = 0.0  
    w007[c.SPDR['index'][-1]] = 0.0  
    w200[c.SPDR['index'][-1]] = -np.sum(w200.values())  
    w100[c.SPDR['index'][-1]] = -np.sum(w100.values())  
    w050[c.SPDR['index'][-1]] = -np.sum(w050.values())  
    w030[c.SPDR['index'][-1]] = -np.sum(w030.values())  
    w015[c.SPDR['index'][-1]] = -np.sum(w015.values())  
    w007[c.SPDR['index'][-1]] = -np.sum(w007.values())  
    #w200[c.SPDR['index'][-1]] += sgn(dw200[c.SPDR['index'][-1]])  
    #w100[c.SPDR['index'][-1]] += sgn(dw100[c.SPDR['index'][-1]])  
    #w050[c.SPDR['index'][-1]] += sgn(dw050[c.SPDR['index'][-1]])  
    #w030[c.SPDR['index'][-1]] += sgn(dw030[c.SPDR['index'][-1]])  
    #w015[c.SPDR['index'][-1]] += sgn(dw015[c.SPDR['index'][-1]])  
    #w007[c.SPDR['index'][-1]] += sgn(dw007[c.SPDR['index'][-1]])  
    w = { i : np.array([w007[i], w015[i] / 2., w030[i] / 4., w050[i] / 8., w100[i] / 16., w200[i] / 32.]) for i in c.secs }  
    dw = { i : np.array([dw007[i], dw015[i] / 2., dw030[i] / 4., dw050[i] / 8., dw100[i] * 16., dw200[i] / 32.]) for i in c.secs }  
    #fw = { i : dw007[i] * (1 + c.margine) if sgn(np.sum(w[i]) + np.sum(dw[i])) == dw007[i] else dw007[i] for i in c.SPDR['sectors'] }  
    #fw.update({ i : dw007[i] * (1 + c.margine) if sgn(np.sum(w[i]) + np.sum(dw[i])) == dw007[i] else dw007[i] for i in c.SPDR['index'] })  
    fw = { i : (sgn(np.sum(dw[i] + w[i])) / c.numsecs) if abs(np.sum(dw[i] + w[i])) > (1. / c.numsecs) else 0.0 for i in c.secs }  
    for i in c.secs:  
        wsum = np.sum(np.abs(fw.values()))  
        fw[i] = (fw[i] / wsum) if wsum != 0.0 else fw[i]  
    [initiate_trade(i, fw[i]) for i in c.SPDR['sectors']]  
    [initiate_trade(i, fw[i]) for i in c.SPDR['index']]

Terence,
Do you think you could get this strategy working (or close to it) with a single pair? If so, I could help you expand the model to support several pairs. You can check out this post to get an idea of how we would go about it. This kind of strategy will be much less messy to code if you take a class based approach. It will also be easier to pass different settings to different pairs and to know that everything is working properly. I know you are pretty new to programming, but Python's bread and butter is in it's object oriented features, it's never too early to introduce yourself to them.

EDIT:
At first glance I noticed two things.

  1. In lines 68, 78, 82, and 100 you use 'return' to kick out of the for loop. My guess is that you just want to skip that pair and move onto the next one. You will want to change the 'return' to 'continue' if that's the case. Using return will kick you completely out of the handle_data function, if the first pair triggers that, the rest of them don't get checked. Switching to 'continue' will make the loop stop processing that pair and move onto the next one.

  2. Your ols_transform is returning the slope and intercept of the OLS regression. That is probably messing up later calculations. See below for returning the slope only

# Select the slope only  
slope = sm.OLS(p0, p1).fit().params[sid2]

# return both the slope and intercept separately  
intercept, slope = sm.OLS(p0, p1).fit().params  

David

Hello David,

Thank you for your feedback! I wrote something with the class you provided. I figure my code is really messy and I'd restart it again. However, I am really stuck at line 33 and line 83. The reason why it doesn't run is because line 83 has an error called:
"Runtime exception: AttributeError: 'float' object has no attribute '_data'"

The program ran but it did not generate any trade, perhaps because there was no data. I would be grateful if you can tell me a bit on how I should resolve the problem with line 83.

Terence

Here is the code:
import math
import numpy as np
import scipy as sp
import datetime as dt
import statsmodels.tsa.stattools as ts
import statsmodels.api as sm

window_length = 30
lev = 1.2

def initialize(context):
context.std_multiplier = 1.5
set_commission(commission.PerTrade(cost=1.00))
context.pairs =[ [sid(24), sid(32146)],
[sid(46216), sid(46220)],
[sid(14517), sid(14516)],
[sid(5484), sid(6119)],
[sid(24), sid(5773)],
[sid(46170), sid(46220)],
[sid(46170), sid(46222)],
[sid(46216), sid(46222)],
[sid(46216), sid(46220)]
]

# Set the allocation per stock  
pct_per_algo = 1.0 / (2*len(context.pairs))  

# Make a separate algo for each stock.  
context.algo = [PairTradeSpread(context.pairs[t][0], context.pairs[t][1], pct_per_algo) for t in range (0, (len(context.pairs)))]

def handle_data(context, data):
for algo in context.algo:
algo.handle_data(context, data)

class PairTradeSpread(object):

# Initialize with a single stock and assign a proportion of the account.  
def __init__(self, ticker1, ticker2, allocation):  
    self.ticker1 = ticker1  
    self.ticker2 = ticker2  
    self.allocation = allocation  

@batch_transform(window_length=30, refresh_period=1)  
def avg_ratio(self, price1, price2, datapanel):  

    price1 = datapanel['price'][self.ticker1]  
    price2 = datapanel['price'][self.ticker2]  
    sid1_arr = np.array(price1)  
    sid2_arr = np.array(price2)  

    if sid1_arr is None or sid2_arr is None:  
        return None  

    return (sid1_arr/sid2_arr).mean()  

@batch_transform(window_length=30, refresh_period=1)  
def bounds(self, price1, price2, mean, context, datapanel):  

    price1 = datapanel['price'][self.ticker1]  
    price2 = datapanel['price'][self.ticker2]  
    sid1_arr = np.array(price1)  
    sid2_arr = np.array(price2)  

    if sid1_arr is None or sid2_arr is None:  
            return None  

    std_amt = (sid1_arr/sid2_arr).std()*context.std_multiplier  
    high_bound = mean + std_amt  
    low_bound = mean - std_amt  

    return (high_bound, low_bound)  

def handle_data(self, context, data):

    cash = context.portfolio.portfolio_value/(2*len(context.pairs))  
    price1 = data[self.ticker1].price  
    price2 = data[self.ticker2].price  

    current_ratio = price1/price2  
    avgratio = self.avg_ratio(price1, price2, data)  
    bounds= self.bounds(price1, price2, avgratio, context, data)  
    high_bound, low_bound = bounds  
    last_trade = None  

    if current_ratio > high_bound and last_trade is not "high":  
        order_value(self.ticker2, +lev*cash)  
        order_value(self.ticker1, -lev*cash)  
        last_trade = "high"  
    elif current_ratio < low_bound and last_trade is not "low":  
        order_value(self.ticker1, +lev*cash)  
        order_value(self.ticker2, -lev*cash)  
        last_trade = "low"  
    elif last_trade is not "mid" and last_trade is "high" and current_ratio < avgratio:  
        order_target(self.ticker1, 0)  
        order_target(self.ticker2, 0)  
        last_trade = "mid"  
    elif last_trade is not "mid" and last_trade is "low" and current_ratio > avgratio:  
        order_target(self.ticker1, 0)  
        order_target(self.ticker2, 0)  
        last_trade = "mid"  
"""  

class PairTradeZscore(object):

def __init__(self, ticker1, ticker2, allocation):  
    self.ticker1 = ticker1  
    self.ticker2 = ticker2  
    self.allocation = allocation  

def ols(self, prices, price1, price2):

    p0 = prices[price1]  
    p1 = sm.add_constant(prices[price2], prepend=True)  
    slope=sm.OLS(p0, p1).fit().params[0]  
    return slope  

def zscore(self, price1, price2, slope, context):  

    slope = self.ols(prices, price1, price2)  
    spread = price1 - (slope * price2)  
    context.spreads.append(spread)  
    zscore = (spread - np.mean(spreads[-window_length:])) / np.std(spreads[-window_length:])  
    return zscore  

def handle_data(self, context, data):  

    cash = context.portfolio.portfolio_value/(2*len(context.pairs))  
    slope = self.ols(prices, price1, price2)  
    zscore = self.zscore(price1, price2, slope, context)  

    if zscore > 2:  
        order_value(self.ticker2, +lev*cash)  
        order_value(self.ticker1, -lev*cash)  
    elif zscore < -2:  
        order_value(self.ticker1, +lev*cash)  
        order_value(self.ticker2, -lev*cash)  
    elif zscore < 0.5 and zscore > -0.5:  
        order_target(self.ticker1, 0)  
        order_target(self.ticker2, 0)  

"""  

If you format the code ```` it would be easy. Use 3 instead of 4. Ctrl + K also does the formatting.

I cleaned up your version and got it running, there is probably some conflicting orders going on because you have the same stock in several pairs. You will want to check your accounting, maybe aggregate the orders so that you can submit as few as possible.

It looks to trade every 30 min and I switched you from batch_transform back to history because batch_transform is being depreciated in favor of history. Feel free to message me if you have any questions.

David

Clone Algorithm
7
Loading...
Backtest from to with initial capital
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
import math
import numpy as np
import scipy as sp
import datetime as dt
import statsmodels.tsa.stattools as ts
import statsmodels.api as sm

window_length = 30
lev = 1.2

def initialize(context):
    context.std_multiplier = 1.5
    set_commission(commission.PerTrade(cost=1.00))
    context.pairs =[ 
        [sid(24),    sid(32146)],
        [sid(46216), sid(46220)],
        [sid(14517), sid(14516)],
        [sid(5484),  sid(6119)],
        [sid(24),    sid(5773)],
        [sid(46170), sid(46220)],
        [sid(46170), sid(46222)],
        [sid(46216), sid(46222)],
        [sid(46216), sid(46220)]
    ]

# Set the allocation per stock  
    pct_per_algo = 1.0 / (2*len(context.pairs))  

# Make a separate algo for each stock.  
    context.algo = [PairTradeSpread(context.pairs[t][0], 
                                    context.pairs[t][1], pct_per_algo) 
                    for t in range (0, (len(context.pairs)))]
    
def handle_data(context, data):
    for algo in context.algo:
        try:
            algo.handle_data(context, data)
        except Exception as e:
            log.debug(e)
            log.info([algo.ticker1.symbol, algo.ticker2.symbol])

class PairTradeSpread(object):

# Initialize with a single stock and assign a proportion of the account.  
    def __init__(self, ticker1, ticker2, allocation):  
        self.ticker1 = ticker1  
        self.ticker2 = ticker2  
        self.allocation = allocation  

   
    def avg_ratio(self):  
        prices = history(30, '1d', 'price')
        price1 = prices[self.ticker1]  
        price2 = prices[self.ticker2]  
        return (price1/price2).mean()  

    def bounds(self, context):  
        prices = history(30, '1d', 'price')
        price1 = prices[self.ticker1]  
        price2 = prices[self.ticker2]
        std_amt = (price1 / price2).std() * context.std_multiplier  
        mean = self.avg_ratio()
        high_bound = mean + std_amt
        low_bound = mean - std_amt
        return (high_bound, low_bound)  

    
    def handle_data(self, context, data):
        now = get_datetime()
        
        # Constrain to only trade every half hour
        if now.minute % 30:
            return
        
        if 'price' not in data[self.ticker1] or 'price' not in data[self.ticker2]:
            log.info('No price data for pair: (%s, %s)'%(
                self.ticker1.symbol, self.ticker2.symbol
            ))
            return
        
        open_orders = get_open_orders()       
        if self.ticker1 in open_orders or self.ticker2 in open_orders:
            return
        
        cash = context.portfolio.portfolio_value * self.allocation
        
        price1 = data[self.ticker1].price  
        price2 = data[self.ticker2].price  

        current_ratio = price1 / price2  
        avgratio = self.avg_ratio()  
        high_bound, low_bound = self.bounds(context)  
        last_trade = None  
        
        # lev is undefined
        lev = 1
        
        if current_ratio > high_bound and last_trade is not "high":  
            order_value(self.ticker2, +lev*cash)  
            order_value(self.ticker1, -lev*cash)  
            last_trade = "high"  
        elif current_ratio < low_bound and last_trade is not "low":  
            order_value(self.ticker1, +lev*cash)  
            order_value(self.ticker2, -lev*cash)  
            last_trade = "low"  
        elif last_trade is not "mid" and last_trade is "high" and current_ratio < avgratio:  
            order_target(self.ticker1, 0)  
            order_target(self.ticker2, 0)  
            last_trade = "mid"  
        elif last_trade is not "mid" and last_trade is "low" and current_ratio > avgratio:  
            order_target(self.ticker1, 0)  
            order_target(self.ticker2, 0)  
            last_trade = "mid"  

# class PairTradeZscore(object):
# 
    # def __init__(self, ticker1, ticker2, allocation):  
        # self.ticker1 = ticker1  
        # self.ticker2 = ticker2  
        # self.allocation = allocation  
# 
    # def ols(self, prices, price1, price2):
# 
        # p0 = prices[price1]  
        # p1 = sm.add_constant(prices[price2], prepend=True)  
        # slope=sm.OLS(p0, p1).fit().params[0]  
        # return slope  
# 
    # def zscore(self, price1, price2, slope, context):  
# 
        # slope = self.ols(prices, price1, price2)  
        # spread = price1 - (slope * price2)  
        # context.spreads.append(spread)  
        # zscore = (spread - np.mean(spreads[-window_length:])) / np.std(spreads[-window_length:])  
        # return zscore  
# 
    # def handle_data(self, context, data):  
# 
        # cash = context.portfolio.portfolio_value/(2*len(context.pairs))  
        # slope = self.ols(prices, price1, price2)  
        # zscore = self.zscore(price1, price2, slope, context)  
# 
        # if zscore > 2:  
            # order_value(self.ticker2, +lev*cash)  
            # order_value(self.ticker1, -lev*cash)  
        # elif zscore < -2:  
            # order_value(self.ticker1, +lev*cash)  
            # order_value(self.ticker2, -lev*cash)  
        # elif zscore < 0.5 and zscore > -0.5:  
            # order_target(self.ticker1, 0)  
            # order_target(self.ticker2, 0)  
There was a runtime error.

Added cash and capital to the plots to make sure there is excess leverage. A good start I think.

Clone Algorithm
1
Loading...
Backtest from to with initial capital
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
import math
import numpy as np
import scipy as sp
import datetime as dt
import statsmodels.tsa.stattools as ts
import statsmodels.api as sm

window_length = 30
lev = 1.2

def initialize(context):
    context.std_multiplier = 1.5
    set_commission(commission.PerTrade(cost=1.00))
    context.pairs =[ 
        [sid(24),    sid(32146)],
        [sid(46216), sid(46220)],
        [sid(14517), sid(14516)],
        [sid(5484),  sid(6119)],
        [sid(24),    sid(5773)],
        [sid(46170), sid(46220)],
        [sid(46170), sid(46222)],
        [sid(46216), sid(46222)],
        [sid(46216), sid(46220)]
    ]

# Set the allocation per stock  
    pct_per_algo = 1.0 / (2*len(context.pairs))  

# Make a separate algo for each stock.  
    context.algo = [PairTradeSpread(context.pairs[t][0], 
                                    context.pairs[t][1], pct_per_algo) 
                    for t in range (0, (len(context.pairs)))]
    
def handle_data(context, data):
    for algo in context.algo:
        try:
            algo.handle_data(context, data)
        except Exception as e:
            log.debug(e)
            log.info([algo.ticker1.symbol, algo.ticker2.symbol])

class PairTradeSpread(object):

# Initialize with a single stock and assign a proportion of the account.  
    def __init__(self, ticker1, ticker2, allocation):  
        self.ticker1 = ticker1  
        self.ticker2 = ticker2  
        self.allocation = allocation  

   
    def avg_ratio(self):  
        prices = history(30, '1d', 'price')
        price1 = prices[self.ticker1]  
        price2 = prices[self.ticker2]  
        return (price1/price2).mean()  

    def bounds(self, context):  
        prices = history(30, '1d', 'price')
        price1 = prices[self.ticker1]  
        price2 = prices[self.ticker2]
        std_amt = (price1 / price2).std() * context.std_multiplier  
        mean = self.avg_ratio()
        high_bound = mean + std_amt
        low_bound = mean - std_amt
        return (high_bound, low_bound)  

    
    def handle_data(self, context, data):
        now = get_datetime()
        
        # Constrain to only trade every half hour
        if now.minute % 30:
            return
        
        if 'price' not in data[self.ticker1] or 'price' not in data[self.ticker2]:
            log.info('No price data for pair: (%s, %s)'%(
                self.ticker1.symbol, self.ticker2.symbol
            ))
            return
        
        open_orders = get_open_orders()       
        if self.ticker1 in open_orders or self.ticker2 in open_orders:
            return
        
        cash = context.portfolio.portfolio_value * self.allocation
        
        price1 = data[self.ticker1].price  
        price2 = data[self.ticker2].price  

        current_ratio = price1 / price2  
        avgratio = self.avg_ratio()  
        high_bound, low_bound = self.bounds(context)  
        last_trade = None  
        
        # lev is undefined
        lev = 1
        
        if current_ratio > high_bound and last_trade is not "high":  
            order_value(self.ticker2, +lev*cash)  
            order_value(self.ticker1, -lev*cash)  
            last_trade = "high"  
        elif current_ratio < low_bound and last_trade is not "low":  
            order_value(self.ticker1, +lev*cash)  
            order_value(self.ticker2, -lev*cash)  
            last_trade = "low"  
        elif last_trade is not "mid" and last_trade is "high" and current_ratio < avgratio:  
            order_target(self.ticker1, 0)  
            order_target(self.ticker2, 0)  
            last_trade = "mid"  
        elif last_trade is not "mid" and last_trade is "low" and current_ratio > avgratio:  
            order_target(self.ticker1, 0)  
            order_target(self.ticker2, 0)  
            last_trade = "mid"
            
        record(cash = context.portfolio.cash, capital = context.portfolio.capital_used)

# class PairTradeZscore(object):
# 
    # def __init__(self, ticker1, ticker2, allocation):  
        # self.ticker1 = ticker1  
        # self.ticker2 = ticker2  
        # self.allocation = allocation  
# 
    # def ols(self, prices, price1, price2):
# 
        # p0 = prices[price1]  
        # p1 = sm.add_constant(prices[price2], prepend=True)  
        # slope=sm.OLS(p0, p1).fit().params[0]  
        # return slope  
# 
    # def zscore(self, price1, price2, slope, context):  
# 
        # slope = self.ols(prices, price1, price2)  
        # spread = price1 - (slope * price2)  
        # context.spreads.append(spread)  
        # zscore = (spread - np.mean(spreads[-window_length:])) / np.std(spreads[-window_length:])  
        # return zscore  
# 
    # def handle_data(self, context, data):  
# 
        # cash = context.portfolio.portfolio_value/(2*len(context.pairs))  
        # slope = self.ols(prices, price1, price2)  
        # zscore = self.zscore(price1, price2, slope, context)  
# 
        # if zscore > 2:  
            # order_value(self.ticker2, +lev*cash)  
            # order_value(self.ticker1, -lev*cash)  
        # elif zscore < -2:  
            # order_value(self.ticker1, +lev*cash)  
            # order_value(self.ticker2, -lev*cash)  
        # elif zscore < 0.5 and zscore > -0.5:  
            # order_target(self.ticker1, 0)  
            # order_target(self.ticker2, 0)  
There was a runtime error.

Hello David and Suminda,

Thank you both for helping me out! I combined with what you both suggested and did a back-test, for a short-term period. I'm not sure whether the program will trade excessive leverage again. Please let me know how I can improve this model, I would be grateful for any feedback!

Terence

Clone Algorithm
56
Loading...
Backtest from to with initial capital
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
import statsmodels.api as sm
import numpy as np
import pandas as pd

def initialize(context):
    set_commission(commission.PerTrade(cost=1.00))
    context.std_multiplier = 2
    context.pair_list=[
                       [sid(1335),sid(20088)],
                       [sid(122),sid(20680)],
                       [sid(5484),sid(6119)],
                       [sid(2214),sid(6868)] 
                       ]
 
    pct_per_algo= 1.0 / len(context.pair_list)
    
    context.algos=pd.Series([Pair_Trade(context.pair_list[t][0], context.pair_list[t][1], pct_per_algo) for t in range(0, (len(context.pair_list)))])
    
def handle_data(context, data):
    context.algos.apply(lambda algo: algo.handle_data(context, data))

class Pair_Trade(object):
    
    def __init__(self, stk0, stk1, pct_of_account):
        self.stk0=stk0
        self.stk1=stk1
        self.pct=pct_of_account

    def handle_data(self, context, data):
        cash=context.portfolio.cash*self.pct
        price_history = history(bar_count=756, frequency='1d', field='price') #3 years
        price_history = price_history.fillna(method='ffill')
        position0=context.portfolio.positions[self.stk0].amount
        position1=context.portfolio.positions[self.stk1].amount
        price1 = data[self.stk0].price  
        price2 = data[self.stk1].price  
        share0 = int(cash / data[self.stk0].price)
        share1 = int(cash / data[self.stk1].price)
        #get the hedge spread ratio and its zscore
        beta = self.ols_transform(price_history, self.stk0, self.stk1) 
        spread=data[self.stk1].price - beta*(data[self.stk0].price)
        rVal = self.getMeanStd(price_history, beta, self.stk0, self.stk1)
        meanPrice, stdPrice = rVal
        z = (spread - meanPrice)/stdPrice
        
        #get the spread ratio and its zscore, as well as high, low bounds
        values = self.getZscore(price_history, self.stk0, self.stk1)
        mean, std = values
        current_ratio=price1/price2
        ##spread = data[self.stk1].price/data[self.stk0].price 
        zscore = (current_ratio - mean)/std
        avgratio = self.avg_ratio()  
        high_bound, low_bound = self.bounds(context)  
        last_trade = None  
        
        if z>=1.65 and position0 == 0 and position1 == 0: 
            num_shares = int(cash / data[self.stk1].price)
            order(self.stk1, num_shares)
            order(self.stk0, -1 * num_shares/beta)

        elif z<=-1.65 and position0 == 0 and position1 == 0: 
            num_shares = int(cash / data[self.stk0].price)
            order(self.stk0, num_shares)
            order(self.stk1, -1 * num_shares*beta)
            
        elif zscore >=2.33 and position0 != 0 and  position1 != 0 and current_ratio > high_bound and last_trade is not "high":  
            order(self.stk1, share1*(2/3))
            order(self.stk0, -1*share0*(2/3))
            last_trade = "high"  
            
        elif zscore<-2.33 and position0 != 0 and position1 != 0 and current_ratio < low_bound and last_trade is not "low":  
            order(self.stk0, share0*(2/3))
            order(self.stk1, -1*share1*(2/3))
            last_trade = "low"
            
        elif zscore >=1.65 and zscore <2.33 and position0 != 0 and position1 != 0:
            order(self.stk1, share1*(1/3))
            order(self.stk0, -1*share0*(1/3))
            
        elif zscore <=-1.65 and zscore >-2.33 and position0 != 0 and  position1 != 0:
            order(self.stk0, share0*(1/3))
            order(self.stk1, -1*share1*(1/3))
            
        elif abs(zscore)<=0.5 and position0 != 0 and position1 != 0 and last_trade is not "mid" and last_trade is "high" and current_ratio < avgratio: 
            order_target_value(self.stk0, 0)
            order_target_value(self.stk1, 0) 
            last_trade = "mid"  
            
        elif abs(zscore)<=0.5 and position0 != 0 and position1 != 0 and last_trade is not "mid" and last_trade is "low" and current_ratio > avgratio:
            order_target_value(self.stk0, 0)
            order_target_value(self.stk1, 0) 
            last_trade = "mid"  
   
        elif abs(z)<=1 and abs(zscore)>=0.5 and position0 != 0 and position1 != 0:
            order_target_value(self.stk0, 0)
            order_target_value(self.stk1, 0) 
            
        record(cash=context.portfolio.cash, capital = context.portfolio.capital_used)
        
    def avg_ratio(self):  
        prices = history(756, '1d', 'price')
        price1 = prices[self.stk0]  
        price2 = prices[self.stk1]  
        return (price1/price2).mean()  

    def bounds(self, context):  
        prices = history(756, '1d', 'price')
        price1 = prices[self.stk0]  
        price2 = prices[self.stk1]
        std_amt = (price1 / price2).std() * context.std_multiplier  
        mean = self.avg_ratio()
        high_bound = mean + std_amt
        low_bound = mean - std_amt
        return (high_bound, low_bound)  
        
    #get the zscore of the spread ratio
    def getZscore(self, prices, sid0, sid1):
        spread=prices[sid1]/prices[sid0]
        mean=np.mean(spread)
        std=np.std(spread)
        return mean, std
    
    #get the zscore of the hedge spread ratio
    def getMeanStd(self, prices, beta, sid0, sid1):
        meanPrice = np.mean(prices[sid1] - beta*prices[sid0])
        stdPrice= np.std(prices[sid1] - beta*prices[sid0])
        if meanPrice is not None and stdPrice is not None :
            return (meanPrice, stdPrice)
        else:
            return None
    #get the slope for the hedge spread ratio
    def ols_transform(self, prices, sid0, sid1):
        prices = prices.fillna(method='bfill')
        p0 = prices[sid0].values
        p1 = prices[sid1].values
        
        slope = sm.OLS(p0, p1).fit().params[0]
        return slope
        
There was a runtime error.

On lines 67,68, 72,73, and the other similar ones, you need to make either the numerator or denominator a float in the ratio at the end. Python 2.x uses floor division for integers, so 2/3 = 0, but 2.0/3 = 0.66666, that is going to give you odd results.

Hi David,

I edited the code and ran a lot more back-tests. I realized that the trading signal can maintain for a long period of time, making the algorithm to trade non-stop to buy over 100k, short 100k. As for line 61:

context.portfolio.cash + context.portfolio.capital_used> sum(abs(price1*position0) + abs(price2*position1) for t in range (0,)):  

I am trying to get a sum of my positions so that it does not exceed my total amount of cash and capital used, but I don't know how to define the range..

I hope you can give me some feedback on whether or not this program will still trade non-stop in the future and defining the range.

Thank you for helping me!

Sincerely,
Terence

Clone Algorithm
56
Loading...
Backtest from to with initial capital
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
import statsmodels.api as sm
import numpy as np
import pandas as pd

def initialize(context):
    set_commission(commission.PerTrade(cost=1.00))
    context.std_multiplier = 2
    context.notional = 5000
    context.pair_list=[
                       [sid(1335),sid(20088)],
                       [sid(122),sid(20680)],
                       [sid(5484),sid(6119)],
                       [sid(2214),sid(6868)],
                       [sid(12691),sid(698)],
                       [sid(41969),sid(8554)]
                       ]
 
    pct_per_algo= 1.0 / len(context.pair_list)
    
    context.algos=pd.Series([Pair_Trade(context.pair_list[t][0], context.pair_list[t][1], pct_per_algo) for t in range(0, (len(context.pair_list)))])
    
def handle_data(context, data):
    context.algos.apply(lambda algo: algo.handle_data(context, data))

class Pair_Trade(object):
    
    def __init__(self, stk0, stk1, pct_of_account):
        self.stk0=stk0
        self.stk1=stk1
        self.pct=pct_of_account

    def handle_data(self, context, data):
        #cash=context.portfolio.cash*self.pct
        cash=context.notional*self.pct
        price_history = history(bar_count=500, frequency='1d', field='price') #2 years
        price_history = price_history.fillna(method='ffill')
        position0=context.portfolio.positions[self.stk0].amount
        position1=context.portfolio.positions[self.stk1].amount
        price1 = data[self.stk0].price  
        price2 = data[self.stk1].price  
        share0 = int(cash / data[self.stk0].price)
        share1 = int(cash / data[self.stk1].price)
       # Pair_val = sum(abs(price1*position0 + price2*position1)) get the value of the pair, make sure that it is smaller than the available cash.
        
        #get the hedge spread ratio and its zscore
        beta = self.ols_transform(price_history, self.stk0, self.stk1) 
        spread=data[self.stk1].price - beta*(data[self.stk0].price)
        rVal = self.getMeanStd(price_history, beta, self.stk0, self.stk1)
        meanPrice, stdPrice = rVal
        z = (spread - meanPrice)/stdPrice
        
        #get the spread ratio and its zscore, as well as high, low bounds
        values = self.getZscore(price_history, self.stk0, self.stk1)
        mean, std = values
        current_ratio=price1/price2 
        zscore = (current_ratio - mean)/std
        avgratio = self.avg_ratio(price_history)  
        high_bound, low_bound = self.bounds(context,price_history)  
        last_trade = None  
        
        if cash > (abs(price1*position0) + abs(price2*position1)) and cash > sum(abs(price1*position0) + abs(price2*position1) for t in range (0,)):
            if z>=1.65 and position0 == 0 and position1 == 0: 
                num_shares = int(cash / price2)
                order(self.stk1, num_shares)
                order(self.stk0, -1 * num_shares/beta)
            elif z<=-1.65 and position0 == 0 and position1 == 0: 
                num_shares = int(cash / price1)
                order(self.stk0, num_shares)
                order(self.stk1, -1 * num_shares*beta)
            elif zscore >=2 and position0 == 0 and  position1 == 0 and current_ratio > high_bound and last_trade is not "high":  
                order(self.stk1, share1)
                order(self.stk0, -1*share0)
                last_trade = "high"  
            elif zscore<-2 and position0 == 0 and position1 == 0 and current_ratio < low_bound and last_trade is not "low":  
                order(self.stk0, share0)
                order(self.stk1, -1*share1)
                last_trade = "low"
            elif abs(zscore)<=0.5 and position0 != 0 and position1 != 0 and last_trade is not "mid" and last_trade is "high" and current_ratio < avgratio:
                order_target_value(self.stk0, 0)
                order_target_value(self.stk1, 0) 
                last_trade = "mid"  
            elif abs(zscore)<=0.5 and position0 != 0 and position1 != 0 and last_trade is not "mid" and last_trade is "low" and current_ratio > avgratio:
                order_target_value(self.stk0, 0)
                order_target_value(self.stk1, 0) 
                last_trade = "mid"  
            elif abs(z)<=1 and abs(zscore)>=0.5 and position0 != 0 and position1 != 0:
                order_target_value(self.stk0, 0)
                order_target_value(self.stk1, 0) 
            record(cash=context.portfolio.cash, capital = context.portfolio.capital_used, position_val=sum(abs(price1*position0) +abs( price2*position1) for t in range (0,)))
            
        elif cash <= (abs(price1*position0) + abs(price2*position1)) and cash <= sum((abs(price1*position0) + abs(price2*position1)) for t in range (0,)): 
            return
 
    def avg_ratio(self, prices):  
        #prices = history(756, '1d', 'price')
        price1 = prices[self.stk0]  
        price2 = prices[self.stk1]  
        return (price1/price2).mean()  

    def bounds(self,context,prices):  
        #prices = history(756, '1d', 'price')
        price1 = prices[self.stk0]  
        price2 = prices[self.stk1]
        std_amt = (price1 / price2).std() * context.std_multiplier  
        mean = self.avg_ratio(prices)
        high_bound = mean + std_amt
        low_bound = mean - std_amt
        return (high_bound, low_bound)  
        
    #get the zscore of the spread ratio
    def getZscore(self, prices, sid0, sid1):
        spread=prices[sid1]/prices[sid0]
        mean=np.mean(spread)
        std=np.std(spread)
        return mean, std
    
    #get the zscore of the hedge spread ratio
    def getMeanStd(self, prices, beta, sid0, sid1):
        meanPrice = np.mean(prices[sid1] - beta*prices[sid0])
        stdPrice= np.std(prices[sid1] - beta*prices[sid0])
        if meanPrice is not None and stdPrice is not None :
            return (meanPrice, stdPrice)
        else:
            return None
        
    #get the slope for the hedge spread ratio
    def ols_transform(self, prices, sid0, sid1):
        prices = prices.fillna(method='bfill')
        p0 = prices[sid0].values
        p1 = prices[sid1].values
        
        slope = sm.OLS(p0, p1).fit().params[0]
        return slope
    """
        elif zscore >=1.65 and zscore <2.33 and position0 != 0 and position1 != 0:
            order(self.stk1, share1/3.0)
            order(self.stk0, -1*share0/3.0)
            
        elif zscore <=-1.65 and zscore >-2.33 and position0 != 0 and  position1 != 0:
            order(self.stk0, share0/3.0)
            order(self.stk1, -1*share1/3.0)
             
        elif cash <= (abs(price1*position0) + abs(price2*position1)) and cash <= sum((abs(price1*position0) + abs(price2*position1)) for t in range (0,)): 
            return
       
    """
There was a runtime error.

Terence, it looks like you are getting closer. I added a few lines and record statements to help track how money is spent.

I commented out all but one pair and It looks like it only traded on that first day. I'd go through and test it with one pair while you get the PairTrade class working properly, then add the other pairs. My guess is that it goes outside of its leverage requirement and doesn't place any more trades as a result.

Clone Algorithm
2
Loading...
Backtest from to with initial capital
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
import statsmodels.api as sm
import numpy as np
import pandas as pd

def initialize(context):
    set_commission(commission.PerTrade(cost=1.00))
    context.std_multiplier = 2
    context.notional = 5000
    context.pair_list=[
                       [sid(1335),sid(20088)],
                       # [sid(122),sid(20680)],
                       # [sid(5484),sid(6119)],
                       # [sid(2214),sid(6868)],
                       # [sid(12691),sid(698)],
                       # [sid(41969),sid(8554)]
                       ]
 
    pct_per_algo= 1.0 / len(context.pair_list)
    
    context.algos=pd.Series([Pair_Trade(pair[0], pair[1], pct_per_algo) 
                             for pair in context.pair_list])
    
def handle_data(context, data):
    #
    # Record your total portfolio leverage and market exposure
    #
    P = context.portfolio
    positions = P.positions
    market_value = sum(abs(positions[stock].amount * data[stock].price) 
                       for stock in data)
    
    leverage = market_value / P.portfolio_value
    exposure = P.positions_value / P.portfolio_value
    
    record(leverage=leverage, exposure=exposure)
    
    context.algos.apply(lambda algo: algo.handle_data(context, data))

class Pair_Trade(object):
    
    def __init__(self, stk0, stk1, pct_of_account):
        self.stk0=stk0
        self.stk1=stk1
        self.pct=pct_of_account

    def handle_data(self, context, data):
        #cash=context.portfolio.cash*self.pct
        cash=context.notional*self.pct
        price_history = history(bar_count=500, frequency='1d', field='price') #2 years
        price_history = price_history.fillna(method='ffill')
        position0=context.portfolio.positions[self.stk0].amount
        position1=context.portfolio.positions[self.stk1].amount
        price1 = data[self.stk0].price  
        price2 = data[self.stk1].price  
        share0 = int(cash / data[self.stk0].price)
        share1 = int(cash / data[self.stk1].price)
        
        market_value = abs(position0 * price1) + abs(position1 * price2)
        
        leverage = market_value / cash
        # record(algo_leverage=leverage)
       # Pair_val = sum(abs(price1*position0 + price2*position1)) get the value of the pair, make sure that it is smaller than the available cash.
        
        #get the hedge spread ratio and its zscore
        beta = self.ols_transform(price_history, self.stk0, self.stk1) 
        spread=data[self.stk1].price - beta*(data[self.stk0].price)
        rVal = self.getMeanStd(price_history, beta, self.stk0, self.stk1)
        meanPrice, stdPrice = rVal
        z = (spread - meanPrice)/stdPrice
        
        #get the spread ratio and its zscore, as well as high, low bounds
        values = self.getZscore(price_history, self.stk0, self.stk1)
        mean, std = values
        current_ratio=price1/price2 
        zscore = (current_ratio - mean)/std
        avgratio = self.avg_ratio(price_history)  
        high_bound, low_bound = self.bounds(context,price_history)  
        last_trade = None  
        
        
        
        # if cash > (abs(price1*position0) + abs(price2*position1)) and cash > sum(abs(price1*position0) + abs(price2*position1) for t in range (0,)):
        if leverage <= 1:
            if z>=1.65 and position0 == 0 and position1 == 0: 
                num_shares = int(cash / price2)
                order(self.stk1, num_shares)
                order(self.stk0, -1 * num_shares/beta)
            elif z<=-1.65 and position0 == 0 and position1 == 0: 
                num_shares = int(cash / price1)
                order(self.stk0, num_shares)
                order(self.stk1, -1 * num_shares*beta)
            elif zscore >=2 and position0 == 0 and  position1 == 0 and current_ratio > high_bound and last_trade is not "high":  
                order(self.stk1, share1)
                order(self.stk0, -1*share0)
                last_trade = "high"  
            elif zscore<-2 and position0 == 0 and position1 == 0 and current_ratio < low_bound and last_trade is not "low":  
                order(self.stk0, share0)
                order(self.stk1, -1*share1)
                last_trade = "low"
            elif abs(zscore)<=0.5 and position0 != 0 and position1 != 0 and last_trade is not "mid" and last_trade is "high" and current_ratio < avgratio:
                order_target_value(self.stk0, 0)
                order_target_value(self.stk1, 0) 
                last_trade = "mid"  
            elif abs(zscore)<=0.5 and position0 != 0 and position1 != 0 and last_trade is not "mid" and last_trade is "low" and current_ratio > avgratio:
                order_target_value(self.stk0, 0)
                order_target_value(self.stk1, 0) 
                last_trade = "mid"  
            elif abs(z)<=1 and abs(zscore)>=0.5 and position0 != 0 and position1 != 0:
                order_target_value(self.stk0, 0)
                order_target_value(self.stk1, 0) 
            # record(cash=context.portfolio.cash, capital = context.portfolio.capital_used, position_val=sum(abs(price1*position0) +abs( price2*position1) for t in range (0,)))
            
        # elif cash <= (abs(price1*position0) + abs(price2*position1)) and cash <= sum((abs(price1*position0) + abs(price2*position1)) for t in range (0,)): 
        else:
            return
 
    def avg_ratio(self, prices):  
        #prices = history(756, '1d', 'price')
        price1 = prices[self.stk0]  
        price2 = prices[self.stk1]  
        return (price1/price2).mean()  

    def bounds(self,context,prices):  
        #prices = history(756, '1d', 'price')
        price1 = prices[self.stk0]  
        price2 = prices[self.stk1]
        std_amt = (price1 / price2).std() * context.std_multiplier  
        mean = self.avg_ratio(prices)
        high_bound = mean + std_amt
        low_bound = mean - std_amt
        return (high_bound, low_bound)  
        
    #get the zscore of the spread ratio
    def getZscore(self, prices, sid0, sid1):
        spread=prices[sid1]/prices[sid0]
        mean=np.mean(spread)
        std=np.std(spread)
        return mean, std
    
    #get the zscore of the hedge spread ratio
    def getMeanStd(self, prices, beta, sid0, sid1):
        meanPrice = np.mean(prices[sid1] - beta*prices[sid0])
        stdPrice= np.std(prices[sid1] - beta*prices[sid0])
        if meanPrice is not None and stdPrice is not None :
            return (meanPrice, stdPrice)
        else:
            return None
        
    #get the slope for the hedge spread ratio
    def ols_transform(self, prices, sid0, sid1):
        prices = prices.fillna(method='bfill')
        p0 = prices[sid0].values
        p1 = prices[sid1].values
        
        slope = sm.OLS(p0, p1).fit().params[0]
        return slope
    """
        elif zscore >=1.65 and zscore <2.33 and position0 != 0 and position1 != 0:
            order(self.stk1, share1/3.0)
            order(self.stk0, -1*share0/3.0)
            
        elif zscore <=-1.65 and zscore >-2.33 and position0 != 0 and  position1 != 0:
            order(self.stk0, share0/3.0)
            order(self.stk1, -1*share1/3.0)
             
        elif cash <= (abs(price1*position0) + abs(price2*position1)) and cash <= sum((abs(price1*position0) + abs(price2*position1)) for t in range (0,)): 
            return
       
    """
There was a runtime error.

Hello David,

I tried again with a few more backtests. I realized that my tests would all have an error issue towards october 2006 when running the test from 2002-2014. However, there is no error issue if I run from 2006-2014. This is the backtest for one year period. It trades normally but I am sure that the leverage problem is not fixed. In this problem, I realized the leverage is only based on one pair position and its allocated cash sum. I made it so that it takes into account for the portfolio value in another version.

I wonder if you can help me on limiting gross value, based on the sum of each pair's gross position, so that the program will not trade excessively.

Thank you again for your inputs and feedback!

Sincerely,
Terence Liu

Clone Algorithm
7
Loading...
Backtest from to with initial capital
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
import statsmodels.api as sm
import numpy as np
import pandas as pd

def initialize(context):
    set_commission(commission.PerTrade(cost=1.00))
    context.std_multiplier = 1.5
    #context.notional = 5000
    context.pair_list=[
                       [sid(1335),sid(20088)],
                        [sid(122),sid(20680)],
                        [sid(5484),sid(6119)],
                        [sid(2214),sid(6868)],
                        [sid(12691),sid(698)],
                        [sid(41969),sid(8554)]
                       ]
 
    pct_per_algo= 1.0 / len(context.pair_list)
    
    context.algos=pd.Series([Pair_Trade(pair[0], pair[1], pct_per_algo) 
                             for pair in context.pair_list])
    
def handle_data(context, data):
    #
    # Record your total portfolio leverage and market exposure
    #
    P = context.portfolio
    positions = P.positions
    market_value = sum(abs(positions[stock].amount * data[stock].price) 
                       for stock in data)
    
    leverage = market_value / P.portfolio_value
    exposure = P.positions_value / P.portfolio_value
    
    record(leverage=leverage, exposure=exposure)
    
    context.algos.apply(lambda algo: algo.handle_data(context, data))

class Pair_Trade(object):
    
    def __init__(self, stk0, stk1, pct_of_account):
        self.stk0=stk0
        self.stk1=stk1
        self.pct=pct_of_account

    def handle_data(self, context, data):
        
        freecash=context.portfolio.cash - context.portfolio.positions_value
        cash=context.portfolio.cash*self.pct
        price_history = history(bar_count=750, frequency='1d', field='price') #3 years
        price_history = price_history.fillna(method='ffill')
        position0=context.portfolio.positions[self.stk0].amount
        position1=context.portfolio.positions[self.stk1].amount
        price1 = data[self.stk0].price  
        price2 = data[self.stk1].price  
        share0 = int(cash / data[self.stk0].price)
        share1 = int(cash / data[self.stk1].price)
        
        market_value = abs(position0 * price1) + abs(position1 * price2)
        
        leverage = market_value / cash
        # record(algo_leverage=leverage)
       # Pair_val = sum(abs(price1*position0 + price2*position1)) get the value of the pair, make sure that it is smaller than the available cash.
        
        #get the hedge spread ratio and its zscore
        beta = self.ols_transform(price_history, self.stk0, self.stk1) 
        spread=data[self.stk1].price - beta*(data[self.stk0].price)
        rVal = self.getMeanStd(price_history, beta, self.stk0, self.stk1)
        meanPrice, stdPrice = rVal
        z = (spread - meanPrice)/stdPrice
        
        #get the spread ratio and its zscore, as well as high, low bounds
        values = self.getZscore(price_history, self.stk0, self.stk1)
        mean, std = values
        current_ratio=price1/price2 
        zscore = (current_ratio - mean)/std
        avgratio = self.avg_ratio(price_history)  
        high_bound, low_bound = self.bounds(context,price_history)  
        last_trade = None  
        
        
        
        # if cash > (abs(price1*position0) + abs(price2*position1)) and cash > sum(abs(price1*position0) + abs(price2*position1) for t in range (0,)):
        if leverage <= 1.01 and leverage >= 0 and context.portfolio.cash > 0 and freecash > 0:
            
            if z>=1.65 and position0 == 0 and position1 == 0: 
                num_shares = int(cash / price2)
                order(self.stk1, num_shares)
                order(self.stk0, -1 * num_shares/beta)
            elif z<=-1.65 and position0 == 0 and position1 == 0: 
                num_shares = int(cash / price1)
                order(self.stk0, num_shares)
                order(self.stk1, -1 * num_shares*beta)
            elif zscore >=2 and position0 == 0 and  position1 == 0 and current_ratio > high_bound and last_trade is not "high":  
                order(self.stk1, share1)
                order(self.stk0, -1*share0)
                last_trade = "high"  
            elif zscore<-2 and position0 == 0 and position1 == 0 and current_ratio < low_bound and last_trade is not "low":  
                order(self.stk0, share0)
                order(self.stk1, -1*share1)
                last_trade = "low"
        else:
            return
        
        if abs(zscore)<=0.75 and position0 != 0 and position1 != 0 and last_trade is not "mid" and last_trade is "high" and current_ratio < avgratio:
            order_target_value(self.stk0, 0)
            order_target_value(self.stk1, 0) 
            last_trade = "mid"  
        elif abs(zscore)<=0.75 and position0 != 0 and position1 != 0 and last_trade is not "mid" and last_trade is "low" and current_ratio > avgratio:
            order_target_value(self.stk0, 0)
            order_target_value(self.stk1, 0) 
            last_trade = "mid"  
        elif abs(z)<=1 and position0 != 0 and position1 != 0:
            order_target_value(self.stk0, 0)
            order_target_value(self.stk1, 0) 
        record(cash=context.portfolio.cash, freecash=freecash)
            # record(cash=context.portfolio.cash, capital = context.portfolio.capital_used, position_val=sum(abs(price1*position0) +abs( price2*position1) for t in range (0,)))
            
        # elif cash <= (abs(price1*position0) + abs(price2*position1)) and cash <= sum((abs(price1*position0) + abs(price2*position1)) for t in range (0,)): 
 
    def avg_ratio(self, prices):  
        #prices = history(756, '1d', 'price')
        price1 = prices[self.stk0]  
        price2 = prices[self.stk1]  
        return (price1/price2).mean()  

    def bounds(self,context,prices):  
        #prices = history(756, '1d', 'price')
        price1 = prices[self.stk0]  
        price2 = prices[self.stk1]
        std_amt = (price1 / price2).std() * context.std_multiplier  
        mean = self.avg_ratio(prices)
        high_bound = mean + std_amt
        low_bound = mean - std_amt
        return (high_bound, low_bound)  
        
    #get the zscore of the spread ratio
    def getZscore(self, prices, sid0, sid1):
        spread=prices[sid1]/prices[sid0]
        mean=np.mean(spread)
        std=np.std(spread)
        return mean, std
    
    #get the zscore of the hedge spread ratio
    def getMeanStd(self, prices, beta, sid0, sid1):
        meanPrice = np.mean(prices[sid1] - beta*prices[sid0])
        stdPrice= np.std(prices[sid1] - beta*prices[sid0])
        if meanPrice is not None and stdPrice is not None :
            return (meanPrice, stdPrice)
        else:
            return None
        
    #get the slope for the hedge spread ratio
    def ols_transform(self, prices, sid0, sid1):
        prices = prices.fillna(method='bfill')
        p0 = prices[sid0].values
        p1 = prices[sid1].values
        
        slope = sm.OLS(p0, p1).fit().params[0]
        return slope
    """
        elif zscore >=1.65 and zscore <2.33 and position0 != 0 and position1 != 0:
            order(self.stk1, share1/3.0)
            order(self.stk0, -1*share0/3.0)
            
        elif zscore <=-1.65 and zscore >-2.33 and position0 != 0 and  position1 != 0:
            order(self.stk0, share0/3.0)
            order(self.stk1, -1*share1/3.0)
             
        elif cash <= (abs(price1*position0) + abs(price2*position1)) and cash <= sum((abs(price1*position0) + abs(price2*position1)) for t in range (0,)): 
            return
       
    """
There was a runtime error.

Terence,
It looks like the way the exposure and leverage are calculated take the entire portfolio into account, not just one pair. I think this might be working correctly, or really close to it, I don't see any glaring problems. It never gets more than 1.4x leveraged, which is totally reasonable, and the overall exposure to the market stays pretty low. It doesn't seem like it trades excessively either.

It looks like it's coming together well, I would say that now you should just go back and test with one pair to confirm each pair is working correctly. Then add one or two pairs at a time and make sure the transition goes as expected. If all that goes well, put any finishing touches on it and paper trade it, ideally on IB, that will give you a good idea of what to expect.

Nice work,
David

I am having a similar problem as the OP, possibly also due to thinly traded pairs. My entry/exit logic should be considerably simpler. I want to trade for reversion on any pair getting a certain distance from the prior day's closing values, closing the position if the pair gets close to reversion or close to end of day, whichever comes first.

I can successfully run the strategy with a single pair (hard-coded SIDs to context.s1 and context.s2) but whenever I run with multiple thinly traded symbols using the dict structure, I get an error that says I can't trade more than 10000000000 shares or something to that effect

Anything obvious I'm doing wrong here? What's best practice for pairs trading of multiple symbols?

Clone Algorithm
3
Loading...
Backtest from to with initial capital
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
import numpy as np
import pytz
from brokers.ib import IBExchange


def initialize(context):
  # context.s1 = symbol('EOI')
  #  context.s2 = symbol('EOS')
  # set_max_position_size(max_notional=50000.0)
  # set_max_position_size(max_notional=50000.0)
    
    context.spy = symbol('SPY')
    context.pair_list=[
                       [sid(36816),sid(36006)], 
                       [sid(40533),sid(42096)]  
                       ]

    context.pClose1 = 0
    context.pClose2 = 0

    context.threshold = 0.01
    
    set_slippage(slippage.FixedSlippage(spread=0))
    set_commission(commission.PerShare(cost=0))
    

def handle_data(context, data):
        exchange_time = get_datetime().astimezone(pytz.timezone('US/Eastern'))
        open = get_open_orders()  
        if len(open) > 0:  
            log.debug('at ' + str(exchange_time) + ' open order count is ' + str(len(open)))  
                
        for pair in context.pair_list:
                context.s1 = pair[0]
                context.s2 = pair[1]
          #      log.info(context.s1,data[context.s1].price)
           #     print context.s1               
                if exchange_time.hour == 16 and exchange_time.minute == 0:
                    context.pClose1 = np.log(data[context.s1].close_price)
                    context.pClose2 = np.log(data[context.s2].close_price)
          #  print context.s1 + context.pClose1 + context.s2 + context.pClose2
                    log.info(context.pClose1, context.pClose2)
                    continue
     
        # calculate change vs prior close
                dayChg1 = np.log(data[context.s1].close_price) - (context.pClose1)
                dayChg2 = np.log(data[context.s2].close_price) - (context.pClose2)
        
        # exit logic
                if exchange_time.hour == 15 and exchange_time.minute >= 55:
                    order_target(context.s1, 0,style=MarketOrder(exchange=IBExchange.SMART))
                    order_target(context.s2, 0,style=MarketOrder(exchange=IBExchange.SMART))
                    continue
                # exit if     
                elif abs(dayChg2 - dayChg1) < context.threshold/4:   
                    order_target(context.s1, 0,style=MarketOrder(exchange=IBExchange.SMART))
                    order_target(context.s2, 0,style=MarketOrder(exchange=IBExchange.SMART))
        
        # entry logic
                if dayChg1 - dayChg2 > context.threshold:
                    order_target_percent(context.s1, -0.1,style=MarketOrder(exchange=IBExchange.SMART))
                    order_target_percent(context.s2, 0.1,style=MarketOrder(exchange=IBExchange.SMART))
            
                elif dayChg2 - dayChg1 > context.threshold:   
                    order_target_percent(context.s1, 0.1,style=MarketOrder(exchange=IBExchange.SMART))
                    order_target_percent(context.s2, -0.1,style=MarketOrder(exchange=IBExchange.SMART))
                    

                   
There was a runtime error.

Chad, it looks like you are running into thinly traded stock issues. I replaced "data[stock].close_price" with "data[stock].price" and it seems to work. When using the 'close_price' field, the prices are not forward filled, meaning that if there was no trades that minute, data[stock].close_price will be NaN. Changing to data[stock].price makes it use whatever the last trade price was.

Clone Algorithm
2
Loading...
Backtest from to with initial capital
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
import numpy as np
import pytz
from brokers.ib import IBExchange


def initialize(context):
  # context.s1 = symbol('EOI')
  #  context.s2 = symbol('EOS')
  # set_max_position_size(max_notional=50000.0)
  # set_max_position_size(max_notional=50000.0)
    
    context.spy = symbol('SPY')
    context.pair_list=[
                       [sid(36816),sid(36006)], 
                       [sid(40533),sid(42096)]  
                       ]

    context.pClose1 = 0
    context.pClose2 = 0

    context.threshold = 0.01
    
    set_slippage(slippage.FixedSlippage(spread=0))
    set_commission(commission.PerShare(cost=0))
    

def handle_data(context, data):
        exchange_time = get_datetime().astimezone(pytz.timezone('US/Eastern'))
        open_orders = get_open_orders()  
        if len(open_orders) > 0:  
            log.debug('at ' + str(exchange_time) + ' open order count is ' + str(len(open_orders)))  
            return
            
                
        for pair in context.pair_list:
                context.s1 = pair[0]
                context.s2 = pair[1]
          #      log.info(context.s1,data[context.s1].price)
           #     print context.s1               
                if exchange_time.hour == 16 and exchange_time.minute == 0:
                    context.pClose1 = np.log(data[context.s1].price)
                    context.pClose2 = np.log(data[context.s2].price)
          #  print context.s1 + context.pClose1 + context.s2 + context.pClose2
                    log.info(context.pClose1, context.pClose2)
                    continue
     
        # calculate change vs prior close
                dayChg1 = np.log(data[context.s1].price) - (context.pClose1)
                dayChg2 = np.log(data[context.s2].price) - (context.pClose2)
        
        # exit logic
                if exchange_time.hour == 15 and exchange_time.minute >= 55:
                    order_target(context.s1, 0,
                                 style=MarketOrder(exchange=IBExchange.SMART))
                    order_target(context.s2, 0,
                                 style=MarketOrder(exchange=IBExchange.SMART))
                    continue
                # exit if     
                elif abs(dayChg2 - dayChg1) < context.threshold/4:   
                    order_target(context.s1, 0,
                                 style=MarketOrder(exchange=IBExchange.SMART))
                    order_target(context.s2, 0,
                                 style=MarketOrder(exchange=IBExchange.SMART))
        
        # entry logic
                if dayChg1 - dayChg2 > context.threshold:
                    order_target_percent(context.s1, -0.1,
                                         style=MarketOrder(exchange=IBExchange.SMART))
                    order_target_percent(context.s2, 0.1,
                                         style=MarketOrder(exchange=IBExchange.SMART))
            
                elif dayChg2 - dayChg1 > context.threshold:   
                    order_target_percent(context.s1, 0.1,
                                         style=MarketOrder(exchange=IBExchange.SMART))
                    order_target_percent(context.s2, -0.1,
                                         style=MarketOrder(exchange=IBExchange.SMART))
                    

                   
There was a runtime error.

Thanks for the fix, David. However, I tried adding some more logging and a few extra conditionals to make the exits more precise and it broke again. I did set some breakpoints and step through for a few bars and noted that the "positions" were all 0 shares, and the exception came from one of the code blocks that's supposed to flatten (order_target) to 0 shares.

In previous backtesting applications I've used, this has been the generalized structure:
1 calculate whatever signals are needed
2 Load a collection of position objects. Foreach, evaluate exit criteria and close if necessary
3 else, loop through each symbol in the universe without a position open and check entry rules

I'd love if there were a smarter way to access the collection of open positions and handle exits, generally controlling the flow smarter. Like I said, I'm new at this platform

Clone Algorithm
4
Loading...
Backtest from to with initial capital
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
import numpy as np
import pytz
from brokers.ib import IBExchange


def initialize(context):
  # context.s1 = symbol('EOI')
  #  context.s2 = symbol('EOS')
  # set_max_position_size(max_notional=50000.0)
  # set_max_position_size(max_notional=50000.0)
    
    context.spy = symbol('SPY')
    context.pair_list=[
                       [sid(26744),sid(26975)],
                       [sid(27843),sid(26746)] 
                       ]

    context.pClose1 = 0
    context.pClose2 = 0

    context.threshold = 0.005
    
    set_slippage(slippage.FixedSlippage(spread=0))
    set_commission(commission.PerShare(cost=0))
    

def handle_data(context, data):
        exchange_time = get_datetime().astimezone(pytz.timezone('US/Eastern'))
        '''
        open_orders = get_open_orders()  
        if len(open_orders) > 0:  
            log.debug('at ' + str(exchange_time) + ' open order count is ' + str(len(open_orders)))  
            return
        '''    
                
        for pair in context.pair_list:
                context.s1 = pair[0]
                context.s2 = pair[1]
          #      log.info(context.s1,data[context.s1].price)
           #     print context.s1               
                if exchange_time.hour == 16 and exchange_time.minute == 0:
                    context.pClose1 = np.log(data[context.s1].price)
                    context.pClose2 = np.log(data[context.s2].price)
          #  print context.s1 + context.pClose1 + context.s2 + context.pClose2
                    #log.info(context.pClose1, context.pClose2)
                    continue
     
        # calculate change vs prior close
                dayChg1 = np.log(data[context.s1].price) - (context.pClose1)
                dayChg2 = np.log(data[context.s2].price) - (context.pClose2)
                spread = dayChg1 - dayChg2
        
        # exit logic
                if exchange_time.hour == 15 and exchange_time.minute == 59:
                    order_target(context.s1, 0,
                                 style=MarketOrder(exchange=IBExchange.SMART))
                    order_target(context.s2, 0,
                                 style=MarketOrder(exchange=IBExchange.SMART))
                    log.info("Exited %s / %s at %s for end of day" %(context.s1,context.s2,exchange_time))
                    continue

                # exit long/short pair if indicator crosses zero     
                elif context.portfolio.positions[context.s1].amount > 0 and  (spread) > 0:   
                    order_target(context.s1, 0,
                                 style=MarketOrder(exchange=IBExchange.SMART))
                    order_target(context.s2, 0,
                                 style=MarketOrder(exchange=IBExchange.SMART))
                    log.info("Exited long %s /short %s at %s for intraday crossover" %(context.s1,context.s2,exchange_time))
                # exit short/long pair if indicator crosses zero

                elif context.portfolio.positions[context.s1].amount < 0 and  (spread) < 0:   
                    order_target(context.s1, 0,
                                 style=LimitOrder(data[context.s1].price))
                    order_target(context.s2, 0,
                                 style=LimitOrder(data[context.s1].price))
                    log.info("Exited short %s /long %s at %s for intraday crossover" %(context.s1,context.s2,exchange_time))

                    
        # entry logic
                if context.portfolio.positions[context.s1].amount == 0 and spread > context.threshold:
                    order_target_percent(context.s1, -0.1,
                                         style=MarketOrder(exchange=IBExchange.SMART))
                    order_target_percent(context.s2, 0.1,
                                         style=MarketOrder(exchange=IBExchange.SMART))
                    log.info("Entered short %s /long %s at %s " %(context.s1,context.s2,exchange_time))
            
                elif context.portfolio.positions[context.s1].amount == 0 and spread < -context.threshold:   
                    order_target_percent(context.s1, 0.1,
                                         style=MarketOrder(exchange=IBExchange.SMART))
                    order_target_percent(context.s2, -0.1,
                                         style=MarketOrder(exchange=IBExchange.SMART))
                    log.info("Entered long %s /short %s at %s " %(context.s1,context.s2,exchange_time))
                    

                   
There was a runtime error.

Also, why is my return (at the point of failure) 4000%? is the market neutral position screwing up the denominator of my return calculation by making account equity equal to longPos - shortPos, and not considering the cash generated from the shorting? Small denominator would make for big RoR

Chad,
The crazy returns were due to new orders being placed when there was already open orders on the books. I made a few changes, the biggest one is that you were actually overwriting data in your main for loop. Defining "context.s1" and "context.s2" meant that the context.pClose variables were being overwritten on each pass of that for loop. Another change I made is that the algo now cancels any stale limit orders on every bar, the thought process is that it will submit a new updated order if the criteria is still met. Very cool strategy, I love that drawdown. Feel free to message me if you have further questions.

David

Clone Algorithm
23
Loading...
Backtest from to with initial capital
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
import numpy as np
import pytz
from brokers.ib import IBExchange
from zipline.utils import tradingcalendar


def initialize(context):
  # context.s1 = symbol('EOI')
  #  context.s2 = symbol('EOS')
  # set_max_position_size(max_notional=50000.0)
  # set_max_position_size(max_notional=50000.0)
    
    context.spy = symbol('SPY')
    context.pair_list=[
        (sid(26744),sid(26975)),
        (sid(27843),sid(26746))
    ]

    context.pClose1 = 0
    context.pClose2 = 0

    context.threshold = 0.005
    
    set_slippage(slippage.FixedSlippage(spread=0))
    set_commission(commission.PerShare(cost=0))
    

def handle_data(context, data):
        exchange_time = get_datetime().astimezone(pytz.timezone('US/Eastern'))
        
        # Cancel stale LOs.
        cancel_open_orders()
        if get_open_orders():
            # Shouldn't trigger
            log.debug("Open Orders: Skipping bar.....")
            return
        
        # Use history, if the freq is '1d', the current price, 
        # and yesterdays close prices are returned.   
        gaps = np.log(history(2, '1d', 'price')).diff().iloc[-1]
        
        for pair in context.pair_list:
                s1 = pair[0]
                s2 = pair[1]
        # calculate change vs prior close
                dayChg1 = gaps[s1]
                dayChg2 = gaps[s2]
                spread = dayChg1 - dayChg2
                record(spread=spread)
        # exit logic
                if exchange_time.hour == 15 and exchange_time.minute == 59:
                    order_target(s1, 0, style=MarketOrder(exchange=IBExchange.SMART))
                    order_target(s2, 0, style=MarketOrder(exchange=IBExchange.SMART))
                    log.info("Exited %s / %s at %s for end of day" %(
                              s1, s2,exchange_time))
                    continue

                # exit long/short pair if indicator crosses zero     
                elif context.portfolio.positions[s1].amount > 0 and (spread > 0):   
                    order_target(s1, 0, style=MarketOrder(exchange=IBExchange.SMART))
                    order_target(s2, 0, style=MarketOrder(exchange=IBExchange.SMART))
                    log.info("Exited long %s /short %s at %s for intraday crossover" %(
                              s1, s2,exchange_time))
                    
                # exit short/long pair if indicator crosses zero
                elif context.portfolio.positions[s1].amount < 0 and  (spread < 0):   
                    order_target(s1, 0, style=LimitOrder(data[s1].price))
                    order_target(s2, 0, style=LimitOrder(data[s1].price))
                    log.info("Exited short %s /long %s at %s for intraday crossover" %(
                              s1,s2,exchange_time))

                    
        # entry logic
                if context.portfolio.positions[s1].amount == 0 and spread > context.threshold:
                    order_target_percent(s1, -0.1, style=MarketOrder(exchange=IBExchange.SMART))
                    order_target_percent(s2, 0.1,  style=MarketOrder(exchange=IBExchange.SMART))
                    log.info("Entered short %s /long %s at %s " %(
                              s1, s2,exchange_time))
            
                elif context.portfolio.positions[s1].amount == 0 and spread < -context.threshold:  
                    order_target_percent(s1, 0.1, style=MarketOrder(exchange=IBExchange.SMART))
                    order_target_percent(s2, -0.1, style=MarketOrder(exchange=IBExchange.SMART))
                    log.info("Entered long %s /short %s at %s " %(
                              s1, s2, exchange_time))
                    
                    
def cancel_open_orders():
    open_orders = get_open_orders()
    for ords in open_orders.itervalues():
        for oo in ords:
            log.info("Cancelling %s"%oo)
            cancel_order(oo)
                    
def market_open(dt):
    ref = tradingcalendar.canonicalize_datetime(dt)
    return dt == tradingcalendar.open_and_closes.T[ref]['market_open']

def market_close(dt):
    ref = tradingcalendar.canonicalize_datetime(dt)
    return dt == tradingcalendar.open_and_closes.T[ref]['market_close']

                   
There was a runtime error.