Вы написали торговую стратегию на Python, загрузили исторические котировки за 5 лет и запустили бэктест. График доходности устремился вверх под 45 градусов, коэффициент Шарпа составил 1.8, а максимальная просадка не превысила 6%.
Вы деплоите алгоритм в прод, подключаете биржевой коннектор — и через два месяца стратегия показывает Шарп 0.4, просадка втрое превышает расчетную, а портфель уверенно генерирует убыток.
Первое, о чем думает разработчик — «рыночный режим внезапно сменился», «модель переобучилась на шуме» или «брокер слишком сильно проскальзывает». Но в практике количественных финансов от 30% до 60% разрыва между результатами бэктеста и реальным исполнением вызваны не логикой стратегии, а скрытыми дефектами исторических данных.
Дефекты рыночных данных — это не просто белый шум. Они структурированы, системны и почти всегда смещают результаты симуляции в сторону оптимизма, создавая иллюзию «грааля».
Ниже — детальный разбор 7 главных ловушек рыночных данных, математическая оценка их влияния на бэктест и рабочий код на Python (Polars / NumPy) для построения строгого Point-in-Time пайплайна очистки.
Таксономия: 7 способов обмануть себя при подготовке датасета
|
Дефект данных |
Определение |
Типичный источник |
Последствие для бэктеста |
|---|---|---|---|
|
1. Ошибка выжившего (Survivorship Bias) |
Исключение делистингованных и обанкротившихся компаний |
Скачивание «текущего состава S&P 500» на 10 лет назад |
Завышение доходности на 2–4% годовых |
|
2. Corporate Actions Look-Ahead |
Пересчет цен задним числом через готовые Adjusted Prices |
Стандартные adjusted-ряды из бесплатных API |
Алгоритм в 2021 году неявно «знает» о сплите 2024 года |
|
3. Stale Prices |
Застрявшие цены без изменений в течение N баров |
Неликвидные акции, облигации, pre-market |
Искусственный сигнал возврата к среднему (mean-reversion) |
|
4. Выпадение тиков (Missing Ticks) |
Дыры в потоке котировок во время пиковых нагрузок |
Перегрузка биржевых шлюзов при VIX > 30 |
Искусственное занижение волатильности, завышение плеча |
|
5. Рассинхрон таймзон и календарей |
Склейка рядов с разных бирж без учета закрытия сессий |
Смешивание UTC, EST, CET и переход на летнее время |
Будущие цены американского рынка попадают в европейский утренний сигнал |
|
6. As-Reported vs As-Restated |
Использование пересчитанных регуляторных отчетов |
Ревизии отчетностей 10-Q/10-K через год после публикации |
Использование фундаментальных цифр, которых не было на дату сделки |
|
7. Bid-Ask Bounce |
Расчет сигналов по Last Trade без учета спреда |
Тиковые данные сделок без стакана (L1/L2) |
Ложная арбитражная доходность, исчезающая при первом реальном спреде |
Разберем каждую ловушку с точки зрения физики рынка и архитектуры данных.
1. Ошибка выжившего (Survivorship Bias)
Классическая ошибка: берется список текущих 500 акций, входящих в индекс S&P 500 на сегодняшний день, и для них выгружаются котировки с 2015 года.
Что произошло на самом деле? Из выборки автоматически выпали компании, которые за эти 10 лет обанкротились, прошли процедуру поглощения с дисконтом или были исключены из индекса из-за падения капитализации (например, Silicon Valley Bank, First Republic, Enron, Lehman Brothers). В выборку попали только те, кто дожил до сегодняшнего дня и преуспел (Apple, Microsoft, Nvidia).
Масштаб искажения: Включение только «выживших» бумаг системно завышает среднегодовую доходность бэктеста на 1.6–4.2 процентных пункта в год. Стратегия кажется прибыльной просто потому, что вы инвестировали в победителей, которых ретроспективно отобрало будущее.
Решение: Датасет должен содержать динамический состав вселенной активов (Index Constituents Point-in-Time). На дату 2018-05-14 алгоритм должен иметь право выбирать только из тех акций, которые входили в индекс именно в этот день, включая те, которые через месяц обнулились.
2. Заглядывание в будущее через Adjusted Prices
Большинство разработчиков для простоты используют готовые ряды Adj Close из Yahoo Finance или аналогичных источников. В них исторические цены скорректированы на все последующие сплиты и дивиденды.
Представим акцию с ценой $1000, которая проводит сплит 10 к 1 в августе 2024 года:
-
Поставщик данных делит все цены до августа 2024 года на 10. Цена в 2022 году становится $100.
-
Если ваша стратегия рассчитывает абсолютные пороги (например: «покупать, если волатильность в долларах превысила $15» или использует фиксированный шаг сетки ордеров), вся динамика до сплита оказывается искажена масштабом будущего сплита.
-
Еще хуже ситуация с дивидендами: дивидендная поправка уменьшает прошлые цены на величину будущих выплат, создавая искусственный повышательный тренд в далеком прошлом.
Реальная история цен (Unadjusted): 2023: $900 -> $950 -> $1000 ---[Сплит 10:1 в 2024]--- 2024: $105 -> $110 Ретроспективно скорректированный ряд (Adjusted): 2023: $90 -> $95 -> $100 ------------------------ 2024: $105 -> $110
Если алгоритм использует не нормированные индикаторы или оптимизирует размеры позиций в долларах, он обучается на рядах, масштаб которых сформирован будущими корпоративными событиями.
3. Stale Prices и фантомный Mean-Reversion
Если бумага обладает низкой ликвидностью (акции третьего эшелона, корпоративные облигации, внебиржевые деривативы), реальных сделок по ней может не быть часами.
Провайдеры данных в таких случаях часто делают Forward Fill — копируют последнюю известную цену в каждый следующий минутный бар.
В датасете появляется цепочка: 100.2, 100.2, 100.2, 100.2, 100.2, 100.2, 103.5
Алгоритм технического анализа видит в этом:
-
Серию баров с нулевой волатильностью.
-
Внезапный скачок (Z-score скачка резко возрастает).
-
Математическую иллюзию сильной отрицательной автокорреляции доходностей.
Статистические тесты выявляют в таких рядах мощный сигнал к «возврату к среднему» (mean-reversion). Стратегия открывает шорт на скачке, рассчитывая на откат. В реальном стакане никакого отката нет — просто через два часа отсутствия торгов прошла первая реальная рыночная сделка.
4. Выпадение тиков в моменты рыночных шоков
Рыночные данные теряются не равномерно. В спокойные дни сеть и шлюзы брокеров справляются со 100% потока. Но когда выходит решение ФРС по ставке или начинается обвал (VIX > 30), частота тиков взлетает на порядок.
Буферы сетевых карт переполняются, брокерские FIX-шлюзы начинают сбрасывать пакеты. В результате именно на самых резких и критических свечах выпадает до 2–5% тиков.
Если расчет волатильности (например, realized variance по тикам или индикатор Parkinson) строится на прореженном потоке, он занижает оценку риска именно в момент катастрофы. Стратегия риск-менеджмента считает, что риск умеренный, и оставляет кредитное плечо максимальным ровно тогда, когда позицию нужно экстренно сокращать.
5. Рассинхрон таймзон и биржевых календарей
Типичный сценарий в кросс-рыночных и статистических арбитражных стратегиях: модель торгует пару европейского и американского активов (например, фьючерс на DAX и фьючерс на S&P 500, или кросс-листинг акции в Лондоне и Нью-Йорке).
Где здесь заложена мина:
-
Переход на летнее/зимнее время (DST). США и Европа переходят на летнее время в разные недели марта и октября. В течение 2–3 недель разница во времени между Лондоном и Нью-Йорком составляет не 5 часов, а 4 или 6 часов.
-
Биржевой календарь. В Великобритании банковский выходной (Bank Holiday), а в США обычный торговый день. Если в скрипте объединить два датафрейма по индексу времени через
inner joinили без строгого маппинга сессий, бары закрытия Нью-Йорка склеятся с барами середины дня Лондона. -
Модель начинает генерировать сигнал на основе данных, которые на самом деле поступили на 2 часа позже. В бэктесте — фантастический арбитраж. В проде — моментальный убыток.
6. As-Reported vs. As-Restated (фундаментальные данные)
Если ваша стратегия опирается на фундаментальные мультипликаторы (P/E, Debt/EBITDA, чистая прибыль), возникает фундаментальная временная ловушка:
-
Финансовый квартал компании завершился 31 декабря.
-
Отчет по форме 10-K компания подала в SEC только 15 марта следующего года.
-
Если в бэктесте привязать прибыль за Q4 к дате
31 декабря, вы заглянули в будущее на 2.5 месяца вперед. -
Более того: через 9 месяцев компания может выпустить ревизию отчета (restatement), списав убытки прошлых периодов. Базы данных ретроспективно заменяют старую цифру на новую. В бэктест попадает очищенная цифра, о которой инвесторы узнали лишь год спустя.
7. Bid-Ask Bounce и иллюзия мгновенной ликвидности (Microstructure Bias)
Большинство розничных и даже институциональных бэктестеров используют ряды цен последних сделок (Last Trade / Close) или середину спреда (Midpoint), полностью игнорируя реальную ширину спреда и структуру биржевого стакана.
В реальном рынке сделки поочередно проходят то по цене покупки продавца (Bid), то по цене предложения покупателя (Ask). Это порождает так называемый Bid-Ask Bounce (отскок спреда):
-
Если на рынке нет никакого реального движения фундаментальной стоимости, но одна сделка прошла по Ask ($100.05), а следующая по Bid ($100.00), математика доходностей фиксирует волатильность и отрицательную автокорреляцию первого порядка.
-
Алгоритмы статистического арбитража или маркет-мейкинга на исторических данных видят в этом «прибыльные колебания» и рассчитывают забирать с рынка по 3–5 базисных пунктов на каждом микроцикле.
Главный самообман бэктеста:
В симуляции алгоритм наивно «покупает» по $100.00 и «продает» по $100.05 на одном и том же баре. Но в реальности, чтобы совершить сделку немедленно, вы обязаны пересечь спред (cross the spread): купить по Ask ($100.05) и продать по Bid ($100.00). Если полуспред составляет 4 базисных пункта, а ваше расчетное математическое ожидание альфы — 3 базисных пункта, то каждая сделка в реальном стакане приносит вам чистый гарантированный убыток в 1 б.п. В бэктесте же этот спред ошибочно капитализируется в вашу «прибыль».
Кроме того, при использовании пассивных лимитных ордеров возникает эффект неблагоприятного отбора (Adverse Selection): Лимитная заявка на покупку исполняется только тогда, когда цена идет вниз сквозь ваш уровень (вы покупаете актив, который прямо сейчас падает). Если же цена разворачивается и улетает вверх, ваша заявка остается стоять в очереди неисполненной. Бэктест, не моделирующий приоритет очереди в стакане, считает, что исполнились 100% ордеров, завышая реальную результативность в разы.
Сколько это стоит: количественная оценка ущерба
Мы взяли простую, классическую стратегию импульса (20-day Momentum, Long-only, топ-50 акций из S&P 500, дневной ребаланс) на периоде 2020–2025 гг. и последовательно внедрили в чистый датасет типичные рыночные артефакты:
|
Состояние данных |
Sharpe Ratio |
Max Drawdown |
Годовая доходность |
Примечание |
|---|---|---|---|---|
|
Идеально чистый Point-in-Time датасет |
1.09 |
-18.4% |
12.8% |
Реальный бенчмарк стратегии |
|
+ Застрявшие цены (Stale prices, forward-fill) |
1.16 |
-17.1% |
13.9% |
Искусственное сглаживание волатильности |
|
+ Неотфильтрованные ценовые спайки (3σ) |
1.29 |
-14.2% |
15.6% |
Ложные импульсные входы на аномалиях |
|
+ Заглядывание через Adjusted-сплиты |
1.34 |
-12.8% |
16.9% |
Неявное знание масштаба цен |
|
+ Ошибка выжившего (только текущие тикеры) |
1.42 |
-10.9% |
18.7% |
Исключение делистингованных банкротов |
|
Суммарный «нарисованный» прирост |
+0.33 |
+7.5 п.п. |
+5.9 п.п. |
Иллюзия превосходной стратегии |
Разница между Шарпом 1.09 и 1.42 — это грань между умеренной рабочей стратегией и «звездным» квант-фондом, в который инвесторы готовы нести сотни миллионов. Но этот избыточный Шарп — целиком артефакт грязных данных.
Архитектурное решение: Bitemporal Data Model
Чтобы гарантировать отсутствие заглядывания в будущее, финансовая база данных должна быть битемпоральной. Каждое наблюдение обязано иметь две временные метки:
-
Event Time (Valid Time): момент времени, к которому событие относится в реальном мире (например, минута закрытия биржевой свечи
2024-03-15 15:59:00 UTCили отчетный период2023-Q4). -
Knowledge Time (System/Transaction Time): точный момент, когда ваша система физически получила и зафиксировала эту информацию (например, пакет пришел по сети
2024-03-15 15:59:00.142 UTC, а отчет SEC стал доступен2024-03-28 10:14:02 UTC).
Любой бэктест на момент времени имеет право запрашивать данные только с фильтром:
Если знание о событии появилось в системе в , алгоритм на шаге
не имеет к нему доступа.
Реализация на Python: пайплайн очистки данных на Polars
Для высокоскоростной обработки временных рядов мы используем Polars. Он на порядок быстрее Pandas на миллионах строк и предоставляет строгую семантику работы со временем.
Ниже два модуля производственного пайплайна: детекция аномалий (спайки и stale prices) и Point-in-Time корректировка корпоративных событий.
1. Детектор спайков и застрявших цен (QC Pipeline)
import polars as plimport numpy as npdef detect_market_data_anomalies( df: pl.DataFrame, price_col: str = "close", stale_limit: int = 5, spike_sigma: float = 3.5,) -> pl.DataFrame: """ Выявляет спайки (fat fingers / сбои фида) и залипшие цены (stale prices). Ожидает колонки: ['timestamp', 'ticker', price_col]. """ return ( df.sort(["ticker", "timestamp"]) .with_columns([ # 1. Проверка на stale prices: считаем количество одинаковых цен подряд (pl.col(price_col) == pl.col(price_col).shift(1)) .cast(pl.Int32) .alias("is_identical"), # Логарифмическая доходность текущего бара (pl.col(price_col).log() - pl.col(price_col).shift(1).log()) .alias("log_ret"), ]) .with_columns([ # Скользящее среднее и стандартное отклонение доходности за 30 баров pl.col("log_ret").rolling_mean(window_size=30).over("ticker").alias("roll_mean"), pl.col("log_ret").rolling_std(window_size=30).over("ticker").alias("roll_std"), # Накопительный счетчик подряд идущих идентичных цен pl.col("is_identical") .rolling_sum(window_size=stale_limit) .over("ticker") .alias("stale_run_len"), ]) .with_columns([ # Флаг спайка: скачок превышает N сигм от локальной волатильности ( (pl.col("log_ret") - pl.col("roll_mean")).abs() > (spike_sigma * pl.col("roll_std").fill_null(1.0)) ).alias("is_price_spike"), # Флаг залипания цены (pl.col("stale_run_len") >= stale_limit).alias("is_stale_sequence"), ]) .drop(["is_identical", "log_ret", "roll_mean", "roll_std", "stale_run_len"]) )
2. Point-in-Time корректировка сплитов без заглядывания в будущее
Вместо того чтобы масштабировать историю назад в прошлое, правильный Point-in-Time бэктест хранит сырые цены (Raw Prices) и применяет накопленный коэффициент сплита только к размеру удерживаемой позиции (Position Shares) на момент наступления даты экс-дивиденда:
from dataclasses import dataclassfrom datetime import datetime@dataclassclass SplitEvent: effective_date: datetime ratio: float # Например, 10.0 для сплита 10 к 1def point_in_time_position_adjustment( positions_df: pl.DataFrame, splits_df: pl.DataFrame,) -> pl.DataFrame: """ Корректирует количество удерживаемых акций без искажения исторических цен. Все расчеты цен остаются в координатах того торгового дня. """ # Связываем позиции с корпоративными событиями строго по дате наступления joined = positions_df.join( splits_df, left_on=["ticker", "timestamp"], right_on=["ticker", "effective_date"], how="left" ).with_columns( pl.col("ratio").fill_null(1.0) ) # Пересчитываем размер позиции: при сплите 10:1 количество акций умножается на 10, # а цена входа (entry_price) делится на 10 строго с момента события. return joined.with_columns([ (pl.col("shares") * pl.col("ratio")).alias("shares_adjusted"), (pl.col("entry_price") / pl.col("ratio")).alias("entry_price_adjusted"), ])
3. Моделирование пересечения спреда (Crossing the Spread)
Чтобы исключить эффект Bid-Ask Bounce и ложную арбитражную доходность, сделки на покупку должны исполняться по цене Ask, а сделки на продажу — по цене Bid с учетом комиссии:
def apply_microstructure_execution( signals_df: pl.DataFrame, quotes_df: pl.DataFrame, fee_bps: float = 1.0,) -> pl.DataFrame: """ Моделирует исполнение ордеров против противоположной стороны стакана. Входные сигналы: +1 (Buy), -1 (Sell), 0 (Hold). Исключает иллюзию бесплатного исполнения по Midpoint/Close. """ return ( signals_df.join(quotes_df, on=["ticker", "timestamp"], how="inner") .with_columns([ pl.when(pl.col("signal") > 0) .then(pl.col("ask_price") * (1.0 + fee_bps / 10000.0)) .when(pl.col("signal") < 0) .then(pl.col("bid_price") * (1.0 - fee_bps / 10000.0)) .otherwise(None) .alias("fill_price") ]) )
Чек-лист проверки бэктеста перед запуском капитала
Перед тем как отправлять стратегию на реальный счет, прогоните ее через этот проверочный список:
-
Вселенная активов динамическая? Модель формирует список доступных для покупки тикеров заново на каждый шаг времени
, используя только те компании, которые реально торговались на бирже в этот день?
-
Как обработаны делистинги? Если акция обанкротилась в 2022 году, как бэктест закрывает позицию? По последней цене перед остановкой торгов или с учетом ликвидационного дисконта?
-
Разделены ли Event Time и Knowledge Time? Доступны ли квартальные отчеты строго после даты их фактической публикации в регуляторный орган (SEC EDGAR / Интерфакс-ЦРКИ), а не с даты окончания отчетного квартала?
-
Учитывается ли спред стакана (Bid/Ask)? Симуляция покупает по
Askи продает поBid, исключая искусственный отскок спреда (Bid-Ask Bounce)? Ограничен ли объем 1–5% от реального объема торгов за бар (Participation Rate)? -
Исключены ли бары с нулевым объемом из расчета волатильности? Проверены ли ряды на залипшие цены (stale prices)?
-
Синхронизированы ли часовые пояса с учетом перехода на летнее время? Все временные метки приведены к единому UTC, а торговые часы привязаны к локальным биржевым календарям конкретной юрисдикции?
-
Тестировалась ли задержка исполнения (Latency)? Заложена ли в бэктест задержка между генерацией сигнала на закрытии бара и выставлением ордера (минимум 50–200 мс для прямого подключения или 1 бар для консервативной оценки)?
Заключение
В современной индустрии количественных финансов конкурентное преимущество сместилось. Продвинутые нейросетевые архитектуры и библиотеки машинного обучения сегодня доступны всем в open-source за пару кликов.
Реальное преимущество и стабильный Sharpe строятся на фундаментальном уровне: на чистоте, связности и строгой темпоральной валидации данных.
Если вы сталкивались с необъяснимым расхождением бэктеста и продакшна — проверьте данные по чеклисту выше. В большинстве случаев разгадка кроется в первых 100 миллисекундах истории котировок.
ссылка на оригинал статьи https://habr.com/ru/articles/1089618/