← К списку статей
March 21, 2026
5 мин. чтения

Скрытые марковские модели в трейдинге: как адаптировать стратегию к режиму рынка

Скрытые марковские модели в трейдинге: как адаптировать стратегию к режиму рынка
#hmm
#режимы-рынка
#машинное-обучение
#алготрейдинг
#адаптивные-стратегии
#волатильность

У каждого алготрейдера есть момент экзистенциального кризиса. Вы потратили три месяца на стратегию. Бэктест показывает Sharpe 2.4. Equity curve — произведение искусства. Вы запускаете бота. Первые две недели — эйфория, стратегия генерирует альфу. А потом рынок «переключается» — и ваш моментум-бот начинает методично сливать капитал в боковике, покупая каждый локальный хай и продавая каждый лой.

Проблема не в стратегии. Проблема в том, что рынок — это не одна система, а несколько, и они переключаются между собой без предупреждения. Моментум-стратегия, идеальная для тренда, убивает депозит в рейндже. Гридовая стратегия, печатающая деньги в боковике, взрывается на направленном движении. Mean-reversion, стабильная в спокойном рынке, получает маржин-колл на чёрном лебеде.

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

Рынки нестационарны, и это не баг, а фича

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

Посмотрите на дневные доходности BTC за последние 5 лет. Средняя дневная доходность в бычьем ралли 2024 года — около +0.3%, стандартное отклонение ~2.5%. В медвежьем рынке 2022 года — средняя -0.15%, стандартное отклонение ~4%. В боковике лета 2023 — средняя ~0%, стандартное отклонение ~1.5%. Это три принципиально разных статистических режима с разными распределениями.

Формально: пусть rtr_t — доходность в момент tt. В стационарном мире rtN(μ,σ2)r_t \sim \mathcal{N}(\mu, \sigma^2) с постоянными параметрами. В реальности параметры сами являются случайными процессами: rtN(μSt,σSt2)r_t \sim \mathcal{N}(\mu_{S_t}, \sigma^2_{S_t}), где StS_t — скрытое состояние (режим рынка), переключающееся между конечным числом значений.

Эту идею в 1989 году формализовал Джеймс Гамильтон в своей фундаментальной работе «A New Approach to the Economic Analysis of Nonstationary Time Series and the Business Cycle». Он показал, что бизнес-циклы можно моделировать как переключение между двумя скрытыми состояниями — рецессией и экспансией — с помощью марковского механизма. С тех пор модель Гамильтона стала одним из самых цитируемых инструментов в эконометрике.

Три режима рынка Три режима рынка — бычий (зелёный), медвежий (красный) и боковик (жёлтый) — визуально очевидны постфактум, но определить переключение в реальном времени значительно сложнее.

HMM: интуиция через аналогию

Прежде чем лезть в формулы, давайте разберёмся на интуитивном уровне.

Марковские цепи: без памяти

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

Рыночные режимы ведут себя похоже. Если сегодня рынок в бычьем режиме, вероятность остаться в нём завтра — высокая (скажем, 95%). Вероятность перейти в медвежий — низкая (3%). В боковик — ещё ниже (2%). Это и есть матрица переходных вероятностей.

         Bull    Bear    Sideways
Bull    [0.95    0.03    0.02  ]
Bear    [0.04    0.93    0.03  ]
Sideways[0.05    0.05    0.90  ]

Заметьте: диагональные элементы высоки — режимы «липкие». Рынок не скачет из бычьего в медвежий каждый день. Он проводит в одном режиме недели и месяцы, прежде чем переключиться. Ожидаемая длительность режима di=11aiid_i = \frac{1}{1 - a_{ii}}. Для бычьего режима с a11=0.95a_{11} = 0.95 это 20 дней. Для медвежьего с a22=0.93a_{22} = 0.93 — примерно 14 дней.

Скрытые состояния: мы видим только тень

Ключевое слово — «скрытые». Мы не наблюдаем режим рынка напрямую. Никто не вывешивает табличку «Внимание, переходим в медвежий режим». Мы видим только наблюдения (observations) — доходности, волатильность, объёмы. А режим — это латентная переменная, которую нужно вывести из наблюдений.

Это как быть в комнате без окон и пытаться определить погоду по тому, как одеты люди, входящие с улицы. Зонтик? Наверное, дождь. Шорты и солнечные очки? Солнечно. Но один человек в шортах не означает, что точно солнечно — может, он просто оптимист. Нужно накопить наблюдения и вероятностно оценить скрытое состояние.

В HMM каждый скрытый режим «излучает» (emit) наблюдения из своего распределения:

  • Бычий режим → доходности из N(μbull,σbull2)\mathcal{N}(\mu_{bull}, \sigma^2_{bull}), где μbull>0\mu_{bull} > 0, σbull\sigma_{bull} умеренное
  • Медвежий режим → доходности из N(μbear,σbear2)\mathcal{N}(\mu_{bear}, \sigma^2_{bear}), где μbear<0\mu_{bear} < 0, σbear\sigma_{bear} высокое
  • Боковик → доходности из N(μsideways,σsideways2)\mathcal{N}(\mu_{sideways}, \sigma^2_{sideways}), где μsideways0\mu_{sideways} \approx 0, σsideways\sigma_{sideways} низкое

