Setting Stop Loss in Pair Trading

Please help me in showing how to set the StopLoss Order if say the spread goes >2.56 stdev away from spread mean or <-2.56 in the following algorithm.

https://www.quantopian.com/algorithms/5669cd02221b3422940001b4

15 responses

We can't see your algorithms at all you know. As for your question, it's basically impossible to place a stop-loss order on a custom spread, since there's an infinitely variety of ways that a spread can diverge to produce a z-score of a stop level. The best you can do is monitor the z-score every minute in handle_date and stop out by hand there. Another way that I am experimenting with is a transformation from z-score to desired position with humps around the entry and flat around the 0, and the stop levels...

Oh ,sorry in asking the question . My point is basically not z-score but if spread goes beyond some specific level in opposite , in that case , we can set up the stop loss ? may be the second point could be relevant to my query.

Well, the simplest is just to monitor the spread in handle_data and if it gets to your stop level, place orders to exit the positions?

Ok, I will try to do that and then get back to you accordingly.

I have tried to write this code but its not running,please see.

***def handle_data(context, data):

try:
for stock_pairs() in data:
order(security, amount, style=StopOrder(stop_level))

except Exception as e:
print(str(e))***


I have no idea what this is doing, sorry. You'll need to debug it.

I have entered the position when the spread is at 1 stdev away from spread mean and want to set stop loss if the spread goes into the more than 2.56 stdev from the spread mean. I hope you got now.

When I full backtest , it says code has some problem.

Yeah, you just have to fix the problem, sorry. I know what you are trying to do, but I can't help you.

It would be much easier for us to help you if you put the relevant code into an algo and attach a backtest for us to clone. The code snippet you did give us will not run, so I'm pretty sure sure your problems are python related. Check out the debugger, that should help you figure out what's going on.

Hi David,
The entire code is here. Problem is when I do the backtest, it says
"Your algorithm couldn't be backtested because it has some code problems." I have just added few lines of codes i.e. def handle_data() part, in addition to the original one given in the Pair Trading Lecture on Quantopian. When I Debug at various Breakpoints in IDE, it still says the same code error.


import numpy as np
import statsmodels.api as sm
import pandas as pd
import pytz

def initialize(context):
# Quantopian backtester specific variables
set_symbol_lookup_date('2014-01-01')
context.stock_pairs = [(symbol('ABGB'), symbol('FSLR')),
(symbol('CSUN'), symbol('ASTI'))]
# set_benchmark(context.y)
context.num_pairs = len(context.stock_pairs)
# strategy specific variables
context.lookback = 20 # used for regression
context.z_window = 20 # used for zscore calculation, must be <= lookback
# context.hedgeRatioTS = np.ndarray((context.num_pairs, 0))
context.inLong = [False] * context.num_pairs
context.inShort = [False] * context.num_pairs
# Only do work 30 minutes before close
schedule_function(func=check_pair_status, date_rule=date_rules.every_day(), time_rule=time_rules.market_close(minutes=30))
# Will be called on every trade event for the securities you specify.
def handle_data(context, data):

if context.inShort[i] and zscore > short_cutoff_level:
order(security, amount, style=StopOrder(short_cutoff_level))

if context.inLong[i] and zscore < long_cutoff_level:
order(security, amount, style=StopOrder(long_cutoff_level))

except Exception as e:
print(str(e))

def check_pair_status(context, data):
if get_open_orders():
return
prices = history(35, '1d', 'price').iloc[-context.lookback::]
for i in range(context.num_pairs):

(stock_y, stock_x) = context.stock_pairs[i]

Y = prices[stock_y]
X = prices[stock_x]

try:
except ValueError as e:
log.debug(e)
return

# context.hedgeRatioTS = np.append(context.hedgeRatioTS, hedge)
new_spreads[i, :] = Y[-1] - hedge * X[-1]

# Keep only the z-score lookback period

if context.inShort[i] and zscore < 0.0:
order_target(stock_y, 0)
order_target(stock_x, 0)
context.inShort[i] = False
context.inLong[i] = False
record(X_pct=0, Y_pct=0)
return

if context.inLong[i] and zscore > 0.0:
order_target(stock_y, 0)
order_target(stock_x, 0)
context.inShort[i] = False
context.inLong[i] = False
record(X_pct=0, Y_pct=0)
return

