Mean Reversion and Statistical Arbitrage › The Johansen Test for Multiple Cointegration

Michael BrenndoerferDecember 27, 20256 min read
Quantitative FinanceAll chapters
Part 11 of 15Quantitative Trading Strategies
Chapter 60Mean Reversion and Statistical Arbitrage › The Johansen Test for Multiple Cointegration
Reading section 8 of 12 · The Johansen Test for Multiple Cointegration · 6 minSection 8 of 12 · 6 min

Book contents

Section 8 of 12 · 6 min read

Mean Reversion and Statistical Arbitrage › The Johansen Test for Multiple CointegrationOne section · Sections in this chapter8 / 12 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

When working with more than two securities, the Engle-Granger method becomes cumbersome. The problem is that Engle-Granger requires you to choose one series as the "dependent variable" and can only identify one cointegrating relationship. With three or more securities, there may be multiple independent cointegrating relationships, and the choice of which series to regress on which becomes arbitrary. The Johansen test provides a more general framework for testing cointegration among multiple time series and identifying all cointegrating relationships simultaneously.

Theoretical Framework

The Johansen procedure tests for cointegration in a vector autoregression (VAR) framework using a Vector Error Correction Model (VECM). The VECM representation is particularly elegant because it decomposes price changes into two distinct components: a long-term error correction component that pulls prices back toward equilibrium when they deviate, and short-term autoregressive dynamics that capture momentum and other temporary effects. For a vector of nn time series yt\mathbf{y}_t, the VECM representation is:

Δyt=Πyt−1+∑i=1p−1ΓiΔyt−i+εt\Delta \mathbf{y}_t = \Pi \mathbf{y}_{t-1} + \sum_{i=1}^{p-1} \Gamma_i \Delta \mathbf{y}_{t-i} + \varepsilon_t

where:

  • Δyt\Delta \mathbf{y}_t: vector of price changes at time tt
  • Π\Pi: impact matrix governing long-run error correction
  • yt−1\mathbf{y}_{t-1}: vector of price levels at time t−1t-1
  • Γi\Gamma_i: matrices of coefficients for short-term dynamics
  • pp: lag order of the VAR model
  • εt\varepsilon_t: vector of error terms

The matrix Π\Pi contains all information about the long-run relationships among the series. Its rank rr equals the number of cointegrating relationships, and gives a characterization of the equilibrium structure:

  • r=0r = 0: No cointegration, all series have unit roots. The series can drift apart without bound.
  • 0<r<n0 < r < n: Cointegration exists with rr cointegrating vectors. There are rr independent linear combinations of the series that are stationary.
  • r=nr = n: All series are stationary. This case is unlikely for price series, which typically have unit roots.

When 0<r<n0 < r < n, the matrix Π\Pi can be decomposed as Π=αβ′\Pi = \alpha \beta', where β\beta is an n×rn \times r matrix whose columns are the cointegrating vectors, and α\alpha is an n×rn \times r matrix of adjustment coefficients that determines how quickly each series responds to deviations from equilibrium.

In[26]:
Code
from statsmodels.tsa.vector_ar.vecm import coint_johansen

# Generate three cointegrated series
np.random.seed(789)
n = 500

# Common stochastic trend with a small drift
trend = np.cumsum(0.05 + np.random.randn(n))

# Add distinct stationary AR(1) deviations around the shared trend.
stationary_deviations = []
for _ in range(3):
    deviation = np.zeros(n)
    for t in range(1, n):
        deviation[t] = 0.6 * deviation[t - 1] + 0.5 * np.random.randn()
    stationary_deviations.append(deviation)

series1 = trend + stationary_deviations[0]
series2 = 0.8 * trend + 50 + stationary_deviations[1]
series3 = 1.2 * trend + 100 + stationary_deviations[2]

# Combine into matrix
data_matrix = np.column_stack([series1, series2, series3])

# Perform Johansen test
# det_order: -1 = no deterministic terms, 0 = constant inside cointegration, 1 = constant outside
johansen_result = coint_johansen(data_matrix, det_order=0, k_ar_diff=2)
Out[27]:
Console
Johansen Cointegration Test Results
==================================================

Trace Statistic Test:
r     Trace Stat      90% CV       95% CV       99% CV      
-------------------------------------------------------
r≤0   153.2536        27.0669      29.7961      35.4628     
r≤1   73.1206         13.4294      15.4943      19.9349     
r≤2   1.9926          2.7055       3.8415       6.6349      

Cointegrating Vectors (normalized):
  Vector 1: [1.1684, -1.7605, 0.2016]
  Vector 2: [-1.0997, -0.7133, 1.3948]
  Vector 3: [0.0021, 0.0496, 0.0593]

The results show that the trace statistic exceeds the critical values for r≤0r \le 0 and r≤1r \le 1, but not for r≤2r \le 2. This implies the existence of two cointegrating relationships (r=2r=2) among the three assets, consistent with the data generation process where a single common trend drives all three series. With three series and two cointegrating relationships, there is exactly one common stochastic trend, which matches our construction.

The trace statistic tests the null hypothesis H0:r≤r0H_0: r \leq r_0 against H1:r>r0H_1: r > r_0. When the trace statistic exceeds the critical value, we reject the null and conclude there are more than r0r_0 cointegrating relationships. The sequential testing procedure starts with r0=0r_0 = 0 and continues until we fail to reject, thereby determining the number of cointegrating relationships.

Constructing Baskets from Cointegrating Vectors

The cointegrating vectors from the Johansen test define stationary linear combinations of the prices. These become tradeable spreads for stat arb strategies. Each cointegrating vector specifies the weights to use when combining the securities into a spread. For instance, if the first cointegrating vector is [1,−0.5,−0.3][1, -0.5, -0.3], the corresponding spread is computed as 1×Series1−0.5×Series2−0.3×Series31 \times \text{Series}_1 - 0.5 \times \text{Series}_2 - 0.3 \times \text{Series}_3. This spread will be stationary even though each individual series is non-stationary.

Out[28]:
Visualization
Line chart of three non-stationary price series driven by a shared stochastic trend.
Three raw price series sharing a common stochastic trend. All three series are individually non-stationary but are cointegrated.
Line chart of the first Johansen cointegrating spread oscillating around its mean with 2-sigma bands and an ADF p-value annotation.
First cointegrating spread from the Johansen eigenvector. The spread is stationary with a very low ADF p-value, confirming mean reversion.
Line chart of the second Johansen cointegrating spread oscillating around its mean with 2-sigma bands and an ADF p-value annotation.
Second cointegrating spread from the Johansen analysis. A second independent stationary linear combination exists among the three assets.