Skip to content

doctor: nothing warns when an open tranche has no live rule and no retry record -- PAXG sat unwatched for five cycles #811

Description

@eaitbrahim

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.
  • its three bracket placements were rejected — 09-16 and 09-17 on 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 on STOP_TRIGGER_PRICE_OUT_OF_BOUNDS,
    the stop having fallen below the market (4521.76 stop vs ~4359 last).
  • 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;
  • both appear in --json with products populated, per The wrappers never fetch and never run doctor — the freshness rail fires and nothing reports it #642, so a wrapper can name the
    tranche without parsing prose;
  • 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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingrailsUn-overridable safety rail / guard (Compliance & rails)toolingDev/release tooling (Docs, CI & tooling)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions