financialnoob.me

Blog about quantitative finance

Cointegration-based multivariate statistical arbitrage

In this article I will implement and backtest a strategy based on a paper ‘Trading in the Presence of Cointegration’ (Galenko et al. 2009). Statistical arbitrage strategies are based on the same principles as pairs trading strategies, but they involve trading in a portfolio of several cointegrated assets instead of just a pair.

In the paper authors describe a strategy and provide a mathematical proof that the expected return of that strategy is always positive (without accounting for transaction costs). I didn’t try to follow the proof so I can’t say if it’s correct or not. Let’s just try to implement and backtest this strategy.


I will first implement what is called ‘in-sample testing’ in the paper. Here we use all available data to estimate all the parameters of the algorithm. So we use the same three years as both formation and trading period. I do not really understand the reason for this, but here is a quote from the paper explaining why it is done:

Performance measures always yield more insights when applied to in-sample testing, since we assume that these strategies could have been implemented ex post.

First step is loading and preparing data. I am going to use the same data I used in a couple of previous articles — 100 small-cap stocks from 2010–01–01to 2012–12–21. In out-of-sample test first two years will be used as a formation period and last year will be used as a trading period. Code for loading data is shown below.

Next thing we need to do is to find a portfolio of cointegrated stocks for trading. In the paper all testing is done on one portfolio of four cointegrated assets — SPY, EEM, EFA, EZU. All are ETFs representing different markets. I will also trade in portfolio of 4 cointegrated assets, but consisting of different stocks. Finding such cointegrated portfolios is probably the most difficult and time consuming (in terms of computation) part of the strategy. There are 3921225 possible combinations of 4 stocks out of universe of only 100 stocks. Testing them all takes a lot of time. And as we increase the number of stocks in the universe (or the number of stocks in a portfolio), this number grows very quick. For example, there are 75287520 possible portfolios of 5 stocks out of 100 stock universe (which is 19.2 times larger than the number of portfolios of 4 stocks).

Here we use Johansen test for cointegration. It is better suited for finding cointegrated portfolios of several assets than CADF test. There are two types of Johansen tests: with trace and with eigenvalues. I will use the one with eigenvalues. You can see how it works on the screenshot below.

Johansen test

First I apply the test to log-prices of four stocks. Them I’m using res.lr2 command to show the test statistics. Command res.cvm shows the critical values for 90%, 95% and 99% levels. Critical values are arranged in columns: first column shows critical values for 90% level, last column shows critical values for 99% level. For a portfolio to be cointegrated at a given level we need test statistics to be larger than critical values. Last line on the screenshot above demonstrates how to check if the portfolio is cointegrated at 95% level (we expected all values to be True).

Now we can look for cointegrated portfolios. Code for doing this is provided below.

It took me some time to figure out how to speed up this process of searching for cointegrated portfolios. First I’m creating a list of all possible combinations of 4 stocks. Then that list is converted to pandas dataframe. It is done to utilize some of its data processing functions that work a lot faster than Python lists (lines 34–38). Combinations are processed in random order and the search is stopped as soon as we find a required number of combinations (or when all the combinations have been proceseed). Note that on line 22 I delete the processed row from the index instead of deleting the dataframe row. It is also done to speed up the process. It turned out that deleting a row from a dataframe is a lot more time consuming.

Here is the portfolio of four stocks I found: ZION, NUAN, KBH, SNV. From Johansen test we also get the cointegration vector. It is the eigenvector associated with the largest eigenvalue. On the screenshot below you can see that the first eigenvalue is the largest, so we need to select the first eigenvector.

Cointegration vector

We can use cointegration vector to construct a linear combination of log-prices which should be stationary. Let’s check it.

Cointegrated portfolio
Augmented Dickey-Fuller test

Based on the results of ADF test shown above we can conclude that the resulting time series is indeed stationary.

Now we can construct the process Z_t which is basically the log returns of a portfolio with weights equal to elements of the cointegration vector.

Code for constructing and plotting Z_t is shown below.

Log-returns of cointegrated portfolio

Next we calculate the cumulative sum of the process Z_t using a rolling window of size P. This will be the spread we use to generate trading signals. Several different values of parameter P are tested in the paper. It seems that P=5 gives the best results, so that’s what I will use. Below you can see the code for constructing the spread and its plot.

Spread

The trading rules are simple: sell the portfolio when the spread is positive and buy the portfolio when the spread is negative. The only thing left is to adjust the weights of the stocks in portfolio such that equal amount of capital is allocated to long and short positions. It is done as follows:

