👀 PairScan

· 9 мин чтения · #methodology #cointegration #adf #mean-reversion #pair-trading #python

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 на уровнях.