Calculation catalog › Spending and withdrawals

Risk based guardrail solved balance thresholds

The cut and raise portfolio levels the risk-based guardrail solver finds for the plan's success band, as a fraction of today's investable portfolio: for each band edge, the lowest point of the solver's ten-step bisection lattice on [0.02, 4] whose fixed-target Monte Carlo success on shared seeded paths is at or above that edge.

Field: Solved risk-based guardrail balance thresholds Unit: factor Basis: no dollar basis Computed by: Engine Status: Complete chain

Appears in: assumptions-card (Spending card, risk-based guardrails callout "cut spending if the portfolio falls below $X; raise if it rises above $Y" (SpendingPolicyRiskBased.tsx RiskBasedThresholdsCallout)); results-table (Risk-based spending guardrails callout "if the portfolio falls below $X ... above $Y"); monte-carlo-page (Risk-based dollar guardrails note "cut below $X · raise above $Y (today's dollars, solved on Spending)")

Calculation records

  • Risk-based guardrail thresholds and suggested adjustments by bisection Model record, justified by derivation

    Purpose: Finds the portfolio levels where the plan's Monte Carlo success meets each edge of its success band, and the spending change that brings success back to the middle of the band there.

    Statement: Let S(f, m) be the success rate of the plan with fixed target spending, with every investable balance scaled by f and target spending by m, on shared seeded Monte Carlo paths (or the value of the optional successProbe(f, m), which replaces that run). The band is L/U percent (the plan's policy, else 70/95) and the recovery target is (L + U)/200. For each edge p = L/100 and U/100: if S(0.02, 1) >= p the outcome is always-above-band; if S(4, 1) < p it is never-reaches-band; otherwise ten bisections on [0.02, 4] move hi to the midpoint when S(mid, 1) >= p and lo otherwise, and the edge is hi, a point 0.02 + k · 3.98/1024 with k a whole number from 1 to 1024 (the smallest such point meeting p), published with balancePct = round(hi · 10,000) / 100, the percent the planner persists. At a solved lower edge f_L the suggested cut bisects m eight times on [max(0.3, required/base), 1], moving lo when S(f_L, m) >= the recovery target, and returns lo, provided the best case S(f_L, lowest m) reaches the target; at a solved upper edge the raise does the same on [1, 2], and reports m = 2 when even S(f_U, 2) stays above the target. annualDollars = |1 − m| · base spending and monthlyDollars = annualDollars / 12. For S(f, m) = min(1, f/(2m)), one $500,000 taxable account, base spending $40,000, no required spending and a 70/95 band: successAtCurrent is 0.5; the edges are k = 356 and 484, balanceFrac 1.403671875 and 1.901171875, balancePct 140.37 and 190.12; the cut multiplier is 0.849609375 ($6,015.625 a year, $501.3020833333333 a month, success after 0.8260689655172412) and the raise multiplier 1.1484375 ($5,937.50 a year, $494.7916666666667 a month, success after 0.8277210884353741); the probe is called 40 times. Units: fractions of starting investable, multipliers, today's dollars. Rounding: none; absolute tolerance 1e-12 on fractions and multipliers and 1e-6 on dollars.

    Formula: edge(p) = min { 0.02 + k · 3.98/1024 : k = 1..1024, S(·, 1) >= p } by ten bisections; m_cut = max { m on the eight-step lattice of [m_min, 1] : S(f_L, m) >= (L + U)/200 }; m_raise likewise on [1, 2]; monthly = |1 − m| · base / 12

    • S(f, m): Fixed-target success rate at balance scale f and spending multiplier m (unit: 1) (domain: [0, 1], nondecreasing in f and nonincreasing in m; worksheet min(1, f/(2m)))
    • L, U: Success band edges (unit: percent) (domain: L < U; worksheet 70 and 95)
    • k: Lattice index of a solved edge (unit: 1) (domain: whole number 1 to 1024; worksheet 356 and 484)
    • m: Spending multiplier of a suggested adjustment (unit: 1) (domain: cut [max(0.3, required/base), 1], raise [1, 2]; worksheet 0.849609375 and 1.1484375)
    • base: Base annual target spending (unit: usd) (domain: > 0; worksheet 40,000)
    • B: Starting investable (risk-based-starting-investable) (unit: usd) (domain: worksheet 500,000)

    Timing: once per solve, on today's balances and today's spending

    Rounding: none

    Limits:

    • Monotonicity of success in f and m is assumed, not checked; with a curve that is not monotone the result is a lattice point but not a meaningful edge
    • The Spending card always solves under the lognormal model at 12 percent volatility around the plan's own expected returns (allocated accounts use their asset classes' volatilities), with 200 paths and the engine's default seed (monte-carlo-default-seed), whatever model the Monte Carlo page shows; the solve stores the seed beside the thresholds, and thresholds saved before schema v6 carry none and were solved on a seed taken from the plan id, which the card says
    • Successes are cached by the balance scale rounded to 1e-6; the lattice step is about 3.9e-3, so two lattice points never share a key. The spending phase is not cached, so successAfter re-evaluates a multiplier the bisection has usually tried already
    • With a single path S is 0 or 1, so the two band edges are the same test and return the same point
    • balancePct rounds in floating point: at three of the 1,024 lattice points (k = 192, 448 and 704) the product lands just below a half and rounds down, persisting 76.62, 176.12 and 275.62 where the exact values round half up to 76.63, 176.13 and 275.63; the callouts print the persisted percent of today's balances (guardrail-threshold-dollars)

    Provenance: derived by claude, implemented by claude-subagent.

    Independent review: codex

    Gates (6 of 6 pass): fixture registers a test: pass; pins resolve: pass; families exist: pass; worksheet exists: pass; mutation receipt exists: pass; provenance independent: pass

    Worksheet: risk-based-guardrail-threshold-solver.md Mutation receipt: risk-based-guardrail-threshold-solver.mutation.md

    Evidence tests: riskBasedGuardrails.evidence.test.ts

    Implemented in: riskBasedGuardrails.ts

    Is the number of: risk-based-guardrail-solved-balance-thresholds, risk-based-guardrail-suggested-adjustment-monthly

    Feeds: display-guardrail-balance-thresholds, spending-guardrail-factor-annual

