Skip to content

feat: model-aware monthly budget gauge + ADR-0051 activation runbook - #237

Draft
ldamasio wants to merge 8 commits into
mainfrom
feat/budget-model-aware-gauge
Draft

ldamasio wants to merge 8 commits into
mainfrom
feat/budget-model-aware-gauge

Conversation

@ldamasio

@ldamasio ldamasio commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • On 2026-10-15 a net +83.50 USDT October with an empty book showed 2 of 4 slots and "1.0% of a 4.0% limit". Production persists hwm_v1 (ADR-0046), which charges the 17.92 give-back from the intra-trade equity peak; the operator decision ADR-0051 (net-from-start, 2026-08-04) says such a month shows 4 slots and 0%. The shadow metric robsond_budget_model_shadow_slots_delta reads 2 in production right now.
  • ADR-0051's activation step was never run, and nothing on the dashboard would have followed the switch: the MONTHLY LIMIT gauge was computed from the HWM consumption regardless of the active model, so a flip would have shown 4 slots next to a 1.0% gauge.
  • This PR makes /status and the card model-aware (ADR-0051 rollout step 3) and adds the activation runbook (step 4). It does not switch the model; that is the operator's guarded UPDATE monthly_state, documented, no restart needed.

Review round (Codex gpt-6.1 xhigh, on the branch)

First verdict: code checks passed (no consumer breaks on the null HWM fields, every monthly_state insert path inherits the model, no cache, rollback resumes from the true peak); three P1 and three P2 on the runbook/policy. Third commit answers them:

  • Non-expanding basis now enforced in code (ADR-0051 §1): MonthlyBudgetModel::allows_capital_base_change withholds capital_base increases under net-from-start in both recalibration paths (ledger-confirmed TRANSFER, manual drift); decreases still apply; HWM unchanged. Unit-tested in robson-domain.
  • Runbook gated on policy: step 0 stops unless the operator has approved the new "Activation exceptions" section of ADR-0051 (marked PENDING OPERATOR APPROVAL; it lists funding-outside-governed-net, Entering cancel, the August ledger gap, each with its bound). Nothing is waived by this PR.
  • Settlement gate: direct ledger SQL (unmatched of any age must be 0), the month's out-of-band sum must stay under 10% of remaining budget, and month_governed_realized_net is cross-checked against the closed-position projection.
  • P2s: MonthlyHalt latch check added; shadow-delta expectation corrected (metric stays slots_nfs − slots_hwm); first-entry validation distinguishes Armed (no reservation) from Entering/Active.

Rounds 2 and 3 (commits 4 and 5): the settlement-gap SQL selects funding by type (the matcher marks FUNDING_FEE rows matched), the slots formula uses monthly_budget_remaining directly, and ledger freshness is checked against the exchange's income history instead of the position-reconciliation metric. An in-memory attribution of withheld deposits in the drift scan was tried and removed on review: it could mask a later genuine withdrawal and did not survive restarts. Accepted, documented consequence: after an intra-month deposit under net-from-start the idle drift scan warns every cycle until the month boundary (fail-loud); the persisted design is #242 and the operating constraint until then is no intra-month deposits under net-from-start.

The PR leaves draft when the operator decides on the ADR-0051 "Activation exceptions" amendment.

