h5i-db: Data Point-in-Time untuk Backtest
Data point-in-time menghalang bias look-ahead dalam backtest pada lapisan storan. Ketahui bagaimana h5i-db menguruskan versi data dan kesan lag pemfailan terhadap sejarah.
Data point-in-time dan integriti backtest
Data point-in-time ialah rekod mengenai kandungan set data pada tarikh yang lalu. Dalam backtest, ia memisahkan hasil yang boleh dipertahankan daripada hasil yang secara senyap menggunakan data masa depan. h5i-db ialah pangkalan data siri masa sumber terbuka yang baharu, ditulis dalam Rust dengan API Python, yang menyimpan setiap penulisan sebagai versi bernombor dan membolehkan sebarang bacaan ditetapkan pada versi terdahulu. Di bawah ialah kebocoran yang disekat oleh fungsi pinning, diukur berdasarkan data pemfailan sebenar, diikuti dengan senario yang boleh anda jalankan pada komputer riba.
2026-07-01 Kebocoran data dalam backtest
Kebocoran data berlaku apabila maklumat daripada masa depan secara tidak sengaja dimasukkan ke dalam model latihan atau ujian. Dalam analisis kewangan, ini sering berlaku apabila set data tidak mencerminkan apa yang diketahui oleh pasaran pada masa sebenar transaksi dilakukan. Sebagai contoh, jika anda menggunakan data penyata kewangan yang disemak semula (restated) untuk menguji strategi yang sepatutnya dilaksanakan pada tahun dua ribu dua puluh, anda sebenarnya menggunakan maklumat yang belum wujud pada tarikh tersebut.
Mekanisme pinning h5i-db
h5i-db menangani isu ini dengan membenarkan pengguna menetapkan "pin" pada versi pangkalan data tertentu. Apabila anda melakukan pertanyaan (query) dengan pin pada tarikh atau nombor versi yang spesifik, pangkalan data akan mengabaikan semua kemas kini yang berlaku selepas titik masa tersebut. Ini memastikan bahawa backtest anda hanya melihat data yang tersedia secara sah pada masa itu, sekali gus menghapuskan risiko bias pandangan ke hadapan (look-ahead bias).
Senario ujian pada komputer riba
Anda boleh mensimulasikan persekitaran ini dengan langkah-langkah berikut:
- Muat turun pustaka h5i-db melalui pengurus pakej Python anda.
- Lakukan beberapa siri penulisan data harga saham dengan cap masa yang berbeza.
- Lakukan pertanyaan (query) tanpa pin untuk melihat data terkini.
- Lakukan pertanyaan (query) dengan pin pada tarikh yang lebih awal untuk melihat bagaimana pangkalan data mengembalikan keadaan data pada masa itu.
Muat turun kod contoh di sini
Apakah data point-in-time dalam backtest?
Setiap fakta pasaran membawa dua cap masa. Masa peristiwa ialah waktu sesuatu perkara itu berlaku. Masa ketibaan ialah waktu maklumat tersebut boleh diketahui oleh sesiapa sahaja di luar firma yang melaporkannya. Laporan pegangan suku tahunan menerangkan kedudukan yang dipegang pada hari terakhir sesuatu suku tahun dan sampai kepada orang awam beberapa minggu kemudian, jadi model yang menggabungkan data berdasarkan masa peristiwa sahaja akan menerima maklumat yang tidak dimiliki oleh sesiapa pun pada waktu itu.
Jarak tersebut boleh diukur. Pengurus institusi memfailkan Borang 13F selepas penutupan setiap suku tahun, dan panel di bawah mengukur bilangan hari antara suku tahun yang diterangkan dalam pemfailan dengan hari ia difailkan.
SQL tepat di sebalik setiap nombor
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_endBagi suku tahun yang berakhir pada 2026-06-30, pemfailan 10688 tiba secara purata 34.1 hari selepas tempoh yang diliputi. Panel tersebut mengulangi pengukuran itu merentasi 16 suku tahun. Seorang pengurus juga boleh meminda laporan lama selepas memfailkannya, jadi rekod yang menerangkan tarikh yang lalu terus berubah sebaik sahaja tarikh tersebut berlalu.
Bias pandang ke hadapan merupakan masalah penyimpanan
Panduan kami mengenai bias pandang ke hadapan dalam ujian ke belakang menganggap kebocoran data sebagai satu disiplin: lambatkan setiap ciri dan patuhi tarikh penerbitan. Disiplin ini bertahan sehingga seseorang terlupa, dan kegagalan tersebut berlaku secara senyap. Ujian ke belakang yang mengalami kebocoran akan mencatatkan Sharpe ratio yang lebih baik tanpa sebarang ralat.
Penyimpanan point-in-time memindahkan jaminan tersebut ke lapisan yang lebih bawah. Apabila kerangka data yang diberikan kepada sesuatu strategi datang daripada bacaan yang dikunci pada satu versi, baris yang ditulis selepas versi tersebut tidak akan muncul di dalamnya, tidak kira apa jua tindakan kod strategi seterusnya. Pemeriksaan itu tidak lagi menjadi semakan kod, sebaliknya menjadi sifat bagi bacaan tersebut.
Dividen menunjukkan jurang masa daripada sudut yang berbeza. Dividen tunai diisytiharkan terlebih dahulu dan tarikh ex-dividend berlaku kemudian, manakala jadual yang dimuatkan hari ini membawa kedua-dua tarikh bagi setiap pembayaran, termasuk pembayaran yang belum diumumkan pada tarikh yang sedang disimulasikan.
SQL tepat di sebalik setiap nombor
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 bermula pada 2026-07-01, pengisytiharan dibuat secara purata 88.2 hari sebelum tarikh ex-dividend, dan panel tersebut meliputi 24 bulan bagi ukuran yang sama. Jika anda membaca jadual dividen moden pada tarikh simulasi di dalam jurang tersebut, pembayaran itu sudah pun tersenarai di sana, beberapa minggu sebelum pengumuman tersebut wujud.
Sejarah tersimpan ditulis semula
Ketibaan lewat merupakan satu mod kegagalan. Penyataan semula adalah satu lagi. Tindakan korporat menulis semula harga yang telah pun direkodkan: selepas pecahan saham 4-untuk-1, setiap harga terdahulu dalam siri terlaras dibahagikan dengan empat, dan siri yang dimuat turun hari ini tidak lagi sepadan dengan pita harga yang diperhatikan oleh pedagang. Nota kami mengenai sejarah harga terlaras pecahan menghuraikan pengiraan tersebut. Perkara yang penting di sini ialah kekerapan.
SQL tepat di sebalik setiap nombor
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_dateDalam suku yang bermula pada 2026-04-01, 131 pecahan saham ke hadapan dan 303 pecahan saham terbalik telah berkuat kuasa. Setiap satunya menyatakan semula sejarah harga yang mungkin telah pun disimpan dalam cache oleh saluran penyelidikan. Storan berversi tidak menghalang penyataan semula tersebut. Ia merekodkan keadaan baharu sebagai versi baharu dan mengekalkan versi lama agar boleh dibaca, yang mana ia mengubah hasil yang lapuk menjadi hasil yang boleh dihasilkan semula.
Cap masa berita membawa perangkap yang sama dalam skala kecil.
SQL tepat di sebalik setiap nombor
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_hourTajuk berita diterbitkan sepanjang masa, merentasi kesemua 24 jam hari dagangan New York. Jam 09:00 mencatatkan 790 artikel sepanjang 90 hari yang lalu, dan jam 20:00 mencatatkan 404. Meletakkan cap masa tajuk berita waktu malam pada harga penutup jam 4:00 petang hari tersebut memberikan strategi yang berdagang pada waktu penutup dengan kelebihan beberapa jam pengetahuan retrospektif.
Senario point-in-time yang boleh anda jalankan
Segala-galanya di sini kekal pada pakej Python. Projek ini juga membekalkan alat baris perintah Rust, satu pemasangan berasingan yang tidak diperlukan oleh panduan ini. Data sampel dijana secara setempat, tanpa sebarang muat turun.
- Pasang keluaran yang ditetapkan:
pip install 'h5i-db==0.1.6', diterbitkan pada 4 Ogos 2026. Ia memerlukan Python 3.9 atau lebih baharu dan membawapyarrow>=14. Wheel yang telah dibina merangkumi Linux pada x86-64 dan arm64, serta macOS Apple silicon dan Windows pada x86-64. - Perihalkan data sekali, selepas
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 ciptaan ke dalam fail setempat dengan
pq.write_table(pa.table({'ts': [d1, d2], 'symbol': ['ACME', 'ACME'], 'price': [10.0, 10.5]}, schema=schema), 'day1.parquet'), di manad1dand2merupakan tarikh-masa yang peka zon waktu. - Cipta pangkalan data dan jadual tersebut, dengan menamakan lajur masa:
db = h5i_db.Database('pit.db', create=True)kemudiandb.create_table('prices', schema, time_column='ts'). - Masukkan fail di bawah satu kunci:
db.append('prices', pq.read_table('day1.parquet'), idempotency_key='load-day1'). Panggilan tersebut mengembalikan komit yang dibuatnya. - Jalankan baris yang sama sekali lagi. Projek ini mendokumenkan bahawa ulangan yang membawa kunci yang sama akan menemui komit yang telah dihasilkannya dan mengembalikannya dengan
"segments_added": 0dan bukannya menulis baris tersebut buat kali kedua. Cetakdb.versions('prices')pada kedua-dua belah percubaan semula dan perhatikan senarai versi kekal tidak berubah. - Masukkan hari kedua di bawah
idempotency_key='load-day2', kemudian buat pertanyaan merentas kedua-duanya:db.sql('SELECT symbol, count(*) AS n, avg(price) AS px FROM prices GROUP BY symbol').to_pandas(). - Baca jadual seperti keadaan sebelum hari kedua dimasukkan:
db.read('prices', version=1). Kaedah yang sama mengambil argumenas_of=dansnapshot=untuk tugasan yang sama.
Langkah 6 adalah langkah yang perlu diberi perhatian. Penambahan pendua tidak mencetuskan ralat. Ia menyebabkan jadual menjadi salah sejak saat itu, dan setiap larian selepasnya mewarisi kerosakan tersebut secara senyap. Langkah 8 adalah hasilnya: nombor yang dikira pada bulan Mac boleh dikira semula pada bulan Ogos daripada versi yang ditetapkan yang sama, iaitu ciri yang dihujahkan oleh penulisan kami mengenai backtest yang boleh dihasilkan semula pada peringkat rangka kerja.
Dakwaan projek dan perkara yang kami semak
README projek tersebut bermula dengan penanda aras:
lebih 4.5 kali lebih pantas daripada DuckDB dan Polars dalam pengiraan OHLCV+VWAP ke atas 20 juta baris data
Angka tersebut merupakan ukuran projek itu sendiri, dipetik daripada README h5i-db seperti yang diperoleh pada Ogos 2026. Kami tidak menjalankan ujian tersebut, dan tiada perkara di atas yang bergantung kepadanya.
Kematangan lebih penting daripada kelajuan dalam konteks ini. Repositori tersebut mempunyai 29 bintang pada masa penulisan dan berada pada versi 0.1.6 di bawah lesen Apache-2.0. Gabungan ini bermakna kumpulan penyelenggara yang kecil dan API yang masih boleh berubah antara keluaran titik. Tiada rekod awam yang panjang mengenai enjin ini yang beroperasi di bawah beban kerja. Menetapkan versi yang tepat dan menyimpan fail parquet yang membekalkan pangkalan data tersebut menyediakan jalan keluar jika sesuatu keluaran mengubah kelakuan sistem. Menyimpan salinan mentah anda sendiri adalah tabiat yang sama yang melindungi daripada vendor yang mengubah sejarah di belakang anda, tema yang berterusan melalui bias kemandirian dalam data saham.
Soalan Lazim
Apakah data point-in-time?
Data point-in-time ialah set data yang disimpan bersama cap masa bagi setiap fakta yang boleh diketahui, supaya pertanyaan boleh membina semula apa yang kelihatan pada mana-mana tarikh yang lalu. Jadual "nilai terkini" biasa tidak boleh melakukan perkara ini, kerana ia menimpa data masa lalu dengan nombor yang telah diperbetulkan pada hari ini.
Adakah storan berversi menghapuskan bias pandang ke hadapan (look-ahead bias)?
Tidak. Pemversian membetulkan satu kelas kebocoran, iaitu jenis di mana satu larian membaca nilai yang ditulis selepas masa keputusan disimulasikan. Pembinaan ciri masih boleh bocor melalui cara lain, seperti menskalakan sampel mengikut statistik yang dikira sepanjang sejarah penuhnya.
Apakah fungsi kunci idempotensi semasa proses ingest?
Ia melabelkan penulisan supaya percubaan semula dikenali sebagai penulisan yang sama. Pemuat (loader) kemudian boleh dijalankan semula selepas kegagalan sistem tanpa menambah baris yang sama dua kali, iaitu kegagalan yang menyebabkan jadual menjadi salah secara senyap.
Adakah h5i-db sedia untuk pengeluaran?
Ia adalah versi 0.1.6 dengan 29 bintang di GitHub pada masa penulisan, di bawah lesen Apache-2.0. Perisian awal dengan saiz sedemikian membawa perubahan API dan rekod prestasi awam yang tipis, dan penetapan versi (version pin) serta salinan fail sumber anda sendiri adalah perkara yang memastikan penilaian boleh diterbalikkan.
Setiap panel di atas dihantar bersama SQL yang menghasilkannya, jadi buka satu dan baca bagaimana nombor itu dikira. Soalan yang sama boleh ditanya dalam bahasa Inggeris biasa pada terminal Strasmore.