437 lines
15 KiB
Markdown
437 lines
15 KiB
Markdown
|
|
# Gap Checker and Backfiller Repair
|
||
|
|
|
||
|
|
## Purpose
|
||
|
|
|
||
|
|
Continue the time-series gap checker and backfiller work
|
||
|
|
from PRs/issues `#62`, `#71`, and `#75`, prioritizing data
|
||
|
|
correctness and crash safety before annotation UX or
|
||
|
|
rendering performance.
|
||
|
|
|
||
|
|
Source baseline:
|
||
|
|
|
||
|
|
- branch: `flake_update`
|
||
|
|
- commit: `c6ec3d41`
|
||
|
|
- gap handoff:
|
||
|
|
`notes_to_self/claudy/gapsannotator_sesh.md`
|
||
|
|
- primary code:
|
||
|
|
`piker/tsp/_anal.py`, `piker/tsp/_history.py`,
|
||
|
|
`piker/storage/nativedb.py`, and
|
||
|
|
`piker/tsp/_annotate.py`
|
||
|
|
|
||
|
|
## Goals
|
||
|
|
|
||
|
|
1. Make gap detection operate on sorted, unique,
|
||
|
|
validated OHLCV frames with fresh derived columns.
|
||
|
|
2. Make backfill completion, frame boundaries, and null
|
||
|
|
repair deterministic under exhaustion and cancellation.
|
||
|
|
3. Prevent a bounded or transient SHM view from
|
||
|
|
destructively replacing older persisted history.
|
||
|
|
4. Persist normalized history through crash-safe atomic
|
||
|
|
file replacement.
|
||
|
|
5. Distinguish expected venue closures from suspicious
|
||
|
|
missing data without production breakpoints.
|
||
|
|
6. Generate correct annotation specs before optimizing
|
||
|
|
the remaining client-side Polars bottleneck.
|
||
|
|
|
||
|
|
## Confirmed Baseline Defects
|
||
|
|
|
||
|
|
These are current-code findings to characterize in tests,
|
||
|
|
not assumptions inferred from historical task state.
|
||
|
|
|
||
|
|
### Gap normalization
|
||
|
|
|
||
|
|
- `dedupe()` computes `time_prev`, `s_diff`, and
|
||
|
|
`dt_diff` before sorting/deduplication, then returns
|
||
|
|
stale derived columns in the normalized frame
|
||
|
|
(`piker/tsp/_anal.py:683-729`).
|
||
|
|
- Negative/out-of-order deltas are treated as ordinary
|
||
|
|
gaps because `detect_time_gaps()` filters on
|
||
|
|
`abs(s_diff)` (`piker/tsp/_anal.py:582-638`).
|
||
|
|
- `get_null_segs()` drops single zero rows and its Polars
|
||
|
|
path does not define all state used by the shared path
|
||
|
|
(`piker/tsp/_anal.py:227-471`).
|
||
|
|
- `iter_null_segs()` ignores a supplied `null_segs`
|
||
|
|
result and rescans the frame (`piker/tsp/_anal.py:474-546`).
|
||
|
|
|
||
|
|
### Backfill control and boundaries
|
||
|
|
|
||
|
|
- `DataUnavailable` returns before `bf_done.set()`, while
|
||
|
|
`tsdb_backfill()` later waits forever on that event
|
||
|
|
(`piker/tsp/_history.py:516-536,1193-1212,1302-1308`).
|
||
|
|
- Empty arrays are indexed before the nominal null-frame
|
||
|
|
branch can handle them (`piker/tsp/_history.py:538-601`).
|
||
|
|
- Provider frame seams have no explicit inclusive/exclusive
|
||
|
|
contract, allowing duplicate adjacent timestamps in SHM.
|
||
|
|
- Cancellation may occur after SHM mutation but before
|
||
|
|
cursor and persistence bookkeeping
|
||
|
|
(`piker/tsp/_history.py:661-789`).
|
||
|
|
|
||
|
|
### Null repair
|
||
|
|
|
||
|
|
- Null query length is derived from an absolute endpoint,
|
||
|
|
not the null-span width (`piker/tsp/_history.py:278-287`).
|
||
|
|
- Fallback repair includes and mutates the valid leading
|
||
|
|
boundary row (`piker/tsp/_history.py:345-367`).
|
||
|
|
- Null repair and reverse backfill share one provider
|
||
|
|
client concurrently without enforcing the backend's
|
||
|
|
declared request limits.
|
||
|
|
|
||
|
|
### Persistence
|
||
|
|
|
||
|
|
- Every iteration passes bounded `shm.array` to
|
||
|
|
`write_ohlcv()`, while NativeDB replaces the complete
|
||
|
|
parquet file. Older rows that do not fit in SHM can be
|
||
|
|
lost (`piker/tsp/_history.py:722-789`,
|
||
|
|
`piker/storage/nativedb.py:305-368`).
|
||
|
|
- NativeDB writes directly to the final path and updates
|
||
|
|
its cache before the write succeeds; interruption can
|
||
|
|
leave disk and cache partial or inconsistent.
|
||
|
|
- Intentional zero-filled SHM space may be persisted while
|
||
|
|
null repair and reverse backfill run concurrently.
|
||
|
|
|
||
|
|
### Annotation contract
|
||
|
|
|
||
|
|
- `markup_gaps()` performs a full Polars filter per gap and
|
||
|
|
remains the measured client bottleneck
|
||
|
|
(`piker/tsp/_annotate.py:139-217`).
|
||
|
|
- Its missing-prior-row fallback selects the current row,
|
||
|
|
and `copysign(1, 0)` makes the flat-price branch
|
||
|
|
unreachable (`piker/tsp/_annotate.py:166-239`).
|
||
|
|
- Remote/reposition timestamp lookups index
|
||
|
|
`searchsorted()` results before excluding values equal
|
||
|
|
to `len(array)`.
|
||
|
|
|
||
|
|
## Design Invariants
|
||
|
|
|
||
|
|
All implementation phases preserve these rules:
|
||
|
|
|
||
|
|
1. Persisted provider rows have nonzero, finite timestamps.
|
||
|
|
2. Normalized timestamps are strictly increasing and
|
||
|
|
unique.
|
||
|
|
3. Time-derived columns are computed only after final
|
||
|
|
sorting and duplicate resolution.
|
||
|
|
4. A detected gap explicitly carries its left and right
|
||
|
|
endpoints; consumers do not infer the left row from
|
||
|
|
`index - 1`.
|
||
|
|
5. Provider frame metadata equals the normalized array's
|
||
|
|
actual first and last timestamps.
|
||
|
|
6. Internal middleware uses one documented boundary
|
||
|
|
convention; provider adapters translate into it.
|
||
|
|
7. Expected sparse venue closures are represented by
|
||
|
|
timestamp distance, not persisted zero or synthetic
|
||
|
|
bars.
|
||
|
|
8. Storage merges preserve all unaffected older rows.
|
||
|
|
9. A storage commit becomes visible atomically, and cache
|
||
|
|
state changes only after disk commit succeeds.
|
||
|
|
10. Rendering or annotation failure cannot change the
|
||
|
|
checker or persistence result.
|
||
|
|
|
||
|
|
## Phase 0: Deterministic Characterization
|
||
|
|
|
||
|
|
Add synthetic OHLCV fixtures based on
|
||
|
|
`piker.data._source.def_iohlcv_fields`; avoid brokers,
|
||
|
|
networking, real actor trees, and Qt in the core suite.
|
||
|
|
|
||
|
|
### Analysis tests
|
||
|
|
|
||
|
|
Create `tests/test_tsp_analysis.py` covering:
|
||
|
|
|
||
|
|
- uniform 1-second and 60-second series;
|
||
|
|
- one and multiple positive gaps;
|
||
|
|
- duplicate plus out-of-order timestamps;
|
||
|
|
- stale derived columns after sort/dedupe;
|
||
|
|
- zero, one, and multiple null segments;
|
||
|
|
- nulls at frame boundaries and nonzero absolute indexes;
|
||
|
|
- NumPy and Polars inputs;
|
||
|
|
- smart-dedupe conflict cases, including differing closes.
|
||
|
|
|
||
|
|
### Backfill tests
|
||
|
|
|
||
|
|
Create `tests/test_history_backfill.py` with scripted
|
||
|
|
`get_hist`, fake SHM, fake sampler stream, and recording
|
||
|
|
storage implementations. Characterize:
|
||
|
|
|
||
|
|
- `DataUnavailable` completion without a hang;
|
||
|
|
- a genuinely empty frame;
|
||
|
|
- exact adjacency, overlap, and a positive storage gap;
|
||
|
|
- inclusive provider seam duplication;
|
||
|
|
- one-row and two-row startup history;
|
||
|
|
- cancellation after query, after SHM mutation, and
|
||
|
|
before persistence;
|
||
|
|
- one and multiple null rows with boundary preservation.
|
||
|
|
|
||
|
|
### Storage tests
|
||
|
|
|
||
|
|
Create `tests/test_storage_nativedb.py` using `tmp_path`:
|
||
|
|
|
||
|
|
- NumPy and Polars round trips;
|
||
|
|
- preservation of history older than the SHM window;
|
||
|
|
- duplicate conflict policy;
|
||
|
|
- write failure leaves the prior parquet readable;
|
||
|
|
- cache changes only after a successful commit;
|
||
|
|
- separate FQME/timeframe series do not interfere.
|
||
|
|
|
||
|
|
### Annotation tests
|
||
|
|
|
||
|
|
Create `tests/test_gap_annotations.py` around a fake
|
||
|
|
`AnnotCtl`, without Qt initially:
|
||
|
|
|
||
|
|
- left-close/right-open rectangle endpoints;
|
||
|
|
- up, down, and flat price direction;
|
||
|
|
- first-row and missing-prior-row behavior;
|
||
|
|
- empty gaps and out-of-range timestamps;
|
||
|
|
- deterministic batched rect/arrow/text payloads.
|
||
|
|
|
||
|
|
Gate: every confirmed defect above has a focused failing
|
||
|
|
test or a written reason it requires a later integration
|
||
|
|
test.
|
||
|
|
|
||
|
|
## Phase 1: Canonical Frame and Gap Model
|
||
|
|
|
||
|
|
Refactor `piker/tsp/_anal.py` around one normalization
|
||
|
|
order:
|
||
|
|
|
||
|
|
1. validate required columns and finite timestamps;
|
||
|
|
2. sort by `time`;
|
||
|
|
3. resolve duplicate timestamps under an explicit policy;
|
||
|
|
4. recompute all datetime/delta columns with `with_dts()`;
|
||
|
|
5. reject/report non-positive ordering deltas;
|
||
|
|
6. detect positive sample-step gaps.
|
||
|
|
|
||
|
|
Keep the first implementation small: a normalized Polars
|
||
|
|
frame may remain the contract, but each gap row must carry
|
||
|
|
explicit `time_prev`, `index_prev`, `close_prev`, current
|
||
|
|
`time/index/open`, duration, and missing-sample count.
|
||
|
|
|
||
|
|
Rewrite null grouping from extracted index/time arrays so
|
||
|
|
the NumPy and Polars paths share one implementation. Treat
|
||
|
|
a single null row as a normal one-row segment and honor a
|
||
|
|
precomputed `null_segs` argument without rescanning.
|
||
|
|
|
||
|
|
Gate: analysis tests pass and no deduped/sorted frame
|
||
|
|
retains stale delta columns.
|
||
|
|
|
||
|
|
## Phase 2: Structured Backfill Completion
|
||
|
|
|
||
|
|
Refactor `start_backfill()` and `tsdb_backfill()` before
|
||
|
|
changing persistence semantics:
|
||
|
|
|
||
|
|
- remove completion events that are awaited only after the
|
||
|
|
owning nursery has already joined its children, or make
|
||
|
|
completion a guaranteed `finally` outcome;
|
||
|
|
- represent `completed`, `provider_exhausted`,
|
||
|
|
`cancelled`, and `failed` as explicit results;
|
||
|
|
- check empty arrays before indexing endpoints;
|
||
|
|
- normalize each provider frame and recompute endpoint
|
||
|
|
metadata from the normalized data;
|
||
|
|
- define and enforce a disjoint seam between each new
|
||
|
|
reverse frame and the already-published frame;
|
||
|
|
- update cursor state immediately with the SHM mutation,
|
||
|
|
before any optional notification checkpoint;
|
||
|
|
- serialize reverse backfill and null repair unless a
|
||
|
|
backend capability explicitly permits concurrent
|
||
|
|
history requests;
|
||
|
|
- enforce declared backend request/rate limits.
|
||
|
|
|
||
|
|
Prefer structured task return values over actor-local
|
||
|
|
events whose only consumer is the parent task.
|
||
|
|
|
||
|
|
Gate: exhaustion and cancellation tests terminate within
|
||
|
|
their timeout, and SHM timestamps remain strictly
|
||
|
|
increasing across all frame seams.
|
||
|
|
|
||
|
|
## Phase 3: Non-Destructive Atomic NativeDB Writes
|
||
|
|
|
||
|
|
Stop treating the bounded SHM view as the full database.
|
||
|
|
|
||
|
|
Add a NativeDB merge/update operation that:
|
||
|
|
|
||
|
|
- accepts only the normalized provider frame being
|
||
|
|
committed;
|
||
|
|
- loads or uses the cached persisted frame;
|
||
|
|
- merges by timestamp while preserving unaffected rows;
|
||
|
|
- uses the historical provider bar as authoritative at a
|
||
|
|
closed-bar seam;
|
||
|
|
- validates ordering, uniqueness, schema, and nonzero
|
||
|
|
timestamps before writing;
|
||
|
|
- writes to a same-directory temporary parquet;
|
||
|
|
- reopens and validates the temporary file;
|
||
|
|
- atomically replaces the final path;
|
||
|
|
- updates `_dfs` only after replacement succeeds;
|
||
|
|
- serializes writers per `(fqme, timeframe)`.
|
||
|
|
|
||
|
|
Backfill commits the validated frame/delta, never a live
|
||
|
|
`shm.array` snapshot containing mutable current bars or
|
||
|
|
intentional zero space.
|
||
|
|
|
||
|
|
Gate: a persisted series can only grow backward or replace
|
||
|
|
explicitly overlapping timestamps; its earliest timestamp
|
||
|
|
never moves forward during ordinary backfill.
|
||
|
|
|
||
|
|
## Phase 4: Exact Null Repair
|
||
|
|
|
||
|
|
Use the canonical null spans from Phase 1:
|
||
|
|
|
||
|
|
- query using explicit boundary timestamps;
|
||
|
|
- trim provider responses to the actual missing interval;
|
||
|
|
- write only null rows, never either valid boundary row;
|
||
|
|
- validate repaired timestamps and OHLC fields before
|
||
|
|
publication or persistence;
|
||
|
|
- leave expected venue closures sparse;
|
||
|
|
- do not persist synthetic flat bars without a durable
|
||
|
|
quality marker and an explicit schema decision.
|
||
|
|
|
||
|
|
If provider data is unavailable, retain and report the
|
||
|
|
missing interval instead of silently converting it into
|
||
|
|
apparently valid history.
|
||
|
|
|
||
|
|
Gate: single and multi-row repairs preserve both boundary
|
||
|
|
bars and either eliminate exactly the target nulls or
|
||
|
|
return an unresolved-gap result.
|
||
|
|
|
||
|
|
## Phase 5: Venue and Provider Classification
|
||
|
|
|
||
|
|
Apply gap classification after canonical detection rather
|
||
|
|
than embedding it in individual warning paths.
|
||
|
|
|
||
|
|
For IB:
|
||
|
|
|
||
|
|
- correct expiry comparison and weekday constants;
|
||
|
|
- classify internal positive gaps as well as trailing
|
||
|
|
request-coverage gaps;
|
||
|
|
- require closure interval alignment, not merely the
|
||
|
|
presence of a weekend/holiday somewhere in the gap;
|
||
|
|
- cover overnight sessions, weekends, holidays, DST,
|
||
|
|
active/expired futures, and unknown calendars;
|
||
|
|
- replace production `breakpoint()` calls with structured
|
||
|
|
data-quality results/errors.
|
||
|
|
|
||
|
|
Provider adapters must return normalized endpoint metadata
|
||
|
|
and translate their native inclusive/exclusive APIs into
|
||
|
|
the middleware boundary contract.
|
||
|
|
|
||
|
|
Gate: every gap is classified as expected closure,
|
||
|
|
suspicious missing data, provider exhaustion, or invalid
|
||
|
|
ordering; no production path pauses interactively.
|
||
|
|
|
||
|
|
## Phase 6: Annotation Correctness and Performance
|
||
|
|
|
||
|
|
Extract a synchronous, Qt-free spec builder from
|
||
|
|
`markup_gaps()`:
|
||
|
|
|
||
|
|
```python
|
||
|
|
build_gap_annotation_specs(frame, gaps)
|
||
|
|
```
|
||
|
|
|
||
|
|
It consumes the explicit gap endpoints from Phase 1 and:
|
||
|
|
|
||
|
|
- extracts needed columns once;
|
||
|
|
- replaces per-gap `wdts.filter()` with a vectorized join
|
||
|
|
or indexed lookup;
|
||
|
|
- handles equal prices explicitly;
|
||
|
|
- preserves previous-close/current-open semantics;
|
||
|
|
- styles expected closures separately from suspicious
|
||
|
|
gaps;
|
||
|
|
- emits deterministic rect/arrow/text batches.
|
||
|
|
|
||
|
|
Then fix remote lookup/reposition bounds by masking
|
||
|
|
`searchsorted()` results before indexing and enforce both
|
||
|
|
FQME and timeframe ownership during redraw.
|
||
|
|
|
||
|
|
Benchmark the same 1,285-gap workload from the Claude
|
||
|
|
handoff. Treat these as local benchmark goals, not portable
|
||
|
|
CI limits:
|
||
|
|
|
||
|
|
- client spec build under 20 ms;
|
||
|
|
- server preparation under 10 ms;
|
||
|
|
- one composite gap graphics item;
|
||
|
|
- no full-frame Polars filter per gap;
|
||
|
|
- total local creation near or below 150 ms.
|
||
|
|
|
||
|
|
Gate: annotation contract tests pass before performance
|
||
|
|
numbers are accepted.
|
||
|
|
|
||
|
|
## Phase 7: Crash and Provider Qualification
|
||
|
|
|
||
|
|
Run deterministic tests first, then the historical manual
|
||
|
|
matrix from issue `#62` on disposable storage:
|
||
|
|
|
||
|
|
- fresh full backfill;
|
||
|
|
- append to existing history;
|
||
|
|
- graceful Ctrl-C;
|
||
|
|
- network interruption;
|
||
|
|
- SIGTERM and SIGKILL during query and parquet commit;
|
||
|
|
- restart and revalidate after every interruption.
|
||
|
|
|
||
|
|
Start with Binance/Kraken/Kucoin, then run IB only with an
|
||
|
|
available account and gateway. Record before/after earliest
|
||
|
|
timestamp, latest timestamp, row count, unique count, null
|
||
|
|
count, non-positive deltas, and parquet readability.
|
||
|
|
|
||
|
|
## Verification Commands
|
||
|
|
|
||
|
|
Use an outer process timeout because actor teardown has a
|
||
|
|
known second-runtime wedge:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
timeout -k 5 30 python -m pytest -q \
|
||
|
|
tests/test_tsp_analysis.py \
|
||
|
|
tests/test_storage_nativedb.py
|
||
|
|
|
||
|
|
timeout -k 5 60 python -m pytest -q \
|
||
|
|
tests/test_history_backfill.py
|
||
|
|
|
||
|
|
timeout -k 5 60 python -m pytest -q \
|
||
|
|
tests/test_gap_annotations.py
|
||
|
|
|
||
|
|
CI=1 timeout -k 20 120 python -m pytest -q -rs \
|
||
|
|
tests/test_feeds.py
|
||
|
|
```
|
||
|
|
|
||
|
|
Run live/network tests separately from deterministic tests.
|
||
|
|
Do not automate `piker store anal` or `piker store ldshm`:
|
||
|
|
both may pause interactively or mutate attached data.
|
||
|
|
|
||
|
|
## Commit Boundaries
|
||
|
|
|
||
|
|
Keep implementation reviewable in this order:
|
||
|
|
|
||
|
|
1. synthetic fixtures and analysis regressions;
|
||
|
|
2. canonical normalization and null grouping;
|
||
|
|
3. backfill completion and frame-boundary fixes;
|
||
|
|
4. NativeDB merge and atomic persistence;
|
||
|
|
5. exact null repair;
|
||
|
|
6. IB/provider gap classification;
|
||
|
|
7. annotation contract fixes and vectorization;
|
||
|
|
8. qualification notes and documentation.
|
||
|
|
|
||
|
|
Each boundary must leave deterministic tests green. Do not
|
||
|
|
mix renderer optimization into storage/backfill commits.
|
||
|
|
|
||
|
|
## Deferred
|
||
|
|
|
||
|
|
The following remain follow-ups until the correctness
|
||
|
|
gates above pass:
|
||
|
|
|
||
|
|
- chart context-menu/manual piecewise refill UX;
|
||
|
|
- automatic checker integration into every live chart;
|
||
|
|
- compact columnar annotation IPC;
|
||
|
|
- persistent synthetic-bar quality metadata/schema;
|
||
|
|
- full Deribit history repair and broad provider cleanup;
|
||
|
|
- Marketstore behavior changes;
|
||
|
|
- long-term incremental parquet partitioning.
|
||
|
|
|
||
|
|
## Source References
|
||
|
|
|
||
|
|
- `gitea/62.md`: workaround-era backfill and manual test
|
||
|
|
history
|
||
|
|
- `gitea/71.md`: IB venue closure behavior and pending
|
||
|
|
account verification
|
||
|
|
- `gitea/75.md`: gap annotation, storage races, and
|
||
|
|
backfiller follow-ups
|
||
|
|
- `notes_to_self/claudy/gapsannotator_sesh.md`: current
|
||
|
|
annotation benchmark and client bottleneck
|
||
|
|
- https://github.com/pikers/piker/pull/486
|
||
|
|
- https://github.com/pikers/piker/pull/446
|
||
|
|
- https://github.com/pikers/piker/issues/536
|