3 Сентября 2026 · инженерия данных

Несуществующие минуты: пропуски, остановки торгов и бары с нулевым объёмом в истории OHLCV

Несуществующие минуты: пропуски, остановки торгов и бары с нулевым объёмом в истории OHLCV

В календарном году должно быть 525,600 минутных баров с интервалом 1 минута. Наша выгрузка бессрочных фьючерсов BTCUSDT за 2025 год вернулась с 525,557 строками — на 43 минуты меньше, полнота составила 99.992%. По бессрочному фьючерсу на альткоин средней капитализации с той же площадки, из той же выгрузки и по тому же коду не хватало 1,247 строк: полнота — 99.76%. А в минутном ряду акций США за тот же год было примерно на 427,000 строк меньше, чем в крипторяду. Это вовсе не пропуски, просто рынок закрывается.

Три из этих чисел ничем не примечательны. Число, за которым стоит целая история, — 43.

525,600минутных баров в невисокосном году
0.008%пропусков по BTCUSDT за 2025 год
61%этих минут пришлись на часы с волатильностью из верхнего дециля

Четыре разных явления, которые выглядят как дыра в dataframe

Прежде чем перейти к 43 минутам, разберём типы пропусков: большинство инструментов для их обработки дают сбой уже на этом уровне. Если строки нет в вашем локальном parquet-файле, вы не знаете, что из перечисленного произошло, а правильные действия в каждом случае разные.

ТипЧто произошло на самом делеКак это выглядит в данныхЧто делать
Нет сделокРынок был открыт, но в эту минуту никто не пересёк спредБар с нулевым объёмом (OHLC равны, trades=0) на одних конечных точках; на других строки нетСохранить. Это реальная информация: никто не хотел торговать.
Площадка недоступнаСистема сведения заявок не работает, планово или нетСтроки нет, иногда пропуски идут сериейПометить данные как устаревшие. Не торговать в этот период.
Торги инструментом приостановленыВыход за пределы диапазона LULD, ожидание новостей, уведомление о делистингеСтроки нет, затем появляется сделка аукциона возобновления торговПометить данные как устаревшие и считать возобновление торгов разрывом.
Ошибка с вашей стороныОграничение частоты запросов при постраничной загрузке, повтор запроса с потерянной страницей, ошибка часового пояса в циклеСтроки нет, и это неотличимо от случаев вышеОбнаружить и загрузить повторно. Это единственную категорию можно исправить.

На разных площадках по-разному обрабатывается первый случай в таблице, и именно здесь кроется проблема. Конечная точка свечей Coinbase полностью пропускает пустые интервалы, поэтому история неликвидной пары усеяна настоящими дырами. Kline Binance обычно выдаёт синтетический бар с объёмом 0, где open=high=low=close и цена зафиксирована на уровне последней сделки. Факт один и тот же, а данные выглядят по-разному. Загрузчик, который переиндексирует ряд на сетку с минутным шагом, превращает один вариант в другой, ничего не сообщая. Сначала выясните, как ведёт себя ваша площадка, и только потом пишите инструмент для заполнения пропусков.

Почти все 1,247 пропущенных минут на альткоине относились к первой категории: в воскресенье в 04:00 UTC стакан был пуст, никто не торговал. Раздражает, но легко объяснить, и в целом безвредно, потому что стратегия всё равно не стала бы тогда торговать. Именно поэтому я перестал разбирать этот случай и вернулся к 43 минутам.

43 минуты не были разбросаны по году

Если бы пропуски распределялись равномерно, 43 минуты за год превратились бы в 43 одиночных случая — по одному раз в восемь с половиной дней, каждый из которых можно было бы списать на погрешность. Но мы увидели не это. Пропуски пришлись на 6 серий: одна длиной 19 минут подряд, одна — 11, две — по 4, и ещё несколько пар. Шесть событий, а не 43 случайности.

И эти события связаны с тем, что для нас важно. Площадки выходят из строя под нагрузкой, а нагрузка растёт, когда движется цена. Я разбил каждый час года по реализованной волатильности и проверил, на какие часы пришлись пропущенные минуты: 61% из них попали в верхний дециль. Безусловная вероятность пропуска любой отдельно взятой минуты — 0.008%. Если же минута приходится на час из верхнего дециля волатильности, вероятность составляет примерно 0.05% — в 6 раз выше, причём пропуски ещё и сгруппированы.

Получается, метрика полноты на вашей панели качества данных измеряет не то, что нужно. 99.992% выглядит так, будто о наборе данных можно больше не думать. На деле это означает, что ряд полон в часы, когда стратегия бездействует, и дыряв именно в те часы, когда она работает на полную. У momentum-системы, которая срабатывает при росте волатильности, вероятность наткнуться на пропуск существенно выше, чем подсказывает общая цифра. И происходит это прямо во время сделки.

Что заполнение предыдущими значениями делает уже через 3 строки кода

Вот сбой, из-за которого я и написал этот текст. Возьмём серию из 19 минут. Стандартная подготовка данных: переиндексируем ряд на полную минутную сетку, заполним OHLC последними значениями, объём установим в 0. Теперь ряд непрерывен, а индикаторы рассчитываются без единого NaN.