if zscore < -1.0 and (not context.inLong[i]):
y_target_shares = 1
X_target_shares = -hedge
context.inLong[i] = True
context.inShort[i] = False

(y_target_pct, x_target_pct) = computeHoldingsPct( y_target_shares,X_target_shares, Y[-1], X[-1] )
order_target_percent( stock_y, y_target_pct * (1.0/context.num_pairs) / float(context.num_pairs) )
order_target_percent( stock_x, x_target_pct * (1.0/context.num_pairs) / float(context.num_pairs) )
record(Y_pct=y_target_pct, X_pct=x_target_pct)
return

if zscore > 1.0 and (not context.inShort[i]):
y_target_shares = -1
X_target_shares = hedge
context.inShort[i] = True
context.inLong[i] = False

(y_target_pct, x_target_pct) = computeHoldingsPct( y_target_shares, X_target_shares, Y[-1], X[-1] )
order_target_percent( stock_y, y_target_pct * (1.0/context.num_pairs) / float(context.num_pairs) )
order_target_percent( stock_x, x_target_pct * (1.0/context.num_pairs) / float(context.num_pairs) )
record(Y_pct=y_target_pct, X_pct=x_target_pct)

model = sm.OLS(Y, X).fit()
return model.params
model = sm.OLS(Y, X).fit()
return model.params.values
def computeHoldingsPct(yShares, xShares, yPrice, xPrice):
yDol = yShares * yPrice
xDol = xShares * xPrice
notionalDol =  abs(yDol) + abs(xDol)
y_target_pct = yDol / notionalDol
x_target_pct = xDol / notionalDol
return (y_target_pct, x_target_p
ct)


Hi quant 1. Looks like the code came from Delaney's 'E. Chan algorithm modified to trade multiple pairs' (nice choice) and you made some changes for the StopOrder experimentation. To help bring you up to speed here, in all programming, variables (like 'spreads' and 'zscore') have to be first initialized, inside every function (or "def") where they are used (unless initialized outside of those functions, globally). So when you copied some code from check_pair_status() to handle_data(), the initialization of those -- were also needed. Also the 'for' loop they were in, which initializes 'i'. And others.

Below are some additions that will at least make that run, otherwise I haven't put any attention into it (except for a couple of log line examples).

Just so you'll have something you can work with.

Edit: If that wasn't clear ...
MAINLY ALL THIS IS DOING IS FIXING ENOUGH ERRORS TO MAKE IT RUN.
Take it from there.

Meanwhile, since you are interested in StopOrder, look thru some of the Quantopian pages that mention it with a search like this:

... or if you'd like to try to make sure there is an attached backtest on each page that you can clone, add handle_data to that (for example), cutting the number of results by over 40% in this case:

"StopOrder" "handle_data" site:quantopian.com

Also on a google results page you can click the 'Search tools' button and then under 'Any time' can select 'Past year' for example, then can order by date with the most recent first. Or, 'Custom range' for specific dates -- works well.

11
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 statsmodels.api as sm
import pandas as pd
import pytz

def initialize(context):
# Quantopian backtester specific variables
set_symbol_lookup_date('2014-01-01')
context.stock_pairs = [(symbol('ABGB'), symbol('FSLR')),
(symbol('CSUN'), symbol('ASTI'))]
# set_benchmark(context.y)
context.num_pairs = len(context.stock_pairs)
# strategy specific variables
context.lookback = 20 # used for regression
context.z_window = 20 # used for zscore calculation, must be <= lookback
# context.hedgeRatioTS = np.ndarray((context.num_pairs, 0))
context.inLong = [False] * context.num_pairs
context.inShort = [False] * context.num_pairs
# Only do work 30 minutes before close
schedule_function(func=check_pair_status, date_rule=date_rules.every_day(), time_rule=time_rules.market_close(minutes=30))

def handle_data(context, data):
for i in range(context.num_pairs):
(stock_y, stock_x) = context.stock_pairs[i]

try:
amount = 10

if context.inShort[i] and zscore > short_cutoff_level:
log.info('{} {} at cutoff {}'.format(
amount, stock_x.symbol, short_cutoff_level))
order(stock_x, amount, style=StopOrder(short_cutoff_level))

