Calculation catalog › Accounts and growth

Accounts balance per account annual

End-of-year balance of each plan account after flows and growth, keyed by account id.

Field: End-of-year balance per account Unit: US dollars Basis: nominal dollars Computed by: Engine Status: Complete chain

Appears in: chart (Investable balances by account type (summed per category by display-balance-by-category-annual)); report (chart-data block per-category series (same category sum)); results-table (Inherited schedule and carryforward callouts read individual balances indirectly; no per-account column is printed); mcp (run_projection.summary.estateBreakdown[].grossBalance (the last projection year's balance, for each investable account whose balance is positive)); mcp (compare_scenarios.a.estateBreakdown[].grossBalance (the last projection year's balance, for each investable account whose balance is positive)); mcp (compare_scenarios.b.estateBreakdown[].grossBalance (the last projection year's balance, for each investable account whose balance is positive))

Calculation records

  • Year-end balance map: one entry per id, written channel by channel Composition record, justified by derivation

    Purpose: The per-account year-end balances the ledger publishes for every projection year.

    Statement: YearResult.balances, materialized by projection/internal/annualSnapshot.ts#annualSnapshot, is the year-end map written in exactly this order: logical investable balances, then property values, then ordinary debt balances, then permanent-life cash values. Object.fromEntries keeps the later-channel write, so when two channels share an id the LAST channel written wins. Each value is that channel's own full-year figure and no netting across channels happens: a debt appears as its positive outstanding balance, not as a negative asset. Units: nominal dollars per id. Rounding: none.

    Formula: balances = fromEntries([...investable, ...property, ...debt, ...insuranceCashValue]) ; last write wins per id

    • investable: One published row per logical investable account id, at its year-end balance (unit: usd) (domain: finite)
    • property, debt: Year-end property values and ordinary debt balances, in insertion order (unit: usd) (domain: nonnegative)
    • insuranceCashValue: Permanent-life cash values, written last (unit: usd) (domain: nonnegative)

    Timing: once per projection year, at the end of the annual pass

    Rounding: none

    Limits:

    • Beyond the worksheet's inputs the evidence plan fixes: a single filer aged 51 in 2026 (no Medicare month, no required distribution), the plan's inflationPct at 3 (the property grows at general inflation, see below), zero return on the investable account, wage and recurring income sized to fund the stated contribution and the $4,000 debt service, no spending other than that debt service and the $15,000 withdrawal realized as a one-time goal, a single-year horizon, and an interest-free debt whose scheduled payment is exactly the $4,000 of principal the worksheet amortizes
    • The withdrawal is realized as an uninflated one-time goal, which is what makes the investable close opening + contributions - withdrawals at a zero return
    • The worksheet names 3% as the PROPERTY's appreciation; internal/propertyEventsAndGrowth.ts grows a property at GENERAL inflation and ignores the property account's own annualReturnPct, so the evidence sets the plan's inflationPct to 3 instead. In the start year every cumulative factor is still 1, so nothing else in the row moves
    • The collision case is asserted by calling annualSnapshot directly with four channels on one id, because no valid plan can reach the snapshot with two channels on one id: the plan checks refuse every pair of rows that would publish a value under a shared id (model/sharedIdCollisions.ts). The overwrite order is a property of the snapshot, not of any plan
    • The map holds no HECM line: HECM debt is published separately and only enters net worth under its non-recourse cap
    • Until decision D-CASH-PROPERTY-ALIAS (2026-09-25, extended the next day) the plan checks accepted several pairs on a single id: a cash account and a property, a property and a debt, two properties, two debts, an account and a permanent-life policy, and two permanent-life policies. The later value then overwrote the earlier one in this map and in every figure read from it (a $300,000 home published as cash), and two properties, two debts or two permanent-life policies under one id counted only one of them in net worth. The checks now refuse every such pair, and a stored plan that has one gets new ids for the colliding rows when it is loaded (an investable account keeps the id), so no plan reaches the snapshot with one

    Provenance: derived by codex, implemented by claude.

    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: accounts-balance-per-account-annual.md Mutation receipt: accounts-balance-per-account-annual.mutation.md

    Evidence tests: annualSnapshot.balances.evidence.test.ts

    Implemented in: annualSnapshot.ts; simulate.ts

    Is the number of: accounts-balance-per-account-annual

    Feeds: display-balance-by-category-annual

Records that feed this number

  • Glidepath compilation: linear interpolation with flat endpoints, staged as a step function Model record, justified by derivation

    Purpose: This year's target weight vector for an allocated account under any of the four policy modes.

    Statement: static: weightsToVector(weights). linear: from = weightsToVector(policy.from), to = weightsToVector(policy.to); year <= startYear or endYear <= startYear returns from; year >= endYear returns to; otherwise lerp(from, to, t) with t = (year - startYear)/(endYear - startYear), where lerp mixes componentwise a_i + (b_i - a_i) t and renormalizes the mix to sum 1 when its sum is positive. staged: stages sorted by fromYear; the target is the last stage whose fromYear <= year, or the earliest stage when none has started. custom: targets sorted by year; a year at or before the first target returns its vector and at or after the last returns the last; otherwise the first bracketing pair lo, hi with lo.year <= year <= hi.year is lerped at t = (year - lo.year)/(hi.year - lo.year). Units: fractions in ASSET_CLASS_IDS order. Rounding: none.

    Formula: w(y) = a + t (b - a), t = (y - y0)/(y1 - y0), renormalized to sum 1; w(y) = a for y <= y0; w(y) = b for y >= y1; staged: w(y) = weights of the last stage with fromYear <= y

    • y: Calendar year being compiled (unit: year) (domain: integer, schema 1900..2200)
    • y0, y1: Glidepath start and end years (or a bracketing custom pair) (unit: year) (domain: y0 < y1 for interpolation)
    • a, b: Normalized endpoint vectors (unit: 1) (domain: each sums to 1)
    • t: Interior fraction of the way from y0 to y1 (unit: 1) (domain: 0 < t < 1)
    • w(y): Target weight vector for the year (unit: 1) (domain: sums to 1)

    Timing: annual; one target per calendar year

    Rounding: none

    Limits:

    • Endpoints clamp flat; a linear or custom policy is never extrapolated
    • Linear and custom mixes are renormalized after interpolation, which changes nothing when both endpoints already sum to 1
    • A degenerate linear policy (endYear <= startYear) holds the from vector for every year
    • A staged policy before its first stage year holds the earliest stage; a custom policy with one target holds it for every year
    • lerpVectors is module-private; the evidence reaches it through targetWeightsAt
    • The worksheet names bucket-lens-allocation upstream; the bucket lens (its own record) reads the published investable total and net portfolio need, refusing a year whose need is not a finite number, and is not listed here

    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: allocation-glidepath-interpolation.md Mutation receipt: allocation-glidepath-interpolation.mutation.md

    Evidence tests: assetClasses.evidence.test.ts

    Implemented in: assetClasses.ts

    Feeds: income-taxable-yield-annual, accounts-balance-per-account-annual

  • Unrebalanced weights after one year of class returns Formula record, justified by derivation

    Purpose: How an allocation drifts when each class grows at its own total return and nothing is rebalanced.

    Statement: grown_i = w_i x max(0, 1 + r_i/100), with a missing r_i read as 0; total = sum of grown_i; the drifted vector is grown_i/total when total > 0, else the input weights unchanged. Units: fractions. Rounding: none. The ledger calls it after each year's growth with r_i = returnPct_i plus the year's class shock, and the result is the next year's tracked weights until a rebalance retargets them.

    Formula: w'_i = w_i (1 + r_i/100) / sum_j w_j (1 + r_j/100), each factor floored at 0

    • w_i: Beginning weight of class i (unit: 1) (domain: w_i >= 0, sum 1)
    • r_i: Total return of class i over the year (unit: percent) (domain: r_i >= -100 for a positive factor)
    • w'_i: Ending weight of class i (unit: 1) (domain: sum 1 when the total is positive)

    Timing: end of year, after growth

    Rounding: none

    Limits:

    • Total-return convention: distributions are treated as reinvested pro rata, so a class's weight moves with its total return rather than its price return
    • A class return at or below -100% is floored at zero ending value rather than going negative
    • A zero total ending value returns the input weights unchanged instead of refusing
    • The worksheet names bucket-lens-allocation upstream; the bucket lens (its own record) reads the published investable total and net portfolio need, refusing a year whose need is not a finite number, and is not listed here

    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: allocation-total-return-drift.md Mutation receipt: allocation-total-return-drift.mutation.md

    Evidence tests: assetClasses.evidence.test.ts

    Implemented in: assetClasses.ts

    Feeds: accounts-balance-per-account-annual, accounts-investable-total-annual

  • Allocation weights to a normalized fraction vector in class order Formula record, justified by derivation

    Purpose: Turns a percent-weights record into the fraction vector every allocation calculation indexes by class.

    Statement: For a weights record in percent, raw_i = max(0, w_i) in ASSET_CLASS_IDS order (usStocks, intlStocks, bonds, cash); W = sum of raw_i; the vector is raw_i/W. When W <= 0 (every weight zero or negative) the vector is all cash, [0, 0, 0, 1]. Units: fractions summing to 1. Rounding: none.

    Formula: f_i = max(0, w_i) / sum_j max(0, w_j); f = [0, 0, 0, 1] when the sum is not positive

    • w_i: Weight of class i as entered (unit: percent) (domain: schema: 0 <= w_i <= 100, total 100 +/- 0.5)
    • W: Sum of the floored weights (unit: percent) (domain: W >= 0)
    • f_i: Normalized fraction of class i (unit: 1) (domain: 0 <= f_i <= 1, sum 1)

    Timing: time-invariant

    Rounding: none

    Limits:

    • Negative weights are floored at 0 before normalizing rather than rejected; the schema already bounds each weight to [0, 100] and the total to 100 +/- 0.5, so a near-100 total normalizes exactly to 1
    • A zero total returns the all-cash vector instead of refusing; the worksheet's domain is a positive total and its test does not reach that branch
    • Class order is positional and fixed by ASSET_CLASS_IDS; every consumer indexes the vector by that order, never by magnitude
    • The worksheet names bucket-lens-allocation upstream; the bucket lens (its own record) reads the published investable total and the published net portfolio need, and refuses a year whose need is not a finite number, so the family is reached only through the balances every growth record feeds and is not listed here

    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: allocation-weight-normalization.md Mutation receipt: allocation-weight-normalization.mutation.md

    Evidence tests: assetClasses.evidence.test.ts

    Implemented in: assetClasses.ts

    Feeds: accounts-balance-per-account-annual, accounts-investable-total-annual

  • Asset-class parameters: assumption overrides laid over the sourced defaults Composition record, justified by derivation

    Purpose: The per-class return, volatility, yield and qualified-share figures every allocation blend reads.

    Statement: For each class id in ASSET_CLASS_IDS order (usStocks, intlStocks, bonds, cash) the resolved record is { label: the default label; returnPct, volatilityPct, interestYieldPct, dividendYieldPct, qualifiedRatioPct: the override object's field when the overrides name that class and supply that field (o?.field ?? d.field), else the sourced default }. An absent overrides object, an absent class and an absent field each fall through to the default; the label cannot be overridden. Units: returnPct, volatilityPct, interestYieldPct and dividendYieldPct in percent per year; qualifiedRatioPct in percent. Rounding: none; every value passes through unchanged.

    Formula: resolved[c].f = override[c]?.f ?? default[c].f for f in {returnPct, volatilityPct, interestYieldPct, dividendYieldPct, qualifiedRatioPct}; resolved[c].label = default[c].label

    • c: Asset class id (unit: enum) (domain: usStocks | intlStocks | bonds | cash)
    • default[c]: Sourced planning default record for the class (DEFAULT_ASSET_CLASS_PARAMS) (unit: mixed) (domain: fixed)
    • override[c]: Optional per-class assumption overrides, each field optional (unit: mixed) (domain: schema-bounded)
    • resolved[c]: The record the blends read (unit: mixed) (domain: every field present)

    Timing: resolved once per simulation and once per Monte Carlo run; time-invariant

    Rounding: none

    Limits:

    • Overrides apply field by field: an override object cannot remove a class or blank a field, and the label is never overridable
    • The sourced defaults (7/7/4/2.5 percent returns, 19.6/21/7.7/0.5 volatilities, the yields and qualified shares) are planning conventions from domain rules section 15, not forecasts; this record pins the overlay, not the defaults
    • No validation here: the plan schema bounds each override field before it reaches the function
    • The worksheet asks for exactness at one-decimal precision; the test compares with an absolute bound of 0 because the helper's exact keyword is reserved for integers, and pass-through doubles are equal bit for bit

    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: asset-class-parameter-overrides.md Mutation receipt: asset-class-parameter-overrides.mutation.md

    Evidence tests: assetClasses.evidence.test.ts

    Implemented in: assetClasses.ts

    Feeds: accounts-balance-per-account-annual, income-taxable-yield-annual, monte-carlo-investable-fan-percentiles

  • Annual surplus invested, floored at zero Formula record, justified by derivation

    Purpose: The residual cash a year ends with, after every stated use, and where it lands.

    Statement: projection/internal/types/result.ts#YearResult.surplusInvested is max(0, accepted cash inflows - expenses.total - contributions - tax - penalties) at the accepted funding fixed point. The credited surplus goes to the lowest-id cash account, otherwise the lowest-id taxable account with equal cost-basis growth, otherwise unassigned cash with a warning; the amount has no cap. Units: nominal USD per year. Rounding: none.

    Formula: surplusInvested = max(0, I - X - C - T - P)

    • I: Accepted cash inflows at the funding fixed point (unit: usd/year) (domain: nonnegative)
    • X: expenses.total for the year (unit: usd/year) (domain: nonnegative)
    • C: Contributions credited (unit: usd/year) (domain: nonnegative)
    • T: Composed tax (unit: usd/year) (domain: nonnegative)
    • P: Penalties (unit: usd/year) (domain: nonnegative)

    Timing: once per projection year, at the accepted funding fixed point

    Rounding: none

    Limits:

    • Asserted as the published field of a real simulatePlan 2026 row that carries all five of the worksheet's members. Plan assumptions beyond the worksheet's inputs: a 55-year-old filing single in KY, so no Medicare or marketplace premium can move expenses.total off 50,000; cash inflows split into an 80,000 wage and a 20,000 tax-free recurring receipt, so a flat 10-percent test calculator prices tax at exactly 8,000 while inflows are exactly 100,000; the 10,000 of contributions go to one taxable brokerage account; and the 2,000 of penalties are the IRC 4974 excise on a completed five-year deadline year (opening benefit 12,000, 4,000 distributed by the 2026 deadline) asserted on an inherited Roth account
    • That excise is the only penalty a surplus year can carry: the early-withdrawal and HSA penalties are charged on need-based withdrawals, and a year whose inflows exceed its uses takes none, so no plan can produce both a positive surplus and an early-withdrawal penalty
    • The flat test calculator is a test double, not the shipped calculator; it is used only to realize the worksheet's stated tax member exactly, and the record makes no claim about federal tax arithmetic

    Provenance: derived by codex, implemented by claude.

    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: surplus-invested-annual.md Mutation receipt: surplus-invested-annual.mutation.md

    Evidence tests: simulate.evidence.test.ts

    Implemented in: result.ts; annualFundingApplicationAndClosePhase.ts

    Is the number of: surplus-invested-annual

    Feeds: accounts-balance-per-account-annual, cash-flow-reconciliation-totals

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

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.