Repository navigation
keel v0.12.2
Built from 65073da. Version binds to this hash:
keel --version reports keel 0.12.2+65073da9cf04 [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.12.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.
Desktop app (macOS / Windows)
Apple's certificate is $99/yr and Azure Trusted Signing is ~$120/yr, and
keel cannot currently afford either. There is no cheaper tier and no free
open-source option on either platform. (Windows is worse value than it looks:
since 2024 even an EV certificate no longer grants an instant SmartScreen
pass — reputation is earned from download volume over time.)
Prefer no warning at all? Install from the wheels above instead — nothing
is downloaded as an application, so nothing objects. Needs a terminal and
Python 3.11+.
Full explanation, including how to check what you downloaded:
https://github.com/CodeGateSoftware/keel/blob/main/docs/desktop-install.md
macOS — the first open is refused. Open System Settings → Privacy &
Security, scroll to the message about keel, and choose Open Anyway.
You will only do this once.
Windows — SmartScreen shows "Windows protected your PC". Choose More
info → Run anyway.
Verify what you downloaded before you run it. Every artifact carries a
GitHub build attestation binding it to this repository, this workflow and this
commit — which is the question a code-signing certificate answers too:
gh attestation verify <file> --repo CodeGateSoftware/keel
SHA256SUMS.txt is attached as well. If either check fails, do not run it.
The app opens keel in your browser; there is no terminal involved. Your
config, database and credentials live in your user Application Support /
LocalAppData folder and are never touched by an update.
Other changes
feat(web): the header is a brand, not a word — mark, home, theme choice, mode badge (#598)
Closes #597
What shipped, per the issue's bullets
The mark and the wordmark, one anchor to /. The favicon's geometry (icons/keel.svg, transcribed by #594) is inlined into the header with its colours re-pointed at the tokens — in light that reproduces the icon exactly (--accent IS #0c5d52, --bg IS #f8f7f3); in dark the same shape wears the mint accent on the slate instead of carrying light-theme literals onto a dark page. Mark + wordmark sit inside ONE <a href="/" aria-label="keel home"> (the app's home since #540); it is an anchor, so it is keyboard-focusable and gets the global :focus-visible ring. test_the_inline_mark_is_the_one_the_icons_carry pins the geometry against the icon file so this cannot drift into a second drawing of the boat.
A theme toggle — two states, persisted, OS-following. Details below.
A mode badge, read-only. Details below.
Non-goals held: no mode switching from the browser (nothing added to the write surface — no new route, no POST), no new views, no nav changes beyond the brand block.
The theme state machine (and the flash)
Two states, light and dark; no stored choice → the OS decides; stored choice → fixed, on any OS. The mechanism is one attribute, data-theme on <html>:
keel.cssnow states the dark palette twice — the existing@media (prefers-color-scheme: dark) :root:not([data-theme="light"])block (OS prefers dark, reader has not pinned light) and a new:root[data-theme="dark"]block (a pinned dark on a light machine — without it the toggle had one working position on a light-OS machine). This is keeltrading.com's own device (global.csscarries the same two blocks).test_a_pinned_dark_choice_paints_dark_on_a_light_machineholds the two blocks exactly equal, and both also setcolor-schemeso browser widgets follow.- Flash avoidance:
js/theme.js, the shell's one CLASSIC script. The standard pattern is an inline head script (that is what the site does), but this shell's CSP isdefault-src 'self'with no'unsafe-inline'— an inline block would be silently refused by the browser. And atype="module"script is deferred, so the page would paint the wrong theme and flip. The CSP-compatible form is a same-origin, parser-blocking classic script in<head>, placed ahead of the stylesheet, sodata-themeis on the root before a single rule is applied to a single element. Theno-inline-scripttest was narrowed to permit exactly one such tag (and asserts it precedes the stylesheet — the no-flash property stated as a byte order);theme.jsjoined the closed_MODULESlist, so every module scan covers it. - The toggle (
main.js) flips from the theme actually showing (askingmatchMediawhen the attribute is absent — otherwise the button would no-op on a dark-OS machine) and stores underkeel-theme, the same key the site's toggle uses and the onetheme.jsrestores. Storage refused (private mode) still flips this load and starts from the OS next time. A test pins the two files spell the key identically. - Both icons ship inline (the site's own sun and moon,
currentColor) — no icon font, no external asset. The same two selectors that paint the page decide which icon shows, so the circle shows the icon of the theme a click switches TO and cannot disagree with the page even beforemain.jsloads.
Where mode/db came from, and why the browser never sets it
/api/config now carries mode, db_path and config_path (payload.config_payload + read_config). Rationale:
- Why that endpoint: it is the one the client reads at boot on every view (worker registration, docs links, the footer build line), so the badge is present everywhere rather than only where a status report happens to load.
modeis the config's own word forauto_trade.mode(paper/confirmtoday; the class table also mapslivefor the day the vocabulary grows it, and an unknown word still renders — the same forward-compatSTATE_CLASSgives). It crosses as a bare string, not aField: aFieldwould force astate, andconfirmbesidepaperis neither good nor bad. The emphasis table inrender.js::MODE_CLASSis the whole visual difference — paper muted, confirm/live carry--accent.- The browser NEVER changes it. The route is GET-only (
test_no_api_route_answers_a_poststill covers the whole table); changingauto_trade.modeis a config-file edit plus a terminal action by design. The payload's docstring and the badge's docstring both record the boundary. The tooltip names the db and config in play (<db> · config <config>) — the where-am-I ofkeel serve, answerable at a glance. - Degradation: a config that is missing (first run) or unreadable yields
mode: ""and the badge hides (:empty) rather than guessing — the whole shell boots off this endpoint and must not 500 on it.
Test delta
tests/web/test_api.py—/api/confignames the served deployment (modepaperfrom the fixture default, both paths); a confirm-mode config reportsconfirm; a machine with no config degrades to""with paths intact.tests/web/test_payload.py— the config entry in_every_payloadcarries the new keys (money-as-strings walk covers them); a focused pin documents bare-strings-not-Fields and the read-only boundary.tests/web/test_client_assets.py— structure pins for the brand anchor / inline mark / toggle / badge; the key-parity scanner extended to/api/config(coveringmodeBadgeandbuildLine, which had read that payload since #538 with no parity check —_view_keysnow ends a region at the next export or end of file, so the last function inrender.jsis no longer the one function no payload check can cover);_MODULES+ served-type + mount-prefix loops gaintheme.js; the no-inline-script pin narrowed to the one classic script, ordered before the stylesheet.tests/web/test_palette_contrast.py— pinned-dark block pinned EQUAL to the media block; and a new global pin that no hex literal exists outside the token blocks, which is how the new surfaces join the contrast suite: they are coloured only with tokens whose pairs the suite already measures (the toggle's boundary is--control-line, the 3:1 token).
Gates
uv run pytest -q— 4437 passed, 3 skippeduv run ruff check keel tests— cleanuv run ruff format --checkon touched files — clean on every line this PR adds (the flagged hunks in those files are pre-existing baseline drift under ruff 0.15.21, byte-identical onorigin/main— CI runsruff check, notformat --check)uv run mypy— clean, 350 files
Hand-checked against a running keel serve
The browserless suite pins bytes, so the interactive half was checked by hand on loopback: the shell serves the brand anchor, badge and toggle with the CSP unchanged (default-src 'self' permits the same-origin classic script, served text/javascript + nosniff); /api/config answered mode: paper with the db/config paths; theme.js was executed out-of-browser against the served bytes for all five storage states (stored light / stored dark / no choice / junk value / storage throws) and set the attribute exactly when a valid choice existed.
chore(release): 0.12.2 (#599)
What this is
The version-bump PR for 0.12.2, following #596's precedent exactly: one commit, every version and every == pin moves together, uv.lock moves with them. Per docs/RELEASING.md, merging this (not the workflow) is what sets the version — the release workflow then refuses to change it.
Patch, not a minor: since v0.12.1 the only user-facing change is #598. No new interfaces, so the patch is right.
What ships in 0.12.2
Enumerated from git log --oneline v0.12.1..HEAD:
- #598 — the header is the brand, not a word (#597): the web UI's header carries the keeltrading.com mark as the home link, a theme toggle (light/dark choice), and a mode badge; the right cluster keeps its margin when the badge degrades, and the mark pin covers
rxandstroke-width. Test-first (#597's799ed11pinned the header brand, toggle, and mode badge before the feature landed). - Nothing else since v0.12.1 — the log is exactly that one PR's commits plus its merge.
Files bumped
pyproject.toml—version0.12.1 → 0.12.2, plus the three sibling pins (keel-core,keel-broker-api,keel-broker-coinbase)packages/{keel-core,keel-broker-api,keel-broker-coinbase,keel-broker-alpaca,keel-broker-fake,keel-broker-robinhood,keel-broker-kraken}/pyproject.toml— eachversion, plus the 11 inter-package==pinsuv.lock— regenerated withuv lock; diff is exactly 8 lines (one version per distribution), no unrelated churn;uv lock --checkpasses
9 files, 25 insertions, 25 deletions. Nothing else: a repo-wide grep for 0.12.1 (excluding uv.lock) finds no live pins, expectations, or even inert prose left behind.
Lock discipline
The trap from #422/#482, verified TDD-style: bumping only the root pyproject.toml's version first made tests/test_packaging.py fail exactly as designed —
FAILED tests/test_packaging.py::test_every_workspace_package_carries_the_same_version
FAILED tests/test_packaging.py::test_workspace_siblings_are_pinned_to_the_exact_version[keel-trader]
keel-trader depends on 'keel-core==0.12.1'; it must be 'keel-core==0.12.2'
After completing the set (all 8 pyprojects, 14 pins, uv lock), the same suite is green. uv.lock rides in the same commit because the release workflow checks it is current before anything else (#424).
Gates
uv run pytest tests/test_packaging.py -q— 22 passeduv run pytest -q— 4437 passed, 3 skippeduv run ruff check .— all checks passeduv run mypy— no issues in 350 source files
Do not merge unreviewed: the release workflow runs next, after merge, with this same version.