if context.inLong[i] and zscore < long_cutoff_level:
log.info('{} {} at cutoff {}'.format(
amount, stock_y.symbol, long_cutoff_level))
order(stock_y, amount, style=StopOrder(long_cutoff_level))

except Exception as e:
print(str(e))

def check_pair_status(context, data):
if get_open_orders():
return
prices = history(35, '1d', 'price').iloc[-context.lookback::]
for i in range(context.num_pairs):

(stock_y, stock_x) = context.stock_pairs[i]

Y = prices[stock_y]
X = prices[stock_x]

try:
except ValueError as e:
log.debug(e)
return

# context.hedgeRatioTS = np.append(context.hedgeRatioTS, hedge)
new_spreads[i, :] = Y[-1] - hedge * X[-1]

# Keep only the z-score lookback period

if context.inShort[i] and zscore < 0.0:
order_target(stock_y, 0)
order_target(stock_x, 0)
context.inShort[i] = False
context.inLong[i] = False
record(X_pct=0, Y_pct=0)
return

if context.inLong[i] and zscore > 0.0:
order_target(stock_y, 0)
order_target(stock_x, 0)
context.inShort[i] = False
context.inLong[i] = False
record(X_pct=0, Y_pct=0)
return

if zscore < -1.0 and (not context.inLong[i]):
y_target_shares = 1
X_target_shares = -hedge
context.inLong[i] = True
context.inShort[i] = False

(y_target_pct, x_target_pct) = computeHoldingsPct( y_target_shares,X_target_shares, Y[-1], X[-1] )
order_target_percent( stock_y, y_target_pct * (1.0/context.num_pairs) / float(context.num_pairs) )
order_target_percent( stock_x, x_target_pct * (1.0/context.num_pairs) / float(context.num_pairs) )
record(Y_pct=y_target_pct, X_pct=x_target_pct)
return

if zscore > 1.0 and (not context.inShort[i]):
y_target_shares = -1
X_target_shares = hedge
context.inShort[i] = True
context.inLong[i] = False

(y_target_pct, x_target_pct) = computeHoldingsPct( y_target_shares, X_target_shares, Y[-1], X[-1] )
order_target_percent( stock_y, y_target_pct * (1.0/context.num_pairs) / float(context.num_pairs) )
order_target_percent( stock_x, x_target_pct * (1.0/context.num_pairs) / float(context.num_pairs) )
record(Y_pct=y_target_pct, X_pct=x_target_pct)

model = sm.OLS(Y, X).fit()
return model.params
model = sm.OLS(Y, X).fit()
return model.params.values

def computeHoldingsPct(yShares, xShares, yPrice, xPrice):
yDol = yShares * yPrice
xDol = xShares * xPrice
notionalDol =  abs(yDol) + abs(xDol)
y_target_pct = yDol / notionalDol
x_target_pct = xDol / notionalDol
return (y_target_pct, x_target_pct)


There was a runtime error.

Ok garyha, let me follow that and see if I am able to do this time. Yes . when I "Build Algorithm". It also says similar things as Undefined entities....Trying to get it right. Thanks.

garyha ,Backtest taking into account the hedge ratio. have you noticed that with stop loss in place the performance has become poor ?

2
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 statsmodels.api as sm
import pandas as pd
import pytz

def initialize(context):
# Quantopian backtester specific variables
set_symbol_lookup_date('2014-01-01')
context.stock_pairs = [(symbol('ABGB'), symbol('FSLR')),
(symbol('CSUN'), symbol('ASTI'))]
# set_benchmark(context.y)
context.num_pairs = len(context.stock_pairs)
# strategy specific variables
context.lookback = 20 # used for regression
context.z_window = 20 # used for zscore calculation, must be <= lookback
# context.hedgeRatioTS = np.ndarray((context.num_pairs, 0))
context.inLong = [False] * context.num_pairs
context.inShort = [False] * context.num_pairs
# Only do work 30 minutes before close
schedule_function(func=check_pair_status, date_rule=date_rules.every_day(), time_rule=time_rules.market_close(minutes=30))

def handle_data(context, data):
for i in range(context.num_pairs):
(stock_y, stock_x) = context.stock_pairs[i]

try:
amounta = 1
amountb = hedge

