keel v0.6.1
Built from 9417343. Version binds to this hash:
keel --version reports keel 0.6.1+9417343ecc0a [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.6.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.
Fixes
fix(deploy): upgrade every keel distribution, and verify all of them (#243)
Found while deploying v0.6.0. The ~/keel venv, immediately before the install:
keel-broker-api 0.5.5
keel-broker-coinbase 0.5.5
keel-broker-fake 0.5.5
keel-core 0.5.5
keel-trader 0.5.7 <- only this had been upgraded, across TWO releases
The documented step installs keel_trader only. Its siblings are required with no version, so
the keel-core 0.5.5 already on disk satisfied keel-core and was never touched. The engine ran
new code against old libraries for two releases.
The verification step could not see it. The README calls keel --version "the check that
matters"; it reports the keel-trader distribution's version and nothing else, so it printed
0.6.0 while keel-core sat at 0.5.5. A check blind to the failure mode is worse than none,
because it is trusted.
Reproduced end to end from the real wheels (0.5.5 set built from 2a58ffb with the versions
rewritten, 0.6.0 set built from 2a58ffb unchanged), running exactly what the README said:
$ uv pip install --python .venv --find-links Release Release/keel_trader-0.6.0-py3-none-any.whl
- keel-trader==0.5.5
+ keel-trader==0.6.0
$ uv pip list --python .venv | grep keel
keel-broker-api 0.5.5
keel-broker-coinbase 0.5.5
keel-broker-fake 0.5.5
keel-core 0.5.5
keel-trader 0.6.0
$ .venv/bin/keel --version
keel 0.6.0+bbbbbbbbbbbb [release]
1. The install command
V=0.6.0
gh release download "v$V" --repo CodeGateSoftware/keel --pattern '*.whl' --dir Release/
uv pip install --python .venv --find-links Release \
Release/keel_core-$V-py3-none-any.whl \
Release/keel_broker_api-$V-py3-none-any.whl \
Release/keel_broker_coinbase-$V-py3-none-any.whl \
Release/keel_trader-$V-py3-none-any.whl
.venv/bin/keel versions
.venv/bin/keel statusA wheel path is a direct requirement — that exact file is installed regardless of what is
already there — so naming all four is what actually moves them. --find-links Release stays, now
resolving the pinned siblings locally rather than from PyPI where they do not exist. Installing
by path rather than by bare name is unchanged and still explained: keel on PyPI is an
unrelated project.
Deliberately not Release/*.whl, the obvious route. uv build --all-packages builds every
workspace member and gh release create dist/* publishes all of them, so the glob installs two
distributions production must not have. Measured:
$ uv pip install --python .venv --find-links Release Release/*.whl
keel-broker-fake 0.6.0
keel-broker-robinhood 0.6.0
pynacl 1.6.2
cffi 2.1.1
The four named wheels are keel-trader's whole dependency closure — nothing more, nothing less.
2. Pins — decided yes, with wheel-metadata evidence
Every intra-workspace dependency is now == the workspace version, in the root and in all four
package pyproject.tomls.
[tool.uv.sources] workspace = true governs resolution during development and says nothing about
published metadata, so I built and read the wheel rather than assuming. Before:
$ unzip -p keel_trader-0.6.0-py3-none-any.whl '*/METADATA' | grep Requires-Dist
Requires-Dist: click>=8.4.2
Requires-Dist: keel-core
Requires-Dist: keel-broker-api
Requires-Dist: keel-broker-coinbase
After:
Requires-Dist: click>=8.4.2
Requires-Dist: keel-core==0.6.0
Requires-Dist: keel-broker-api==0.6.0
Requires-Dist: keel-broker-coinbase==0.6.0
The constraint lands, and the pin alone fixes the bug for anyone who ignores the README — the
old command against the new metadata, from the same 0.5.5 starting state:
$ uv pip install --python .venv --find-links Release Release/keel_trader-0.6.0-py3-none-any.whl
- keel-broker-api==0.5.5 + keel-broker-api==0.6.0
- keel-broker-coinbase==0.5.5 + keel-broker-coinbase==0.6.0
- keel-core==0.5.5 + keel-core==0.6.0
- keel-trader==0.5.5 + keel-trader==0.6.0
Why pin, given the README fix already works. The README binds only the person reading it. The
pin binds pip. These are not third-party dependencies with independent release cycles — they are
cut from this repo, at one version, in one uv build --all-packages, so == states a fact rather
than guessing at compatibility, and there is no version skew it would wrongly forbid. The cost is
that a bump must move the pins; the bump commit already edits version in all five files, so this
is incremental work on a commit that exists rather than a new obligation, and
tests/test_packaging.py fails the build if a pin is left behind — the failure mode of a forgotten
pin is otherwise silent (the wheel still builds and still installs, it just stops forcing the
upgrade). >= was rejected: it forbids an older sibling but not a mixed install, and mixed is
the thing that happened. uv.lock is unchanged — workspace sources already resolved to the members.
3. A verification step that can fail
New keel versions, registered in keel/cli.py. It prints the same build-identity line, then
every keel-* distribution in the running interpreter's environment, and exits non-zero when they
disagree. No config, no database, no network, so a non-zero exit is unambiguous.
Healthy — the deployment after the new install command:
$ .venv/bin/keel versions
keel 0.6.0+deb8fa7e978d [release]
keel-broker-api 0.6.0
keel-broker-coinbase 0.6.0
keel-core 0.6.0
keel-trader 0.6.0
ok: 4 keel distributions, all at 0.6.0.
exit=0
Mismatched — same venv, keel-core forced back to 0.5.5 with --no-deps (the pin has to be
bypassed on purpose now):
$ .venv/bin/keel versions
keel 0.6.0+deb8fa7e978d [release]
keel-broker-api 0.6.0
keel-broker-coinbase 0.6.0
keel-core 0.5.5
keel-trader 0.6.0
error: PARTIAL INSTALL: 4 keel distributions at 2 different versions (0.5.5, 0.6.0).
`keel --version` reports keel-trader's version alone and cannot see this. Reinstall
every wheel by path (README, 'Deploying a new version').
exit=1
keel --version on that same venv now says so too, on stderr, leaving its stdout string exactly
what it was (the release workflow greps it):
keel 0.6.0+deb8fa7e978d [release]
warning: PARTIAL INSTALL -- this line reports the keel-trader distribution only, and the
other keel distributions do not agree with it. Run `keel versions`.
It ships in the wheel, not in scripts/. scripts/ is operator tooling and is not packaged;
a deployment is a .venv beside a Release/ directory with no checkout of this repo, so a script
there could not be run without first fetching it. The check has to travel inside the artifact it
is checking. The rules live in keel.version.InstallReport as a pure value, tested without
installing anything; the command is rendering and an exit code. The release workflow now runs
keel versions against the wheels it just built, so a release cannot publish a set that disagrees
with itself.
keel-broker-fake
It should not be there and the check now says so: a release build with keel-broker-fake
installed exits 1 with uv pip uninstall --python .venv keel-broker-fake. A checkout does not
fail — that is exactly where the fake belongs, and a check that cried wolf on every developer's
machine would be ignored by the time it mattered. tests/test_packaging.py also asserts nothing
in the workspace can pull it in as a runtime dependency. The dev group is untouched.
It is inert today (nothing calls load_broker()), but it registers a fake venue under
keel.brokers, and "inert" is a property of this release rather than of the package. The README
and the RELEASING asset table now say not to install it, which is also the reason the install
command names wheels instead of globbing.
Gates
$ uv run ruff check keel tests packages scripts
All checks passed!
$ uv run pytest -q
2696 passed, 1 skipped in 30.51s
SKIPPED [1] packages/keel-broker-api/keel_broker_api/conformance/suite.py:254
$ uv run mypy
Success: no issues found in 224 source files
Baseline on origin/main, re-measured in this worktree, was 2669 passed / 1 skipped; +27 tests,
same single expected skip.
No version bump here — that stays a separate reviewed change. docs/experiments/ untouched.
Docs, CI & tooling
docs: separate paper from live, so a figure from one is never read as the other (#242)
Why
Conflating the paper and live accounts is the single most costly confusion in this project. It produced a wrong diagnosis: a paper-run position size was checked against live-account equity, and the conclusion was wrong as a result.
Nothing in the docs made the split explicit, and nothing on the dashboard says which account a number belongs to — paper and live render in an identical layout. This makes the mistake hard to repeat.
What
README.md — new ## Paper vs. live section (placed before ## Before trading live). It leads with the thing that causes errors — two separate accounts, separate databases, separate configs, separate schedules, separate histories, so a figure from one says nothing about the other — then a comparison table of the measured values, then:
- which one am I looking at —
equity_state_modeon any dashboard, the--config/--dbpair on the command line, and that--dbdefaults tokeel.db, so a live command that omits it silently reads the paper database; - order gating — paper places freely against synthetic cash; live runs
mode: confirm, which asks for a typedyand fails closed headless unless autonomy is armed, cross-referencingConfirm vs. autonomy; - cadence — "it fires hourly" and "it runs once a day" are both true. Both jobs have hourly triggers plus
RunAtLoad; both runners are day-stamped; the trigger count is catch-up breadth, not cadence, because launchd does not re-run an interval that passed while the machine was off. A failed cycle leaves no stamp and is retried next hour. The two differ only in anchor: paper is local-date, live is UTC-date. - sizing — paper sizes off its own synthetic equity (
equity_override); live falls back tocaps.max_exposure_usdas a proxy. Same rule, same setup, same day, different quantities.
keel/commands/tui.py — new Which account is this? help section (a few lines, before Live balance): which field disambiguates, and that switching accounts means relaunching with the other --config/--db pair, since there is no in-app switch.
tests/commands/test_tui.py — one test in the existing section-scoped style, pinning that the help names equity_state_mode, both flags, keel-live.db, and the keel.db default.
Notes
- Documentation only. No behaviour, default or config value change.
pyproject.tomluntouched, nothing underdocs/experiments/touched. - Branched from
origin/mainalongside the open #240. Both files that overlap are edited in separate, non-adjacent sections: this README section sits at the end of the file rather than next to #240's### How much money moves(which it cross-references instead of restating), and the TUI section sits beforeLive balancerather than at #240's Glossary anchor. No new module-level test helper, so #240's_help_section()cannot collide. - One deviation from the brief worth flagging: the live plist explicitly warns against describing the live path as placing nothing unattended, because the deployment runs with autonomy ON. The confirm-mode text is therefore qualified with autonomy rather than stated flat.
- The two plists are not identical in cadence — paper has 12 local triggers (09:00–20:00), live has 24 anchored on UTC at :20. The design is the same; the section says so and names the difference.
Gates
$ uv run ruff check keel tests packages scripts
All checks passed!
$ uv run pytest -q
2665 passed, 1 skipped in 38.56s
$ uv run mypy
Success: no issues found in 221 source files
Baseline on origin/main is 2664 passed / 1 skipped; the +1 is the new test. The single skip is the expected one:
SKIPPED [1] packages/keel-broker-api/keel_broker_api/conformance/suite.py:254: adapter serves no granularity the suite could exercise