fix(rails): exempt DCA buys from the total-exposure rail; rail 14 bounds DCA (#841) - #842
Merged
Merged
Conversation
…nds DCA (#841) The operator's decision (2026-09-27): DCA is bounded by the venue plan's cap (rail 14, the attested monthly buy cap), not by caps.max_exposure_usd. With 7 DCA rules and about $213 held against a $400 cap, rail 4 would veto the sleeve after about one buy on Oct 9. - guards.check: rail 4 (total_exposure_cap) now gates on `is_buy and not intent.is_dca`. Rail 6 (per-asset concentration) and rail 14 still bind DCA; every other rail is unchanged. DCA holdings still count toward the total a rule-trading entry sees. - SimAccount.can_open: the same exemption, so `keel simulate` models live. max_affordable_notional is untouched: only the rule path calls it. - docs/rails: new rail-4 page with the DCA exemption table; rail-14 page says it is now the limit that bounds DCA. Config templates' caps comment says the same (repo config.yaml re-synced with its template). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eaitbrahim
force-pushed
the
fix/dca-exempt-from-total-exposure
branch
from
September 28, 2026 00:17
dd52c08 to
c3522d0
Compare
eaitbrahim
commented
Sep 28, 2026
The operator runbook still called caps.max_exposure_usd "the ceiling on total notional held at any one moment (rail 4)" -- true before #841, no longer true after: DCA buys are exempt from rail 4 and bounded instead by rail 14's monthly buy cap and rail 6's per-asset cap. Correct the bullet to describe what rail 4 now binds, note that DCA holdings still count toward the total (so a growing DCA sleeve can consume rule-trading room), and link to docs/rails/rail-4-total-exposure.md for the detail. Addresses the unresolved review comment on PR #842 (docs/rails/rail-4-total-exposure.md:35). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eaitbrahim
added a commit
that referenced
this pull request
Sep 28, 2026
…an's cap (#845) MINOR: live and paper DCA now spend each rule's own size_usd, and DCA buys are exempt from the total-exposure rail. No schema change since 0.18.0. What lands: #843 (#840) -- executor sizes each DCA buy from the rule's size_usd; absent falls back to dca.budget_usd, present-but-invalid skips the buy. #842 (#841) -- rail 4 (total exposure) no longer vetoes DCA buys; rail 14 (monthly buy cap) and rail 6 (per-asset) still bind them. #837 (#836) -- rail 14 relabelled a monthly buy cap, not fee-free. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
eaitbrahim
added a commit
that referenced
this pull request
Sep 28, 2026
…lan's cap (#846) * docs(plan): keel dca plan -- service, CLI and read-only web card Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(plan): amend R7 and withdraw R9 after #843; note #842's rail 4 exemption Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(dca): plan inputs and rail 14's monthly buy cap, read as the rail reads it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(dca): the plan's universe -- admitted, weighted, allowlisted, no existing DCA rule Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(dca): per-buy amounts, fees at the configured rate, rail 14 blockers 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> * feat(dca): render the plan; rail 14 is stated as a monthly buy cap, never fee-free Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(dca): approve writes one candidate dca rule per asset via rules add, all or nothing Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(dca): keel dca plan -- a thin CLI over the plan service, candidate-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> * fix(dca): check the worst calendar month against rail 14; refuse case-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> * fix(dca): a repeated allowlist entry is one asset; refuse non-finite 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> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #841
The decision (the operator's, 2026-09-27, final)
The reason: the live account accumulates through 7 DCA rules. About $213 is held against the $400
max_exposure_usdcap, so rail 4 would veto the sleeve after about one buy on Oct 9.Which rails bind DCA now
total_exposure_capper_asset_concentration_capmonthly_subscription_allowancemax_per_asset_pct × max_exposure_usd.max_exposure_usdstill bounds DCA per asset, but no longer in total.What changed
keel/execution/guards.py: rail 4 now gates onis_buy and not intent.is_dca. I updated the module docstring's "DCA exemptions" paragraph, rail 4's comment and rail 14's comment. The docstring now also names rail 16, which was already exempt but was missing from that paragraph.keel/sim/account.py:SimAccount.can_openapplies the same exemption, sokeel simulatemodels what live does. The sim's DCA path (portfolio_sim._process_dca_signals) goes throughcan_open, which vetoes and does not clamp. Before this PR it vetoed DCA overmax_exposure_usd; now it does not.max_affordable_notionalis unchanged because only the rule-slot path calls it.docs/rails/rail-4-total-exposure.mdis new and holds the exemption table.docs/rails/rail-14-subscription-allowance.mdnow says rail 14 is what bounds DCA.caps:comment inkeel/templates/config.yamlandconfig.live.yamlsays the same. The repo-rootconfig.yamlis re-synced with its template, astest_the_template_stays_in_sync_with_the_repo_configrequires.keel doctorhas no text that lists rails or their DCA exemptions, so nothing changed there.executor.pyis not touched.This reaches live only after a release and deploy. Until then, the deployed rail 4 still vetoes DCA over
max_exposure_usd.Tests (written first; each was seen to fail for the stated reason, or is a pin)
test_rail4_dca_buy_over_max_exposure_is_not_vetoed_by_total_exposuretotal_exposure_cap: open exposure 960.0 + 45 = 1005.0 exceeds max_exposure_usd 1000test_rail4_the_same_buy_without_is_dca_is_still_vetoed_by_total_exposuretest_rail6_dca_buy_over_the_per_asset_cap_is_still_vetoed_by_concentrationtotal_exposure_capbesideper_asset_concentration_captest_rail14_dca_buy_over_the_monthly_cap_is_still_vetoed_when_exposure_is_also_overtotal_exposure_capbesidemonthly_subscription_allowancetest_a_dca_buy_over_max_exposure_is_not_vetoed_by_the_exposure_cap(sim)total_exposure_cap … 1005 exceeds … 1000test_a_dca_buy_over_the_per_asset_cap_is_vetoed_by_concentration_only(sim)total_exposure_caplisted beforeper_asset_concentration_captest_a_rule_buy_over_max_exposure_is_still_vetoed_by_the_exposure_cap(sim)test_parity_with_guards_check_when_total_exposure_is_the_tightest_captest_rail4_never_gates_a_sell_whatever_the_order_class[False/True]Two existing sim tests,
test_exposure_cap_rejects_over_capandtest_close_frees_up_exposure_for_a_subsequent_open, probed the exposure cap with the sim helper's defaultis_dca=Trueintent. They now probe with a rule-trading intent, which is what they test.Mutants (each proven applied: the source differed from the saved copy; restored from that copy, never
git checkout)test_rail4_total_exposure_cap_rejects_over_capandtest_a_malformed_history_row_still_counts_toward_the_exposure_capChecks
uv run pytest -q: 6850 passed, 3 skipped, exit 0uv run ruff check keel tests: exit 0uv run ruff format --check keel tests: exit 0uv run mypy: no issues in 473 files, exit 0🤖 Generated with Claude Code