@retiregolden/engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +79 -0
- package/dist/allocation/assetClasses.d.ts +94 -0
- package/dist/allocation/assetClasses.js +215 -0
- package/dist/decisions/annuitization.d.ts +72 -0
- package/dist/decisions/annuitization.js +184 -0
- package/dist/decisions/decisionFixtures.d.ts +48 -0
- package/dist/decisions/decisionFixtures.js +253 -0
- package/dist/decisions/evaluateCandidate.d.ts +61 -0
- package/dist/decisions/evaluateCandidate.js +193 -0
- package/dist/decisions/generators.d.ts +110 -0
- package/dist/decisions/generators.js +667 -0
- package/dist/decisions/index.d.ts +17 -0
- package/dist/decisions/index.js +17 -0
- package/dist/decisions/insightsAdapter.d.ts +27 -0
- package/dist/decisions/insightsAdapter.js +51 -0
- package/dist/decisions/objectives.d.ts +96 -0
- package/dist/decisions/objectives.js +319 -0
- package/dist/decisions/pensionElection.d.ts +112 -0
- package/dist/decisions/pensionElection.js +239 -0
- package/dist/decisions/search.d.ts +51 -0
- package/dist/decisions/search.js +110 -0
- package/dist/decisions/spendingSolver.d.ts +70 -0
- package/dist/decisions/spendingSolver.js +180 -0
- package/dist/decisions/spiaQuotes.d.ts +21 -0
- package/dist/decisions/spiaQuotes.js +43 -0
- package/dist/decisions/stochastic.d.ts +11 -0
- package/dist/decisions/stochastic.js +66 -0
- package/dist/decisions/swrComparator.d.ts +54 -0
- package/dist/decisions/swrComparator.js +89 -0
- package/dist/decisions/tournament.d.ts +53 -0
- package/dist/decisions/tournament.js +104 -0
- package/dist/decisions/types.d.ts +125 -0
- package/dist/decisions/types.js +12 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/insights/detectors/annuitizationHeadroom.d.ts +11 -0
- package/dist/insights/detectors/annuitizationHeadroom.js +77 -0
- package/dist/insights/detectors/assetLocation.d.ts +9 -0
- package/dist/insights/detectors/assetLocation.js +86 -0
- package/dist/insights/detectors/hecmBufferCandidate.d.ts +12 -0
- package/dist/insights/detectors/hecmBufferCandidate.js +76 -0
- package/dist/insights/detectors/incomeFloorFunded.d.ts +8 -0
- package/dist/insights/detectors/incomeFloorFunded.js +52 -0
- package/dist/insights/detectors/irmaaTierEdge.d.ts +2 -0
- package/dist/insights/detectors/irmaaTierEdge.js +102 -0
- package/dist/insights/detectors/pensionElectionPending.d.ts +11 -0
- package/dist/insights/detectors/pensionElectionPending.js +62 -0
- package/dist/insights/detectors/qcdEfficiency.d.ts +2 -0
- package/dist/insights/detectors/qcdEfficiency.js +62 -0
- package/dist/insights/detectors/rothBridgeHeadroom.d.ts +2 -0
- package/dist/insights/detectors/rothBridgeHeadroom.js +77 -0
- package/dist/insights/detectors/spendingGuardrails.d.ts +2 -0
- package/dist/insights/detectors/spendingGuardrails.js +77 -0
- package/dist/insights/detectors/spendingHeadroom.d.ts +12 -0
- package/dist/insights/detectors/spendingHeadroom.js +92 -0
- package/dist/insights/detectors/ssBridgeGap.d.ts +9 -0
- package/dist/insights/detectors/ssBridgeGap.js +92 -0
- package/dist/insights/detectors/stateRelocation.d.ts +12 -0
- package/dist/insights/detectors/stateRelocation.js +99 -0
- package/dist/insights/detectors/widowsPenalty.d.ts +12 -0
- package/dist/insights/detectors/widowsPenalty.js +145 -0
- package/dist/insights/registry.d.ts +4 -0
- package/dist/insights/registry.js +62 -0
- package/dist/insights/runInsights.d.ts +6 -0
- package/dist/insights/runInsights.js +15 -0
- package/dist/insights/types.d.ts +70 -0
- package/dist/insights/types.js +1 -0
- package/dist/ladder/bridge.d.ts +72 -0
- package/dist/ladder/bridge.js +59 -0
- package/dist/ladder/fedInvest.d.ts +55 -0
- package/dist/ladder/fedInvest.js +77 -0
- package/dist/ladder/fundedRatio.d.ts +41 -0
- package/dist/ladder/fundedRatio.js +46 -0
- package/dist/ladder/ladderMath.d.ts +88 -0
- package/dist/ladder/ladderMath.js +140 -0
- package/dist/longevity/ssaPeriod2022.d.ts +10 -0
- package/dist/longevity/ssaPeriod2022.js +51 -0
- package/dist/longevity/types.d.ts +39 -0
- package/dist/longevity/types.js +1 -0
- package/dist/model/migrations.d.ts +20 -0
- package/dist/model/migrations.js +67 -0
- package/dist/model/plan.d.ts +3108 -0
- package/dist/model/plan.js +1514 -0
- package/dist/montecarlo/frontiers.d.ts +22 -0
- package/dist/montecarlo/frontiers.js +61 -0
- package/dist/montecarlo/historicalReturns.d.ts +27 -0
- package/dist/montecarlo/historicalReturns.js +124 -0
- package/dist/montecarlo/historicalSuites.d.ts +45 -0
- package/dist/montecarlo/historicalSuites.js +102 -0
- package/dist/montecarlo/ltcShock.d.ts +42 -0
- package/dist/montecarlo/ltcShock.js +62 -0
- package/dist/montecarlo/marketModels.d.ts +248 -0
- package/dist/montecarlo/marketModels.js +672 -0
- package/dist/montecarlo/mortality.d.ts +31 -0
- package/dist/montecarlo/mortality.js +61 -0
- package/dist/montecarlo/riskBasedGuardrails.d.ts +93 -0
- package/dist/montecarlo/riskBasedGuardrails.js +200 -0
- package/dist/montecarlo/rng.d.ts +18 -0
- package/dist/montecarlo/rng.js +44 -0
- package/dist/montecarlo/run.d.ts +210 -0
- package/dist/montecarlo/run.js +353 -0
- package/dist/montecarlo/sharedPaths.d.ts +43 -0
- package/dist/montecarlo/sharedPaths.js +28 -0
- package/dist/montecarlo/survival.d.ts +49 -0
- package/dist/montecarlo/survival.js +126 -0
- package/dist/params/data/realYieldCurve2026.d.ts +17 -0
- package/dist/params/data/realYieldCurve2026.js +26 -0
- package/dist/params/data/year2026.d.ts +14 -0
- package/dist/params/data/year2026.js +199 -0
- package/dist/params/index.d.ts +63 -0
- package/dist/params/index.js +134 -0
- package/dist/params/provenance.d.ts +29 -0
- package/dist/params/provenance.js +123 -0
- package/dist/params/state/data/year2026.d.ts +18 -0
- package/dist/params/state/data/year2026.js +581 -0
- package/dist/params/state/index.d.ts +13 -0
- package/dist/params/state/index.js +26 -0
- package/dist/params/state/types.d.ts +73 -0
- package/dist/params/state/types.js +10 -0
- package/dist/params/types.d.ts +201 -0
- package/dist/params/types.js +7 -0
- package/dist/projection/annuityForms.d.ts +62 -0
- package/dist/projection/annuityForms.js +87 -0
- package/dist/projection/compare.d.ts +96 -0
- package/dist/projection/compare.js +216 -0
- package/dist/projection/flatTax.d.ts +7 -0
- package/dist/projection/flatTax.js +14 -0
- package/dist/projection/optimizePlan.d.ts +328 -0
- package/dist/projection/optimizePlan.js +1039 -0
- package/dist/projection/relocation.d.ts +149 -0
- package/dist/projection/relocation.js +317 -0
- package/dist/projection/simulate.d.ts +61 -0
- package/dist/projection/simulate.js +2754 -0
- package/dist/projection/types.d.ts +381 -0
- package/dist/projection/types.js +9 -0
- package/dist/rmd/jointLifeTable.d.ts +8 -0
- package/dist/rmd/jointLifeTable.js +663 -0
- package/dist/rmd/rmd.d.ts +29 -0
- package/dist/rmd/rmd.js +34 -0
- package/dist/scenarios/scenarios.d.ts +70 -0
- package/dist/scenarios/scenarios.js +130 -0
- package/dist/socialSecurity/benefitFactor.d.ts +13 -0
- package/dist/socialSecurity/benefitFactor.js +40 -0
- package/dist/socialSecurity/claimFactor.d.ts +33 -0
- package/dist/socialSecurity/claimFactor.js +58 -0
- package/dist/socialSecurity/disability.d.ts +36 -0
- package/dist/socialSecurity/disability.js +42 -0
- package/dist/socialSecurity/familyMaximum.d.ts +38 -0
- package/dist/socialSecurity/familyMaximum.js +48 -0
- package/dist/socialSecurity/maritalBenefits.d.ts +57 -0
- package/dist/socialSecurity/maritalBenefits.js +87 -0
- package/dist/socialSecurity/nra.d.ts +30 -0
- package/dist/socialSecurity/nra.js +83 -0
- package/dist/socialSecurity/piaFromEarnings.d.ts +100 -0
- package/dist/socialSecurity/piaFromEarnings.js +202 -0
- package/dist/socialSecurity/ssaWageData.d.ts +68 -0
- package/dist/socialSecurity/ssaWageData.js +306 -0
- package/dist/socialSecurity/survivorBenefit.d.ts +61 -0
- package/dist/socialSecurity/survivorBenefit.js +61 -0
- package/dist/spending/abw.d.ts +57 -0
- package/dist/spending/abw.js +80 -0
- package/dist/spending/flexibleGoals.d.ts +62 -0
- package/dist/spending/flexibleGoals.js +136 -0
- package/dist/spending/guardrails.d.ts +78 -0
- package/dist/spending/guardrails.js +100 -0
- package/dist/spending/layers.d.ts +81 -0
- package/dist/spending/layers.js +67 -0
- package/dist/spending/shapePresets.d.ts +42 -0
- package/dist/spending/shapePresets.js +77 -0
- package/dist/strategies/accountEligibility.d.ts +81 -0
- package/dist/strategies/accountEligibility.js +98 -0
- package/dist/strategies/inheritedIra.d.ts +45 -0
- package/dist/strategies/inheritedIra.js +43 -0
- package/dist/strategies/iraBasis.d.ts +48 -0
- package/dist/strategies/iraBasis.js +46 -0
- package/dist/strategies/optimizer.d.ts +307 -0
- package/dist/strategies/optimizer.js +551 -0
- package/dist/strategies/rothBasis.d.ts +67 -0
- package/dist/strategies/rothBasis.js +83 -0
- package/dist/strategies/rothConversion.d.ts +55 -0
- package/dist/strategies/rothConversion.js +83 -0
- package/dist/strategies/sepp.d.ts +38 -0
- package/dist/strategies/sepp.js +53 -0
- package/dist/tax/aca.d.ts +40 -0
- package/dist/tax/aca.js +82 -0
- package/dist/tax/federalTax.d.ts +110 -0
- package/dist/tax/federalTax.js +249 -0
- package/dist/tax/medicare.d.ts +20 -0
- package/dist/tax/medicare.js +36 -0
- package/dist/tax/propertySale.d.ts +46 -0
- package/dist/tax/propertySale.js +35 -0
- package/dist/tax/stateTax.d.ts +66 -0
- package/dist/tax/stateTax.js +198 -0
- package/dist/testing/money.d.ts +7 -0
- package/dist/testing/money.js +17 -0
- package/dist/testing/planFixtures.d.ts +29 -0
- package/dist/testing/planFixtures.js +141 -0
- package/package.json +75 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-state income tax parameters (V5, "big levers"). One pack per tax year;
|
|
3
|
+
* a data-only refresh each fall. Models brackets, standard deduction, whether
|
|
4
|
+
* the state taxes Social Security, and the major age-based retirement-income
|
|
5
|
+
* exclusion. Per-state credits, local/city taxes, and income phase-outs of
|
|
6
|
+
* exclusions are out of scope.
|
|
7
|
+
*
|
|
8
|
+
* @see DOCS/features/taxes.md
|
|
9
|
+
*/
|
|
10
|
+
import type { FilingStatus, PerStatus } from '../types.js';
|
|
11
|
+
/** Marginal bracket: `ratePct` applies to taxable income above `lowerBound`. */
|
|
12
|
+
export interface StateTaxBracket {
|
|
13
|
+
lowerBound: number;
|
|
14
|
+
ratePct: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Exclusion of retirement income from state taxable income.
|
|
18
|
+
* - none: the state taxes retirement income like other ordinary income.
|
|
19
|
+
* - full: retirement income is entirely exempt (subject to minAge if set).
|
|
20
|
+
* - capped: each age-eligible person excludes up to capPerPerson.
|
|
21
|
+
*/
|
|
22
|
+
export interface StateRetirementExclusion {
|
|
23
|
+
kind: 'none' | 'full' | 'capped';
|
|
24
|
+
/** Annual cap per eligible person (kind 'capped'), in pack-year dollars. */
|
|
25
|
+
capPerPerson?: number;
|
|
26
|
+
/** Exclusion applies only to people at or above this age. */
|
|
27
|
+
minAge?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface StateTaxParams {
|
|
30
|
+
/** Two-letter code, e.g. 'KY'. */
|
|
31
|
+
code: string;
|
|
32
|
+
name: string;
|
|
33
|
+
/** Nine states levy no broad income tax → everything below is ignored. */
|
|
34
|
+
hasIncomeTax: boolean;
|
|
35
|
+
/** ~9 states tax Social Security benefits (to the federally taxable extent). */
|
|
36
|
+
taxesSocialSecurity: boolean;
|
|
37
|
+
/** Most states tax long-term gains as ordinary income. */
|
|
38
|
+
capitalGainsAsOrdinary: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Percent of modeled realized/net capital gain included in the state base.
|
|
41
|
+
* Defaults to 100 when `capitalGainsAsOrdinary` is true, else 0.
|
|
42
|
+
*/
|
|
43
|
+
capitalGainsTaxablePct?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Whether the state follows the federal capital-loss carryforward netting
|
|
46
|
+
* already applied by the ledger, or taxes only current-year realized gains
|
|
47
|
+
* and ignores prior-year carryforward offsets (PA-style planning case).
|
|
48
|
+
*/
|
|
49
|
+
capitalLossCarryforwardConformity?: 'federal' | 'currentYearOnly';
|
|
50
|
+
/** Source notes for state capital-gain/conformity treatment. */
|
|
51
|
+
capitalGainsNotes?: string;
|
|
52
|
+
capitalGainsSources?: string[];
|
|
53
|
+
standardDeduction: PerStatus<number>;
|
|
54
|
+
brackets: PerStatus<StateTaxBracket[]>;
|
|
55
|
+
/** Private pensions, annuities, traditional IRA/401(k), RMD, SEPP, and inherited distributions. */
|
|
56
|
+
retirementPrivate: StateRetirementExclusion;
|
|
57
|
+
/** Public civil-service / military pensions, where state law separates them. */
|
|
58
|
+
retirementPublic: StateRetirementExclusion;
|
|
59
|
+
/**
|
|
60
|
+
* True when the state has one all-retirement rule copied into both buckets
|
|
61
|
+
* (no separate public-pension law): a capped exclusion then applies once to
|
|
62
|
+
* the combined retirement income, never once per bucket.
|
|
63
|
+
*/
|
|
64
|
+
retirementRuleShared?: boolean;
|
|
65
|
+
/** Citation / modeled simplifications for the data-refresh workstream. */
|
|
66
|
+
notes?: string;
|
|
67
|
+
}
|
|
68
|
+
export interface StateTaxPack {
|
|
69
|
+
year: number;
|
|
70
|
+
/** Keyed by two-letter code. Absent states fall back to the flat override. */
|
|
71
|
+
states: Record<string, StateTaxParams>;
|
|
72
|
+
}
|
|
73
|
+
export type { FilingStatus };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-state income tax parameters (V5, "big levers"). One pack per tax year;
|
|
3
|
+
* a data-only refresh each fall. Models brackets, standard deduction, whether
|
|
4
|
+
* the state taxes Social Security, and the major age-based retirement-income
|
|
5
|
+
* exclusion. Per-state credits, local/city taxes, and income phase-outs of
|
|
6
|
+
* exclusions are out of scope.
|
|
7
|
+
*
|
|
8
|
+
* @see DOCS/features/taxes.md
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Annual parameter pack shape. One pack per tax year; values come from IRS,
|
|
3
|
+
* SSA, CMS, and HHS releases each fall. Refreshing a year is a data-only PR.
|
|
4
|
+
*
|
|
5
|
+
* Sources and the meaning of each figure: DOCS/domain/domain-rules-reference.md.
|
|
6
|
+
*/
|
|
7
|
+
export type FilingStatus = 'single' | 'marriedFilingJointly';
|
|
8
|
+
/** One point on the TIPS par real-yield curve. */
|
|
9
|
+
export interface RealYieldCurvePoint {
|
|
10
|
+
maturityYears: number;
|
|
11
|
+
/** Par real yield, percent per year (above inflation). */
|
|
12
|
+
realYieldPct: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* TIPS real-yield curve snapshot: the discount curve for ladder quotes and
|
|
16
|
+
* essential-spending present values. Points sorted ascending by maturity;
|
|
17
|
+
* the ladder engine interpolates linearly and holds the endpoints flat.
|
|
18
|
+
*/
|
|
19
|
+
export interface RealYieldCurve {
|
|
20
|
+
/** Publication date of the yields (shown as "curve as of" in the UI). */
|
|
21
|
+
asOfIso: string;
|
|
22
|
+
/** Human-readable provenance of the snapshot. */
|
|
23
|
+
source: string;
|
|
24
|
+
points: RealYieldCurvePoint[];
|
|
25
|
+
}
|
|
26
|
+
/** Marginal bracket: `ratePct` applies to taxable income above `lowerBound`. */
|
|
27
|
+
export interface TaxBracket {
|
|
28
|
+
lowerBound: number;
|
|
29
|
+
ratePct: number;
|
|
30
|
+
}
|
|
31
|
+
export interface PerStatus<T> {
|
|
32
|
+
single: T;
|
|
33
|
+
marriedFilingJointly: T;
|
|
34
|
+
}
|
|
35
|
+
export interface IrmaaTier {
|
|
36
|
+
/** Tier applies when MAGI (2-year lookback) exceeds this. */
|
|
37
|
+
magiOver: PerStatus<number>;
|
|
38
|
+
/**
|
|
39
|
+
* Statutory share of program cost (35/50/65/80/85). Standard premium is 25%,
|
|
40
|
+
* so Part B total = partBStandardMonthly * applicablePct / 25.
|
|
41
|
+
*/
|
|
42
|
+
applicablePct: number;
|
|
43
|
+
/** Published monthly Part D surcharge. null = not yet verified for this year. */
|
|
44
|
+
partDSurchargeMonthly: number | null;
|
|
45
|
+
}
|
|
46
|
+
export interface ParameterPack {
|
|
47
|
+
year: number;
|
|
48
|
+
federalTax: {
|
|
49
|
+
brackets: PerStatus<TaxBracket[]>;
|
|
50
|
+
standardDeduction: PerStatus<number>;
|
|
51
|
+
/** Additional standard deduction at 65+ (per qualifying person for MFJ). */
|
|
52
|
+
age65Addition: PerStatus<number>;
|
|
53
|
+
/**
|
|
54
|
+
* SALT (state + local + property tax) itemized-deduction cap. OBBBA raised it
|
|
55
|
+
* to $40k for 2025 (+1%/yr through 2029, reverting to $10k in 2030). The
|
|
56
|
+
* high-income (>$500k MAGI) phase-out toward a $10k floor is not modeled.
|
|
57
|
+
*/
|
|
58
|
+
saltCap: number;
|
|
59
|
+
/**
|
|
60
|
+
* Net capital loss deductible against ordinary income per year ($3,000;
|
|
61
|
+
* $1,500 MFS — not modeled). Statutory, never indexed since 1978; the
|
|
62
|
+
* remainder carries forward indefinitely.
|
|
63
|
+
*/
|
|
64
|
+
capitalLossOrdinaryOffsetLimit: number;
|
|
65
|
+
/**
|
|
66
|
+
* IRC §121 primary-residence gain exclusion caps ($250k single / $500k
|
|
67
|
+
* joint). Statutory, unchanged since 1997 and never inflation-indexed —
|
|
68
|
+
* do not apply limitScale/inflation projection to these.
|
|
69
|
+
*/
|
|
70
|
+
section121Exclusion: PerStatus<number>;
|
|
71
|
+
/** OBBBA senior deduction (2025–2028): per person 65+, MAGI phase-out. */
|
|
72
|
+
seniorDeduction: {
|
|
73
|
+
amountPerPerson: number;
|
|
74
|
+
magiPhaseOutStart: PerStatus<number>;
|
|
75
|
+
phaseOutRatePct: number;
|
|
76
|
+
lastApplicableYear: number;
|
|
77
|
+
} | null;
|
|
78
|
+
/** Planning-grade individual AMT screen (Form 6251 thresholds). */
|
|
79
|
+
amt: {
|
|
80
|
+
exemption: PerStatus<number>;
|
|
81
|
+
exemptionPhaseOutStart: PerStatus<number>;
|
|
82
|
+
exemptionPhaseOutRatePct: number;
|
|
83
|
+
rate28StartsAbove: number;
|
|
84
|
+
rate26Pct: number;
|
|
85
|
+
rate28Pct: number;
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
capitalGains: {
|
|
89
|
+
/** Taxable-income thresholds where the 15% and 20% LTCG rates begin. */
|
|
90
|
+
rate15StartsAbove: PerStatus<number>;
|
|
91
|
+
rate20StartsAbove: PerStatus<number>;
|
|
92
|
+
};
|
|
93
|
+
niit: {
|
|
94
|
+
ratePct: number;
|
|
95
|
+
/** MAGI thresholds; statutorily unindexed. */
|
|
96
|
+
magiThreshold: PerStatus<number>;
|
|
97
|
+
};
|
|
98
|
+
/** Provisional-income thresholds for Social Security benefit taxation (unindexed). */
|
|
99
|
+
ssBenefitTaxation: {
|
|
100
|
+
tier50Start: PerStatus<number>;
|
|
101
|
+
tier85Start: PerStatus<number>;
|
|
102
|
+
};
|
|
103
|
+
contributionLimits: {
|
|
104
|
+
employee401k: number;
|
|
105
|
+
catchUp50: number;
|
|
106
|
+
/** SECURE 2.0 ages 60–63 "super" catch-up (replaces catchUp50). */
|
|
107
|
+
superCatchUp60to63: number;
|
|
108
|
+
/** Prior-year FICA wages above this force catch-ups to be Roth. */
|
|
109
|
+
rothCatchUpWageThreshold: number;
|
|
110
|
+
ira: number;
|
|
111
|
+
iraCatchUp50: number;
|
|
112
|
+
hsaSelfOnly: number;
|
|
113
|
+
hsaFamily: number;
|
|
114
|
+
hsaCatchUp55: number;
|
|
115
|
+
/** IRS Section 415(c)(1)(A) defined contribution total additions limit. */
|
|
116
|
+
section415cLimit: number;
|
|
117
|
+
};
|
|
118
|
+
rmd: {
|
|
119
|
+
/** IRS Uniform Lifetime Table (Pub 590-B, 2022+): age -> divisor. */
|
|
120
|
+
uniformLifetimeTable: Record<number, number>;
|
|
121
|
+
/** Annual QCD exclusion limit. */
|
|
122
|
+
qcdAnnualLimit: number;
|
|
123
|
+
};
|
|
124
|
+
/** Fixed-annuity purchase planning parameters (guaranteed-income depth). */
|
|
125
|
+
annuities: {
|
|
126
|
+
/**
|
|
127
|
+
* QLAC (Qualified Longevity Annuity Contract) maximum premium that a
|
|
128
|
+
* qualified purchase may exclude from the owner's RMD balances. SECURE 2.0
|
|
129
|
+
* removed the 25%-of-balance limit, leaving a flat, inflation-indexed dollar
|
|
130
|
+
* cap. Premium above the cap is not QLAC-eligible.
|
|
131
|
+
*/
|
|
132
|
+
qlacPremiumCap: number;
|
|
133
|
+
/**
|
|
134
|
+
* IRS Pub 939 Table V (ordinary life annuity, one life) expected-return
|
|
135
|
+
* multiples: age at the annuity starting date → remaining life expectancy
|
|
136
|
+
* in years. Used to derive the non-qualified exclusion ratio
|
|
137
|
+
* (investment-in-contract ÷ expected return). Ages between table entries
|
|
138
|
+
* interpolate linearly; ages outside the table clamp to the endpoints.
|
|
139
|
+
*/
|
|
140
|
+
expectedReturnMultiples: Record<number, number>;
|
|
141
|
+
};
|
|
142
|
+
/** HECM (reverse mortgage) planning parameters (annuity-pension-and-home-equity decisions). */
|
|
143
|
+
hecm: {
|
|
144
|
+
/**
|
|
145
|
+
* Published principal-limit factors (percent of home value available as
|
|
146
|
+
* the initial principal limit) by age of the youngest borrower, at the
|
|
147
|
+
* expected rate below. Used only as the planning default when the user
|
|
148
|
+
* has not entered a lender-quoted line size; ages between entries
|
|
149
|
+
* interpolate linearly and ages outside clamp to the endpoints.
|
|
150
|
+
*/
|
|
151
|
+
principalLimitFactorPctByAge: Record<number, number>;
|
|
152
|
+
/** Expected interest rate the factor table was published at (context for the UI). */
|
|
153
|
+
plfExpectedRatePct: number;
|
|
154
|
+
/** Planning default for line/loan growth (note rate + annual MIP). */
|
|
155
|
+
defaultGrowthRatePct: number;
|
|
156
|
+
};
|
|
157
|
+
medicare: {
|
|
158
|
+
partBStandardMonthly: number;
|
|
159
|
+
/** Ascending by threshold. */
|
|
160
|
+
irmaaTiers: IrmaaTier[];
|
|
161
|
+
};
|
|
162
|
+
socialSecurity: {
|
|
163
|
+
colaPct: number;
|
|
164
|
+
taxableWageBase: number;
|
|
165
|
+
/** Annual exempt amounts for the retirement earnings test. */
|
|
166
|
+
earningsTestBelowFraAnnual: number;
|
|
167
|
+
earningsTestFraYearAnnual: number;
|
|
168
|
+
/**
|
|
169
|
+
* Substantial Gainful Activity (SGA) monthly earnings limit for SSDI: wages
|
|
170
|
+
* above this generally stop disability benefits. (Statutorily blind SGA is
|
|
171
|
+
* higher; not modeled.) @see DOCS/domain/domain-rules-reference.md §4 SSDI
|
|
172
|
+
*/
|
|
173
|
+
sgaMonthlyNonBlind: number;
|
|
174
|
+
/**
|
|
175
|
+
* Employee-side OASDI payroll-tax rate (%). The employer pays the same again;
|
|
176
|
+
* the self-employed pay double (12.4%). Used only for the "what you paid in
|
|
177
|
+
* vs. what you get back" education readout (no working-years tax is modeled in
|
|
178
|
+
* the projection). Statutory since 1990; applied uniformly over the career
|
|
179
|
+
* (historical rate drift pre-1990 is a documented simplification).
|
|
180
|
+
*/
|
|
181
|
+
oasdiEmployeeRatePct: number;
|
|
182
|
+
};
|
|
183
|
+
/** HHS poverty guideline (48 contiguous states) used for ACA in this coverage year. */
|
|
184
|
+
federalPovertyLine: {
|
|
185
|
+
firstPerson: number;
|
|
186
|
+
perAdditionalPerson: number;
|
|
187
|
+
};
|
|
188
|
+
/** ACA premium-tax-credit scale (post-2025: enhanced credits expired, 400% FPL cliff restored). */
|
|
189
|
+
aca: {
|
|
190
|
+
/**
|
|
191
|
+
* Expected-contribution percentage of MAGI, piecewise-linear between
|
|
192
|
+
* breakpoints sorted by fplPct. Income above maxFplPctForCredit gets no
|
|
193
|
+
* credit at all (the cliff).
|
|
194
|
+
*/
|
|
195
|
+
applicablePctBreakpoints: Array<{
|
|
196
|
+
fplPct: number;
|
|
197
|
+
applicablePct: number;
|
|
198
|
+
}>;
|
|
199
|
+
maxFplPctForCredit: number;
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Annuity payout forms: payment continuation and exclusion-ratio inputs per
|
|
3
|
+
* form (annuity-pension-and-home-equity decisions, step 1).
|
|
4
|
+
*
|
|
5
|
+
* The IRS Pub 939 General Rule prices a non-qualified annuity's tax-free share
|
|
6
|
+
* as investment-in-contract ÷ expected return, where expected return = annual
|
|
7
|
+
* payment × an expected-return multiple. The multiple depends on the payout
|
|
8
|
+
* form:
|
|
9
|
+
* - life-only: Pub 939 Table V by age at the starting date (embedded in the
|
|
10
|
+
* parameter pack — the shipped guaranteed-income behavior);
|
|
11
|
+
* - life with N years certain: Pub 939 adjusts the investment for the value
|
|
12
|
+
* of the refund/period-certain feature (Tables III/VII). We approximate the
|
|
13
|
+
* same economics from the payment side: the expected payment stream can
|
|
14
|
+
* never be shorter than the guarantee, so the multiple is
|
|
15
|
+
* max(Table V multiple, N). Documented planning-grade approximation;
|
|
16
|
+
* - joint & survivor: Pub 939 uses Tables VI/VIA (joint last-survivor and
|
|
17
|
+
* joint-life multiples). We decompose by expectation instead — exactly the
|
|
18
|
+
* stream the ledger pays: the full payment for the owner's expected
|
|
19
|
+
* lifetime (Table V) plus the survivor share for the years the joint
|
|
20
|
+
* annuitant is expected to outlive the owner (joint last-survivor
|
|
21
|
+
* expectancy from the SSA-derived mortality model, per-sex where Pub 939's
|
|
22
|
+
* tables are unisex). Same General-Rule method, sourced from the tables
|
|
23
|
+
* already in the repo.
|
|
24
|
+
*
|
|
25
|
+
* In all forms the exclusion ratio is fixed at the starting date and the
|
|
26
|
+
* excludable share of each payment continues to a survivor/beneficiary until
|
|
27
|
+
* the investment is recovered (IRS Pub 575/939 treatment).
|
|
28
|
+
*/
|
|
29
|
+
import type { Account, AnnuityPayoutForm, Person } from '../model/plan.js';
|
|
30
|
+
import type { ParameterPack } from '../params/types.js';
|
|
31
|
+
type AnnuityAccount = Extract<Account, {
|
|
32
|
+
type: 'annuity';
|
|
33
|
+
}>;
|
|
34
|
+
/** The payout form in effect; absent = life-only (legacy behavior). */
|
|
35
|
+
export declare function annuityPayoutForm(account: AnnuityAccount): AnnuityPayoutForm;
|
|
36
|
+
/**
|
|
37
|
+
* Expected-return multiple (years of the FULL annual payment) for the
|
|
38
|
+
* exclusion ratio under the account's payout form. `owner` is the annuitant;
|
|
39
|
+
* `joint` is the other household member for a joint-and-survivor form (the
|
|
40
|
+
* form is validated to require a two-person household).
|
|
41
|
+
*/
|
|
42
|
+
export declare function annuityExclusionMultiple(pack: ParameterPack, account: AnnuityAccount, owner: Person, joint: Person | undefined): number;
|
|
43
|
+
/**
|
|
44
|
+
* Fraction of the full payment paid this year under the payout form.
|
|
45
|
+
* - life-only: 1 while the owner is alive, else 0;
|
|
46
|
+
* - period certain: 1 while the owner is alive; after the owner's death the
|
|
47
|
+
* remaining guaranteed years continue to the household (any member alive).
|
|
48
|
+
* Documented simplification: if the whole household dies inside the
|
|
49
|
+
* guarantee window, the remaining certain payments (which a real contract
|
|
50
|
+
* would keep paying to a beneficiary or the estate) are not modeled — the
|
|
51
|
+
* ledger has no post-household cash-flow path, so a long guarantee's
|
|
52
|
+
* estate value is understated in that case (domain rules §19);
|
|
53
|
+
* - joint & survivor: 1 while the owner is alive, survivorPct% while the
|
|
54
|
+
* joint annuitant survives them.
|
|
55
|
+
*/
|
|
56
|
+
export declare function annuityPayoutFraction(form: AnnuityPayoutForm, state: {
|
|
57
|
+
ownerAlive: boolean;
|
|
58
|
+
otherAlive: boolean;
|
|
59
|
+
anyAlive: boolean;
|
|
60
|
+
yearsSinceStart: number;
|
|
61
|
+
}): number;
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Annuity payout forms: payment continuation and exclusion-ratio inputs per
|
|
3
|
+
* form (annuity-pension-and-home-equity decisions, step 1).
|
|
4
|
+
*
|
|
5
|
+
* The IRS Pub 939 General Rule prices a non-qualified annuity's tax-free share
|
|
6
|
+
* as investment-in-contract ÷ expected return, where expected return = annual
|
|
7
|
+
* payment × an expected-return multiple. The multiple depends on the payout
|
|
8
|
+
* form:
|
|
9
|
+
* - life-only: Pub 939 Table V by age at the starting date (embedded in the
|
|
10
|
+
* parameter pack — the shipped guaranteed-income behavior);
|
|
11
|
+
* - life with N years certain: Pub 939 adjusts the investment for the value
|
|
12
|
+
* of the refund/period-certain feature (Tables III/VII). We approximate the
|
|
13
|
+
* same economics from the payment side: the expected payment stream can
|
|
14
|
+
* never be shorter than the guarantee, so the multiple is
|
|
15
|
+
* max(Table V multiple, N). Documented planning-grade approximation;
|
|
16
|
+
* - joint & survivor: Pub 939 uses Tables VI/VIA (joint last-survivor and
|
|
17
|
+
* joint-life multiples). We decompose by expectation instead — exactly the
|
|
18
|
+
* stream the ledger pays: the full payment for the owner's expected
|
|
19
|
+
* lifetime (Table V) plus the survivor share for the years the joint
|
|
20
|
+
* annuitant is expected to outlive the owner (joint last-survivor
|
|
21
|
+
* expectancy from the SSA-derived mortality model, per-sex where Pub 939's
|
|
22
|
+
* tables are unisex). Same General-Rule method, sourced from the tables
|
|
23
|
+
* already in the repo.
|
|
24
|
+
*
|
|
25
|
+
* In all forms the exclusion ratio is fixed at the starting date and the
|
|
26
|
+
* excludable share of each payment continues to a survivor/beneficiary until
|
|
27
|
+
* the investment is recovered (IRS Pub 575/939 treatment).
|
|
28
|
+
*/
|
|
29
|
+
import { annuityExpectedReturnMultiple } from '../params/index.js';
|
|
30
|
+
import { jointLastSurvivorExpectancy } from '../montecarlo/mortality.js';
|
|
31
|
+
/** The payout form in effect; absent = life-only (legacy behavior). */
|
|
32
|
+
export function annuityPayoutForm(account) {
|
|
33
|
+
return account.payoutForm ?? { kind: 'lifeOnly' };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Expected-return multiple (years of the FULL annual payment) for the
|
|
37
|
+
* exclusion ratio under the account's payout form. `owner` is the annuitant;
|
|
38
|
+
* `joint` is the other household member for a joint-and-survivor form (the
|
|
39
|
+
* form is validated to require a two-person household).
|
|
40
|
+
*/
|
|
41
|
+
export function annuityExclusionMultiple(pack, account, owner, joint) {
|
|
42
|
+
const form = annuityPayoutForm(account);
|
|
43
|
+
const lifeMultiple = annuityExpectedReturnMultiple(pack, account.startAge);
|
|
44
|
+
if (form.kind === 'lifeOnly')
|
|
45
|
+
return lifeMultiple;
|
|
46
|
+
if (form.kind === 'periodCertain') {
|
|
47
|
+
// The guarantee floors the expected payment years at the certain period.
|
|
48
|
+
return Math.max(lifeMultiple, form.certainYears);
|
|
49
|
+
}
|
|
50
|
+
if (!joint)
|
|
51
|
+
return lifeMultiple;
|
|
52
|
+
// Joint & survivor: full payment for the owner's expected lifetime, then
|
|
53
|
+
// survivorPct of it for the expected years the joint annuitant survives the
|
|
54
|
+
// owner. Ages at the starting date: the joint annuitant's age when the owner
|
|
55
|
+
// attains startAge.
|
|
56
|
+
const ownerBirthYear = Number(owner.dob.slice(0, 4));
|
|
57
|
+
const jointBirthYear = Number(joint.dob.slice(0, 4));
|
|
58
|
+
const jointAgeAtStart = account.startAge + (ownerBirthYear - jointBirthYear);
|
|
59
|
+
const jointExpectancy = jointLastSurvivorExpectancy(account.startAge, owner.sex, Math.max(0, jointAgeAtStart), joint.sex);
|
|
60
|
+
const survivorYears = Math.max(0, jointExpectancy - lifeMultiple);
|
|
61
|
+
return lifeMultiple + (form.survivorPct / 100) * survivorYears;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Fraction of the full payment paid this year under the payout form.
|
|
65
|
+
* - life-only: 1 while the owner is alive, else 0;
|
|
66
|
+
* - period certain: 1 while the owner is alive; after the owner's death the
|
|
67
|
+
* remaining guaranteed years continue to the household (any member alive).
|
|
68
|
+
* Documented simplification: if the whole household dies inside the
|
|
69
|
+
* guarantee window, the remaining certain payments (which a real contract
|
|
70
|
+
* would keep paying to a beneficiary or the estate) are not modeled — the
|
|
71
|
+
* ledger has no post-household cash-flow path, so a long guarantee's
|
|
72
|
+
* estate value is understated in that case (domain rules §19);
|
|
73
|
+
* - joint & survivor: 1 while the owner is alive, survivorPct% while the
|
|
74
|
+
* joint annuitant survives them.
|
|
75
|
+
*/
|
|
76
|
+
export function annuityPayoutFraction(form, state) {
|
|
77
|
+
if (form.kind === 'lifeOnly')
|
|
78
|
+
return state.ownerAlive ? 1 : 0;
|
|
79
|
+
if (form.kind === 'periodCertain') {
|
|
80
|
+
if (state.ownerAlive)
|
|
81
|
+
return 1;
|
|
82
|
+
return state.yearsSinceStart < form.certainYears && state.anyAlive ? 1 : 0;
|
|
83
|
+
}
|
|
84
|
+
if (state.ownerAlive)
|
|
85
|
+
return 1;
|
|
86
|
+
return state.otherAlive ? form.survivorPct / 100 : 0;
|
|
87
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projection summaries and the convert-vs-don't comparison (roadmap V3).
|
|
3
|
+
* Scenario diffing generalizes in V4; this covers the headline question.
|
|
4
|
+
*/
|
|
5
|
+
import type { Plan } from '../model/plan.js';
|
|
6
|
+
import { type SimulateOptions } from './simulate.js';
|
|
7
|
+
import type { ProjectionResult } from './types.js';
|
|
8
|
+
export type EstateDestination = 'spouse' | 'nonSpouse' | 'charity';
|
|
9
|
+
/** How one account's ending balance is discounted on the way to heirs/charity. */
|
|
10
|
+
export interface EstateAccountBreakdown {
|
|
11
|
+
accountId: string;
|
|
12
|
+
name: string;
|
|
13
|
+
category: 'cash' | 'taxable' | 'traditional' | 'roth' | 'hsa';
|
|
14
|
+
grossBalance: number;
|
|
15
|
+
destination: EstateDestination;
|
|
16
|
+
/** Pre-tax portion exposed to heir income tax (traditional net of 8606 basis; non-spouse HSA). */
|
|
17
|
+
taxablePretaxBase: number;
|
|
18
|
+
heirTaxRatePct: number;
|
|
19
|
+
/** Portion passing to charity, untaxed. */
|
|
20
|
+
charityAmount: number;
|
|
21
|
+
heirTax: number;
|
|
22
|
+
/** grossBalance − charityAmount − heirTax. */
|
|
23
|
+
netToHeirs: number;
|
|
24
|
+
}
|
|
25
|
+
export interface ProjectionSummary {
|
|
26
|
+
/** Sum of taxes and penalties over the whole projection. */
|
|
27
|
+
lifetimeTaxesAndPenalties: number;
|
|
28
|
+
lifetimeRothConversions: number;
|
|
29
|
+
endingInvestable: number;
|
|
30
|
+
endingNetWorth: number;
|
|
31
|
+
/**
|
|
32
|
+
* Ending net worth net of the income tax heirs owe on inherited pre-tax
|
|
33
|
+
* (traditional) balances, at the plan's assumed heir tax rate. Nondeductible
|
|
34
|
+
* (after-tax) IRA basis remaining at the horizon is excluded — the heir
|
|
35
|
+
* inherits it tax-free and files a separate Form 8606. Roth, taxable
|
|
36
|
+
* (stepped-up at death), cash, and property are treated as passing through
|
|
37
|
+
* untaxed — the standard simplification for an estate comparison. An HSA
|
|
38
|
+
* left to a non-spouse beneficiary is fully taxable in the death year (IRC
|
|
39
|
+
* §223(f)(8)(B)), so its ending balance is also taxed at the heir rate; an
|
|
40
|
+
* HSA inherited by a spouse (or with no beneficiary set) passes untaxed.
|
|
41
|
+
*/
|
|
42
|
+
endingAfterTaxEstate: number;
|
|
43
|
+
/** Total heir income tax discounted from the estate (sum of the breakdown's heirTax). */
|
|
44
|
+
endingEstateHeirTax: number;
|
|
45
|
+
/** Total passing to charity untaxed (sum of the breakdown's charityAmount). */
|
|
46
|
+
endingEstateToCharity: number;
|
|
47
|
+
/** Per-account estate discounting, so the summary can explain each class's haircut. */
|
|
48
|
+
estateBreakdown: EstateAccountBreakdown[];
|
|
49
|
+
/** End-of-plan balance by account category. */
|
|
50
|
+
endingByCategory: {
|
|
51
|
+
cash: number;
|
|
52
|
+
taxable: number;
|
|
53
|
+
traditional: number;
|
|
54
|
+
roth: number;
|
|
55
|
+
hsa: number;
|
|
56
|
+
};
|
|
57
|
+
depletionYear: number | null;
|
|
58
|
+
warnings: string[];
|
|
59
|
+
savingsRates: Array<{
|
|
60
|
+
year: number;
|
|
61
|
+
ratePct: number;
|
|
62
|
+
}>;
|
|
63
|
+
averagePreRetirementSavingsRatePct: number;
|
|
64
|
+
fiNumber: number;
|
|
65
|
+
fiYear: number | null;
|
|
66
|
+
fiAge: number | null;
|
|
67
|
+
coastFireNumber: number;
|
|
68
|
+
}
|
|
69
|
+
export declare function summarizeProjection(plan: Plan, result: ProjectionResult): ProjectionSummary;
|
|
70
|
+
export interface RothComparison {
|
|
71
|
+
withConversions: ProjectionSummary;
|
|
72
|
+
withoutConversions: ProjectionSummary;
|
|
73
|
+
}
|
|
74
|
+
/** Runs the plan as-is and with conversions disabled, for side-by-side display. */
|
|
75
|
+
export declare function compareRothConversion(plan: Plan, opts: SimulateOptions): RothComparison;
|
|
76
|
+
export interface LtcStressComparison {
|
|
77
|
+
/** No care episode, LTC policies held out — the premium-neutral baseline. */
|
|
78
|
+
noCare: ProjectionSummary;
|
|
79
|
+
/** The care episode occurs and is fully self-funded (LTC policies removed). */
|
|
80
|
+
careUninsured: ProjectionSummary;
|
|
81
|
+
/** The care episode occurs with the plan's LTC policies (premiums + benefits). */
|
|
82
|
+
careInsured: ProjectionSummary;
|
|
83
|
+
hasCareEvents: boolean;
|
|
84
|
+
hasLtcPolicy: boolean;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The "LTC stress" comparison: how a deterministic care episode hits the plan,
|
|
88
|
+
* and what the LTC policy does about it.
|
|
89
|
+
*
|
|
90
|
+
* noCare and careUninsured both hold LTC out, so they differ only by the care
|
|
91
|
+
* episode — `noCare − careUninsured` is the raw care shock, free of premium
|
|
92
|
+
* effects. careInsured adds the policies back, so `careInsured − careUninsured`
|
|
93
|
+
* is the policy's value *net of its premiums* (it can be negative if lifetime
|
|
94
|
+
* premiums exceed the benefits paid). The UI labels it as such.
|
|
95
|
+
*/
|
|
96
|
+
export declare function compareLtcStress(plan: Plan, opts: SimulateOptions): LtcStressComparison;
|