financialnoob.me

Blog about quantitative finance

Pairs trading with cointegrated Logistic Mixture Autoregressive (LMAR) model

In the previous article I presented a working implementation of EM algorithm for estimating parameters of LMAR time series model. In this article I will describe and implement a pairs trading strategy that uses LMAR to model the spread. The strategy is based on a paper ‘Basket trading under co-integration with the logistic mixture autoregressive mode’ (Cheng et al. 2011).

I’ve already described how LMAR model is represented and how to use EM algorithm to estimate its parameters, so I’m not going to repeat it here. You can read more about it in my previous articles:

Since EM algorithm is already implemented, we can start building a trading strategy.


First step is preparing the data and selecting pairs for trading. Pair selection is done based on the parameters of LMAR model, therefore we need to try and fit LMAR model to all potential pairs. Since this process is very time consuming I’m going to use only 100 stocks in my stock universe. This will result in 4950 potential pairs.

I use 100 random stocks from Vanguard Small-Cap Value ETF (VBR). The training period is 3 years — from 2013–01–01 to 2015–12–31. The trading period is from 2016–01–04 to 2016–04–20 (75 trading days following the training period).

The trading period is so short because I only fit the models and selected pairs for trading once. Increasing the trading period will require refitting LMAR models every 75 days (possibly even more often), and as I said, it is computationally expensive and takes a lot of time, so I just perform it once.

Stock universe is selected as follows:

Stock universe

Full code for data preparation is shown below.

Now we create a list of potential pairs and try to determine the dimensions of LMAR model for each of them. Note that the spread is constructed from cumulative returns, not log returns.

Recall that LMAR model has 3 dimensions:

  • n — number of lags in logistic equation for mixing probabilities
  • m_1 — order of AR model of the first (stationary) regime
  • m_2 — order of AR model of the second (non-stationary) regime

I’m trying two values ([1,2]) for each of the dimensions, so for each pair we try to fit 2³=8 models. In the paper they use lags up to 3, so the total number of models tested for each pair increases to 3³=27, which is almost 3.5 times more computationally expensive.

Since the results of EM algorithm depend on the starting parameters, I run it up to 5 times for each set of dimensions (or until convergence). The best model is selected based on Bayesian Information Criterion (BIC). Parameters of the best model are saved in a dataframe. You can read more about model selection process in my previous article. Note that when calculating BIC (line 46) the number of parameters of the model (last term in parenthesis) is increased by 2 to include parameters of the spread.

In the end we have a dataframe that looks like this:

LMAR models dataframe

This process takes a lot of time, so that dataframe is provided in csv format on my github page.

Now we need to get better estimates and calculate standard deviations of the LMAR model parameters for each pair. To save some time I’m going to use only a subset of all possible pairs — I’m selecting only those pairs, for which more than 4 out of 8 possible models converged successfully (as indicated in the column num_fit in the dataframe). My reasoning for this is the following: if LMAR model is indeed a good fit for modeling the spread of a given pair, then at least several possible models (with different dimensions) should converge. For example, in the simulation studies, that I performed in the previous article, all 8 potenital models converged in all of the simulations. So the next step is performed only for pairs with num_conv>4.

First I prepare a dataframe for new data. Code for it is shown below.

First I select pairs with num_conv>4. Then I remove unnecessary columns and calculate the standard deviation of the spread, which will be needed later.

Now for each of the pairs I run EM algorithm 10 times, each time with different initial values. At each iteration, the algorithm can be restarted up to max_trials=5 times if it doesn’t converge in max_iter=150 iterations. I use the model dimensions determined at the previous step. The results of each run are saved in a numpy array. Code for doing it is provided below.

This process also takes a very long time. In the end I have a dictionary, where for each pair I have the results from each of 10 runs of EM algorithm.

Now I remove the outliers and calculate the best values of parameters and their standard deviations.

The best parameters are set to be the median of the results from 10 runs of the EM algorithm (excluding outliers). I also save the number of runs for which the algorithm converged. In the end we get a dataframe that looks as follows (it is also provided in a csv format on my github page).

LMAR parameters dataframe

Note that we are only interested in parameters delta — coefficients of the logistic equation for mixing probabilities.

Potentially tradable pairs should have all deltas (except delta_0) non-negative, with at least one delta positive. I also use an additional condition that the EM algorithm converged for at least 5 out of 10 iterations, which should indicate that the tested model is indeed a good fit. Code for selecting such pairs is shown below.

The first condition (cond1) is true if delta_1 is more than 2 SD larger than 0 (significantly larger than 0) and delta_2 is within 2 SD from 0 (not significantly different from 0). The second condition (cond2) is true if delta_1is not significantly different from 0 and delta_2 is significantly larger than 0. And the third condition (cond3) is true if both delta_1 and delta_2 are significantly larger than 0. We select the pairs that satisfy either one of these conditions.

