Skip to content

keel v0.11.2

Choose a tag to compare

@github-actions github-actions released this 23 Aug 00:34
· 433 commits to main since this release
829172f

Built from 829172f. Version binds to this hash:
keel --version reports keel 0.11.2+829172f9ddfe [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.11.2-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.

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

fix(executor): quantize SELL base_size, without letting an unknown increment block an exit (#516) (#517)

Closes #516. Completes #513/#514, which fixed the BUY half and deliberately left this.

A SELL still emitted str(Decimal), so the first exit carrying a non-round quantity would have been rejected with the same INVALID_SIZE_PRECISION that killed the XLM entry — and an exit that cannot leave is a worse failure than an entry that cannot start.

The asymmetry is the design, not an oversight

increment BUY (#513) SELL (this PR)
known quantize down, send quantize down, send
unknown refuse send as-is, log

A refused BUY costs nothing; the opportunity passes. A refused SELL strands a position. Sending full precision at least sometimes works — a round quantity is accepted — so refusing would replace "sometimes exits" with "never exits" and leave the engine worse than before the fix.

Same reasoning for the protective bracket: one the venue refuses leaves the position unprotected.

Tests assert both halves side by side (test_a_buy_refuses_where_a_sell_sends) so that "make it consistent with BUY" fails loudly rather than looking like a tidy-up.

Three supporting changes

  • cb_client.list_products dropped the field. It projects a fixed set and base_increment/quote_increment were not in it — which is why there was nothing to quantize against. The venue always sent them; docs/experiments/2026-08-05-coinbase-asset-class-probe.py reads them off the raw response.
  • OrderIntent.base_increment carries it, following available_quote exactly: venue state the executor fetches and hands in, so _order_configuration stays a pure function of the intent with no broker access.
  • Cached in agent_state under base_increment:<product_id> with a 7-day TTL — matching rail 17's cadence rather than inventing a second one, and needing no migration. One list_products call returns every product, so a miss caches all of them rather than re-fetching ~900 once per allowlisted asset.

The lookup never raises: venue error, no broker (paper mode), or a malformed field all return unknown, and the exit proceeds exactly as it did before this PR.

Smaller decisions worth a look

  • Down, not nearest, on both sides. Selling slightly less than held leaves dust; selling more is rejected for insufficient funds anyway.
  • A quantity smaller than one whole increment is sent unchanged, not suppressed. Silently retiring a holding keel still believes it has would be worse than an audited rejection.
  • Only the size is quantized. Bracket prices have their own increment and are out of scope here.

Verification

  • pytest -q — 4,497 passed, 3 skipped
  • ruff check keel tests packages — clean
  • mypy — clean, 352 source files

ruff format --check flags keel/execution/equity.py, pre-existing drift in a file this PR does not touch. CI does not run the format check.

Reviewer attention

  1. Is the 7-day TTL right? An increment changing is an announced venue event, and a stale value is still a multiple the venue accepts unless it got finer.
  2. Caching all ~900 products on a miss — one transaction, or too many agent_state rows for your taste?
  3. quote_increment is now plumbed but unused. #513's QUOTE_INCREMENTS table could become a fallback rather than the source of truth — worth a follow-up, or leave it?

chore(release): 0.11.2 (#518)

Version bump so #517's SELL-side size-precision fix can be released. RELEASING.md step 1 requires this in a reviewed PR.

Patch, not minor. No interface changed. Behaviour differs only in that an exit carrying a non-round quantity now serialises a value the venue accepts — and an unknown increment still sends unquantized rather than refusing, because refusing a SELL strands a position.

What moved

All eight distributions together — keel-trader plus the seven workspace packages — the == sibling pins, and uv.lock.

Verification

  • pytest -q — 4,498 passed, 3 skipped
  • ruff check keel tests packages — clean
  • mypy — clean, 352 source files
  • keel versions — ok: 8 keel distributions, all at 0.11.2

Context

0.11.1 shipped the BUY half (#513/#514). This ships the SELL half (#516/#517), so the two sides of _order_configuration are finally consistent in behaviour while remaining deliberately asymmetric in their unknown case.

Deployment is currently running 0.11.1+2560ffdf0c6e.