Records that feed this number

  • Default Monte Carlo seed and the headline run's options Data record, justified by derivation

    Purpose: The one base seed every plan's default Monte Carlo draws its markets from, and the headline configuration every host can match.

    Statement: DEFAULT_MONTE_CARLO_SEED = 0x5eeded = 6,221,293 (decision D-MC-DEFAULT-SEED, 2026-09-28) is the base seed of every default Monte Carlo run, the same for every plan whatever its id: the headline success rate, the Monte Carlo page until the reader re-rolls (Re-roll replaces it for that page's runs only and is never saved), every comparison the planner pairs with the headline run (the Insight preview, Scenarios, the Social Security analysis, relocation, Optimize), the risk-based guardrail threshold solve, which stores the seed beside the thresholds, and the Optimize tournament's downside-resilience metric. headlineMonteCarloOptions(plan, startYear, pathCount = 1000) publishes { startYear, pathCount, seed: DEFAULT_MONTE_CARLO_SEED, model: buildLognormalModelConfigForPlan(plan, 12) }, so a host that uses it draws the app's markets. Path i is seeded with derivePathSeed(6,221,293, i): 2,931,854,348 for path 0 and 3,741,805,609 for path 1. Before the decision the planner hashed the plan id (FNV-1a), so Duplicate, Save to My Plans and import drew new markets and two plans compared side by side ran on independent markets. Units: unsigned 32-bit integer. Rounding: none.

    Formula: seed = 0x5eeded = 6,221,293 for every plan; pathSeed_i = derivePathSeed(seed, i); headline = { pathCount 1000, seed, lognormal(plan, 12) }

    • seed: Base Monte Carlo seed of a default run (unit: uint32) (domain: 6,221,293)
    • i: Zero-based path index (unit: 1) (domain: integer >= 0; worksheet 0 and 1)

    Timing: once per run; the page's Re-roll replaces it for that page only

    Rounding: none

    Limits:

    • Any fixed seed is one draw from the same sampling distribution: a headline rate on 1,000 paths still carries about 1.4 points of sampling error, which one seed for every plan does not remove; it removes the id's part of it and pairs every comparison
    • The value was kept because it was already the tournament's seed, before any figure was measured on it; choosing a seed by its outcomes would bias every figure
    • An allocated plan draws class shocks after each year's return and inflation draws, so an allocated plan and a single-return plan share only their first year's draws even on one seed
    • RetireGolden-MCP adopted these options in 0.12.0: run_monte_carlo with no arguments runs headlineMonteCarloOptions (1,000 paths, this seed, the plan's lognormal model at 12 percent), where through 0.11.x it ran 200 paths on seed 42 with a plain lognormal model; an explicit seed, pathCount or returnVolPct replaces only its own default, and returnVolPct sets only the market factor, since an allocated account draws its classes' own volatilities
    • Risk-based thresholds saved before schema v6 carry no stored seed and were solved on the plan-id seed; the Spending card says so and offers to solve again (guardrailThresholdSeedBasis)

    Provenance: derived by claude, implemented by claude.

    Independent review: codex

    Gates (6 of 6 pass): fixture registers a test: pass; pins resolve: pass; families exist: pass; worksheet exists: pass; mutation receipt exists: pass; provenance independent: pass

    Worksheet: monte-carlo-default-seed.md Mutation receipt: monte-carlo-default-seed.mutation.md

    Evidence tests: headline.evidence.test.ts

    Implemented in: rng.ts; headline.ts

    Feeds: monte-carlo-success-rate, monte-carlo-investable-fan-percentiles, monte-carlo-ending-investable-histogram, monte-carlo-ending-after-tax-estate-percentiles, monte-carlo-depletion-probability-by-year, insight-monte-carlo-success-delta, risk-based-guardrail-solved-balance-thresholds

Transformations

  • The solver publishes balancePct = round(balanceFrac × 10,000) / 100 (montecarlo/riskBasedGuardrails.ts#balanceThresholdPct), which useThresholdSolve persists into plan.expenses.spendingPolicy.lowerBalanceThresholdPct / upperBalanceThresholdPct; guardrailThresholdDollars prints it in dollars (family display-guardrail-balance-thresholds).

Engine source: engine/src/montecarlo/riskBasedGuardrails.ts (RiskBasedGuardrailSolution.lower.balanceFrac / upper.balanceFrac)

The evidence links on this page (each record's worksheet, mutation receipt, evidence tests and implementation, and the source links) are pinned to engine commit 2624dbdba9fd, committed 2026-10-11. The live calculation index and output-family census links point at the repository's main branch, so you can check this snapshot against them.