Technical

Johansen Cointegration Test: Practical Guide (2026)

Use the Johansen cointegration test correctly: VECM rank, trace and maximum-eigenvalue tests, deterministic cases, Python and trading pitfalls.

15 min read·

What Is the Johansen Cointegration Test?

In 1988, Søren Johansen published Statistical Analysis of Cointegration Vectors. The paper considers an I(1)I(1) Gaussian vector autoregression and estimates the dimension of its cointegration space by maximum likelihood. With kk series, the method can find a rank from zero to k−1k-1, not merely ask whether one bivariate spread is stationary.

The Johansen cointegration test is therefore a sequence of likelihood-ratio tests on the rank of a long-run matrix in a Vector Error Correction Model (VECM). The trace statistic tests a broad alternative of more than rr cointegrating relations. The maximum-eigenvalue statistic tests rr against r+1r+1.

It is more than a multivariate Engle-Granger button. Lag length, deterministic terms, integration order and structural stability determine the null distribution and the economic meaning of the result. Our cointegration guide covers the foundation; this article concentrates on the rank test and what a trading researcher must do after it.


From VAR to Cointegrating Rank

For a kk-vector yty_t, start with a VAR of order pp:

yt=A1yt−1+⋯+Apyt−p+εt.y_t=A_1y_{t-1}+\cdots+A_py_{t-p}+\varepsilon_t.

Its VECM representation is

Δyt=Πyt−1+∑i=1p−1ΓiΔyt−i+εt,\Delta y_t=\Pi y_{t-1} +\sum_{i=1}^{p-1}\Gamma_i\Delta y_{t-i} +\varepsilon_t,

with

Π=∑i=1pAi−I,Γi=−∑j=i+1pAj.\Pi=\sum_{i=1}^{p}A_i-I, \qquad \Gamma_i=-\sum_{j=i+1}^{p}A_j.

The rank of Π\Pi gives the long-run structure:

  • rank⁡(Π)=0\operatorname{rank}(\Pi)=0: no stationary linear combination is identified and the model is a VAR in differences.
  • 0<rank⁡(Π)=r<k0<\operatorname{rank}(\Pi)=r<k: there are rr cointegrating relations and Π=αβT\Pi=\alpha\beta^{\mathsf T}.
  • rank⁡(Π)=k\operatorname{rank}(\Pi)=k: the levels are stationary under the specification, so this is not an I(1)I(1) cointegrated system.

The columns of β\beta span the cointegration space. The rows of βTyt\beta^{\mathsf T}y_t are stationary relations under the model. The loading matrix α\alpha describes how each variable responds to disequilibrium. Neither matrix is unique without normalising restrictions: replacing α,β\alpha,\beta by αC−T,βC\alpha C^{-\mathsf T},\beta C for any invertible CC leaves Π\Pi unchanged.

Johansen's eigenvalues are squared canonical correlations between residualised Δyt\Delta y_t and residualised yt−1y_{t-1}. Order them λ^1≥⋯≥λ^k\widehat\lambda_1\geq\cdots\geq\widehat\lambda_k. With effective sample size TeT_e,

Jtrace(r)=−Te∑i=r+1klog⁡(1−λ^i),J_{trace}(r)=-T_e\sum_{i=r+1}^{k} \log(1-\widehat\lambda_i),

tests H0:rank⁡(Π)≤rH_0:\operatorname{rank}(\Pi)\leq r against rank greater than rr, while

Jmax(r)=−Telog⁡(1−λ^r+1)J_{max}(r)=-T_e\log(1-\widehat\lambda_{r+1})

tests rank rr against r+1r+1. The distributions are non-standard and depend on the deterministic specification. Do not compare them with chi-squared critical values.


Rank Selection and Deterministic Terms

Rank is selected sequentially. Start at r=0r=0, reject or fail to reject using the critical value for the chosen case, then move to r=1r=1 only after rejection. The first non-rejection is the estimated rank. "Fail to reject" is the right phrase; the procedure does not prove that the rank equals rr.

Suppose Te=200T_e=200 and three ordered eigenvalues are 0.12, 0.05 and 0.01. Then