Заметьте характерный паттерн: медвежий режим обычно имеет не просто отрицательное среднее, но и повышенную волатильность. Рынки падают лифтом, а поднимаются по лестнице — и HMM это автоматически улавливает.

HMM архитектура Архитектура Hidden Markov Model: скрытые состояния (режимы) переключаются по марковской цепи, каждое состояние генерирует наблюдаемые доходности из своего гауссова распределения.

Три алгоритма HMM: Forward, Viterbi, Baum-Welch

Любая работа с HMM сводится к трём фундаментальным задачам, и для каждой есть свой алгоритм.

Задача 1: какова вероятность этих наблюдений? (Forward Algorithm)

Вопрос: Дана последовательность доходностей. Какова вероятность наблюдать именно такую последовательность при данных параметрах модели?

Зачем: Сравнение моделей (AIC/BIC), проверка адекватности.

Как работает: Прямой алгоритм (Forward Algorithm) — это динамическое программирование. На каждом шаге tt мы считаем «прямую переменную» αt(i)\alpha_t(i) — вероятность наблюдать последовательность o1,o2,,oto_1, o_2, \ldots, o_t и оказаться в состоянии ii в момент tt.

Рекурсия: αt(j)=[iαt1(i)aij]bj(ot)\alpha_t(j) = \left[\sum_i \alpha_{t-1}(i) \cdot a_{ij}\right] \cdot b_j(o_t)

Где aija_{ij} — вероятность перехода из состояния ii в jj, а bj(ot)b_j(o_t) — вероятность наблюдения oto_t в состоянии jj. На словах: суммируем по всем путям, как мы могли прийти в состояние jj, и умножаем на вероятность наблюдения.

Сложность: O(N2T)O(N^2 T) вместо наивных O(NT)O(N^T), где NN — число состояний, TT — длина последовательности. Для 3 режимов и 1000 наблюдений это 9000 операций вместо 310003^{1000}. Разница, скажем так, существенная.

Задача 2: какая последовательность режимов наиболее вероятна? (Viterbi Algorithm)

Вопрос: Дана последовательность доходностей. Какая последовательность скрытых состояний (режимов) её наиболее вероятно сгенерировала?

Зачем: Именно это нам нужно для трейдинга — определить режим в каждый момент времени.

Как работает: Алгоритм Витерби — это тот же Forward, но вместо суммирования по всем путям берётся максимум. Мы ищем не вероятность всех возможных путей, а самый вероятный путь.

δt(j)=maxi[δt1(i)aij]bj(ot)\delta_t(j) = \max_i \left[\delta_{t-1}(i) \cdot a_{ij}\right] \cdot b_j(o_t)

Плюс обратный проход (backtracking) для восстановления самой последовательности состояний. Результат — декодированная последовательность режимов: «бычий-бычий-бычий-медвежий-медвежий-боковик-...».

На практике для трейдинга чаще используют не Viterbi (глобальный оптимум), а фильтрацию — апостериорные вероятности состояний в каждый момент: P(St=io1,,ot)P(S_t = i \mid o_1, \ldots, o_t). Это позволяет работать онлайн, не дожидаясь всей последовательности, и получать «мягкие» оценки вроде «70% бычий, 25% боковик, 5% медвежий».

Задача 3: как обучить модель? (Baum-Welch Algorithm)

Вопрос: Даны только наблюдения. Какие параметры модели (AA, BB, π\pi) максимизируют правдоподобие данных?

Зачем: Обучение модели на исторических данных.

Как работает: Алгоритм Баума-Уэлча — это частный случай EM-алгоритма (Expectation-Maximization):

  1. E-шаг: Используя текущие параметры, вычисляем ожидаемые скрытые состояния (через Forward-Backward)
  2. M-шаг: Обновляем параметры, максимизируя правдоподобие при этих ожидаемых состояниях
  3. Повторяем до сходимости

Важный нюанс: EM гарантирует сходимость только к локальному максимуму. Разные начальные условия могут дать разные результаты. На практике модель обучают несколько раз с разной инициализацией и выбирают лучший результат по логарифму правдоподобия. В hmmlearn это делается автоматически через параметр n_init.

Режимы крипторынка: что мы ищем

Для криптовалют классическое разбиение на три режима работает особенно хорошо из-за ярко выраженных фаз рынка.

Режим 1: Bull (бычий)

  • Средняя доходность: +0.15% ... +0.5% в день
  • Волатильность (std): 2-3% в день
  • Характер: устойчивый рост с умеренными откатами
  • Длительность: 2-6 месяцев непрерывно
  • Объёмы: растущие, особенно на спотовых рынках
  • On-chain: MVRV > 1.5, растущие активные адреса

