Поиск по материалам

Стратегия mean reversion: возврат к среднему — как работает и где торгуется

·

Mean reversion — это идея, что цена колеблется вокруг своего среднего, и большие отклонения временные. Стратегии возврата к среднему — основа парного трейдинга, статистического арбитража, торговли по Боллинджеру и Z-score. Разбираем, почему это работает, на чём проверить (тест Дики-Фуллера на стационарность), как реализовать на Python и где главная ловушка — момент, когда среднее само начинает двигаться.

Что такое mean reversion

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

Противоположная идея — momentum / trend-following: цена движется в одну сторону, тренд продолжается. Эти два подхода почти зеркальны и работают на разных типах временны́х рядов. Главная задача алготрейдера — определить, к какому типу относится конкретный инструмент в конкретный период.

В чистом виде mean reversion работает на разностях, а не на ценах: пары активов, спреды между фьючерсом и спотом, кросс-биржевые корзины. На обычной цене акции долгосрочный mean reversion не работает — акция может расти годами и никогда не вернуться к средней. На разности же двух связанных акций (Сбер vs ВТБ) такая возвращаемость есть.

Почему работает: статистический фундамент

Mean reversion опирается на свойство стационарности временного ряда. Стационарный ряд — это ряд, у которого среднее, дисперсия и автокорреляция не меняются во времени. На таком ряду имеет смысл говорить о «среднем», к которому идёт возврат.

Формальная проверка — тест Дики-Фуллера (Augmented Dickey-Fuller, ADF). Гипотеза:

  • H₀: ряд не стационарный (есть единичный корень).
  • H₁: ряд стационарный.

Если p-value < 0.05 — отвергаем H₀, считаем ряд стационарным и применимым для mean reversion. Если p-value > 0.05 — стратегия применима с осторожностью или не применима вовсе.

from statsmodels.tsa.stattools import adfuller

def is_stationary(series, alpha=0.05):
    result = adfuller(series.dropna())
    return result[1] < alpha, result[1]

# Пример: проверка спреда двух акций
spread = sber_close - 5 * vtb_close
ok, pvalue = is_stationary(spread)
print(f"стационарность: {ok}, p-value={pvalue:.4f}")

Если спред стационарен — пара подходит для парного трейдинга (статистического арбитража). Если нет — придётся либо переподобрать коэффициент 5, либо искать другую пару.

Базовые реализации

Bollinger Bands reversion

Самая простая mean reversion-стратегия. Bollinger Bands — это скользящее среднее ± k стандартных отклонений (k = 2 по умолчанию).

Правила:

  • Цена закрылась ниже нижней полосы → покупка.
  • Цена закрылась выше верхней полосы → продажа (для лонг-онли — закрытие позиции).
  • Возврат к скользящей средней → выход.
import pandas as pd

def bollinger_signals(df, n=20, k=2.0):
    df = df.copy()
    df["mean"] = df["close"].rolling(n).mean()
    df["std"]  = df["close"].rolling(n).std()
    df["upper"] = df["mean"] + k * df["std"]
    df["lower"] = df["mean"] - k * df["std"]

    df["signal"] = 0
    df.loc[df["close"] < df["lower"], "signal"] = 1   # buy
    df.loc[df["close"] > df["upper"], "signal"] = -1  # sell
    return df

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

Z-score reversion

Z-score — нормализация отклонения от среднего:

z = (x − rolling_mean) / rolling_std

Z = 2 означает «два стандартных отклонения вверх» — статистически редкое событие. Сигналы:

  • z > 2 → шорт (или закрытие лонга).
  • z < −2 → лонг.
  • |z| < 0.5 → выход (близко к среднему).
def zscore_signals(series, n=30, entry=2.0, exit=0.5):
    mean = series.rolling(n).mean()
    std  = series.rolling(n).std()
    z = (series - mean) / std

    signal = pd.Series(0, index=series.index)
    signal[z >  entry] = -1
    signal[z < -entry] =  1
    signal[(z > -exit) & (z < exit)] = 0  # close
    return signal, z

