Skip to content

Rail 17 reads Coinbase; skipped DCA buys retry until the next window and alert on macOS (#1010) - #1016

Merged
eaitbrahim merged 20 commits into
mainfrom
docs/rail-17-venue-read-and-dca-catchup
Nov 6, 2026
Merged

eaitbrahim merged 20 commits into
mainfrom
docs/rail-17-venue-read-and-dca-catchup

Conversation

@eaitbrahim

@eaitbrahim eaitbrahim commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1010.

Live DCA buys kept stopping without notice whenever the 7-day withdrawal attestation (rail 17) lapsed, and a vetoed DCA window was lost for good. Since August that cost every window of the five 14-day rules. This replaces the attestation with a check keel makes itself, retries skipped DCA buys, and tells the operator every time a live buy is skipped.

Changed

  • Rail 17 reads Coinbase. On every live BUY, keel reads allow_withdrawals from GET /v2/accounts for the wallet the order spends (USD/USDC) and the coin being bought. Both allowed → pass. Either disallowed → veto naming the wallet. A coin with no wallet yet is decided on the quote wallet. Venue unreadable, quote wallet missing, suspension store unreadable, or an adapter without the read → veto (fails closed). SELLs and paper are unaffected. The existing key is enough (can_transfer: false). The flag has only ever been observed true; it has not been seen flipping during a real freeze, which is why the manual brake stays.
  • keel withdrawals is a manual switch. attest --suspended halts live entries with no expiry; attest --enabled clears it and still needs a typed yes at a terminal. withdrawals show prints the manual state and the venue's answer per wallet.
  • DCA catch-up. A DCA buy that was due and did not fill, for any reason, is retried on each later daily cycle until it fills or the rule's next window opens. At most one fill per rule per window, decided by the orders ledger. A resting DCA order blocks retries. Only windows whose buying cycle ran after the rule went live are retried. Backtests and the account sim are unchanged.
  • keel dca-plan notes that catch-up can add up to one buy per rule to a calendar month; rail 14 still bounds it.

Added

  • macOS notification for every skipped live DCA buy, posted by keel itself ("keel-live: DCA buy skipped"): product, reason, and when it stops retrying. At most once per rule per UTC day, repeating daily while skipped. KEEL_DISABLE_MACOS_NOTIFY silences it. A DCA order stuck pending is reported daily by order id.
  • Local-only cancel for a stuck pending order with no venue id, behind a typed yes, so a blocked rule can be released. Automatic venue lookup is Settle a pending DCA order with no venue id by looking it up at Coinbase by client_order_id #1017.

Removed

  • The 7-day withdrawal-attestation TTL and everything built on it: expiry warnings in keel status, keel doctor, notifications, the web console banner, and the weekly runbook step. Rail 22 (cash posture) and rail 14 (subscription) attestations are unchanged.

Deploy note

The first live cycle after deploy retries every DCA rule whose current window is unfilled. On the windows that opened on the 2026-11-05 bar, that is all seven live rules (about $150), subject to rails 13 and 14. Checked before deploy: the live DB has no resting live BUY rows that would block a rule.

Design: docs/superpowers/specs/2026-11-06-rail-17-venue-read-and-dca-catchup-design.md · Plan: docs/superpowers/plans/2026-11-06-rail-17-venue-read-and-dca-catchup.md

Supersedes #1013.

🤖 Generated with Claude Code

eaitbrahim and others added 12 commits November 6, 2026 12:27
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…owed docstring (#1010)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…7-day TTL (#1010)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inery (#1010)

Rail 17 reads the venue per buy, so nothing about it expires any more.
- executor: delete _withdrawals_enabled and WITHDRAWAL_ATTESTATION_TTL_SEC.
- withdrawals CLI: attest --enabled/--suspended only set/clear the manual brake
  (no timestamp, no expiry wording; the typed-yes TTY gate is unchanged).
  show prints the tri-state suspension and the venue's per-wallet answer via a
  bounded _build_broker; any build/read failure prints "venue unreadable", exit 0.
- attestations: rail 17 leaves the survey; withdrawal_expiry/_withdrawal_dates deleted.
- doctor: attestation_findings(subscription, now_ts, withdrawals_suspended): FAIL on
  suspension, WARN on unreadable store, OK otherwise; never reads the old timestamp.
- status/pulse/web: WithdrawalAttestationStatus is just `suspended` (tri-state,
  unknown never rendered as off); console card shows the manual suspension line.
- notifications: no rail-17 attestation event.
- setup wizard step explains the venue read and the manual switch.
- docs/fiqh-basis.md mechanics sentence + its pin test updated (full docs rewrite is Task 6).

Deleted tests (expiry wording, not loosened):
test_attestation_expiry.py: test_rail_17_expiry_comes_from_the_executors_constant_and_is_not_restated,
 test_an_unattested_rail_reports_missing_rather_than_expired, test_rail_17_passes_through_ok_then_expiring_then_expired,
 test_seconds_left_never_goes_negative, test_doctor_reads_rail_17s_TTL_from_the_executor_rather_than_restating_it,
 test_doctor_starts_warning_on_rail_17_exactly_when_the_banner_does, test_a_suspended_attestation_is_not_a_healthy_one,
 test_reordering_the_definitions_does_not_cross_the_wires, test_the_warning_window_never_outlives_the_attestation_it_guards,
 test_forgetting_rail_17s_verdict_reports_too_loud_rather_than_too_quiet.
test_doctor.py: test_rail17_fresh_reports_days_remaining, test_rail17_absent_fails_and_names_the_attest_command,
 test_rail17_expired_fails_with_days_over.
test_status.py: test_rail17_attested_fresh_shows_time_remaining, test_rail17_expired_names_the_halt_and_the_fix,
 test_rail17_expired_in_paper_names_the_state_not_a_halt, test_rail17_never_attested_is_said_as_such,
 test_rail17_suspended_attestation_still_names_the_halt, test_rail17_freshness_uses_the_executors_own_ttl.
test_notifications.py: test_an_attestation_due_within_two_days_fires_and_a_fresh_one_does_not,
 test_an_expired_attestation_fires_too, test_both_attestation_rails_are_notified_when_both_are_unhealthy.
test_payload.py: test_an_expired_withdrawal_attestation_reads_as_bad.
test_fiqh_basis.py: test_rail_17s_seven_day_ttl_is_pinned_to_the_executor (replaced).
Rewritten onto rail 22 / new shape: test_attestation_alert_once.py, test_attestation_banner.py, test_pulse.py rail-17 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
)

Dca.detect gains a ledger-fed catch_up state (DcaCatchUp): a window whose
buy did not fill is retried on every later daily cycle until it fills or
the next window opens, at most one fill per rule per window. Backtests,
the sim and research never set it and keep window-open-day behaviour.

The agent reads each rule's own BUYs in the orders.mode the cycle writes
(live/paper); an unreadable ledger or a rule row with no timestamps
disables catch-up for that rule. The same-UTC-day DCA characterization
test is rewritten, as its header asks, now that a dedupe exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A vetoed live DCA buy is a zero-exit cycle, so neither the wrapper's alerts nor the
unconfigured webhook reached the operator. keel now posts a macOS notification itself,
once per rule per UTC day, for every due buy that did not go out: vetoed, feed stale,
entries withheld, or blocked by a resting order (named, with how to resolve it).
Live cycles only; the sink is inert off macOS and under pytest and never fails a cycle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t bar (#1010)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lerts (#1010)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@eaitbrahim eaitbrahim changed the title docs: rail 17 reads Coinbase, skipped DCA buys retry until the next window Rail 17 reads Coinbase; skipped DCA buys retry until the next window and alert on macOS (#1010) Nov 6, 2026
eaitbrahim and others added 8 commits November 6, 2026 15:10
#1010)

F1: `keel orders cancel N` marks a live entry left `pending` with no venue
order id canceled LOCALLY after a typed `yes` at a terminal; the prompt
says to check the Coinbase app first, the broker is never built, and the
row's raw_response records why under `local_cancel`. A row with a venue id
keeps the venue path unchanged. Registered in keel.capabilities.
F4: an in-flight skip names that command and says "Blocked until that
order is resolved" instead of a false "Retrying daily until".
F2: an in-flight DCA BUY counts as a DCA day for the same-day sleeve-sell
exclusion. F3: catch-up state is read after reconcile, so a row reconcile
settles this cycle is not counted in flight.
F11: the runbook names what clears each kind of skip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F5: rail 17's UNKNOWN veto promises a retry only to a DCA buy.
F6: `keel withdrawals show` names the exception type when the venue is
unreadable (still exit 0).
F7: the doctor source-scan test is now a behaviour test: a garbage or
expired `withdrawals_attested_at` leaves the rail-17 finding unchanged.
F8: Pulse's next DCA buy is derived from `Dca.due_window` on the clock
with the ledger's catch-up state, so a pending catch-up shows as the next
cycle's bar; the console column reads "next buy day (UTC)".
F9: `keel status --help` and the withdrawals capability row describe the
venue read and the manual suspension, not an expiring attestation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nce (#1010)

F10: the rail-17 page states that a venue whose adapter lacks
`withdrawals_allowed` (Alpaca, Kraken, Robinhood) has every live BUY
refused, and both manual-switch lines use the live config and database.
F12: the go-live runbook's garbled rail-17 sentence is rewritten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F1 registered `keel orders cancel` (local-only) as a capability; wall 1's
vocabulary must trip on it, so `cancel` joins FORBIDDEN_SUBSTRINGS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ad-and-dca-catchup

Brings in #1013 (6f816f2), a competing fix for #1010. This branch's design wins, so the merge
keeps one DCA retry mechanism, `Dca.catch_up`.

Removed from #1013: DcaWindowLedger, Dca.window_ledger and next_dca_bar_ts (dca.py);
_dca_window_ledger, _arm_dca_ledger and the agent.dca_window_missed log (agent.py);
DcaWindowAfterExpiry, dca_windows_after_expiry and live_dca_windows_after_expiry
(attestations.py); dca_cadence_findings / attest.dca_cadence (doctor.py); the status and
`withdrawals attest` expiry warnings; Repository.last_live_buy_created_at and signal_ts_since,
which had no caller left; the old rail-17-withdrawal-attestation.md section; the tests for all
of these (tests/strategy/test_dca_window_retry.py, tests/test_dca_windows_after_expiry.py and the
additions to the doctor, status, repository, guards, withdrawal-attestation and agent tests).
The rail-17 UNKNOWN message keeps this branch's wording.

Kept: the guards.py note that rail 17 does not exempt DCA (module docstring and rail comment),
the same sentence in docs/rails/rail-17-withdrawal-capability.md, and the historical plan
docs/superpowers/plans/2026-10-27-rail17-ttl-dca-cadence.md, unedited.

Ported to catch_up (tests/test_agent.py):
test_a_partially_filled_buy_blocks_its_windows_catch_up,
test_paper_era_activity_does_not_make_a_live_window_due,
test_a_rule_promoted_on_its_window_open_day_catches_that_window_up,
test_a_rule_promoted_on_its_windows_buying_day_catches_that_window_up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`keel orders cancel` on a partially_filled DCA BUY leaves the row `canceled` with its executed
`filled_quantity` still on it. `_dca_catch_up_state` counted only `filled`, so the next cycle
caught that window up and bought it a second time. `_buy_executed` (filled, or
filled_quantity > 0) now decides it for both the catch-up ledger and `_bought_today`. A
terminal row that executed nothing still leaves its window to catch up. The in-flight block
for resting statuses is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-test docstring (#1010)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@eaitbrahim
eaitbrahim merged commit bf55544 into main Nov 6, 2026
4 checks passed
@eaitbrahim
eaitbrahim deleted the docs/rail-17-venue-read-and-dca-catchup branch November 6, 2026 21:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rail 17's 7-day attestation TTL cannot cover a 14-day DCA cadence; vetoed windows are never retried

1 participant