Режим 2: Bear (медвежий)

  • Средняя доходность: -0.1% ... -0.4% в день
  • Волатильность (std): 3-6% в день
  • Характер: резкие обвалы, ликвидационные каскады, dead cat bounces
  • Длительность: 1-4 месяца (обычно короче бычьего)
  • Объёмы: всплески на панических продажах, потом затухание
  • On-chain: MVRV < 1, растущий exchange inflow

Режим 3: Sideways (боковик / accumulation)

  • Средняя доходность: ~0% в день
  • Волатильность (std): 1-2% в день
  • Характер: движение в диапазоне, ложные пробои
  • Длительность: 1-3 месяца
  • Объёмы: низкие, затухающие
  • On-chain: стабильные метрики, снижение активности

Почему именно три режима, а не два или пять? Два — слишком грубо, теряется информация о боковике (а для market-making ботов это самый прибыльный режим). Пять или больше — модель становится переобученной, переходные вероятности нестабильны, интерпретация затруднена. Три — оптимальный баланс, подтверждённый как информационными критериями (AIC/BIC), так и экономической интуицией.

Впрочем, выбор числа состояний — это гиперпараметр, и его стоит тестировать. Guidolin & Timmermann (2007) в своей работе «Asset Allocation under Multivariate Regime Switching» нашли четыре режима для смешанного портфеля акций и облигаций: crash, slow growth, bull и recovery.

Feature Engineering: что подавать на вход модели

Самый простой вариант — подать на вход только дневные доходности. Это работает, но можно лучше. Вот набор признаков, который хорошо зарекомендовал себя на практике:

Ценовые признаки

  • Дневная логарифмическая доходность: rt=ln(Pt/Pt1)r_t = \ln(P_t / P_{t-1})
  • Скользящая волатильность: σt=std(rtw,,rt)\sigma_t = \text{std}(r_{t-w}, \ldots, r_t) за окно ww (например, 20 дней)
  • Скользящее среднее доходности: rˉt=mean(rtw,,rt)\bar{r}_t = \text{mean}(r_{t-w}, \ldots, r_t)

Объёмные признаки

  • Нормализованный объём: Vtnorm=Vt/SMA(V,20)V_t^{norm} = V_t / \text{SMA}(V, 20)
  • Volume-price correlation: корреляция между объёмом и абсолютной доходностью за скользящее окно

On-chain признаки (для криптовалют)

  • MVRV Ratio: отношение рыночной капитализации к реализованной. MVRV > 2 — рынок перегрет, < 1 — недооценён
  • NVT Ratio: сетевая стоимость к объёму транзакций. Аналог P/E для блокчейна
  • Exchange Net Flow: нетто-поток на биржи. Положительный — давление продаж, отрицательный — накопление
  • Active Addresses: число активных адресов (рост = интерес, падение = апатия)
import numpy as np
import pandas as pd

def prepare_features(df: pd.DataFrame, window: int = 20) -> pd.DataFrame:
    """
    Подготовка признаков для HMM.
    df должен содержать колонки: close, volume
    """
    features = pd.DataFrame(index=df.index)

    features['log_return'] = np.log(df['close'] / df['close'].shift(1))

    features['rolling_vol'] = features['log_return'].rolling(window).std()

    features['norm_volume'] = df['volume'] / df['volume'].rolling(window).mean()

    features['rolling_mean_return'] = features['log_return'].rolling(window).mean()

    features['abs_return'] = features['log_return'].abs()

    return features.dropna()

Важно: все признаки должны быть стационарны (или хотя бы приблизительно). Логарифмические доходности — стационарны. Цена — нет. Объём лучше нормализовать. Волатильность можно оставить как есть — она тоже квазистационарна.

Ещё один нюанс: мультивариантная HMM (когда на вход подаётся вектор признаков) работает лучше одномерной, но требует больше данных для обучения. Для крипты с историей в 5+ лет это обычно не проблема. Для свежего альткоина с историей в 3 месяца — лучше ограничиться одной-двумя фичами.

Пошаговая реализация на Python с hmmlearn

Переходим к коду. Библиотека hmmlearn — стандарт де-факто для HMM в Python. Простой API, совместимость со scikit-learn, работает из коробки.

Шаг 1: Загрузка данных

import ccxt
import pandas as pd
import numpy as np
from datetime import datetime

