fix(fetch): window-scope the gap-proven display — the two counts must share a window - #330
Merged
Conversation
… the docstring claimed it, now it's pinned
eaitbrahim
added a commit
that referenced
this pull request
Aug 17, 2026
Version bump across all six distributions. Patch: exactly one change since v0.8.0 (#330), the window-scoped gap-proven display in keel fetch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
keel fetchprinted, for real series today (2026-08-17):A negative "proven absent" count is an impossible claim — you cannot prove fewer than zero
gaps absent — and it MASKED the real state: for those series the whole-series unexplained-gap
count was 160 hourly / 6 daily (some gaps are NOT proven absent), plus 2+1 missing bars sit
OUTSIDE the assessed window, yet the display read as if reconciliation had happened.
Root cause:
_assess_productscomputedcoverage()'s gap count over a WINDOW-BOUNDED read(
get_candles(product, gran, start_ts, None)) butunexplained_gap_count()over the WHOLEseries, then
_print_freshnesssubtracted one from the other. Any bar missing older thanstart_tsdrove the suffix negative.The fix makes the two counts share a window by construction:
unexplained_gap_countgains an optionalstart_ts(defaultNone= whole series,behavior identical to today). Given
start_ts, it reads the same bounded slicecoverage()reads, so detection covers the same bars. The docstring records the honestboundary caveat: a hole that straddles
start_tsis not interior to the bounded slice, soneither count can see it — the window can only under-report a hole crossing its own start
boundary, never claim one absent, and the whole-series default still sees it.
_assess_productspassesstart_ts;_print_freshnesskeepsproven = gaps - unexplainedwith NO
max(0, ...)clamp — the point is the shared window, and a clamp would only re-hidea future mismatch.
--fail-on-gapsscope deliberately UNCHANGED (whole series). Reading thefetchflow:the flag judged
_assess_products' whole-series return, and the advice it prints (run keel fetch --repair-gaps) points atrepair_series, which itself reads the whole series — so ahole older than the fetch window is still fixable, still unproven, and still the flag's
business. The flag now computes its whole-series count where it is judged (the
--checkbranch) instead of riding the display's window-bounded number, and a test pins that a gap
lying entirely OUTSIDE the fetch window still fails
--check --fail-on-gaps(and that plain--checkstill reports it in the closing "N have UNEXPLAINED gaps" message). The default ofunexplained_gap_countstays whole-series and is pinned by test.Tests-first evidence
Red first (
uv run pytest tests/data/test_fetch_cli.py tests/data/test_gap_repair.py -q):Failing for the right reasons:
test_the_gap_suffix_shares_the_fetch_window_so_it_cannot_go_negative— assertion failurewhose diff SHOWS the bug (the exact old rendered line):
GAPS SOL-USD ONE_DAY n=363 0 bars behind, 2 internal gaps (-1 proven absent at venue)test_every_assessed_row_keeps_proven_absent_never_negative—AssertionError: ('SOL-USD', ONE_DAY, 2, 3): the field shape (window gaps 2 < whole-series unexplained 3).test_unexplained_gap_count_bounded_ignores_holes_before_start_tsandtest_a_hole_straddling_start_ts_is_invisible_to_the_bounded_count—TypeError: unexplained_gap_count() got an unexpected keyword argument 'start_ts'(theinterface under test not existing yet).
test_fail_on_gaps_still_judges_holes_older_than_the_fetch_windowwas BORN GREEN, onpurpose and stated in its docstring: it pins preserved
--fail-on-gapssemantics, not theregression — it must pass both before and after or the flag's scope changed.
Fixture = the field repro: (a) an in-window hole recorded absent in
candle_gap_probes,(b) an in-window hole NOT recorded, (c) a 2-bar hole entirely older than
start_ts.Before → after, exact rendered line:
The suffix now claims exactly the one proven bar; the unproven in-window hole rides in the
2 internal gapscount unclaimed; the outside-window hole perturbs neither number, and theclosing message still carries the whole-series truth (
1 have UNEXPLAINED gaps).Gates (all must pass)
uv run ruff checkclean —All checks passed!(overkeel tests packages)uv run mypyclean —Success: no issues found in 237 source filesuv run pytest -qgreen —2862 passed, 1 skipped in 36.56sScope check
function's optional parameter only. No rail, no rule, no classification, no broker
surface, no default changed (
--fail-on-gapswhole-series semantics preserved andpinned by test).