Skip to content

Rail 17: warn when the attestation expires before a DCA window, and retry a vetoed window (#1010) - #1013

Open
eaitbrahim wants to merge 21 commits into
mainfrom
fix/1010-rail17-ttl-vs-dca-cadence
Open

eaitbrahim wants to merge 21 commits into
mainfrom
fix/1010-rail17-ttl-vs-dca-cadence

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

Closes #1010

Problem

Rail 17's withdrawal attestation lasts 7 days. Some live DCA rules buy every 14 days. On 2026-10-23 the attestation (made 2026-10-10 19:25Z) had been expired for six days, and rail 17 vetoed all seven DCA signals for the 2026-10-22 bar. Nothing warned beforehand. A vetoed window was also lost until the next cadence multiple, which for a 14-day rule means a month between buys. Finally, the veto text said "exits and DCA-exempt paths are unaffected", which reads as if DCA is exempt. It is not.

Approach

  • D1, the warning. One pure helper, attestations.dca_windows_after_expiry, holds all the arithmetic: the due day is the cadence bar plus one day, and a rule is warned when the expiry comes before due + 1 day. A never-raising wrapper, live_dca_windows_after_expiry, applies it to the live DCA rules. It stays silent unless the attestation is fresh (OK or EXPIRING) and says withdrawals are enabled (R15). Three surfaces call the wrapper and never restate the arithmetic:
    • keel withdrawals attest --enabled prints a WARNING: line per rule.
    • keel status prints the same line under the rail-17 line. It does this in live modes only, and the line is also in --json.
    • keel doctor always emits attest.dca_cadence, as OK or WARN and never FAIL (R11).
  • D2, the retry. Dca.detect stays pure and reads an optional per-cycle DcaWindowLedger. Only live cycles set it (R10), from two new Repository reads.
    • A window counts as satisfied when a filled or pending live BUY's bar day (created_at//86400 - 1) is at or after the window start (R4, R5).
    • A retry fires only if the rule persisted a signal, while live, for an earlier bar of the same window (R6, R13 revised: promotion time floored to midnight UTC; no promoted_at means no retry).
    • A window that ends unbought is reported on the next cadence day as context["previous_window_missed_ts"] plus a WARNING agent.dca_window_missed (R7). It is never back-filled.
    • If building the ledger fails, the rule falls back to cadence-only and a WARNING agent.dca_window_ledger_failed is logged (R14).
    • With no ledger (paper, sim, backtest), behaviour is unchanged.
  • D3, the text. The rail-17 UNKNOWN veto now says "(exits are unaffected; DCA buys are NOT exempt)". The guards module docstring now reads "like rails 11/16/17 (11 and 16 exempt DCA; 17 does not)" (R16).
  • WITHDRAWAL_ATTESTATION_TTL_SEC is unchanged. No config, plist or DB file is touched. There is no CHANGELOG entry (R12); docs/rails/rail-17-withdrawal-attestation.md gains a section on this instead.

Considered and rejected

  • Lengthening the TTL. The issue rejects it.
  • Counting a window as satisfied when created_at falls inside it. A live order is placed the day after its bar. With that rule, a buy for a window's last bar would land in the next window and suppress that window's buy, and a cadence-1 rule would never buy again. Bar-day attribution (R4) avoids both.
  • Surfacing the warning through attestations.survey(), so the web banner shows it. That would change the notification alert-once windows and the web banners. survey() is unchanged (R9); the web console can pick this up in a follow-up.

Verification

Commands run on the final tree, c55a39d:

$ uv sync --all-extras --dev
Resolved 56 packages in 18ms
(exit 0)
$ uv run ruff check keel tests packages
All checks passed!
(exit 0)
$ uv run ruff format --check .
554 files already formatted
(exit 0)
$ uv run mypy
Success: no issues found in 513 source files
(exit 0)
$ uv run pytest -q
8541 passed, 3 skipped in 924.65s (0:15:24)
(exit 0)
$ git diff --stat origin/main...HEAD -- '*.yaml' '*.plist' '*.db' keel/execution/executor.py
(empty)

How each criterion is met (D4):

  • D1:
    • tests/test_dca_windows_after_expiry.py reproduces the 2026-10-10/10-17/10-23 lapse with the exact warning line, and covers the R15 silent cases.
    • tests/execution/test_withdrawal_attestation.py::test_attest_enabled_names_each_live_dca_rule_whose_next_buy_falls_after_expiry
    • tests/commands/test_status.py::test_status_lists_each_live_dca_rule_due_after_the_attestation_expires (and the JSON and paper tests)
    • tests/commands/test_doctor.py::test_gather_findings_wires_the_dca_cadence_check
  • D2:
    • tests/strategy/test_dca_window_retry.py (8 tests)
    • tests/test_agent.py #1010 section: retry on the next fresh bar, no double buy on a same-day rerun or a later bar, missed window reported but not back-filled, rule promoted mid-window, paper-era signal, ledger-read failure, and paper cycles staying cadence-only.
  • D3: tests/execution/test_guards.py::test_rail17_unknown_text_does_not_imply_dca_is_exempt
  • D4: every task's test was run red before its implementation. The coordinator independently confirmed red for Task 4 by reverting agent.py (5 failed, 2 regression guards passed) and for Task 5 by reverting withdrawals.py. Coverage gaps were closed with mutation-checked tests in c55a39d: the paper-cycle guard, the non-DCA kind filter, and skipping an unbuildable row.

Expected red check: the security / dependencies (pip-audit) check is red on main for certifi PYSEC-2026-4320. PR #1012 fixes it and is still open, so expect that one check red here as well. It is unrelated to this diff.

Where to look first

  • keel/strategy/rules/dca.py Dca.detect (the ledger branch): the window start, the satisfied check (counted >= window_start), the retry condition (a signal at a bar day in [ws, d)), and the previous-window-missed condition.
  • keel/agent.py _dca_window_ledger: the since lower bound (max(now_day - 2c - 1, floor(promoted_at))). promoted_at is read from the live rules only.
  • tests/test_agent.py::test_two_cycles_in_one_utc_day_place_one_dca_order: this is a characterization test that pinned the same-day DCA double buy. Its own text said to rewrite it, not delete it, once a dedupe existed. D2's one buy per window is that dedupe, so it now asserts one order.
  • keel/attestations.py live_dca_windows_after_expiry: the freshness gate uses the module's own Attestation model rather than restating it.
  • Behaviour note, documented in the rail-17 doc: a retry day can coincide with another rule's cadence day. A single day can then carry more DCA spend than keel dca plan's busiest-day figure. Rails 3 and 14 still bound it.

Commits

  1. feat(Rail 17's 7-day attestation TTL cannot cover a 14-day DCA cadence; vetoed windows are never retried #1010): compute which live DCA windows fall after rail 17's expiry
  2. feat(Rail 17's 7-day attestation TTL cannot cover a 14-day DCA cadence; vetoed windows are never retried #1010): repository reads for a DCA rule's counted buys and recent signals
  3. feat(Rail 17's 7-day attestation TTL cannot cover a 14-day DCA cadence; vetoed windows are never retried #1010): Dca retries a vetoed window from a caller-supplied ledger, one buy per window
  4. feat(Rail 17's 7-day attestation TTL cannot cover a 14-day DCA cadence; vetoed windows are never retried #1010): live cycles retry a vetoed DCA window and report a missed one
  5. feat(Rail 17's 7-day attestation TTL cannot cover a 14-day DCA cadence; vetoed windows are never retried #1010): withdrawals attest warns when a live DCA buy falls after expiry (merged from a parallel branch)
  6. feat(Rail 17's 7-day attestation TTL cannot cover a 14-day DCA cadence; vetoed windows are never retried #1010): keel status warns when a live DCA buy falls after rail 17's expiry (merged)
  7. feat(Rail 17's 7-day attestation TTL cannot cover a 14-day DCA cadence; vetoed windows are never retried #1010): doctor reports live DCA buys due after rail 17's expiry (merged)
  8. fix(Rail 17's 7-day attestation TTL cannot cover a 14-day DCA cadence; vetoed windows are never retried #1010): rail 17's veto text no longer implies DCA is exempt (merged)
  9. docs(Rail 17's 7-day attestation TTL cannot cover a 14-day DCA cadence; vetoed windows are never retried #1010): rail 17 TTL vs DCA cadence, the retry and the warnings
  10. test(Rail 17's 7-day attestation TTL cannot cover a 14-day DCA cadence; vetoed windows are never retried #1010): pin paper cycles staying cadence-only and the live wrapper's row filters

The plan is in docs/superpowers/plans/2026-10-27-rail17-ttl-dca-cadence.md (two earlier commits).

🤖 Generated with Claude Code

eaitbrahim and others added 16 commits October 27, 2026 07:20
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t signals

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r, one buy per window

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… expiry

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…7's expiry

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d one

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…er's row filters

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread keel/strategy/rules/dca.py Resolved
Comment thread keel/agent.py Resolved
eaitbrahim and others added 2 commits October 27, 2026 09:31
On a placement day the 01:20 cycle has acted on yesterday's bar, so the rail-17 cadence
warning now looks at the following bar (live wrapper passes the newest counted live BUY's
bar day; with no information the helper checks both bars).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread keel/attestations.py Resolved
Comment thread keel/attestations.py Resolved
…rapper bar day

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
eaitbrahim and others added 2 commits October 27, 2026 10:55
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… bar

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@eaitbrahim

Copy link
Copy Markdown
Contributor Author

Phase 6 hardening (#1015): fe73c3a counts partially_filled as the window's buy, 52dd9e6 fixes the promotion-floor docstring.

This branch has not been deployed

No deployments
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