keel v0.5.0
Built from 251ceb1. Version binds to this hash:
keel --version reports keel 0.5.0+251ceb11eeee [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.0-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
feat(guards): add rail 19, the spot-instrument rail (R2) (#167)
Implements R2 from docs/experiments/2026-08-05-coinbase-asset-class-feasibility.md, closing the residual rail 18 (#164) deliberately left open.
The residual
Rail 18 checks the settlement leg. It does not check the instrument shape:
quote_currency_of("BTC-PERP-USD") == "USD" -> rail 18 passes
_asset("BTC-PERP-USD") == "BTC" -> rail 1 passes (BTC is allowlisted)
Verified against the real live config — a derivative-shaped id with a legitimate final segment clears both shipped defences. Rail 19 is the only thing that stops it, and that is asserted as a test, in the same "the hole, stated as a test" style as rail 18's.
| rail 1 | rail 18 | rail 19 | |
|---|---|---|---|
BTC-PERP-USD, SELL and BUY, live and offline |
passes | passes | VETO 4/4 |
| the 6 live deployment rules | passes | passes | passes 4/4 |
A validator, not a classifier
A classifier (id → {spot, futures, equity}) must enumerate the shapes keel refuses, so it fails open on a shape Coinbase has not invented yet. A validator answers only "is this a well-formed spot id" and fails closed on novelty — the right posture for a spot-only halal agent, and it avoids building the id→class classifier the study prices as A1 scope.
Grammar: [A-Z0-9]{1,16}-[A-Z0-9]{2,10}. All 936 live spot ids match; all 99 futures (ROOT-DDMMMYY-CDE) and all 1000 equity ids (64-hex) fail. The quote bound is tied to config's _CURRENCY_CODE_RE so the two grammars cannot drift apart. quote_currency_of is untouched — rails 13/18 depend on its loose parse to name "CDE" in their messages.
Two deliberate corrections
The history walk counts, it does not skip. The study said "skip-or-flag" an unparseable row in _open_exposure_by_asset. Skipping would reduce measured exposure and thereby loosen rails 4/5/6 — fail-open. It now logs a warning and still counts the row. Overcounting is the closed direction.
assets screen is exempt from --products validation. Every other caller (fetch, monitor, simulate, rules seed) now refuses an untradeable id at the keyboard. Screening must not: it is the command that answers "may keel trade this, and why not", and screen_asset reports REJECT with the reason. A usage error would make the one tool whose job is to explain an inadmissible asset the one tool that cannot be asked about one. Pinned by a test.
What was deliberately NOT built
No engine read of capabilities().asset_classes. The live path builds CoinbaseClient, which has no capabilities(); paper passes broker=None. Such a gate is dead code that reads as a defence — the same pattern already built and deleted once in #164. The field instead gets a vocabulary check and conformance assertions so it cannot rot, and the reconciliation is deferred to the broker-port migration as a load-time check.
Also not built: the id→class classifier (A1 scope, fails open), and capabilities() on CoinbaseClient (that is the migration itself, or a second source of truth).
Verification
1804 tests pass (1698 → +106), ruff check . clean.
Separately, the deployment upgrade to 0.4.0 was verified end to end: a full agent cycle ran clean against a throwaway DB copy — mode=confirm polled=225 products=['ADA-USD','BTC-USD','ETH-USD','PAXG-USD','XLM-USD'] stale=[] signals=0.
chore: bump to 0.5.0 for the rail 19 release (#168)
Version bump only — no code change. The release workflow refuses to set the version itself, so this is the reviewed PR that decision belongs in (docs/RELEASING.md).
Why minor: rail 19 (#167) is a new un-overridable hard rail, adds a spot_instrument screen criterion, adds a load-time failure for a non-uppercase allowlist entry, and tightens --products at the CLI on every command that writes or trades.
Why now: ~/keel runs 0.4.0, which has rail 18 but not rail 19. The BTC-PERP-USD residual — an id that passes rails 1 and 18 and is stopped only by rail 19 — is still open on the live path until this ships.
uv.lock is relocked in the same commit. Leaving it behind is exactly what failed the 0.4.0 release: CI's uv sync rewrote a tracked file, so the artifact self-reported (DIRTY).
Verified by replaying the full CI sequence on a clean clone — sync (tree stays clean) → stamp → uv build --all-packages → install the wheel by path:
keel 0.5.0+f4944aea64a8 [release]
No (DIRTY). 1888 tests pass, ruff clean.
After merge: Actions → Release → Run workflow with 0.5.0.