keel v0.3.2
Built from 712c976. Version binds to this hash:
keel --version reports keel 0.3.2+712c976b3f33 [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.2-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(engine): log why a rule declined, with the near-miss numbers (#158)
A cycle reporting signals=0 said nothing about why. Whether price was 1% or 40% away from
the trigger is the difference between "nearly fired" and "nowhere near", and answering it meant
replaying the rules against the database by hand.
Now the log answers it:
{"event": "engine.no_signal", "rule": "turtle_breakout", "product": "ETH-USD",
"gate": "donchian_high", "close": 1919.9, "entry_level": 1979.0, "gap_pct": 3.08}How
Rule.last_rejection (additive, defaults None) records the gate a bar tripped and the numbers
behind it. engine.evaluate folds it into the engine.no_signal event it already emits per
declining rule, along with the rule's product.
No new event and no new call site — that is the constraint that shaped this. strategy.backtest
and sim.portfolio_sim drive detect() once per bar, so logging from inside the rule would emit
millions of events in a sim. The rule only ever writes an attribute; the engine, which already
logs exactly once per declining rule, does the logging.
product comes from the rule rather than a Setup (there isn't one) and is omitted entirely when
a rule declares none — every turtle rule is named turtle_breakout, so without it the five lines
a five-product cycle emits are indistinguishable.
What TurtleBreakout records
Every decline path: insufficient_history, donchian_high, adx, macd_histogram,
min_volume, atr, stop_not_below_entry, s1_filter. From donchian_high down, each carries
close / entry_level / gap_pct — so the interesting case, price clearing the channel and
something else declining it, is visible rather than inferred. That is exactly ETH-USD on
2026-07-28: it closed above its 40-day high and only the ADX filter (21.3 < 25) declined it,
which signals=0 gave no hint of.
A bar that fires clears the field, so a stale reason can never misreport a firing rule.
Scope
Purely diagnostic — no gate, guard or sizing path reads last_rejection, and a rule that records
nothing logs the bare event it logged before (pinned by a test). Only turtle_breakout records
reasons so far; the other three rules are unaffected and log exactly as before.
Verified against the live paper database: the emitted numbers reproduce the by-hand analysis
exactly (BTC 4.82% below channel, ETH 3.08%, PAXG 7.49%, XLM 44.68%, ADA 23.49%).
Fixes
fix(tui): toast on 'r' so refresh is distinguishable from a dead key (#159)
Pressing r in the TUI appeared to do nothing. It was working — it just never said so.
What was wrong
The handler set no message:
if ch == ord("r"):
last_balance_ts = 0 # force the balance to re-fetch on the next iteration too
continueThree things stacked to make a working key look dead:
- No toast.
a(autonomy) andf(fetch) both assignmessage;rdidn't. - The repaint is byte-identical.
rre-reads the same local DB, which only changes when an
agent cycle runs — once a day on a daily-Turtle deployment. - Its one visible effect is deferred. The balance re-read sits after
_painton purpose
(so the first frame never blocks on a network call), so the new value appears on the next
repaint, not the onertriggered.
The fix
r now toasts, and the wording also answers the question the silence provoked — what r is for
compared to f:
refreshed local state -- balance updating (no candle fetch; use [f] for that)
Deliberate wording choices:
- "balance updating", not a claim it is already showing — it lands on the following frame.
- "no candle fetch" — the real distinction.
rpulls no market data at all;fbackfills 5
years for every allowlisted product. The footer calling both "refresh"/"fetch" gave no hint. - 77 chars, so it fits an 80-column terminal.
- Styles as
ok— a routine success must not borrow thealertcolour that_message_style
reserves for failures and for arming autonomy ON.
Tests
rpaints a toast, driven throughrun_livewith the existing_KeySequenceStdscr+
fake-curses harness, so it exercises the real key handler rather than a stand-in.- the toast styles as
ok, guarding against a future rewording that trips_message_style's
"failed" / "cancelled" keywords.
No behaviour change beyond the message: r does exactly what it did before.
Docs, CI & tooling
docs(readme): how to deploy a release, and ignore the operator wrappers (#157)
Two small chores that had been sitting outside the repo.
Deploying a release into a deployment
docs/RELEASING.md covers cutting a release but stops at the published artifacts. How to get
one into a deployment (~/keel) lived only in shell history, which is how a deployment ends up
a version behind without anyone noticing. New Deploying a new version section in the README
documents the four commands, plus the parts that are easy to get wrong:
- run them from the deployment directory — every path (
Release/,.venv) is relative to it --find-links Releaseis what lets the singlekeel_traderwheel resolve itskeel-core/
keel-broker-*siblings, which is why step 1 downloads all the wheels- installing by path rather than by bare name is deliberate:
keelon PyPI is an unrelated
project, sopip install keelfetches a stranger's code keel --versionis the check that matters — it must report[release]bound to a commit, and a
(DIRTY)or[checkout]build corresponds to no commit and must not be run against live funds- a new build takes effect on the next scheduled cycle with nothing to restart, since each cycle is
a fresh process — but a long-runningkeel tuikeeps the build it started with
The four commands were run against the live deployment as written; keel --version returns exactly
the line quoted in the README.
Ignore the operator wrappers
keel-live and keel-paper are deployment artifacts with config and database paths pinned
together, copied to ~/keel rather than tracked here — the same treatment their sibling scripts
already get. They were showing up as untracked in every git status.