Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
769a3e6
docs: design for rail 17 reading Coinbase and DCA catch-up (#1010)
eaitbrahim Nov 6, 2026
184584c
docs: implementation plan for rail 17 venue read and DCA catch-up (#1…
eaitbrahim Nov 6, 2026
2d2b21e
feat(coinbase): read per-wallet allow_withdrawals for rail 17 (#1010)
eaitbrahim Nov 6, 2026
7fd680f
fix(coinbase): duplicate wallets fail closed; correct withdrawals_all…
eaitbrahim Nov 6, 2026
3d63154
feat(rail-17): decide withdrawal capability from the venue, drop the …
eaitbrahim Nov 6, 2026
cb57e44
fix(rail-17): an unreadable suspension store fails closed (#1010)
eaitbrahim Nov 6, 2026
cbf7b20
style: wrap docstring line (#1010)
eaitbrahim Nov 6, 2026
682ccd5
feat(rail-17): withdrawals is a manual switch; remove the expiry mach…
eaitbrahim Nov 6, 2026
f7560c9
feat(dca): retry a skipped buy daily until the rule's next window (#1…
eaitbrahim Nov 6, 2026
6e6161e
feat(alerts): macOS notification for every skipped live DCA buy (#1010)
eaitbrahim Nov 6, 2026
082d1f2
fix(alerts): judge a skipped DCA buy by today, not a stale feed's las…
eaitbrahim Nov 6, 2026
924d4cf
docs(rail-17): venue-read withdrawal capability, DCA catch-up, skip a…
eaitbrahim Nov 6, 2026
e6bf1df
fix(dca): a stuck pending buy has a way out; blocked skips say blocke…
eaitbrahim Nov 6, 2026
f05356d
fix(rail-17): truthful messages, next-buy day honours catch-up (#1010)
eaitbrahim Nov 6, 2026
c865a60
docs(rail-17): name the venues rail 17 refuses; fix the go-live sente…
eaitbrahim Nov 6, 2026
286d315
test(mcp): the write vocabulary catches the local-only cancel (#1010)
eaitbrahim Nov 6, 2026
aae6713
Merge remote-tracking branch 'origin/main' into docs/rail-17-venue-re…
eaitbrahim Nov 6, 2026
d7ee2cd
Merge remote-tracking branch 'origin/main' into docs/rail-17-venue-re…
eaitbrahim Nov 6, 2026
b48f49b
fix(dca): a buy that executed any quantity fills its window (#1010)
eaitbrahim Nov 6, 2026
87a4cde
docs: mark #1013's plan superseded; truthful rail-17 slide and ported…
eaitbrahim Nov 6, 2026
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,9 @@ applies the confirm/autonomy gate, then places and logs. There is deliberately n
per-day spend caps, exposure and concentration caps, correlation-aware sizing, a
minimum-move floor, no-martingale/no-stop-widening, a fails-closed kill-switch, total and
weekly drawdown breakers, a consecutive-loss/edge-decay breaker, feed-staleness and
quote-balance checks, venue **subscription/withdrawal attestations** — rail 17 encodes
quote-balance checks, venue **subscription attestation**, and rail 17, which encodes
§65.4 *qabd*: an asset that cannot be withdrawn may not have been validly possessed, so
withdrawal capability is attested and enforced, not assumed. A rail veto names itself and
every live buy reads the venue's own withdrawal flag and fails closed if it cannot. A rail veto names itself and
the command that clears it. Beside the rails sits one routing-time check that needs the
venue's own book, which a broker-less rail cannot see: the **max-spread entry gate** (#350)
refuses a live BUY whose previewed `(best_ask − best_bid) / mid` is at or beyond
Expand Down
8 changes: 5 additions & 3 deletions docs/fiqh-basis.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,11 @@ test: possession is the ability to dispose, not physical custody (§65.4 Ayub ·
Abuzaid & Oweida's *Handbook of Islamic Finance* (2022) quoting the OIC Fiqh Academy
resolution (Res. 53/4-6) that holds electronic constructive possession sufficient.

Mechanics: the operator attests with `keel withdrawals attest`; the attestation is live-read
on every intent and expires after 7 days (`WITHDRAWAL_ATTESTATION_TTL_SEC`,
`keel/execution/executor.py`) — "a stale attestation is no better than none". ENTRIES ONLY,
Mechanics: on every BUY the executor asks the venue whether the wallets the order touches
allow withdrawals (`_withdrawal_capability`, `keel/execution/executor.py`; Coinbase's
`allow_withdrawals`), so a freeze takes effect on the next order and its lifting on the one
after. There is no attestation and no expiry. The operator keeps one manual brake,
`keel withdrawals attest --suspended`, which halts entries whatever the venue says. ENTRIES ONLY,
like rails 11/16: existing holdings are already ours, and forcing a sale to "fix" a
withdrawal freeze would be strictly worse than holding through it. Unknown fails closed.

Expand Down
10 changes: 10 additions & 0 deletions docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ breakers, the allowlist, settlement-currency, spot-shape and trade-scope checks.

Source: keel's own vocabulary -- docs/fiqh-basis.md's rails table (the prudential rails 2-14, 16, 20) plus its prose sections for rails 1, 17, 18 and 19

## withdrawal capability

Whether the account's wallets allow withdrawals right now, the test rail 17 applies on every live
BUY. keel reads it from the venue (Coinbase `allow_withdrawals` for the order's quote wallet and
the bought coin's wallet) rather than from an operator attestation; unreadable means veto. The
flag has only been observed `true`. `keel withdrawals attest --suspended` is a manual brake with
no expiry.

Source: docs/rails/rail-17-withdrawal-capability.md

## attestation

market facts are computed, Shariah classifications are **ATTESTED, never inferred**.
Expand Down
10 changes: 5 additions & 5 deletions docs/go-live-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ unattested venue or asset is treated as unknown, not as fine.
```bash
keel assets list # what is attested, and what is not
keel subscription show # rail 14's spend allowance comes from this record
keel withdrawals show # rail 17: withdrawal capability is a compliance precondition
keel withdrawals show # rail 17: manual suspension + the venue's per-wallet answer
keel scope show # rail 20: can this venue's credential place a live ENTRY?
```

Attest anything missing (`keel assets attest`, `keel subscription attest`,
`keel withdrawals attest`, `keel scope attest --trading`) before continuing. If a rail vetoes
later, its message names the command that fixes it.
Attest anything missing with `keel assets attest`, `keel subscription attest` and
`keel scope attest --trading`. Rail 17 needs no attestation: it reads the venue's withdrawal flag
on every live BUY. If a rail vetoes later, its message names the command that fixes it.

Rail 20 exists because a credential that *reads* fine is not evidence it can *trade*: a
well-formed `ROBINHOOD_API_KEY` passed every read this deployment ever made, and the first live
Expand Down Expand Up @@ -235,7 +235,7 @@ unreadable state halts trading rather than permitting it.
| `keel resume-entries` | a consecutive-loss halt (rail 16) |
| `keel record-flow --amount ±N` | rebases rail 11's high-water mark |
| `keel reset-hwm` | rail 11's high-water mark, clearing a stuck drawdown halt |
| `keel withdrawals attest --enabled` | rail 17's entry halt |
| `keel withdrawals attest --enabled` | rail 17's manual entry halt (set by `--suspended`; no expiry) |

The de-risking direction of each is always allowed and needs no terminal: `keel kill`,
`keel withdrawals attest --suspended`, `keel autonomy off`.
Expand Down
86 changes: 43 additions & 43 deletions docs/operator-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ Design home: `docs/superpowers/specs/2026-07-16-keel-broker-abstraction-design.m
## Pre-live checklist

Run through this **before arming the agent for live trading**, and re-check after any change to the
Coinbase account. Item 3 is not a one-time check: it recurs **weekly** for as long as the agent
runs.
Coinbase account. Item 3 is not a checklist step: it says what to do when a DCA buy is skipped.

### 1. ⛔ Disable interest / rewards on idle balances — **required**

Expand Down Expand Up @@ -70,39 +69,38 @@ A zakat estimate (~2.5% of holdings' market value per lunar year) is a **positiv
item 1's prohibition, and it is informational: keel reports, you decide and discharge it. Tracked at
KB §33.1. No pre-live action; noted here so the account-level obligation set is complete in one place.

### 3. Withdrawal-capability attestation — **weekly refresh** (rail 17)

**Why.** Rail 17 (§65.4 *qabd*) halts all BUY entries unless the withdrawal-capability attestation is
fresh, and "fresh" means a **7-day TTL** (`WITHDRAWAL_ATTESTATION_TTL_SEC`,
`keel/execution/executor.py`). An expired attestation reads as UNKNOWN, and rail 17 fails closed on
unknown — live DCA buys are vetoed. That is not hypothetical: on **2026-08-14** it vetoed the only
live DCA signal because the attestation had lapsed, and as of 2026-08-17 every deployment's
attestation was weeks stale, so rail 17 was halting entries on the **live** deployment (it is a
`LIVE_STATE` rail, skipped in paper, where a stale attestation matters only to the status display).
Each deployment carries its own attestation (they do not share a database), so the live one must be
refreshed for the rail and the paper ones to quiet their status lines.

Rail 17 fails closed by itself — what it cannot do is refresh its own input, and that input is
deliberately human (see the warning below). This entry is the cadence obligation the rail cannot
enforce, which is why it lives here rather than in `guards.py`.

**How to verify.** `keel status` prints the rail-17 line with days-to-expiry (`attested, expires in
3d`, `EXPIRED 12d ago`, or `never attested`) — staleness is visible there *before* it vetoes, not
only in the veto log. `keel withdrawals show` reads the same state with the age to one decimal.

**Cadence.** Re-attest **weekly** — a calendar reminder is the intended mechanism. Confirm the
balances really are withdrawable on demand, then, per deployment:

```bash
keel withdrawals attest --enabled
keel --config config.live-sandbox.yaml --db keel-live.db withdrawals attest --enabled
```

> ⚠️ **The typed confirmation is deliberately human.** `--enabled` RELEASES a rail-17 entry halt and
> demands a typed `yes` at a terminal — so that a scheduled job can never release a §65.4 halt, the
> same posture as `keel autonomy on`. Do not script this command and do not pipe a `yes` into it:
> the weekly habit is the fix for staleness, and automating the release would undo the rail. If a
> calendar reminder ever feels like it should be a cron job, re-read this warning.
### 3. A macOS notification says a DCA buy was skipped

keel itself posts a notification titled "keel-live: DCA buy skipped" when a live DCA buy was due
and did not place, at most once per rule per UTC day, repeating each day while it stays skipped.
Set `KEEL_DISABLE_MACOS_NOTIFY` to silence it. A DCA order stuck `pending` is reported daily too,
naming the order id.

**What to do.** Read the reason in the notification; what clears it depends on the reason.

- **A stale feed or a cash shortfall** clears on its own once the feed catches up or the cash
arrives. keel retries the buy on each later daily cycle until it fills or the rule's next window
opens ("Retrying daily until ..."), at most one fill per rule per window.
- **A rail veto** names the rail and what clears it, verbatim; follow that. Some rails clear on
their own (an unreadable venue on rail 17), others need you (an attestation, a halt release).
- **A stuck order** ("previous DCA order #N is still pending ... Blocked until that order is
resolved") does NOT retry: a resting (`pending`/`partially_filled`) DCA order blocks the rule
until it is resolved. Reconcile settles one the venue gave an id on its own cycle. One the venue
never gave an id (the placement raised, e.g. a timeout) is settled by nothing, so run
`keel orders cancel N` at a terminal: first check in the Coinbase app that no such order exists
and nothing filled, then type `yes`. keel marks the row canceled locally without contacting the
venue, and the rule catches up on the next cycle.
- **A manual suspension** (rail 17's `--suspended` switch) stays until you clear it with
`keel withdrawals attest --enabled`.

Catch-up only covers windows whose buying cycle ran after the rule went live. `keel dca-plan` states that
catch-up can add up to one buy per rule to a calendar month; rail 14 still bounds the total.

Rail 17 (withdrawal capability) needs no weekly chore: it reads the venue's `allow_withdrawals`
flag for the order's wallets on every live BUY (`docs/rails/rail-17-withdrawal-capability.md`).
`keel withdrawals show` prints the manual suspension and the venue's per-wallet answer;
`keel withdrawals attest --suspended` is a manual brake with no expiry, and `--enabled` clears it
(typed `yes` at a terminal).

---

Expand Down Expand Up @@ -723,11 +721,12 @@ candidates carries the purification obligation in their own records.
an ACATS transfer to another broker. It is attested like any venue:

```bash
keel --config config.paper-equities.yaml --db keel-equities.db withdrawals attest --enabled
keel --config config.paper-equities.yaml --db keel-equities.db withdrawals show
```

Rail 17 is a live-state rail (skipped in paper), so this is recorded for the day live is ever
considered, and it lapses weekly like every deployment's attestation.
Rail 17 is a live-state rail (skipped in paper). It reads the venue's own withdrawal flag, not an
attestation, so there is nothing to record or renew; the venue read for an equities broker is
whatever that adapter supports, and a broker without it fails closed on a live BUY.

### T+1 settlement × daily cadence

Expand Down Expand Up @@ -1035,8 +1034,9 @@ covers the rest from a plain shell.
**What the browser cannot do, and that is deliberate.** Every capability-increasing action --
arming autonomy, releasing the kill-switch, clearing a consecutive-loss halt, re-seeding the
drawdown high-water mark, declaring a deposit or withdrawal, attesting withdrawal capability, scope
or posture, declaring an out-of-band close, and replacing the binary -- is a CLI command behind a
typed confirmation at an interactive terminal. `keel capabilities` lists all ten with the gate
or posture, declaring an out-of-band close, cancelling a stuck order the venue never gave an id,
and replacing the binary -- is a CLI command behind a typed confirmation at an interactive
terminal. `keel capabilities` lists all eleven with the gate
covering each. The one exception is the console's
HALT button, which engages the kill-switch (stopping is never gated). The server's gated routes
under `/api/gates/` can also release the kill-switch and clear the consecutive-loss halt, but only
Expand Down Expand Up @@ -1110,7 +1110,7 @@ no handler attached at all: an offline install makes zero network calls. This ch
unchanged by the notification layer below and stays independent of it.

**Opt-in event notifications (default OFF).** The events an operator most needs are silent
precisely because they are not errors — rail 17's attestation nearing expiry fails CLOSED and
precisely because they are not errors — a lapsed attestation fails CLOSED and
quietly vetoed a real setup for weeks before anyone opened the TUI, the console of the day (#541). The notification layer
(#444) delivers those over the SAME generic webhook, per-event opt-in in Freqtrade's
`notification_settings` shape:
Expand All @@ -1121,7 +1121,7 @@ quietly vetoed a real setup for weeks before anyone opened the TUI, the console
notifications:
format: plain # plain = generic JSON (default); slack = {"text": ...} chat payload
events:
attestation.expiring: true
attestation.expiring: true # rail 22 (cash posture) attestation only
rail.armed: true
setup.unplaced: true
allowance.nearing_exhaustion: true
Expand All @@ -1135,7 +1135,7 @@ doctor's own findings, so the alert and the diagnostic can never disagree):

| Event key | Fires when | Why it is not a CRITICAL log |
|---|---|---|
| `attestation.expiring` | rail-17 withdrawal attestation has ≤2 of its 7 TTL days left (or has expired, or was never attested) | an expired attestation silently vetoes every entry — cycles keep running, nothing errors |
| `attestation.expiring` | the rail-22 cash-posture attestation is near expiry, expired, or never made (rail 17 no longer has an attestation or an expiry) | an expired attestation silently vetoes every entry — cycles keep running, nothing errors |
| `rail.armed` | rail 16's consecutive-loss halt arms, or rail 11's drawdown reaches 20% | a halt is a correct state, not a fault; the kill switch is deliberately absent (you engaged it at a terminal — you know) |
| `setup.unplaced` | a cycle detected an entry setup and could not place it | the veto is a WARNING; the rail-17 incident looked like a quiet week |
| `allowance.nearing_exhaustion` | month-to-date BUY spend reaches 80% of the in-force rail-14 allowance — or there is spend against an allowance of 0 (no subscription in force: lapsed or never attested) | rail 14 only speaks when it vetoes, which is too late to re-tier |
Expand Down
7 changes: 4 additions & 3 deletions docs/presentations/se.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,9 +316,10 @@ Four design decisions worth discussing:

**Rail 17 is the one to dwell on.** Constructive possession — "the ability to dispose, not
physical custody" — is tested by asking whether the asset can be withdrawn from the venue. If it
cannot, you do not effectively possess it, so acquiring more is stopped. The attestation expires
after 7 days (`WITHDRAWAL_ATTESTATION_TTL_SEC`) because "a stale attestation is no better than
none". A classical contract rule, expressed as a TTL.
cannot, you do not effectively possess it, so acquiring more is stopped. Before every live buy
keel asks the venue itself (Coinbase's per-wallet `allow_withdrawals`) and refuses the buy when
the answer is no or cannot be read. A classical contract rule, expressed as a check the venue
answers on every order.

### 6.6 `keel/execution/executor.py` — placing it

Expand Down
Loading
Loading