Преимущество перед Bollinger Bands — гибкие пороги: разные entry и exit для входа и выхода.

RSI reversion

RSI(2) на дневках — классическая mean-reversion стратегия Ларри Коннорса:

  • RSI(2) < 10 → покупка.
  • RSI(2) > 90 → продажа.

Короткий период RSI делает индикатор чувствительным к экстремальным движениям. Стратегия работает на индексных ETF (SPY, QQQ) и крупных blue-chip акциях; на сырьё и крипту — нет, там часто настоящие тренды.

Парный трейдинг (статистический арбитраж)

Самая надёжная разновидность mean reversion. Берём две сильно коррелирующих бумаги (Сбер и ВТБ, Лукойл и Роснефть, Северсталь и НЛМК), считаем линейную регрессию одной на другую:

ВТБ_t = α + β × Сбер_t + ε_t

Спред ε_t = ВТБ_t − α − β × Сбер_t должен быть стационарным (ADF p-value < 0.05). Если да — торгуем по Z-score этого спреда:

  • z(спред) > 2 → шорт ВТБ + лонг (β × Сбер) (спред слишком большой, ждём сужения).
  • z(спред) < −2 → лонг ВТБ + шорт (β × Сбер) (спред слишком маленький, ждём расширения).
from sklearn.linear_model import LinearRegression
import numpy as np

# Подбор коэффициента
X = sber_prices.values.reshape(-1, 1)
y = vtb_prices.values
reg = LinearRegression().fit(X, y)
alpha, beta = reg.intercept_, reg.coef_[0]

spread = vtb_prices - alpha - beta * sber_prices
ok, p = is_stationary(spread)
if ok:
    z = (spread - spread.rolling(60).mean()) / spread.rolling(60).std()
    # торгуем z по уже знакомым правилам

Главное преимущество — рыночно-нейтральная позиция: лонг + шорт практически равных размеров, движение всего рынка не влияет на P&L. Стратегия зарабатывает только на отклонениях пары.

Размер позиции и риск

В mean reversion стратегиях характер риска отличается от trend-following:

  • Серии маленьких прибылей + редкие большие убытки, когда «средне-возвращаемость» внезапно ломается. Это противоположная trend-following кривая P&L.
  • Просадки короткие, но глубокие в момент структурного сдвига (изменение фундаментального соотношения активов).

Размер позиции:

risk_money = depo × 1%       # умеренный риск из-за частоты сделок
size = risk_money / |entry − stop|

Стоп-лосс ставится на сильное отклонение от точки входа:

  • Для Bollinger — пробой 3-σ полосы.
  • Для Z-score — выход за |z| > 4.
  • Для парного трейдинга — рост спреда на 1.5-кратное стандартное отклонение от точки входа.

Тейк — возврат к среднему: |z| < 0.5 или пересечение скользящей средней.

Бэктест: как тестировать без overfit

Главная ошибка в mean reversion-бэктесте — подгонка параметров на исторических данных. С 5 параметрами (период, k, entry-z, exit-z, stop-z) и 1000 свечей вы найдёте настройку, которая показывает 200% годовых на бумаге. На реальных данных в следующем месяце она сольёт.

Защитные практики:

  1. Walk-forward анализ: окно обучения 1 год → окно теста 3 месяца → сдвиг → повтор. На каждом шаге переоптимизируем параметры на обучающем окне и проверяем результат на тестовом.
  2. Out-of-sample датасет: 70% истории — для подбора параметров, 30% — для финальной оценки. Параметры из первой части не трогаем при просмотре результатов на второй.
  3. Ограниченный набор параметров: чем меньше параметров, тем меньше overfit. Для Z-score достаточно (period, entry, exit) = 3 параметра. Не вводите 5-й, если 3 уже дают стабильный результат.
  4. Множественная проверка: тестируем не на одной бумаге, а на 10–20 однотипных. Если стратегия работает на 15 из 20 — это паттерн. Если на 4 из 20 — overfit на лучших.
