keel v0.5.4
Built from 5da34af. Version binds to this hash:
keel --version reports keel 0.5.4+5da34afd7935 [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.4-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(admission): pick the shortlist by name, not by whichever json is newest (#180)
Found by real use, minutes after 0.5.3 deployed.
What happened
The propose overlay globbed *.json under the proposals directory and took the newest by mtime. But that directory is shared: a scout run writes three files into it — <date>-shortlist.json, <date>-param-proposals.json, and <date>-research-briefs.md. The overlay selected whichever was written last, which was the param-proposals file, one minute newer than the real shortlist. Pressing p produced:
the shortlist file is malformed
.../2026-08-07-param-proposals.json: proposal must be an object with a 'candidates' list.
Nothing was wrong with either file. The fail-soft path worked correctly — it explained itself instead of crashing — it was just reading the wrong file, and blaming the operator's own strategy output for it.
The fix
Selection now matches on the filename convention (*shortlist.json), so a sibling written seconds later can never shadow the real shortlist.
Deliberately not "prefer a shortlist, else fall back to any *.json." On a run that produced param proposals and no candidates, that fallback re-selects the sibling and reproduces the same confusing error in a rarer, harder-to-diagnose case. Strict matching means an absent shortlist reads as absent, which is the truth.
The pattern is *shortlist.json, not *-shortlist.json. I wrote the hyphenated form first and it rejected plain shortlist.json — the most natural name an operator would reach for, and the one 12 existing tests already used. Requiring the prefix separator bought nothing and broke the obvious case.
The not-found message now names the pattern, since selection-by-convention is undiscoverable from "no shortlist yet".
| filename | selected? |
|---|---|
shortlist.json |
yes |
2026-08-07-shortlist.json |
yes |
2026-08-07-param-proposals.json |
no |
2026-08-07-research-briefs.md |
no |
candidates.json |
no — message says what to rename it to |
Verification
Test-first; both new tests failed for the right reason before the fix. One pins that a newer sibling loses to a correctly-named shortlist; the other pins that a directory holding only param proposals reads as "no shortlist" rather than "malformed shortlist". 2085 tests pass, ruff and mypy clean. Verified against the real directory: it now selects 2026-08-07-shortlist.json.
chore: bump to 0.5.4 to unbreak the TUI propose overlay (#181)
Version bump only — no code changes. Ships #180.
Patch: no schema change, no rail change, nothing that alters what the agent trades or when.
Why this is worth cutting on its own
0.5.3 shipped the propose overlay selecting the newest *.json in the proposals directory. That directory is shared — a scout run writes a shortlist plus sibling param-proposal and research-brief files — so the overlay picked whichever was written last and told the operator their own strategy output was a malformed shortlist.
The overlay is unusable on the deployment until this ships, and it is the surface the entire admission workflow in #176 was added for. Batching it behind other work would leave the feature broken on the box for no reason.
uv.lock relocked in the same commit, per 0.5.0 through 0.5.3. Verified with uv sync --frozen, which accepted the lock and rewrote nothing beyond the five pyproject.toml bumps.
Gate: 2085 tests pass, ruff clean.