Changes

  • Key changes in this PR:
    • robsond/src/api.rs: new monthly_budget_utilization_pct (consumed under the active model, % of the 4% budget); month_peak_net and monthly_giveback_pct become Option and are served only while hwm_v1 is persisted (null under net-from-start, ADR-0051 §4). month_equity_net stays as informational mark-to-market. Slots, remaining budget and halt were already model-aware.
    • frontend/src/lib/presentation/budget.ts: budgetModelKind() (net_from_start | hwm | legacy) and deriveBudgetView prefers the utilization field; BudgetView.model exposed.
    • frontend/.../dashboard/+page.svelte: MONTHLY LIMIT copy per model ("Net governed loss since month start · Profits do not raise the limit" with net / reserved / left; HWM keeps peak / now; legacy unchanged).
    • frontend/src/lib/api/robson.ts: monthly_budget_model, monthly_budget_utilization_pct, month_governed_realized_net, monthly_net_loss_consumed, monthly_open_risk_reserved, monthly_budget_amount typed and normalized (null on older backends).
    • docs/runbooks/activate-net-from-start-budget.md (+ README index): preconditions (flat account, unmatched_income_count == 0, shadow check), guarded UPDATE for the current month, validation, rollback, accepted gaps (funding excluded from governed net; Entering cancel).
    • robson-domain/src/policy.rs, robsond/src/income_ledger.rs, robsond/src/reconciliation_worker.rs: non-expanding basis under net-from-start.
    • ADR-0051 status note and "Activation exceptions" (pending approval); CHANGELOG.

Tests

  • How to verify:
    • cargo test -p robson-domain -p robsond --features robsond/postgres (container, rust 1.92) — 170 + 362 passed; clippy -D correctness -D suspicious clean; cargo +nightly-2026-08-22 fmt --check clean
    • cd frontend && npx vitest run — 133 passed (budget.test.ts 12); svelte-check 0 errors
  • Added/updated tests:
    • robsond/src/api.rs status test asserts the HWM outputs are Some under hwm_v1 and utilization mirrors active consumption.
    • robson-domain/src/policy.rs: allows_capital_base_change under both models.
    • frontend/tests/unit/budget.test.ts: production payload of 2026-10-15 under both models, model-kind detection, and the guarantee that the gross-loss fallback is never used once utilization is reported (it would read 49.6% on a month that is net +83.50).

Migrations

  • No schema changes (the monthly_budget_model column and CHECK exist since migration 25)

Backward Compatibility

  • /status JSON: two fields may now be null (month_peak_net, monthly_giveback_pct), only after the operator switches the model. The frontend on main already types them as number | null and robson-cli does not read them. New field is additive.
  • Deploy order: frontend and daemon can go in any order; the behaviour only changes when the runbook is executed, which requires both to be deployed (the runbook checks for monthly_budget_utilization_pct).
  • Merging the daemon part restarts the pod: merge with an empty book (it is empty now).

Security & Data

  • No secrets. No new external calls. The runbook runs SQL through kubectl exec on the prod Postgres pod with the operator's kubeconfig.

Checklist

  • Scope is focused and documented
  • Code follows project conventions (see docs/DEVELOPER.md)
  • Migrations created (if needed) and rationale explained
  • Tests added/updated and passing locally
  • Docs updated (runbook, ADR-0051, CHANGELOG)

🤖 Generated with Claude Code

ldamasio and others added 3 commits October 15, 2026 09:27
A net +83.50 USDT October with an empty book showed 2 of 4 slots and
"1.0% of a 4.0% limit" on 2026-10-15. Production persists `hwm_v1`
(ADR-0046), so the 17.92 give-back from the intra-trade equity peak is
charged against the monthly limit; ADR-0051 (net-from-start, operator
decision 2026-08-04) would show 4 slots and 0%, and the shadow metric
`robsond_budget_model_shadow_slots_delta` reads 2 in production. The
ADR's activation step was never run, partly because nothing on the
dashboard would have followed the switch: `monthly_giveback_pct` was
computed from the HWM consumption regardless of the active model.

- `/status`: new `monthly_budget_utilization_pct` = consumed under the
  ACTIVE model as a share of the 4% budget. `month_peak_net` and
  `monthly_giveback_pct` become `Option` and are served only while
  `hwm_v1` is persisted (ADR-0051 §4). `month_equity_net` stays as the
  informational mark-to-market. Slots, remaining budget and halt were
  already model-aware.
- Runbook `docs/runbooks/activate-net-from-start-budget.md`: §5.4
  preconditions (flat account, no unmatched income, shadow check), the
  guarded `UPDATE monthly_state` for the current month, validation,
  rollback, and the accepted gaps (funding excluded from governed net,
  Entering cancel). No restart: the model is read on every snapshot and
  later months inherit it on every insert path.
