Торговля на отклонениях: почему мы вернулись к тесту Дики-Фуллера (ADF)

от автора

Как бы мы ни пытались отказаться от этого инструмента в поисках более изящных алгоритмических решений, каждый раз мы к нему возвращаемся.

В прошлой статье про Гамма-флип я вскользь касался механики работы с отклонениями (Mean Reversion), но не раскрыл тему до конца. К тому же, в комментариях справедливо заметили, что текст вышел перегруженным терминологией, применимой скорее к американскому опционному рынку.

В этой статье мы углубимся в стохастический анализ и рассмотрим методы определения стационарности временных рядов в реальном времени. Разберем математический аппарат расширенного теста Дики-Фуллера (ADF), причины его интеграции в ядро нашей торговой системы и особенности реализации на Python при работе с большими массивами данных.

Проблема двух режимов

Разработка алгоритмических торговых стратегий неизбежно сталкивается с фундаментальной проблемой архитектуры рынка. Ценовые ряды нелинейны и попеременно находятся в двух базовых состояниях:

  1. Флэт (консолидация): цена колеблется в определенном диапазоне, демонстрируя тенденцию к возврату к среднему значению.

  2. Тренд: направленное движение цены, при котором происходит смещение математического ожидания без существенных откатов.

Если алгоритм, эксплуатирующий возврат к средней (Mean Reversion), работает во время флэта, он показывает стабильный рост PnL. Однако при переходе рынка в фазу направленного тренда такие стратегии начинают накапливать убыток, пытаясь усреднять позицию против движения.

Классический подход к фильтрации трендов с помощью технических осцилляторов (RSI, MACD) имеет критический недостаток — фатальное запаздывание. Они констатируют факт тренда постфактум, опираясь на сглаженные исторические данные.

Нам требуется математически строгое обоснование текущего состояния ценового ряда. Необходимо понимать, обладает ли рынок прямо сейчас статистической «памятью» (возвратом к среднему), или эта структура нарушена. В математической статистике это свойство называется стационарностью.

Математический смысл стационарности и ADF-тест

Временной ряд считается слабо стационарным (ковариационно стационарным), если его математическое ожидание и дисперсия остаются неизменными во времени, а ковариация зависит только от величины лага, но не от самого времени.

Если ценовой ряд актива стационарен, любые отклонения, вызванные микроструктурными шоками или локальным дисбалансом ликвидности, со временем компенсируются. Если ряд нестационарен (например, является процессом случайного блуждания, y_t = y_{t-1} + \epsilon_t), дисперсия растет пропорционально времени, возврат к исходному уровню не гарантирован, и модель Mean Reversion неприменима.

Для выявления стационарности мы применяем Расширенный тест Дики-Фуллера (Augmented Dickey-Fuller, ADF). Суть метода сводится к проверке нулевой гипотезы (H_0) о наличии единичного корня в авторегрессионной модели.

Уравнение для ADF-теста в общем виде выглядит следующим образом:

\Delta y_t = \alpha + \beta t + \gamma y_{t-1} + \sum_{i=1}^p \delta_i \Delta y_{t-i} + \epsilon_t

Где \Delta y_t — разность временного ряда первого порядка, \alpha — константа, \beta t — временной тренд, а p — количество лагов (добавлено для устранения автокорреляции остатков \epsilon_t).

Тестирование проводится для коэффициента \gamma. Нулевая гипотеза предполагает, что \gamma = 0 (единичный корень присутствует, ряд нестационарен). Наша задача — получить достаточно статистических оснований, чтобы отвергнуть H_0.

Результатом расчета является ADF-статистика. Чем меньше (отрицательнее) это значение, тем сильнее доказательства стационарности. Критические значения статистики зависят от выбранного уровня значимости:

  • > -2.58 — нулевая гипотеза не отвергается. Ряд нестационарен, возврат к среднему математически не обоснован.

  • < -2.86 — стационарность с уровнем доверия 95%. Достаточное условие для поиска точек входа на отскок.

  • < -3.43 — стационарность с уровнем доверия 99%. Высокая статистическая значимость флэта.

Прототипирование на Pine Script и перенос логики на Python

На этапе первичного тестирования гипотезы мы не писали логику с нуля, а взяли за основу готовый открытый скрипт ADF-теста на Pine Script в TradingView. Мы доработали его под свои задачи: настроили расчет в скользящем окне (rolling window) и добавили пост-сглаживание (ALMA) для устранения лишнего шума на графике.

Однако для алгоритмической системы, обрабатывающей массивы данных по десяткам синтетических пар на минутных таймфреймах, мощностей TradingView недостаточно. Потребовался перенос логики на бэкенд.

