Data quality
Every venue is captured twice, from two regions, and merged by the venue's own sequence numbers. Anything still missing after the merge is written to a table you can query. Corrections are published as new revisions, and old files are never edited.
Dual-region capture
Each venue stream is captured independently in Tokyo (ap-northeast-1, primary) and Ireland (eu-west-1, secondary). From Phase 2, N. Virginia (us-east-1) is added as a third region. The two capture sessions share nothing: separate hosts, separate AWS accounts, separate network paths and separate connections to the venue. A venue disconnect, a regional network event or a bad deploy on one side leaves the other side recording.
ts_capture. Both copies are kept.seq and keeps one row per message. Its ts_capture is the earliest receive time across the regions, and region names the region that received it first. Messages only the secondary saw are marked region = euw1.gaps. The book is resynced from a fresh venue snapshot.Live and historical merges
The live stream cannot wait for Ireland on every message without adding about 105 ms of delay for a Tokyo-hosted venue. It forwards the first copy of each message to reach it, which for a Tokyo-hosted venue is almost always the primary. When the primary detects a sequence break, the stream holds that symbol for up to 750 ms waiting for the secondary to supply the missing messages, then either emits them (marked region = euw1) or emits a gap notice and resyncs.
The historical merge runs again after the UTC day closes, with no time limit, over both raw copies. It can fill messages that reached Ireland after the live holdback expired. Historical files are therefore at least as complete as the live stream was, and the gaps table lists only what the final merge could not fill.
Sequence checks
A gap you cannot see is worse than a gap you can. We check continuity with the strongest signal each venue provides:
| Venue | Sequence field | Continuity rule |
|---|---|---|
| Hyperliquid | Block height, event index | Every block height present, consecutive. seq encodes height and position in block. |
| Binance | U, u, pu | Spot: first U = previous u + 1. Futures: pu = previous u. |
| Bybit | u, seq | u = previous u + 1; u = 1 marks a venue-side reset. |
| OKX | seqId, prevSeqId | prevSeqId = previous seqId, plus the book checksum. |
| Coinbase | sequence | +1 per product. |
| Deribit | change_id, prev_change_id | prev_change_id = previous change_id. |
| Kraken | Book checksum | CRC32 checksum on every book update. |
For trade streams without a sequence, we compare trade ids where the venue assigns them contiguously, and cross-check trade counts against the venue's REST history after the day closes. Every venue other than Hyperliquid is planned, subject to a data agreement with the exchange; the rules above for those venues follow their public API documentation and will be confirmed with each venue before capture starts.
The gaps table
Every interval that survives the merge without data gets a row in gaps. Gaps are recorded and never filled: we do not interpolate, forward-fill or synthesize rows, and order books are never backfilled from other sources. Intervals where data is present but a quality bound was missed, such as a capture clock outside its error budget, are not gaps; they are published with the daily quality metrics below.
| Column | Type | Description |
|---|---|---|
gap_id | string | Stable id. |
venue, dataset, symbol | enum, enum, string | What was affected. symbol is null when a whole venue stream was down. |
seq_start, seq_end | int64 | Missing sequence range, where the stream is sequenced. |
ts_start, ts_end | int64 | Last good and first good ts_capture around the gap. |
reason | enum | seq_break, checksum_mismatch, venue_outage or disconnect (table below). |
regions_missing | list<string> | Regions that missed it. A row exists only when every capturing region did. |
resync | bool | True when the book was rebuilt from a fresh venue snapshot after the gap. |
detected_by | enum | How the gap was found: seq, checksum, region_compare, snapshot_compare or trade_reconcile. |
detected_at | int64 | When our pipeline recorded the gap. |
revision | int32 | Revision in which the row first appeared. |
note | string | Plain-text detail, such as the venue's maintenance notice id. |
reason | Meaning |
|---|---|
seq_break | The venue's sequence skipped and neither region received the missing messages. |
checksum_mismatch | A venue checksum failed at capture, so the book state since the last good check cannot be trusted. |
venue_outage | The venue stopped publishing, including announced maintenance (the notice reference is in note). |
disconnect | Our connections in every capturing region dropped at the same time while the venue may still have been publishing. |
All columns are on Schema reference. Every Parquet file also states how many gap rows overlap it in its fdc.gap_count metadata, so a pipeline can check completeness without a query.
import pandas as pd
from fdc import Client
fdc = Client()
gaps = pd.DataFrame(fdc.gaps(venue="hyperliquid", symbol="BTC-PERP",
start="2026-06-01", end="2026-07-01"))
# drop trades that fall inside any gap before computing a statistic
trades = fdc.trades(venue="hyperliquid", symbol="BTC-PERP",
start="2026-06-01", end="2026-07-01").to_pandas()
for g in gaps.itertuples():
trades = trades[~trades.ts_capture.between(g.ts_start, g.ts_end)]curl -s https://api.financedatacloud.com/v1/gaps \
-H "Authorization: Bearer $FDC_API_KEY" \
-G -d venue=hyperliquid -d symbol=BTC-PERP \
-d start=2026-06-01 -d end=2026-07-01{"gap_id":"gap_hl_20260611_0007","venue":"hyperliquid","dataset":"book_l3",
"symbol":null,"reason":"venue_outage",
"ts_start":1781179412804113201,"ts_end":1781179431266048873,
"seq_start":1020315471708160,"seq_end":1020315634237439,
"regions_missing":["apne1","euw1"],"resync":true,"detected_by":"seq","detected_at":1781179413560220514,
"revision":1,"note":"API nodes stopped publishing; both regions disconnected within 40 ms."}The gaps table is available on every plan, including test keys.
Venue maintenance
Venues announce maintenance windows through their own channels. We track announcements from each venue's status page and API channels, and treat the window like any other interruption: data before and after is kept, the interval itself is recorded.
- Feed down, trading down. One
gapsrow per affected dataset withreason = venue_outageand the venue's notice reference innote.instrumentsgets ahaltedrow for the window. - Feed up, trading halted. No gap, because the feed is complete. The
instrumentsstatus change tohaltedorreduce_onlytells you the book was not live. - After the window. The book restarts from a venue snapshot, and the gap row has
resync = true. Venues sometimes cancel resting orders during maintenance; the first snapshot after the window shows the book they restored, and the difference is not reported as cancel events. - Funding. If a venue skips or delays a funding settlement during maintenance, the
settledrow appears when the venue publishes it, withts_fundingas the venue states. A skipped settlement has no row.
Revisions
Published data is never modified in place. When we correct something, we publish a new revision of each affected file and record it in the changelog. Code that read revision 1 last month can read revision 1 again today and get the same bytes.
What causes a revision
- A normalization bug: a field mapped incorrectly for a venue or a period.
- Recovered data: messages found in the raw tier after the daily merge, for example from a delayed regional upload.
- Venue corrections: trades the venue later busts or amends, and reference data the venue republishes.
Schema changes are a separate mechanism with their own rules; see the versioning section of Schema reference.
How a revision is published
- The file at the canonical S3 path is replaced by the new revision, with
fdc.revisionincremented in its metadata. - Earlier revisions stay readable as S3 object versions of the same key for 24 months, and through the
revisionparameter on REST and in the SDK. - The changelog entry lists the dataset, venues, symbols, date range, new revision number, cause, and counts of rows added, removed and changed.
- Account contacts get an email on the day of publication. The
gapstable is revised the same way: a gap filled by recovered data is removed in the new revision and remains readable in the earlier one.
DATA REVISION book_l2 · hyperliquid · all symbols
Range 2026-02-03 to 2026-02-05 (UTC dates of ts_capture)
Revision 1 -> 2
Cause Hourly snapshots omitted levels with size below 1e-8.
Rows +18,442 added · 0 removed · 0 changed
Action Re-download if you replay from hourly snapshots in this range.To pin a revision in a pipeline that must be reproducible, pass it explicitly:
fdc.download(dataset="book_l2", venue="hyperliquid", symbol="BTC-PERP",
start="2026-02-03", end="2026-02-06", revision=1, path="./pinned")Quality metrics
We compute the following per venue, dataset and UTC day, and publish them on Status. The same numbers drive our internal alerts.
| Metric | Definition | Target |
|---|---|---|
| Sequence completeness | Share of expected sequence numbers present after the final merge. | ≥ 99.999% |
| Gap time | Seconds covered by gaps rows. | ≤ 1 s/day outside venue events |
| Dual-capture overlap | Share of messages received by both regions. Low overlap means we were running on one leg. | ≥ 99.9% |
| Secondary fill rate | Share of rows supplied by the secondary region. | reported |
| Checksum pass rate | Share of venue-checksummed messages that passed validation at capture. | 100% |
| Clock error, p99 | Capture host clock error bound. Intervals above budget are listed with start and end times. | < 50 µs |
| Capture latency | ts_capture − ts_exchange, primary region, p50 and p99. | reported |
| Publication time | When the day's files were finalized. | by 02:00 UTC |
| Metric | trades | book_l2 | book_l4 |
|---|---|---|---|
| Sequence completeness | 100.000% | 100.000% | 99.9996% |
| Gap time | 0 s | 0 s | 0.4 s |
| Dual-capture overlap | 99.982% | 99.979% | 99.977% |
| Secondary fill rate | 0.0031% | 0.0044% | 0.0047% |
| Clock error, p99 | 11 µs | 11 µs | 11 µs |
| Capture latency p50 / p99 | 18.4 / 77.8 ms | 17.9 / 76.1 ms | 18.2 / 77.0 ms |
If a metric misses its target, the affected day is annotated on Status and in the file metadata. To report something that looks wrong in the data, write to daniel@financedatacloud.com with the venue, symbol and a seq or time range; confirmed issues are fixed through the revision process above.