keel v0.5.1
Built from d082371. Version binds to this hash:
keel --version reports keel 0.5.1+d082371226e4 [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.5.1-py3-none-any.whl
keel --version
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(telemetry): log an unreachable venue as a warning, not an ERROR traceback (#169)
An unreachable venue is now logged as a one-line warning instead of a full ERROR traceback, so grep ERROR in the live log means something again.
What was happening
A 35-minute offline window on 2026-08-06 — laptop asleep, DNS not up after wake — wrote 124 ERROR records with 20-frame tracebacks into keel-live.log. The TUI polls the live balance every 30s (_BALANCE_REFRESH_SEC), and every failed poll was logged twice: once by cb_client.get_accounts, then again by executor._fetch_available_quote, which re-logged the exception the client had already logged and re-raised.
Nothing was broken. The venue was unreachable, every caller already fails soft, and the condition cleared by itself. The cost was that a real 401 Unauthorized in the same log is one line among 124.
What changed
telemetry.log_venue_failure, for any call that crosses the network to a venue. It picks severity by what the failure actually cost:
| condition | level | body |
|---|---|---|
| unreachable, no cycle bound (a dashboard balance refresh) | WARNING | one line, unreachable=true + truncated error summary |
| unreachable, inside a trade cycle | ERROR | no traceback — rail 13 fails closed on a missing balance, so an order did not go out |
| anything else (auth, malformed, a bug) | ERROR | full traceback, byte-for-byte what log_exception emitted |
Unreachability is matched on exception type name over the __cause__ chain, so keel-core needs no requests/urllib3 dependency to classify their exceptions, and the check holds for any broker adapter's HTTP stack. SSLError is deliberately excluded — a failed handshake can mean interception, which an operator must see at ERROR.
Control flow is untouched. get_accounts still raises; _fetch_available_quote still returns None so rail 13 still fails closed. Only the severity of the record changes.
Verification
- 1902 tests pass,
ruff check keel tests packagesclean, mypy clean across 94 files. - 25 new tests, written failing first.
- Validated against a real
requestsfailure rather than only the name-matched fakes:ConnectionError → MaxRetryError → NameResolutionError → gaierror, all four in the match set. - Replaying all 124 recorded failures from
keel-live.logthrough the classifier leaves 2 at ERROR — exactly the 401.
chore: bump to 0.5.1 for the venue-unreachable logging fix (#170)
Version bump only — no code changes. Pairs with #169.
Patch, not minor. #169 changes log severity and nothing else: cb_client.get_accounts still raises, executor._fetch_available_quote still returns None, and rail 13 still fails closed on a missing balance. No behaviour an operator or a rail can observe has changed — only whether a record lands at ERROR with a 20-frame traceback or at WARNING on one line.
uv.lock is relocked in the same commit, per 0.5.0. Leaving it behind is what made the 0.4.0 release fail its self-identification check: CI's uv sync rewrote a tracked file, so the artifact reported (DIRTY). Verified here with uv sync --frozen, which accepted the lock and rewrote nothing beyond the five pyproject.toml bumps.
The live deployment is on 0.5.0, whose TUI balance poll writes a full ERROR traceback every 30s for as long as the machine is offline. Cutting this release is what gets the fix there.