Skip to content

keel v0.5.6

Choose a tag to compare

@github-actions github-actions released this 08 Aug 23:37
· 296 commits to main since this release
9d7dede

Built from 9d7dede. Version binds to this hash:
keel --version reports keel 0.5.6+9d7dedeb5b64 [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.5.6-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.

Other changes

docs(config): correct the "unvalidated skip set" claim on the live allowlist (#185)

What

Re-assessment of a judgment carried in a live-money config, config.live-sandbox.yaml, directly above the real-money allowlist:

# The 5-trend Turtle set (same as the paper-forward + the walk-forward/PBO analysis).
# SOL/LTC/LINK are deliberately excluded -- they're the unvalidated skip set.
allowlist: [BTC, ETH, PAXG, ADA, XLM]

Verdict: the decision is right; the stated reasons were mostly wrong. The comment is rewritten to say what's actually true, and the working is recorded in a new experiment doc. The allowlist is unchanged.

What the old comment got right

"Same as the paper-forward" is accurate at the rule levelkeel.db's paper-status rules are exactly 10/11/12 (BTC/ETH/PAXG) + 22 (XLM) + 24 (ADA), i.e. the live 5. That's a coherent origin story for the exclusion and the PR now credits it.

What it got wrong

  • "The walk-forward/PBO analysis" describes work that does not exist for these assets. Every walk-forward/PBO/ablation run in docs/experiments/ stops at BTC/ETH/PAXG. trials-ledger.jsonl symbol census over 69 rows: BTC 32, ETH 33, PAXG 32, and 0 each for ADA, XLM, SOL, LTC, LINK. The walk-forward that exists tuned a parameter (turtle_breakout.py:113, entry_lookback 20 → 40).
  • "Unvalidated" doesn't distinguish anything. None of the five live assets was validated either. keel-live.db rules 1–5 carry promoted_at IS NULL — seeded straight to live through the bypass rules seed warns about, and left in place 14 days.
  • No compliance asymmetry. SOL/XLM/LTC/ADA were attested in the same second (2026-07-23 12:15:12); LINK 160s later under the same governing source. All three excluded assets ADMIT keel assets screen and carry more daily history (1827 bars) than PAXG (456, on the list only via a documented waiver).
  • Wrong gate. The comment attaches a rule-level reason to an asset-level control; rail 1 (guards.py:369) is a flat membership test consulting neither attestations nor rule status.

The measurement nobody had run

The old comment guessed. Running keel rules backtest against a copy of keel.db settles the direction:

rule asset n_trades win rate expectancy pf
21 SOL 13 23.08% −12.41 0.259
23 LTC 5 0.00% −13.09 0.000
25 LINK 15 20.00% −0.665 0.630
22 XLM 8 37.50% +0.056 11.44
24 ADA 6 50.00% +0.121 5.52

All three excluded assets are negative-expectancy; LTC won none of five. The exclusion is correct — now for a reproducible reason instead of an invented one.

Stated honestly: n = 5–15 against min_trades = 100, in-sample, one window, candles 6 days stale for the excluded three. A directional check, not validation. None of the five clears the gate — which is why the allowlist is unchanged, and why the new comment explicitly warns against reading a positive expectancy as licence to add an asset.

What this PR does NOT do

The allowlist is unchanged. No asset attested, no rule promoted, no production database written — backtests ran against a scratchpad copy; ~/keel/keel.db's mtime is untouched.

Verification

  • load_config yields an identical allowlist, caps and weights; a comment-stripped diff against main shows no non-comment differences.
  • python -m pytest tests/ -q2078 passed. (uv run pytest fails to import keel_core on a clean tree too — pre-existing, unrelated; this diff touches zero Python.)

Review corrections (second commit)

An independent adversarial review caught three false/unsupported claims in the first commit, two of them in the live config:

  1. "Nothing has traded" was FALSEkeel-live.db holds signals=1, a filled live order (BTC-USD BUY 0.000778 @ 64267.30) and an open position from the properly-promoted dca rule 6. The deployment is not inert and carries exposure. Removed.
  2. The min_trades argument used the wrong mechanismrules promote gates on a candle backtest (commands/rules.py:180), not trade_outcomes. Conclusion unchanged, now resting on ADA n=6 / XLM n=8.
  3. "Never been backtested" was unprovable and self-contradicted the doc's own point that the backtests table is dead schema (db.py:140, zero writers). Dropped.

Plus: credited the true paper-forward half, softened "same source" for LINK (its attestation deliberately classifies it a utility token), and made the entry_lookback quote verbatim.

Open operator items

Recorded in the doc, not actioned here:

  1. Deployment drift~/keel/config.live-sandbox.yaml still has the old comment; sync it.
  2. keel-live.db carries almost no compliance state — screening the live allowlist against a copy, 0 of 5 admit; SOL is ironically the only asset it admits. Harmless (rail 1 reads config, not the DB) but inverts the comment's picture.
  3. Live-seeded rules left in place 14 days, against rules seed's own instruction.
  4. Stale candles — re-run the backtests after keel fetch before relying on them.
  5. Two open attestation questions — LINK's staking floor is emissions-funded; LTC has carried MWEB (opt-in confidential transactions) since May 2022.

docs: correct the SOL attestation note -- the staking page supports pays_yield=0 (#186)

Follow-up to #185, correcting a claim I made there.

The correction

Open item 3 said the live-DB SOL attestation's source (solana.com/staking) "sits awkwardly against pays_yield=0". Fetching the page shows the opposite — it is direct evidence for the field:

"In order to earn staking rewards … the tokens in a stake account must be delegated to a validator."

No rebasing, no automatic distribution to holders. Bare holding earns nothing, which is exactly what pays_yield=0 asserts.

The gap that is actually there

It is the wrong kind of source, not a contradictory one. AssetAttestation.source is documented as "where this was established — a URL, a standard, a scholar's ruling." A vendor product page establishes the mechanical fact; it cannot establish the ruling that a native L1 coin is Mal Hukmi, lawful to own and trade.

Every keel.db attestation carries both — KB source-86, the three-layer screen reasoning, and an explicit "Attests bare unstaked spot only." This row carries only the mechanical half, and only implicitly. screen_asset cannot tell the difference: screen.py:249 checks source.strip() is non-empty and nothing more.

Provenance — closed, not left as "should be confirmed"

~/.zsh_history records the exact command at epoch 1786148982, one second before the row's attested_at:

./.venv/bin/keel --config config.live-sandbox.yaml --db keel-live.db \
  assets attest --asset SOL --sector "layer-1 smart-contract blockchain infrastructure" \
    --backing native --source "https://www.solana.com/staking" \
    --attested-by "Elmehdi Aitbrahim"

Preceded by a placeholder attempt at 1786148907 and two keel fetch --products SOL-USD runs. Hand-typed at the terminal — not an agent, not the daemon, not the #185 investigation.

Also added

  • SOL now has two attestations with different evidentiary bases; which one governs depends on which --db a command runs against.
  • The page notes ~8% initial inflation decaying to 1.5%, so a bare holder is diluted rather than paid — not a riba yield, but whether that makes staking economically quasi-compulsory is a scholarly question, not one this file can answer.
  • None of it is live-reachable: SOL is not in the live allowlist and has no rule in keel-live.db, so rail 1 blocks any SOL intent regardless of the attestation.

Docs-only. No code, no config, no database touched.

docs: record the live-seeded rules as a deliberate, bounded standing exception (#187)

What

keel-live.db rules 1–5 (turtle_breakout on BTC/ETH/PAXG/ADA/XLM) have carried status = live with promoted_at IS NULL since 2026-07-24 — against rules seed's own printed instruction:

⚠️ seeded at LIVE status, bypassing the promotion gate … Do not leave live-seeded rules in place afterwards.

Reviewed 2026-08-08. Kept deliberately. Now recorded rather than silent.

This applies the standard the project already set for PAXG in docs/experiments/2026-07-20-allowlist-screen-first-run.md:

The one thing not to do is leave it silently exempt because it was already there.

The exception was already load-bearing — it just wasn't written down anywhere.

Why keep them

min_trades is 100 per rule, and this strategy cannot reach it. Measured by backtesting each rule over ~5.02 years of daily bars:

rule trades rate years to 100 trades
BTC 13 2.59/yr ~39
ETH 13 2.59/yr ~39
XLM 8 1.59/yr ~63
ADA 6 1.19/yr ~84
PAXG 4 3.20/yr ~31

Waiting for the gate is not a slower route to the same destination — it is no route. The sandbox exists to accumulate the live evidence the promotion gate demands and cannot itself generate. Demoting these rules would end that experiment without putting anything in its place.

What bounds the risk, since the gate didn't

The caps (max_exposure_usd 200 total at once, max_per_order_usd 100), the eighteen un-overridable guards.py rails, rail 1's allowlist, and rail 14's monthly allowance.

The bypass is of the evidence gate, not the safety rails — separate mechanisms, and only the first was skipped.

What it is not

Not a precedent for admitting assets. Not a reason to raise caps. Not a claim these rules are validated — no walk-forward or PBO run has ever covered ADA or XLM, and none of the five clears any promotion axis (see #185).

Revisit triggers

Named, rather than an open-ended pass: caps rising above this sandbox's few-dollars-of-damage scale; the rule set changing; a rule trading materially more often than the table above; or the sandbox being treated as evidence for anything beyond itself.

Where it's recorded

Two places an operator actually reads:

  • config.live-sandbox.yaml — a block beside the allowlist it applies to.
  • docs/go-live-runbook.md §3 — a new "Standing exception" subsection. §3 previously documented the act of short-circuiting the gate but not the ongoing state.

Verification

  • load_config yields an identical allowlist, caps and weights; comment-stripped diff against main is empty.
  • python -m pytest tests/ -q2078 passed.
  • No database written, no rule status changed, no config value altered.

docs(experiments): build the §80.16 independence harness; measure cross-asset (#188)

What

research/independence.py has implemented §80.16's five measurements since PR #103, but nothing in the repo drove them over two real rules. The 2026-07-20 cross-horizon run was ad hoc and left no script behind, so every future between-family question started from zero. This adds the script, plus its write-up and three diagnostic_only ledger rows.

Three passes

pass varies mean P&L corr (closed) outcome
between-family (turtle vs rsi_meanrev) rule family degenerate, arm B never trades
cross-horizon (40/20 vs 80/40) lookback 0.508 reproduces PR #103 exactly
cross-asset (turtle vs itself, other asset) the underlying 0.011 the finding

The between-family pass is degenerate, not low. On daily bars rsi_meanrev takes zero trades on all five allowlisted assets — oversold=20 on a daily RSI(14) is near-unreachable (PAXG and XLM never print one). Arm B is settled-refuted anyway (§74.3), so it could never have tested candidacy; it is a shakedown.

The calibration is why the rest is trustworthy. BTC 0.802, ETH 0.934, median entry gap 0 days — exact reproduction of 2026-07-20-horizon-independence.md. It also settles which P&L convention PR #103 used, which that write-up never states: closed reproduces the published figures; mark-to-market gives 0.813 / 0.759.

Cross-asset is the finding. 0.011 / 0.175 / 0.144 against cross-horizon's 0.508 / 0.585 / 0.510, same method, same assets. Asset breadth is roughly an order of magnitude better than horizon breadth as a source of independent evidence. §79.1 redirected the frequency plan to breadth without saying which; this says which. PAXG stands out — zero Jaccard against ETH, ADA and XLM.

This also tests an assumption 2026-07-20-minbtl-sizing.md's allowlist case rested on: that added assets multiply trades/year roughly linearly, true only if those trades carry independent information. At the measured correlations they largely do.

Confidence

Arm A reproduces the shipped trade counts exactly (13/13/4/6/8), so the harness is wired to the real rule. Correlations rest on 4–13 trades per asset with no confidence intervals — the direction is far more robust than any individual cell. Caveats are in the write-up, including that this window had crypto broadly trending together (§79.16).

Ledger

Three rows, all diagnostic_only / a_priori, keel trials verify → chain intact. M 69 → 72, N_decisions unchanged at 30, so MinBTL is untouched. The cross-asset ρ̄ is deliberately not substituted into §78.2's , which corrects a trials count and is a different quantity.

Scope

⚠️ Corrected after merge. This section originally read "docs/experiments/ only — no library code, no config, no rule, no gate." That was true when written and false at merge: a second commit was pushed to this branch before it squashed, so a code change shipped inside this PR. Corrected here rather than left to mislead, because scripts/release_notes.py inlines this body into the 0.5.6 release notes.

The research half is docs/experiments/ only. ruff clean; docs/ is outside the project's mypy scope (files = ["packages", "keel", "tests"]), matching the existing 2026-08-05-coinbase-asset-class-probe.py.

Also shipped in this PR — fix(assets): the discovery pre-filter now measures what the gate measures

assets discover pre-filtered on the venue's reported 24h quote volume; assets screen admits on the median of volume × close over cached history. Same units, different statistics, and the gap changes outcomes in both directions:

  • BICO was shortlisted on a reported $12.81M/24h, then rejected by the screen at a median daily volume of 108,004 — 9× under the floor, a 227× gap.
  • DOGE was nearly dropped for sitting 1.7% below the $5M discovery floor when, on the admission criterion, it is ~30× above it.

screen.median_daily_quote_volume is now the single definition of the statistic — cli._market_facts had it inline, and a second copy next to discover would drift, which is the bug. assets discover --probe-liquidity samples 180 days of daily candles per candidate (one extra request, same cost model as --probe-history) and marks LOW/ok against the same floor screen_asset applies.

Against the live venue it catches BICO (36,137) plus two the old filter passed through: IMU (71,713) and GWEI (440,864) — three of the top fourteen by 24h volume are sub-floor on the real criterion.

An estimator, not a verdict, and the output says so: 180 days recent vs the screen's full cached history. ZEC probes 33.1M against a full-history median of 1.23M — a 27× spread from a recent surge. ok means "worth pulling candles for", never "will be admitted". No behaviour change to the gate: same criterion, same floor, same verdicts. Tests: 2137 passed, ruff and mypy clean.

chore: bump to 0.5.6 for the discovery liquidity probe (#189)

Bumps pyproject.toml and all four workspace members 0.5.5 → 0.5.6, plus uv.lock. Same shape as #184.

Why now

assets discover --probe-liquidity and screen.median_daily_quote_volume landed on main in #188 but are in no release, so the deployment — running keel 0.5.5+7b02bfd8fbbc [release] — cannot run them. The keel-asset-scout skill has been updated to make the probe the first step of every sweep, which fails outright against 0.5.5. This closes that gap.

What 0.5.6 contains

The discovery pre-filter now measures what the admission gate measures. assets discover filtered on the venue's reported 24h quote volume while assets screen admits on the median of volume × close over cached history — same units, different statistics, and the gap changes outcomes in both directions:

  • BICO was shortlisted on a reported $12.81M/24h and then rejected by the screen at a median daily volume of 108,004 — 9× under the floor, a 227× gap between the two numbers.
  • DOGE was nearly dropped for sitting 1.7% below the $5M discovery floor when, on the admission criterion, it is ~30× above it.

screen.median_daily_quote_volume is now the single definition of the statistic (cli._market_facts had it inline; a second copy next to discover would drift, and that drift is the bug). --probe-liquidity samples 180 days of daily candles per candidate — one extra request, same cost model as --probe-history — and marks LOW/ok against the same floor screen_asset applies.

Against the live venue it catches BICO (36,137) plus two the old filter passed straight through: IMU (71,713) and GWEI (440,864). Three of the top fourteen by 24h volume are sub-floor on the real criterion.

It is an estimator, not a verdict, and the output says so: 180 days recent vs the screen's full cached history. ZEC probes 33.1M against a full-history median of 1.23M — a 27× spread from a recent surge. ok means "worth pulling candles for", never "will be admitted". A recent window also makes a false LOW unlikely for a currently-active asset, which is the safe direction for a pre-filter.

No behaviour change to the gate itself — same criterion, same floor, same verdicts.

Verification

  • 2137 passed, ruff clean, mypy clean across 203 source files
  • keel --version0.5.6+9dcccadb6939 on this branch

Note on #188

#188 squash-merged the code change together with the docs/experiments harness, so its title and body describe only the research work. Its body is what scripts/release_notes.py will inline for 0.5.6, so it has been edited to describe the code change that actually shipped in it.