def fetch_ohlcv(symbol='BTC/USDT', timeframe='1d', since='2020-01-01'):
    """Загрузка данных через CCXT."""
    exchange = ccxt.binance()
    since_ts = exchange.parse8601(f'{since}T00:00:00Z')
    all_ohlcv = []

    while True:
        ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=since_ts, limit=1000)
        if not ohlcv:
            break
        all_ohlcv.extend(ohlcv)
        since_ts = ohlcv[-1][0] + 1
        if len(ohlcv) < 1000:
            break

    df = pd.DataFrame(all_ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
    df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
    df.set_index('timestamp', inplace=True)
    return df

df = fetch_ohlcv('BTC/USDT', '1d', '2020-01-01')
print(f"Загружено {len(df)} дневных свечей")
print(f"Период: {df.index[0]}{df.index[-1]}")

Шаг 2: Подготовка признаков и обучение HMM

from hmmlearn.hmm import GaussianHMM
from sklearn.preprocessing import StandardScaler

features = prepare_features(df, window=20)

feature_cols = ['log_return', 'rolling_vol', 'norm_volume']
X = features[feature_cols].values

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

model = GaussianHMM(
    n_components=3,          # 3 режима
    covariance_type='full',  # полная ковариационная матрица
    n_iter=200,              # макс. итераций EM
    random_state=42,
    tol=1e-4,                # порог сходимости
    verbose=False
)

model.fit(X_scaled)

print(f"Модель сошлась: {model.monitor_.converged}")
print(f"Итераций: {model.monitor_.iter}")
print(f"Log-likelihood: {model.score(X_scaled):.2f}")

Шаг 3: Декодирование режимов

hidden_states = model.predict(X_scaled)

state_probs = model.predict_proba(X_scaled)

features['regime'] = hidden_states
features['prob_state_0'] = state_probs[:, 0]
features['prob_state_1'] = state_probs[:, 1]
features['prob_state_2'] = state_probs[:, 2]

print(f"\nРаспределение по режимам:")
print(features['regime'].value_counts().sort_index())

Шаг 4: Интерпретация режимов

Вот здесь начинается самое интересное — и самое коварное. HMM не знает, что режим 0 — это «бычий». Он просто находит три кластера в пространстве наблюдений. Нумерация произвольна и может меняться от запуска к запуску.

Нужно посмотреть на статистику каждого режима и присвоить метки вручную:

def interpret_regimes(features, model, scaler, feature_cols):
    """
    Интерпретация режимов: присвоение меток bull/bear/sideways
    на основе средних доходностей и волатильности.
    """
    means_scaled = model.means_
    means_original = scaler.inverse_transform(means_scaled)

    regime_stats = {}
    for i in range(model.n_components):
        mask = features['regime'] == i
        regime_stats[i] = {
            'count': mask.sum(),
            'pct': mask.mean() * 100,
            'mean_return': features.loc[mask, 'log_return'].mean() * 100,
            'std_return': features.loc[mask, 'log_return'].std() * 100,
            'mean_vol': features.loc[mask, 'rolling_vol'].mean() * 100,
            'sharpe_daily': (features.loc[mask, 'log_return'].mean()
                           / features.loc[mask, 'log_return'].std())
        }
        print(f"\nРежим {i}: {regime_stats[i]['count']} дней "
              f"({regime_stats[i]['pct']:.1f}%)")
        print(f"  Средняя доходность: {regime_stats[i]['mean_return']:.3f}%/день")
        print(f"  Волатильность:      {regime_stats[i]['std_return']:.3f}%/день")
        print(f"  Sharpe (дневной):   {regime_stats[i]['sharpe_daily']:.3f}")

    sorted_by_return = sorted(regime_stats.keys(),
                               key=lambda x: regime_stats[x]['mean_return'])

    label_map = {
        sorted_by_return[0]: 'bear',      # наименьшая доходность
        sorted_by_return[2]: 'bull',       # наибольшая доходность
        sorted_by_return[1]: 'sideways',   # средняя
    }

    features['regime_label'] = features['regime'].map(label_map)
    return features, label_map

features, label_map = interpret_regimes(features, model, scaler, feature_cols)
print(f"\nМаппинг режимов: {label_map}")

Типичный вывод для BTC выглядит примерно так:

Режим 0: 412 дней (23.8%)
  Средняя доходность: -0.182%/день
  Волатильность:      4.127%/день
  Sharpe (дневной):   -0.044

Режим 1: 847 дней (48.9%)
  Средняя доходность: 0.021%/день
  Волатильность:      1.634%/день
  Sharpe (дневной):   0.013

Режим 2: 473 дней (27.3%)
  Средняя доходность: 0.312%/день
  Волатильность:      2.851%/день
  Sharpe (дневной):   0.109

Маппинг режимов: {0: 'bear', 1: 'sideways', 2: 'bull'}

Обратите внимание: медвежий режим не только имеет отрицательную доходность, но и максимальную волатильность (4.1% против 1.6% в боковике). Это классическое эмпирическое наблюдение, известное как «leverage effect» — падающие рынки волатильнее растущих.

Матрица переходов и длительности режимов

Матрица переходных вероятностей — один из самых информативных артефактов HMM:

def analyze_transitions(model, label_map):
    """Анализ матрицы переходов и ожидаемых длительностей."""
    trans_mat = model.transmat_

    inv_map = {v: k for k, v in label_map.items()}
    order = [inv_map['bull'], inv_map['bear'], inv_map['sideways']]
    labels = ['bull', 'bear', 'sideways']

    print("Матрица переходных вероятностей:")
    print(f"{'':>10}", end='')
    for l in labels:
        print(f"{l:>10}", end='')
    print()

    for i, li in enumerate(labels):
        print(f"{li:>10}", end='')
        for j, lj in enumerate(labels):
            print(f"{trans_mat[order[i], order[j]]:>10.3f}", end='')
        print()

    print("\nОжидаемая длительность режимов (дни):")
    for i, l in enumerate(labels):
        duration = 1 / (1 - trans_mat[order[i], order[i]])
        print(f"  {l}: {duration:.1f} дней")

analyze_transitions(model, label_map)

Типичный результат:

Матрица переходных вероятностей:
               bull      bear  sideways
      bull    0.952     0.018     0.030
      bear    0.031     0.937     0.032
   sideways   0.043     0.027     0.930

Ожидаемая длительность режимов (дни):
  bull: 20.8 дней
  bear: 15.9 дней
  sideways: 14.3 дней

Что мы видим:

  1. Режимы липкие: вероятность остаться в текущем режиме > 93% для всех состояний
  2. Бычий режим длится дольше медвежьего (20.8 vs 15.9 дней) — опять рынки растут медленнее, чем падают
  3. Прямой переход bull→bear маловероятен (1.8%) — обычно рынок проходит через боковик

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

Торговая стратегия: один режим — одна стратегия

Теперь применяем полученные знания. Идея: не торговать одну стратегию всё время, а переключаться между стратегиями в зависимости от определённого режима.

Bull → Агрессивный моментум

  • Увеличенный размер позиции (до 100% капитала)
  • Трендовые стратегии: пробои, следование за скользящими средними
  • Стоп-лоссы — широкие (не выбивать на откатах)
  • Не шортить (или шортить минимально)

Bear → Защитная / короткая позиция

  • Уменьшенный размер позиции (30-50% капитала)
  • Шорт-стратегии или полный кэш
  • Тайт стоп-лоссы
  • Хеджирование через пут-опционы или фьючерсы

Sideways → Mean-reversion / Grid

  • Средний размер позиции (50-70% капитала)
  • Сеточные стратегии (grid trading)
  • Mean-reversion: покупка у нижней границы, продажа у верхней
  • Market-making с узкими спредами
def regime_adaptive_strategy(features, initial_capital=10000):
    """
    Простая режим-адаптивная стратегия.
    Bull: лонг 100%, Bear: шорт 50%, Sideways: лонг 30%.
    """
    capital = initial_capital
    position = 0  # 1 = лонг, -1 = шорт, 0 = нет позиции
    equity = [capital]
    positions = []

    for i in range(1, len(features)):
        regime = features.iloc[i]['regime_label']
        ret = features.iloc[i]['log_return']

        if regime == 'bull':
            target_exposure = 1.0   # 100% лонг
        elif regime == 'bear':
            target_exposure = -0.5  # 50% шорт
        elif regime == 'sideways':
            target_exposure = 0.3   # 30% лонг (или grid)
        else:
            target_exposure = 0.0

        daily_pnl = capital * target_exposure * ret

        capital += daily_pnl
        equity.append(capital)
        positions.append(target_exposure)

    features = features.copy()
    features['equity'] = equity
    features['position'] = [0] + positions

    return features

Бэктест: HMM-адаптивная стратегия vs Buy-and-Hold

Теперь главный вопрос: работает ли это лучше, чем тупой Buy-and-Hold?

def run_backtest(features, initial_capital=10000):
    """Сравнительный бэктест: Buy-and-Hold vs HMM-Adaptive."""

    cumulative_returns = (1 + features['log_return']).cumprod()
    bnh_equity = initial_capital * cumulative_returns

    features = regime_adaptive_strategy(features, initial_capital)

    def calc_metrics(equity_series):
        returns = pd.Series(equity_series).pct_change().dropna()
        total_return = (equity_series.iloc[-1] / equity_series.iloc[0] - 1) * 100
        annual_return = ((1 + total_return / 100) ** (365 / len(returns)) - 1) * 100
        sharpe = returns.mean() / returns.std() * np.sqrt(365)
        max_dd = ((equity_series / equity_series.cummax()) - 1).min() * 100
        return {
            'Total Return (%)': total_return,
            'Annual Return (%)': annual_return,
            'Sharpe Ratio': sharpe,
            'Max Drawdown (%)': max_dd
        }

    bnh_metrics = calc_metrics(bnh_equity)
    hmm_metrics = calc_metrics(features['equity'])

    print(f"{'Метрика':<25} {'Buy&Hold':>12} {'HMM-Adaptive':>14}")
    print("-" * 53)
    for key in bnh_metrics:
        print(f"{key:<25} {bnh_metrics[key]:>12.2f} {hmm_metrics[key]:>14.2f}")

    return features, bnh_equity

features, bnh_equity = run_backtest(features)

Бэктест результаты Сравнение equity curves: Buy-and-Hold (синий) и HMM-адаптивная стратегия (оранжевый). Адаптивная стратегия существенно снижает просадки в медвежьих фазах.

Типичные результаты для BTC (2020-2025):

Метрика                    Buy&Hold   HMM-Adaptive
-----------------------------------------------------
Total Return (%)             487.32         623.18
Annual Return (%)             42.71          49.84
Sharpe Ratio                   1.12           1.68
Max Drawdown (%)             -76.42         -38.17

Ключевое наблюдение: HMM-адаптивная стратегия не обязательно даёт больший общий доход (хотя в данном случае даёт), но драматически снижает максимальную просадку — с 76% до 38%. Sharpe вырос с 1.12 до 1.68. Это улучшение risk-adjusted returns, а не просто «больше денег».

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

Визуализация результатов

import matplotlib.pyplot as plt
import matplotlib.dates as mdates

fig, axes = plt.subplots(3, 1, figsize=(14, 10), sharex=True)

axes[0].plot(features.index, bnh_equity, label='Buy & Hold', alpha=0.8)
axes[0].plot(features.index, features['equity'], label='HMM-Adaptive', alpha=0.8)
axes[0].set_ylabel('Капитал ($)')
axes[0].legend()
axes[0].set_title('Equity Curve: Buy & Hold vs HMM-Adaptive')

colors = {'bull': '#2ecc71', 'bear': '#e74c3c', 'sideways': '#f39c12'}
for regime in ['bull', 'bear', 'sideways']:
    mask = features['regime_label'] == regime
    axes[1].scatter(features.index[mask], df.loc[features.index[mask], 'close'],
                    c=colors[regime], s=2, label=regime, alpha=0.7)
axes[1].set_ylabel('Цена BTC ($)')
axes[1].set_yscale('log')
axes[1].legend()
axes[1].set_title('Цена BTC с раскраской по режимам')

for i, (regime, color) in enumerate(colors.items()):
    inv_map = {v: k for k, v in label_map.items()}
    state_idx = inv_map[regime]
    axes[2].fill_between(features.index,
                          features[f'prob_state_{state_idx}'],
                          alpha=0.4, color=color, label=regime)
axes[2].set_ylabel('Вероятность режима')
axes[2].legend()
axes[2].set_title('Апостериорные вероятности режимов')

plt.tight_layout()
plt.savefig('hmm_backtest.png', dpi=150)
plt.show()

Продвинутые техники

Базовая HMM — хорошая отправная точка, но далеко не предел.

Иерархическая HMM (Hierarchical HMM)

В иерархической HMM верхний уровень определяет «макро-режим» (глобальный тренд, годовые циклы), а нижний — «микро-режим» (внутринедельные/внутримесячные колебания). Пакет fHMM для R, опубликованный в Journal of Statistical Software в 2024 году (Oelschlager, Adam, Michels), реализует именно эту идею для финансовых временных рядов.

Пример: макро-режим «бычий цикл» содержит внутри себя микро-режимы «ралли», «коррекция» и «консолидация». Это позволяет не паниковать при каждом 10%-ном откате в бычьем рынке — модель понимает, что коррекция внутри бычьего цикла — это норма.

Мультивариантная HMM с расширенными фичами

Вместо одномерных доходностей подаём вектор признаков: доходности + волатильность + объём + on-chain данные. Это позволяет модели «видеть» больше информации о состоянии рынка.

from hmmlearn.hmm import GaussianHMM

extended_features = ['log_return', 'rolling_vol', 'norm_volume',
                     'rolling_mean_return', 'abs_return']

X_extended = features[extended_features].values
scaler_ext = StandardScaler()
X_ext_scaled = scaler_ext.fit_transform(X_extended)

model_mv = GaussianHMM(
    n_components=3,
    covariance_type='full',     # полная ковариационная матрица
    n_iter=300,
    random_state=42,
    init_params='stmc',         # инициализировать все параметры
    verbose=False
)
model_mv.fit(X_ext_scaled)

n_params_base = 3 * (3 + 3 + 3*4/2) + 3*2    # упрощённая оценка
n_params_ext = 3 * (5 + 5 + 5*6/2) + 3*2

bic_base = -2 * model.score(X_scaled) * len(X_scaled) + n_params_base * np.log(len(X_scaled))
bic_ext = -2 * model_mv.score(X_ext_scaled) * len(X_ext_scaled) + n_params_ext * np.log(len(X_ext_scaled))

print(f"BIC базовая модель: {bic_base:.0f}")
print(f"BIC расширенная:    {bic_ext:.0f}")
print(f"Расширенная лучше: {bic_ext < bic_base}")

HMM + ML Ensemble

Современный подход: использовать HMM не как торговую систему, а как генератор признаков для последующей модели. Идея, описанная в работе Gupta et al. (2025) «A forest of opinions: A multi-model ensemble-HMM voting framework for market regime shift detection and trading»:

  1. HMM определяет текущий режим (или вероятности режимов)
  2. Режим подаётся как дополнительный признак в Random Forest / Gradient Boosting
  3. ML-модель принимает конкретные торговые решения с учётом режима
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import TimeSeriesSplit

features['regime_0_prob'] = state_probs[:, 0]
features['regime_1_prob'] = state_probs[:, 1]
features['regime_2_prob'] = state_probs[:, 2]

features['target'] = (features['log_return'].shift(-1) > 0).astype(int)

ml_features = ['log_return', 'rolling_vol', 'norm_volume',
               'regime_0_prob', 'regime_1_prob', 'regime_2_prob']

X_ml = features[ml_features].dropna()
y_ml = features.loc[X_ml.index, 'target'].dropna()

common_idx = X_ml.index.intersection(y_ml.index)
X_ml = X_ml.loc[common_idx]
y_ml = y_ml.loc[common_idx]

tscv = TimeSeriesSplit(n_splits=5)
scores = []

for train_idx, test_idx in tscv.split(X_ml):
    X_train, X_test = X_ml.iloc[train_idx], X_ml.iloc[test_idx]
    y_train, y_test = y_ml.iloc[train_idx], y_ml.iloc[test_idx]

    clf = GradientBoostingClassifier(n_estimators=100, max_depth=3, random_state=42)
    clf.fit(X_train, y_train)
    score = clf.score(X_test, y_test)
    scores.append(score)

print(f"Walk-Forward Accuracy: {np.mean(scores):.3f} +/- {np.std(scores):.3f}")

Продакшен: подводные камни

Красивый бэктест — это полдела. В продакшене вас ждут несколько неприятных сюрпризов.

Проблема лага (Look-Ahead Bias)

HMM определяет режим на основе текущих и прошлых данных, но в бэктесте есть соблазн обучить модель на всём датасете, включая будущие данные. Это — look-ahead bias, и он превращает бэктест в фикцию.

Решение: Walk-Forward подход. Обучаем модель на данных до момента tt, предсказываем режим в момент tt, затем сдвигаем окно. Именно так, как описано в нашей статье о Walk-Forward Optimization.

def walk_forward_hmm(features, feature_cols, train_window=252, retrain_freq=21):
    """
    Walk-Forward HMM: обучаем на скользящем окне,
    предсказываем на следующих retrain_freq днях.
    """
    regimes_wf = pd.Series(index=features.index, dtype=float)

    for start in range(train_window, len(features), retrain_freq):
        train_data = features.iloc[start - train_window:start]
        X_train = train_data[feature_cols].values

        scaler = StandardScaler()
        X_train_scaled = scaler.fit_transform(X_train)

        model = GaussianHMM(n_components=3, covariance_type='full',
                            n_iter=100, random_state=42)
        try:
            model.fit(X_train_scaled)
        except Exception:
            continue

        end = min(start + retrain_freq, len(features))
        test_data = features.iloc[start:end]
        X_test = test_data[feature_cols].values
        X_test_scaled = scaler.transform(X_test)

        predicted = model.predict(X_test_scaled)
        regimes_wf.iloc[start:end] = predicted

    return regimes_wf

Расписание переобучения

Как часто переобучать модель? Слишком редко — модель устареет, рынок изменится. Слишком часто — модель будет нестабильной, режимы будут «прыгать».

Эмпирические рекомендации:

  • Для дневных данных: переобучение раз в 1-4 недели (21 торговых дня — хороший дефолт)
  • Окно обучения: 6-12 месяцев (252 торговых дня — один год)
  • Мониторинг: если log-likelihood на новых данных падает ниже порога — внеплановое переобучение

Нестабильность меток

При каждом переобучении нумерация состояний может измениться: то, что было «режим 0» (бычий), может стать «режим 2». Нужно автоматически сопоставлять состояния по их статистике (средние доходности, волатильность).

Online-обновление

Для real-time торговли полное переобучение каждый день — избыточно. Можно использовать Forward-фильтрацию: фиксируем параметры модели, но обновляем апостериорные вероятности состояний с каждым новым наблюдением. Это мгновенная операция.

def online_regime_update(model, scaler, new_observation, prev_state_probs):
    """
    Online обновление вероятностей режимов
    без переобучения всей модели.
    """
    obs_scaled = scaler.transform(new_observation.reshape(1, -1))

    from scipy.stats import multivariate_normal
    emission_probs = np.array([
        multivariate_normal.pdf(obs_scaled[0],
                                 mean=model.means_[i],
                                 cov=model.covars_[i])
        for i in range(model.n_components)
    ])

    transition = model.transmat_.T  # переход из столбца в строку
    predicted = transition @ prev_state_probs
    updated = emission_probs * predicted
    updated /= updated.sum()  # нормализация

    return updated

Выбор числа состояний

Хотя три режима — хороший дефолт, стоит проверить альтернативы:

from hmmlearn.hmm import GaussianHMM

def select_n_components(X_scaled, max_components=6):
    """Выбор оптимального числа состояний по BIC."""
    results = []
    for n in range(2, max_components + 1):
        model = GaussianHMM(n_components=n, covariance_type='full',
                            n_iter=200, random_state=42)
        model.fit(X_scaled)

        log_likelihood = model.score(X_scaled) * len(X_scaled)
        n_features = X_scaled.shape[1]
        n_params = (n * (n - 1)
                   + n * n_features
                   + n * n_features * (n_features + 1) / 2
                   + (n - 1))
        bic = -2 * log_likelihood + n_params * np.log(len(X_scaled))

        results.append({'n_components': n, 'BIC': bic,
                        'log_likelihood': log_likelihood})
        print(f"n={n}: BIC={bic:.0f}, LL={log_likelihood:.0f}")

    best = min(results, key=lambda x: x['BIC'])
    print(f"\nОптимальное число состояний по BIC: {best['n_components']}")
    return results

results = select_n_components(X_scaled)

Ограничения и предостережения

Было бы нечестно умолчать о проблемах.

Гауссово допущение. Базовая GaussianHMM предполагает, что доходности в каждом режиме распределены нормально. Реальные распределения имеют толстые хвосты и асимметрию. Частичное решение — использовать Student-t распределение или GMMHMM (Gaussian Mixture per state).

Число состояний — ваш выбор. BIC помогает, но не всегда однозначно. Два разных исследователя могут прийти к разным числам режимов и оба будут «правы».

Переходные периоды. Модель неуверенно себя чувствует при переключении режимов. Вероятности распределяются примерно поровну, и стратегия получает «размытый» сигнал. Решение — пороговое правило: переключать стратегию, только когда вероятность нового режима превысит 70-80%.

Overfitting. Как и любая модель, HMM может переобучиться. Особенно с большим числом состояний или признаков. Walk-Forward валидация — обязательна.

Крипто-специфика. Криптовалютный рынок молод и структурно нестабилен. Режим «бычий рынок» в 2017 и «бычий рынок» в 2024 — это статистически разные явления. Модель может не генерализовать через циклы.

Что почитать дальше

Для тех, кто хочет углубиться:

Фундаментальные работы:

  • Hamilton, J.D. (1989). A New Approach to the Economic Analysis of Nonstationary Time Series and the Business Cycle. Econometrica, 57(2), 357-384. — Основополагающая работа по марковским переключающимся моделям
  • Guidolin, M., & Timmermann, A. (2007). Asset Allocation under Multivariate Regime Switching. Journal of Economic Dynamics and Control, 31(11), 3503-3544. — Практическое применение к распределению активов
  • Ang, A., & Bekaert, G. (2002). Regime Switches in Interest Rates. Journal of Business & Economic Statistics, 20(2), 163-182. — Режимы в процентных ставках

Современные исследования:

  • Gupta, R., Kapoor, S., Gupta, H., & Natesan, S. (2025). A forest of opinions: A multi-model ensemble-HMM voting framework for market regime shift detection and trading. Data Science in Finance and Economics. — Ensemble-подход к детекции режимов
  • Oelschlager, L., Adam, T., & Michels, R. (2024). fHMM: Hidden Markov Models for Financial Time Series in R. Journal of Statistical Software. — Иерархические HMM для финансов
  • Bitcoin Price Regime Shifts: A Bayesian MCMC and Hidden Markov Model Analysis of Macroeconomic Influence. Mathematics, 2025. — HMM для Bitcoin с байесовским подходом

Практические руководства:

Заключение

Скрытые марковские модели — это не серебряная пуля, а инструмент. Полезный, математически обоснованный, с полувековой историей в статистике и тремя десятилетиями в финансах.

Главная ценность HMM для трейдинга — не в том, что она «предсказывает рынок» (никто не предсказывает), а в том, что она формализует интуицию опытного трейдера: рынок бывает в разных фазах, и стратегия должна адаптироваться. Вместо ручного «я чувствую, что рынок сейчас медвежий» вы получаете «вероятность медвежьего режима 82%, средняя длительность медвежьего цикла 16 дней, мы в нём уже 5-й день».

Стоит ли внедрять HMM в ваш торговый стэк? Если у вас несколько стратегий для разных рыночных условий и вы устали переключать их вручную — определённо да. Если вы торгуете одну стратегию и не планируете расширяться — отложите на потом, но имейте в виду.

И помните: лучшая модель — та, которая работает в продакшене, а не та, которая выигрывает на бэктесте.


Цитирование: Если вы используете материалы этой статьи в своих исследованиях или проектах, пожалуйста, сделайте ссылку:

Скрытые марковские модели в трейдинге: как адаптировать стратегию к режиму рынка. marketmaker.cc, 2026. URL: https://marketmaker.cc/ru/blog/post/regime-detection-hmm-adaptive-trading

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

MarketMaker.cc Team

Количественные исследования и стратегии

Обсудить в Telegram
Newsletter

Будьте в курсе событий

Подпишитесь на нашу рассылку, чтобы получать эксклюзивную аналитику по AI-трейдингу и обновления платформы.

Мы уважаем вашу конфиденциальность. Отписаться можно в любой момент.