Calculation catalog › Roth

Optimizer recommended conversion annual

Per-year Roth conversion dollars the optimizer recommends: the solver schedule, the full-projection tournament winner's conversions, and the post-processed (cleaned) executable schedule.

Field: Recommended Roth conversion schedule Unit: US dollars Basis: nominal dollars Computed by: Engine Status: Complete chain

Appears in: optimize-page (Conversion schedule chart bars and "N year(s)" schedule summary; post-processing adjustment list); report (modeled-findings winner conversions are summed into requested/executed totals); mcp (run_optimizer.schedule.conversions[].amount); mcp (run_optimizer.schedule.schedule[].conversion (every plan year; schedule.conversions[] lists the same amount for each year above $0.50)); mcp (run_optimizer.tournament.winnerConversions[].amount)

Calculation records

  • Full projection cleaned schedule Model record, justified by derivation

    Purpose: Trim a raw solver conversion schedule to what the full year-by-year projection actually executes.

    Statement: projection/optimizePlan.ts#postProcessExactLedgerSchedule publishes ExactLedgerPostProcessing.cleanedSchedule as the per-year minimum of the raw requested conversion and the ledger's executed conversion, taking that minimum only when execution exceeds the neutral tolerance and zero otherwise, and records each materially changed year as an ExactLedgerScheduleAdjustment whose reason is ledger-capped when the cleaned amount survives, dropped-zero when it does not, and estate-pruned when the trailing prune pass removes the year. Those three are the only members of the reason union.

    Formula: cleaned_y = executed_y > neutralTolerance ? min(requested_y, executed_y) : 0; reason_y = cleaned_y <= neutralTolerance ? dropped-zero : ledger-capped, or estate-pruned when the prune pass drops year y

    • requested_y: Raw solver schedule amount for year y (unit: nominal USD) (domain: nonnegative)
    • executed_y: max(0, the rerun ledger's rothConversion for year y) (unit: nominal USD) (domain: nonnegative)
    • neutralTolerance: DECISION_NEUTRAL_TOLERANCE_DOLLARS (unit: nominal USD) (domain: 1)

    Timing: raw request, full-projection execution, rerun, repeated to a fixed point within the stabilization budget

    Rounding: adjustment rows carry cents-rounded requested, executed and cleaned amounts; the dollar totals carry binary floating-point representation

    Limits:

    • Cleaned is the per-year minimum, not the request: keeping the request republishes 15000 dollars in a year the ledger funded with 5000, and summing the raw requests publishes 30000 dollars rather than the cleaned 20000. The reason on that year is ledger-capped. A fourth member, rounding, was once declared on the reason union and never assigned; decision D-ADJUSTMENT-ROUNDING-REASON removed it on 2026-09-25, and the evidence still asserts it is not among the reasons the run assigns. Of the worksheet's three live reasons only ledger-capped is realized by its stated inputs; dropped-zero and estate-pruned were NOT constructed from them, because each needs a different raw schedule and, for estate-pruned, a plan whose trailing conversion year harms the exact estate; the worksheet states neither, so the test asserts instead that the run's reasons lie inside the three live values and that rounding is not among them. Beyond the worksheet's inputs the test assumes a validated single-person plan with a 10000-dollar cash account and an empty Roth account beside the 20000-dollar traditional one, zero healthcare expenses, a planning age of 67 and the production federal tax calculator from start year 2026, so Y1 is 2026 and Y2 is 2027; the raw schedule is a full OptimizedSchedule literal whose per-year solver rows are zero apart from the conversion, which the entry requires and the worksheet does not name. The MILP-sourced winning schedule the worksheet mentions is a solver-run limit with no number and is not asserted here.

    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: exact-ledger-cleaned-schedule.md Mutation receipt: exact-ledger-cleaned-schedule.mutation.md

    Evidence tests: optimizePlan.evidence.test.ts

    Implemented in: optimizePlan.ts

    Is the number of: optimizer-recommended-conversion-annual

    Feeds: exact-ledger-tournament-margin-over-milp-dollars

  • Optimizer's per-year solution Model record, justified by derivation

    Purpose: What the conversion optimizer's solver plans for each year: its conversion, its withdrawal from each bucket, the gain and taxable income it books, and its year-end balances.

    Statement: strategies/optimizer.ts#optimizeSchedule publishes OptimizedSchedule.schedule, one OptimizedYear per plan year in order, from the solution HiGHS returns for the model #buildOptimizerModel writes, when the solve has one (the raw solution's primal status Feasible: an optimum, or the incumbent a node or time limit stopped at), and no rows when it has none (an infeasible model, or a limit reached before any incumbent). conversion, withdrawTraditional (the required-distribution floor included), withdrawInheritedTraditional, withdrawOther (from the tax-free bucket of Roth, cash and HSA), withdrawTaxable and taxableOrdinary are the solution's values of the year's conversion, withdrawal, sale and taxable-income columns; endTrad, endInheritedTrad, endOther and endTaxable are the four buckets' balances at the start of the next year, after the year's flows and growth, endTaxable 0 when the plan has no taxable bucket; taxableGainRealized is the published withdrawTaxable times the gain fraction min(1, max(0, 1 − the taxable basis ratio)); irmaaTier is the highest tier whose binary is above one half, 0 for none. The engine reads each value from HiGHS's raw solution, which HiGHS writes to 16 significant digits, through a wrapper around the highs package's solution writer (not the package's six-digit parse of the pretty print); each is then rounded to cents, and a column the solution does not carry reads 0. In the model, taxable ordinary income is at least the year's ordinary income base plus the taxable part of its conversion and its traditional and inherited withdrawals, less the standard deduction with the age-65 addition (and the senior deduction net of its phase-out when the solve prices it), and at least 0; minimizing tax holds it there. The tax-free withdrawal enters the model only as a difference with the surplus the year saves, which is not published, so a solution sets one of the two to 0; and a reward of one millionth of a dollar per converted dollar reports traditional drainage as a conversion, leaving withdrawTraditional at its floor unless the year needs the cash. OptimizedSchedule.conversions lists each year whose conversion is above $0.50, with the same amount, and is empty with no solution. Units: each year's nominal dollars.

    Formula: with a solution: v_t = round2(column_t); taxableGainRealized_t = round2(withdrawTaxable_t × gf), gf = min(1, max(0, 1 − basisRatio)); irmaaTier_t = max{k : b_{t,k} > 0.5}, else 0; endBucket_t = bucket_{t+1}; conversions = [(t, conversion_t) : conversion_t > 0.5]; with no solution: schedule = [], conversions = []

    • column_t: The raw solution's value of year t's conversion, withdrawal, sale, taxable-income or next-year balance column, as HiGHS writes it (16 significant digits) (unit: nominal USD) (domain: nonnegative; 0 when the solution carries none)
    • basisRatio: The opening taxable balance's cost basis over the balance, one ratio for the whole plan (unit: fraction) (domain: 0..1; 1 when not given)
    • b_{t,k}: The solution's value of year t's IRMAA tier k binary (unit: none) (domain: 0 or 1)

    Timing: every plan year of one solve

    Rounding: cents of the raw solution's value

    Limits:

    • Cents of the solution: a planned conversion of $107,028.8775 is published as $107,028.88, in schedule[] and in schedule.conversions[] alike. Which solution HiGHS returns where more than one is optimal belongs to the highs package the engine pins (1.15.2), and so does the format of its raw solution: a solve whose raw solution the engine cannot read throws an error rather than publish; an upgrade that puts any figure the evidence compares more than half a cent from the value stated here fails it.
    • A node-limit or timeout status publishes the rows of the incumbent HiGHS held when it stopped, which is not a proven optimum (optimizer-schedule-objective-and-lifetime-tax states the two limits); a solve with no solution publishes no rows, and the status says whether the model was infeasible or a limit came first.
    • The solver's plan, not what the projection of the same conversions does. The projection executes each conversion only up to what the owners' accounts hold (the cleaned schedule), funds spending in the plan's own withdrawal order and taxes each account by its own basis and rules, so its withdrawals, realized gains, taxable income and balances (withdrawals-by-category-annual, tax-realized-gains-annual, accounts-balance-per-account-annual) can differ from these in every year. The solver's buckets grow at one blended rate a year and its sale realizes one gain fraction for the whole plan.
    • withdrawOther is the net draw on the tax-free bucket when the year needs one and 0 when the year saves; the saving itself is not published and shows only in the next year-end balance (in the evidence's two-year case, $7,057.25 saved in 2027 ends as $7,410.11).
    • irmaaTier is a classification, a census exclusion; its surcharges enter the solver's lifetime tax.
    • Evidence: the two cases of optimizer-schedule-objective-and-lifetime-tax, derived by hand from the library example rmd-irmaa's facts without its qualified charitable distribution: the one-year case's conversion through the 24% bracket, required distribution, tax-free draw and year-end balances, and the two-year case's required distributions, inherited draws, sale and its gain, two tiers and saved surplus; and its two infeasible inputs, which publish no rows. A year with both a conversion and a sale, and an inherited distribution set by a divisor, are not exercised; they follow the same readout.

    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: optimizer-schedule-year-solution.md Mutation receipt: optimizer-schedule-year-solution.mutation.md

    Evidence tests: optimizer.schedule.evidence.test.ts

    Implemented in: optimizer.ts

    Is the number of: optimizer-recommended-conversion-annual, optimizer-schedule-withdrawal-by-bucket-annual, optimizer-schedule-taxable-gain-realized-annual, optimizer-schedule-taxable-ordinary-income-annual, optimizer-schedule-ending-balance-by-bucket-annual

    Feeds: optimizer-schedule-lifetime-tax, optimizer-schedule-ending-after-tax-objective, optimizer-schedule-conversion-total

Records that feed this number

  • Expected return for an account: allocation blend, else account rate, else plan default Composition record, justified by derivation

    Purpose: Which expected nominal return applies to an account this year, by precedence.

    Statement: For an account, the plan assumptions and a year: when the account is taxable, traditional, roth or hsa and carries an allocation policy (accountAllocation), the return is blendedReturnPct(targetWeightsAt(policy, year), resolveAssetClassParams(assumptions.assetClassParams)) and the account's own annualReturnPct is ignored; otherwise the account's annualReturnPct when the account type carries that field and it is not null; otherwise assumptions.defaultReturnPct. Units: percent per year, nominal. Rounding: none.

    Formula: r = blend(targetWeightsAt(policy, year), classParams) if the account has an allocation; else annualReturnPct if not null; else defaultReturnPct

    • policy: The account's opt-in allocation policy, if any (unit: policy) (domain: taxable, traditional, roth or hsa accounts only)
    • annualReturnPct: The account's own scalar expected return (unit: percent/year) (domain: number or null)
    • defaultReturnPct: The plan-wide default expected return (unit: percent/year) (domain: finite)
    • r: The expected return selected for the account (unit: percent/year) (domain: finite)

    Timing: annual; the blend uses the year's target weights

    Rounding: none

    Limits:

    • Only taxable, traditional, Roth and HSA accounts can carry an allocation; every other type falls to the scalar precedence
    • The blend uses the year's target weights, not the drifted weights the ledger holds under rebalancing "none"; the two coincide at the start year and under annual rebalancing
    • DUPLICATION: the ledger does not call this function. annualPostSolveAccountGrowth.ts applies the same precedence inline (the class blend over the account's tracked weights when a track exists, else state.account.annualReturnPct ?? defaultReturnPct); the production callers are optimizePlan.ts#blendedGrowth in the engine and assumptionsExport.ts in planner-ui, which formats the Assumptions card and has no census family. The evidence drives the ledger phase with the worksheet's allocated account and requires it to ignore the 9% scalar the same way
    • The worksheet names the per-account balance, investable-total and net-worth families upstream; the ledger's inline precedence produces them, so this record feeds the optimizer schedule families its caller reaches

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

    Evidence tests: assetClasses.evidence.test.ts

    Implemented in: assetClasses.ts

    Feeds: optimizer-recommended-conversion-annual, optimizer-schedule-conversion-total

  • Blended expected nominal return of a weight vector Formula record, justified by derivation

    Purpose: The one-period weighted mean of the class expected returns for a given allocation.

    Statement: For a weight vector w in ASSET_CLASS_IDS order and resolved class parameters, r = sum over i of w_i x returnPct_i, with a missing w_i read as 0. Units: percent per year, nominal; the class returns are percents and the weights fractions, so the result is in percent. Rounding: none.

    Formula: r = sum_i w_i r_i

    • w_i: Fraction of the account in class i (unit: 1) (domain: w_i >= 0, sum 1 for a normalized vector)
    • r_i: Expected nominal total return of class i (unit: percent/year) (domain: finite)
    • r: Blended expected nominal return (unit: percent/year) (domain: between min and max r_i for a normalized vector)

    Timing: one period (a year)

    Rounding: none

    Limits:

    • Weights are taken as given; the function neither normalizes nor validates them, so a vector not summing to 1 returns a scaled blend
    • Arithmetic one-period blend, not geometric: the expected return of a mix over one year is the weighted mean of the class returns
    • DUPLICATION: packages/engine/src/projection/internal/annualPostSolveAccountGrowth.ts forms the same sum of w_i r_i inline, with the year's class shock added to each r_i, to grow an allocated account; the ledger never calls blendedReturnPct, which reaches production only through expectedAccountReturnPct (the optimizer's balance-weighted growth rate in optimizePlan.ts#blendedGrowth) and the planner-ui AllocationPanel preview. The evidence drives annualPostSolveAccountGrowth at zero shock and requires its growth to equal blendedReturnPct on the worksheet's inputs
    • The worksheet names the per-account balance and investable-total families upstream; the ledger's inline copy produces them, so this record feeds the optimizer schedule families its own callers reach and pins parity with the copy instead of claiming the balance families

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

    Evidence tests: assetClasses.evidence.test.ts

    Implemented in: assetClasses.ts

    Feeds: optimizer-recommended-conversion-annual, optimizer-schedule-conversion-total

  • Claim age co-optimization Model record, justified by derivation

    Purpose: Count the Social Security claim combinations priced and publish the current-claim and joint exact estates, or say why no claim age was searched.

    Statement: projection/optimizePlan.ts#optimizePlanCoOptimizingClaimAge publishes ClaimAgeCoOptimization.outcome: already-claimed when every one of the first two Social Security streams was claimed before the start year (socialSecurity/openClaims.ts#isClaimAlreadyMade: birth year plus claim years before the start year), aca-unpriced when the plan as entered has a year whose premium tax credit cannot be priced (each year published with its blocking support codes), no-claims with no stream, no-age-left when a claim is open but the generator offers no candidate (every canonical age is the current claim or before the start year), and searched otherwise; only a searched outcome prices candidates. combinationsEvaluated is one plus the claim candidates decisions/generators.ts#socialSecurityClaimGenerator produced (for each of the first two streams whose claim is not already made, the three canonical claim ages 62y0m, the person's own full retirement age computed from the birth year and labelled FRA, and 70y0m, skipping the stream's own current claim age and any age whose claim year is before the start year), currentClaimExactEstate is the exact after-tax estate of the optimum at the current claim ages, and jointExactEstate is the exact after-tax estate of the best evaluated claim-and-schedule pair, which stays the current-claim estate unless a candidate beats it by more than DEFAULT_CLAIM_SWITCH_MARGIN_DOLLARS, which is 1000 dollars. The claims held fixed are published as alreadyClaimed.

    Formula: combinationsEvaluated = 1 + generatedClaimCandidates (0 candidates unless the outcome is searched); jointExactEstate = the best pair estate that clears currentClaimExactEstate + 1000, else currentClaimExactEstate; winningClaimLabel and winningClaimPatch are null exactly when no pair clears it

    • generatedClaimCandidates: Claim candidates from socialSecurityClaimGenerator: for each open stream (at most 2), the canonical ages that differ from its current claim and are not before the start year (unit: count) (domain: integer 0 to 6)
    • currentClaimExactEstate: Exact after-tax estate of the optimum at the plan's current claim ages (unit: nominal USD at horizon) (domain: finite)
    • jointExactEstate: Exact after-tax estate of the winning claim-and-schedule pair (unit: nominal USD at horizon) (domain: finite)
    • claimSwitchMarginDollars: DEFAULT_CLAIM_SWITCH_MARGIN_DOLLARS (unit: nominal USD) (domain: 1000)

    Timing: one full conversion optimum per claim combination, the current claim included

    Rounding: none; the count is an integer and the estates carry binary floating-point representation

    Limits:

    • The count includes the current claim and excludes the stream's own current age as a candidate: an open stream claiming at 70y0m generates two candidates and so evaluates three combinations, and a plan with no stream, or whose claims are all made, still evaluates one. The middle point is the person's own full retirement age from socialSecurity/nra.ts#fraForBirthYear. The two estates are RUN-PINNED, not derived: the worksheet states no dollar figure for either, so the test's example carries the value one execution of the co-optimizer produced on the stated plan, and it is evidence of that execution rather than of an independent derivation. What is derived is their equality: with no traditional balance every schedule is empty and no claim clears the 1000-dollar margin, so the joint estate IS the current-claim estate and both claim outputs are null. The strictness of that margin at exactly 1000 dollars was NOT constructed: it needs a candidate whose exact estate sits one margin above the current-claim optimum, which no plan input the worksheet states can produce
    • The two estate dollars (1,130,409.20 in the current-claim-wins case) stay pinned from one engine run. Nothing outside the engine derives them, so the test shows only that the run has not changed, not that the figure is right; the independent review of 2026-09-29 asked for an outside derivation, and none is made
    • It differs from the Social Security page's whole-plan sweep (social-security-claim-age-sweep): it re-optimizes conversions for each candidate but tries only 62, full retirement age and 70, one stream at a time, compares on the estate alone under every objective, and needs a 1000-dollar margin; the sweep holds the plan's conversions and tries every whole year. Adding the sweep's winner as one more candidate is a later change
    • A claim already made, and a canonical age whose claim year is before the start year, is not offered: an early claim cannot be paid for months before its application (20 CFR 404.621(a)(3)), and a later one only six months back ((a)(2)); withdrawal within 12 months and voluntary suspension from full retirement age are not modeled
    • Where the premium tax credit cannot be priced in a year of the plan as entered, no claim candidate is priced, since a claim age moves the income that credit depends on and the ledger cannot say which way; the conversion side keeps its own refusal (the tournament's ACA veto)
    • Its streams are the first two Social Security streams, where the page's sweep takes the first two with a benefit (socialSecurity/openClaims.ts#claimAgeStreams: a positive entered PIA or an earnings history). A plan with a zero-PIA stream among its first two, on which the ledger can still pay a spouse benefit, is searched differently on the two pages, and a past claim on such a stream is named on the Optimize card only; no example plan has one

    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: claim-age-co-optimization.md Mutation receipt: claim-age-co-optimization.mutation.md

    Evidence tests: optimizePlan.evidence.test.ts

    Implemented in: optimizePlan.ts; generators.ts; openClaims.ts

    Is the number of: claim-age-co-optimization-combinations-evaluated, claim-age-co-optimization-current-claim-exact-estate, claim-age-co-optimization-joint-exact-estate

    Feeds: optimizer-recommended-conversion-annual

  • Conversion coordinate descent search Model record, justified by derivation

    Purpose: Refine an annual conversion schedule through deterministic local search.

    Statement: decisions/search.ts#refineConversionSchedule deterministically searches annual Roth-conversion dollars by fixed-order coordinate descent, first coarse then fine steps, retaining only hard-constraint-feasible moves whose full-projection primary metric improves by more than the minimum, subject to simulation and sweep caps.

    Formula: for coarse then fine step, for each year coordinate try amount +/- step; retain a feasible candidate only when score > incumbent + minimumImprovement

    • schedule: Annual conversion amounts (unit: nominal USD) (domain: nonnegative)
    • step: Coarse/fine move size (unit: USD) (domain: positive)
    • score: Objective metric (unit: objective dollars) (domain: finite)

    Timing: deterministic annual-coordinate order under simulation and sweep budgets

    Rounding: schedule cents; no statistical tolerance

    Limits:

    • Local search only; no global-optimum claim. The worksheet score table is injected at the evaluation seam with a four-evaluation budget covering exactly its four entries; any unspecified probe is refused rather than guessed. This proves search selection only. The returned aggregate schedule still passes downstream legal-owner/action readiness and publication gates, so it feeds rather than publishes optimizer recommendations.

    Provenance: derived by codex, implemented by codex.

    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: conversion-coordinate-descent-search.md Mutation receipt: conversion-coordinate-descent-search.mutation.md

    Evidence tests: search.evidence.test.ts

    Implemented in: search.ts

    Feeds: optimizer-recommended-conversion-annual, optimizer-schedule-conversion-total

Engine source: engine/src/projection/optimizePlan.ts (ExactLedgerTournament.winnerConversions[].amount / ExactLedgerScheduleAdjustment.requested, executed, cleaned)

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.