Skip to content

Reconcile: out-of-band venue executions leave phantom exposure; no keel positions close #798

Description

@eaitbrahim

Problem

An operator cannot reduce keel's measured exposure, and a sale made on the venue makes
keel's picture of the account wrong in the tightening direction.

_open_exposure_by_asset (keel/execution/guards.py:339) builds the per-asset figure that
feeds rails 4/5/6 from repo.get_orders(mode="live", ...) — keel's own orders table. Three
paths could reconcile that with reality, and none does:

  1. No manual close. keel orders exposes only cancel and list. close_position
    exists at keel/data/repository.py:1397 but has no CLI.
  2. keel db import writes elsewhere. keel/data/csv_import.py imports Coinbase
    transaction-history CSVs into the transactions table. Guard 6 never reads it.
  3. reconcile_open_orders cannot discover. keel/execution/reconcile.py:89 iterates
    _polled_rows(repo) and calls broker.get_order(native_id) per row. It refreshes orders
    keel placed; it has no path to an order keel did not place.

Consequence

Sell a position on Coinbase and keel still counts the original BUY notional. Exposure stays
high, so rails 4/5/6 keep vetoing entries against inventory that is gone — and any exit the
engine later generates would try to sell what is no longer there.

The failure is quiet: nothing logs, doctor reports nothing, and the only symptom is
entries being refused for a reason that is no longer true.

Observed

On the live profile, guard 6 reports BTC exposure 101.81 (the two filled DCA buys at
50.00 + 51.81) against a 100.00 ceiling. Every BTC entry is vetoed. The obvious
operator response — sell a tranche — would not move the number at all.

Proposed

  • keel positions close <id> — records an operator-declared exit so the orders ledger
    reflects it, confirmation-gated like the other dangerous verbs, written in a way guard 6
    actually reads (an order row, not only a positions mutation).
  • Balance reconciliation — compare venue-reported holdings against
    _open_exposure_by_asset each cycle and surface a divergence. Given the module's stated
    principle ("never let a row nobody can read make this figure SMALLER"), the safe default
    is to report the skew loudly rather than silently trust the venue and shrink a cap.
  • A doctor check for the divergence, so it is visible before it blocks something.

Note on direction

The current behaviour fails closed, which is the right default — it refuses orders rather
than permitting them. This is not a request to make exposure shrink automatically. It is a
request for an auditable operator path and for the skew to be visible, since today it is
neither.

Found while diagnosing why the live loop had placed no order since 2026-08-21. Related: #797.

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)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions