Statistical arbitrage, in the context of cointegration, aims to exploit the temporary price deviations between two or more securities that exhibit a long-term equilibrium relationship. We will consider a pair trading strategy based on cointegration between two historically cointegrated stocks. This is done in a few steps:
1. Preprocess the data.
2. Test for cointegration.
3. Fit a linear model with least squares.
4. Implement a pairs trading/mean-reversion strategy.
5. Measure performance and risk.
Before implementing the example, let’s import the required libraries:
import numpy as np
import pandas as pd
import statsmodels.api as sm
import matplotlib.pyplot as plt
Let’s consider we have historical daily adjusted closing price data for two stocks (Stock A and Stock B) named ’stock_A’ and ’stock_B’. Here’s a sample of the stocks’ DataFrame:
print(data.head())
stock_A stock_B
0 153.629898 153.629898
1 153.629898 153.629898
2 153.629898 153.629898
3 152.537521 152.537521
4 152.537521 152.537521
## Step 1: Preprocess the price data
First, let’s ensure the data do not have any missing or non-numeric values. We can also generate the log returns of the two stocks:
data = data.dropna()
log_returns_A = np.log(data['stock_A']).diff().dropna()
log_returns_B = np.log(data['stock_B']).diff().dropna()
## Step 2: Test for cointegration
We test for cointegration using the ADF (Augmented Dickey-Fuller) and the Engle-Granger two-step methodology. First, make sure that the individual log price series are non-stationary, then we test for the stationarity of the residuals from a linear regression.
from statsmodels.tsa.stattools import adfuller
adf_A = adfuller(log_returns_A)
adf_B = adfuller(log_returns_B)
adf_A_diff = adfuller(log_returns_A.diff().dropna())
adf_B_diff = adfuller(log_returns_B.diff().dropna())
print("ADF p-value for Stock A: ", adf_A[1])
print("ADF p-value for Stock B: ", adf_B[1])
print("ADF p-value for Stock A first differences: ", adf_A_diff[1])
print("ADF p-value for Stock B first differences: ", adf_B_diff[1])
If the ADF test indicates that the two price series are non-stationary, then we proceed with the Engle-Granger test:
from statsmodels.tsa.stattools import coint
eg_test = coint(log_returns_A, log_returns_B)
print("Engle-Granger test p-value: ", eg_test[1])
If the Engle-Granger test’s p-value is less than a predefined level of significance (e.g., 0.05), we can conclude that the two price series are cointegrated.
## Step 3: Fit a linear model with least squares
Next, we fit a linear regression model to establish the cointegration relationship between the two stocks:
X = sm.add_constant(log_returns_A)
model = sm.OLS(log_returns_B, X).fit()
hedge_ratio = model.params[1]
We obtain the hedging ratio called ’hedge_ratio’. This signifies how many units of Stock B we have to sell short for each unit of Stock A we buy.
## Step 4: Implement a pairs trading/mean-reversion strategy
Now, let’s create a trading signal based on the residual values obtained from the linear regression. We start by subtracting the log returns of Stock A and Stock B after considering the hedge ratio:
residuals = log_returns_B - hedge_ratio * log_returns_A
rolling_mean = residuals.rolling(window=60).mean()
rolling_std = residuals.rolling(window=60).std()
# Calculate z-score
z_score = (residuals - rolling_mean) / rolling_std
We then create trading signals based on the z-scores:
entry_threshold = 2
exit_threshold = 0
longs = (z_score <= -entry_threshold)
shorts = (z_score >= entry_threshold)
exits = (np.abs(z_score) <= exit_threshold)
positions = pd.DataFrame(index=residuals.index).fillna(0)
positions['stock_A'] = longs.astype(int) - shorts.astype(int)
positions['stock_B'] = -hedge_ratio * positions['stock_A']
## Step 5: Measure performance and risk
Finally, let’s measure performance and risk using various metrics:
pnl_A = positions['stock_A'].shift(1) * log_returns_A
pnl_B = positions['stock_B'].shift(1) * log_returns_B
total_pnl = pnl_A + pnl_B
total_ret = total_pnl.cumsum()
sharpe_ratio = np.sqrt(252) * total_pnl.mean() / total_pnl.std()
print("Sharpe Ratio: ", sharpe_ratio)
plt.plot(total_ret)
plt.title("Cumulative Returns")
plt.xlabel("Time")
plt.ylabel("Return")
plt.show()
This chart shows the cumulative returns of our strategy. You can also calculate other performance metrics such as max drawdown, annual return, or the number of trades executed.
This provides an example of a statistical arbitrage strategy based on cointegration between two historically cointegrated stocks. The success of the strategy depends on the quality of the cointegration relationship, the market’s overall conditions, and execution efficiency.