h5i-db: Data Point-in-Time untuk Backtest
Data point-in-time mencegah look-ahead bias pada backtest sejak di storage. Pelajari versioning setiap write di h5i-db dan dampak filing lag terhadap riwayat data.
Data point-in-time adalah catatan mengenai isi suatu dataset pada tanggal tertentu di masa lalu. Dalam backtest, data ini membedakan hasil yang dapat dipertanggungjawabkan dari hasil yang diam-diam menggunakan angka yang baru tersedia keesokan harinya. h5i-db adalah database time-series open-source yang masih baru, ditulis dalam Rust dengan API Python. Database ini menyimpan setiap penulisan sebagai versi bernomor dan memungkinkan setiap pembacaan dikunci pada versi sebelumnya. Di bawah ini adalah leakage yang dicegah oleh penguncian versi tersebut, diukur menggunakan data filing aktual, lalu diikuti skenario yang dapat dijalankan di laptop.
Apa yang dimaksud dengan data point-in-time dalam backtest?
Setiap fakta pasar memiliki dua timestamp. Event time adalah saat suatu peristiwa terjadi. Arrival time adalah saat informasi tersebut dapat diketahui oleh pihak mana pun di luar perusahaan yang melaporkannya. Laporan kepemilikan kuartalan menggambarkan posisi yang dimiliki pada hari terakhir suatu kuartal, tetapi baru dipublikasikan beberapa minggu kemudian. Karena itu, model yang hanya melakukan join berdasarkan event time akan menerima informasi yang belum tersedia bagi siapa pun pada saat tersebut.
Selisih waktunya dapat diukur. Manajer institusional menyampaikan Form 13F setelah penutupan setiap kuartal. Panel di bawah ini mengukur jumlah hari antara kuartal yang dijelaskan dalam suatu filing dan tanggal filing tersebut.
SQL tepat di balik setiap angka
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_endUntuk kuartal yang berakhir pada 2026-06-30, filing 10688 tiba rata-rata 34.1 hari setelah periode yang dicakupnya berakhir. Panel ini mengulangi pengukuran tersebut untuk 16 kuartal. Manajer juga dapat mengamendemen laporan jauh setelah filing awal disampaikan. Karena itu, catatan yang menggambarkan tanggal masa lalu dapat terus berubah setelah tanggal tersebut berlalu.
Look-ahead bias adalah masalah penyimpanan
Panduan kami tentang look-ahead bias dalam backtesting memperlakukan kebocoran data sebagai disiplin: beri jeda pada setiap feature dan patuhi tanggal publikasi. Disiplin berlaku sampai seseorang lupa, dan kegagalannya berlangsung tanpa terlihat. Backtest yang mengalami kebocoran dapat mencatat rasio Sharpe yang lebih baik tanpa menghasilkan error apa pun.
Penyimpanan point-in-time memindahkan jaminan tersebut ke lapisan yang lebih rendah. Jika frame yang diberikan kepada strategi berasal dari pembacaan yang dikunci pada suatu versi, baris yang ditulis setelah versi tersebut tidak dapat muncul di dalamnya, apa pun yang dilakukan kode strategi selanjutnya. Pemeriksaan tidak lagi bergantung pada code review, melainkan menjadi properti dari pembacaan data.
Dividen menunjukkan kesenjangan waktu tersebut dari sisi lain. Dividen tunai pertama-tama diumumkan, lalu memasuki tanggal ex-dividend pada waktu berikutnya. Tabel yang dimuat hari ini memuat kedua tanggal untuk setiap pembayaran, termasuk pembayaran yang belum diumumkan pada tanggal yang sedang disimulasikan.
SQL tepat di balik setiap angka
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 monthPada bulan yang dimulai pada 2026-07-01, pengumuman diterbitkan rata-rata 88.2 hari sebelum tanggal ex-dividend, dan panel tersebut mencakup 24 bulan dengan pengukuran yang sama. Jika tabel dividen modern dibaca menggunakan tanggal simulasi di dalam jeda tersebut, pembayaran itu sudah tercatat di sana, berminggu-minggu sebelum pengumumannya diterbitkan.
Riwayat tersimpan dapat ditulis ulang
Keterlambatan publikasi adalah salah satu sumber kegagalan. Restatement adalah sumber lainnya. Corporate action menulis ulang harga yang sudah tercatat: setelah stock split four-for-one, setiap harga sebelumnya dalam seri yang telah disesuaikan dibagi empat, sehingga seri yang diunduh hari ini tidak lagi sama dengan tape yang dipantau trader. Catatan kami tentang riwayat harga yang disesuaikan dengan split menjelaskan perhitungannya. Yang penting di sini adalah frekuensinya.
SQL tepat di balik setiap angka
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_datePada kuartal yang dimulai 2026-04-01, 131 forward split dan 303 reverse split berlaku. Masing-masing menulis ulang riwayat harga yang mungkin sudah disimpan dalam cache oleh pipeline riset. Penyimpanan berversi tidak mencegah restatement tersebut. Sistem ini mencatat keadaan baru sebagai versi baru dan tetap membuat versi lama dapat dibaca. Dengan demikian, hasil yang sudah kedaluwarsa dapat direproduksi.
Timestamp berita memiliki jebakan yang sama dalam skala lebih kecil.
SQL tepat di balik setiap angka
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_hourHeadline terbit sepanjang waktu, selama seluruh 24 jam dalam hari perdagangan New York. Jam 09.00 memuat 790 artikel selama 90 hari terakhir, sedangkan jam 20.00 memuat 404 artikel. Menempelkan headline malam hari pada penutupan pukul 16.00 di hari yang sama memberikan beberapa jam informasi masa depan kepada strategi yang bertransaksi pada penutupan.
Skenario point-in-time yang dapat dijalankan
Semua yang dijelaskan di sini menggunakan Python package. Proyek ini juga menyediakan command-line tool berbasis Rust, tetapi instalasi terpisah tersebut tidak diperlukan dalam panduan ini. Sample data dibuat secara lokal tanpa pengunduhan.
- Instal rilis yang dipatok versinya:
pip install 'h5i-db==0.1.6', yang dipublikasikan pada 4 Agustus 2026. Rilis ini memerlukan Python 3.9 atau yang lebih baru dan menyertakanpyarrow>=14. Prebuilt wheels tersedia untuk Linux pada x86-64 dan arm64, serta macOS dengan Apple silicon dan Windows pada x86-64. - Deskripsikan data satu kali, setelah
import pyarrow as padanimport pyarrow.parquet as pq:schema = pa.schema([('ts', pa.timestamp('us', tz='UTC')), ('symbol', pa.string()), ('price', pa.float64())]). - Tulis dua baris buatan ke file lokal dengan
pq.write_table(pa.table({'ts': [d1, d2], 'symbol': ['ACME', 'ACME'], 'price': [10.0, 10.5]}, schema=schema), 'day1.parquet'), dengand1dand2berupa datetime yang menyertakan informasi zona waktu. - Buat database dan tabel, lalu tetapkan nama kolom waktu:
db = h5i_db.Database('pit.db', create=True)kemudiandb.create_table('prices', schema, time_column='ts'). - Ingest file dengan sebuah key:
db.append('prices', pq.read_table('day1.parquet'), idempotency_key='load-day1'). Pemanggilan ini mengembalikan commit yang dibuat. - Jalankan kembali baris yang sama persis. Dokumentasi proyek menjelaskan bahwa pengulangan dengan key yang sama akan menemukan commit yang telah dibuat dan mengembalikannya dengan
"segments_added": 0, alih-alih menulis ulang baris untuk kedua kalinya. Cetakdb.versions('prices')di kedua sisi retry dan amati bahwa daftar versi tetap tidak berubah. - Ingest hari kedua dengan
idempotency_key='load-day2', lalu lakukan query untuk kedua hari tersebut:db.sql('SELECT symbol, count(*) AS n, avg(price) AS px FROM prices GROUP BY symbol').to_pandas(). - Baca tabel sebagaimana kondisinya sebelum data hari kedua masuk:
db.read('prices', version=1). Method yang sama menerima argumenas_of=dansnapshot=untuk pekerjaan yang sama.
Langkah 6 perlu diperhatikan. Append yang terduplikasi tidak menimbulkan error. Sejak saat itu, isi tabel menjadi salah, dan setiap proses berikutnya mewarisi kerusakan tersebut secara diam-diam. Langkah 8 menunjukkan manfaat utamanya: angka yang dihitung pada Maret dapat dihitung ulang pada Agustus dari versi yang sama dan tetap dipatok, yaitu karakteristik yang kami dukung pada tingkat framework dalam tulisan tentang backtest yang dapat direproduksi.
Klaim proyek dan hal yang kami periksa
README diawali dengan benchmark:
lebih dari 4,5× lebih cepat daripada DuckDB dan Polars untuk rollup OHLCV+VWAP pada 20 juta baris
Angka tersebut merupakan pengukuran proyek itu sendiri, yang dikutip dari README h5i-db sebagaimana diambil pada Agustus 2026. Kami tidak menjalankan pengujian tersebut, dan tidak ada bagian di atas yang bergantung padanya.
Kematangan proyek lebih penting daripada kecepatan dalam hal ini. Repositori tersebut memiliki 29 stars pada saat penulisan dan berada pada versi 0.1.6 di bawah lisensi Apache-2.0. Kombinasi ini menunjukkan jumlah maintainer yang sedikit dan API yang masih dapat berubah antarrilis minor. Belum ada rekam jejak publik yang panjang mengenai engine ini saat berjalan di bawah beban. Menetapkan versi yang tepat dan menyimpan file parquet yang digunakan untuk mengisi database memberi Anda jalur pemulihan jika suatu rilis mengubah perilaku. Menyimpan salinan data mentah sendiri merupakan kebiasaan yang sama untuk melindungi Anda dari vendor yang mengubah riwayat data secara diam-diam, sebagaimana dibahas dalam survivorship bias dalam data saham.
FAQ
Apa yang dimaksud dengan data point-in-time?
Data point-in-time adalah dataset yang disimpan bersama timestamp saat setiap fakta mulai diketahui. Dengan demikian, query dapat merekonstruksi informasi yang terlihat pada tanggal mana pun di masa lalu. Tabel yang hanya menyimpan “nilai terbaru” tidak dapat melakukan hal itu karena menimpa data historis dengan angka yang telah dikoreksi hari ini.
Apakah penyimpanan berversi menghilangkan look-ahead bias?
Tidak. Versioning memperbaiki satu jenis kebocoran, yaitu ketika suatu proses membaca nilai yang ditulis setelah waktu keputusan simulasi. Konstruksi feature masih dapat mengalami kebocoran dengan cara lain, misalnya ketika sampel diskalakan menggunakan statistik yang dihitung dari seluruh riwayatnya.
Apa fungsi idempotency key saat ingest?
Idempotency key menandai suatu penulisan agar percobaan ulang dikenali sebagai penulisan yang sama. Loader kemudian dapat dijalankan kembali setelah crash tanpa menambahkan baris yang sama dua kali. Penambahan ganda inilah yang dapat membuat tabel secara diam-diam menjadi tidak benar.
Apakah h5i-db siap digunakan dalam produksi?
Pada saat artikel ini ditulis, h5i-db berada pada version 0.1.6, memiliki 29 stars di GitHub, dan menggunakan lisensi Apache-2.0. Software tahap awal dengan ukuran seperti itu masih dapat mengalami perubahan API dan memiliki rekam jejak publik yang terbatas. Karena itu, version pin serta salinan file sumber Anda sendiri diperlukan agar evaluasi tetap dapat dibatalkan.
Setiap panel di atas dilengkapi SQL yang digunakan untuk menghasilkan panel tersebut. Buka salah satunya dan lihat cara angka itu dihitung. Pertanyaan yang sama dapat diajukan dalam bahasa Inggris biasa melalui terminal Strasmore.