@retiregolden/planner-ui 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 +181 -0
- package/package.json +77 -0
- package/src/App.tsx +246 -0
- package/src/RouteErrorBoundary.tsx +45 -0
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/vite.svg +1 -0
- package/src/data/fedInvestClient.ts +113 -0
- package/src/data/localStore.ts +42 -0
- package/src/data/planOrigin.ts +24 -0
- package/src/data/planStore.ts +165 -0
- package/src/data/v2Backup.ts +101 -0
- package/src/import/ImportPage.tsx +347 -0
- package/src/import/ReviewChecklistView.tsx +38 -0
- package/src/import/brokerCsv.ts +395 -0
- package/src/import/csv.ts +133 -0
- package/src/import/genericCsv.ts +224 -0
- package/src/import/projectionLab.ts +350 -0
- package/src/import/reviewChecklist.ts +33 -0
- package/src/import/tenForty.ts +275 -0
- package/src/index.css +630 -0
- package/src/index.ts +16 -0
- package/src/learn/ArticleBody.tsx +78 -0
- package/src/learn/ArticlePage.tsx +57 -0
- package/src/learn/GlossaryPage.tsx +33 -0
- package/src/learn/LearnAboutScreen.tsx +41 -0
- package/src/learn/LearnCards.tsx +41 -0
- package/src/learn/LearnLink.tsx +91 -0
- package/src/learn/LearningCenterPage.tsx +114 -0
- package/src/learn/SourcesPage.tsx +98 -0
- package/src/learn/components/ArticleFigure.tsx +34 -0
- package/src/learn/components/ArticleShell.tsx +86 -0
- package/src/learn/components/ComparisonTable.tsx +42 -0
- package/src/learn/components/FormulaBlock.tsx +34 -0
- package/src/learn/components/PurchasingPowerChart.tsx +41 -0
- package/src/learn/components/RelatedArticles.tsx +27 -0
- package/src/learn/components/ScenarioCard.tsx +24 -0
- package/src/learn/components/SourceList.tsx +23 -0
- package/src/learn/components/charts.tsx +21 -0
- package/src/learn/content/about-retiregolden.ts +100 -0
- package/src/learn/content/aca-premium-tax-credits-and-magi.ts +103 -0
- package/src/learn/content/account-types-overview.ts +106 -0
- package/src/learn/content/after-tax-estate.ts +111 -0
- package/src/learn/content/agi-magi-and-taxable-income.ts +112 -0
- package/src/learn/content/appealing-irmaa-ssa-44.ts +95 -0
- package/src/learn/content/assumption-general-inflation.ts +82 -0
- package/src/learn/content/assumption-healthcare-inflation.ts +85 -0
- package/src/learn/content/assumption-heir-tax-rate.ts +79 -0
- package/src/learn/content/assumption-investment-returns.ts +90 -0
- package/src/learn/content/assumption-longevity-planning-age.ts +78 -0
- package/src/learn/content/assumption-recent-magi.ts +83 -0
- package/src/learn/content/assumption-social-security-cola.ts +89 -0
- package/src/learn/content/assumption-social-security-trust-fund.ts +83 -0
- package/src/learn/content/assumption-state-tax-override.ts +79 -0
- package/src/learn/content/beneficiaries-and-account-titling.ts +99 -0
- package/src/learn/content/break-even-useful-lens.ts +94 -0
- package/src/learn/content/building-a-retirement-spending-budget.ts +100 -0
- package/src/learn/content/cola-and-inflation-protection.ts +102 -0
- package/src/learn/content/divorced-spousal-and-survivor-records.ts +104 -0
- package/src/learn/content/dynamic-spending-guardrails.ts +90 -0
- package/src/learn/content/earnings-test-before-fra.ts +100 -0
- package/src/learn/content/employer-match-and-contribution-order.ts +104 -0
- package/src/learn/content/examplePlanArticles.ts +525 -0
- package/src/learn/content/fees-expense-ratios-and-compounding-drag.ts +98 -0
- package/src/learn/content/fi-number-and-four-percent-rule.ts +64 -0
- package/src/learn/content/filling-a-tax-bracket-with-roth-conversions.ts +98 -0
- package/src/learn/content/funded-ratio.ts +70 -0
- package/src/learn/content/healthcare-after-65.ts +103 -0
- package/src/learn/content/healthcare-before-65.ts +104 -0
- package/src/learn/content/historical-vs-random-return-models.ts +101 -0
- package/src/learn/content/how-assumptions-change-the-answer.ts +105 -0
- package/src/learn/content/how-much-can-i-spend.ts +105 -0
- package/src/learn/content/how-social-security-is-taxed.ts +95 -0
- package/src/learn/content/how-the-optimizer-thinks.ts +102 -0
- package/src/learn/content/how-the-optimizer-values-after-tax-estate.ts +97 -0
- package/src/learn/content/how-to-model-accumulation.ts +67 -0
- package/src/learn/content/how-to-read-a-retirement-projection.ts +115 -0
- package/src/learn/content/hsas-and-qualified-medical-expenses.ts +108 -0
- package/src/learn/content/hsas-as-retirement-accounts.ts +101 -0
- package/src/learn/content/inflation-risk.ts +98 -0
- package/src/learn/content/inherited-ira-10-year-rule.ts +105 -0
- package/src/learn/content/insurance-in-your-retirement-plan.ts +103 -0
- package/src/learn/content/irmaa-two-year-lookback.ts +99 -0
- package/src/learn/content/long-term-care-costs-and-insurance.ts +103 -0
- package/src/learn/content/long-term-care-insurance-as-risk-transfer.ts +98 -0
- package/src/learn/content/longevity-risk.ts +99 -0
- package/src/learn/content/marginal-vs-effective-tax-rate.ts +98 -0
- package/src/learn/content/medicare-part-b-vs-part-d-irmaa.ts +102 -0
- package/src/learn/content/mortality-weighted-social-security.ts +113 -0
- package/src/learn/content/moving-to-retiregolden.ts +86 -0
- package/src/learn/content/niit-high-income-investment-tax.ts +98 -0
- package/src/learn/content/ordinary-income-vs-capital-gains.ts +103 -0
- package/src/learn/content/paying-conversion-taxes-taxable-vs-ira.ts +102 -0
- package/src/learn/content/pensions-and-annuities.ts +101 -0
- package/src/learn/content/permanent-life-insurance-in-a-plan.ts +106 -0
- package/src/learn/content/pia-aime-and-bend-points.ts +103 -0
- package/src/learn/content/planner-overview.ts +106 -0
- package/src/learn/content/planning-for-couples-and-survivor-years.ts +108 -0
- package/src/learn/content/privacy-what-stays-in-your-browser.ts +99 -0
- package/src/learn/content/qcds-qualified-charitable-distributions.ts +101 -0
- package/src/learn/content/reading-the-results-page.ts +96 -0
- package/src/learn/content/reading-the-social-security-analysis-page.ts +106 -0
- package/src/learn/content/real-estate-home-equity-and-debt.ts +100 -0
- package/src/learn/content/reports-csv-exports-and-sharing.ts +101 -0
- package/src/learn/content/risk-based-guardrails.ts +100 -0
- package/src/learn/content/rmds-required-minimum-distributions.ts +100 -0
- package/src/learn/content/roth-conversion-basics.ts +104 -0
- package/src/learn/content/rsus-and-espp.ts +101 -0
- package/src/learn/content/rule-of-55-and-72t.ts +107 -0
- package/src/learn/content/savings-rate-biggest-lever.ts +66 -0
- package/src/learn/content/seed-your-plan-from-your-tax-return.ts +93 -0
- package/src/learn/content/sensitivity-testing-what-changes-the-answer.ts +104 -0
- package/src/learn/content/sequence-of-returns-risk.ts +98 -0
- package/src/learn/content/social-security-bridge.ts +67 -0
- package/src/learn/content/social-security-claiming-age-basics.ts +113 -0
- package/src/learn/content/social-security-taxes-vs-benefits.ts +76 -0
- package/src/learn/content/spending-profiles-and-the-retirement-smile.ts +92 -0
- package/src/learn/content/spousal-and-survivor-benefits.ts +120 -0
- package/src/learn/content/ssdi-and-retirement-planning.ts +72 -0
- package/src/learn/content/standard-deduction-senior-deduction-and-itemizing.ts +97 -0
- package/src/learn/content/state-income-taxes-in-retirement.ts +97 -0
- package/src/learn/content/step-up-in-basis.ts +102 -0
- package/src/learn/content/survivor-planning-for-couples.ts +110 -0
- package/src/learn/content/survivor-spending-in-couple-plans.ts +98 -0
- package/src/learn/content/tax-cliffs-and-bracket-edges.ts +105 -0
- package/src/learn/content/tax-loss-and-gain-harvesting.ts +99 -0
- package/src/learn/content/taxable-brokerage-basis-and-capital-gains.ts +99 -0
- package/src/learn/content/three-big-questions-spending-time-risk.ts +103 -0
- package/src/learn/content/tips-ladders.ts +92 -0
- package/src/learn/content/todays-dollars-vs-future-dollars.ts +107 -0
- package/src/learn/content/traditional-vs-roth-contributions.ts +113 -0
- package/src/learn/content/troubleshooting-surprising-results.ts +105 -0
- package/src/learn/content/trust-fund-haircut-scenarios.ts +101 -0
- package/src/learn/content/understanding-monte-carlo-success-rate.ts +118 -0
- package/src/learn/content/understanding-your-plan-assumptions.ts +134 -0
- package/src/learn/content/using-assumptions-and-provenance.ts +98 -0
- package/src/learn/content/using-scenarios-to-compare-choices.ts +99 -0
- package/src/learn/content/what-changes-when-you-move-states.ts +141 -0
- package/src/learn/content/what-is-fire.ts +65 -0
- package/src/learn/content/what-monte-carlo-proves.ts +98 -0
- package/src/learn/content/what-retiregolden-models.ts +103 -0
- package/src/learn/content/what-retirement-healthcare-really-costs.ts +117 -0
- package/src/learn/content/why-95-percent-is-not-a-guarantee.ts +98 -0
- package/src/learn/content/why-roth-conversions-raise-other-costs.ts +106 -0
- package/src/learn/content/why-small-tax-cliffs-can-matter.ts +109 -0
- package/src/learn/content/widows-penalty-and-survivor-brackets.ts +106 -0
- package/src/learn/content/withdrawal-order-basics.ts +105 -0
- package/src/learn/glossary.ts +191 -0
- package/src/learn/inlineMarkdown.tsx +54 -0
- package/src/learn/learn.css +537 -0
- package/src/learn/learningRegistry.ts +502 -0
- package/src/longevity/LongevityResults.tsx +85 -0
- package/src/longevity/LongevityWizard.tsx +305 -0
- package/src/longevity/constants.ts +15 -0
- package/src/longevity/factors.ts +125 -0
- package/src/longevity/model.ts +31 -0
- package/src/longevity/persistedGuard.ts +129 -0
- package/src/longevity/storage.ts +40 -0
- package/src/mc/messages.ts +118 -0
- package/src/mc/monteCarlo.worker.ts +44 -0
- package/src/mc/pool.ts +267 -0
- package/src/mc/runRequest.ts +125 -0
- package/src/optimize/messages.ts +84 -0
- package/src/optimize/optimize.worker.ts +29 -0
- package/src/optimize/runOptimize.ts +92 -0
- package/src/optimize/runSpendingSolve.ts +47 -0
- package/src/optimize/runner.ts +21 -0
- package/src/optimize/spendingMessages.ts +44 -0
- package/src/optimize/spendingRunner.ts +21 -0
- package/src/optimize/spendingSolve.worker.ts +18 -0
- package/src/planner/AssumptionsCardPage.tsx +136 -0
- package/src/planner/BucketLensCard.tsx +114 -0
- package/src/planner/ComparePlansPage.tsx +219 -0
- package/src/planner/DisclaimerPage.tsx +88 -0
- package/src/planner/HowTestedPage.tsx +159 -0
- package/src/planner/LiveStatus.tsx +15 -0
- package/src/planner/LongevityModal.tsx +55 -0
- package/src/planner/Modal.tsx +97 -0
- package/src/planner/MonteCarloPage.tsx +907 -0
- package/src/planner/OptimizePage.tsx +611 -0
- package/src/planner/PlanContext.tsx +198 -0
- package/src/planner/PlanPickerPage.tsx +124 -0
- package/src/planner/PlanWorkspace.tsx +290 -0
- package/src/planner/ProvenancePanel.tsx +45 -0
- package/src/planner/RelocationComparePage.tsx +485 -0
- package/src/planner/ReportPage.tsx +375 -0
- package/src/planner/ResultsPage.tsx +817 -0
- package/src/planner/ScenariosPage.tsx +285 -0
- package/src/planner/SocialSecuritySection.tsx +556 -0
- package/src/planner/SpendingSolverPage.tsx +512 -0
- package/src/planner/SsAnalysisPage.tsx +1134 -0
- package/src/planner/SurvivalPercentileModal.tsx +161 -0
- package/src/planner/SurvivorTransitionPage.tsx +286 -0
- package/src/planner/assumptionsExport.ts +371 -0
- package/src/planner/bucketLens.ts +89 -0
- package/src/planner/chartFrame.ts +8 -0
- package/src/planner/chartStyle.ts +11 -0
- package/src/planner/dialogViews.tsx +184 -0
- package/src/planner/dialogs.tsx +133 -0
- package/src/planner/examples/ExampleLibrary.tsx +189 -0
- package/src/planner/examples/ExamplePreviewBanner.tsx +55 -0
- package/src/planner/examples/ExamplesPage.tsx +25 -0
- package/src/planner/examples/OpenExampleButton.tsx +61 -0
- package/src/planner/examples/buildAggressiveSaver.ts +102 -0
- package/src/planner/examples/buildAnnuityEstate.ts +137 -0
- package/src/planner/examples/buildBaristaFire.ts +115 -0
- package/src/planner/examples/buildBracketFillRoth.ts +65 -0
- package/src/planner/examples/buildBridgeEarlyRetirement.ts +94 -0
- package/src/planner/examples/buildBrokerageNoHsa.ts +109 -0
- package/src/planner/examples/buildCoastFire.ts +88 -0
- package/src/planner/examples/buildContext.ts +20 -0
- package/src/planner/examples/buildEarlyCareerMatch.ts +93 -0
- package/src/planner/examples/buildEarlyRetireeAca.ts +61 -0
- package/src/planner/examples/buildExampleCouple.ts +103 -0
- package/src/planner/examples/buildFixedTargetSpending.ts +74 -0
- package/src/planner/examples/buildGlidepathAllocation.ts +131 -0
- package/src/planner/examples/buildGuardrailsFlex.ts +120 -0
- package/src/planner/examples/buildHsaPropertyDepth.ts +109 -0
- package/src/planner/examples/buildHsaStealthRetirement.ts +97 -0
- package/src/planner/examples/buildLeanFatFire.ts +109 -0
- package/src/planner/examples/buildLtcShock.ts +62 -0
- package/src/planner/examples/buildMovingStateTax.ts +53 -0
- package/src/planner/examples/buildNoAnnuityBrokerage.ts +92 -0
- package/src/planner/examples/buildRmdIrmaa.ts +55 -0
- package/src/planner/examples/buildSalaryGrowthEscalation.ts +96 -0
- package/src/planner/examples/buildStaticAllocationControl.ts +96 -0
- package/src/planner/examples/buildSurvivorYears.ts +62 -0
- package/src/planner/examples/buildUnderSavedSingle.ts +51 -0
- package/src/planner/examples/exampleCopy.ts +23 -0
- package/src/planner/examples/loadExample.ts +90 -0
- package/src/planner/examples/registry.ts +313 -0
- package/src/planner/explainPanels.tsx +233 -0
- package/src/planner/fields.tsx +381 -0
- package/src/planner/format.ts +33 -0
- package/src/planner/home/DataAndPrivacyCard.tsx +56 -0
- package/src/planner/home/GettingStartedPaths.tsx +46 -0
- package/src/planner/home/GettingStartedReopener.tsx +32 -0
- package/src/planner/home/StartHereLinks.tsx +22 -0
- package/src/planner/home/WelcomeHero.tsx +39 -0
- package/src/planner/home/YourPlans.tsx +72 -0
- package/src/planner/home/importErrorMessage.ts +22 -0
- package/src/planner/home/startHereSlugs.ts +7 -0
- package/src/planner/home/useHomeData.ts +190 -0
- package/src/planner/home/useHomeMode.ts +47 -0
- package/src/planner/householdActions.ts +22 -0
- package/src/planner/insights/InsightCardView.tsx +340 -0
- package/src/planner/insights/InsightsPage.tsx +204 -0
- package/src/planner/insights/categoryLabels.ts +11 -0
- package/src/planner/learnLinks.ts +85 -0
- package/src/planner/marketModelPicker.ts +172 -0
- package/src/planner/optimizePageChart.ts +40 -0
- package/src/planner/optimizePageClaim.ts +64 -0
- package/src/planner/planCompleteness.ts +27 -0
- package/src/planner/planContextCore.ts +26 -0
- package/src/planner/planner.css +2304 -0
- package/src/planner/provenanceLinks.ts +25 -0
- package/src/planner/sections/AccountFields.tsx +872 -0
- package/src/planner/sections/AccountsSection.tsx +89 -0
- package/src/planner/sections/AllocationPanel.tsx +261 -0
- package/src/planner/sections/AssumptionsSection.tsx +256 -0
- package/src/planner/sections/HouseholdSection.tsx +243 -0
- package/src/planner/sections/IncomeFloorSection.tsx +418 -0
- package/src/planner/sections/IncomeSection.tsx +170 -0
- package/src/planner/sections/InsuranceSection.tsx +362 -0
- package/src/planner/sections/SpendingSection.tsx +904 -0
- package/src/planner/sections/StrategySection.tsx +349 -0
- package/src/planner/sections/UpdateBalancesPanel.tsx +182 -0
- package/src/planner/sections/sectionHelpers.ts +48 -0
- package/src/planner/sections/shared.tsx +15 -0
- package/src/planner/sections.tsx +15 -0
- package/src/planner/ssAnalysis.ts +325 -0
- package/src/planner/successBand.ts +20 -0
- package/src/planner/survivorAnalysis.ts +277 -0
- package/src/planner/usStates.ts +19 -0
- package/src/planner/useMcSuccessRate.ts +77 -0
- package/src/planner/useProjection.ts +63 -0
- package/src/relocation/messages.ts +21 -0
- package/src/relocation/relocation.worker.ts +18 -0
- package/src/relocation/runRelocation.ts +17 -0
- package/src/relocation/runner.ts +22 -0
- package/src/report/brandingContext.ts +15 -0
- package/src/report/downloadReport.ts +34 -0
- package/src/report/reportHtml.ts +547 -0
- package/src/routes/LearnRoutes.tsx +46 -0
- package/src/routes/PlanRoutes.tsx +55 -0
- package/src/routes/RouteFallback.tsx +9 -0
- package/src/socialSecurity/breakEven.ts +107 -0
- package/src/socialSecurity/expectedPv.ts +164 -0
- package/src/socialSecurity/explain.ts +92 -0
- package/src/socialSecurity/ficaReturn.ts +81 -0
- package/src/socialSecurity/persistedSsGuard.ts +138 -0
- package/src/socialSecurity/ssFormUtils.ts +48 -0
- package/src/socialSecurity/ssaStatementXml.ts +156 -0
- package/src/socialSecurity/storage.ts +69 -0
- package/src/socialSecurity/survivorSwitching.ts +153 -0
- package/src/testSupport/samplePlan.ts +2 -0
- package/src/workers/run.ts +45 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** Accounts section: add/edit accounts. */
|
|
2
|
+
|
|
3
|
+
import type { Account } from '@retiregolden/engine/model/plan'
|
|
4
|
+
import { AccountFields } from './AccountFields'
|
|
5
|
+
import { ACCOUNT_LABEL, isIndividuallyOwnedAccount } from './sectionHelpers'
|
|
6
|
+
import { usePlan } from '../planContextCore'
|
|
7
|
+
import { Issues } from './shared'
|
|
8
|
+
import { newId } from './sectionHelpers'
|
|
9
|
+
import { UpdateBalancesPanel } from './UpdateBalancesPanel'
|
|
10
|
+
|
|
11
|
+
function makeAccount(type: Account['type'], primaryPersonId: string): Account {
|
|
12
|
+
const base = { id: newId(), ownerPersonId: isIndividuallyOwnedAccount(type) ? primaryPersonId : null, annualReturnPct: null }
|
|
13
|
+
switch (type) {
|
|
14
|
+
case 'cash':
|
|
15
|
+
return { ...base, type, name: 'Savings', balance: 0, annualContribution: 0 }
|
|
16
|
+
case 'taxable':
|
|
17
|
+
return {
|
|
18
|
+
...base,
|
|
19
|
+
type,
|
|
20
|
+
name: 'Brokerage',
|
|
21
|
+
balance: 0,
|
|
22
|
+
costBasis: 0,
|
|
23
|
+
interestYieldPct: 0,
|
|
24
|
+
dividendYieldPct: 0,
|
|
25
|
+
qualifiedRatio: 0.85,
|
|
26
|
+
reinvestDividends: true,
|
|
27
|
+
annualContribution: 0,
|
|
28
|
+
}
|
|
29
|
+
case 'equityComp':
|
|
30
|
+
return { ...base, type, name: 'RSU / ESPP', balance: 0, costBasis: 0, annualContribution: 0, vestingMode: 'cliff', vestDate: `${new Date().getFullYear()}-12-31` }
|
|
31
|
+
case 'traditional':
|
|
32
|
+
return { ...base, type, name: '401(k)', kind: 'employer', balance: 0, annualContribution: 0 }
|
|
33
|
+
case 'roth':
|
|
34
|
+
return { ...base, type, name: 'Roth IRA', kind: 'ira', balance: 0, annualContribution: 0 }
|
|
35
|
+
case 'hsa':
|
|
36
|
+
return { ...base, type, name: 'HSA', balance: 0, annualContribution: 0 }
|
|
37
|
+
case 'pension':
|
|
38
|
+
return { ...base, type, name: 'Pension', source: 'private', startAge: 65, monthlyAmount: 0, colaPct: 0, survivorPct: 50 }
|
|
39
|
+
case 'annuity':
|
|
40
|
+
return { ...base, type, name: 'Annuity', startAge: 65, monthlyAmount: 0, colaPct: 0, taxablePct: 100 }
|
|
41
|
+
case 'property':
|
|
42
|
+
return { ...base, type, name: 'Home', value: 0, plannedSaleYear: null, expectedNetProceeds: null }
|
|
43
|
+
case 'debt':
|
|
44
|
+
return { ...base, type, name: 'Mortgage', balance: 0, interestPct: 5, monthlyPayment: 0 }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
export function AccountsSection() {
|
|
50
|
+
const { plan, update } = usePlan()
|
|
51
|
+
const primaryPersonId = plan.household.people[0]!.id
|
|
52
|
+
return (
|
|
53
|
+
<section>
|
|
54
|
+
<div className="card">
|
|
55
|
+
<h2>Accounts</h2>
|
|
56
|
+
<p className="card-hint">Balances as of today. Investable accounts grow at their expected return (or the default assumption) and are drained per your withdrawal strategy.</p>
|
|
57
|
+
{plan.accounts.length === 0 ? <div className="empty-state"><p>No accounts yet — add your first below.</p></div> : null}
|
|
58
|
+
{plan.accounts.map((a, i) => (
|
|
59
|
+
<div className="item-row" key={a.id} data-testid="account-row" data-account-type={a.type} data-account-name={a.name}>
|
|
60
|
+
<div className="item-row-head">
|
|
61
|
+
<span className="item-row-title">
|
|
62
|
+
<span className="type-chip">{ACCOUNT_LABEL[a.type]}</span>
|
|
63
|
+
{a.name}
|
|
64
|
+
</span>
|
|
65
|
+
<button type="button" className="btn-ghost btn-ghost-danger" onClick={() => update((d) => void d.accounts.splice(i, 1))}>
|
|
66
|
+
Remove
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
<AccountFields account={a} index={i} />
|
|
70
|
+
</div>
|
|
71
|
+
))}
|
|
72
|
+
<div className="add-row">
|
|
73
|
+
{(Object.keys(ACCOUNT_LABEL) as Account['type'][]).map((t) => (
|
|
74
|
+
<button key={t} type="button" className="btn btn-secondary btn-small" onClick={() => update((d) => void d.accounts.push(makeAccount(t, primaryPersonId)))}>
|
|
75
|
+
+ {ACCOUNT_LABEL[t]}
|
|
76
|
+
</button>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
<Issues />
|
|
80
|
+
</div>
|
|
81
|
+
<UpdateBalancesPanel />
|
|
82
|
+
</section>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Income
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/** Asset-allocation panel and return estimator for allocatable accounts. */
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import type { AllocationWeights, AssetAllocationPolicy, Plan } from '@retiregolden/engine/model/plan'
|
|
6
|
+
import { ASSET_CLASS_IDS } from '@retiregolden/engine/model/plan'
|
|
7
|
+
import type { AllocatableAccount } from './sectionHelpers'
|
|
8
|
+
import {
|
|
9
|
+
blendedReturnPct,
|
|
10
|
+
blendedTaxableYield,
|
|
11
|
+
DEFAULT_ASSET_CLASS_PARAMS,
|
|
12
|
+
resolveAssetClassParams,
|
|
13
|
+
targetWeightsAt,
|
|
14
|
+
} from '@retiregolden/engine/allocation/assetClasses'
|
|
15
|
+
import { NumberField, PercentField, ReadonlyField, SelectField } from '../fields'
|
|
16
|
+
import { Modal } from '../Modal'
|
|
17
|
+
import { currentStartYear } from '../useProjection'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Allocation → expected-return estimator. Long-run nominal assumptions
|
|
21
|
+
* (illustrative, before fees): stocks ≈ 7%, bonds ≈ 4%, cash ≈ 2.5%.
|
|
22
|
+
*/
|
|
23
|
+
const ASSET_RETURN = { stocks: 7, bonds: 4, cash: 2.5 } as const
|
|
24
|
+
|
|
25
|
+
function riskLabel(stocksPct: number): string {
|
|
26
|
+
if (stocksPct < 25) return 'Conservative — low volatility, lower growth'
|
|
27
|
+
if (stocksPct < 50) return 'Balanced — moderate ups and downs'
|
|
28
|
+
if (stocksPct < 75) return 'Growth — meaningful swings in bad years'
|
|
29
|
+
return 'Aggressive — expect large drawdowns on the way'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function ReturnEstimatorModal({ initialPct, onApply, onClose }: { initialPct: number | null; onApply: (pct: number) => void; onClose: () => void }) {
|
|
33
|
+
const guessStocks = initialPct === null ? 60 : Math.min(100, Math.max(0, Math.round(((initialPct - ASSET_RETURN.cash) / (ASSET_RETURN.stocks - ASSET_RETURN.cash)) * 100 / 5) * 5))
|
|
34
|
+
const [stocks, setStocks] = useState(guessStocks)
|
|
35
|
+
const [bonds, setBonds] = useState(Math.min(100 - guessStocks, 30))
|
|
36
|
+
const cash = Math.max(0, 100 - stocks - bonds)
|
|
37
|
+
const blended = (stocks * ASSET_RETURN.stocks + bonds * ASSET_RETURN.bonds + cash * ASSET_RETURN.cash) / 100
|
|
38
|
+
return (
|
|
39
|
+
<Modal title="Estimate expected return" onClose={onClose}>
|
|
40
|
+
<p className="card-hint">
|
|
41
|
+
Describe roughly how this account is invested; we blend long-run nominal return assumptions (stocks {ASSET_RETURN.stocks}%,
|
|
42
|
+
bonds {ASSET_RETURN.bonds}%, cash {ASSET_RETURN.cash}% — illustrative, before fees, not a forecast). More stocks
|
|
43
|
+
means higher expected growth and bigger swings; Monte Carlo is where that risk shows up.
|
|
44
|
+
</p>
|
|
45
|
+
<div className="alloc-row">
|
|
46
|
+
<span>Stocks</span>
|
|
47
|
+
<input
|
|
48
|
+
type="range"
|
|
49
|
+
min={0}
|
|
50
|
+
max={100}
|
|
51
|
+
step={5}
|
|
52
|
+
value={stocks}
|
|
53
|
+
aria-label="Stocks percent"
|
|
54
|
+
onChange={(e) => {
|
|
55
|
+
const v = Number(e.target.value)
|
|
56
|
+
setStocks(v)
|
|
57
|
+
setBonds((b) => Math.min(b, 100 - v))
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
<span>{stocks}%</span>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="alloc-row">
|
|
63
|
+
<span>Bonds</span>
|
|
64
|
+
<input
|
|
65
|
+
type="range"
|
|
66
|
+
min={0}
|
|
67
|
+
max={100 - stocks}
|
|
68
|
+
step={5}
|
|
69
|
+
value={bonds}
|
|
70
|
+
aria-label="Bonds percent"
|
|
71
|
+
onChange={(e) => setBonds(Number(e.target.value))}
|
|
72
|
+
/>
|
|
73
|
+
<span>{bonds}%</span>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="alloc-row">
|
|
76
|
+
<span>Cash</span>
|
|
77
|
+
<span className="muted small">remainder</span>
|
|
78
|
+
<span>{cash}%</span>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="alloc-result">
|
|
81
|
+
<strong>{blended.toFixed(1)}%</strong>
|
|
82
|
+
<span className="muted small">{riskLabel(stocks)}</span>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="add-row" style={{ marginTop: '1rem' }}>
|
|
85
|
+
<button type="button" className="btn btn-primary btn-small" onClick={() => { onApply(Math.round(blended * 10) / 10); onClose() }}>
|
|
86
|
+
Use {blended.toFixed(1)}%
|
|
87
|
+
</button>
|
|
88
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={onClose}>
|
|
89
|
+
Cancel
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
</Modal>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Asset allocation (opt-in per account)
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
function WeightsGrid({ title, weights, onCommit }: { title?: string; weights: AllocationWeights; onCommit: (w: AllocationWeights) => void }) {
|
|
101
|
+
const sum = ASSET_CLASS_IDS.reduce((s, id) => s + weights[id], 0)
|
|
102
|
+
return (
|
|
103
|
+
<div className="nested-form-section field-span-full">
|
|
104
|
+
{title ? <h4>{title}</h4> : null}
|
|
105
|
+
<div className="form-grid nested-control-grid">
|
|
106
|
+
{ASSET_CLASS_IDS.map((id) => (
|
|
107
|
+
<PercentField
|
|
108
|
+
key={id}
|
|
109
|
+
label={DEFAULT_ASSET_CLASS_PARAMS[id].label}
|
|
110
|
+
value={weights[id]}
|
|
111
|
+
min={0}
|
|
112
|
+
max={100}
|
|
113
|
+
step={5}
|
|
114
|
+
onCommit={(v) => onCommit({ ...weights, [id]: Math.min(100, Math.max(0, v ?? 0)) })}
|
|
115
|
+
/>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
<p className={Math.abs(sum - 100) <= 0.5 ? 'muted small' : 'issue-list small'} role="status">
|
|
119
|
+
Total {sum.toFixed(0)}%{Math.abs(sum - 100) > 0.5 ? ' — weights must sum to 100%' : ''}
|
|
120
|
+
</p>
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Allocation policy editor: mode (static / glidepaths), rebalancing, and the
|
|
127
|
+
* weight grid(s) for the selected mode. All money math (blends, per-year
|
|
128
|
+
* targets) comes from engine/allocation.
|
|
129
|
+
*/
|
|
130
|
+
export function AllocationPanel({ account, plan, onCommit }: { account: AllocatableAccount; plan: Plan; onCommit: (a: AssetAllocationPolicy) => void }) {
|
|
131
|
+
const policy = account.allocation!
|
|
132
|
+
const year = currentStartYear()
|
|
133
|
+
const params = resolveAssetClassParams(plan.assumptions.assetClassParams)
|
|
134
|
+
const weightsNow = targetWeightsAt(policy, year)
|
|
135
|
+
const blendedNow = blendedReturnPct(weightsNow, params)
|
|
136
|
+
const yieldNow = blendedTaxableYield(weightsNow, params)
|
|
137
|
+
|
|
138
|
+
const currentWeights = (): AllocationWeights => {
|
|
139
|
+
const w = targetWeightsAt(policy, year)
|
|
140
|
+
return {
|
|
141
|
+
usStocks: Math.round(w[0]! * 100),
|
|
142
|
+
intlStocks: Math.round(w[1]! * 100),
|
|
143
|
+
bonds: Math.round(w[2]! * 100),
|
|
144
|
+
cash: Math.round(w[3]! * 100),
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const switchMode = (mode: AssetAllocationPolicy['mode']) => {
|
|
148
|
+
if (mode === policy.mode) return
|
|
149
|
+
const weights = currentWeights()
|
|
150
|
+
const shared = { rebalancing: policy.rebalancing }
|
|
151
|
+
if (mode === 'static') onCommit({ mode, ...shared, weights })
|
|
152
|
+
else if (mode === 'linear') onCommit({ mode, ...shared, from: weights, to: weights, startYear: year, endYear: year + 20 })
|
|
153
|
+
else if (mode === 'staged') onCommit({ mode, ...shared, stages: [{ fromYear: year, weights }] })
|
|
154
|
+
else onCommit({ mode, ...shared, targets: [{ year, weights }] })
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div className="nested-form-section field-span-full" data-testid="allocation-panel">
|
|
159
|
+
<div className="form-grid nested-control-grid">
|
|
160
|
+
<SelectField
|
|
161
|
+
label="Allocation over time"
|
|
162
|
+
help="Static holds one target mix. A linear glidepath slides from a starting mix to an ending mix between two years. Staged jumps to a new mix in given years; custom targets interpolate between the mixes you set."
|
|
163
|
+
value={policy.mode}
|
|
164
|
+
options={[
|
|
165
|
+
{ value: 'static', label: 'Static target mix' },
|
|
166
|
+
{ value: 'linear', label: 'Linear glidepath (from → to)' },
|
|
167
|
+
{ value: 'staged', label: 'Staged (step changes)' },
|
|
168
|
+
{ value: 'custom', label: 'Custom year targets' },
|
|
169
|
+
]}
|
|
170
|
+
onCommit={switchMode}
|
|
171
|
+
/>
|
|
172
|
+
<SelectField
|
|
173
|
+
label="Rebalancing"
|
|
174
|
+
help="Annual rebalancing trades back to the target mix each January. In a brokerage account those sales realize capital gains through your cost basis; retirement-account rebalancing is tax-free. Drift lets winners run — the mix (and risk) then wanders from the target."
|
|
175
|
+
value={policy.rebalancing}
|
|
176
|
+
options={[
|
|
177
|
+
{ value: 'annual', label: 'Rebalance annually to target' },
|
|
178
|
+
{ value: 'none', label: 'No rebalancing (drift)' },
|
|
179
|
+
]}
|
|
180
|
+
onCommit={(v) => onCommit({ ...policy, rebalancing: v })}
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
{policy.mode === 'static' ? (
|
|
184
|
+
<WeightsGrid weights={policy.weights} onCommit={(weights) => onCommit({ ...policy, weights })} />
|
|
185
|
+
) : null}
|
|
186
|
+
{policy.mode === 'linear' ? (
|
|
187
|
+
<>
|
|
188
|
+
<div className="form-grid nested-control-grid">
|
|
189
|
+
<NumberField label="From year" value={policy.startYear} min={1900} max={2200} onCommit={(v) => onCommit({ ...policy, startYear: Math.round(v ?? year) })} />
|
|
190
|
+
<NumberField label="To year" value={policy.endYear} min={1900} max={2200} onCommit={(v) => onCommit({ ...policy, endYear: Math.round(v ?? year + 20) })} />
|
|
191
|
+
</div>
|
|
192
|
+
<WeightsGrid title="Starting mix" weights={policy.from} onCommit={(from) => onCommit({ ...policy, from })} />
|
|
193
|
+
<WeightsGrid title="Ending mix" weights={policy.to} onCommit={(to) => onCommit({ ...policy, to })} />
|
|
194
|
+
</>
|
|
195
|
+
) : null}
|
|
196
|
+
{policy.mode === 'staged' || policy.mode === 'custom' ? (
|
|
197
|
+
<>
|
|
198
|
+
{(policy.mode === 'staged' ? policy.stages : policy.targets).map((row, i) => {
|
|
199
|
+
const rows = policy.mode === 'staged' ? policy.stages : policy.targets
|
|
200
|
+
const setRows = (next: typeof rows) =>
|
|
201
|
+
policy.mode === 'staged'
|
|
202
|
+
? onCommit({ ...policy, stages: next as { fromYear: number; weights: AllocationWeights }[] })
|
|
203
|
+
: onCommit({ ...policy, targets: next as { year: number; weights: AllocationWeights }[] })
|
|
204
|
+
const rowYear = 'fromYear' in row ? row.fromYear : row.year
|
|
205
|
+
return (
|
|
206
|
+
<div key={i} className="nested-phase-row">
|
|
207
|
+
<div className="form-grid nested-phase-grid">
|
|
208
|
+
<NumberField
|
|
209
|
+
label={policy.mode === 'staged' ? 'From year' : 'Target year'}
|
|
210
|
+
value={rowYear}
|
|
211
|
+
min={1900}
|
|
212
|
+
max={2200}
|
|
213
|
+
onCommit={(v) => {
|
|
214
|
+
const y = Math.round(v ?? rowYear)
|
|
215
|
+
setRows(rows.map((r, idx) => (idx === i ? ('fromYear' in r ? { ...r, fromYear: y } : { ...r, year: y }) : r)) as typeof rows)
|
|
216
|
+
}}
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
<WeightsGrid
|
|
220
|
+
weights={row.weights}
|
|
221
|
+
onCommit={(weights) => setRows(rows.map((r, idx) => (idx === i ? { ...r, weights } : r)) as typeof rows)}
|
|
222
|
+
/>
|
|
223
|
+
{rows.length > 1 ? (
|
|
224
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={() => setRows(rows.filter((_, idx) => idx !== i) as typeof rows)}>
|
|
225
|
+
Remove
|
|
226
|
+
</button>
|
|
227
|
+
) : null}
|
|
228
|
+
</div>
|
|
229
|
+
)
|
|
230
|
+
})}
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
className="btn btn-secondary btn-small"
|
|
234
|
+
onClick={() => {
|
|
235
|
+
const weights = currentWeights()
|
|
236
|
+
if (policy.mode === 'staged') {
|
|
237
|
+
const lastYear = policy.stages[policy.stages.length - 1]?.fromYear ?? year
|
|
238
|
+
onCommit({ ...policy, stages: [...policy.stages, { fromYear: lastYear + 5, weights }] })
|
|
239
|
+
} else {
|
|
240
|
+
const lastYear = policy.targets[policy.targets.length - 1]?.year ?? year
|
|
241
|
+
onCommit({ ...policy, targets: [...policy.targets, { year: lastYear + 5, weights }] })
|
|
242
|
+
}
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
+ Add {policy.mode === 'staged' ? 'stage' : 'target'}
|
|
246
|
+
</button>
|
|
247
|
+
</>
|
|
248
|
+
) : null}
|
|
249
|
+
<ReadonlyField
|
|
250
|
+
label="This year's blend"
|
|
251
|
+
help="Expected nominal return of this account's target mix this year, blended from the asset-class assumptions. For a brokerage account, the mix also sets the taxable interest/dividend yield used for annual tax drag."
|
|
252
|
+
value={
|
|
253
|
+
account.type === 'taxable'
|
|
254
|
+
? `${blendedNow.toFixed(1)}% return · ${yieldNow.interestYieldPct.toFixed(1)}% interest + ${yieldNow.dividendYieldPct.toFixed(1)}% dividends`
|
|
255
|
+
: `${blendedNow.toFixed(1)}% return`
|
|
256
|
+
}
|
|
257
|
+
/>
|
|
258
|
+
</div>
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/** Assumptions section: inflation, returns, taxes, asset classes. */
|
|
2
|
+
|
|
3
|
+
import { Link } from 'react-router-dom'
|
|
4
|
+
|
|
5
|
+
import type { AssetClassId } from '@retiregolden/engine/model/plan'
|
|
6
|
+
import { ASSET_CLASS_IDS } from '@retiregolden/engine/model/plan'
|
|
7
|
+
import { DEFAULT_ASSET_CLASS_PARAMS, resolveAssetClassParams } from '@retiregolden/engine/allocation/assetClasses'
|
|
8
|
+
import { LATEST_PACK_YEAR, TRUSTEES_DEFAULT_SS_HAIRCUT } from '@retiregolden/engine/params'
|
|
9
|
+
import { usePlan } from '../planContextCore'
|
|
10
|
+
import { provenanceSource } from '../provenanceLinks'
|
|
11
|
+
import { CheckboxField, MoneyField, NumberField, PercentField, SelectField } from '../fields'
|
|
12
|
+
import { LearnAboutScreen } from '../../learn/LearnAboutScreen'
|
|
13
|
+
import { LEARN } from '../learnLinks'
|
|
14
|
+
import { Issues } from './shared'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Assumptions-level asset-class table: return / volatility / yield per class,
|
|
18
|
+
* shown with the sourced defaults and editable as overrides. Only accounts
|
|
19
|
+
* that opt into "Model asset classes" use these numbers.
|
|
20
|
+
*/
|
|
21
|
+
function AssetClassAssumptions() {
|
|
22
|
+
const { plan, update } = usePlan()
|
|
23
|
+
const resolved = resolveAssetClassParams(plan.assumptions.assetClassParams)
|
|
24
|
+
const hasOverrides = plan.assumptions.assetClassParams !== undefined
|
|
25
|
+
type OverrideKey = 'returnPct' | 'volatilityPct' | 'interestYieldPct' | 'dividendYieldPct' | 'qualifiedRatioPct'
|
|
26
|
+
const setParam = (id: AssetClassId, key: OverrideKey, v: number) =>
|
|
27
|
+
update((d) => {
|
|
28
|
+
const overrides = d.assumptions.assetClassParams ?? {}
|
|
29
|
+
overrides[id] = { ...overrides[id], [key]: v }
|
|
30
|
+
d.assumptions.assetClassParams = overrides
|
|
31
|
+
})
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
<h3>Asset classes</h3>
|
|
35
|
+
<p className="card-hint">
|
|
36
|
+
Used by accounts with "Model asset classes" turned on: growth blends these returns, Monte Carlo shocks each
|
|
37
|
+
class with long-horizon historical volatility and correlations, and brokerage yields drive annual tax drag.
|
|
38
|
+
Defaults are documented planning assumptions — edit them to match your own outlook.
|
|
39
|
+
{hasOverrides ? (
|
|
40
|
+
<>
|
|
41
|
+
{' '}
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
className="btn-ghost"
|
|
45
|
+
onClick={() => update((d) => void (d.assumptions.assetClassParams = undefined))}
|
|
46
|
+
>
|
|
47
|
+
Reset to defaults
|
|
48
|
+
</button>
|
|
49
|
+
</>
|
|
50
|
+
) : null}
|
|
51
|
+
</p>
|
|
52
|
+
{ASSET_CLASS_IDS.map((id) => {
|
|
53
|
+
const p = resolved[id]
|
|
54
|
+
const stockLike = id === 'usStocks' || id === 'intlStocks'
|
|
55
|
+
return (
|
|
56
|
+
<div key={id} className="nested-form-section" data-testid={`asset-class-${id}`}>
|
|
57
|
+
<h4>{p.label}</h4>
|
|
58
|
+
<div className="form-grid nested-control-grid">
|
|
59
|
+
<PercentField label="Expected return" value={p.returnPct} onCommit={(v) => setParam(id, 'returnPct', v ?? DEFAULT_ASSET_CLASS_PARAMS[id].returnPct)} />
|
|
60
|
+
<PercentField label="Volatility" help="Annual standard deviation of returns; drives the size of this class's Monte Carlo shocks." value={p.volatilityPct} min={0} onCommit={(v) => setParam(id, 'volatilityPct', v ?? DEFAULT_ASSET_CLASS_PARAMS[id].volatilityPct)} />
|
|
61
|
+
{stockLike ? (
|
|
62
|
+
<>
|
|
63
|
+
<PercentField label="Dividend yield" help="Distributed as dividends each year; taxable in a brokerage account." value={p.dividendYieldPct} min={0} onCommit={(v) => setParam(id, 'dividendYieldPct', v ?? DEFAULT_ASSET_CLASS_PARAMS[id].dividendYieldPct)} />
|
|
64
|
+
<PercentField label="Qualified share" help="Share of this class's dividends taxed at long-term capital-gain rates." value={p.qualifiedRatioPct} min={0} max={100} onCommit={(v) => setParam(id, 'qualifiedRatioPct', v ?? DEFAULT_ASSET_CLASS_PARAMS[id].qualifiedRatioPct)} />
|
|
65
|
+
</>
|
|
66
|
+
) : (
|
|
67
|
+
<PercentField label="Interest yield" help="Distributed as taxable interest each year in a brokerage account." value={p.interestYieldPct} min={0} onCommit={(v) => setParam(id, 'interestYieldPct', v ?? DEFAULT_ASSET_CLASS_PARAMS[id].interestYieldPct)} />
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
})}
|
|
73
|
+
</>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function AssumptionsSection() {
|
|
78
|
+
const { plan, update } = usePlan()
|
|
79
|
+
const a = plan.assumptions
|
|
80
|
+
return (
|
|
81
|
+
<section>
|
|
82
|
+
<div className="card">
|
|
83
|
+
<h2>Assumptions</h2>
|
|
84
|
+
<p className="card-hint">Sensible defaults; every projection is only as good as these. Monte Carlo varies returns and inflation around them.</p>
|
|
85
|
+
<div className="form-grid">
|
|
86
|
+
<PercentField
|
|
87
|
+
label="Inflation"
|
|
88
|
+
help="How fast prices rise each year. Results are shown in today's dollars, so this sets how much future dollars are discounted back."
|
|
89
|
+
learn={LEARN.generalInflation}
|
|
90
|
+
value={a.inflationPct}
|
|
91
|
+
onCommit={(v) => update((d) => void (d.assumptions.inflationPct = v ?? 2.5))}
|
|
92
|
+
/>
|
|
93
|
+
<PercentField
|
|
94
|
+
label="Healthcare extra inflation"
|
|
95
|
+
hint="On top of general inflation."
|
|
96
|
+
help="Additional inflation applied to healthcare and Medicare costs."
|
|
97
|
+
learn={LEARN.healthcareInflation}
|
|
98
|
+
value={a.healthcareExtraInflationPct}
|
|
99
|
+
onCommit={(v) => update((d) => void (d.assumptions.healthcareExtraInflationPct = v ?? 3))}
|
|
100
|
+
/>
|
|
101
|
+
<PercentField
|
|
102
|
+
label="Default return"
|
|
103
|
+
hint="For accounts without their own."
|
|
104
|
+
help="Assumed long-term nominal return rate for investment accounts."
|
|
105
|
+
learn={LEARN.investmentReturns}
|
|
106
|
+
value={a.defaultReturnPct}
|
|
107
|
+
onCommit={(v) => update((d) => void (d.assumptions.defaultReturnPct = v ?? 5.5))}
|
|
108
|
+
/>
|
|
109
|
+
<PercentField
|
|
110
|
+
label="State effective tax (override)"
|
|
111
|
+
help="Leave at 0 to use the modeled per-state brackets for your state(s) of residence. Set a flat rate above 0 to override the model everywhere — useful to correct a state, or to approximate a state that isn't modeled yet."
|
|
112
|
+
hint="0 = use modeled state brackets."
|
|
113
|
+
learn={LEARN.stateTaxOverride}
|
|
114
|
+
source={provenanceSource('state-income-tax')}
|
|
115
|
+
value={a.stateEffectiveTaxPct}
|
|
116
|
+
min={0}
|
|
117
|
+
max={20}
|
|
118
|
+
onCommit={(v) => update((d) => void (d.assumptions.stateEffectiveTaxPct = v ?? 0))}
|
|
119
|
+
/>
|
|
120
|
+
<PercentField
|
|
121
|
+
label="Local income tax"
|
|
122
|
+
help="Optional flat city/county income tax. When modeled state brackets are active, this applies to state taxable income; with the state override, it adds to the same flat override base."
|
|
123
|
+
hint="For MD counties, NYC, OH municipalities."
|
|
124
|
+
value={a.localIncomeTaxPct}
|
|
125
|
+
min={0}
|
|
126
|
+
max={10}
|
|
127
|
+
onCommit={(v) => update((d) => void (d.assumptions.localIncomeTaxPct = v ?? 0))}
|
|
128
|
+
/>
|
|
129
|
+
<MoneyField
|
|
130
|
+
label="Recent annual MAGI"
|
|
131
|
+
hint="Seeds IRMAA's two-year lookback."
|
|
132
|
+
help="Used to seed the two-year lookback for Medicare premium calculations during your first two years."
|
|
133
|
+
learn={LEARN.recentMagi}
|
|
134
|
+
source={provenanceSource('medicare-irmaa')}
|
|
135
|
+
value={a.recentAnnualMagi}
|
|
136
|
+
onCommit={(v) => update((d) => void (d.assumptions.recentAnnualMagi = v ?? 0))}
|
|
137
|
+
/>
|
|
138
|
+
<PercentField
|
|
139
|
+
label="Safe withdrawal rate (SWR)"
|
|
140
|
+
help="The percentage of your portfolio you assume is safe to withdraw in the first year of retirement, adjusted for inflation thereafter. For example, 4% is the common rule-of-thumb lens."
|
|
141
|
+
learn={LEARN.fiNumber}
|
|
142
|
+
value={a.safeWithdrawalRatePct ?? 4}
|
|
143
|
+
min={0.1}
|
|
144
|
+
onCommit={(v) => update((d) => void (d.assumptions.safeWithdrawalRatePct = v ?? 4))}
|
|
145
|
+
/>
|
|
146
|
+
<SelectField
|
|
147
|
+
label="Social Security COLA"
|
|
148
|
+
help="How the annual cost-of-living adjustment is modeled: matched to general inflation, or set at a fixed rate."
|
|
149
|
+
learn={LEARN.ssCola}
|
|
150
|
+
value={a.ssCola.mode}
|
|
151
|
+
options={[
|
|
152
|
+
{ value: 'matchInflation', label: 'Match inflation' },
|
|
153
|
+
{ value: 'fixed', label: 'Fixed rate' },
|
|
154
|
+
]}
|
|
155
|
+
onCommit={(v) =>
|
|
156
|
+
update((d) => {
|
|
157
|
+
d.assumptions.ssCola = v === 'fixed' ? { mode: 'fixed', annualPct: 2 } : { mode: 'matchInflation' }
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
/>
|
|
161
|
+
{a.ssCola.mode === 'fixed' ? (
|
|
162
|
+
<PercentField
|
|
163
|
+
label="COLA rate"
|
|
164
|
+
help="The annual cost-of-living adjustment rate."
|
|
165
|
+
learn={LEARN.ssCola}
|
|
166
|
+
value={a.ssCola.annualPct}
|
|
167
|
+
onCommit={(v) => update((d) => void (d.assumptions.ssCola = { mode: 'fixed', annualPct: v ?? 2 }))}
|
|
168
|
+
/>
|
|
169
|
+
) : null}
|
|
170
|
+
<PercentField
|
|
171
|
+
label="Heir tax rate"
|
|
172
|
+
help="Assumed tax rate heirs pay on inherited traditional pre-tax balances, used for the after-tax estate metric."
|
|
173
|
+
learn={LEARN.heirTaxRate}
|
|
174
|
+
value={a.heirTaxRatePct}
|
|
175
|
+
min={0}
|
|
176
|
+
max={50}
|
|
177
|
+
onCommit={(v) => update((d) => void (d.assumptions.heirTaxRatePct = v ?? 25))}
|
|
178
|
+
/>
|
|
179
|
+
<CheckboxField
|
|
180
|
+
label="Override heir tax by account class"
|
|
181
|
+
help="Set different heir tax rates for traditional and HSA accounts instead of applying the flat rate to all pre-tax balances. For example, a higher-income heir might face a 32% marginal rate on inherited traditional IRA distributions but only 22% on a modest HSA."
|
|
182
|
+
value={a.heirTaxByClass !== undefined}
|
|
183
|
+
onCommit={(v) =>
|
|
184
|
+
update((d) => void (d.assumptions.heirTaxByClass = v ? { traditional: d.assumptions.heirTaxRatePct, hsa: d.assumptions.heirTaxRatePct } : undefined))
|
|
185
|
+
}
|
|
186
|
+
/>
|
|
187
|
+
{a.heirTaxByClass !== undefined ? (
|
|
188
|
+
<>
|
|
189
|
+
<PercentField
|
|
190
|
+
label="Traditional heir tax"
|
|
191
|
+
help="Tax rate heirs pay on inherited traditional IRA/401(k) balances. Overrides the flat heir tax rate for this account class."
|
|
192
|
+
value={a.heirTaxByClass.traditional ?? a.heirTaxRatePct}
|
|
193
|
+
min={0}
|
|
194
|
+
max={50}
|
|
195
|
+
onCommit={(v) =>
|
|
196
|
+
update((d) => {
|
|
197
|
+
if (!d.assumptions.heirTaxByClass) return
|
|
198
|
+
d.assumptions.heirTaxByClass.traditional = v ?? undefined
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
/>
|
|
202
|
+
<PercentField
|
|
203
|
+
label="HSA heir tax"
|
|
204
|
+
help="Tax rate heirs pay on inherited HSA balances (non-spouse beneficiary). Overrides the flat heir tax rate for this account class."
|
|
205
|
+
value={a.heirTaxByClass.hsa ?? a.heirTaxRatePct}
|
|
206
|
+
min={0}
|
|
207
|
+
max={50}
|
|
208
|
+
onCommit={(v) =>
|
|
209
|
+
update((d) => {
|
|
210
|
+
if (!d.assumptions.heirTaxByClass) return
|
|
211
|
+
d.assumptions.heirTaxByClass.hsa = v ?? undefined
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
/>
|
|
215
|
+
</>
|
|
216
|
+
) : null}
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<AssetClassAssumptions />
|
|
220
|
+
|
|
221
|
+
<h3>Social Security trust fund</h3>
|
|
222
|
+
<div className="form-grid">
|
|
223
|
+
<CheckboxField
|
|
224
|
+
label="Model a benefit cut"
|
|
225
|
+
hint="Trustees project depletion around 2034 absent action."
|
|
226
|
+
help="Model a benefit cut if the Social Security trust fund exhausts reserves."
|
|
227
|
+
learn={LEARN.ssTrustFund}
|
|
228
|
+
source={provenanceSource('social-security')}
|
|
229
|
+
value={a.ssHaircut !== null}
|
|
230
|
+
onCommit={(v) => update((d) => void (d.assumptions.ssHaircut = v ? { ...TRUSTEES_DEFAULT_SS_HAIRCUT } : null))}
|
|
231
|
+
/>
|
|
232
|
+
{a.ssHaircut ? (
|
|
233
|
+
<>
|
|
234
|
+
<NumberField label="From year" value={a.ssHaircut.fromYear} min={1900} max={2200} onCommit={(v) => update((d) => void (d.assumptions.ssHaircut!.fromYear = Math.round(v ?? TRUSTEES_DEFAULT_SS_HAIRCUT.fromYear)))} />
|
|
235
|
+
<PercentField label="Cut" value={a.ssHaircut.cutPct} min={0} max={100} onCommit={(v) => update((d) => void (d.assumptions.ssHaircut!.cutPct = v ?? TRUSTEES_DEFAULT_SS_HAIRCUT.cutPct))} />
|
|
236
|
+
</>
|
|
237
|
+
) : null}
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<p className="card-hint">
|
|
241
|
+
Tax brackets, contribution limits, RMD factors, Medicare/IRMAA, and per-state rules come from published{' '}
|
|
242
|
+
{LATEST_PACK_YEAR} figures. <Link to="/disclaimer">See where the numbers come from →</Link>{' '}
|
|
243
|
+
<Link to={`/plan/${plan.id}/assumptions-card`}>See every live assumption on one card →</Link>
|
|
244
|
+
</p>
|
|
245
|
+
<Issues />
|
|
246
|
+
|
|
247
|
+
<LearnAboutScreen route="/plan/:planId/assumptions" limit={10} />
|
|
248
|
+
</div>
|
|
249
|
+
</section>
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Insurance (roadmap V6)
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|