Skip to content

keel v0.9.1

Choose a tag to compare

@github-actions github-actions released this 18 Aug 01:30
· 48 commits to main since this release
1be07f2

Built from 1be07f2. Version binds to this hash:
keel --version reports keel 0.9.1+1be07f208d40 [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.9.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.

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

Other changes

docs(experiments): the honest-cost restatement and the DCA dip-bonus ablation (#339) (#347)

What & why

Closes #339's measurement checkboxes. Two firsts, one date:

The honest-cost simulate re-run — the first keel simulate under per-product slippage (#334, v0.9.0): verdict TRAIN MORE, unchanged, and the correction runs exactly the conservative direction — BTC identical (floor-anchored), XLM 4.7567→4.4435, ADA 1.7638→1.7087, PAXG capped at 50bp. No flattered outlier survived; the TON-class 3.751-on-n=9 result can no longer print at thin-book pricing. This is the measurement that decides #335: the gate opt-in stays precautionary (nothing promotes on thin assets today), with the evidence now on record.

The DCA dip-bonus ablation — first-ever coverage of the only unswept rule family, three cells (dip_bonus_pct 0/1/2) through sim/portfolio_sim at keel's real costs over ~5 years of BTC hourly. The pre-registered ordinal expectation was NOT confirmed: cells within 0.65% relative, no monotone direction (1.40742 / 1.40424 / 1.41325). The honest reading is stated: mechanism mismatch (the citation holds cash until threshold dips; the param always buys and scales size) plus single-asset/single-window power. No cell preferred; the live rule stays at 0.

Both trials appended to the hash-chained ledger (chain verified clean). Also records the deployment actions: DCA twins re-enabled via keel rules enable + promote --force (the documented bypass, warnings logged), so the paper pipeline now has an always-firing family. v0.9.0 was cut and deployed first (PR #346; four distributions verified, keel versions clean, both DBs at schema 10).

Tests-first evidence

Measurement PR: the instruments' outputs are quoted verbatim; the ledger rows were appended through keel.research.ledger.append_trial with verify_chain returning []. Gates: 2947 passed / 1 skipped; ruff clean; mypy clean.

Gates

  • uv run ruff check keel tests packages — All checks passed!
  • uv run mypy — Success: no issues found in 238 source files
  • uv run pytest -q — 2947 passed, 1 skipped

Scope check

  • Documentation, experiment records, and ledger rows only — no code, rails, rules, or classifications touched.

fix(data): keel fetch honors config.market_data.granularities (#354)

Bug (#349)

keel fetch hardcoded the module constant _SIM_GRANULARITIES = [ONE_HOUR, ONE_DAY] everywhere in its flow, so it ignored config.market_data.granularities. Every shipped config lists ONE_DAY/ONE_HOUR/FIFTEEN_MINUTE, which meant the runbook's documented warm step keel fetch never warmed the FIFTEEN_MINUTE confirmation series: a fresh deployment's cache had zero 15-minute candles, and the agent's first cycles inherited a full multi-hundred-request catch-up. keel agent and keel monitor already did the right thing (list(config.market_data.granularities)); fetch was the odd one out.

Fix

The fetch flow in keel/cli.py now computes granularities = list(config.market_data.granularities) once and threads it through all four places that previously hardcoded the simulate pair:

  • _assess_products takes the granularities as an explicit parameter (both call sites updated);
  • the --check --fail-on-gaps unexplained count;
  • ensure_history(...) — the actual warm fetch, core of the bug;
  • the --repair-gaps loop.

Also fixed: the TUI's _do_fetch (keel/commands/tui.py) — the dashboard twin of keel fetch, documented as such ("matches keel fetch --years's own default") — had the identical bug and now uses the same config-driven list.

simulate deliberately KEEPS _SIM_GRANULARITIES ([ONE_HOUR, ONE_DAY]): those are the backtest engine's supported timeframes — an engine limit, not a data choice. A short comment where the fetch list is computed explains the asymmetry, and a new pin test (test_simulate_still_fetches_exactly_the_engine_timeframes) locks it.

Tests (red first)

New tests in tests/data/test_fetch_cli.py, all confirmed RED against the pre-fix code for the right reasons, then green:

  • test_fetch_warms_every_configured_granularities — before the fix: Extra items in the right set: FIFTEEN_MINUTE (fetch requested only ONE_DAY/ONE_HOUR under a three-granularity config);
  • test_check_reports_freshness_for_every_configured_granularity — before: 'FIFTEEN_MINUTE' not in output;
  • test_fetch_touches_only_the_configured_granularities — before: Extra items in the left set: ONE_HOUR (config listed only ONE_DAY; fetch ignored it).

Existing tests updated to match the intended behavior change (they seed a three-granularity config, so their fixtures now seed the 15-minute series too; the repair pass count goes 6 -> 9 = 3 products x 3 granularities). Full suite green.

Behavior change for existing deployments (intentional, benign)

Scheduled and manual keel fetch runs now also ensure FIFTEEN_MINUTE history. The agent already polls 15-minute candles every cycle, so the cache receives them anyway — fetch just makes the documented warm step honest. --years (default 5) applies to all granularities uniformly; a venue lacking deep 15-minute history lands in the already-existing "some series are still short ... usually benign" branch — no new handling needed.

Docs

  • docs/operations/scheduled-fetch.md: notes that fetch warms exactly market_data.granularities (same series agent/monitor poll) and that simulate is the deliberate exception.
  • docs/operator-runbook.md: the paper-hourly bootstrap warm-step comment now states the config is the source of the granularity list.

Gates

  • uv run pytest -q — 2951 passed, 1 skipped
  • uv run ruff check keel tests packages — all checks passed
  • uv run mypy — no issues in 238 source files

Fixes #349

test(data): collapse repeated fetch fixture seeding into helpers (#355)

Dedupes the three-granularity seeding block repeated across six fetch tests in tests/data/test_fetch_cli.py (Sonar flagged 19.2% duplicated lines on new code in #354, gate <=3%). The block now lives in _seed_current (day_lag for the tolerance fixture, skip_day for the gap fixtures, replacing _seed_with_hole), and the two day-only stale fixtures share _seed_stale_days. No behavior change: bar counts and timestamp semantics are identical, and the same tests pass before and after (19/19 in the module; full suite 2951 passed, 1 skipped).

chore(release): 0.9.1 (#356)

Version bump across the six pyprojects for the 0.9.1 release.

Ships since 0.9.0:

  • #349 / #354keel fetch (and the TUI's _do_fetch twin) now honors config.market_data.granularities, so the documented warm step also ensures the FIFTEEN_MINUTE confirmation series every shipped config lists. simulate deliberately keeps [ONE_HOUR, ONE_DAY] (backtest engine limit), pinned by a new test. Intentional, benign behavior change: scheduled/manual fetch on existing deployments now also ensures 15-minute history (the agent already polls it every cycle).
  • #355 — test-fixture dedupe (SonarCloud duplication gate back to green).

Reviewed as #354 (adversarial round + independent final review: goal met, no schema/rails changes) and #355.

chore(release): pin workspace siblings to 0.9.1 (#357)

Follow-up to #356: the bump updated the six version fields but not the ==-pinned sibling dependencies, so tests/test_packaging.py::test_workspace_siblings_are_pinned_to_the_exact_version fails on main (5 failures). This updates every keel-core==/keel-broker-api==/keel-broker-coinbase== pin to 0.9.1.

Verified locally: uv run pytest -q tests/test_packaging.py → all pass. Required before re-running the 0.9.1 release workflow (the previous dispatch was canceled).