- ADR-0051 status note and CHANGELOG.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`deriveBudgetView` now prefers `monthly_budget_utilization_pct` and
exposes the model (`net_from_start` | `hwm` | `legacy`) derived from
`monthly_budget_model` with field-presence fallback. Under net-from-start
the card reads "Net governed loss since month start · Profits do not
raise the limit" with `net / reserved / left`; under HWM it keeps
`peak / now`; older backends keep the gross-loss fallback. Without this
the activation would have shown 4 slots next to a 1.0% gauge, and a
backend that withholds the HWM fields would have fallen back to the
gross-loss formula (49.6% on a month that is net +83.50).

Tests: production payload of 2026-10-15 under both models, model kind
detection, and the no-fallback guarantee once utilization is reported.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…te the runbook on ADR approval

Review findings (Codex, P1/P2) on the activation runbook:

- ADR-0051 §1 says the monthly budget basis MUST NOT increase during the
  month under net-from-start, but both recalibration paths (ledger-
  confirmed TRANSFER in income_ledger.rs, manual drift in
  reconciliation_worker.rs) could raise capital_base regardless of the
  model. `MonthlyBudgetModel::allows_capital_base_change` now withholds
  increases under net-from-start; decreases still apply; HWM unchanged.
- The runbook accepted §5.4 blockers (funding outside governed net,
  Entering cancel, August ledger gap) on its own authority. It now stops
  at step 0 unless the operator has approved the "Activation exceptions"
  amendment added to ADR-0051, which lists each gap with its bound.
- `unmatched_income_count == 0` is not settlement completeness: it skips
  acknowledged rows and the grace window and reads 0 on query failure.
  The runbook now queries the ledger directly (unmatched of any age must
  be zero), lists this month's out-of-band items and refuses activation
  when their sum exceeds 10% of the remaining budget, and cross-checks
  month_governed_realized_net against the closed-position projection.
- Added the MonthlyHalt latch check (switching models does not clear a
  latched halt), corrected the shadow-delta expectation (the metric stays
  slots_nfs - slots_hwm after activation) and the first-entry validation
  (Armed reserves nothing; Entering/Active do).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…asure funding by type in the runbook

Second review round (Codex):

- Withholding a capital_base increase under net-from-start left the
  wallet above `capital_base + month_net` forever, so the idle drift scan
  warned about the same explained deposit on every cycle. The manager now
  keeps the withheld amount (set by both recalibration paths, cleared when
  the delta is within tolerance or a recalibration is applied) and the
  drift scan adds it to the expected wallet balance. In-memory only: a
  restart re-derives it on the next ledger cycle.
- The runbook's settlement-gap query filtered on `matched_at IS NULL`, but
  the matcher marks FUNDING_FEE rows matched, so it excluded the very
  funding it claimed to measure. Funding is now selected by type, plus
  acknowledged and still-unmatched rows, transfers excluded; and a fresh
  reconciliation scan (last completed within 10 minutes) is required.
- First-entry validation: `monthly_budget_remaining` already subtracts the
  reservation, so slots are `min(4, floor(remaining / risk_unit))`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ldamasio and others added 4 commits October 15, 2026 10:02
…loud deposit nuisance and require ledger freshness

Third review round (Codex): the in-memory "withheld basis increase"
scalar could mask a genuine withdrawal of the same size, survived basis
resets, and could not be rebuilt after a restart for manual gains. It is
removed; withholding stays and the recurring drift warning after an
intra-month deposit is documented as expected, fail-loud behaviour with
the persisted design tracked in #242 (runbook, ADR amendment row,
CHANGELOG; operating constraint: no intra-month deposits under
net-from-start until then).

Runbook step 2 no longer treats a fresh position-reconciliation scan as
ledger freshness: it checks the ledger's newest item and this month's row
count against the exchange's income history and the last 30 minutes of
ingestion logs before trusting the settlement-gap query.

Co-Authored-By: Claude Fable 5.1 <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