h5i-db dane point-in-time w backtestach
Baza h5i-db eliminuje błąd wyprzedzenia w backtestach poprzez wersjonowanie każdego zapisu. Analiza opóźnień w raportowaniu danych finansowych pozwala uniknąć błędów historii.
Dane typu point-in-time a rzetelność backtestów
Dane typu point-in-time stanowią zapis zawartości zbioru danych w określonym momencie w przeszłości. W procesie backtestingu pozwalają one odróżnić wyniki możliwe do obrony od tych, które niejawnie korzystają z przyszłych odczytów. h5i-db to nowa baza danych szeregów czasowych typu open-source, napisana w języku Rust z API w Pythonie. Każdy zapis w tej bazie jest przechowywany jako ponumerowana wersja, co umożliwia przypięcie odczytu do dowolnego wcześniejszego stanu. Poniżej przedstawiono wyciek danych, któremu zapobiega mechanizm przypinania, zmierzony na rzeczywistych danych ze sprawozdań, a następnie scenariusz, który można uruchomić na komputerze osobistym.
Czym są dane typu point-in-time w backteście?
Każde zdarzenie rynkowe posiada dwa znaczniki czasowe. Czas zdarzenia to moment, w którym faktycznie ono wystąpiło. Czas dotarcia to moment, w którym informacja stała się dostępna dla podmiotów zewnętrznych względem raportującej instytucji. Kwartalny raport o portfelu opisuje pozycje utrzymywane w ostatnim dniu kwartału, lecz trafia do publicznej wiadomości dopiero kilka tygodni później. Model łączący dane wyłącznie w oparciu o czas zdarzenia korzysta zatem z informacji, którymi w tamtym momencie nikt nie dysponował.
Opóźnienie to jest mierzalne. Zarządzający instytucjonalni składają formularz 13F po zakończeniu każdego kwartału, a poniższy panel mierzy liczbę dni między kwartałem opisywanym w raporcie a dniem jego złożenia.
Dokładny kod SQL dla każdej liczby
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_endDla kwartału kończącego się 2026-06-30, zgłoszenia 10688 docierały średnio po 34.1 dniach od okresu, którego dotyczą. Panel powtarza ten pomiar dla 16 kwartałów. Zarządzający może również skorygować raport długo po jego złożeniu, dlatego zapis opisujący przeszłą datę ulega zmianie nawet po upływie tego terminu.
Błąd „look-ahead” jako problem przechowywania danych
Nasz przewodnik dotyczący błędu „look-ahead” w backtestingu traktuje wyciek danych jako kwestię dyscypliny: opóźnianie każdej zmiennej i przestrzeganie dat publikacji. Dyscyplina trwa do momentu, w którym ktoś popełni błąd, a awaria przebiega bezgłośnie. Backtest z wyciekiem danych wykazuje lepszy wskaźnik Sharpe’a, nie generując przy tym żadnego błędu.
Przechowywanie danych typu „point-in-time” przenosi gwarancję poprawności na niższy poziom. Gdy zbiór danych dostarczony do strategii pochodzi z odczytu przypisanego do konkretnej wersji, wiersz zapisany po tej wersji nie może się w nim pojawić, niezależnie od dalszych działań kodu strategii. Weryfikacja przestaje być kwestią przeglądu kodu, a staje się właściwością samego odczytu.
Dywidendy obrazują lukę czasową z drugiej strony. Dywidenda gotówkowa jest najpierw ogłaszana, a dopiero później przypada jej data ex-dividend. Tabela wczytana dzisiaj zawiera obie daty dla każdej płatności, w tym również dla tych, które w dniu symulacji nie zostały jeszcze ogłoszone.
Dokładny kod SQL dla każdej liczby
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 monthW miesiącu rozpoczynającym się 2026-07-01 ogłoszenia pojawiały się średnio na 88.2 dni przed datą ex-dividend, a panel obejmuje 24 miesięcy tego samego pomiaru. Odczyt współczesnej tabeli dywidend w dniu symulacji przypadającym wewnątrz tej luki sprawia, że informacja o płatności jest już dostępna, na wiele tygodni przed faktycznym ogłoszeniem.
Aktualizacja historycznych danych
Opóźnione dotarcie informacji to jeden z rodzajów awarii. Drugim jest korekta danych. Zdarzenia korporacyjne zmieniają ceny, które zostały już zarejestrowane: po splicie w stosunku cztery do jednego każda wcześniejsza cena w szeregu korygowanym jest dzielona przez cztery, a szereg pobrany dzisiaj nie odpowiada już notowaniom, które obserwował trader. Nasza notatka na temat historii cen korygowanej o splity wyjaśnia stosowaną arytmetykę. Kluczowa jest tutaj częstotliwość.
Dokładny kod SQL dla każdej liczby
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_dateW kwartale rozpoczynającym się 2026-04-01 weszło w życie 131 splitów oraz 303 odwrotnych splitów. Każde z nich zmienia historię cen, która mogła zostać już zapisana w pamięci podręcznej potoku badawczego. Wersjonowanie danych nie zapobiega korektom. Zapisuje ono nowy stan jako nową wersję i zachowuje możliwość odczytu starej, co pozwala przekształcić nieaktualny wynik w wynik powtarzalny.
Znaczniki czasu wiadomości niosą ze sobą to samo zagrożenie w mniejszej skali.
Dokładny kod SQL dla każdej liczby
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_hourNagłówki publikowane są przez całą dobę, w ciągu wszystkich 24 godzin dnia w Nowym Jorku. W godzinie dziewiątej opublikowano 790 artykułów w ciągu ostatnich dziewięćdziesięciu dni, a w godzinie dwudziestej 404. Przypisanie wieczornego nagłówka do zamknięcia sesji o godzinie szesnastej daje strategii handlującej na zamknięciu kilka godzin przewagi wynikającej z wiedzy o zdarzeniach, które już nastąpiły.
Scenariusz typu point-in-time do uruchomienia
Wszystkie elementy znajdują się w pakiecie Python. Projekt dostarcza również narzędzie wiersza poleceń napisane w Rust, czyli oddzielną instalację, która nie jest wymagana w tym przewodniku. Przykładowe dane są generowane lokalnie, bez pobierania plików.
- Zainstaluj przypiętą wersję:
pip install 'h5i-db==0.1.6', opublikowaną 4 sierpnia 2026 roku. Wymaga ona języka Python w wersji 3.9 lub nowszej i zawierapyarrow>=14. Gotowe pakiety typu wheel obejmują systemy Linux na architekturach x86-64 i arm64, a także systemy macOS z procesorami Apple silicon oraz Windows na architekturze x86-64. - Zdefiniuj dane jednokrotnie, po
import pyarrow as paorazimport pyarrow.parquet as pq:schema = pa.schema([('ts', pa.timestamp('us', tz='UTC')), ('symbol', pa.string()), ('price', pa.float64())]). - Zapisz dwa przykładowe wiersze do lokalnego pliku za pomocą
pq.write_table(pa.table({'ts': [d1, d2], 'symbol': ['ACME', 'ACME'], 'price': [10.0, 10.5]}, schema=schema), 'day1.parquet'), gdzied1orazd2to daty i godziny uwzględniające strefę czasową. - Utwórz bazę danych oraz tabelę, nazywając kolumnę czasu:
db = h5i_db.Database('pit.db', create=True), a następniedb.create_table('prices', schema, time_column='ts'). - Zaimportuj plik pod określonym kluczem:
db.append('prices', pq.read_table('day1.parquet'), idempotency_key='load-day1'). Wywołanie zwraca utworzony commit. - Uruchom tę samą linię ponownie. Dokumentacja projektu wskazuje, że powtórzenie operacji z tym samym kluczem wykrywa już istniejący commit i zwraca go wraz z
"segments_added": 0, zamiast ponownie zapisywać wiersze. Wyświetldb.versions('prices')po obu stronach ponowienia i obserwuj, jak lista wersji pozostaje niezmieniona. - Zaimportuj dane z drugiego dnia pod kluczem
idempotency_key='load-day2', a następnie wykonaj zapytanie obejmujące oba dni:db.sql('SELECT symbol, count(*) AS n, avg(price) AS px FROM prices GROUP BY symbol').to_pandas(). - Odczytaj stan tabeli sprzed dodania danych z drugiego dnia:
db.read('prices', version=1). Ta sama metoda przyjmuje argumentyas_of=orazsnapshot=w celu wykonania tego zadania.
Krok szósty wymaga szczególnej uwagi. Zduplikowane dołączenie danych nie powoduje błędu. Od tego momentu tabela pozostaje w błędnym stanie, a każde kolejne uruchomienie dziedziczy ten błąd w sposób niezauważalny. Krok ósmy stanowi kluczowy atut: wynik obliczony w marcu może zostać ponownie wyliczony w sierpniu przy użyciu tej samej przypiętej wersji, co stanowi właściwość, którą w naszym opracowaniu dotyczącym powtarzalnego backtestu uzasadniamy na poziomie frameworka.
Deklaracje projektu a nasze ustalenia
Plik README otwiera się następującym benchmarkiem:
ponad 4,5 raza szybszy niż DuckDB i Polars w agregacjach OHLCV+VWAP dla 20 milionów wierszy
Powyższa wartość jest autorskim pomiarem projektu, przytoczonym z pliku README h5i-db według stanu na sierpień 2026 roku. Nie przeprowadzaliśmy własnych testów, a powyższe wnioski nie są od nich zależne.
W tym przypadku dojrzałość rozwiązania jest ważniejsza niż szybkość. W momencie pisania niniejszego tekstu repozytorium posiadało 29 gwiazdek, a projekt znajduje się w wersji 0.1.6 na licencji Apache-2.0. Takie połączenie oznacza niewielką grupę osób utrzymujących kod oraz API, które może ulegać zmianom pomiędzy wydaniami typu point release. Brak jest długiej historii publicznego wykorzystania silnika pod obciążeniem. Zablokowanie konkretnej wersji oraz zachowanie plików parquet, które zasilały bazę danych, pozwala na powrót do poprzedniego stanu w razie zmiany zachowania silnika w nowej wersji. Przechowywanie własnej kopii danych źródłowych to ten sam nawyk, który chroni przed sytuacją, w której dostawca zmienia historię bez wiedzy użytkownika – motyw przewijający się w artykule błąd przeżywalności w danych giełdowych.
Często zadawane pytania
Czym są dane typu point-in-time?
Dane typu point-in-time to zbiór informacji przechowywany wraz ze znacznikami czasu, które wskazują moment, w którym dany fakt stał się znany. Dzięki temu zapytanie pozwala odtworzyć stan wiedzy dostępny w dowolnym dniu w przeszłości. Standardowa tabela z „najnowszą wartością” nie posiada takiej funkcjonalności, ponieważ nadpisuje dane historyczne dzisiejszymi, skorygowanymi wartościami.
Czy wersjonowanie danych eliminuje błąd look-ahead bias?
Nie. Wersjonowanie rozwiązuje jeden rodzaj wycieku danych, polegający na odczytywaniu wartości zapisanych po symulowanym momencie podjęcia decyzji. Konstrukcja zmiennych nadal może prowadzić do wycieków w inny sposób, na przykład poprzez skalowanie próby w oparciu o statystyki obliczone dla całej jej historii.
Jaką funkcję pełni klucz idempotentności podczas przesyłu danych?
Oznacza on operację zapisu w taki sposób, aby ponowna próba została rozpoznana jako ten sam zapis. Dzięki temu moduł ładujący może zostać uruchomiony ponownie po awarii bez ryzyka dwukrotnego dodania tych samych wierszy, co jest błędem prowadzącym do cichej korupcji danych w tabeli.
Czy h5i-db jest gotowe do wdrożenia produkcyjnego?
Obecnie dostępna jest wersja 0.1.6, która w momencie pisania tego tekstu posiada dwadzieścia dziewięć gwiazdek w serwisie GitHub i jest udostępniona na licencji Apache-2.0. Wczesne oprogramowanie tej skali wiąże się z ryzykiem częstych zmian w API oraz ograniczoną historią użytkowania. Zastosowanie sztywnej wersji (version pin) oraz posiadanie własnej kopii plików źródłowych to środki zapewniające powtarzalność wyników ewaluacji.
Każdy z powyższych paneli zawiera kod SQL, który wygenerował prezentowane dane; warto go otworzyć, aby sprawdzić metodologię obliczeń. Analogiczne zapytania można kierować w języku angielskim za pośrednictwem terminala Strasmore.