Creating and Back-Testing a Pairs Trading Strategy in Python.
By: Sofien Kaabar
Tags: Machine Learning, Finance, Trading, Data Science, Algorithmic Trading

Creating and Back-Testing a Pairs Trading Strategy in Python.

Creating a Systematic Equity Pairs Trading Strategy in Python.

Pairs trading is one of the many mean-reversion strategies. It is considered non-directional and relative as it aims to trade on both related stocks with similar statistical and economical properties. It first identifies a historical relationship that has been recently broken and then buys the implied undervalued security while shorting the overvalued security in attempt to revert to the original relationship. We must first understand the difference between correlation and cointegration. Correlation (ρ) measures the degree of the linear relationship between variables, it has the following features:

ρ = -1, means a perfectly negative relationship. They move in the opposite direction.

ρ = 0, means there is no linear relationship between the two.

ρ = 1, means a perfect linear relationship between the datasets. They move hand in hand.

Correlation does not imply causality. It is not used to explain variations in one dataset caused by the other. We use regression analysis for that issue. Negatively correlated assets can be used to hedge one another, and it is the basic diversification rule for portfolio management. If we have two different datasets and their two respective variances, we can calculate a measure called covariance. Covariance has the same principle as correlation except that it is unbounded and therefore not really meaningful. It is however used in the formula of correlation:

If the correlation coefficient is zero, it does not necessarily mean that there is absolutely no relationship between the two datasets. It simply means that there is not a linear one.

A chart showing perfect correlation between two datasets. Correlation = 1

A chart showing a strong correlation between two datasets. Correlation = 0.7

A chart showing a strong negative correlation between two datasets. Correlation = -0.7

Disclaimer: The below choice for the two related securities is inspired by Dr. Ernest Chan. The back-test to follow does not take into account the transaction costs and therefore the reality may differ with respect to the results found at the end of the article.

We must be extremely cautious with correlation. With financial data you will most likely have to calculate correlations of returns and not prices. If you’re interested in short-term relationships, go for returns’ correlations, otherwise, opt for prices if you’re in it for the long-term. Cointegration, on the other hand, is not too dissimilar to correlation. In plain terms, it means that the ratio between the two financial time series will vary around a constant mean. Pairs trading can rely on the constant ratio that is expected to revert to its long-term mean (i.e. converge). Below are two ETFs with the following details:

EWA : iShares MSCI Australia ETF, seeks to replicate investment results that correspond to the price and yield performance of publicly traded securities in the Australian market.

: iShares MSCI Australia ETF, seeks to replicate investment results that correspond to the price and yield performance of publicly traded securities in the Australian market. EWC: iShares MSCI Canada ETF seeks to replicate investment results that correspond to the price and yield performance of publicly traded securities in the Canadian market.

EWA and EWC chart.

They do seem very correlated, but are they cointegrated? In other words, is their spread mean-reverting and stationary? That’s what we will be back-testing. If the series are indeed stationary then we will form a pairs trading simple strategy based on their spread (ignoring the hedge ratio). Let’s consider EWA to be Asset_2 variable and EWC to be Asset_1 variable in our code. The calculated correlation between the two seems to be the first unlocked door towards the strategy:

import numpy as np

np.corrcoef(Asset_1, Asset2)

The result we get will be around 0.85 which is a pretty strong linear correlation. Next, we will separately check whether the two time series are stationary or not, and then check if they are cointegrated or not. We will use statsmodels.tsa.stattools for the below.

def stationarity(a, cutoff = 0.05):

a = np.ravel(a)

if adfuller(a)[1] < cutoff:

print(‘The series is stationary’)

print(‘p-value = ‘, adfuller(a)[1])

else:

print(‘The series is NOT stationary’)

print(‘p-value = ‘, adfuller(a)[1]) stationarity(Asset_1)

stationarity(Asset_2) def cointegration(a, b):

if coint(a, b)[1] < 0.05:

print(‘The series are cointegrated’)

print(‘p-value = ‘, coint(a, b)[1])

else:

print(‘The series are NOT cointegrated’)

print(‘p-value = ‘, coint(a, b)[1]) cointegration(Asset_1, Asset_2)

The above code will give these results. Naturally, we do not expect that these two random-like time series to be stationary (I just needed an excuse to provide the code) but what interests us is the result of the cointegration test which has a p-value of less than our chosen cut-off (5%), thus, rejecting the null hypothesis that there is no cointegration. Now, we have confirmation to back-test a strategy based on the two assets.

The series is NOT stationary (Asset_1):

p-value = 0.057

The series is NOT stationary (Asset_2):

p-value = 0.20

The series are cointegrated:

p-value = 0.048

# Another check for stationarity in the ratio between the two

Data[:, 2] = Asset_2 / Asset_1

Ratio = Data[:, 2] stationarity(Ratio)

When we calculate the ratio between the two and plot it we will have the next graph which seems to be mean-reverting. The spread seems to satisfy the stationarity test as well.

The series is stationary

p-value = 0.032

The ratio between EWA and EWC.

Now, we should standardize the ratio we have in order to normalize the signals.

# Standardization assuming the spread

for i in range(len(Data)):

try:

Data[i, 3] = (Data[i — lookback:i + 1, 2].mean())

except IndexError:

pass # Calculating Standard deviation

for i in range(len(Data)):

Data[i, 4] = ((Data[i — lookback:i + 1, 2].std())) # Standardizing

for i in range(len(Data)):

Data[i, 5] = (Data[i, 2] — Data[i, 3]) / Data[i, 4]

The standardized ratio between EWA and EWC. The arbitrarily chosen barriers of 2 and -2 will be used to initiate trades. If the standardized ratio reaches -2, we go long the ratio while if it reaches 2, we go short the ratio.

Main idea: We will back-test a dollar-neutral pairs trading strategy on the EWA and EWC ETFs. As we have explained above, the conditions of the back-test will be the following:

Holding period = 60

Lookback period = 9

Upper barrier = 2

Lower barrier = -2

We can also use a variable period that relies on the when the mean reverts back to normality but in our back-test the holding period will be fixed.

Signals generated from the strategy with red downwards pointing arrows as sell signals and green upwards pointing arrows as buy signals.

# Buy/Sell Conditions

for i in range(len(Data)):

try:

if Data[i, 2] <= lower_barrier and Data[i — 1, 2] > lower_barrier:

Data[i + 1, 4] = -1

Data[i + 1, 5] = 1

elif Data[i, 2] >= upper_barrier and Data[i — 1, 2] < upper_barrier:

Data[i + 1, 6] = 1

Data[i + 1, 7] = -1

else:

continue

except IndexError:

pass for i in range(len(Data)):

try:

if Data[i, 5] == 1:

Data[i + holding_period, 8] = (Data[i + holding_period, 1] — Data[i, 1])

if Data[i, 6] == 1:

Data[i + holding_period, 8] = (Data[i + holding_period, 0] — Data[i, 0])

if Data[i, 4] == -1:

Data[i + holding_period, 9] = (Data[i, 0] — Data[i + holding_period, 0])

if Data[i, 7] == -1:

Data[i + holding_period, 9] = (Data[i, 1] — Data[i + holding_period, 1])

except IndexError:

pass

The next graph shows the equity curve from the strategy (gross of fees) and the results (assuming $1 investment with no leverage).

Equity curve following the strategy.

It is worth-mentioning again that the profit was only indicative as it is gross of fees and any other costs. Also, no leverage was used, so, the profit is not magnified which explains its weakness, but the strategy does seem to work even though we have a small bias by choosing the barriers and the lookback before starting.