Skip to content

keel v0.15.1

Choose a tag to compare

@github-actions github-actions released this 16 Sep 02:42
· 96 commits to main since this release
68656b5

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.

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

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_failed on a protective leg after order_placed needs its own event and a doctor
    check for filled BUYs with no position row
  • reconciling live order id=4 is 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")),   # unguarded

That 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.toml version fields, 0.15.0 → 0.15.1
  • fourteen inter-package ==0.15.0 pins → ==0.15.1. These are separate from the version
    field and easy to miss; left behind they are exactly what makes keel versions report a
    PARTIAL INSTALL, which this deployment has hit before.
  • uv.lock, refreshed via uv 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.