Здесь мы столкнулись с классической проблемой вычислительной сложности. Если использовать стандартную библиотеку statsmodels в Python, применяя ее к DataFrame в скользящем окне (df.rolling().apply()), возникает серьезная просадка производительности.

Расчет ADF требует построения линейной регрессии (Метод наименьших квадратов, OLS) на каждом шаге скользящего окна. На массивах от 100 000 свечей асимптотическая сложность такого подхода делает его непригодным для высоконагруженных систем — latency возрастает неконтролируемо.

Ниже привожу упрощенный фрагмент ядра нашего сканера. Здесь мы вручную решаем матрицу OLS без дорогостоящих операций инвертирования, что радикально ускоряет расчеты:

import numpy as npfrom numba import njit, float64, int32@njit(float64(float64[:]), fastmath=True, cache=True)def _numba_adf_fast(y):    """    Fast ADF stat calc (lag=1, no trend) via unpacked OLS.    Avoids np.linalg.inv for speed in the scanner loop.    """    n = len(y)    dy = y[1:] - y[:-1]    y_lag = y[:-1]        sum_y = np.sum(y_lag)    sum_y2 = np.sum(y_lag**2)    sum_dy = np.sum(dy)    sum_ydy = np.sum(y_lag * dy)        det = (n - 1) * sum_y2 - sum_y**2    if det == 0:        return np.nan            gamma = ((n - 1) * sum_ydy - sum_y * sum_dy) / det    alpha = (sum_dy - gamma * sum_y) / (n - 1)        residuals = dy - (alpha + gamma * y_lag)    variance = np.sum(residuals**2) / (n - 3)        if variance <= 0:        return np.nan            se_gamma = np.sqrt(variance * (n - 1) / det)    return gamma / se_gamma@njit(float64[:](float64[:], int32))def get_rolling_adf(prices, window):    out = np.full(len(prices), np.nan)    for i in range(window, len(prices)):        out[i] = _numba_adf_fast(prices[i - window : i])    return out# Usage in pipeline:# arr = df['close'].to_numpy()# df['adf_stat'] = get_rolling_adf(arr, window=100)

Синтез метрик: ADF как триггер режимов и Z-Score

Сам по себе тест Дики-Фуллера не является торговым индикатором и не дает конкретных точек входа. Его единственная задача — бинарная классификация текущего состояния рынка.

Для поиска самих точек разворота мы используем нормированное отклонение цены от скользящего среднего — Z-Score. Математически это выражается стандартизацией:

 Z = \frac{x_t - \mu}{\sigma}

Где x_t — текущая цена, \mu — скользящее среднее (SMA) за период N, а \sigma — стандартное отклонение за тот же период.

В базовой стратегии Mean Reversion сигнал формируется при достижении Z > 3 (перекупленность, сигнал в шорт) или Z < -3 (перепроданность, сигнал в лонг). Однако на криптовалютном рынке подобные значения Z-Score зачастую являются предвестниками сильного направленного импульса. Вход в позицию исключительно на основе экстремумов Z-Score неизбежно ведет к аккумуляции убытков.

Интеграция метрик решает эту проблему: сигнал по Z-Score валидируется системой только при условии, что ADF-статистика в этот же момент находится ниже критического значения (например, < -2.86). Алгоритм получает подтверждение, что аномальное отклонение произошло в рамках стационарного режима.

Детектирование слома стационарности (Анализ производной)

Наиболее уязвимая точка любой контр-трендовой алгоритмической стратегии — это момент смены режима (Regime Shift). Рынок может демонстрировать идеальную стационарность на протяжении нескольких часов, а затем за несколько минутных баров перейти в фазу жесткого тренда из-за каскадных ликвидаций.

Если алгоритм уже находится в позиции, ожидая конвергенции к среднему, он должен обладать механизмом мгновенной идентификации слома рыночной структуры, чтобы ликвидировать сделку с минимальным убытком.

Для решения этой задачи мы анализируем не только абсолютное значение ADF, но и динамику его изменения — первую разность (дельту) \Delta ADF = ADF_t - ADF_{t-1}.

Пример логики экстренного закрытия:

  1. Система открывает короткую позицию при Z > 3.0 и ADF = -3.2. Условие стационарности выполнено.

  2. На следующем баре ценовое движение продолжается против позиции. Z-Score возрастает до 3.5.

  3. Значение ADF резко повышается с -3.2 до -1.8.

Этот скачок \Delta ADF — математический маркер разрушения стационарности. Ряд перестал быть ковариационно стационарным, и нулевая гипотеза о наличии единичного корня больше не может быть отвергнута. В этот момент алгоритм принудительно закрывает позицию по рынку (Market Close), игнорируя первоначальный уровень тейк-профита и минимизируя хвостовые риски.