Jtrace(0)=37.84,Jtrace(1)=12.27,Jtrace(2)=2.01,J_{trace}(0)=37.84,\quad J_{trace}(1)=12.27,\quad J_{trace}(2)=2.01,

and

Jmax(0)=25.57,Jmax(1)=10.26,Jmax(2)=2.01.J_{max}(0)=25.57,\quad J_{max}(1)=10.26,\quad J_{max}(2)=2.01.

Those numbers have no verdict on their own. They must be compared with case-specific critical values for three variables. A table of invented normal p-values, as appears in many tutorials, is wrong.

The deterministic case is part of the null hypothesis. A constant restricted to the cointegration relation allows a non-zero spread mean without a linear drift in levels. An unrestricted constant permits drift in the differenced process. Trend cases make still stronger assumptions. Johansen (1991), Estimation and Hypothesis Testing of Cointegration Vectors in Gaussian Vector Autoregressive Models, develops the likelihood framework and restrictions.

Statsmodels' low-level coint_johansen uses det_order=-1 for no deterministic term, 0 for a constant and 1 for a linear trend. Zero is not "no constant". The higher-level VECM API then splits that constant into two economically different cases: inside the cointegration relation (ci) or outside it (co).

Johansen caseWhat it allowsselect_coint_rankVECM(deterministic=...)
No deterministic termNo intercept, no trenddet_order=-1"n"
Restricted constantNon-zero spread mean, no linear drift in levelsdet_order=0"ci"
Unrestricted constantDrift in the differenced processdet_order=0"co"
Restricted trendTrend inside the cointegrating relationdet_order=1"li"
Unrestricted trendLinear trend outside the relationdet_order=1"lo"

The rank-test flag cannot tell ci from co. Both use det_order=0 and the same tabulated critical values. The VECM string is what encodes the economic restriction, so it must match the case you actually intend.


Python workflow

The code below uses log price levels, selects a VAR lag order on the training sample, converts pp to the VECM's p−1p-1 lagged differences and then applies the trace procedure. The select_coint_rank documentation supports significance levels 1%, 5% and 10%.

import numpy as np import pandas as pd from statsmodels.tsa.api import VAR from statsmodels.tsa.vector_ar.vecm import VECM, select_coint_rank def fit_johansen(prices: pd.DataFrame, max_var_lags: int = 10): levels = np.log(prices).replace([np.inf, -np.inf], np.nan).dropna() selected = VAR(levels).select_order(maxlags=max_var_lags) var_lags = max(1, int(selected.selected_orders["bic"] or 1)) k_ar_diff = var_lags - 1 rank_test = select_coint_rank( levels, det_order=0, k_ar_diff=k_ar_diff, method="trace", signif=0.05, ) model = VECM( levels, k_ar_diff=k_ar_diff, coint_rank=rank_test.rank, deterministic="ci", ).fit() return rank_test, model

The example uses the restricted-constant case: a non-zero mean in the cointegrating relation and no linear drift in levels. That is the usual specification for log-price spreads. If you instead want an unrestricted constant, change deterministic to "co" and keep the same det_order=0 rank test, but do not mix the two interpretations in the same write-up. If theory says there is no intercept at all, use det_order=-1 with "n".

After fitting, inspect residual autocorrelation, normality, parameter stability and the roots of the companion system. Normalise β\beta for interpretation, but remember that each eigenvector is an estimated basis vector, not a uniquely identified trading portfolio.

For a one-vector system, a normalisation such as

βTyt=y1,t−0.6y2,t−0.3y3,t−0.1y4,t\beta^{\mathsf T}y_t= y_{1,t}-0.6y_{2,t}-0.3y_{3,t}-0.1y_{4,t}

defines a spread. It does not by itself define notionals. Log-price coefficients describe elasticities, while a self-financing trade needs prices, contract multipliers, currency conversion and a rebalancing convention. Test the spread's half-life and stability using the methods in our mean-reversion testing guide.

From cointegration space to a candidate trade

When r>1r>1, the reported eigenvectors are only one basis for an rr-dimensional space. Rotating that basis produces different-looking stationary combinations with the same span. Selecting the vector with the fastest apparent mean reversion from the same sample is another optimisation step. Put that choice inside the training loop.

