3 septembrie 2026 · ingineria datelor

Minutele care nu există: goluri, întreruperi și bare cu volum zero în istoricul OHLCV

Minutele care nu există: goluri, întreruperi și bare cu volum zero în istoricul OHLCV

Un an calendaristic de bare de 1 minut ar trebui să aibă 525,600 de rânduri. Extragerea noastră din 2025 pentru contractele perpetue BTCUSDT a venit cu 525,557 — lipsesc 43 de minute, o rată de completitudine de 99.992%. Un contract perpetuu pentru un altcoin cu capitalizare medie, de pe aceeași platformă, din aceeași extragere și pe aceeași cale de procesare, a venit cu 1,247 de minute lipsă: 99.76%. Iar o serie de minute pentru acțiuni americane din același an avea cu aproximativ 427,000 de rânduri mai puțin decât seria crypto, ceea ce nu e deloc un gol, ci pur și simplu o piață care se închide.

Trei dintre aceste numere sunt neinteresante. Cel care spune mai mult decât o mie de cuvinte este 43.

525,600bare de 1 minut într-un an nebisect
0.008%lipsă pentru BTCUSDT, în 2025
61%dintre aceste minute au fost în orele cu volatilitate din decila superioară

Patru lucruri diferite care arată toate ca o gaură în dataframe

Înainte să revenim la cele 43, să clarificăm categoriile, fiindcă majoritatea codului de gestionare a golurilor greșește deja la acest nivel. Când lipsește un rând din fișierul parquet local, nu ai de unde să știi care dintre aceste cauze l-a produs, iar răspunsul corect diferă în fiecare caz.

TipCe s-a întâmplat de faptCum apareRăspunsul corect
Nicio tranzacțiePiața era deschisă; nimeni nu a acceptat spread-ul în acel minutBară cu volum zero (OHLC toate egale, trades=0) pe unele endpoint-uri, rând absent pe altelePăstreaz-o. Este informație reală: nimeni nu a vrut să tranzacționeze.
Platformă indisponibilăMotorul de potrivire a ordinelor era offline, planificat sau nuRând absent, uneori într-o succesiune de mai multe rânduriMarcheaz-o ca veche. Nu tranzacționa peste ea.
Instrument suspendatDepășirea benzii LULD, știri în așteptare, notificare de delistareRând absent, apoi o tranzacție la licitația de redeschidereMarcheaz-o ca veche și tratează redeschiderea ca pe o discontinuitate.
Eroarea taPaginare limitată de rată, o reîncercare care a omis o pagină, o eroare de limită de fus orar în buclăRând absent, imposibil de deosebit de cazurile de mai susDetecteaz-o și descarcă datele din nou. Aceasta e singura pe care o poți remedia.

Platformele nu se comportă la fel pentru primul rând din tabel, iar aici apare problema. Endpoint-ul de lumânări Coinbase omite complet intervalele goale, așa că istoricul unei perechi ilichide e plin de goluri propriu-zise. Endpoint-ul klines Binance oferă de regulă o bară sintetică cu volum 0 și open=high=low=close fixate la ultima tranzacție. Același fapt de bază, două forme diferite, iar un încărcător care reindexează datele pe o grilă completă de minute le transformă pe una în cealaltă fără să te anunțe. Verifică ce face platforma ta înainte să scrii codul de completare a golurilor, nu după.

Cele 1,247 de minute lipsă pentru altcoin erau aproape toate din categoria unu: registru de ordine subțire duminică la 04:00 UTC, fără tranzacții. Enervant, ușor de înțeles și în mare parte inofensiv, pentru că strategia oricum nu ar fi tranzacționat atunci. Tocmai de aceea am încetat să mă mai uit la el și m-am întors la cele 43.

Cele 43 de minute nu erau împrăștiate

Dacă minutele lipsă ar fi fost distribuite uniform, cele 43 de minute dintr-un an ar fi apărut ca 43 de cazuri izolate, câte unul la fiecare opt zile și jumătate, fiecare o eroare de rotunjire. Nu asta am obținut. Au apărut în șase intervale: unul de 19 minute consecutive, unul de 11, două de câte 4 și încă două perechi. Șase evenimente, nu 43 de accidente.

Iar apariția acestor evenimente depinde de ceea ce te interesează. Platformele se defectează când sunt suprasolicitate, iar suprasolicitarea apare când prețul se mișcă. Am împărțit fiecare oră din an după volatilitatea realizată și am verificat unde apăreau minutele lipsă: 61% dintre ele erau în decila superioară. Probabilitatea necondiționată ca un anumit minut să lipsească este 0.008%. Dacă minutul se află într-o oră din decila superioară a volatilității, probabilitatea este de aproximativ 0.05% — de șase ori mai mare, și pe deasupra minutele se și grupează.

Așadar, indicatorul de completitudine din tabloul tău de calitate a datelor măsoară lucrul greșit. 99.992% pare să descrie un set de date la care nu mai trebuie să te gândești. De fapt, descrie o serie completă în orele în care strategia ta nu face nimic și cu goluri în orele în care face totul. Un sistem de momentum care se declanșează la extinderea volatilității are o probabilitate mult mai mare de a întâlni un gol decât sugerează cifra generală, iar golul apare în mijlocul tranzacției.

Ce face forward-fill câteva rânduri mai târziu

Iată eroarea care m-a făcut să scriu acest articol. Luăm intervalul de 19 minute. Igienă standard: reindexăm pe grila completă de minute, aplicăm forward-fill la OHLC folosind ultimul close și setăm volumul la zero. Seria este acum continuă, iar indicatorii rulează fără niciun NaN la vedere.