Практическая обкатка

Здесь стоит сделать важное отступление. Поиск стационарности на классических направленных активах (например, одиночный график BTC) — задача с низким математическим ожиданием. Криптовалютные активы по своей природе склонны к трендовости.

Поэтому основное применение связки ADF + Z-Score в нашей архитектуре направлено на синтетические инструменты — статистический арбитраж и парный трейдинг. Мой коллега пришел к этому после того, как обнаружил специфический паттерн в поведении синтетических спредов на младших таймфреймах. Уникальность этого паттерна в том, что он позволяет находить точки входа без ресурсоемкого перерасчета классической коинтеграции и корреляции для каждой пары «на лету», что колоссально экономит ресурсы сервера.

Сейчас мы активно дорабатываем эту логику. Что нас особенно радует — к процессу исследований подключился один из учеников нашего тестового потока (на базе заложенного нами фундамента мы теперь вместе улучшаем систему). Чтобы ускорить проверку подобных торговых гипотез и дать возможность ребятам визуально «набить руку» на паттернах, мы начали строить собственную платформу с удобным ручным бэктестером.

К слову, так как инфраструктура и софт постепенно обретают законченный вид, мы планируем расширить нашу фокус-группу. Если вам близок количественный анализ рынка — будем рады видеть вас в команде (ссылки для связи традиционно оставлю в конце).

В видеоформате, который мы записывали с рабочих терминалов в процессе дебаггинга системы, этот процесс выглядит весьма наглядно:

на сигналы не обращайте внимание, они отвечают за другое

на сигналы не обращайте внимание, они отвечают за другое

С точки зрения архитектуры бэкенда, процесс сканирования выглядит следующим образом:

1. Демон-процесс в асинхронном режиме подтягивает минутные свечи по сотням тикеров.

2. Происходит формирование синтетических пар на лету (например, GIGAUSDT/CROSSUSDT).

3. Для оптимизации вычислительных ресурсов мы не считаем ADF для каждого спреда на каждом тике.

Первичным триггером выступает именно “глобальный” Z-Score.

4. Как только спред отклоняется на заданную величину, функция передает массив данных в модуль расчета ADF для валидации режима.

Иллюзии бэктестов, издержки

Любой разработчик знает: на исторических данных стратегии возврата к среднему часто демонстрируют идеальную кривую доходности. Спреды послушно сужаются, графики PnL стремятся вверх. Однако при переносе алгоритма в продакшен эта математическая идиллия разбивается о микроструктуру рынка.

Торговля кросс-курсами и спредами требует одновременного исполнения ордеров по обеим ногам синтетического инструмента. В реальном мире мы сталкиваемся с тремя барьерами: комиссии биржи, проскальзывание в разреженных стаканах альткоинов и сетевые задержки.

Преодоление этих барьеров и защита от хвостовых рисков заняли у нас больше всего времени. Мы долго не решались масштабировать систему и запускать внешние пулы ликвидности (что-то вроде алго-фонда) из-за отсутствия надежного страховочного обеспечения. Нам нужен был буфер на случай, если рынок поведет себя непредсказуемо.

Сейчас этот гарантийный фонд сформирован, и мы запустили в тестовом формате нашего инвест-бота, который торгует собранные аномалии в полностью автоматическом режиме. Фильтр через критические уровни ADF (например, ADF < -3.43) позволяет алгоритму отбирать только те ситуации, где «пружина» сжата настолько сильно, что потенциал возврата с запасом перекрывает все транзакционные издержки.

Заключение

Тест Дики-Фуллера — это не самостоятельный торговый алгоритм и тем более не «Святой Грааль». Это строгий математический предохранитель.

В условиях современного рынка, где алгоритмы хеджирования способны создавать жесткие направленные тренды из пустоты, попытки ловить развороты «на глаз» или по классическим осцилляторам неизбежно ведут к деградации депозита. Внедрение статистических тестов на ковариационную стационарность позволяет автоматизировать процесс определения рыночных режимов и защитить систему в моменты структурных сдвигов.

Будем рады обсудить в комментариях ваши подходы. Кто-нибудь применяет показатель Херста (Hurst exponent) или тест KPSS в связке с ADF для анализа стационарности? Делитесь опытом проектирования своих систем.

P.S. Если вам интересен алгоритмический трейдинг, разработка торговых систем и анализ микроструктуры рынка — присоединяйтесь к нашему каналу [Ссылка].
Beto инвест-пул [Ссылка]

ссылка на оригинал статьи https://habr.com/ru/articles/1043810/