Repository navigation
keel v0.15.1
Built from 68656b5. Version binds to this hash:
keel --version reports keel 0.15.1+68656b507b30 [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.15.1-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
ci(snyk): déclarer le dépôt public au CLI, sinon le scan est facturé comme privé (#796)
Le piège
Les plafonds du plan gratuit Snyk — 200 tests Open Source/mois, par organisation, partagés avec
les autres dépôts de CodeGateSoftware — ne s'appliquent qu'aux dépôts privés. keel est public :
ses scans devraient être gratuits et illimités.
Sauf que le CLI ne peut pas le deviner. Un checkout actions/checkout n'expose pas de remote
exploitable, si bien que Snyk classe par défaut tout scan lancé en CI comme un test privé —
y compris sur un dépôt public. --remote-repo-url est le mécanisme documenté pour le lui dire.
Sans lui, ce job consommerait le quota privé de l'organisation à chaque push sur main : ~196 par
mois sur la période mesurée, soit à lui seul le plafond entier partagé par tous les dépôts.
Pourquoi maintenant, alors que rien ne casse
Le job est aujourd'hui skipped faute de SNYK_TOKEN/SNYK_ORG — vérifié sur huit exécutions
consécutives. C'est justement le bon moment : le drapeau est posé pour que l'activation du scan ne
vide pas le quota sans prévenir.
Posé sur snyk test et sur snyk monitor : les deux créent une cible, donc les deux doivent la
déclarer publique.
fix(coinbase): guard the adapter's preview Decimal conversions against empty fields (#799) (#800)
Closes the code half of #799.
The bug
_field(response, key, "0") supplies a default for a missing key. It does nothing for a key
that is present and empty, which is what Coinbase sent. Decimal("") raises InvalidOperation.
- est_base_size=Decimal(_field(response, "base_size", "0")),
+ est_base_size=Decimal(_field(response, "base_size", "0") or "0"),…and the same for quote_size and commission_total.
Why it mattered
2026-08-25, live, cycle e917fb3d21544dc2:
13:20:20 executor.order_placed PAXG-USD BUY order_id=4 status=filled
13:20:21 executor.preview_failed PAXG-USD SELL
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]
The entry filled. The protective SELL's preview then threw, and the exception propagated out
of executor._run_order before agent.py's repo.open_position(...) ran. Three weeks later that
holding is still stranded: no positions row, so no channel exit and no 2xATR stop can ever fire;
its computed stop of 4521.76 exists only in a log line (PAXG is now ~4312, so the stop was
breached and nothing acted); and $61.71 still counts against the exposure caps, because
_open_exposure_by_asset reads orders, not positions.
agent.tranche_not_recorded — the warning that exists for "entry filled but not recorded" — did
not fire. It only covers a missing actual_fill/qty. This path throws earlier and logs a
preview failure, which reads like a harmless pre-trade hiccup.
Why it was still live
The retired keel/data/cb_client.py already carries or "0". The adapter that replaced it does
not — #524 flipped _build_broker to the registry-resolved adapter, so the fix landed on the
path being retired and not on the path that replaced it. The August failure would recur today.
Test
Parametrized over the three fields. Each case asserts the emptied field reads 0 and that the
other two still parse from the fixture, so a blanket try/except zeroing the whole Preview
does not pass it. Verified by mutation: restoring the bare conversion for one field fails that
field's case alone, leaving the other two green.
ruff format --check and ruff check clean; mypy clean apart from a pre-existing PyYAML stub
note in scripts/release_notes.py; tests/broker_coinbase tests/broker_api tests/execution →
602 passed.
Not in this PR
#799's other three items are deliberately left open — they are design changes, not a one-line
guard:
- a filled entry must survive a later failure (record the position, or downgrade to an explicit
UNBRACKETED state the codebase already models) preview_failedon a protective leg afterorder_placedneeds its own event and adoctor
check for filled BUYs with no position row- reconciling live order
id=4is an operational decision, not a code one
chore(release): 0.15.1 -- the adapter preview guard reaches the live path (#801)
Version bump only. No behaviour change in this PR — it exists so the fix already merged as #800
can actually ship.
Why now, specifically
~/keel reports keel 0.15.0+cd6f0d193bfe [release], and 0.15.0's wheels predate #800. The
deployed adapter still reads:
est_base_size=Decimal(_field(response, "base_size", "0")), # unguardedThat is the line behind #799: on 2026-08-25 a live PAXG entry filled, the protective SELL's
preview raised decimal.InvalidOperation, and the exception propagated out of
executor._run_order (deployed executor.py:979 previews inside it) before the position row was
written — stranding funded inventory with no exit for three weeks.
The recovery for that position has now been armed on the live deployment (a positions row plus
an unbracketed:PAXG-USD intent, so reconcile_unbracketed_positions re-brackets it). That
recovery runs through the same preview path, on the same unguarded code, until this ships.
What moved
- eight workspace
pyproject.tomlversionfields,0.15.0→0.15.1 - fourteen inter-package
==0.15.0pins →==0.15.1. These are separate from theversion
field and easy to miss; left behind they are exactly what makeskeel versionsreport a
PARTIAL INSTALL, which this deployment has hit before. uv.lock, refreshed viauv lock(keel-trader,keel-core,keel-broker-*→ 0.15.1). The
release workflow checks the lockfile before anything else and fails if it was left stale.
Verified locally
grep -rn '0\.15\.0' pyproject.toml packages/*/pyproject.toml -> (none)
ruff check keel tests packages -> All checks passed!
ruff format --check . -> 502 files already formatted
pytest tests/broker_coinbase tests/execution -> 559 passed
keel/templates/config.live.yaml -> mode: confirm
The last line is the release asset the workflow refuses to publish in any other mode.
There is one pre-existing E501 in docs/experiments/2026-09-02-equities-cost-fidelity.py. The
workflow scopes ruff check to keel tests packages, so it does not gate this release; noted
rather than silently fixed in a release PR.
After merge
Actions → Release → Run workflow, entering 0.15.1. Then the five production wheels install into
~/keel by path, and keel versions should report 0.15.1 across all five.