Skip to content

keel v0.3.0

Choose a tag to compare

@github-actions github-actions released this 25 Jul 20:38
· 342 commits to main since this release
0a1e1fc

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

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

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 from gather_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. Repository gains no new methods — every read goes through existing get_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 the trend_follow floor (min_trades=100) is read, never hardcoded.
  • Testable core + thin click I/O, mirroring keel/commands/status.py / tui.py. Decimal throughout; --json is valid json.loads-able (full-precision Decimals via default=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 (or i again, or q) 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-free gather_status. The build is wrapped in try/except Exception (not KeyboardInterrupt), so a transient database is locked from the concurrent keel agent writer paints an error line and the loop keeps polling — mirroring the existing main-view handler.
  • Circular import avoided: insights.py already imports from tui.py, so the reverse import is TYPE_CHECKING-only + lazy in-function (same pattern tui.py already uses for clitui). keel/commands/insights.py is unmodified.
  • keel tui --once is 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.pyparse_proposal (schema + per-candidate validation), build_proposal_report (routes each candidate through an injected screen_fn), render_proposal_report (human) + report_to_jsonable (--json). It does not import keel.cli (no cycle); the CLI passes the real _screen_product as the gate.
  • Thin assets propose command in cli.py, beside assets 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 Repository method. screen_asset / ScreenPolicy / _screen_product are byte-unchanged.
  • Same gate, by construction. assets propose and assets screen return the identical verdict for the same asset (test asserts ADMIT on both sides). Because attestation=None fails closed, a freshly-scouted asset REJECTs 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_hypothesis can't influence admission. build_proposal_report passes only (repo, product, quote) to the gate; the hypothesis is rendered as an explicit UNVERIFIED (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 idRule.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.py and tests/execution/test_reconcile.py have a zero-line source diff and pass unchanged.
  • test_rule_id_is_purely_additive_metadata_placement_and_guards_are_unchanged runs two otherwise-identical signals (with/without rule_id) through execute() and asserts identical placed / vetoed_by / broker preview+place calls / order_configuration — only the rule_id column differs.

Scope

  • Forward-only. No historical backfill migration; SCHEMA_VERSION unchanged. Old NULL rows stay NULL (unrecoverable by kind/name) — noted as a follow-up.
  • place_bracket / scale_out / _roll_stop still write rule_id=NULL (they only receive a rule_name, and rules.name has 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.yaml
  • keel-live-run.sh
  • com.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.