Estimate the VECM, then form spreads with coefficients frozen at the end of each training window. Generate the next-period signal from those frozen coefficients and rebalance at an available later price. Recomputing β\beta on the full history before plotting the spread sends future data into every past value.

The loading matrix helps with economic interpretation. For one relation, row ii of α\alpha measures variable ii's response to the lagged equilibrium error, conditional on short-run terms. A coefficient insignificantly different from zero suggests weak exogeneity for that relation; it does not prove that the asset can be treated as an error-free anchor. Formal restrictions on α\alpha and β\beta should be tested within the Johansen likelihood framework.

Convert statistical coefficients into tradable units at each rebalance. A coefficient on log futures prices does not automatically respect contract values or duration risk. Dollar-neutral, beta-neutral and duration-neutral portfolios are different normalisations. Choose the exposure that matches the economic hypothesis, then calculate residual market, sector, currency and carry risk.

The final gate is predictive. Track out-of-sample spread variance, estimated half-life, sign of adjustment, turnover and net P&L. A rank that remains statistically non-zero while coefficients drift wildly is not a stable trading relation.


Where the Johansen Test Goes Wrong

Mixed integration orders. The classical setup assumes the variables are I(1)I(1) and innovations satisfy the model conditions. An I(0)I(0) series can raise the estimated rank; an I(2)I(2) series needs a different treatment. ADF and KPSS tests have limited power and opposite nulls, so combine them with plots, economic reasoning and break-aware tests rather than using a mechanical gate.

Lag search on the full sample. Selecting pp, deterministic terms and the asset basket using all dates leaks the test period into the specification. Make those choices in each training window. Too few lags leave serial dependence; too many consume power and can destabilise estimates.

Structural breaks. A stable-looking relation over the full history may be two unrelated regimes joined together. Rolling eigenvalues, recursive estimates and break tests matter more to a trading desk than one full-sample rejection. A merger, index change or monetary-policy shift can destroy the relation without warning.

Multiple testing. Searching thousands of baskets and keeping those that reject at 5% does not produce a 5% false-discovery rate. Correct the research search, reserve an out-of-sample period and include all failed baskets in the trial ledger.

Trace versus maximum eigenvalue. Neither statistic is universally "more conservative" or guaranteed more powerful in short samples. Report both, pre-specify the rank rule and investigate disagreement. Small-sample and non-Gaussian behaviour may justify bootstrap critical values, but the bootstrap must preserve the fitted null and time dependence.

Trading interpretation. Cointegration is an equilibrium property, not a profit forecast. Borrow costs, execution, shortability and slow adjustment can consume the spread. The loading vector α\alpha helps identify which series adjusts, but its sign and magnitude must be read jointly with the chosen normalisation of β\beta.

Our pairs trading guide covers the execution layer and the statistical arbitrage guide places a candidate spread inside portfolio and risk controls.


Frequently Asked Questions

What does the Johansen test test?

It tests the rank of the long-run matrix Π\Pi in a VECM, using non-standard trace or maximum-eigenvalue likelihood-ratio statistics.

How is Johansen different from Engle-Granger?

Engle-Granger estimates one relation, is sensitive to the chosen dependent variable and is commonly used for two series. Johansen estimates a cointegration space and can identify more than one relation in a multivariate system.

What is the null hypothesis of the trace test?

For candidate rank rr, the null is rank⁡(Π)≤r\operatorname{rank}(\Pi)\leq r; the alternative is rank greater than rr. Rank selection stops at the first failure to reject.

Does statsmodels return Johansen p-values?

Its standard result exposes test statistics and tabulated critical values, not ordinary normal or chi-squared p-values. select_coint_rank performs the sequential decision.

Should the test use prices or returns?

Use the non-stationary level variables whose cointegration you are testing, often log prices. Returns are commonly I(0)I(0), in which case the classical cointegrating-rank question is misplaced.

Can a significant vector be traded directly?

No. Normalisation, stability, adjustment speed, notional mapping, costs and out-of-sample performance all remain to be established.

Skip the £25k programme - try the alternative

Master's programmes are slow and expensive. Quantt is a self-paced alternative. Start free with a real lesson and interview practice, then unlock 50+ courses and your personalised plan.

Free to start · No credit card required