keel v0.3.0
Built from 0a1e1fc. Version binds to this hash:
keel --version reports keel 0.3.0+0a1e1fca6aef [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.3.0-py3-none-any.whl
keel --version
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.
Features
feat(insights): read-only insights + journaling reporting surface (#149)
What
Adds a read-only insights/journaling reporting surface — a pure view over the existing keel.db, in the same spirit as keel tui. Turns the slow, months-long paper-forward evidence accrual into something legible.
Two subcommands under a new keel insights group:
keel insights summary [--json] [--rule KIND] [--mode paper|live]— per-rule track record (n, win rate, R:R, expectancy, profit factor, max DD), an account/drawdown summary projected fromgather_status, and a "distance to the promotion gate" view: how many more trades and which floors each paper rule still needs before it could be promoted to live.keel insights journal [--json] [--rule] [--asset] [--limit] [--since] [--until] [--include-open]— chronological per-trade log with fee-honest net P&L, R-multiples, win/loss/open/scratch outcome, and DCA rows flagged "no stop — excluded from R/expectancy."
Design
- Read-only, zero new write paths. No orders, no rails/guards, no mutation of rule status / equity / agent_state.
Repositorygains no new methods — every read goes through existingget_rules/get_trade_outcomes/track_record/gather_status. - Pure VIEW, not a re-derivation. Rail-11 / drawdown / promotion floors are projected from existing outputs. Gate distance reuses
can_promote+floor_for_class(promotion_class_of(rule))verbatim — so thetrend_followfloor (min_trades=100) is read, never hardcoded. - Testable core + thin click I/O, mirroring
keel/commands/status.py/tui.py.Decimalthroughout;--jsonis validjson.loads-able (full-precision Decimals viadefault=str, no disclaimer footer); human render quantizes money/ratios to 2dp. - Honest at small samples: an explicit "n<30: not yet statistically distinguishable from random entry" note; never implies a proven edge below the gate.
- Self-contained: no TUI panel integration (deferred), no CTS/signals accessor (no clean join key; out of scope).
Tests
tests/commands/test_insights.py — 31 new tests (empty-DB-first, gate distance, DCA r_multiple=None, --limit pre-count, --include-open count line, small-sample note, ratio quantization, --json validity for both subcommands). Full suite: 1597 passed, ruff clean.
Review
Designed (Opus), built TDD (Sonnet), then reviewed end-to-end by an independent fresh-context Opus agent. Review found and this branch fixes: (1) journal count line was counting open rows as "closed" (showing N of M), (2) full-precision Decimal leaking into the human render, plus (3) the missing regression test. Verdict after fixes: read-only airtight, gate/promotion reuse correct.
feat(tui): add read-only insights overlay (i keybind) (#151)
What
Adds an i keybind to the live keel tui dashboard that opens a browsable, scrollable, read-only insights overlay — reusing the pure builders shipped in keel/commands/insights.py (PR #149). This is the deferred TUI follow-up to #149.
The overlay shows: account/drawdown summary, per-rule track record with distance-to-promotion-gate, the small-sample honesty note, and a recent-trades tail (last 5) — all as a live view that refreshes each poll.
- Footer/keybind bar now advertises
[i] insights. Esc(oriagain, orq) closes the overlay and returns to the dashboard; scrolling uses the same arrows/j/k/PgUp/PgDn/Home/End + clamped-offset infra as the existing help overlay.
Design / safety
- Read-only, fail-soft. The overlay calls only read builders over the already-computed
StatusReport+ repo reads — no writes, no order/broker/network beyond the broker-freegather_status. The build is wrapped intry/except Exception(notKeyboardInterrupt), so a transientdatabase is lockedfrom the concurrentkeel agentwriter paints an error line and the loop keeps polling — mirroring the existing main-view handler. - Circular import avoided:
insights.pyalready imports fromtui.py, so the reverse import isTYPE_CHECKING-only + lazy in-function (same pattern tui.py already uses forcli↔tui).keel/commands/insights.pyis unmodified. keel tui --onceis unchanged except the footer gaining the[i]hint.- Diff confined to
keel/commands/tui.py+tests/commands/test_tui.py.
Tests
+12 tests (1597 → 1609), ruff clean. Covers: overlay open-on-i / close-on-Esc (asserts return to the dashboard), scroll offset movement, empty-DB friendly render, and the insights-branch fail-soft path (rigged database is locked → error line painted, loop survives).
Review
Designed (Opus), built TDD (Sonnet), independently reviewed by a fresh-context Opus agent → verdict: mergeable-as-is (read-only / fail-soft / circular-import / --once impact all verified clean). The two test-coverage NITs it raised (fail-soft path + Esc-close isolation) are included in this branch.
feat(proposer): keel assets propose — screen an LLM asset shortlist, admit nothing (#153)
What
Implements roadmap direction #3, slice (A): keel assets propose --from <shortlist.json> — a read-only candidate source that ingests an externally-produced LLM asset shortlist and routes each candidate through the existing, unmodified _screen_product admission gate.
Follows docs/superpowers/specs/2026-07-24-llm-asset-proposer-design.md and docs/superpowers/plans/2026-07-24-llm-asset-proposer.md.
Design — outside-first hybrid
The LLM + web-search scouting happens outside keel (operator / Claude + firecrawl skills produce the JSON shortlist). keel owns only the deterministic, testable half: validate → screen → report.
- New pure module
keel/proposer.py—parse_proposal(schema + per-candidate validation),build_proposal_report(routes each candidate through an injectedscreen_fn),render_proposal_report(human) +report_to_jsonable(--json). It does not importkeel.cli(no cycle); the CLI passes the real_screen_productas the gate. - Thin
assets proposecommand incli.py, besideassets screen/assets holdings.
Guarantees (verified by an independent review — by construction, not just by test)
- Read-only / admits nothing. No INSERT/UPDATE/DELETE, no attestation write, no allowlist mutation, no new
Repositorymethod.screen_asset/ScreenPolicy/_screen_productare byte-unchanged. - Same gate, by construction.
assets proposeandassets screenreturn the identical verdict for the same asset (test assertsADMITon both sides). Becauseattestation=Nonefails closed, a freshly-scouted assetREJECTs until a human attests it and data is fetched — the proposer surfaces candidates, it never admits them. - Provenance enforced by code. A candidate with zero source citations is rejected at schema validation, never screened.
shariah_hypothesiscan't influence admission.build_proposal_reportpasses only(repo, product, quote)to the gate; the hypothesis is rendered as an explicitUNVERIFIED (never used for admission)hint and can never become an attestation.
Out of scope (per spec)
No embedded LLM/web API, no new dependency, no second secret, no proposal-audit table, no allowlist mutation. Strategy-proposal (B) and insights/veto (C) are separate future specs.
Tests
+29 tests (1629 → 1658), ruff clean. keel/proposer.py unit tests (schema, builder, renderers, JSON) + CLI integration tests (same-gate equivalence, admits-nothing [reopens the DB], --json validity, hypothesis-never-admits, citation-required, hyphenated-asset rejection, non-dict entry, disclaimer on human path).
Review
Built TDD (Sonnet) from the plan, independently reviewed by a fresh-context Opus agent → mergeable-as-is. The 4 hardening items it raised are included: single-sourced DATA_DERIVED_FAILURES in screen.py (with a pin test so a tag rename breaks a test rather than silently disabling suppression), alphanumeric-asset validation, a stronger writes-nothing test, and two coverage tests.
Fixes
fix(orders): populate orders.rule_id from originating rule (Phase-2 debt) (#152)
What
Closes the long-standing Phase-2 debt where orders.rule_id (and signals.rule_id) were always written NULL. The column has always existed (FK → rules(id)), but the originating rule's DB id was dropped early (_build_rule discarded row["id"]), so it never reached the insert.
This threads the real rule id through the chain — additively, defaulting None:
get_rules row id → Rule.rule_id (set in _build_rule) → Signal.rule_id (engine ENTER + agent EXIT) → OrderIntent.rule_id → the order-row insert. Also wires signals.rule_id in engine._persist_signal, and the paper path (enter/close, including restart-rehydrated positions).
Metadata-only — nothing about order placement changes
The only behavioral difference is a previously-NULL column now carries the rule id. No sizing, guard, rail, veto, confirm, or placement logic changed. Every hunk is either a new None-defaulted field, a hardcoded None swapped to the real id, or a docstring.
tests/execution/test_guards.pyandtests/execution/test_reconcile.pyhave a zero-line source diff and pass unchanged.test_rule_id_is_purely_additive_metadata_placement_and_guards_are_unchangedruns two otherwise-identical signals (with/withoutrule_id) throughexecute()and asserts identicalplaced/vetoed_by/ broker preview+place calls /order_configuration— only therule_idcolumn differs.
Scope
- Forward-only. No historical backfill migration;
SCHEMA_VERSIONunchanged. Old NULL rows stay NULL (unrecoverable by kind/name) — noted as a follow-up. place_bracket/scale_out/_roll_stopstill writerule_id=NULL(they only receive arule_name, andrules.namehas no UNIQUE constraint so a name→id lookup would be ambiguous). FK-safe; left as an accepted scope limit.
Tests
+20 tests (1597 → 1617), ruff clean. Covers: repository non-NULL round-trip, live ENTER + live EXIT order rows carry the seeded rule's id, a full agent cycle, paper enter/close, restart-rehydration of a legacy-NULL position (no crash), and the additive-metadata placement-identical guarantee.
Review
Built TDD (Sonnet), then an independent fresh-context Opus safety review of the order path → verdict: mergeable-as-is. It verified the metadata-only guarantee (no rail reads rule_id), id correctness in every write path, FK-safety (PRAGMA foreign_keys = ON; all ids are real PKs or NULL), and backward-compat. The two MINOR test-coverage gaps it raised (live-exit rule_id, legacy-NULL rehydration) are included in this branch.
Docs, CI & tooling
chore(gitignore): ignore supervised-live sandbox operational files (#150)
Adds the three supervised-live sandbox operational files to .gitignore so they stop showing as untracked:
config.live-sandbox.yamlkeel-live-run.shcom.keel.live.plist
These are the operator's tiny-cap confirm-mode live-detector bundle (authored in the dev repo, deployed to ~/Documents/Keel). They mirror the already-ignored paper-forward operational files (config.paperforward.yaml / paperforward-run.sh / com.keel.paperforward.plist) and must never be committed. Their ignore lines had only ever lived in the working tree, so the three files were surfacing as untracked in every git status.
Diff is .gitignore-only. No code, no tests affected.