h5i-dbで学ぶバックテスト用ポイントインタイムデータ
ポイントインタイムデータで先読みバイアスを防ぐ方法を解説します。h5i-dbの全書き込みのバージョン管理と、提出書類データの遅延計測を紹介します。
ポイントインタイムデータとは、過去のある日にデータセットが保持していた内容を記録したものです。バックテストでは、結果が検証可能なものか、気付かないうちに翌日のデータを読み込んだものかを分けます。h5i-dbは、Rustで記述され、Python APIを備えた新しいオープンソースの時系列データベースです。すべての書き込みを番号付きバージョンとして保存し、任意の読み取りで過去のバージョンを指定できます。以下では、実際の提出書類データで計測した、バージョン指定によって防げるデータリーケージを示します。続いて、ノートパソコンで実行できるシナリオを紹介します。
バックテストにおけるpoint-in-timeデータとは何か
市場データの各事実には、二つのタイムスタンプがあります。イベント時刻は、その事象が発生した時点です。到着時刻は、報告した企業の外部にいる人が、その情報を知り得るようになった時点です。四半期保有銘柄報告書は、四半期最終日時点の保有状況を示しますが、公表されるのは数週間後です。そのため、イベント時刻だけでデータを結合するモデルには、当時誰も知らなかった情報が渡されます。
この時間差は測定できます。機関投資家の運用会社は各四半期の終了後にForm 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_end2026-06-30終了四半期について、10688の提出書類は、対象期間の終了から平均34.1日後に到着しました。このパネルでは、同じ測定を16四半期にわたって繰り返しています。運用会社は提出後かなり時間がたってから報告書を訂正することもあります。そのため、過去の日付を示す記録は、その日付が過ぎた後も変わり続けます。
先読みバイアスはデータ保存の問題です
バックテストにおける先読みバイアスの解説では、情報漏洩を規律の問題として扱います。すべての特徴量にラグを設け、公開日を守ります。ただし、誰かが忘れれば、その規律は機能しません。しかも失敗は静かに起こります。情報が漏洩したバックテストでは、エラーが表示されないまま、より高いSharpeレシオが出力されます。
ポイントインタイム保存は、この保証を一段下の層に移します。戦略に渡すデータフレームが、特定のバージョンに固定された読み取りから作られていれば、そのバージョンの後に書き込まれた行は、戦略コードがその後どのように動作してもデータフレームに入りません。確認作業はコードレビューではなく、読み取り処理の性質になります。
配当は、別の側面からこの時間差を示します。現金配当は先に発表され、その後に権利落ち日を迎えます。一方、当日に読み込んだテーブルには、対象となるすべての支払いについて、この両方の日付が記録されています。シミュレーション対象日にまだ発表されていなかった支払いも含まれます。
各数値の背後にある正確な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 month2026-07-01で始まる月では、発表日は権利落ち日の平均88.2日前でした。このパネルは、同じ測定について24カ月分をカバーしています。この発表日と権利落ち日の間にあるシミュレーション日で、最新の配当テーブルを読み込むと、発表前であるはずの数週間前から、その支払いがすでに記録されています。
保存済みの履歴は書き換えられる
遅延は、障害の一つです。もう一つは、訂正です。コーポレートアクションは、すでに成立した価格を書き換えます。4対1の株式分割後は、調整後の系列における過去の価格がすべて四分の一になります。そのため、当日ダウンロードした系列は、トレーダーが見ていた売買記録と一致しなくなります。分割調整後の価格履歴に関する当社の解説では、計算方法を確認しています。ここで重要なのは頻度です。
各数値の背後にある正確な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_date2026-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ヘッドラインは、ニューヨーク時間の一日24時間を通じて配信されます。直近90日間では、09:00台に790本の記事があり、20:00台には404本ありました。夜間のヘッドラインをその日の16:00の終値に付与すると、引けで取引する戦略に数時間分の後知恵を与えることになります。
ある時点のシナリオを実行する
ここではPythonパッケージだけを使用します。プロジェクトにはRust製のコマンドラインツールも含まれますが、別途インストールが必要で、この手順では使用しません。サンプルデータはダウンロードせず、ローカルで生成します。
- 固定されたリリースをインストールします。
pip install 'h5i-db==0.1.6'は2026年8月4日に公開されています。Python 3.9以降が必要で、pyarrow>=14も導入されます。ビルド済みwheelは、x86-64およびarm64のLinux、Appleシリコン搭載macOS、x86-64のWindowsに対応しています。 import pyarrow as paとimport pyarrow.parquet as pqの後に、データを一度だけ記述します。schema = pa.schema([('ts', pa.timestamp('us', tz='UTC')), ('symbol', pa.string()), ('price', pa.float64())])。pq.write_table(pa.table({'ts': [d1, d2], 'symbol': ['ACME', 'ACME'], 'price': [10.0, 10.5]}, schema=schema), 'day1.parquet')を使って、架空の二行をローカルファイルに書き込みます。d1とd2はタイムゾーン対応のdatetimeです。- データベースとテーブルを作成し、時刻列に名前を付けます。
db = h5i_db.Database('pit.db', create=True)、続いてdb.create_table('prices', schema, time_column='ts')です。 - キーを付けてファイルを取り込みます。
db.append('prices', pq.read_table('day1.parquet'), idempotency_key='load-day1')。この呼び出しは、実行したコミットを返します。 - 同じ行をもう一度実行します。同じキーを付けて再実行すると、プロジェクトの仕様上、すでに生成したコミットを見つけ、二度目の書き込みは行わず、
"segments_added": 0を付けてそのコミットを返します。再試行の前後でdb.versions('prices')を出力し、バージョン一覧が変わらないことを確認します。 idempotency_key='load-day2'の下で二日目のデータを取り込み、その後、両日をまたいでクエリを実行します。db.sql('SELECT symbol, count(*) AS n, avg(price) AS px FROM prices GROUP BY symbol').to_pandas()。- 二日目のデータが取り込まれる前の状態でテーブルを読み取ります。
db.read('prices', version=1)。同じ処理について、このメソッドはas_of=とsnapshot=の引数も受け取ります。
特に確認すべきなのはステップ6です。重複した追加処理がエラーを返すことはありません。その時点からテーブルの内容が誤った状態になり、その後のすべての実行がその損傷を気付かないまま引き継ぎます。ステップ8がこの仕組みの利点です。3月に計算した数値を、同じ固定バージョンから8月に再計算できます。これは、フレームワークレベルで再現可能なバックテストを主張する記事が重視する特性です。
プロジェクトの主張と確認した内容
READMEの冒頭には、次のベンチマーク結果が示されています。
2,000万行のOHLCV+VWAPローリング集計で、DuckDBとPolarsより4.5倍超高速
この数値は、2026年8月時点で取得したh5i-db READMEから引用した、プロジェクト独自の測定結果です。私たちは測定を再現しておらず、上記の内容もこの数値には依存していません。
ここでは、速度より成熟度が重要です。執筆時点でリポジトリのスター数は29件で、Apache-2.0ライセンスの下、バージョンは0.1.6です。この組み合わせは、保守担当者が少なく、ポイントリリース間でもAPIが変更される可能性があることを示します。高負荷環境でこのエンジンが稼働してきたことを示す、長期にわたる公開実績もありません。正確なバージョンを固定し、データベースに取り込んだparquetファイルを保管しておけば、リリースによって挙動が変わった場合に元へ戻す手段を確保できます。自分で生データのコピーを保管することも、ベンダーがその裏で過去の履歴を作り替えるリスクに備える同じ習慣です。これは、株価データにおけるsurvivorship biasをめぐる本稿のテーマでもあります。
よくある質問
point-in-timeデータとは何ですか?
point-in-timeデータとは、各事実が判明した時点のタイムスタンプとともに保存されたデータセットです。これにより、クエリで過去の任意の日に見えていた情報を再構成できます。単純な「最新値」テーブルでは、過去の値が現在の修正値で上書きされるため、これを実現できません。
バージョン管理された保存で先読みバイアスはなくなりますか?
いいえ。バージョン管理で防げるのは、シミュレーション上の意思決定時点より後に書き込まれた値を処理が読み込むという種類の情報漏洩です。特徴量の作成では、別の形で情報が漏れる可能性があります。例えば、標本全体の履歴から計算した統計値で標本をスケーリングする場合です。
取り込み時の冪等性キーは何をしますか?
書き込みにラベルを付け、再試行された書き込みを同一のものとして認識できるようにします。これにより、クラッシュ後にローダーを再実行しても同じ行が二重に追加されません。二重追加は、テーブルを気付かないうちに不正確な状態にする障害です。
h5i-dbは本番環境で利用できる状態ですか?
執筆時点で、h5i-dbはバージョン0.1.6、GitHubで29 stars、ライセンスはApache-2.0です。この規模の初期段階のソフトウェアには、APIの変更が多く、公開実績も限られている可能性があります。評価をいつでも取り消せる状態に保つには、バージョンを固定し、ソースファイルを自分で複製しておくことが重要です。
上記の各パネルには、その結果を生成したSQLが付属しています。パネルを開き、数値の集計方法を確認してください。同じ質問は、Strasmore terminalで平易な英語を使って尋ねることもできます。