if context.inShort[i] and zscore > short_cutoff_level:
log.info('{} {} at cutoff {}'.format(
amount, stock_x.symbol, short_cutoff_level))
order(stock_y, amounta, style=StopOrder(short_cutoff_level))
order(stock_x, -amountb, style=StopOrder(short_cutoff_level))

if context.inLong[i] and zscore < long_cutoff_level:
log.info('{} {} at cutoff {}'.format(
amount, stock_y.symbol, long_cutoff_level))
order(stock_y, -amounta, style=StopOrder(long_cutoff_level))
order(stock_x, amountb, style=StopOrder(long_cutoff_level))

except Exception as e:
print(str(e))

def check_pair_status(context, data):
if get_open_orders():
return
prices = history(35, '1d', 'price').iloc[-context.lookback::]
for i in range(context.num_pairs):

(stock_y, stock_x) = context.stock_pairs[i]

Y = prices[stock_y]
X = prices[stock_x]

try:
except ValueError as e:
log.debug(e)
return

# context.hedgeRatioTS = np.append(context.hedgeRatioTS, hedge)
new_spreads[i, :] = Y[-1] - hedge * X[-1]

# Keep only the z-score lookback period

if context.inShort[i] and zscore < 0.0:
order_target(stock_y, 0)
order_target(stock_x, 0)
context.inShort[i] = False
context.inLong[i] = False
record(X_pct=0, Y_pct=0)
return

if context.inLong[i] and zscore > 0.0:
order_target(stock_y, 0)
order_target(stock_x, 0)
context.inShort[i] = False
context.inLong[i] = False
record(X_pct=0, Y_pct=0)
return

if zscore < -1.0 and (not context.inLong[i]):
y_target_shares = 1
X_target_shares = -hedge
context.inLong[i] = True
context.inShort[i] = False

(y_target_pct, x_target_pct) = computeHoldingsPct( y_target_shares,X_target_shares, Y[-1], X[-1] )
order_target_percent( stock_y, y_target_pct * (1.0/context.num_pairs) / float(context.num_pairs) )
order_target_percent( stock_x, x_target_pct * (1.0/context.num_pairs) / float(context.num_pairs) )
record(Y_pct=y_target_pct, X_pct=x_target_pct)
return

if zscore > 1.0 and (not context.inShort[i]):
y_target_shares = -1
X_target_shares = hedge
context.inShort[i] = True
context.inLong[i] = False

(y_target_pct, x_target_pct) = computeHoldingsPct( y_target_shares, X_target_shares, Y[-1], X[-1] )
order_target_percent( stock_y, y_target_pct * (1.0/context.num_pairs) / float(context.num_pairs) )
order_target_percent( stock_x, x_target_pct * (1.0/context.num_pairs) / float(context.num_pairs) )
record(Y_pct=y_target_pct, X_pct=x_target_pct)

model = sm.OLS(Y, X).fit()
return model.params
model = sm.OLS(Y, X).fit()
return model.params.values

def computeHoldingsPct(yShares, xShares, yPrice, xPrice):
yDol = yShares * yPrice
xDol = xShares * xPrice
notionalDol =  abs(yDol) + abs(xDol)
y_target_pct = yDol / notionalDol
x_target_pct = xDol / notionalDol
return (y_target_pct, x_target_pct)


There was a runtime error.

I took a look at this and it looks there are still some issues. One thing to remember (Simon pointed this out above) is that it is impossible to submit a stoploss for a pair trade. Since you are combining two stocks into a virtual security you need to base a stop on the spread, then submit market/limit orders to close each leg when the spread breaks your stop threshold.

I tweaked the algo a bit to submit market orders to close the pairs. I still think there are some issues with the ordering/stop logic though, it behaves odd sometimes, but I don't have to look into it.

I also have it trading a single pair, I would stick to one pair until you have that working the way you expect, then add more later.

11
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 statsmodels.api as sm
import pandas as pd
import pytz

