Skip to content
All library documents

Interpreting Johansen Cointegration Tests and Spread Stationarity

Article Quant Q&A · Author: Marx

Summary

The document describes a pairs-trading screening problem: applying the Johansen test to pairs of daily log stock prices, selecting rank-one cointegration candidates from trace statistics, and then checking the resulting spreads with ADF and KPSS tests. The author reports that only a minority of selected spreads pass the stationarity checks, with similar outcomes across the training-window lengths considered.

It raises a useful diagnostic issue but provides no answer explaining the discrepancy. The reported procedure uses default settings for the follow-up tests and constructs a spread from a normalized eigenvector coefficient. The document therefore does not establish whether the difference comes from test specification, finite-sample behavior, deterministic terms, lag choices, or implementation details. Its evidence is an empirical observation from the author's dataset, not a general conclusion about Johansen testing or pairs-trading performance.

Key ideas

  • Johansen trace statistics can be used to screen stock pairs for a candidate cointegration rank.
  • The author reports that ADF and KPSS checks find stationarity in only a minority of selected spreads.
  • The spread is formed using a coefficient from a normalized Johansen eigenvector.
  • The document raises possible specification and finite-sample issues but does not resolve them.

Tags

Full text
# Johansen Cointegration Test returns non-stationary error even though trace statistics suggest their existence


# Johansen Cointegration Test returns non-stationary error even though trace statistics suggest their existence












I'm working with Python and use the `statsmodels.tsa.vector_ar.vecm.coint_johansen` function to analyze if several stocks have a stationary error term with respect to pairs trading. I pass the log of the close prices for the stocks into the function and chose `constant term` for `det_order` (0) and number of lagged differences as 1 for `k_ar_diff` (1). Based on the trace statistics I select stock pairs if the `r0_trace` > `r0_crit` (5% quantile) and `r1_trace` <= `r1_crit` (5% quantile). Which is in line with the literature that I have read into that topic. Based on the weights for the pairs the resulting error should then be stationary.

Out of curiosity I then used the ADF and KPSS test on the resulting errors for the training periods, which then resulted in only 30% of the errors being stationary. Now I'm wondering where this issue has its roots, is it rather on the application of the Johansen-Test or based on the ADF/ KPSS-test.

The dataset contains daily close prices and the training period is either 1 year, 6 months, 3 months, or 1 month. The results for the ADF and KPSS tests do not really differ based on the training period duration.

For KPSS and ADF I use the default setting of the `statsmodels.tsa.stattools.adfuller/kpss` function.

Below is the code that determines if a pair has a stationary error (spread) for the training period.

```

# Here I define the Johansen summary

def coint_johansen_summary(data,Quantil=95):
    if Quantil == 90:
        a = 0
    elif Quantil == 99:
        a = 2
    else: 
        a = 1
    cj_test = coint_johansen(data, 0, 1)
    r0_trace, r1_trace = cj_test.lr1
    r0_crit, r1_crit = cj_test.cvt[:, a]
    co_int_vector = (cj_test.evec / cj_test.evec[0])[:, 0]
    return r0_trace, r0_crit, r1_trace, r1_crit, co_int_vector

# Here I calculate the Johansen summary for each possible pair in my data set for pair in combinations(training_data.columns, 2):
        p1, p2 = pair
        pair_names.append(p1 + '-' + p2)
        r0_t, r0_c, r1_t, r1_c, co_vec = coint_johansen_summary(training_data.loc[:, [p1, p2]].values, Trace_Quantil)
        r0_trace.append(r0_t)
        r0_crit.append(r0_c)
        r1_trace.append(r1_t)
        r1_crit.append(r1_c)
        coint_vectors.append(co_vec)

    results = pd.DataFrame(
        data = np.concatenate(
        (np.array(r0_trace).reshape(-1, 1), np.array(r0_crit).reshape(-1, 1),
        np.array(r1_trace).reshape(-1, 1), np.array(r1_crit).reshape(-1, 1),
        np.array(coint_vectors)),
        axis = 1),
        columns = ['r0_trace', 'r0_crit', 'r1_trace', 'r1_crit', 'co_vec1', 'co_vec2'],
        index = pair_names
    )

# Here I chose which pairs fit the trace statistics

co_int_pairs = results[(results.r0_trace > r0_crit) & (results.r1_trace <= r1_crit)]

# Here I then calculate the spread for the training data for each pair. Afterwards I would calculate the ADF and KPSS Test. 

    for row in co_int_pairs.iterrows():
        pair = row[0]
        hedge_ratio = row[1].loc['co_vec2']
        df_pair = training_data.loc[:, pair.split('-')]
        df_pair.loc[:, 'spread'] = df_pair.iloc[:, 0] - hedge_ratio * df_pair.iloc[:, 1]
```

Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.