Skip to content

keel v0.8.1

Choose a tag to compare

@github-actions github-actions released this 17 Aug 19:15
· 62 commits to main since this release
498e02c

Built from 498e02c. Version binds to this hash:
keel --version reports keel 0.8.1+498e02c8f571 [release].

Install

Download all wheels from this release into one directory, then install the
keel_trader wheel by path:

pip install --find-links . ./keel_trader-0.8.1-py3-none-any.whl
keel versions

keel versions — not keel --version — is the check: it reports every
keel distribution in the venv and exits non-zero if a sibling was left behind at
an older version, which --version cannot see. Upgrading an existing
deployment: see "Deploying a new version" in the README.

⚠️ Never install by bare name. The distribution is keel-trader; the name
keel on PyPI belongs to an unrelated project, so pip install keel fetches
someone else's package. A build reporting DIRTY or [checkout] is not this
release and must not be run against live funds.

Configure

config.yaml is attached to this release: the production config, in
auto_trade.mode: confirm — keel previews every order and waits for your
approval. Drop it beside the install (or run keel init-config --live), put
your CDP key in a git-ignored .env, then:

keel migrate     # existing database: apply schema migrations
keel init        # fresh deployment: write config + seed candidate rules

Seeded rules start as candidate and trade nothing until you promote them.

Other changes

fix(fetch): window-scope the gap-proven display — the two counts must share a window (#330)

What & why

keel fetch printed, for real series today (2026-08-17):

GAPS  SOL-USD  ONE_HOUR  n=43640  1 bars behind, 158 internal gaps (-2 proven absent at venue)
GAPS  SOL-USD  ONE_DAY   n=1819   0 bars behind, 5 internal gaps (-1 proven absent at venue)

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_products computed coverage()'s gap count over a WINDOW-BOUNDED read
(get_candles(product, gran, start_ts, None)) but unexplained_gap_count() over the WHOLE
series, then _print_freshness subtracted one from the other. Any bar missing older than
start_ts drove the suffix negative.

The fix makes the two counts share a window by construction:

  • unexplained_gap_count gains an optional start_ts (default None = whole series,
    behavior identical to today). Given start_ts, it reads the same bounded slice
    coverage() reads, so detection covers the same bars. The docstring records the honest
    boundary caveat: a hole that straddles start_ts is not interior to the bounded slice, so
    neither 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_products passes start_ts; _print_freshness keeps proven = gaps - unexplained
    with NO max(0, ...) clamp — the point is the shared window, and a clamp would only re-hide
    a future mismatch.

--fail-on-gaps scope deliberately UNCHANGED (whole series). Reading the fetch flow:
the flag judged _assess_products' whole-series return, and the advice it prints (run keel fetch --repair-gaps) points at repair_series, which itself reads the whole series — so a
hole 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 --check
branch) 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
--check still reports it in the closing "N have UNEXPLAINED gaps" message). The default of
unexplained_gap_count stays 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):

4 failed, 40 passed in 1.36s

Failing for the right reasons:

  • test_the_gap_suffix_shares_the_fetch_window_so_it_cannot_go_negative — assertion failure
    whose 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_negativeAssertionError: ('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_ts and
    test_a_hole_straddling_start_ts_is_invisible_to_the_bounded_count
    TypeError: unexplained_gap_count() got an unexpected keyword argument 'start_ts' (the
    interface under test not existing yet).
  • test_fail_on_gaps_still_judges_holes_older_than_the_fetch_window was BORN GREEN, on
    purpose and stated in its docstring: it pins preserved --fail-on-gaps semantics, not the
    regression — 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:

- GAPS     SOL-USD      ONE_DAY   n=363     0 bars behind, 2 internal gaps (-1 proven absent at venue)
+ GAPS     SOL-USD      ONE_DAY   n=363     0 bars behind, 2 internal gaps (1 proven absent at venue)

The suffix now claims exactly the one proven bar; the unproven in-window hole rides in the
2 internal gaps count unclaimed; the outside-window hole perturbs neither number, and the
closing message still carries the whole-series truth (1 have UNEXPLAINED gaps).

  • Tests written first, seen failing for the right reason

Gates (all must pass)

  • uv run ruff check clean — All checks passed! (over keel tests packages)
  • uv run mypy clean — Success: no issues found in 237 source files
  • uv run pytest -q green — 2862 passed, 1 skipped in 36.56s

Scope check

  • This PR touches a rail or a default classification — unchecked: display + one pure
    function's optional parameter only. No rail, no rule, no classification, no broker
    surface, no default changed (--fail-on-gaps whole-series semantics preserved and
    pinned by test).
  • New dependency added — none.

chore(release): 0.8.1 (#331)

What & why

Version bump across all six distributions. Patch release: exactly one change since v0.8.0 — #330, the window-scoped gap-proven display. keel fetch was computing proven = row.gaps - unexplained from two counts over different windows (coverage's window-bounded read vs. the whole-series unexplained count), printing impossible negative suffixes like "158 internal gaps (-2 proven absent at venue)" for real series, and masking that those gaps were largely unproven. The fix makes both counts read the identical slice; --fail-on-gaps keeps its whole-series scope deliberately (the --repair-gaps remediation reads unbounded too).

Tests-first evidence

tests/test_packaging.py and tests/test_python_floor.py pin the invariants this bump touches; after the bump and uv lock: 2862 passed, 1 skipped; ruff clean; mypy clean.

Gates

  • uv run ruff check keel tests packages — All checks passed!
  • uv run mypy — Success: no issues found in 237 source files
  • uv run pytest -q — 2862 passed, 1 skipped

Scope check

  • Version numbers and uv.lock only; no code, rails, rules, or classifications touched.