def initialize(context):
# Quantopian backtester specific variables
set_symbol_lookup_date('2014-01-01')
context.stock_pairs = [
(symbol('PG'), symbol('JNJ'))
]
# set_benchmark(context.y)
context.num_pairs = len(context.stock_pairs)
# strategy specific variables
context.lookback = 20 # used for regression
context.z_window = 20 # used for zscore calculation, must be <= lookback
# context.hedgeRatioTS = np.ndarray((context.num_pairs, 0))
context.inLong = [False] * context.num_pairs
context.inShort = [False] * context.num_pairs
# Only do work 30 minutes before close
schedule_function(func=check_pair_status,
date_rule=date_rules.every_day(),
time_rule=time_rules.market_close(minutes=30))

def handle_data(context, data):
open_orders = get_open_orders()
for i in range(context.num_pairs):
(stock_y, stock_x) = context.stock_pairs[i]

if stock_y in open_orders or stock_x in open_orders:
continue

try:

if context.inShort[i] and zscore > 2.56:
log.info('[Short Stop] zscore = {}'.format(zscore))
order_target(stock_y, 0)
order_target(stock_x, 0)
context.inShort[i] = False
context.inLong[i] = False

if context.inLong[i] and zscore < -2.56:
log.info('[Long Stop] zscore = {}'.format(zscore))
order_target(stock_y, 0)
order_target(stock_x, 0)
context.inShort[i] = False
context.inLong[i] = False

except IndexError:
pass

def check_pair_status(context, data):
if get_open_orders():
return
prices = history(35, '1d', 'price').iloc[-context.lookback::]
for i in range(context.num_pairs):

(stock_y, stock_x) = context.stock_pairs[i]

Y = prices[stock_y]
X = prices[stock_x]

try:
except ValueError as e:
log.debug(e)
return

# context.hedgeRatioTS = np.append(context.hedgeRatioTS, hedge)
new_spreads[i, :] = Y[-1] - hedge * X[-1]

# Keep only the z-score lookback period

if context.inShort[i] and zscore < 0.0:
order_target(stock_y, 0)
order_target(stock_x, 0)
context.inShort[i] = False
context.inLong[i] = False
record(X_pct=0, Y_pct=0)
return

if context.inLong[i] and zscore > 0.0:
order_target(stock_y, 0)
order_target(stock_x, 0)
context.inShort[i] = False
context.inLong[i] = False
record(X_pct=0, Y_pct=0)
return

if zscore < -1.0 and (not context.inLong[i]):
y_target_shares = 1
X_target_shares = -hedge
context.inLong[i] = True
context.inShort[i] = False

(y_target_pct, x_target_pct) = computeHoldingsPct( y_target_shares,X_target_shares, Y[-1], X[-1] )
order_target_percent( stock_y, y_target_pct * (1.0/context.num_pairs) / float(context.num_pairs) )
order_target_percent( stock_x, x_target_pct * (1.0/context.num_pairs) / float(context.num_pairs) )
record(Y_pct=y_target_pct, X_pct=x_target_pct)
return

if zscore > 1.0 and (not context.inShort[i]):
y_target_shares = -1
X_target_shares = hedge
context.inShort[i] = True
context.inLong[i] = False

(y_target_pct, x_target_pct) = computeHoldingsPct( y_target_shares, X_target_shares, Y[-1], X[-1] )
order_target_percent( stock_y, y_target_pct * (1.0/context.num_pairs) / float(context.num_pairs) )
order_target_percent( stock_x, x_target_pct * (1.0/context.num_pairs) / float(context.num_pairs) )
record(Y_pct=y_target_pct, X_pct=x_target_pct)

model = sm.OLS(Y, X).fit()
return model.params
model = sm.OLS(Y, X).fit()
return model.params.values

def computeHoldingsPct(yShares, xShares, yPrice, xPrice):
yDol = yShares * yPrice
xDol = xShares * xPrice
notionalDol =  abs(yDol) + abs(xDol)
y_target_pct = yDol / notionalDol
x_target_pct = xDol / notionalDol
return (y_target_pct, x_target_pct)

`
There was a runtime error.

Hi David,
Your backtest performance is better . My view from experience is that Stop Loss can be submitted for pair trade. Indeed, we put that on the spread and once it goes out of our level of tolerance , we need to cut both the stocks positions. As far as z-score is concerned, it is calculated from spread only. So,putting stop loss on z-score .
Or , what do you think,It could be that if P&L goes say for example <-\$1000 for each trade, we need to exit from both the positions. Would you please try this and backtest.