Skip to content

keel v0.12.2

Choose a tag to compare

@github-actions github-actions released this 28 Aug 20:27
· 346 commits to main since this release
65073da

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.

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

Desktop app (macOS / Windows)

⚠️ These builds are not code-signed, so your computer will warn you.
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.css now 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.css carries the same two blocks). test_a_pinned_dark_choice_paints_dark_on_a_light_machine holds the two blocks exactly equal, and both also set color-scheme so 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 is default-src 'self' with no 'unsafe-inline' — an inline block would be silently refused by the browser. And a type="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, so data-theme is on the root before a single rule is applied to a single element. The no-inline-script test was narrowed to permit exactly one such tag (and asserts it precedes the stylesheet — the no-flash property stated as a byte order); theme.js joined the closed _MODULES list, so every module scan covers it.
  • The toggle (main.js) flips from the theme actually showing (asking matchMedia when the attribute is absent — otherwise the button would no-op on a dark-OS machine) and stores under keel-theme, the same key the site's toggle uses and the one theme.js restores. 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 before main.js loads.

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.
  • mode is the config's own word for auto_trade.mode (paper / confirm today; the class table also maps live for the day the vocabulary grows it, and an unknown word still renders — the same forward-compat STATE_CLASS gives). It crosses as a bare string, not a Field: a Field would force a state, and confirm beside paper is neither good nor bad. The emphasis table in render.js::MODE_CLASS is 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_post still covers the whole table); changing auto_trade.mode is 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 of keel 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/config names the served deployment (mode paper from the fixture default, both paths); a confirm-mode config reports confirm; a machine with no config degrades to "" with paths intact.
  • tests/web/test_payload.py — the config entry in _every_payload carries 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 (covering modeBadge and buildLine, which had read that payload since #538 with no parity check — _view_keys now ends a region at the next export or end of file, so the last function in render.js is no longer the one function no payload check can cover); _MODULES + served-type + mount-prefix loops gain theme.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 skipped
  • uv run ruff check keel tests — clean
  • uv run ruff format --check on 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 on origin/main — CI runs ruff check, not format --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 rx and stroke-width. Test-first (#597's 799ed11 pinned 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 — version 0.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 — each version, plus the 11 inter-package == pins
  • uv.lock — regenerated with uv lock; diff is exactly 8 lines (one version per distribution), no unrelated churn; uv lock --check passes

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 passed
  • uv run pytest -q — 4437 passed, 3 skipped
  • uv run ruff check . — all checks passed
  • uv run mypy — no issues in 350 source files

Do not merge unreviewed: the release workflow runs next, after merge, with this same version.