Strasmore Research
Обучение Matt ConnorАвтор: Matt Connor · data as of August 16, 2026 · refreshed weekly

h5i-db: данные point-in-time для надежных бэктестов

Использование point-in-time данных исключает ошибки заглядывания в будущее при тестировании стратегий. Узнайте, как версионность записей в h5i-db защищает точность ваших моделей.

Данные Point-in-time и проблема утечки информации

Данные point-in-time представляют собой фиксацию состояния набора данных на конкретную прошедшую дату. При проведении бэктестинга такие данные позволяют отделить обоснованный результат от того, который был получен с использованием «заглядывания в будущее». База данных h5i-db — это молодой проект с открытым исходным кодом для работы с временными рядами. Она написана на языке Rust и имеет API для Python. Система сохраняет каждую операцию записи как пронумерованную версию, что позволяет при чтении обращаться к любому предыдущему состоянию. Ниже описана утечка данных, которую предотвращает использование версионности, на примере реальных отчетных данных, а также сценарий, который можно воспроизвести на обычном ноутбуке.

2026-07-01 Механика утечки данных

В финансовом моделировании утечка данных происходит, когда алгоритм получает доступ к информации, которая на момент принятия решения еще не была публично доступна. Классический пример — использование данных из квартального отчета, который был опубликован в мае, для оценки стратегии за апрель. Если база данных не поддерживает point-in-time, запрос к ней вернет текущее значение показателя, даже если вы запрашиваете данные за прошлый период. В результате модель «видит» будущее, что приводит к завышенным показателям доходности, которые невозможно повторить в реальной торговле.

Решение через версионность

Система h5i-db решает эту проблему за счет хранения истории изменений. Каждая запись в базе получает уникальный номер версии. При выполнении бэктеста исследователь может зафиксировать (pin) состояние базы на конкретный момент времени. Это гарантирует, что все расчеты будут опираться исключительно на те данные, которые были известны рынку на тот момент.

Практический сценарий

Для демонстрации работы системы можно использовать следующий алгоритм:

  • Загрузите набор данных с историей подачи отчетности компаниями.
  • Выполните серию операций записи в h5i-db, имитируя последовательное поступление данных.
  • Попробуйте извлечь показатели компании за прошлый период без фиксации версии — вы получите актуальные данные.
  • Повторите запрос, указав номер версии, соответствующий дате до публикации отчета.
  • Сравните результаты: разница между ними и есть объем «утечки», которую вы бы допустили при отсутствии версионности.

Заключение

Использование инструментов, поддерживающих point-in-time, является обязательным требованием для профессионального количественного анализа. Это позволяет исключить систематические ошибки в бэктестах и обеспечить достоверность результатов тестирования стратегий.

Что такое point-in-time данные в бэктесте?

Каждый рыночный факт имеет две временные метки. Время события — это момент, когда произошло само событие. Время поступления — это момент, когда информация стала доступна кому-либо за пределами отчитавшейся компании. Квартальный отчет о портфеле описывает позиции на последний день квартала, но становится публичным лишь спустя несколько недель. Если модель использует только время события, она получает доступ к данным, которыми в тот момент никто не обладал.

Этот временной разрыв поддается измерению. Институциональные управляющие подают форму 13F после закрытия каждого квартала. Нижеприведенная панель показывает количество дней между окончанием отчетного квартала и датой подачи формы.

ЗапросВремя публикации отчетов 13F
Точный SQL-код для каждого числа
WITH per_filing AS
(
    SELECT
        accession_number,
        any(toDate(parseDateTimeBestEffortOrNull(toString(period)))) AS period_end,
        any(toDate(filing_date))                                     AS filed_on
    FROM global_markets.stocks_13f_filings
    WHERE filing_date >= '2023-01-01'
    GROUP BY accession_number
)
SELECT
    toString(period_end)                                 AS period_end_date,
    countDistinct(accession_number)                      AS filings_count,
    round(avg(dateDiff('day', period_end, filed_on)), 1) AS avg_days_to_public
FROM per_filing
WHERE period_end IS NOT NULL
  AND filed_on >= period_end
  AND filed_on <= period_end + 400
GROUP BY period_end
HAVING filings_count >= 100
ORDER BY period_end
Run this yourself

Для квартала, завершившегося 2026-06-30, отчеты 10688 поступали в среднем через 34.1 дней после окончания отчетного периода. Панель отображает эти показатели для 16 кварталов. Управляющий также может внести изменения в отчет спустя долгое время после его подачи, поэтому данные, описывающие прошлые периоды, могут меняться даже после того, как эти даты остались в прошлом.

Ошибка «заглядывания в будущее» как проблема хранения данных

