Roczna seria 1-minutowych świec powinna liczyć 525,600 wierszy. Nasz zrzut kontraktów perpetual BTCUSDT za 2025 rok zawierał 525,557 — brakowało 43 minut, a kompletność wynosiła 99.992%. W przypadku kontraktu perpetual na altcoina ze średniej półki, na tej samej giełdzie, z tego samego pobrania i tą samą ścieżką kodu, brakowało 1,247 minut: kompletność wynosiła 99.76%. Z kolei minutowa seria akcji amerykańskich za ten sam rok miała około 427,000 wierszy mniej niż seria kryptowalutowa — ale to wcale nie luka, tylko efekt zamknięcia rynku.
Trzy z tych liczb nie robią wrażenia. Ta, o której warto powiedzieć tysiąc słów, to 43.
Cztery różne sytuacje, które w Twoim dataframe wyglądają jak luka
Zanim przejdziemy do 43, uporządkujmy pojęcia, bo większość kodu obsługującego luki zawodzi już na tym poziomie. Gdy w lokalnym pliku parquet brakuje wiersza, nie wiesz, która z tych sytuacji jest przyczyną, a właściwa reakcja zależy od każdej z nich.
| Rodzaj | Co się naprawdę wydarzyło | Jak to wygląda w danych | Właściwa reakcja |
|---|---|---|---|
| Brak transakcji | Rynek był otwarty, ale nikt w tej minucie nie przekroczył spreadu | Świeca o zerowym wolumenie (wszystkie wartości OHLC są równe, trades=0) w niektórych endpointach; w innych brak wiersza | Zachowaj ją. To prawdziwa informacja: nikt nie chciał handlować. |
| Awaria giełdy | Silnik dopasowujący zlecenia nie działał — planowo lub nie | Brak wiersza, czasem cała seria brakujących wierszy | Oznacz dane jako nieaktualne. Nie handluj w czasie przerwy. |
| Wstrzymanie instrumentu | Przekroczenie pasma LULD, oczekiwanie na wiadomości, komunikat o wycofaniu z obrotu | Brak wiersza, a potem transakcja z aukcji wznawiającej notowania | Oznacz dane jako nieaktualne i potraktuj wznowienie jako skok ceny. |
| Błąd po Twojej stronie | Ograniczenie liczby zapytań podczas stronicowania, ponowienie próby z pominiętą stroną, błąd pętli na granicy strefy czasowej | Brak wiersza, nie do odróżnienia od powyższych przypadków | Wykryj i pobierz dane ponownie. To jedyny problem, który możesz naprawić. |
Giełdy różnie obsługują pierwszy przypadek z tabeli, co może sprawić kłopot. Endpoint świec Coinbase całkowicie pomija puste przedziały, więc historia niepłynnej pary jest pełna dosłownych luk. Binance w danych klines zazwyczaj zwraca syntetyczną świecę z wolumenem 0 oraz open=high=low=close równymi cenie poprzedniej transakcji. Ta sama sytuacja, dwa różne kształty danych, a loader, który indeksuje serię na pełnej siatce minutowej, zamienia jeden w drugi, nic Ci o tym nie mówiąc. Sprawdź, jak zachowuje się Twoja giełda, zanim napiszesz kod uzupełniający luki, a nie dopiero po fakcie.
Prawie wszystkie spośród 1,247 brakujących minut dla altcoina należały do pierwszej kategorii: płytki arkusz zleceń w niedzielę o 04:00 UTC, nikt nie handlował. To irytujące, łatwe do zrozumienia i w dużej mierze nieszkodliwe, bo strategia i tak nie zawierałaby wtedy transakcji. Właśnie dlatego przestałem się tym zajmować i wróciłem do 43.
Te 43 minuty nie były rozproszone
Gdyby braki rozkładały się równomiernie, 43 minuty w ciągu roku oznaczałyby 43 pojedyncze luki, jedną co osiem i pół dnia — każda bez większego znaczenia. Nie tak to wyglądało. Braki wystąpiły w sześciu seriach: jednej trwającej 19 kolejnych minut, jednej trwającej 11, dwóch po 4 minuty i dwóch parach. Sześć zdarzeń, a nie 43 przypadkowe braki.
Zdarzenia te zależą od tego, co Cię interesuje. Giełdy przestają działać, gdy są przeciążone, a są przeciążone, gdy ceny mocno się zmieniają. Podzieliłem każdą godzinę roku według zrealizowanej zmienności i sprawdziłem, w których godzinach przypadły brakujące minuty: 61% z nich trafiło do górnego decyla. Bezwarunkowe prawdopodobieństwo braku danych w dowolnej minucie wynosi 0.008%. W godzinie z górnego decyla zmienności to około 0.05% — sześć razy więcej, a do tego braki są skupione w seriach.
Metryka kompletności na Twoim dashboardzie jakości danych mierzy więc niewłaściwą rzecz. 99.992% wygląda jak wynik, o którym nie trzeba już myśleć. W rzeczywistości opisuje serię kompletną w godzinach, gdy strategia nic nie robi, i pełną luk w godzinach, gdy robi wszystko. System momentum reagujący na wzrost zmienności ma znacznie większą szansę natrafić na lukę, niż sugeruje główna wartość — i trafia na nią w trakcie transakcji.
Co robi forward-fill kilka linijek później
Oto błąd, który skłonił mnie do napisania tego tekstu. Weźmy serię trwającą 19 minut. Standardowe porządkowanie danych: indeksujemy serię na pełnej siatce minutowej, uzupełniamy OHLC ostatnią znaną ceną zamknięcia i ustawiamy wolumen na zero. Seria jest teraz ciągła, a wskaźniki działają bez ani jednego NaN.
Te 19 świec ma high == low == close. True range wynosi zero w każdej z nich. ATR(14) obliczony dla tego okna, przychodzący z poziomu około 240 USDT sprzed awarii, spada do około 34, zanim giełda wróci do działania — za całą średnią odpowiada 5 rzeczywistych świec, które przetrwały w tym oknie. Teraz podaj tę wartość do skalera pozycji uwzględniającego zmienność, tego zwykłego size = risk_budget / ATR. Wielkość pozycji rośnie siedmiokrotnie.
Następna rzeczywista świeca to świeca otwarcia po wznowieniu handlu, a nie jest to spokojna świeca. W naszym przypadku otworzyła się 1.8% od ostatniego zamknięcia sprzed awarii. Backtest bez wahania otworzył pozycję 7x przy luce wynoszącej 1.8%, po cenie, której nie dało się uzyskać i której nikt nie kwotował. Ta jedna syntetyczna transakcja przyniosła na krzywej kapitału więcej niż miesiąc rzeczywistego P&L — w niewłaściwym kierunku. Powstała wyłącznie przez linijkę czyszczącą dane, która miała uporządkować dataframe.
Usunięcie wierszy zamiast ich uzupełnienia też nie rozwiązuje problemu — to ten sam błąd w innym przebraniu. Po ich usunięciu lookbacki oparte na indeksach całkowitych wprowadzają w błąd: „EMA z 20 świec” obejmuje teraz 39 minut czasu zegarowego z awarią pośrodku, stopa zwrotu między sąsiednimi świecami na granicy luki uwzględnia cały skok 1.8% jako ruch w ciągu jednej minuty, a każdy per-świecowy szacunek zmienności odczytuje go jako zdarzenie o wartości 60 sigma. Nic Cię przed tym nie ostrzega. Indeks nadal jest monotoniczny.
Resampling sprawia, że problem staje się niewidoczny
Większość analiz nie korzysta ze świec 1-minutowych, lecz z danych zagregowanych, a agregacja maskuje problem. Resampling do 5 minut zamienia lukę trwającą 19 minut w 4 świece, z których pierwsza i ostatnia obejmują tylko część przedziału. Pandas obliczy pozornie zupełnie poprawne OHLC na podstawie 2 zachowanych minut i oznaczy je tak samo jak świecę utworzoną z 5 minut danych. W wyniku nic nie pozwala ich odróżnić.
Najprostsze rozwiązanie, jakie znam: przenoś kolumnę bars_in_window przez każdy resampling i nigdy jej nie usuwaj. Jedna liczba całkowita w każdym wierszu wystarczy, by odpowiedzieć na każde późniejsze pytanie o wiarygodność świecy. Przenosimy też seconds_since_last_real_print, czyli tę samą informację w formie, na którą warstwa egzekucji może zareagować.
Nasze zasady, takie jakie są
Oto kolejność, w jakiej działają teraz nasze agenty:
- Nigdy nie indeksuj ponownie po cichu. Loader generuje manifest luk — początek, koniec, długość i przypuszczalną kategorię spośród 4. Jeśli seria brakujących minut obejmuje mniej niż 3 świece, a wolumen w sąsiednich świecach jest niski, uznajemy je za minuty bez transakcji. Każdą dłuższą lukę w godzinach aktywnego handlu traktujemy jako awarię, dopóki nie udowodnimy inaczej.
- Pobierz dane ponownie, zanim zaczniesz je interpretować. Połowa naszych wczesnych luk wynikała z błędów stronicowania. Drugie pobranie z innego endpointu lub od innego dostawcy rozwiązuje problem z kategorii 4 i zmniejsza skalę problemu, zanim trzeba podejmować jakiekolwiek decyzje.
- Bramka aktualności danych zamiast uzupełniania luk. Strategia otrzymuje wejście
data_agei twardą zasadę: nie otwieraj nowych pozycji, gdy ostatnia rzeczywista transakcja miała miejsce dawniej niż N świec temu, a otwarte pozycje zamykaj po wznowieniu handlu wyłącznie zleceniem rynkowym, którego cena uwzględnia jawny haircut na ryzyko luki. Transakcje zawarte po cenach, których nie dało się uzyskać, są gorsze niż brak transakcji. - Wskaźniki widzą NaN, nie fikcję. Ceny uzupełnione przez forward-fill nigdy nie trafiają do warstwy cech. Jeśli nie da się obliczyć ATR, jego wartość pozostaje niezdefiniowana, a to oznacza brak pozycji. Głośny błąd jest lepszy niż pozycja 7x otwarta po cichu.
- Raportuj wyniki warunkowe względem luk. Każdy publikowany przez nas backtest pokazuje P&L zarówno dla wszystkich transakcji, jak i z wyłączeniem transakcji sąsiadujących z awarią. Jeśli to te transakcje odpowiadają za wynik, mamy do czynienia z artefaktem danych.
Szybki audyt, który możesz przeprowadzić już dziś: pogrupuj brakujące minuty w serie kolejnych luk, a następnie sprawdź, jaki odsetek transakcji w Twoim backteście otwiera się lub zamyka w ciągu 30 minut od granicy takiej serii. Jeśli to mniej niż 1%, luki prawdopodobnie nie wpływają na wyniki. Jeśli to 5% lub więcej, krzywa kapitału opowiada częściowo historię o przestojach giełdy.
Z czasem nauczyłem się ufać bardziej kształtowi braków niż ich liczbie. Zbiór danych z tysiącami rozproszonych luk w martwych godzinach zwykle nie stanowi problemu. Zbiór z kilkoma skupionymi seriami mówi Ci, że coś zawodzi pod obciążeniem, a właśnie tam, gdzie coś zawodzi pod obciążeniem, działa Twoja strategia.
← Wszystkie wpisy
