You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
keel doctor reports data health, rails and ledger consistency, but nothing asks the
question that matters most about a held position: is anything still watching it?
Found in the live deployment on 2026-09-22, while answering "why has no order been placed
since 2026-09-18". The answer to that question was benign — the only live rule is the
weekly BTC DCA, which is between buys. The PAXG tranche sitting beside it was not.
What the deployment looks like right now
positions.id=3 PAXG-USD turtle_breakout opened 2026-08-25
qty 0.01320427494019137563114227965
entry_fill 4673.23
initial_stop 4521.76390215979454
bracket_order_id NULL <- nothing resting at the venue
status open
rule 3 (turtle_breakout PAXG-USD) was demoted to status=paper on 2026-09-15, so the
live cycle no longer evaluates PAXG at all: agent.feed_polled products=['BTC-USD']
on every cycle since 2026-09-16. No live exit rule can ever close this tranche.
reconcile.position_unprotected fired CRITICAL on each of those cycles — and then stopped. unbracketed:PAXG-USD is absent from agent_state (it is still present in keel-live.db.bak-before-0.16.0-20260916-202118), so reconcile_unbracketed_positions skips the tranche at its if not intent: continue
and says nothing. Five cycles (09-18 … 09-22) have passed in silence.
Whatever cleared that record — the shipped rejection path re-writes it, so most likely a
hand clear during the #807 session — the failure shape is the point:
the only channel that reported this position was driven off a retry record, and
clearing the retry record silenced the channel without closing the exposure.
A ledger-driven check cannot be silenced that way. positions is the table that knows a
tranche is held, and it still says open.
Proposed: two findings in keel/commands/doctor.py
Both sit naturally beside orphan_bracket_findings and unbooked_exit_findings, and
follow the same discipline: stated over the tables that must agree, not over the bug that
broke them; pure functions over plain rows; every finding names the fix.
position.unmanaged — an OPEN tranche whose product is not covered by any live rule
in this profile. Demote a rule with inventory open and the exit side vanishes with it;
nothing today notices. Condition: no row in rules with status='live' whose params.product_id equals the tranche's product_id. Note that positions.rule_id is
NULL on everything opened before #803 (all four open tranches here), so the check must
match on product, not on the ownership link.
position.unprotected — an OPEN tranche that recorded an initial_stop > 0, has no
resting bracket (bracket_order_id NULL, or the order is not in {pending, partially_filled, filled} — reuse reconcile._has_resting_bracket), and has
no unbracketed:<product> retry record either. That third clause is what makes it the
complement of the reconcile sweep rather than a duplicate of it: the sweep covers tranches
with a retry record, this covers tranches whose retry record is gone.
initial_stop > 0 excludes DCA for the same reason reconcile_unbracketed_positions skips it silently — DCA carries no stop by design, and a
finding that fired on all three open BTC tranches every run is a finding that gets ignored.
(The DCA tranches would still surface under position.unmanaged if rule 6 were ever
demoted, which is correct: unmanaged inventory is unmanaged whether or not it had a stop.)
Status: WARN for both, not FAIL. Holding spot with no stop is a legitimate position a
human may have chosen — the PAXG tranche above is being held deliberately now that it has
been surfaced. What is wrong is that nobody was told. FAIL would also halt cycles over a
state the operator has already accepted.
Fix lines.position.unmanaged → re-promote the owning rule, or close the tranche by
hand; this is the second issue pointing at the gap #798 names (keel positions close does
not exist). position.unprotected → keel doctor cannot re-place the bracket itself, so
it should name the position and the levels and say that the next cycle will not retry.
Acceptance
a demoted rule with an open tranche produces position.unmanaged WARN naming the
product, tranche id and the rule's current status;
an open tranche with a stop, no resting bracket and no retry record produces position.unprotected WARN; adding either the bracket or the retry record clears it;
a DCA tranche (initial_stop 0/absent) produces neither;
run against ~/keel/keel-live.db as it stands today, the pair reports PAXG-USD tranche 3
under both findings.
Related: #798 (out-of-band exposure, no keel positions close), #195 (the retry sweep this
complements), #802/#807 (the rejections that produced the state), #639 (the sibling
ledger-consistency check).
keel doctorreports data health, rails and ledger consistency, but nothing asks thequestion that matters most about a held position: is anything still watching it?
Found in the live deployment on 2026-09-22, while answering "why has no order been placed
since 2026-09-18". The answer to that question was benign — the only
liverule is theweekly BTC DCA, which is between buys. The PAXG tranche sitting beside it was not.
What the deployment looks like right now
turtle_breakout PAXG-USD) was demoted tostatus=paperon 2026-09-15, so thelive cycle no longer evaluates PAXG at all:
agent.feed_polled products=['BTC-USD']on every cycle since 2026-09-16. No live exit rule can ever close this tranche.
Too many decimals in order price(Bracket prices are never quantized to the venue's price tick -- every protective bracket is rejected #802/fix(execution): a cache record predating quote_increment is a MISS, not an "unknown" #807), then 09-17 14:52 UTC onSTOP_TRIGGER_PRICE_OUT_OF_BOUNDS,the stop having fallen below the market (4521.76 stop vs ~4359 last).
reconcile.position_unprotectedfired CRITICAL on each of those cycles — and thenstopped.
unbracketed:PAXG-USDis absent fromagent_state(it is still present inkeel-live.db.bak-before-0.16.0-20260916-202118), soreconcile_unbracketed_positionsskips the tranche at itsif not intent: continueand says nothing. Five cycles (09-18 … 09-22) have passed in silence.
Whatever cleared that record — the shipped rejection path re-writes it, so most likely a
hand clear during the #807 session — the failure shape is the point:
A ledger-driven check cannot be silenced that way.
positionsis the table that knows atranche is held, and it still says
open.Proposed: two findings in
keel/commands/doctor.pyBoth sit naturally beside
orphan_bracket_findingsandunbooked_exit_findings, andfollow the same discipline: stated over the tables that must agree, not over the bug that
broke them; pure functions over plain rows; every finding names the fix.
position.unmanaged— an OPEN tranche whose product is not covered by anyliverulein this profile. Demote a rule with inventory open and the exit side vanishes with it;
nothing today notices. Condition: no row in
ruleswithstatus='live'whoseparams.product_idequals the tranche'sproduct_id. Note thatpositions.rule_idisNULL on everything opened before #803 (all four open tranches here), so the check must
match on product, not on the ownership link.
position.unprotected— an OPEN tranche that recorded aninitial_stop> 0, has noresting bracket (
bracket_order_idNULL, or the order is not in{pending, partially_filled, filled}— reusereconcile._has_resting_bracket), and hasno
unbracketed:<product>retry record either. That third clause is what makes it thecomplement of the reconcile sweep rather than a duplicate of it: the sweep covers tranches
with a retry record, this covers tranches whose retry record is gone.
initial_stop > 0excludes DCA for the same reasonreconcile_unbracketed_positionsskips it silently — DCA carries no stop by design, and afinding that fired on all three open BTC tranches every run is a finding that gets ignored.
(The DCA tranches would still surface under
position.unmanagedif rule 6 were everdemoted, which is correct: unmanaged inventory is unmanaged whether or not it had a stop.)
Status: WARN for both, not FAIL. Holding spot with no stop is a legitimate position a
human may have chosen — the PAXG tranche above is being held deliberately now that it has
been surfaced. What is wrong is that nobody was told. FAIL would also halt cycles over a
state the operator has already accepted.
Fix lines.
position.unmanaged→ re-promote the owning rule, or close the tranche byhand; this is the second issue pointing at the gap #798 names (
keel positions closedoesnot exist).
position.unprotected→keel doctorcannot re-place the bracket itself, soit should name the position and the levels and say that the next cycle will not retry.
Acceptance
position.unmanagedWARN naming theproduct, tranche id and the rule's current status;
position.unprotectedWARN; adding either the bracket or the retry record clears it;initial_stop0/absent) produces neither;--jsonwithproductspopulated, per The wrappers never fetch and never run doctor — the freshness rail fires and nothing reports it #642, so a wrapper can name thetranche without parsing prose;
~/keel/keel-live.dbas it stands today, the pair reports PAXG-USD tranche 3under both findings.
Related: #798 (out-of-band exposure, no
keel positions close), #195 (the retry sweep thiscomplements), #802/#807 (the rejections that produced the state), #639 (the sibling
ledger-consistency check).