Перед тем как запускать стратегию в реальную торговлю, ее много раз прогоняют на исторических данных. Бэктестинг — это тестирование стратегии на прошлых котировках, чтобы увидеть, как она вела бы себя в реальных рыночных условиях. Данный процесс позволяет заранее выявить слабые места стратегии и избежать ненужных потерь.
Качественный бэктест требует понимания не только программирования, но и специфики финансовых рынков: от особенностей исполнения ордеров до статистических ловушек при анализе временных рядов.
Результаты бэктестирования помогают принять решение о запуске стратегии в продакшен или о необходимости ее доработки. В то же время, даже небольшие ошибки в построении бэктеста могут сильно исказить картину эффективности стратегии. В этой статье мы подробно разберем наиболее распространенные ошибки и покажем, как их избежать.
Что такое бэктестинг?
Бэктестинг представляет собой симуляцию торговой стратегии на исторических рыночных данных. Система генерирует сигналы на покупку и продажу согласно заданным правилам, после чего рассчитывается гипотетическая доходность портфеля. Основная цель — получить статистику производительности стратегии в различных рыночных условиях без риска реальных средств.
Процесс отличается от форвард-тестирования (paper trading), где стратегия работает в режиме реального времени на текущих данных без реального исполнения сделок:
- Бэктест использует уже известные исторические цены, что создает риск использования информации из будущего;
- Форвард-тест устраняет эту проблему, но требует длительного времени для накопления статистики и не позволяет быстро протестировать множество вариантов параметров.
Корректный бэктест должен максимально точно воспроизводить условия реальной торговли. Он должен включать: моделирование проскальзывания, учет bid-ask спреда, ограничения ликвидности и задержек в получении данных и т. д. Без учета этих факторов доходность в тестах получается завышенной. И чем больше частота сделок, тем больше разница.
Основные компоненты бэктеста
Исторические данные
Качество данных напрямую влияет на достоверность результатов. Минутные и тиковые данные содержат больше информации для внутридневных стратегий, но требуют значительных вычислительных ресурсов. Дневные бары подходят для позиционных стратегий с горизонтом удержания от нескольких дней.
Источники данных различаются полнотой и точностью. Провайдеры уровня Bloomberg или Refinitiv предоставляют скорректированные цены с учетом сплитов и дивидендов, но стоят от нескольких тысяч долларов в год. Бесплатные альтернативы типа Yahoo Finance содержат ошибки в исторических данных и пропуски в котировках, особенно для бумаг с низкой ликвидностью.
Ключевые проверки данных включают:
- Выявление пропусков во временных рядах;
- Наличие спайков, аномальных ценовых выбросов;
- Наличие баров с нулевыми или аномальными объемами;
- Ошибки в котировках открытия/закрытия свечей, неконсистентность между источниками данных;
- Технические сбои в исторических рядах.
Даже один пропущенный день в стратегии может исказить расчет стандартного отклонения и привести к ложным сигналам. Для обнаружения выбросов часто используют z-score с порогом 4–5 стандартных отклонений, что позволяет выявлять технические ошибки в данных.
Логика стратегии
Торговые правила должны быть формализованы в виде математических условий без двусмысленности интерпретации. Расплывчатые критерии типа «купить при сильном росте объема» не подходят для автоматизации. Вместо этого используются конкретные пороги: «открыть длинную позицию, если объем превышает 20-дневную скользящую среднюю в 2 раза».
Стратегия включает:
- Правила входа в позицию;
- Правила выхода по тейк-профиту или стоп-лоссу;
- Размер позиции;
- Размер реинвестирования прибыли.
Каждый компонент влияет на итоговую доходность и риск-профиль. Фиксированный размер позиции, например 10% капитала на сделку, формирует один профиль просадок, тогда как волатильно-взвешенные позиции по формуле Келли меняют распределение прибыли и риски.
Чтобы эти правила можно было эффективно применить к историческим данным и быстро протестировать, стратегию реализуют на C++ или Python с векторизацией через датафреймы pandas. Такой подход позволяет обрабатывать сигналы для всего набора данных одновременно, ускоряя вычисления в 50–100 раз по сравнению с циклом по строкам. При этом векторизация требует особого внимания к последовательным зависимостям между сделками и предотвращению «заглядывания в будущее» (look-ahead bias).
Модель исполнения
Упрощенная модель предполагает исполнение по ценам закрытия баров без задержек и проскальзывания. Такой подход завышает доходность, поскольку игнорирует реальность рыночной микроструктуры. На практике ордера исполняются с задержкой минимум один бар после генерации сигнала, а цена исполнения отличается от желаемой.
Более реалистичная модель учитывает bid-ask спред через вычитание половины спреда из цены покупки и добавление к цене продажи. Для ликвидных акций спред составляет 0.01-0.05%, для менее торгуемых достигает 0.3-1%. Проскальзывание моделируется как процент от волатильности или фиксированная величина в базисных пунктах.
Комиссии брокера варьируются от $0.001 до $0.005 за акцию для розничных трейдеров на американском рынке. Частота торговли определяет их влияние: стратегия с 200 сделками в год теряет 2-4% годовой доходности на комиссиях, при 2000 сделок потери достигают 20-40%. Институциональные трейдеры получают меньшие комиссии, но сталкиваются с impact cost — рыночным воздействием сделки, которое возникает при больших объемах и снижает эффективность стратегии.
Метрики производительности
Базовый набор метрик включает:
- Совокупную доходность;
- Годовую доходность;
- Максимальную просадку;
- Коэффициент Шарпа.
Совокупная доходность показывает рост капитала за весь период тестирования, но не учитывает риск. Годовая доходность нормализует результат для сравнения стратегий на разных временных интервалах.
Коэффициент Шарпа (Sharpe ratio) — ключевой показатель. В идеале он должен быть сильно больше 1. Он вычисляется как отношение средней избыточной доходности к стандартному отклонению доходности:
SR = (R̄ — Rᶠ) / σ
где:
- R̄ — средняя доходность стратегии за период;
- Rᶠ — безрисковая ставка (обычно доходность гособлигаций);
- σ — стандартное отклонение доходности.
Коэффициент показывает доходность на единицу принятого риска. Значения выше 1.0 считаются хорошими, выше 2.0 — отличными. Однако Sharpe ratio чувствителен к выбросам и предполагает нормальное распределение доходностей, что часто не соответствует реальности финансовых рынков.
При бэктестах всегда обращают внимание на колебания кривой доходности, особенно в периоды просадок. Максимальная просадка (maximum drawdown) измеряет наибольшее падение капитала от пика до минимума в процентах. Метрика критична для оценки психологической устойчивости трейдера и требований к капиталу. Просадка в 40% требует последующего роста на 67% для возврата к исходному уровню, что может занять годы.
Это основные показатели эффективности стратегии. Кроме них, для бэктестов иногда используют дополнительные метрики:
- Коэффициент Сортино (Sortino ratio) — учитывает только негативную волатильность, игнорируя колебания вверх. Чем выше значение, тем лучше стратегия компенсирует риск отрицательных движений цены;
- Коэффициент Калмара (Calmar ratio) — отношение годовой доходности к максимальной просадке. Он позволяет оценить, насколько стратегия устойчива к сильным падениям капитала;
- Винрейт (Win rate) — процент прибыльных сделок. Важно помнить, что винрейт выше 50% не гарантирует прибыльность; ключевое значение имеет соотношение средней прибыли к среднему убытку.
Типичные ошибки при проведении бэктестов
Использование будущих значений или Look-ahead bias
Смещение данных возникает, когда для генерации торгового сигнала используется информация, которая на самом деле еще не была доступна. Классический пример — расчет скользящих средних, уровней поддержки / сопротивления или других индикаторов с учетом будущих значений временного ряда. Такая ошибка приводит к нереалистично высокой доходности в бэктесте, которая не воспроизводится в реальной торговле.
Распространенная ошибка — использование цены закрытия текущего бара для генерации сигнала и исполнения на этой же цене. Реальная система узнает цену закрытия только после завершения бара, поэтому исполнение возможно минимум на следующем баре. Сдвиг сигналов на один период назад относительно исполнения устраняет проблему.
Другой источник look-ahead bias — применение функций типа shift() или rolling() без правильной индексации. Pandas по умолчанию включает текущее значение в расчет скользящего окна, что создает утечку данных из будущего. Корректная реализация требует явного исключения текущего наблюдения или использования параметра closed=’left’ в rolling().
Проверка на утечку данных из будущего включает ручной пошаговый анализ нескольких сделок с выводом доступных данных на момент генерации сигнала. Если стратегия использует данные, которые появятся только в будущих барах, бэктест содержит ошибку. Увы, но решений по автоматической детекции таких случаев нет, поэтому от специалиста требуется тщательная ревизия кода.
Ошибка выживших или Survivorship bias
Ошибка выживших в бэктестинге возникает при тестировании стратегии только на активах, которые существуют в настоящее время. Компании, обанкротившиеся или делистированные с биржи, исключаются из анализа, что искусственно завышает историческую доходность. Этот эффект особенно заметен для долгосрочных стратегий с горизонтом 10 и более лет.
Индекс S&P 500 меняет состав компаний каждый год — добавляются растущие бизнесы, исключаются проблемные. Бэктест стратегии на текущем составе индекса за последние 20 лет тестирует только выживших победителей, игнорируя неудачников. Реальная стратегия в прошлом торговала бы смесью успешных и провальных компаний.
Устранить такую ошибку можно за счет использования исторических датасетов с актуальным состоянием на каждую дату (point-in-time), которые показывают точный состав индексов и список торгуемых активов в прошлом. Поставщики данных, такие как Norgate Data или Sharadar, предоставляют такие наборы за дополнительную плату, тогда как бесплатные источники обычно содержат только текущие «выжившие» компании.
Влияние survivorship bias на доходность составляет примерно 1–3% годовых для широких индексов и 5–10% для стратегий на акциях с малой капитализацией. Особенно чувствительны к этому стоимостные стратегии, так как они часто покупают проблемные компании, многие из которых впоследствии исключаются с биржи.
Переобучение или переподгонка стратегии
Переподгонка (overfitting) возникает при чрезмерной оптимизации параметров стратегии под исторические данные. Стратегия с множеством правил и условий может идеально работать на тестовом периоде, но полностью проваливаться на новых данных. Все потому, что модель запоминает случайный шум вместо устойчивых рыночных закономерностей.
Типичный пример — перебор тысяч комбинаций параметров индикаторов в поиске максимальной доходности. Найденная оптимальная комбинация скорее всего отражает случайные совпадения в прошлом, а не предсказательную силу. Вероятность найти прибыльную комбинацию случайно растет с количеством попыток согласно множественному тестированию.
Частые признаки переподгонки:
- Резкое падение производительности стратегии на данных вне выборки (out-of-sample) по сравнению с данными внутри выборки (in-sample);
- Слишком высокий Sharpe ratio (выше 3–4), не соответствующий реальному риску. Как правило, чем выше доходность, тем выше и риск;
- Идеальную кривую капитала без просадок;
- Чрезмерную сложность правил стратегии — стратегии с десятком условий и параметров чаще переподогнаны по сравнению с простыми логиками из 2–3 правил;
- Нестабильность сигналов при небольших изменениях входных данных;
- Зависимость результатов от редких экстремальных событий (outlier sensitivity).
Эти признаки помогают выявлять стратегии, которые скорее запомнили случайный шум истории, чем выявили устойчивые рыночные закономерности.
Методы борьбы с переподгонкой включают:
- Ограничение числа оптимизируемых параметров;
- Использование регуляризации;
- Проверку стратегии на нескольких независимых исторических периодах;
- Тестирование на разных рынках;
- Применение техники кросс-валидации с разными настройками по фолдам для оценки стабильности параметров;
- Упрощение логики стратегии и удаление незначимых сигналов для снижения сложности.
В трейдинге действует принцип бритвы Оккама — простые стратегии обычно более устойчивы к изменениям рыночного режима и меньше подвержены случайному шуму.
Игнорирование транзакционных издержек
Транзакционные издержки включают комиссии брокера, bid-ask спред, проскальзывание и рыночное воздействие сделки (impact cost) для крупных ордеров. Начинающие трейдеры часто учитывают только комиссии, игнорируя остальные компоненты, что приводит к переоценке реальной прибыльности стратегии.
Спред Bid-ask варьируется в течение дня и расширяется в периоды низкой ликвидности. Утренний спред сразу после открытия биржи в 2-3 раза шире дневного среднего. Стратегии, торгующие на открытии или закрытии сессии, несут повышенные издержки. Использование среднедневного спреда в бэктесте недооценивает реальные потери.
Проскальзывание возрастает с размером ордера относительно среднедневного объема торгов. Ордер на 10% дневного объема сдвигает цену исполнения на 0.5-2% против трейдера в зависимости от ликвидности инструмента. Моделирование проскальзывания через квадратный корень от отношения размера ордера к объему дает приближенную оценку impact cost.
Частота торговли усиливает влияние издержек. Стратегия с периодом удержания в 2-3 дня генерирует 80-120 сделок в год, теряя 1-2% на издержках при комиссии $0.002 за акцию. Увеличение частоты до 1-2 сделок в день дает 250-500 сделок в год и потери 5-15%. Высокочастотные подходы с тысячами сделок требуют стратегий с доходностями минимум 20-30% годовых для покрытия издержек.
Тестирование на тренировочной и тестовой выборках
Разделение исторических данных на тренировочный (in-sample) и тестовый (out-of-sample) периоды — базовое требование для объективной оценки стратегии:
- In-sample период используется для разработки логики и оптимизации параметров;
- Out-of-sample — для проверки устойчивости найденных закономерностей на данных, которые алгоритм еще не видел, что приближенно к реальности.
Типичное соотношение составляет 80/20 или 70/30 в пользу тренировочного периода. Для временных рядов длиной 10 лет используется 7 лет для обучения и 3 года для валидации. Слишком короткий in-sample период не дает достаточной статистики для оптимизации, слишком длинный сокращает возможности проверки на свежих данных.
Правильная временная последовательность данных крайне важна для финансовых временных рядов. Случайное перемешивание наблюдений разрушает автокорреляцию и может привести к утечке данных из будущего (look-ahead bias). Поэтому данные всегда разделяют последовательно: первая часть истории используется для обучения модели, а последующая — для тестирования. Информация из будущего никогда не должна попадать в тренировочный период.
Выбор границы между периодами учитывает структурные изменения рынка. Разделение до и после финансового кризиса 2008-2009 создает два разных рыночных режима. Стратегия, оптимизированная на бычьем рынке 2009-2020, может провалиться в условиях повышенной волатильности. Включение разных рыночных фаз в оба периода дает более реалистичную оценку.
Walk-forward анализ
Walk-forward анализ расширяет концепцию разделения данных через множественные итерации оптимизации и тестирования. Метод моделирует непрерывное обновление параметров стратегии по мере поступления новых данных, что приближает бэктест к реальным условиям адаптивной торговли.
Алгоритм работает следующим образом:
- Оптимизируем параметры на первом окне данных длиной N периодов;
- Применяем найденные параметры на следующих M периодах для генерации сделок;
- Сдвигаем окно вперед и повторяем процесс.
Типичные значения — N=252 торговых дня (1 год) для оптимизации, M=63 дня (3 месяца) для тестирования.
Результаты walk-forward теста показывают стабильность стратегии во времени. Если производительность значительно различается между окнами, стратегия чувствительна к выбору параметров и рыночному режиму. Стабильная доходность на всех окнах указывает на устойчивые закономерности. Допустимая вариация годовой доходности между окнами составляет ±30-50% от средней.
Вычислительная сложность walk-forward анализа существенно выше простого разделения, поскольку требует повторной оптимизации на каждой итерации. Для стратегии с 5 параметрами и 10 значений каждого получается 100,000 комбинаций на одно окно. При 20 окнах общее число бэктестов достигает 2 миллионов, что требует часов расчетов даже на быстрых машинах.
Кросс-валидация в трейдинге
Кросс-валидация (cross-validation) адаптируется для временных рядов с учетом их специфики. Стандартный k-fold подход со случайным разделением нарушает временную последовательность и почти гарантирует заглядывание в будущее. Поэтому для временных рядов используют последовательную версию кросс-валидации с временными фолдами.
Метод Time series cross-validation разделяет данные на K последовательных периодов:
- Первый фолд служит тренировочным, второй — тестовым;
- Затем первые два становятся тренировочными, третий — тестовым, и так далее.
Метод дает K-1 независимых оценок производительности на разных временных интервалах.
Метод Purged k-fold cross-validation усовершенствует базовый подход через удаление наблюдений вокруг границ между фолдами. Удаление предотвращает утечку информации через корреляцию соседних наблюдений во временных рядах. Типичный размер purged периода составляет 5-10 дней для дневных данных, что соответствует периоду автокорреляции большинства финансовых инструментов.
Комбинаторная purged cross-validation генерирует все возможные комбинации тренировочных и тестовых фолдов, что дает больше независимых оценок по сравнению с последовательным подходом. Метод требует значительных вычислений: для 10 фолдов получается C(10,2) = 45 комбинаций. Каждая комбинация дает независимую оценку производительности, усреднение которых снижает вариацию итогового результата.
Практическая реализация бэктеста на Python
Мы рассмотрели ключевые аспекты теории бэктестинга, теперь давайте перейдем от теории к практике.
Рассмотрим конкретную стратегию и ее тестирование. В хедж-фондах одной из наиболее популярных является стратегия Mean reversion или стратегия возврата к среднему. Она основана на предположении, что цены инструментов со временем возвращаются к своему среднему уровню после колебаний вверх или вниз.
Для практической реализации стратегии часто используют пары акций, рассчитывая z-score спреда между их ценами. Это позволяет формализовать торговые сигналы и оценить, насколько отклонение цены от среднего уровня может служить сигналом для открытия позиции.
Ниже представлен пример кода бэктеста, который демонстрирует процесс генерации сигналов, расчета позиций и оценки доходности стратегии.
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
# Загрузка данных для двух коррелированных акций
tickers = ['SLB', 'HAL'] # Schlumberger и Halliburton
start_date = '2021-01-01'
end_date = '2024-12-31'
data = yf.download(tickers, start=start_date, end=end_date)['Close']
# Проверка на MultiIndex и выравнивание
if isinstance(data.columns, pd.MultiIndex):
data = data.droplevel(0, axis=1)
# Разделение на in-sample и out-of-sample
split_date = '2024-01-01'
data_train = data[data.index < split_date].copy() data_test = data[data.index >= split_date].copy()
print(f"Train период: {data_train.index[0]} - {data_train.index[-1]}")
print(f"Test период: {data_test.index[0]} - {data_test.index[-1]}")
# Функция расчета спреда и z-score
def calculate_spread_zscore(prices, lookback=20):
"""
Рассчитываем спред между двумя активами и его z-score
"""
ratio = prices.iloc[:, 0] / prices.iloc[:, 1]
spread_mean = ratio.rolling(window=lookback).mean()
spread_std = ratio.rolling(window=lookback).std()
zscore = (ratio - spread_mean) / spread_std
return ratio, zscore
# Генерация торговых сигналов
def generate_signals(zscore, entry_threshold=2.0, exit_threshold=0.5):
"""
Генерация сигналов на основе z-score:
- Длинная позиция при z-score < -entry_threshold - Короткая позиция при z-score > entry_threshold
- Закрытие при возврате к exit_threshold
"""
signals = pd.DataFrame(index=zscore.index)
signals['zscore'] = zscore
signals['position'] = 0
position = 0
positions_list = []
for z in signals['zscore']:
if pd.isna(z):
positions_list.append(position)
continue
if z < -entry_threshold and position == 0: position = 1 elif z > entry_threshold and position == 0:
position = -1
elif z > -exit_threshold and position == 1:
position = 0
elif z < exit_threshold and position == -1: position = 0 positions_list.append(position) signals['position'] = pd.Series(positions_list, index=signals.index) signals['position'] = signals['position'].shift(1).fillna(0) return signals # Расчет доходности стратегии def calculate_strategy_returns(prices, signals, transaction_cost=0.001): returns = prices.pct_change() spread_returns = returns.iloc[:, 0] - returns.iloc[:, 1] strategy_returns = signals['position'] * spread_returns position_changes = signals['position'].diff().abs() costs = position_changes * transaction_cost strategy_returns = strategy_returns - costs return strategy_returns # Расчет метрик производительности def calculate_metrics(returns): cumulative_return = (1 + returns).cumprod().iloc[-1] - 1 n_years = len(returns) / 252 annual_return = (1 + cumulative_return) ** (1 / n_years) - 1 annual_vol = returns.std() * np.sqrt(252) sharpe = annual_return / annual_vol if annual_vol > 0 else 0
cumulative = (1 + returns).cumprod()
running_max = cumulative.expanding().max()
max_drawdown = ((cumulative - running_max) / running_max).min()
win_rate = (returns > 0).sum() / (returns != 0).sum() if (returns != 0).sum() > 0 else 0
return {
'Cumulative Return': f"{cumulative_return:.2%}",
'Annual Return': f"{annual_return:.2%}",
'Annual Volatility': f"{annual_vol:.2%}",
'Sharpe Ratio': f"{sharpe:.2f}",
'Max Drawdown': f"{max_drawdown:.2%}",
'Win Rate': f"{win_rate:.2%}",
'Total Trades': int((returns != 0).sum())
}
# Бэктест на in-sample данных
ratio_train, zscore_train = calculate_spread_zscore(data_train, lookback=20)
signals_train = generate_signals(zscore_train, entry_threshold=2.0, exit_threshold=0.5)
returns_train = calculate_strategy_returns(data_train, signals_train, transaction_cost=0.001)
print("\nIN-SAMPLE МЕТРИКИ")
metrics_train = calculate_metrics(returns_train)
for key, value in metrics_train.items():
print(f"{key}: {value}")
# Бэктест на out-of-sample данных
ratio_test, zscore_test = calculate_spread_zscore(data_test, lookback=20)
signals_test = generate_signals(zscore_test, entry_threshold=2.0, exit_threshold=0.5)
returns_test = calculate_strategy_returns(data_test, signals_test, transaction_cost=0.001)
print("\nOUT-OF-SAMPLE МЕТРИКИ")
metrics_test = calculate_metrics(returns_test)
for key, value in metrics_test.items():
print(f"{key}: {value}")
# Визуализация
fig, axes = plt.subplots(3, 1, figsize=(14, 10))
# График 1
ax1 = axes[0]
ax1.plot(ratio_train.index, ratio_train, label='Train Period', color='#2C3E50', linewidth=1)
ax1.plot(ratio_test.index, ratio_test, label='Test Period', color='#E74C3C', linewidth=1)
ax1.axvline(x=pd.Timestamp(split_date), color='gray', linestyle='--', alpha=0.7)
ax1.set_ylabel('Price Ratio (SLB/HAL)')
ax1.set_title('Спред между Schlumberger и Halliburton ')
ax1.legend()
ax1.grid(alpha=0.3)
# График 2
ax2 = axes[1]
ax2.plot(zscore_train.index, zscore_train, label='Train Z-score', color='#2C3E50', linewidth=1)
ax2.plot(zscore_test.index, zscore_test, label='Test Z-score', color='#E74C3C', linewidth=1)
ax2.axhline(y=2.0, color='green', linestyle='--', alpha=0.5, label='Entry Threshold')
ax2.axhline(y=-2.0, color='green', linestyle='--', alpha=0.5)
ax2.axhline(y=0.5, color='orange', linestyle='--', alpha=0.5, label='Exit Threshold')
ax2.axhline(y=-0.5, color='orange', linestyle='--', alpha=0.5)
ax2.axvline(x=pd.Timestamp(split_date), color='gray', linestyle='--', alpha=0.7)
ax2.set_ylabel('Z-score')
ax2.set_title('Z-score и торговые сигналы')
ax2.legend()
ax2.grid(alpha=0.3)
# График 3
ax3 = axes[2]
cum_returns_train = (1 + returns_train).cumprod()
cum_returns_test = (1 + returns_test).cumprod()
cum_returns_test_adjusted = cum_returns_test * cum_returns_train.iloc[-1]
ax3.plot(cum_returns_train.index, cum_returns_train, label='Train Period', color='#2C3E50', linewidth=1.5)
ax3.plot(cum_returns_test.index, cum_returns_test_adjusted, label='Test Period', color='#E74C3C', linewidth=1.5)
ax3.axvline(x=pd.Timestamp(split_date), color='gray', linestyle='--', alpha=0.7, label='Split Date')
ax3.set_ylabel('Cumulative Returns')
ax3.set_xlabel('Date')
ax3.set_title('Накопленная доходность стратегии')
ax3.legend()
ax3.grid(alpha=0.3)
plt.tight_layout()
plt.show()
# Таблица метрик
comparison = pd.DataFrame({
'In-Sample': metrics_train,
'Out-of-Sample': metrics_test
})
print("\nСРАВНЕНИЕ МЕТРИК")
print(comparison)

