keel v0.8.1
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.
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_countgains an optionalstart_ts(defaultNone= whole series,
behavior identical to today). Givenstart_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 straddlesstart_tsis 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_productspassesstart_ts;_print_freshnesskeepsproven = gaps - unexplained
with NOmax(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_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_tsand
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_windowwas BORN GREEN, on
purpose and stated in its docstring: it pins preserved--fail-on-gapssemantics, 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 checkclean —All checks passed!(overkeel tests packages) -
uv run mypyclean —Success: no issues found in 237 source files -
uv run pytest -qgreen —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-gapswhole-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.