Calculation catalog › Cash flow and summary

Spending guardrail factor annual

Multiplier applied to flexible spending after the projection year guardrail decision.

Field: guardrailFactor Unit: factor Basis: no dollar basis Computed by: Engine Status: Complete chain

Appears in: csv (guardrailFactor)

Calculation records

  • Risk-based guardrail: one year's cut, raise or hold against real-balance thresholds Model record, justified by derivation

    Purpose: The decision that rations discretionary spending when the deflated portfolio leaves its solved balance band.

    Statement: adj = (adjustmentPct ?? 10)/100; prev = clamp(prevMultiplier) into [0, max(0, maxMultiplier)]. When startingBalance is not > 0, or currentRealBalance is not finite or is negative: hold at prev. lower = (lowerBalanceThresholdPct/100) x startingBalance when that percent is supplied and > 0, else null; upper likewise from upperBalanceThresholdPct. When both are set and lower >= upper: hold. currentRealBalance < lower: multiplier = clamp(prev - adj), action "cut" when it fell by more than 1e-9, else "hold". currentRealBalance > upper: multiplier = clamp(prev + adj), action "raise" when it rose by more than 1e-9, else "hold". Otherwise hold. Units: balances in real (deflated) dollars; thresholds as percent of the starting real portfolio; the multiplier a fraction. Rounding: none. annualGuardrailFunding.ts deflates the start-of-year portfolio by the year's inflation factor and anchors startingBalance at the first solvent year's real portfolio.

    Formula: L = l/100 x S, U = u/100 x S; m = clamp(prev - adj) if B < L; m = clamp(prev + adj) if B > U; m = prev otherwise, and always when L or U is unset or L >= U

    • B: This year's real start-of-year portfolio (unit: usd, real) (domain: B >= 0 and finite)
    • S: The starting real portfolio the thresholds are scaled from (unit: usd, real) (domain: S > 0)
    • l, u: Lower and upper thresholds as percent of S (unit: percent) (domain: each > 0 when set; l < u)
    • adj: Step size as a fraction of the full discretionary layer (unit: 1) (domain: default 0.1)
    • m: This year's discretionary multiplier (unit: 1) (domain: 0 <= m <= maxMultiplier)

    Timing: once per projection year, before the funding fixed point

    Rounding: none

    Limits:

    • Thresholds come from the shared-path solver (montecarlo/riskBasedGuardrails.ts); a policy whose thresholds are absent, zero or inverted holds, so the mode never acts on unsolved numbers
    • The dollar thresholds the pages print (display-guardrail-balance-thresholds) are this step's L and U on the first-year anchor, published by montecarlo/riskBasedGuardrails.ts#guardrailThresholdDollars (the record guardrail-threshold-dollars); at a zero starting balance the anchor is the first year the portfolio has a balance, which only a simulation knows
    • Same fixed step and clamp semantics as the withdrawal-rate step; clampRange is module-private and reached through nextBalanceGuardrailMultiplier
    • The Monte Carlo action counts and cut-depth percentiles aggregate this decision's action and multiplier exactly as they do for the withdrawal-rate mode

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

    Independent review: grok

    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: balance-risk-guardrail-step.md Mutation receipt: balance-risk-guardrail-step.mutation.md

    Evidence tests: guardrails.evidence.test.ts

    Implemented in: guardrails.ts

    Is the number of: spending-guardrail-factor-annual

    Feeds: monte-carlo-guardrail-action-counts, monte-carlo-max-cut-depth-percentiles, display-guardrail-balance-thresholds

  • Withdrawal-rate guardrail: one year's cut, raise or hold of the discretionary multiplier Model record, justified by derivation

    Purpose: The Guyton-Klinger-style decision that rations discretionary spending when the withdrawal rate leaves its band.

    Statement: upper = (upperGuardrailPct ?? 120)/100; lower = (lowerGuardrailPct ?? 80)/100; adj = (adjustmentPct ?? 10)/100; prev = clamp(prevMultiplier) into [0, max(0, maxMultiplier)] with maxMultiplier defaulting to 1 and a non-finite prev read as min(1, that ceiling). When startingRate is not > 0, or currentRate is not finite or is negative: hold at prev. Otherwise ratio = currentRate/startingRate. ratio > upper: multiplier = clamp(prev - adj), action "cut" when it fell by more than 1e-9, else "hold". ratio < lower: multiplier = clamp(prev + adj), action "raise" when it rose by more than 1e-9, else "hold". Otherwise hold. Units: the multiplier is a fraction of the discretionary layer; the rates are ratios of target spending to the start-of-year portfolio whose units cancel. Rounding: none.

    Formula: ratio = current/starting; m = clamp(prev - adj) if ratio > upper; m = clamp(prev + adj) if ratio < lower; m = prev otherwise; clamp into [0, maxMultiplier]

    • prev: Last year's discretionary multiplier (unit: 1) (domain: clamped into [0, maxMultiplier])
    • current, starting: This year's and the first year's withdrawal rate (unit: 1) (domain: starting > 0, current >= 0 and finite)
    • upper, lower: Band edges as fractions of the starting rate (unit: 1) (domain: defaults 1.2 and 0.8)
    • adj: Step size as a fraction of the full discretionary layer (unit: 1) (domain: default 0.1)
    • m: This year's discretionary multiplier (unit: 1) (domain: 0 <= m <= maxMultiplier)

    Timing: once per projection year, before the funding fixed point

    Rounding: none

    Limits:

    • The signal is gross recurring target spending over the start-of-year portfolio, not the net portfolio draw: a documented simplification that keeps the rule non-circular with the spending it decides
    • The step is a fixed adjustmentPct of the full discretionary layer, not a percentage of the previous multiplier
    • A cut or raise the clamp absorbs is reported as hold, so downstream counts reflect real moves only
    • clampRange is module-private; the evidence reaches it through nextGuardrailMultiplier
    • The Monte Carlo counts each year's action (run.ts) and takes 1 - guardrailFactor as the year's cut depth, so both aggregates are fed by this decision

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

    Independent review: cursor

    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: withdrawal-rate-guardrail-step.md Mutation receipt: withdrawal-rate-guardrail-step.mutation.md

    Evidence tests: guardrails.evidence.test.ts

    Implemented in: guardrails.ts

    Is the number of: spending-guardrail-factor-annual

    Feeds: monte-carlo-guardrail-action-counts, monte-carlo-max-cut-depth-percentiles

Records that feed this number

  • 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

Engine source: engine/src/projection/internal/types/result.ts (YearResult.expenses.guardrailFactor)

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.