У этих 19 баров high == low == close. Истинный диапазон каждого из них равен 0. ATR(14), рассчитанный на этом участке после значения около 240 USDT до сбоя, к моменту восстановления площадки снизится примерно до 34: всё среднее теперь определяется 5 настоящими барами, пережившими сбой. Передайте это в инструмент расчёта размера позиции с учётом волатильности, стандартный size = risk_budget / ATR типа. Размер позиции вырастет в 7 раз.

Следующий настоящий бар — сделка при возобновлении торгов, и тихим его не назовёшь. В нашем случае открытие произошло на расстоянии 1.8% от последнего значения цены до сбоя. Бэктест без вопросов открыл позицию в 7 раз больше обычной прямо перед разрывом в 1.8%, по цене, по которой исполнение было невозможно и которую никто не выставлял. Эта единственная синтетическая сделка принесла на кривой капитала больше, чем месяц реальной прибыли, — в неправильном направлении. И появилась она исключительно из-за строки очистки данных, добавленной ради аккуратного dataframe.

Удалить строки вместо заполнения — тоже не выход: это та же ошибка, только в другой маскировке. После удаления индексированные целым числом ретроспективные окна начинают врать: «EMA на 20 баров» охватывает 39 минут реального времени с учётом простоя, доходность между соседними барами на границе пропуска учитывает весь скачок в 1.8% как движение за 1 минуту, а любая оценка волатильности на бар считает его событием в 60 сигм. Ничто об этом не предупреждает. Индекс по-прежнему монотонный.

При ресэмплинге проблема становится невидимой

Большинство исследований работает не с минутными барами с интервалом 1 минута, а с агрегированными данными, и агрегация заметает проблему под ковёр. При ресэмплинге до 5 минут пропуск длиной 19 минут превращается в 4 бара, из которых первый и последний охватывают неполный интервал. Pandas рассчитает OHLC, на первый взгляд вполне правдоподобный, по двум уцелевшим минутам и пометит его так же, как бар, построенный по 5 минутам данных. В результате ничто не позволит их различить.

Самое простое решение из известных мне — переносить столбец bars_in_window через каждый этап ресэмплинга и никогда его не удалять. Одного целого числа в каждой строке достаточно, чтобы отвечать на любой последующий вопрос о надёжности бара. Мы также сохраняем seconds_since_last_real_print — ту же информацию в формате, с которым может работать слой исполнения.

Наша политика, насколько она вообще сформировалась

Теперь наши агенты действуют так:

  1. Никогда не переиндексировать без предупреждения. Загрузчик создаёт манифест пропусков: начало, конец, длина и предполагаемая категория из четырёх. Если серия пропущенных минут короче 3 баров, а объём в соседних барах небольшой, это минута без сделок. Всё, что длиннее и приходится на активные часы, считаем сбоем, пока не доказано обратное.
  2. Сначала загрузить повторно, потом делать выводы. Половина ранних пропусков оказалась ошибками постраничной загрузки. Второй запрос к другой конечной точке или другому поставщику снимает вопрос о четвёртой категории и сокращает число проблем ещё до начала анализа.
  3. Контроль давности данных вместо заполнения пропусков. Стратегия получает на вход data_age и жёсткое правило: не открывать новые позиции, если последний реальный принт старше N баров; открытые позиции при возобновлении торгов закрывать только рыночным ордером с явной поправкой на риск разрыва. Несуществующие исполнения хуже, чем несостоявшиеся сделки.
  4. Для индикаторов — NaN, а не выдуманные данные. Цены, протянутые вперёд, никогда не попадают на слой признаков. Если ATR рассчитать нельзя, его значение не определено, а значит, позиция должна быть нулевой. Явная ошибка лучше тихого увеличения размера в 7 раз.
  5. Показывать результаты с учётом близости к сбоям. В каждом публикуемом нами бэктесте прибыль и убыток показываются как с учётом сделок вблизи сбоев, так и без них — вместе с общим результатом. Если именно эти сделки приносят прибыль, результат объясняется артефактом данных.

Быстрая проверка, которую можно провести сегодня: сгруппируйте пропущенные минуты в последовательные серии и проверьте, какая доля сделок в бэктесте открывается или закрывается в пределах 30 минут от границы серии. Если доля меньше 1%, пропуски, скорее всего, ни на что не влияют. Если она составляет 5% или больше, кривая капитала отчасти отражает простои биржи.

Я научился доверять скорее характеру пропусков, чем их числу. Набор данных с тысячами отдельных дыр в часы затишья обычно не вызывает проблем. Если же пропуски собраны в несколько плотных серий, значит, под нагрузкой что-то ломается. А именно под нагрузкой и работает ваша стратегия.

пропуски OHLCVинженерия данныхбэктестингресэмплингкриптофьючерсы
ПоделитьсяXLinkedInFacebookRedditHacker NewsWhatsAppTelegramEmail
← Все статьи