Now we are ready to backtest. Since we used all the available data to calculate the cointegration vector, the weights will be the same in absolute values during the whole trading period, just their sign will change depending on whether we take long or short position. Note that I don’t calculate the amount of shares to buy (as done in the paper). I simply calculate the returns based on the fraction of capital allocated to each stock.

I will also calculate the returns of a simple buy-and-hold strategy where we allocate equal amount of capital to buy each of the four stocks at the beginning of trading period and sell them at the end of the trading period.

Cumulative returns (in-sample)

Apart from traditional performance metrics I also use bootstrap method to assess the strategy performance. It is described in detail in this article.

Performance metrics

Performance metrics of the algorithm are better than performance metrics of the buy-and-hold strategy or of individual stocks. Nonetheless Sharpe ratio of the algorithm is only 0.76 and it has large drawdown and drawdown duration. Note that I didn’t account for transaction costs, which will be big since the portfolio is rebalanced daily.

In the paper authors also provide some other performance metrics for assessing algorithm performance. I’ve also calculated some of them.

Additional performance metrics

We can see that returns of the algorithm are more volatile compared to returns of individual stocks or buy-and-hold strategy. Results provided in the paper are different. There the algorithm returns are less volatile than returns of individual stocks.

Another important metric is correlation of the algorithm returns with the returns of individual stocks. We expect such correlation to be close to zero. Let’s check it.

Return correlation

As expected algorithm returns are not correlated with returns of individual stocks.


Now we can perform an out-of-sample test. I can’t use the same four stocks because I used all the available data to find them, so this choice would have a look-ahead bias. I need to repeat the search process again but using only formation period data. The code is exactly the same, we just change one line:

res = coint_johansen(log_prices_form[[s1,s2,s3,s4]], det_order=0, k_ar_diff=1)

We change log_prices[[s1,s2,s3,s4]] to log_prices_form[[s1,s2,s3,s4]]. Portfolio I found consists of the following stocks: LPX, ZION, PBCT, TGNA.

After cointegrated portfolio for trading is found we proceed as follows:

  • On each trading day we use data from last W days to determine the cointegration vector b (I use W=504).
  • Calculated vector b is then used to construct a spread and calculate weights of stocks in the traded portfolio.
  • When the spread is positive we open a short position, when the spread is negative we open a long position.

The process above is repeated for each trading day. In the end we get a dataframe with positions we take at each trading day. We can use it to calculate algorithm returns. Code for this is shown below.

Code for calculating returns and different performance metrics is the same. First let’s take a look at the plot of cumulative returns.

Cumulative returns

Total return we get here is quite good — almost 100% in one year, but volatility is high. Performance metrics are shown below.

Performance metrics

Performance metrics of the out-of-sample test are actually better than performance metrics of in-sample test. Sharpe ratio has been increased almost twofold. One possible reason for this is using dynamic cointegration vector which is updated daily instead of using the same vector for the whole trading period.

Additional performance metrics

Again we see that returns of the algorithm are more volatile than returns of individual stocks. Another interesting thing is high correlation between algorithm returns and returns of TGNA stock (see below). The reason for this is not clear.

Return correlation

Overall the idea of finding and trading in portfolios of cointegrated assets seems interesting. Since there are so many possible combinations of stocks, the are more trading opportunities compared to pairs trading. Although this particular strategy does not provide a very good results, we need to keep in mind that this strategy is very simple.

Here are some ideas that can improve the performance:

  • During the trading period I never check that the selected portfolio is still cointegrated. Probably we should do it and in case it’s not — we should look for another portfolio to trade.
  • We always rebalance portfolio daily and don’t have any thresholds for opening and closing positions. It incurs high transaction costs. Setting thresholds for opening and closing positions can at least help us limit the amount of transaction costs.
  • Try different values of parameters and P.
  • Try portfolios with more than four assets or increase the total number of traded portfolios.

Jupyter notebook with source code is available here.

If you have any questions, suggestions or corrections please post them in the comments. Thanks for reading.


References

[1] Trading in the Presence of Cointegration (Galenko et al. 2009)

[2] Statistical arbitrage pairs trading strategies: Review and outlook (Krauss, 2015)

[3] Using cointegration to hedge and trade international equities. (Burgess, 2003)

Leave a Reply

Your email address will not be published. Required fields are marked *