Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions docs/fiqh-basis.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,12 @@ found from. The trigger is the venue's own statement, never keel's ledger: cance
over a position that really exists strips a live holding of its only protection, and the ledger
can be stale in exactly that direction.

What remains open at the venue boundary is #666: on a cash account every case above is a
rejected order rather than a short, and keel has no cash-account posture check on Coinbase —
`verify_cash_account` exists only on the Alpaca adapter.
At the venue boundary the distinction that matters is the account's posture: on a cash account
every case above is a rejected order rather than a short. Both adapters now carry a
`verify_cash_account` check (#666), and **rail 22** (#691) reads a per-venue operator attestation
of that posture, vetoing new ENTRIES when none stands. Neither the check nor the rail can affirm
a cash account — see "Nothing can affirm that the account cannot go short" below for what that
means and what carries the residual.

Beside the rails — not among them, and not numbered — sits one routing-time check with the
same prudential character: the **max-spread entry gate** (#350, `keel/execution/executor.py`)
Expand Down Expand Up @@ -321,10 +324,33 @@ Stated, not hidden — each is a place where keel's encoded behaviour could be w
possession, and here silence is not evidence of a cash account either.

So the layered defence is sound whenever the venue contradicts itself, and rests on the
operator's own knowledge whenever it does not. That residual is a human attestation this
repository has not yet built — the second half of the #233 pattern, where venue evidence can
refute an attestation but cannot manufacture one. Until it exists, "this account cannot go
short" is something the operator knows and keel does not.
operator's own knowledge whenever it does not. **Since 2026-09-03 that residual is recorded
rather than merely acknowledged (#691).** `keel posture attest --spot-cash` writes a per-venue
claim; **rail 22** vetoes new ENTRIES — and only entries, never an exit — when none stands.
This is the second half of the #233 pattern: venue evidence can refute an attestation and
cannot manufacture one, so an INTX portfolio found at broker-build marks a standing claim
REFUTED, and nothing ever marks one confirmed.

Three properties of that record are load-bearing, and none is incidental:

- **There is no `CONFIRMED` state.** Trade scope has one because the venue re-proves it on
every accepted placement. Nothing can ever prove a spot account is cash-only, so a
`CONFIRMED` value would be a state nothing is entitled to write — and an unreachable state is
one a later reader eventually writes anyway. Its absence is the design.
- **The claim EXPIRES**, after 90 days. Nothing re-confirms it, ever, so a due date is the only
thing standing between a lapsed claim and a live entry — the same reasoning as the
subscription record, which is likewise user-asserted with no observation channel. `doctor`
warns 15 days out rather than at the cliff.
- **The claim is about a credential, not just a venue.** A posture attested under one
credential is not a claim about the account another credential reaches (#633), so a rotated
key withdraws permission until the operator re-attests.

What has NOT changed is the epistemics, and this is the sentence to keep if only one survives:
**keel still cannot verify this, and neither can the venue.** What the engine now does is
record a human's statement, expire it on a clock, let venue evidence contradict it, and refuse
to trade without it. "This account cannot go short" remains something the operator knows and
keel takes on trust — the difference is that the trust is now dated, revocable, and written
down, rather than assumed.

- **ZEC and the rest of the deferrals.** The candidate-universe record lists the open
questions the attestation step has to answer and "which this agent must not answer".
Expand Down
120 changes: 120 additions & 0 deletions keel/commands/brokers.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,24 @@
from __future__ import annotations

import json
import sqlite3
import textwrap
import time
from dataclasses import asdict, dataclass
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as dist_version
from pathlib import Path
from typing import Any

import click
from keel_core.cash_posture import (
MARGIN_ENABLED,
CashPostureState,
VenueCashPosture,
)

from keel.commands._common import DISCLAIMER
from keel.commands.posture import _utc_date
from keel.venue_readiness import VenueReadinessRow, gather_readiness

#: The venues a SHIPPED deployment selects -- the whole wired/optional classification, in
Expand Down Expand Up @@ -325,6 +333,113 @@ def render_readiness_lines(rows: list[VenueReadinessRow]) -> list[str]:
return lines


#: The line the posture block ends on, and it is not decoration. Every other readiness-adjacent
#: surface in keel reports something it CHECKED; this one reports something no venue will confirm.
#: A reader who takes these rows for verification has the guarantee backwards -- which is the one
#: misreading `docs/fiqh-basis.md`'s cash-posture section exists to prevent.
CASH_POSTURE_HONESTY_LINE = (
" No venue exposes a cash-versus-margin field for spot, so these are the OPERATOR's "
"statements, expiring on a clock. Venue evidence can refute one; nothing can confirm one."
)


def render_cash_posture_lines(
records: list[VenueCashPosture], *, now_ts: int, unreadable: bool = False
) -> list[str]:
"""The cash-posture block (#691): what a human has stated about each venue account.

A THIRD block, after declarations and readiness, and that follows this codebase's own
argument rather than a preference. `keel/venue_readiness.py` renders readiness separately
because merging two different questions "would re-blur exactly the distinction #233 exists to
draw". Posture is a third question -- readiness asks whether this CREDENTIAL may trade,
posture asks whether this ACCOUNT can borrow -- and folding it into either would repeat the
mistake both were shaped to avoid.

PURE over the records it is handed. Sorted by venue so the same database renders the same
text twice.
"""
lines = ["", "cash posture (rail 22) -- attested by you, refutable by the venue:"]
if unreadable:
# Deliberately does NOT print the attest command. That prompt is the thing this whole
# distinction exists to withhold: the record may be perfectly good and merely unread,
# and re-attesting over it would reset its clock on an unchecked account.
lines.append(
" a database is present but could not be read -- whether a posture is attested is "
"UNKNOWN, which is not the same as nothing being attested"
)
lines.append(CASH_POSTURE_HONESTY_LINE)
return lines
if not records:
lines.append(" no venue has an attested cash posture -- rail 22 vetoes live ENTRIES")
lines.append(" next: keel posture attest --spot-cash")
lines.append(CASH_POSTURE_HONESTY_LINE)
return lines
for record in sorted(records, key=lambda r: r.venue):
state = record.state.value.upper()
if record.state is CashPostureState.ATTESTED and not record.is_current(now_ts):
state = "EXPIRED"
expires = (
_utc_date(record.attest_due_ts) if record.attest_due_ts is not None else "never set"
)
lines.append(
f" {record.venue}: {state} attested={record.attested_posture} expires={expires}"
)
if record.attested_posture == MARGIN_ENABLED:
lines.append(" margin-enabled: rail 22 vetoes live ENTRIES on this venue")
if record.refuted_ts is not None:
reason = f": {record.refuted_reason}" if record.refuted_reason else ""
lines.append(
f" venue evidence contradicted a claim here on "
f"{_utc_date(record.refuted_ts)}{reason}"
)
lines.append(CASH_POSTURE_HONESTY_LINE)
return lines


def _cash_posture_records(
ctx: click.Context,
) -> tuple[list[VenueCashPosture], bool]:
"""`(records, unreadable)` -- never raises, never migrates, never writes, never creates a file.

THE TUPLE IS NOT DECORATION, and `venue_readiness._read_only_trade_scope` already paid for
this lesson one function away: returning a bare empty list for both "nothing is attested" and
"there is a database this process could not read" lets the display assert the former about
the latter, and then advise `keel posture attest --spot-cash`. That is worse here than it is
for trade scope -- re-attesting resets `attested_ts` and `attest_due_ts`, and the TTY gate
would ask the operator to affirm a cash account they may not have re-checked. A display bug
would become a prompt to make an unverified claim.

⚠️ **Not `keel.data.db.connect`**, for the reason that function's own docstring gives: it is
the read-WRITE opener, it runs `PRAGMA journal_mode = WAL` on whatever it opens, and a
read-only informational command must not modify the deployment database or leave `-wal`/`-shm`
sidecars behind. The `mode=ro` URI shape (#610's seam) is what a display path uses, with an
existence check in front of it because `mode=ro`'s own refusal is an exception and this path
wants an ANSWER.
"""
obj = ctx.obj or {}
db_path = obj.get("db_path")
if db_path is None or not Path(db_path).exists():
# NOT "unreadable": there is no deployment, so "nothing is attested" is a true statement
# about this machine rather than an admission of ignorance.
return [], False
conn = None
try:
from keel.data.repository import Repository

conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
conn.row_factory = sqlite3.Row
return Repository(conn).list_venue_cash_postures(), False
except Exception:
# A file IS there and this process could not read it: a schema older than v18, a
# permissions problem, or a WAL database whose `-shm` sidecar is absent (every copied
# backup has that shape), which `mode=ro` cannot open because SQLite would have to CREATE
# it. Reported as unknown, never as "nothing is attested".
return [], True
finally:
if conn is not None:
conn.close()


def _readiness_rows(ctx: click.Context) -> list[VenueReadinessRow]:
"""Gathers this deployment's readiness rows for `keel brokers list` -- `gather_readiness`'s
CLI wiring, the same service `/api/venues` wires to a repo it knows is migrated.
Expand Down Expand Up @@ -394,5 +509,10 @@ def brokers_list(ctx: click.Context, as_json: bool) -> None:
click.echo(line)
for line in render_readiness_lines(_readiness_rows(ctx)):
click.echo(line)
posture_records, posture_unreadable = _cash_posture_records(ctx)
for line in render_cash_posture_lines(
posture_records, now_ts=int(time.time()), unreadable=posture_unreadable
):
click.echo(line)
click.echo("")
click.echo(DISCLAIMER)
Loading
Loading