Наше руководство по ошибке «заглядывания в будущее» при бэктестинге рассматривает утечку данных как вопрос дисциплины: необходимо учитывать временной лаг для каждого фактора и соблюдать даты публикации. Дисциплина работает, пока кто-то не совершит ошибку, которая останется незамеченной. Бэктест с утечкой данных показывает более высокий коэффициент Шарпа, не выдавая при этом никаких ошибок.

Хранение данных с привязкой к моменту времени (point-in-time) переносит гарантию точности на уровень ниже. Когда набор данных, поступающий в стратегию, считывается из версии, зафиксированной на определенную дату, запись, добавленная после этой версии, не попадет в выборку, независимо от того, что делает код стратегии. Проверка перестает быть задачей ревью кода и становится свойством самого процесса чтения данных.

Дивиденды демонстрируют этот временной разрыв с другой стороны. Денежный дивиденд сначала объявляется, а затем наступает дата отсечки (ex-dividend date). Таблица, загруженная сегодня, содержит обе даты для каждой выплаты, включая те, которые еще не были объявлены на дату, используемую в симуляции.

ЗапросКоличество дней между объявлением дивидендов и экс-дивидендной датой, по месяцам
Точный SQL-код для каждого числа
SELECT
    toString(toStartOfMonth(ex_dividend_date))                         AS month,
    round(avg(dateDiff('day', declaration_date, ex_dividend_date)), 1) AS avg_days_announced_ahead,
    countDistinct(ticker)                                              AS payers_count
FROM global_markets.stocks_dividends
WHERE ex_dividend_date >= toStartOfMonth(today() - 730)
  AND ex_dividend_date <  toStartOfMonth(today())
  AND declaration_date >= '1990-01-01'
  AND declaration_date <= ex_dividend_date
GROUP BY month
ORDER BY month
Run this yourself

В месяце, начинающемся с 2026-07-01, объявления поступали в среднем за 88.2 дней до даты отсечки, а панель охватывает 24 месяцев аналогичных измерений. Если считать современную таблицу дивидендов на симулируемую дату внутри этого промежутка, информация о выплате уже будет присутствовать в данных за недели до того, как само объявление было сделано.

Пересмотр исторических данных

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

ЗапросДробление и консолидация акций по кварталам
Точный SQL-код для каждого числа
SELECT
    toString(toStartOfQuarter(execution_date))    AS quarter_start_date,
    countDistinctIf(id, split_to > split_from)    AS forward_splits,
    countDistinctIf(id, split_to < split_from)    AS reverse_splits
FROM global_markets.stocks_splits
WHERE execution_date >= toStartOfQuarter(today() - 1460)
  AND execution_date <  toStartOfQuarter(today())
  AND split_from > 0
  AND split_to   > 0
GROUP BY quarter_start_date
ORDER BY quarter_start_date
Run this yourself

В квартале, начавшемся 2026-04-01, вступили в силу 131 прямых дроблений и 303 обратных дроблений акций. Каждое из них пересчитывает ценовую историю, которая уже могла быть сохранена в кэше аналитической системы. Версионное хранение данных не предотвращает пересчёт. Оно фиксирует новое состояние как новую версию и сохраняет доступ к старой, что превращает устаревший результат в воспроизводимый.

Временные метки новостей содержат ту же ловушку в миниатюре.

ЗапросВремя выхода рыночных новостей по часам (Нью-Йорк)
Точный SQL-код для каждого числа
SELECT
    formatDateTime(toTimeZone(published_utc, 'America/New_York'), '%H:00') AS et_hour,
    countDistinct(id)                                                     AS articles
FROM global_markets.stocks_news
WHERE published_utc >= today() - 90
GROUP BY et_hour
ORDER BY et_hour
Run this yourself

Заголовки новостей выходят круглосуточно, в течение всех 24 часов торгового дня в Нью-Йорке. На девятый час приходилось 790 статей за последние девяносто дней, а на двадцатый час — 404. Присвоение вечернего заголовка закрытию дня в шестнадцать часов дает стратегии, торгующей на закрытии, преимущество в несколько часов «послезнания».

Сценарий point-in-time для запуска

Все действия выполняются в рамках пакета Python. Проект также включает инструмент командной строки на Rust — отдельную установку, которая не требуется для данного руководства. Пример данных генерируется локально, загрузка не требуется.

  1. Установите зафиксированный релиз: pip install 'h5i-db==0.1.6', опубликованный четвертого августа две тысячи двадцать шестого года. Требуется Python версии три и девять или новее, устанавливается pyarrow>=14. Готовые сборки (wheels) доступны для Linux на архитектурах x86-64 и arm64, а также для macOS на Apple silicon и Windows на x86-64.
  2. Опишите данные один раз после import pyarrow as pa и import pyarrow.parquet as pq: schema = pa.schema([('ts', pa.timestamp('us', tz='UTC')), ('symbol', pa.string()), ('price', pa.float64())]).
  3. Запишите две вымышленные строки в локальный файл с помощью pq.write_table(pa.table({'ts': [d1, d2], 'symbol': ['ACME', 'ACME'], 'price': [10.0, 10.5]}, schema=schema), 'day1.parquet'), где d1 и d2 — значения даты и времени с учетом часового пояса.
  4. Создайте базу данных и таблицу, указав имя столбца времени: db = h5i_db.Database('pit.db', create=True), затем db.create_table('prices', schema, time_column='ts').
  5. Загрузите файл под определенным ключом: db.append('prices', pq.read_table('day1.parquet'), idempotency_key='load-day1'). Вызов возвращает созданный коммит.
  6. Запустите эту же строку повторно. Документация проекта гласит, что при повторе с тем же ключом система находит уже созданный коммит и возвращает его с "segments_added": 0, вместо того чтобы записывать строки второй раз. Выведите db.versions('prices') до и после повторной попытки и убедитесь, что список версий не изменился.
  7. Загрузите данные за второй день под idempotency_key='load-day2', затем выполните запрос по обоим дням: db.sql('SELECT symbol, count(*) AS n, avg(price) AS px FROM prices GROUP BY symbol').to_pandas().
  8. Прочитайте состояние таблицы на момент до добавления данных за второй день: db.read('prices', version=1). Для этой задачи метод принимает аргументы as_of= и snapshot=.

На шестом шаге стоит остановиться подробнее. Повторяющееся добавление не вызывает ошибку. Оно оставляет таблицу в некорректном состоянии с этого момента, и каждый последующий запуск неявно наследует эту ошибку. Восьмой шаг — это основной результат: число, рассчитанное в марте, можно пересчитать в августе с использованием той же зафиксированной версии. Именно это свойство мы обосновываем на уровне фреймворка в нашем описании воспроизводимого бэктеста.

Заявления проекта и результаты нашей проверки

В файле README приводится сравнительный показатель:

более чем в четыре с половиной раза быстрее, чем DuckDB и Polars при выполнении агрегаций OHLCV+VWAP на массиве из двадцати миллионов строк

Эта цифра является собственным замером проекта, взятым из README h5i-db по состоянию на август две тысячи двадцать шестого года. Мы не проводили собственных тестов, и все вышеизложенное не зависит от этого показателя.

В данном случае зрелость продукта важнее скорости. На момент написания статьи репозиторий имел двадцать девять звезд и версию 0.1.6 под лицензией Apache-2.0. Такое сочетание указывает на ограниченный круг разработчиков и API, который может меняться между минорными релизами. Длительной истории работы движка под нагрузкой нет. Фиксация точной версии и сохранение файлов в формате parquet, которые использовались для загрузки в базу данных, позволяют вернуться к исходным данным, если поведение системы изменится после обновления. Хранение собственной копии исходных данных — это та же привычка, которая защищает от ситуации, когда поставщик данных меняет историю «задним числом»; эта тема проходит красной нитью через ошибку выжившего в биржевых данных.

Часто задаваемые вопросы

Что такое данные на момент времени (point-in-time)?

Данные на момент времени — это набор данных, хранящийся с метками времени, которые указывают, когда именно стал известен каждый факт. Это позволяет запросу восстановить картину, доступную на любую прошлую дату. Обычная таблица с «последними значениями» на это не способна, так как она перезаписывает прошлые данные сегодняшними скорректированными показателями.

Исключает ли версионное хранилище ошибку заглядывания в будущее (look-ahead bias)?

Нет. Версионирование устраняет один тип утечки данных, когда алгоритм считывает значения, записанные после момента принятия решения в модели. Утечка может произойти и при формировании признаков, например, при масштабировании выборки на основе статистики, рассчитанной по всей её истории.

Зачем нужен идемпотентный ключ при загрузке данных?

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

Готова ли h5i-db к промышленной эксплуатации?

На момент написания статьи это версия 0.1.6 с двадцатью девятью звездами на GitHub, распространяемая по лицензии Apache-2.0. Раннее программное обеспечение такого масштаба подвержено изменениям API и имеет ограниченную историю публичного использования. Фиксация версии и наличие собственной копии исходных файлов — это то, что позволяет сделать оценку воспроизводимой.


Каждая панель выше поставляется с SQL-запросом, который сформировал данные; откройте его, чтобы увидеть, как был произведен расчет. Те же вопросы можно задать на обычном английском языке в терминале Strasmore.