Another thing we should check for is that the spread consists of opposite positions (long and short) in two stocks. When we run OLS algorithm to calculate the spread, some pairs have a negative coefficient in front of the second stock, which would require two long positions to construct the spread. Such spread violates the basic principle of pairs trading — having both long and short positions simultaneously. So we need to remove such pairs. I should have done it earlier, at the first step, but I realized that too late.

In the end I get a dataframe of 234 potential pairs and their parameters.

Recall that there is a kind if regime detection mechanism built into LMAR model. The rule for opening and closing positions is based on the probability of being in a stationary (mean-reverting) regime. Recall that this probability is determined by the logistic equation shown below.

Logistic equation for mixing probabilities

In the equation above a_t indicates the value of the spread. The position is opened when the probability of being in mean-reverting regime p_1 exceeds its 0.95 quantile. The position is closed when p_1 falls below its 0.5 quantile (median value). Quantiles are calculated based on training data.

The type of the position (long or short) is determined based on the sign of the spread. We sell short the spread if it is positive, and we buy the spread if it is negative.

Let’s extend our dataframe with 0.5 and 0.95 quantiles of the mixing probability p_1.

The resulting dataframe looks like this:

Pairs dataframe


The strategy proposed in the paper works as follows:

  • Out of potential pairs select 10 pairs with the smallest standard deviation of the spread in the training period.
  • For each pair calculate 0.95 and 0.5 quantile of the probability p_1.
  • Monitor the selected pairs for the observation period of 25 trading days. If no positions are opened during that period, the whole process of model fitting and pair selection needs to be repeated again.
  • After position is opened, set the maximum trading life to 50 days. If the position is not closed during that period, we close it on the last day of the trading life.

There are also some rules for optimal capital allocation: we calculate the maximum number of simultaneously open positions during some training period and use it for capital allocation in the trading period. I’m am not going to implement that part.

Let’s see how many of the top 10 pairs have positions opened within the first 25 trading days. Code is provided below.

The results are shown below. Only one pair out of 10 is traded during 25 day observation period.

Let’s try to calculate the performance of this strategy assuming that all capital is allocated to that one pair.

Note that in the end I cut the days after the last position is closed. At this point we would need to rerun our model selection and parameter estimation processes and look for the new pairs.

Performance metrics of this algorithm along with the plot of its cumulative returns are provided below.

Performance metrics
Plot of cumulative returns

Above we assumed that one unit of capital is allocated to trade that pair, but let’s try to look at the positions we take.

Positions dataframe

Note that we have only 0.4638 units of capital invested in the long leg of the pair. Opening a short position costs us nothing and maybe we can even use a portion of the capital we get from short selling, but I will not take that into account. What we need to account for is that the returns we got at the previous step should be divided by the amount of invested capital, which is equal to 0.4638.

Code for calculating new cumulative returns is shown below.

Performance metrics

As you seen it more than doubles our returns. And this is the way that authors use to calculate returns of the strategy in the paper.

The results we got look very good, but they are not very trustworthy since they are based only on one traded pair (and only one position). I have noticed that if I increase the observation period to 30 days, I get 2 pairs to trade. More data is always better, so let’s try to see how the algorithm performs with 2 pairs. I’m not going to post all the code again, you can find it in a Jupyter notebook. I’ll just show some results.

The traded pairs are the following:

Selected pairs

And the performance metrics of all tested approaches are shown below.

Performance metrics

Results look promising, but with only 2 pairs (and less than 75 trading days) in our backtest they are not really trustworthy. Another issue is that we can’t know beforehand how much capital we will use and how to allocate it smartly between different positions, so the results presented above are not exactly realistic. To do a proper backtest we need to do it over a longer period of time with some rules for capital allocation.

One way to try and improve the use of the capital would be to always have an up to date dataframe with model parameters for each pair (refreshed every day) and open the next position as soon as the previous one closes. I believe it can significantly improve the performance of the strategy, but it would be significantly more computationally expensive. Below I describe some other things that can potentially improve algorithm’s performance.

Further improvements:

  • increase model dimensions (lags up to 3 are used in the paper)
  • increase the number of trials \ the number of runs of EM algorithm to get better estimates of the parameters
  • update probability quantiles during trading period (with each new data point)
  • use bigger stock universe
  • increase the significance level when testing whether coefficients delta are significantly positive or insignificantly different from zero (e.g. use SD=3 instead of SD=2).

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] Basket trading under co-integration with the logistic mixture autoregressive model

[2] On a logistic mixture autoregressive model

Leave a Reply

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