Interpreting Johansen Cointegration Tests and Spread Stationarity
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.