Aceste 19 bare au high == low == close. True range este zero pentru fiecare dintre ele. Un ATR(14) calculat pe această fereastră, pornind de la o valoare de aproximativ 240 USDT înainte de întrerupere, scade spre aproximativ 34 până când platforma revine — cele cinci bare reale rămase în fereastră susțin întreaga medie. Apoi furnizăm valoarea unui mecanism de dimensionare a poziției scalat după volatilitate, de tipul obișnuit size = risk_budget / ATR. Mărimea poziției crește de șapte ori.

Următoarea bară reală este tranzacția de la redeschidere și nu e deloc o bară liniștită. În cazul nostru, s-a deschis la 1.8% distanță de ultimul close de dinaintea întreruperii. Backtestul a deschis fără probleme o poziție de 7x chiar înaintea unui gap de 1.8%, pe o execuție care nu ar fi putut exista, la un preț pe care nimeni nu îl cota. Acea singură tranzacție sintetică valora mai mult decât o lună de P&L legitim în curba de capital, în direcția greșită — și a fost creată în întregime de o linie de curățare a datelor scrisă ca să facă dataframe-ul ordonat.

Nici eliminarea rândurilor în locul completării lor nu rezolvă problema; e aceeași eroare cu altă pălărie. Dacă le elimini, ferestrele de retrospectivă bazate pe indici întregi te induc în eroare: un „EMA de 20 de bare” acoperă acum 39 de minute de timp real peste întrerupere, randamentul dintre bare peste graniță este întreaga variație de 1.8% tratată ca o mișcare de un minut, iar orice estimare a volatilității per bară o citește ca pe un eveniment de 60 sigma. Nimic nu te avertizează. Indicele este în continuare monoton.

La reeșantionare, problema devine invizibilă

Majoritatea cercetărilor nu rulează pe bare de 1 minut, ci pe date agregate, iar agregarea spală problema. Dacă reeșantionăm la 5 minute, un gol de 19 minute devine patru bare, dintre care prima și ultima sunt parțiale. Pandas va calcula valori OHLC care par perfect rezonabile pe baza a două minute disponibile și le va eticheta identic cu o bară construită din cinci minute. Rezultatul nu oferă nimic care să le deosebească.

Cea mai simplă soluție pe care o cunosc: păstrăm o coloană bars_in_window la fiecare reeșantionare și nu o eliminăm niciodată. Un număr întreg pentru fiecare rând, iar toate întrebările ulterioare despre fiabilitatea unei bare devin verificabile. Păstrăm și seconds_since_last_real_print, care conține aceeași informație într-o formă pe care stratul de execuție o poate folosi.

Politica noastră, atât cât există

Iată ce aplică acum agenții noștri, în această ordine:

  1. Nu reindexa niciodată fără să anunți. Încărcătorul emite un manifest al golurilor — început, sfârșit, durată și categoria din cele patru în care crede că se încadrează fiecare. Dacă un interval de minute lipsă are sub 3 bare, iar volumul barelor din jur este redus, îl considerăm un minut fără tranzacții. Orice interval mai lung în orele active este tratat ca o întrerupere până la proba contrarie.
  2. Descarcă din nou înainte să interpretezi. Jumătate dintre golurile noastre inițiale proveneau din erori de paginare. O a doua extragere de la un alt endpoint sau furnizor rezolvă categoria patru și reduce problema înainte să fie nevoie de orice judecată.
  3. Folosește un prag de prospețime, nu completarea golurilor. Strategia primește o intrare data_age și o regulă strictă: nu deschide poziții noi dacă ultima tranzacție reală este mai veche de N bare, iar pozițiile deschise se închid la redeschidere doar printr-un ordin market, cu un haircut explicit pentru riscul de gap inclus în preț. Execuțiile care nu ar fi putut avea loc sunt mai rele decât tranzacțiile care nu au avut loc.
  4. Indicatorii văd NaN, nu ficțiune. Prețurile completate prin forward-fill nu ajung niciodată la stratul de caracteristici. Dacă ATR nu poate fi calculat, rămâne nedefinit, iar nedefinit înseamnă poziție neutră. O eroare vizibilă e mai bună decât un 7x tăcut.
  5. Raportează performanța condiționată de goluri. Fiecare backtest pe care îl publicăm arată P&L-ul principal, alături de P&L-ul fără tranzacțiile din apropierea întreruperilor. Dacă acele tranzacții susțin rezultatul, atunci rezultatul este un artefact al datelor.

Un audit rapid pe care îl poți face chiar azi: grupează minutele lipsă în intervale consecutive, apoi verifică ce fracțiune din tranzacțiile backtestului se deschid sau se închid la cel mult 30 de minute de limita unui interval. Sub 1% înseamnă că probabil golurile nu influențează rezultatul. Dacă sunt 5% sau mai multe, curba de capital spune în parte o poveste despre perioadele în care platforma de schimb a fost indisponibilă.

Indiciul în care am ajuns să am încredere este forma lipsei datelor, nu amploarea ei. Un set de date cu mii de goluri izolate în orele inactive este de obicei în regulă. Un set de date cu câteva grupuri compacte îți spune că ceva cedează sub sarcină, iar strategia ta operează tocmai acolo unde acel ceva cedează.

goluri OHLCVingineria datelorbacktestingreeșantionarecontracte futures crypto
← Toate articolele