Cointegration vs correlation: ловушка, которая убивает pair trading
Почему высокая correlation не делает пару прибыльной — и что cointegration реально тестит. Engle-Granger с runnable Python.
Если ты прогонял корреляции 50 crypto-пар за 540 дней, сортировал по Pearson r, брал верхние и пробовал торговать их ratio — ты, скорее всего, уже знаешь, что высокая correlation не делает пару прибыльной. Это самая распространённая ошибка в pair trading, и она кладёт больше retail-попыток стратегии, чем slippage и комиссии вместе взятые.
Correlation говорит, что две серии склонны двигаться в одну сторону в одно время. Она не говорит, что их ratio возвращается к стабильному значению. Это разные статистические свойства, и pair trading на ratio зависит от второго, не от первого.
Правильный тест — cointegration. Этот пост проходит через то, что cointegration реально означает, как тестить её в Python и как она соотносится с Hurst + ADF подходом из нашего walk-forward гайда. К концу ты должен уметь отличить tradable пару от correlated-but-doomed без единого бэктеста.
Что correlation реально мерит
Pearson's коэффициент r мерит линейную связь между двумя сериями
после того, как каждая центрирована на свой mean и масштабирована на
свой standard deviation. Формула:
r = np.corrcoef(returns_a, returns_b)[0, 1]
Что это даёт: число между −1 и +1. Высокий positive r значит, что
когда одна серия выше своего mean, другая тоже склонна быть выше
своего mean. И всё.
Три вещи, которые r не говорит:
- Стабилен ли ratio. Две серии могут обе расти с похожей
скоростью с
rоколо 1.0, при этом их ratio монотонно дрейфует в одну сторону. - Стабильно ли уровневое отношение. Correlation
scale-invariant. Серия в 100× больше другой может иметь
r = 0.99, если их дневные движения совпадают по знаку. - Есть ли mean-reversion в спреде. Correlation — свойство returns; mean-reversion — свойство уровней (или log-ratio уровней). Разные домены.
Последний пункт — источник путаницы. Люди гоняют np.corrcoef на
ценах (не returns), видят высокое число и заключают, что пара
подходит для трейдинга. Correlation на ценах двух trending серий
почти всегда высокая — не потому что пара mean-revert'ит, а потому
что обе серии разделяют общий trend-компонент.
Killer контрпример
Вот синтетика, которая показывает, почему correlation сама по себе обманчива. Две geometric Brownian motion серии с похожим drift и похожей volatility:
import numpy as np
import pandas as pd
np.random.seed(42)
n = 540
dt = 1.0 / 365.0
drift_a = 0.6 # ~60% annualized drift
drift_b = 0.5
sigma = 0.8
shocks_a = np.random.normal(0, sigma * np.sqrt(dt), n)
shocks_b = np.random.normal(0, sigma * np.sqrt(dt), n)
log_price_a = np.cumsum((drift_a - 0.5 * sigma**2) * dt + shocks_a)
log_price_b = np.cumsum((drift_b - 0.5 * sigma**2) * dt + shocks_b)
price_a = 100 * np.exp(log_price_a)
price_b = 100 * np.exp(log_price_b)
# Correlation on prices
r_prices = np.corrcoef(price_a, price_b)[0, 1]
print(f"Correlation on prices: {r_prices:.3f}")
# Correlation on returns
returns_a = np.diff(log_price_a)
returns_b = np.diff(log_price_b)
r_returns = np.corrcoef(returns_a, returns_b)[0, 1]
print(f"Correlation on returns: {r_returns:.3f}")
# Now the log-ratio
log_ratio = np.log(price_a / price_b)
print(f"Log-ratio drift: {log_ratio[-1] - log_ratio[0]:.3f}")
Типичный output:
Correlation on prices: 0.972
Correlation on returns: -0.018
Log-ratio drift: 1.840
Correlation на ценах — 0.97. Выглядит отлично. Correlation на returns — практически ноль: между сериями нет общего шок-структуры. А log-ratio дрейфует на 1.84 за окно — то есть в ценовом выражении ratio больше чем утроился. Эта серия и близко не mean-reverting, и любой pair-trade entry на основе «высокой correlation» сливал бы деньги всё это время.
Урок: высокая correlation на ценах — это в основном утверждение про общий trend, не про tradable spread.
Что cointegration реально тестит
Cointegration (Engle & Granger, 1987) тестит другое. Интуиция: если
две non-stationary серии A и B cointegrated, то существует линейная
комбинация A − β·B, residuals которой стационарны. Stationary
значит, что residual-серия имеет стабильный mean и ограниченную
дисперсию — ровно то свойство, которое нужно pair trading'у.
Если проще: A и B могут каждая дрейфовать сама по себе (обе «I(1)» — integrated of order one), но если ты комбинируешь их с правильным hedge ratio β, комбинация остаётся в стабильной полосе.
Для pair trading на log-ratio с равными весами это сводится к
вопросу: стационарен ли log(A/B)? На это и отвечает стандартный
ADF на log-ratio. Это частный случай cointegration, где β
зафиксирован равным 1 в log-пространстве.
Для более общего теста cointegration ты сначала фитишь β по данным, потом гонишь ADF на residuals. Это и есть Engle-Granger 2-step.
Engle-Granger 2-step в коде
Стандартная процедура. Используем statsmodels для OLS-регрессии и
для ADF:
import numpy as np
import statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller
def engle_granger(price_a, price_b):
"""Engle-Granger 2-step cointegration test.
Returns (beta, adf_pvalue). p < 0.05 → cointegrated."""
log_a = np.log(np.asarray(price_a))
log_b = np.log(np.asarray(price_b))
# Step 1: regress log(A) on log(B) to find hedge ratio
X = sm.add_constant(log_b)
model = sm.OLS(log_a, X).fit()
beta = model.params[1]
residuals = model.resid
# Step 2: ADF test on residuals
adf_stat, p_value, *_ = adfuller(residuals, autolag='AIC')
return beta, p_value
Запускаем на синтетике сверху:
beta, p = engle_granger(price_a, price_b)
print(f"Hedge ratio β: {beta:.3f}")
print(f"ADF p-value on residuals: {p:.3f}")
Для GBM-примера p-value обычно вернётся сильно выше 0.05 — ADF-тест корректно не отвергает unit-root гипотезу на residuals. Серии не cointegrated, хотя их price correlation была 0.97. Ровно то, что мы от теста и хотим.
Практический caveat: Engle-Granger асимметричен. Если регрессишь A на B, получаешь один β; если регрессишь B на A — другой (не просто обратный). Residuals и p-value тоже разные. Конвенция — брать более ликвидную ногу как regressor B, но в crypto, где ликвидность по парам похожая, лучше прогнать обе стороны и взять более консервативный p-value.
Для более симметричного и статистически robust теста есть Johansen, который обрабатывает обе стороны одновременно и естественно расширяется на больше двух активов. Мы не используем его в проде — для двухактивного pair trading на ликвидной crypto это overkill, но знать про него стоит для portfolio-level работы.
Hurst on log-ratio vs ADF on residuals
В литературе по pair trading постоянно мелькают три разных теста, и их легко перепутать:
- Hurst exponent на log-ratio. Тестит long-term memory. H < 0.5 = anti-persistent / mean-reverting.
- ADF на log-ratio напрямую. Тестит stationarity при фиксированном β=1 в log-space. Эквивалентно вопросу «равновесные свопы — это правильный размер?».
- ADF на residuals из log(A) ~ log(B). Тестит cointegration с фитированным hedge ratio. Более общий.
Первые два тестят один и тот же объект (log-ratio) разными статистическими фреймворками. Они склонны соглашаться на сильно mean-reverting парах и расходиться на borderline-кейсах. По нашему опыту, Hurst мягче на noisy crypto data — строгий ADF (p < 0.05) на log-ratio отвергает слишком много пар, которые визуально выглядят нормально.
Третий тест делает фундаментально другое: он позволяет β абсорбировать любой устойчивый дрейф в уровневом отношении между A и B, так что residuals просто должны быть стационарны вокруг нуля. Пара, которая фейлит ADF на log-ratio (потому что ratio медленно дрейфует), может пройти Engle-Granger, если дрейф пойман в β.
По нашему опыту прогона на crypto-парах за 540-дневные окна:
- Два log-ratio теста (Hurst и ADF) соглашаются примерно в 80% случаев на том, mean-revert'ит ли пара. Расхождения концентрируются вокруг H ∈ [0.45, 0.55] и ADF p ∈ [0.05, 0.4].
- Engle-Granger флагает на ~15-20% больше пар как «cointegrated», чем log-ratio ADF флагает как «stationary», потому что Engle-Granger абсорбирует drift в β. Tradable ли эти лишние пары на практике — зависит от того, стабильна ли фитированная β во времени. Обычно нет.
Когда тебе реально нужна полноценная cointegration
Для большинства crypto-vs-crypto пар equal-weight log-ratio (β=1 в log-space) — правильный выбор, и более простого теста достаточно. Причины:
- Ликвидность по major crypto-парам грубо сопоставима, поэтому своп равных log-units операционно чист.
- Фитированная β, не равная 1, подразумевает позицию с неравной экспозицией на каждую ногу — что усложняет хеджирование и rebalancing.
- Фитированная β, оценённая на 540 днях данных, не стабильна. Ре-фит каждый месяц — и увидишь, как β дрейфует на 10-30% по большинству пар. Этот дрейф — необсчитанный риск в бэктесте.
Полноценный cointegration framework начинает быть нужен, когда:
- Активы имеют очень разные уровни цены и очень разные volatility profiles (например, high-cap vs small-cap), где equal-weight log-ratio переоверуэйчивает меньшую ногу.
- Ты торгуешь корзину (3+ актива) и нужна Johansen-style оценка множественных cointegrating relationships.
- Делаешь index arbitrage или basis trading, где hedge ratio — известная конструкция (например, дельта из options-модели), а не фитированный статистический артефакт.
Для стандартной связки «два ликвидных crypto-актива, log-ratio, mean-reversion на 540-дневном окне», на которой PairScan сосредоточен, log-ratio с β=1 — более чистый и более честный выбор. Hedge ratio, который ты не можешь стабильно оценить — это hedge ratio, на который не стоит полагаться.
Что использует PairScan и почему
Скринер на pairscan.io не гоняет cointegration-фильтр как таковой. Он гоняет:
- Hurst exponent на log-ratio (H < 0.5)
- ADF на log-ratio (p < 0.7 — мягче чем учебник, потому что скомбинирован с другими фильтрами)
- Range width фильтр (≥ 40% от исторического диапазона)
- Чередующиеся касания границ (≥ 2 на сторону)
- Volume фильтр ($1M+ daily spot на ногу)
Это четыре фильтра на log-ratio плюс liquidity-gate. Engle-Granger ADF на residuals добавил бы шестой тест. Мы экспериментировали — и маржинальный выигрыш оказался маленьким: пары, проходящие наши четыре фильтра, в основном проходят и Engle-Granger, а те, что проходят Engle-Granger, но фейлят наши фильтры — обычно trending пары с фитированной β, абсорбирующей trend (ровно тот failure mode, который мы хотим избегать).
Честный ответ: cointegration как отдельный фильтр строга в теории, но добавляет compute cost и false positives в нашем конкретном домене. Возможно, добавим её как опциональный продвинутый фильтр в будущем релизе — для пользователей, которые хотят накладывать её вручную.
Если хочешь гонять cointegration-тесты на своих данных рядом с
PairScan-фильтрами, open-source утилита на
github.com/pairscan/ratio-mean-reversion
включает Hurst + ADF + walk-forward стек под MIT-лицензией.
Engle-Granger 2-step из этого поста ложится сверху чисто — около 10
строк поверх statsmodels.
Где этот анализ фейлится
Несколько честных ограничений cointegration как инструмента:
β не стабильна во времени. Hedge ratio, оцененный на последних 540 днях — не тот hedge ratio, который продержится следующие 540. Рынки regime-shift'ят, volatility profiles меняются, нарративы переврешивают underlying drivers. «Cointegrated» пара прошлого года может оказаться просто двумя коррелированными трендами в этом.
ADF имеет low power на коротких crypto-историях. Стандартная эконометрическая литература использует 30+ лет месячных данных. У нас 1-3 года дневных данных по большинству crypto, меньше по cross-asset. ADF просто не может различить медленную mean-reverting серию от медленного random walk на таких коротких выборках. P-value около 0.3 на практике статистически неотличимы друг от друга.
Cointegration descriptive, не predictive. Пара, которая была cointegrated в прошлом окне, может не быть в следующем — то же самое, что и failure modes для mean-reversion в целом. Regime changes ломают cointegration так же, как ломают Hurst-фильтры.
Выбор regressor имеет значение. Engle-Granger асимметрия реальна. На crypto, где ни одна нога не очевидно «правильный» regressor, гонять обе стороны и быть консервативным — ответственный ход, но это удваивает риск false-rejection.
Источники
- Engle, R.F. & Granger, C.W.J. (1987). "Co-integration and Error Correction: Representation, Estimation, and Testing". Econometrica, 55(2), 251-276.
- Dickey, D.A. & Fuller, W.A. (1979). "Distribution of the Estimators for Autoregressive Time Series with a Unit Root". Journal of the American Statistical Association, 74(366), 427-431.
- Lo, A.W. & MacKinlay, A.C. (1988). "Stock Market Prices Do Not Follow Random Walks: Evidence from a Simple Specification Test". The Review of Financial Studies, 1(1), 41-66.
- Krauss, C. (2017). "Statistical Arbitrage Pairs Trading Strategies: Review and Outlook". Journal of Economic Surveys, 31(2), 513-545.
- Gatev, E., Goetzmann, W.N., Rouwenhorst, K.G. (2006). "Pairs Trading: Performance of a Relative-Value Arbitrage Rule". The Review of Financial Studies, 19(3), 797-827.
Попробуй
Скринер на pairscan.io гоняет четырёхфильтровый pipeline плюс walk-forward бэктест по 170+ crypto- и cross-asset парам каждые 6 часов. Free показывает топ-3 пары в день. Personal $19/мес даёт полный бэктест с trade markers и Telegram-алерты на zone entries.
Если предпочитаешь реализовать и проверить методологию сам — github.com/pairscan/ratio-mean-reversion содержит весь стек (фильтры, бэктест, no-lookahead тест) под MIT. Engle-Granger из этого поста ложится сверху ~15 строками — дропни и гоняй cointegration-тесты рядом с существующими фильтрами на своих данных.
Смысл этого поста не в том, что cointegration — единственно правильный тест. Смысл в том, что correlation — точно не он, и что разница важнее, чем большинство pair-trading контента признаёт. Если выбираешь пару по одному числу — выбирай stationarity-тест на спреде, а не correlation на уровнях.