Quant500 Academy

7. Pairs trading: the difference, not the direction

Level 5: Momentum, Mean-Reversion and Pairs Trading Strategies

Pairs trading takes the idea of mean reversion to two related assets instead of a single one. Rather than looking at whether one price has drifted from its own average, you look at the difference (the spread) between two assets, and bet that this difference — not the price of each one separately — returns to its usual level.

For that bet to make statistical sense, the two assets must be cointegrated: in plain terms, that although each one moves more or less randomly on its own, the difference between them stays stable around a long-run average. In practice, two companies selling products that substitute for each other (Coca-Cola and Pepsi, for example) tend to be cointegrated: both move with the consumption and sector cycle, without drifting far apart from each other in any sustained way.

from statsmodels.tsa.stattools import coint

datos = yf.download(["KO", "PEP"], start="2020-01-01", end="2023-01-01", progress=False)
precio_ko = datos["Close"]["KO"]
precio_pep = datos["Close"]["PEP"]

estadistico, pvalor, valores_criticos = coint(precio_ko, precio_pep)

The cointegration test sets up a null hypothesis that there is NO cointegration between the two series, against the alternative hypothesis that there is. A small p-value (usually below 0.05) lets you reject that null hypothesis: there is statistical evidence that the relationship is cointegrated, and therefore the strategy rests on reasonable ground.

With cointegration confirmed, the mechanics are the same as in the previous module, applied to the spread rather than to a single asset's price: when the spread drifts above its average (z-score > 1), you go short the asset that rose relatively more and long the one that rose relatively less; when the spread falls below (z-score < -1), the position is reversed.

spread = precio_pep - precio_ko

def zscore(serie):
    return (serie - serie.expanding().mean()) / serie.expanding().std()

z = zscore(spread)

The advantage of trading the spread, rather than each asset separately, is that the strategy ends up market neutral: if both shares fall 5% on a macro headline that hits the whole sector, the spread barely moves — but careful, that only holds if the spread is weighted. A raw subtraction of prices is not: with PEP at 180 and KO at 60, a common fall of 5% takes the spread from 120 to 114. You have to use the hedge ratio that the cointegration regression itself estimates (spread = precio_pep - beta * precio_ko), or work with the price ratio or with log prices — the gain or loss comes from the relative difference between the two, not from the general direction of the market.

So that you are not left testing pairs at random, the search can be automated: sweep every possible combination inside a universe of shares and keep only those that pass the cointegration test.

def encontrar_pares_cointegrados(precios):
    n = precios.shape[1]
    matriz_pvalores = np.ones((n, n))
    columnas = precios.columns
    pares = []

    for i in range(n):
        for j in range(i + 1, n):
            _, pvalor, _ = coint(precios[columnas[i]], precios[columnas[j]])
            matriz_pvalores[i, j] = pvalor
            if pvalor < 0.05:
                pares.append((columnas[i], columnas[j]))

    return matriz_pvalores, pares

The loop for j in range(i + 1, n), starting at i + 1 rather than at 0, avoids two redundancies: comparing an asset with itself, and testing the same pair twice in reverse order (testing A-B is exactly the same as testing B-A as far as cointegration goes). It is a small detail, but in a universe of hundreds of shares, avoiding redundant comparisons saves a real amount of computation.

To go deeper into this lesson

The rest of Quant500

What you are learning, applied to the index every day:

The model portfolio, weekly in your inbox

Every Saturday, its week in one minute: how it went, the best and the worst, and which insiders bought with their own money. Plus, the day the model is rebuilt, which stocks come in and which go out. A simulation, not advice. At most one email a week, no ads, no account; one-click unsubscribe. Made by one person: Pol Gracia.

A model portfolio with simulated results, not a personalized recommendation. Your email is used for this only: privacy.

Behind the Academy and the rest of Quant500 there is one person: Pol Gracia Serrahima.