financialnoob.me

Blog about quantitative finance

Pairs trading with copulas

Two previous articles were dedicated to describing the general idea behind copula and different copula families. Now we can apply these ideas to implementing a pairs trading strategy. In this article I am going to implement and backtest a strategy based on paper ‘Trading strategies with copulas’ by Stander at al. (2013).


The general algorithm is this:

  • Select pairs for trading
  • Fit marginal distribution for returns of each stock in the pair (either parametric of empirical)
  • Use probability integral transform to make marginal returns uniformly distributed
  • Fit copula to transformed returns
  • Use fitted copula to calculate conditional probabilities
  • Enter long\short positions when conditional probabilities fall outside predefined confidence bands (I will describe trading rules in more detail later)

We start with selecting stocks and pairs for trading. I will use constituents of Vanguard Small-Cap Value ETF (ticker: VBR) as my universe of stocks. The time period will be from 2018–11–20 to 2021–11–19. I am going to use first 30 months as formation period and last 6 months as trading period.

First step is identifying potential pairs for trading. I will select pairs with the highest Kendall’s rank correlation coefficient (Kendall’s tau) between log-returns. There are 256686 possible pairs and computing correlation for each pair takes some time. I provide the results of this computation in a csv file here, so that you don’t have to do it.

I am going to select 25 pairs with the highest Kendall’s tau (and consisting of different stocks). So if potential pair contains a stock that is already selected as a part of another pair, I’ll drop such pair and proceed to the next. Code for pair selection is shown below.

Let’s have a look at the selected pairs:

Now we can try and fit marginal distributions to log-returns data. I will try fitting four parametric distributions: normal, Student’s t, logistic and extreme. The selection will be made based on Akaike Information Criterion(AIC). For selected distribution I will also compute Bayesian Information Criterion (BIC) and p-value of Kolmogorov-Smirnov goodness-of-fit test.

Let’s look at the first few rows of resulting dataframe.

Marginal distributions

For all the stocks above Student’s t distribution was selected. P-values of Kolmogorov-Smirnov test are quite large, meaning that we can’t reject the null hypothesis that tested distributions are the same. That allows us to conclude that selected parametric distributions model empirical data sufficiently well.

Let’s see if any other distribution families were selected for other stocks.

No, Student’s t family is the best fit for all of the stocks.

Now let’s see if there are any stocks with low KS-test p-values (meaning that the selected parametric distribution is not a good fit).

No, seems that we are able to model all returns quite well. Now we can proceed to fitting copulas.

I am going to reuse the code I wrote for previous two articles on copulas. I made some changes and created classes for all copula families, but basically it’s the same code in a different wrap. Everything is saved in separate file (copulas.py), which I import in line 1. Another thing I need is two-dimensional Kolmogorov-Smirnov test, which is not implemented in scipy. I am using an implementation found here. It is imported in line 2. Everything else should be clear from the code:

  • Fit marginal distributions (using only Student’s t distribution family)
  • Apply probability integral transform to marginal returns (by plugging returns into selected distribution’s cdf)
  • Fit each copula family (Gaussian, Clayton, Gumbel, Frank, Joe) and compute AIC
  • Select copula with the smallest AIC and calculate its BIC and KS-test p-value
Selected copulas for first 10 pairs

We can see above (from KS-test p-values) that selected copulas provide a good fit for the empirical data. We can also check if any other copula families were selected for other pairs and if any pairs have low p-values of KS-test.

Only two copula families are used (Gaussian and Gumbel) and they seem to fit the empirical data sufficiently well.

In the paper authors try to fit all 22 different families of Archimedean copulas listed and Roger B. Nelsen’s book ‘An introduction to copulas’. I wasn’t able to find a library for Python with all Archimedean copulas implemented, so I decided to only use a limited number of copulas I implemented myself.

Now we need to define trading rules. Remember that we are trading in a spread — buying one stock and simultaneously selling another. So we need to know when one stock is underpriced and another stock is overpriced. That’s where copula can help.

First we need to calculate conditional probabilities:

Conditional probabilities from copula

It is not necessary to solve these equations analytically. I have implemented class methods to compute conditional probabilities numerically from a random sample. Now we just need to plug returns into respective marginal CDF (apply probability integral transform to transform returns to standard uniform distribution) and we are ready to calculate conditional probabilities. If calculated probability is too low, then the stock is underpriced, it calculated probability is too high, then the stock is overpriced. Conditions for opening long\short positions are presented below. Random variables Uand V represent transformed returns of each stock in the pair and is called a confidence level.

Rules for opening positions

In many other pairs trading strategies decisions are made based on the price (or return) of the spread. If the spread was mispriced we automatically assumed that one stock must be too high and another is too low (or vice versa). Here we explicitly check both conditions: that one stock is overpriced and another is underpriced. Both conditions must be true for us to open a position.

According to the paper, we should ‘exit the trade when the two stocks revert to their historical relationship’, but no specific rules are provided. In another paper (Liew and Wu, 2013) positions are closed when the conditional probabilities cross the boundary of 0.5. I’m going to use that rule in my backtest.

Probably these rules not seem very clear now, but it’ll be easier to understand from the code below.

I process each pair separately and do the following:

  • Fit marginal distribution for each stock in the pair
  • Transform marginal returns into standard uniform distribution using probability integral tranfsorm
  • Fit copula on transformed returns (all steps up to this are performed on formation period data)
  • Calculate conditional probabilities for each day in trading period (using marginals and copula from the previous steps)
  • Create positions dataframe according to the rules described above
  • Calculate algorithm returns

After running that code, we have a dictionary with returns for each individual pair. Recall that we were using log-returns and they need to be transformed back to simple returns.

To calculate total returns we sum daily returns of each pair and divide it by total number of traded pairs (assuming equal capital allocation to each pair). We also multiply total return by 2 since we can use cash from short positions to open long positions. Below you can see the code for calculating and plotting returns. I also plot returns of VBR ETF for comparison.

Algorithm returns vs VBR returns

The total return is quite small, but we can see that algorithm returns are a lot less volatile. Let’s look at some metrics.

Algorithm metrics vs VBR metrics

Even though the total return is not high, the Sharpe ratio of out algorithm is very good and its maximum drawdown is only 0.4% compared to 9.6% drawdown of VBR. Please note that here I didn’t account for any transaction costs.

Let’s try a little modification of this strategy and see what happens when we exit positions earlier. Instead of using conditional probability boundary of 0.5, I will use a boundary of 0.3 for long positions and 0.7 for short positions. We only need to modify one part of the code (where we define rules for exiting positions):

Plot of the cumulative returns and performance metrics are shown below.

Algorithm returns vs VBR returns
Algorithm metrics vs VBR metrics

We get a slight increase in APR and Sharpe ratio, but I can’t say that we got any significant improvement.


Overall I think that the results we got are not that bad. It might be possible to improve the performance of the strategy by making some modifications. Some ideas to test:

  • Use better methods of pair selection.
  • Implement and try to fit more different copulas.
  • Update pairs (or at least refit marginal distributions and copulas) more often.
  • Derive better rules for closing positions.
  • Use higher frequency data.

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.


UPDATE 05.04.22: There was a mistake in data loading part of the notebook. It is fixed now. Thanks to Valery T for pointing it out.

UPDATE 29.09.22: Fixed a small typo in the part about probability integral transform. Thanks to Jonah Chan for pointing it out.

Leave a Reply

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