# Walk-forward скелет
def walk_forward(prices, train_size=252, test_size=63):
    results = []
    for start in range(0, len(prices) - train_size - test_size, test_size):
        train = prices.iloc[start:start + train_size]
        test = prices.iloc[start + train_size:start + train_size + test_size]
        params = optimize(train)
        pnl = backtest(test, params)
        results.append({"start": test.index[0], "pnl": pnl, "params": params})
    return pd.DataFrame(results)

Где работает mean reversion на MOEX

  • Парный трейдинг внутри сектора: Сбер—ВТБ, Лукойл—Роснефть, Северсталь—НЛМК—ММК, Магнит—X5. Высокая корреляция фундаменталий, временные расхождения цен.
  • Спред между акцией и фьючерсом (например, SBER vs SiM для USD/RUB не подходит, но SBRF фьючерс на Сбер и обыкновенные акции — подходит).
  • Облигации одного эмитента с разной дюрацией — рынок неэффективен на длинных сериях, спред между 3-летней и 5-летней ОФЗ часто mean-revertable.
  • Внутридневные пары на ликвидных голубых фишках: экстремальные движения в первые 30 минут сессии часто откатываются к середине дня.

Где не работает:

  • Узколиквидные второэшелонные акции — спреды большие, проскальзывание съедает edge.
  • Криптовалюты в тренде — на бычьем/медвежьем 6-месячном движении mean reversion на цене теряет смысл.
  • Бумаги с регуляторными рисками (санкции, расследования) — структурные сдвиги ломают стационарность.

Подводные камни

  • «Не средне-возвращаемая» серия. Цена ушла в новую парадигму (новые фундаменталии) и не возвращается. Bot открывает лонг на падении, цена падает дальше, бот добавляет позицию, цена ещё ниже. Без жёсткого стопа — обнуление депозита.
  • Сдвиг среднего. Среднее само по себе движется (по Brownian motion), и mean reversion работает только относительно текущего среднего. Это правильно учитывается через скользящее среднее, не статическое.
  • Корреляция в стрессе. В кризисах все корреляции «диверсификации» обнуляются — пары перестают быть парами, спред расходится надолго. Парный трейдинг в марте 2020 показал крупнейшие просадки за десятилетие.
  • Транзакционные издержки. Mean reversion = много мелких сделок. Если комиссия и spread больше edge — стратегия отрицательная даже при формально верных сигналах. Считайте edge за вычетом комиссий.
  • Длительные просадки. Mean reversion может 6–12 месяцев не давать прибыли в трендовых режимах (2020 март–декабрь, 2022 на крипте). Психологически тяжело держать стратегию в это время — большинство трейдеров выключает бот именно перед возвратом edge.

Когда mean reversion не работает

Признаки трендового режима, в котором стратегия даёт убытки:

  • Высокий ADX (> 25) — индикатор силы тренда.
  • Цена держится далеко от 50/200-дневных средних на одном направлении.
  • Z-score не возвращается к нулю в течение долгого времени, а накапливает значения в одну сторону.

В таких режимах разумно отключать mean reversion-боты или переключаться на trend-following. Гибридные системы (regime detection + переключение стратегий) — уровень развитого алготрейдинга, требующий 6+ месяцев исследований.

Что запомнить

  • Mean reversion = торговля на возврате цены к среднему. Работает на стационарных рядах (тест ADF, p < 0.05).
  • На обычной цене акции долгосрочный mean reversion не работает — нужен спред, разность или нормализация.
  • Базовые реализации: Bollinger Bands, Z-score, RSI(2), парный трейдинг (статистический арбитраж).
  • Парный трейдинг — самый надёжный mean-reversion подход благодаря рыночной нейтральности.
  • Защита от overfit: walk-forward, out-of-sample, минимальный набор параметров, проверка на 15+ инструментах.
  • Не работает на узколиквидных бумагах и в трендовых режимах — нужен фильтр режима (ADX, наклон скользящих).
  • Серии маленьких прибылей + редкие большие убытки — характерная P&L кривая. Жёсткий стоп обязателен.
mean reversion стратегии статистический арбитраж Z-score Python