Skip to content

keel v0.5.0

Choose a tag to compare

@github-actions github-actions released this 05 Aug 23:00
· 317 commits to main since this release
251ceb1

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

⚠️ 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

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.