Почему ваш бэктест врёт: 7 ловушек грязных рыночных данных и как написать Point-in-Time пайплайн на Python

Вы написали торговую стратегию на 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 pl
import numpy as np
def 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 dataclass
from datetime import datetime
@dataclass
class SplitEvent:
effective_date: datetime
ratio: float # Например, 10.0 для сплита 10 к 1
def 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 миллисекундах истории котировок.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.