Skip to content

feat(dca): keel dca plan -- propose a DCA schedule within the venue plan's cap - #846

Merged
eaitbrahim merged 10 commits into
mainfrom
feat/dca-plan-service
Sep 28, 2026
Merged

eaitbrahim merged 10 commits into
mainfrom
feat/dca-plan-service

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

PR 1 of docs/superpowers/plans/2026-09-27-dca-plan.md (Tasks 1–6): the plan service and the keel dca plan CLI. The read-only web card (Tasks 7–8) comes in PR 2.

Behaviour

  • keel dca plan --budget <monthly USD> --buffer-pct <fraction> [--cadence-days 7] proposes a weekly DCA schedule.
    • The universe is allowlist ∩ admitted by screen_product ∩ positive target_weights, minus assets that already have a non-disabled dca rule. Weights are renormalised over that set.
    • Every excluded asset is listed with each reason that applies: not_on_allowlist, not_admitted, no_weight, or has_dca_rule. An asset with a DCA rule is shown as "existing, unchanged".
  • Spend is budget × (1 − buffer). It is checked against rail 14's monthly buy cap for the deployment's venue, read exactly as guards.check reads it. A parity test drives guards.check at the cap and one cent over it.
  • Rail 14 is stated as a monthly BUY cap, never a fee waiver (Rail 14: the "fee-free allowance" is not fee-free on Advanced Trade -- relabel it a monthly buy cap, and re-scope what rests on the premise #836). "fee-free" appears nowhere.
  • Fees are estimated at the configured fees.taker_pct and labelled as such. The venue minimum order size is unknown to keel, and the output says so.
  • At a TTY the prompt is [Y] Approve / [E] Edit weights / [N] Cancel.
    • [Y] writes one candidate dca rule per asset through rules.add_rule_row, all or nothing (R2).
    • The command never promotes, never writes paper or live, and never touches an existing rule.
    • A blocked plan does not offer [Y].
  • Off a TTY it prints the plan, writes nothing, and exits 0 when the plan is approvable or 1 when it is blocked.
  • keel/commands/dca_plan.py is a pure service with no click, ready for PR 2's GET-only reader. keel/commands/dca.py is the thin click front-end.

Rulings in force

R1–R19 as in the plan, with these changes:

  • R7 amended (after fix(executor): size each DCA buy from the rule's own amount (#840) #843). Each existing live DCA rule's monthly commitment uses that rule's own budget_usd, because the executor sizes each DCA buy from the rule's size_usd. The commitment is added to spend, and the plan warns, without blocking, when the sum exceeds the cap. A live rule with dip_bonus_pct > 0 gets its own warning, since its buys can exceed the listed amount.
  • R9 withdrawn. There is no "executor sizes DCA from config.dca.budget_usd" warning, and no executor_budget_usd field. A test pins the warning's absence.
  • fix(rails): exempt DCA buys from the total-exposure rail; rail 14 bounds DCA (#841) #842 note. Nothing in the output describes max_exposure_usd as a DCA limit, and a test pins that.
  • R20 (new). Interactivity is decided once, first. Off a TTY the database is opened with _open_repo_ro (mode=ro), so the preview cannot create a missing DB file or migrate an old schema.
  • Write-loop sqlite3.Error (new). A locked database mid-write is caught and refused, and the refusal names the rule ids already written. This is R2's stated cost-if-wrong, now made explicit.

Test evidence

  • New files: tests/commands/test_dca_plan.py and tests/commands/test_dca_cli.py. tests/test_rail14_is_a_buy_cap.py gets one test, and SERVICE_MODULES gets two modules.
  • Each task was TDD red then green. Mutations were proven applied, then shown to be killed:
    • a write inside select_universe;
    • config budget used instead of each rule's own in R7;
    • rail 14 note removed from the render;
    • pre-validation loop removed from apply_dca_plan;
    • rail 14 no-record cap changed.
  • Fixtures are proven to reach the code under test: the fake screen_fn is called 3 times, the patched build_rule_from_params is called for PAXG, the patched add_rule_row is called twice, and _open_repo_ro is called once off a TTY.
  • uv run pytest -q: 6962 passed, 3 skipped, exit 0.
  • uv run ruff check keel tests exited 0, uv run ruff format --check keel tests exited 0, and bare uv run mypy exited 0.
  • Manual sanity run: the template config against a fresh migrated temp DB, off a TTY. It exits 1 with named blockers: every asset is unadmitted, and rail 14 is at $0 with no attestation, together with the attest command. The DB checksum is unchanged.

guards.py and executor.py are untouched.

🤖 Generated with Claude Code

eaitbrahim and others added 8 commits September 27, 2026 21:37
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…xemption

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l reads it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… existing DCA rule

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kers and warnings

R7 as amended after #843: live DCA commitment is each rule's own budget_usd;
R9's executor-sizing warning is withdrawn and its absence pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ever fee-free

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…add, all or nothing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te-only on approval

Off a terminal the database is opened read-only, so the preview cannot write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread keel/commands/dca_plan.py Outdated
Comment thread keel/commands/dca_plan.py Outdated
Comment thread keel/commands/dca.py
…-colliding weights (#847, #848)

- R6 amended: the blocker compares the worst UTC calendar month for the
  cadence (max cadence days in any month x the per-cycle total) against
  rail 14's cap; per-buy sizing is unchanged. The output says the worst
  month is what was checked.
- target_weights (and [E] edits) whose keys collide once uppercased are
  refused, naming both keys, instead of silently dropping one.
- A live DCA row with no stored budget_usd is counted at Dca's own
  default (read from its signature) and named in a warning, not as $0.
- An absurd --budget is a usage error, not a decimal traceback.
- A DcaPlanError from the plan build reaches the CLI as a clean error;
  [E] re-prompting on a bad weight is covered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread keel/commands/dca_plan.py Outdated
…weights (#849)

- allowlist entries are de-duplicated case-insensitively, first-seen
  order, so [BTC, ETH, btc] gives one BTC allocation, buy and rule
  rather than a double share and two candidates.
- A NaN/inf target_weights value is a DcaPlanError naming the key,
  not an InvalidOperation traceback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@eaitbrahim
eaitbrahim merged commit b7e1158 into main Sep 28, 2026
4 checks passed
@eaitbrahim
eaitbrahim deleted the feat/dca-plan-service branch September 28, 2026 03:16
eaitbrahim added a commit that referenced this pull request Sep 28, 2026
…n and its read-only card (#877)

MINOR: a rail changes scope for DCA (#869) and new commands and routes land
(#846, #850). No schema change since 0.19.0.

What lands:
  #846 (#831 follow-up) -- keel dca plan: schedule by target weights within
  rail 14's worst calendar month; [Y] writes candidate rules only.
  #850 -- GET /api/dca-plan and a read-only card on /rules.
  #869 (#853) -- rail 6 (per-asset) no longer vetoes DCA buys; the plan
  blocks on rail 3 and warns on rail 5.
  #867 (#854), #876 (#874) -- a blocked plan names the largest passing
  budget and smallest passing buffer, clearing rails 14, 3 and 2.
  #870 (#856) -- admission screen cached 5 minutes.
  #858 (#855) -- deploy/live-rules.json synced.
  #859 (#857) -- design spec for the DCA sleeve's sell side.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant