Repository navigation
keel v0.11.2
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.
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_productsdropped the field. It projects a fixed set andbase_increment/quote_incrementwere 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.pyreads them off the raw response.OrderIntent.base_incrementcarries it, followingavailable_quoteexactly: venue state the executor fetches and hands in, so_order_configurationstays a pure function of the intent with no broker access.- Cached in
agent_stateunderbase_increment:<product_id>with a 7-day TTL — matching rail 17's cadence rather than inventing a second one, and needing no migration. Onelist_productscall 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 skippedruff check keel tests packages— cleanmypy— 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
- 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.
- Caching all ~900 products on a miss — one transaction, or too many
agent_staterows for your taste? quote_incrementis now plumbed but unused. #513'sQUOTE_INCREMENTStable 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 skippedruff check keel tests packages— cleanmypy— clean, 352 source fileskeel 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.