Рис. 1: Бэктест стратегии Mean reversion на паре Schlumberger (SLB) и Halliburton (HAL). Верхняя панель показывает динамику спреда между акциями с границей разделения на тренировочный и тестовый периоды. Средняя панель отображает z-score спреда с пороговыми уровнями для входа (±2.0) и выхода (±0.5) из позиций. Нижняя панель демонстрирует кумулятивную доходность стратегии отдельно для каждого периода, нормализованную для визуального сравнения динамики
Train период: 2021-01-04 00:00:00 - 2023-12-29 00:00:00
Test период: 2024-01-02 00:00:00 - 2024-12-30 00:00:00
IN-SAMPLE МЕТРИКИ
Cumulative Return: 28.16%
Annual Return: 8.66%
Annual Volatility: 12.60%
Sharpe Ratio: 0.69
Max Drawdown: -15.25%
Win Rate: 50.00%
Total Trades: 230
OUT-OF-SAMPLE МЕТРИКИ
Cumulative Return: 13.39%
Annual Return: 13.45%
Annual Volatility: 13.06%
Sharpe Ratio: 1.03
Max Drawdown: -8.77%
Win Rate: 53.09%
Total Trades: 81
СРАВНЕНИЕ МЕТРИК
In-Sample Out-of-Sample
Cumulative Return 28.16% 13.39%
Annual Return 8.66% 13.45%
Annual Volatility 12.60% 13.06%
Sharpe Ratio 0.69 1.03
Max Drawdown -15.25% -8.77%
Win Rate 50.00% 53.09%
Total Trades 230 81
Представленный код реализует полный цикл бэктестирования стратегии Mean reversion на паре акций Schlumberger (SLB) и Halliburton (HAL). Выбор этих инструментов обусловлен их высокой корреляцией как конкурентов в одной индустрии и достаточной ликвидностью для минимизации издержек.
Логика стратегии основана на z-score нормализованного спреда между ценами:
- Когда z-score опускается ниже -2, спред аномально низкий — открываем длинную позицию, ожидая возврата к среднему;
- При z-score выше +2 открываем короткую позицию;
- Выход происходит при возврате z-score к уровню ±0.5, что соответствует частичной реверсии к среднему.
Разделение данных на периоды позволяет оценить устойчивость параметров. Параметры стратегии (lookback=20, entry=2.0, exit=0.5) оптимизируются только на тренировочном периоде и применяются к тестовому без изменений. Важное правило — сдвиг сигналов на один период через shift(1) предотвращает look-ahead bias.
Транзакционные издержки в 0.1% на сделку учитывают комиссии и bid-ask спред. Для пары ликвидных акций эта оценка консервативна. Издержки вычитаются при каждом изменении позиции, что моделирует реальные расходы на вход и выход из сделок.
Метрики производительности рассчитываются идентично для обоих периодов:
- Sharpe ratio показывает риск-скорректированную доходность;
- Max Drawdown — наихудший сценарий потерь;
- Win Rate — процент прибыльных дней торговли.
Сравнение метрик между периодами выявляет переподгонку: значительное ухудшение на out-of-sample указывает на нестабильность стратегии.
Количество сделок влияет на статистическую значимость результатов и чувствительность к издержкам. Стратегия с 30 сделками за год дает недостаточную статистику для надежных выводов — случайность играет большую роль. Минимум 100-200 сделок требуется для начальной уверенности в устойчивости результатов, 500+ сделок дают высокую статистическую мощность.
Как сравнивать in-sample с out-of-sample
Деградация производительности модели между периодами — нормальное явление. Здесь важен не столь факт ухудешния метрик, а его масштаб.
Падение коэффициента Sharpe на 20–30% обычно считается допустимым и ожидаемым. Снижение более чем на 50% говорит либо о сильной переоптимизации параметров стратегии под тренировочный период, либо о существенном изменении рыночных условий. Если же прибыльность полностью исчезает на out-of-sample, это указывает на то, что результат на in-sample был случайным.
Сравнивая периоды, особое внимание нужно уделять максимальной просадке. Она важнее абсолютной доходности. Если просадка на out-of-sample в 2-3 раза глубже, чем на тренировочном периоде, это признак того, что стратегия плохо контролирует риск в новых условиях. Если же масштабы просадок сопоставимы, можно говорить о стабильности риск-менеджмента.
Также имеет значение структура сделок. Если количество сделок резко изменяется на тестовом периоде, стратегия чувствительна к рыночному режиму. Например, если в бычьем тренде она совершает 100 сделок в год, а в боковом — только 20, то ее работоспособность зависит от направленного движения рынка, и в нейтральных условиях эффективность резко падает.
Наконец, полезно смотреть на корреляцию доходностей между периодами. Высокая корреляция (выше 0.6) ежемесячных доходностей говорит о том, что стратегия продолжает использовать тот же источник альфы. Низкая корреляция (ниже 0.3) может означать, что источник прибыли изменился или что результаты на тренировочном периоде были случайными.
Заключение
Бэктестинг является ключевым инструментом оценки работоспособности торговых стратегий до их использования на реальном рынке. Понимание принципов корректного тестирования — от чистоты данных и формализации логики до учета издержек и проверки на устойчивость — позволяет избежать ложной уверенности и значительно снижает риск финансовых потерь. Без тщательного бэктеста даже перспективная стратегия может оказаться нежизнеспособной из-за скрытых ошибок, которые становятся очевидными только в реальных условиях.
Усвоение принципов, описанных в статье, дает практическое преимущество: умение отличать реальные торговые преимущества от статистического шума. Ключевые требования к качественному бэктесту:
- Использовать корректные исторические данные и проверять их на пропуски, выбросы и ошибки.
- Формализовать логику стратегии без двусмысленных условий и «ручных» интерпретаций.
- Учитывать транзакционные издержки (комиссии, спред, проскальзывание).
- Избегать look-ahead bias и утечек данных, сдвигая сигналы относительно исполнения.
- Сравнивать результаты in-sample и out-of-sample, оценивая устойчивость стратегии в разных условиях рынка.
Эти принципы — основа для построения стратегий, которые не только хорошо выглядят в теории, но и способны приносить устойчивый результат в реальной торговле.