@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,904 @@
|
|
|
1
|
+
/** Spending section: baseline, phases, goals, debts, property. */
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import type { GoalFlexibility, SpendingClassification, SpendingPolicy } from '@retiregolden/engine/model/plan'
|
|
6
|
+
import { annualDeltaPhases, spendingShapePhases, type SpendingShapeId } from '@retiregolden/engine/spending/shapePresets'
|
|
7
|
+
import { startingInvestableOf, type RiskBasedGuardrailSolution } from '@retiregolden/engine/montecarlo/riskBasedGuardrails'
|
|
8
|
+
import { runRiskBasedGuardrailSolve } from '../../mc/pool'
|
|
9
|
+
import { usePlan } from '../planContextCore'
|
|
10
|
+
import { CheckboxField, MoneyField, NumberField, PercentField, SelectField, TextField } from '../fields'
|
|
11
|
+
import { fmtMoney } from '../format'
|
|
12
|
+
import { buildModel } from '../marketModelPicker'
|
|
13
|
+
import { currentStartYear, seedFromPlanId } from '../useProjection'
|
|
14
|
+
import { LearnAboutScreen } from '../../learn/LearnAboutScreen'
|
|
15
|
+
import { LearnLink } from '../../learn/LearnLink'
|
|
16
|
+
import { LEARN } from '../learnLinks'
|
|
17
|
+
import { Issues } from './shared'
|
|
18
|
+
import { newId } from './sectionHelpers'
|
|
19
|
+
|
|
20
|
+
// Named spending shapes compile to ordinary `expenses.phases` rows (visible
|
|
21
|
+
// and editable afterwards) — never a parallel model. Calibrations and sources
|
|
22
|
+
// live with the compiler in engine/spending/shapePresets.ts.
|
|
23
|
+
type SpendingProfileId = SpendingShapeId
|
|
24
|
+
|
|
25
|
+
/** Solver budget for the on-demand threshold solve (worker; ~40 probes). */
|
|
26
|
+
const THRESHOLD_SOLVE_PATH_COUNT = 200
|
|
27
|
+
|
|
28
|
+
export function SpendingSection() {
|
|
29
|
+
const { plan, update } = usePlan()
|
|
30
|
+
const e = plan.expenses
|
|
31
|
+
const [solvingThresholds, setSolvingThresholds] = useState(false)
|
|
32
|
+
const [thresholdSolveError, setThresholdSolveError] = useState<string | null>(null)
|
|
33
|
+
const [thresholdSolution, setThresholdSolution] = useState<RiskBasedGuardrailSolution | null>(null)
|
|
34
|
+
// Latest committed band, readable from async solve callbacks: a solve result
|
|
35
|
+
// computed against an older band (edited while the worker was busy) is
|
|
36
|
+
// discarded instead of persisted.
|
|
37
|
+
const committedBandRef = useRef({ mode: e.spendingPolicy?.mode, lower: 70, upper: 95 })
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
committedBandRef.current = {
|
|
40
|
+
mode: e.spendingPolicy?.mode,
|
|
41
|
+
lower: e.spendingPolicy?.targetSuccessLowerPct ?? 70,
|
|
42
|
+
upper: e.spendingPolicy?.targetSuccessUpperPct ?? 95,
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
const solveThresholds = () => {
|
|
46
|
+
setSolvingThresholds(true)
|
|
47
|
+
setThresholdSolveError(null)
|
|
48
|
+
const solvedBand = { ...committedBandRef.current }
|
|
49
|
+
void runRiskBasedGuardrailSolve(plan, {
|
|
50
|
+
startYear: currentStartYear(),
|
|
51
|
+
pathCount: THRESHOLD_SOLVE_PATH_COUNT,
|
|
52
|
+
seed: seedFromPlanId(plan.id),
|
|
53
|
+
model: buildModel('lognormal', plan.assumptions.inflationPct, 12, 60, plan),
|
|
54
|
+
})
|
|
55
|
+
.then((solution) => {
|
|
56
|
+
const current = committedBandRef.current
|
|
57
|
+
if (current.mode !== 'riskBasedGuardrails' || current.lower !== solvedBand.lower || current.upper !== solvedBand.upper) {
|
|
58
|
+
return // the band (or mode) changed mid-solve; the result no longer applies
|
|
59
|
+
}
|
|
60
|
+
setThresholdSolution(solution)
|
|
61
|
+
update((d) => {
|
|
62
|
+
const policy = d.expenses.spendingPolicy
|
|
63
|
+
if (!policy || policy.mode !== 'riskBasedGuardrails') return
|
|
64
|
+
if (solution.lower) policy.lowerBalanceThresholdPct = Math.round(solution.lower.balanceFrac * 10_000) / 100
|
|
65
|
+
else delete policy.lowerBalanceThresholdPct
|
|
66
|
+
if (solution.upper) policy.upperBalanceThresholdPct = Math.round(solution.upper.balanceFrac * 10_000) / 100
|
|
67
|
+
else delete policy.upperBalanceThresholdPct
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
.catch((err: unknown) => setThresholdSolveError(err instanceof Error ? err.message : String(err)))
|
|
71
|
+
.finally(() => setSolvingThresholds(false))
|
|
72
|
+
}
|
|
73
|
+
const hasEarlyPullFlexibleGoals = e.oneTimeGoals.some((g) => {
|
|
74
|
+
const flexibility = g.flexibility ?? 'fixed'
|
|
75
|
+
const earliestYear = Math.min(g.earliestYear ?? g.year, g.year)
|
|
76
|
+
return flexibility !== 'fixed' && earliestYear < g.year
|
|
77
|
+
})
|
|
78
|
+
const applyProfile = (profile: SpendingProfileId) =>
|
|
79
|
+
update((d) => {
|
|
80
|
+
d.expenses.phases = spendingShapePhases(profile, plan.household.people[0]?.retirementAge ?? 65)
|
|
81
|
+
})
|
|
82
|
+
const [customDeltaPct, setCustomDeltaPct] = useState(-1.5)
|
|
83
|
+
const applyCustomDelta = () =>
|
|
84
|
+
update((d) => {
|
|
85
|
+
d.expenses.phases = annualDeltaPhases(customDeltaPct, plan.household.people[0]?.retirementAge ?? 65)
|
|
86
|
+
})
|
|
87
|
+
// Mutate the ABW parameter object, creating it on first edit.
|
|
88
|
+
const setAbw = (mutate: (abw: NonNullable<SpendingPolicy['abw']>) => void) =>
|
|
89
|
+
update((d) => {
|
|
90
|
+
const policy = d.expenses.spendingPolicy
|
|
91
|
+
if (!policy || policy.mode !== 'abw') return
|
|
92
|
+
policy.abw ??= { returnSource: 'fixed' }
|
|
93
|
+
mutate(policy.abw)
|
|
94
|
+
})
|
|
95
|
+
return (
|
|
96
|
+
<section>
|
|
97
|
+
<div className="card">
|
|
98
|
+
<h2>Spending</h2>
|
|
99
|
+
<p className="card-hint">
|
|
100
|
+
Today's dollars; the engine inflates everything. Healthcare is separate: enter full (unsubsidized) premiums pre-65
|
|
101
|
+
and optionally apply the modeled ACA premium credit; Medicare + IRMAA from 65. <LearnLink {...LEARN.retirementHealthcareCosts} />
|
|
102
|
+
</p>
|
|
103
|
+
<div className="form-grid">
|
|
104
|
+
<MoneyField
|
|
105
|
+
label="Baseline annual spending"
|
|
106
|
+
help="Everyday living costs in today's dollars: food, utilities, transportation, clothing, entertainment, routine travel, auto insurance, and out-of-pocket medical (copays, deductibles, dental, vision) — the costs with no separate input. Leave OUT anything modeled elsewhere: mortgage/loan payments (debt accounts); property tax & homeowner's insurance (enter those on the home/property account, where they correctly continue after the mortgage is paid off); health-insurance premiums (Healthcare below); and long-term-care or life-insurance premiums (Insurance). The Results page breaks all of these out in a Spending-by-category chart."
|
|
107
|
+
learn={LEARN.spendingBudget}
|
|
108
|
+
hint="Living costs incl. auto insurance & out-of-pocket medical; exclude mortgage, property tax, premiums."
|
|
109
|
+
value={e.baseAnnual}
|
|
110
|
+
onCommit={(v) =>
|
|
111
|
+
update((d) => {
|
|
112
|
+
d.expenses.baseAnnual = v ?? 0
|
|
113
|
+
if (d.expenses.requiredAnnual !== undefined) {
|
|
114
|
+
d.expenses.requiredAnnual = Math.min(d.expenses.requiredAnnual, d.expenses.baseAnnual)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
/>
|
|
119
|
+
<MoneyField
|
|
120
|
+
label="Required floor (today's $)"
|
|
121
|
+
help="The must-fund slice of baseline spending — the least you could live on in a bad market, before any discretionary lifestyle. Only matters when Spending guardrails (below) are on: the guardrail rations the gap between baseline and this floor but never cuts below it. Leave 0 (or equal to baseline) to treat all spending as required, which is today's behavior."
|
|
122
|
+
learn={LEARN.spendingBudget}
|
|
123
|
+
hint="Guardrails never cut below this. 0 = all spending is required."
|
|
124
|
+
value={e.requiredAnnual ?? 0}
|
|
125
|
+
onCommit={(v) =>
|
|
126
|
+
update((d) => {
|
|
127
|
+
if (!v || v <= 0) delete d.expenses.requiredAnnual
|
|
128
|
+
else d.expenses.requiredAnnual = Math.min(v, d.expenses.baseAnnual)
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
/>
|
|
132
|
+
<MoneyField
|
|
133
|
+
label="Ideal annual upside"
|
|
134
|
+
help="Flexible annual spending above your target lifestyle: extra travel, upgrades, gifts, or other spending you would fund only when the plan is running ahead. Fixed target mode funds it every year; guardrails fund it only after target spending has been restored."
|
|
135
|
+
learn={LEARN.spendingBudget}
|
|
136
|
+
hint="Above baseline target; funded after required and target."
|
|
137
|
+
value={e.idealAnnual ?? 0}
|
|
138
|
+
onCommit={(v) =>
|
|
139
|
+
update((d) => {
|
|
140
|
+
if (!v || v <= 0) delete d.expenses.idealAnnual
|
|
141
|
+
else d.expenses.idealAnnual = v
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
/>
|
|
145
|
+
<MoneyField
|
|
146
|
+
label="Excess annual upside"
|
|
147
|
+
help="Opportunistic annual spending funded last in strong paths. Use this for lifestyle upside you are comfortable skipping before touching required or target spending."
|
|
148
|
+
learn={LEARN.spendingBudget}
|
|
149
|
+
hint="Funded after ideal; 0 = none."
|
|
150
|
+
value={e.excessAnnual ?? 0}
|
|
151
|
+
onCommit={(v) =>
|
|
152
|
+
update((d) => {
|
|
153
|
+
if (!v || v <= 0) delete d.expenses.excessAnnual
|
|
154
|
+
else d.expenses.excessAnnual = v
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
/>
|
|
158
|
+
{plan.household.people.length > 1 ? (
|
|
159
|
+
<PercentField
|
|
160
|
+
label="Survivor spending"
|
|
161
|
+
help="Household spending in years when only one of you is alive, as a percent of the couple's spending. Studies of retired couples typically land between 60% and 80% — housing and utilities barely drop, while food, travel, and healthcare for the second person do. Scales baseline + phase spending only; one-time goals, healthcare premiums, debt payments, and property costs keep their own schedules."
|
|
162
|
+
learn={LEARN.survivorSpending}
|
|
163
|
+
hint="100% = no change in survivor years."
|
|
164
|
+
step={5}
|
|
165
|
+
min={0}
|
|
166
|
+
max={100}
|
|
167
|
+
value={e.survivorSpendingPct ?? 100}
|
|
168
|
+
onCommit={(v) =>
|
|
169
|
+
update((d) => {
|
|
170
|
+
if (v === null || v >= 100) delete d.expenses.survivorSpendingPct
|
|
171
|
+
else d.expenses.survivorSpendingPct = Math.max(0, v)
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
/>
|
|
175
|
+
) : null}
|
|
176
|
+
<MoneyField
|
|
177
|
+
label="Bequest target (today's $)"
|
|
178
|
+
help="The after-tax estate you want the plan to still leave at the end, in today's dollars. Used as the estate floor by the sustainable-spending solver ('How much can I spend?') and by the estate-floor optimizer objective — it does not change the projection itself. Leave 0 for no target."
|
|
179
|
+
learn={LEARN.sustainableSpending}
|
|
180
|
+
hint="Estate floor for the spending solver and optimizer objectives; 0 = none."
|
|
181
|
+
value={e.bequestTargetDollars ?? 0}
|
|
182
|
+
onCommit={(v) =>
|
|
183
|
+
update((d) => {
|
|
184
|
+
if (!v || v <= 0) delete d.expenses.bequestTargetDollars
|
|
185
|
+
else d.expenses.bequestTargetDollars = v
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
/>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<h3>Dynamic spending policy</h3>
|
|
192
|
+
<p className="card-hint">
|
|
193
|
+
Let spending flex with the market instead of holding a fixed budget. Guardrails trim and restore the
|
|
194
|
+
discretionary layer (baseline minus the required floor) — the floor is never cut. Amortized spending
|
|
195
|
+
(ABW, the rule behind VPW and TPAW) goes further: it replaces the baseline entirely, re-computing each
|
|
196
|
+
year's spending from the actual portfolio and remaining horizon. Applies in Results and Monte Carlo.{' '}
|
|
197
|
+
<LearnLink {...LEARN.spendingBudget} />
|
|
198
|
+
</p>
|
|
199
|
+
<div className="form-grid">
|
|
200
|
+
<SelectField
|
|
201
|
+
label="Spending policy"
|
|
202
|
+
help="Fixed target funds the whole budget every year (today's behavior). Withdrawal-rate guardrails ration the discretionary layer path by path based on how the current withdrawal rate compares to the starting rate. Risk-based guardrails trigger on dollar portfolio thresholds solved from your target probability-of-success band — cut only when the plan's odds actually leave the band, not on the withdrawal rate alone. Amortized spending (ABW) ignores the baseline and phases and spends each year's amortized payment: the actual start-of-year portfolio spread over the remaining horizon at an expected real return, so spending self-corrects after good or bad markets and the portfolio is designed to be spent down by the horizon."
|
|
203
|
+
learn={LEARN.dynamicSpendingGuardrails}
|
|
204
|
+
value={e.spendingPolicy?.mode ?? 'fixedTarget'}
|
|
205
|
+
options={[
|
|
206
|
+
{ value: 'fixedTarget', label: 'Fixed target (no guardrails)' },
|
|
207
|
+
{ value: 'withdrawalRateGuardrails', label: 'Withdrawal-rate guardrails' },
|
|
208
|
+
{ value: 'riskBasedGuardrails', label: 'Risk-based guardrails (success band)' },
|
|
209
|
+
{ value: 'abw', label: 'Amortized spending (ABW / VPW)' },
|
|
210
|
+
]}
|
|
211
|
+
onCommit={(mode) =>
|
|
212
|
+
update((d) => {
|
|
213
|
+
if (mode === 'fixedTarget') delete d.expenses.spendingPolicy
|
|
214
|
+
else d.expenses.spendingPolicy = { ...d.expenses.spendingPolicy, mode }
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
/>
|
|
218
|
+
{e.spendingPolicy?.mode === 'withdrawalRateGuardrails' ? (
|
|
219
|
+
<>
|
|
220
|
+
<PercentField
|
|
221
|
+
label="Upper guardrail"
|
|
222
|
+
help="Cut discretionary spending when the current withdrawal rate exceeds this percent of the starting rate. A common setting is 120%."
|
|
223
|
+
hint="% of the starting withdrawal rate that triggers a cut."
|
|
224
|
+
learn={LEARN.spendingBudget}
|
|
225
|
+
step={5}
|
|
226
|
+
min={100}
|
|
227
|
+
max={200}
|
|
228
|
+
value={e.spendingPolicy.upperGuardrailPct ?? 120}
|
|
229
|
+
onCommit={(v) => update((d) => void (d.expenses.spendingPolicy!.upperGuardrailPct = v ?? 120))}
|
|
230
|
+
/>
|
|
231
|
+
<PercentField
|
|
232
|
+
label="Lower guardrail"
|
|
233
|
+
help="Restore discretionary spending when the current withdrawal rate falls below this percent of the starting rate. A common setting is 80%."
|
|
234
|
+
hint="% of the starting withdrawal rate that allows a raise."
|
|
235
|
+
learn={LEARN.spendingBudget}
|
|
236
|
+
step={5}
|
|
237
|
+
min={0}
|
|
238
|
+
max={100}
|
|
239
|
+
value={e.spendingPolicy.lowerGuardrailPct ?? 80}
|
|
240
|
+
onCommit={(v) => update((d) => void (d.expenses.spendingPolicy!.lowerGuardrailPct = v ?? 80))}
|
|
241
|
+
/>
|
|
242
|
+
<PercentField
|
|
243
|
+
label="Adjustment size"
|
|
244
|
+
help="How much of the full discretionary layer each cut or raise moves. A common setting is 10%."
|
|
245
|
+
hint="Cut/raise step, as a % of the discretionary layer."
|
|
246
|
+
learn={LEARN.spendingBudget}
|
|
247
|
+
step={5}
|
|
248
|
+
min={0}
|
|
249
|
+
max={100}
|
|
250
|
+
value={e.spendingPolicy.adjustmentPct ?? 10}
|
|
251
|
+
onCommit={(v) => update((d) => void (d.expenses.spendingPolicy!.adjustmentPct = v ?? 10))}
|
|
252
|
+
/>
|
|
253
|
+
<CheckboxField
|
|
254
|
+
label="Allow upside raises"
|
|
255
|
+
help="When enabled, strong paths can restore target spending and then fund ideal/excess annual layers or pull flexible goals earlier within their window. The required floor still stays protected in down markets."
|
|
256
|
+
learn={LEARN.spendingBudget}
|
|
257
|
+
value={e.spendingPolicy.allowRaisesAboveTarget ?? ((e.idealAnnual ?? 0) + (e.excessAnnual ?? 0) > 0 || hasEarlyPullFlexibleGoals)}
|
|
258
|
+
onCommit={(v) => update((d) => void (d.expenses.spendingPolicy!.allowRaisesAboveTarget = v))}
|
|
259
|
+
/>
|
|
260
|
+
</>
|
|
261
|
+
) : null}
|
|
262
|
+
{e.spendingPolicy?.mode === 'riskBasedGuardrails' ? (
|
|
263
|
+
<>
|
|
264
|
+
<PercentField
|
|
265
|
+
label="Cut when success falls below"
|
|
266
|
+
help="The lower edge of your target probability-of-success band. The solver finds the portfolio balance where the plan's Monte Carlo success probability would drop to this level; spending cuts trigger below that dollar threshold."
|
|
267
|
+
hint="Lower edge of the target success band."
|
|
268
|
+
learn={LEARN.riskBasedGuardrails}
|
|
269
|
+
step={5}
|
|
270
|
+
min={1}
|
|
271
|
+
max={99}
|
|
272
|
+
value={e.spendingPolicy.targetSuccessLowerPct ?? 70}
|
|
273
|
+
onCommit={(v) => {
|
|
274
|
+
// The solved thresholds (and any displayed suggestions) belonged to the old band.
|
|
275
|
+
setThresholdSolution(null)
|
|
276
|
+
update((d) => {
|
|
277
|
+
const policy = d.expenses.spendingPolicy!
|
|
278
|
+
// Keep the band ordered: the cut edge must stay below the raise edge.
|
|
279
|
+
policy.targetSuccessLowerPct = Math.min(v ?? 70, (policy.targetSuccessUpperPct ?? 95) - 1)
|
|
280
|
+
delete policy.lowerBalanceThresholdPct
|
|
281
|
+
delete policy.upperBalanceThresholdPct
|
|
282
|
+
})
|
|
283
|
+
}}
|
|
284
|
+
/>
|
|
285
|
+
<PercentField
|
|
286
|
+
label="Raise when success rises above"
|
|
287
|
+
help="The upper edge of your target probability-of-success band. Above the balance where success clears this level, discretionary spending can be restored or raised."
|
|
288
|
+
hint="Upper edge of the target success band."
|
|
289
|
+
learn={LEARN.riskBasedGuardrails}
|
|
290
|
+
step={5}
|
|
291
|
+
min={1}
|
|
292
|
+
max={100}
|
|
293
|
+
value={e.spendingPolicy.targetSuccessUpperPct ?? 95}
|
|
294
|
+
onCommit={(v) => {
|
|
295
|
+
setThresholdSolution(null)
|
|
296
|
+
update((d) => {
|
|
297
|
+
const policy = d.expenses.spendingPolicy!
|
|
298
|
+
policy.targetSuccessUpperPct = Math.max(v ?? 95, (policy.targetSuccessLowerPct ?? 70) + 1)
|
|
299
|
+
delete policy.lowerBalanceThresholdPct
|
|
300
|
+
delete policy.upperBalanceThresholdPct
|
|
301
|
+
})
|
|
302
|
+
}}
|
|
303
|
+
/>
|
|
304
|
+
<PercentField
|
|
305
|
+
label="Adjustment size"
|
|
306
|
+
help="How much of the full discretionary layer each cut or raise moves. A common setting is 10%."
|
|
307
|
+
hint="Cut/raise step, as a % of the discretionary layer."
|
|
308
|
+
learn={LEARN.riskBasedGuardrails}
|
|
309
|
+
step={5}
|
|
310
|
+
min={0}
|
|
311
|
+
max={100}
|
|
312
|
+
value={e.spendingPolicy.adjustmentPct ?? 10}
|
|
313
|
+
onCommit={(v) => update((d) => void (d.expenses.spendingPolicy!.adjustmentPct = v ?? 10))}
|
|
314
|
+
/>
|
|
315
|
+
<CheckboxField
|
|
316
|
+
label="Allow upside raises"
|
|
317
|
+
help="When enabled, strong paths can restore target spending and then fund ideal/excess annual layers or pull flexible goals earlier within their window. The required floor still stays protected in down markets."
|
|
318
|
+
learn={LEARN.riskBasedGuardrails}
|
|
319
|
+
value={e.spendingPolicy.allowRaisesAboveTarget ?? ((e.idealAnnual ?? 0) + (e.excessAnnual ?? 0) > 0 || hasEarlyPullFlexibleGoals)}
|
|
320
|
+
onCommit={(v) => update((d) => void (d.expenses.spendingPolicy!.allowRaisesAboveTarget = v))}
|
|
321
|
+
/>
|
|
322
|
+
</>
|
|
323
|
+
) : null}
|
|
324
|
+
{e.spendingPolicy?.mode === 'abw' ? (
|
|
325
|
+
<>
|
|
326
|
+
<SelectField
|
|
327
|
+
label="Expected return source"
|
|
328
|
+
help="Where the amortization's expected real return comes from. Fixed uses the rate you enter (the VPW preset's approach — its published global returns, 60/40 weighted, are about 3.8%/yr real). CAPE conditions on valuations: expected stock return = 100 ÷ CAPE (the cyclically-adjusted earnings yield), blended with the bond yield at your stock share — richer valuations mean lower planned spending. TIPS yield prices the whole portfolio at a real bond yield — the most conservative reading."
|
|
329
|
+
learn={LEARN.spendingBudget}
|
|
330
|
+
value={e.spendingPolicy.abw?.returnSource ?? 'fixed'}
|
|
331
|
+
options={[
|
|
332
|
+
{ value: 'fixed', label: 'Fixed real return (VPW-style)' },
|
|
333
|
+
{ value: 'cape', label: 'CAPE earnings yield (valuation-aware)' },
|
|
334
|
+
{ value: 'tips', label: 'TIPS real yield (most conservative)' },
|
|
335
|
+
]}
|
|
336
|
+
onCommit={(v) => setAbw((abw) => void (abw.returnSource = v))}
|
|
337
|
+
/>
|
|
338
|
+
{(e.spendingPolicy.abw?.returnSource ?? 'fixed') === 'fixed' ? (
|
|
339
|
+
<PercentField
|
|
340
|
+
label="Expected real return"
|
|
341
|
+
help="Expected portfolio return per year above inflation, used to amortize the balance over the remaining horizon. The Bogleheads VPW table's global internal rates of return (stocks 5.0%, bonds 1.9% real) weighted 60/40 give about 3.8%. Higher values front-load spending and risk deeper later cuts if markets disappoint."
|
|
342
|
+
hint="%/yr above inflation. VPW 60/40 ≈ 3.8%."
|
|
343
|
+
learn={LEARN.spendingBudget}
|
|
344
|
+
step={0.1}
|
|
345
|
+
min={-5}
|
|
346
|
+
max={12}
|
|
347
|
+
value={e.spendingPolicy.abw?.fixedRealReturnPct ?? 3.8}
|
|
348
|
+
onCommit={(v) => setAbw((abw) => void (abw.fixedRealReturnPct = v ?? 3.8))}
|
|
349
|
+
/>
|
|
350
|
+
) : null}
|
|
351
|
+
{e.spendingPolicy.abw?.returnSource === 'cape' ? (
|
|
352
|
+
<>
|
|
353
|
+
<NumberField
|
|
354
|
+
label="Starting CAPE"
|
|
355
|
+
help="The cyclically-adjusted price/earnings ratio used for the expected stock return (100 ÷ CAPE). Around 25 matches this app's CAPE-conditioned market model default; check a current published CAPE for today's value."
|
|
356
|
+
hint="Expected stock return = 100 ÷ CAPE."
|
|
357
|
+
learn={LEARN.spendingBudget}
|
|
358
|
+
step={1}
|
|
359
|
+
min={5}
|
|
360
|
+
max={60}
|
|
361
|
+
value={e.spendingPolicy.abw?.startingCape ?? 25}
|
|
362
|
+
onCommit={(v) => setAbw((abw) => void (abw.startingCape = v ?? 25))}
|
|
363
|
+
/>
|
|
364
|
+
<PercentField
|
|
365
|
+
label="Stock share"
|
|
366
|
+
help="How much of the portfolio is priced at the CAPE earnings yield; the rest is priced at the real bond yield below."
|
|
367
|
+
hint="Blends the CAPE yield with the bond yield."
|
|
368
|
+
learn={LEARN.spendingBudget}
|
|
369
|
+
step={5}
|
|
370
|
+
min={0}
|
|
371
|
+
max={100}
|
|
372
|
+
value={e.spendingPolicy.abw?.equitySharePct ?? 60}
|
|
373
|
+
onCommit={(v) => setAbw((abw) => void (abw.equitySharePct = v ?? 60))}
|
|
374
|
+
/>
|
|
375
|
+
</>
|
|
376
|
+
) : null}
|
|
377
|
+
{e.spendingPolicy.abw?.returnSource === 'cape' || e.spendingPolicy.abw?.returnSource === 'tips' ? (
|
|
378
|
+
<PercentField
|
|
379
|
+
label="Real bond/TIPS yield"
|
|
380
|
+
help="The real (above-inflation) bond yield: the whole portfolio under the TIPS source, or the non-stock share under CAPE. Long TIPS real yields were near 2% in mid-2026; check the current curve."
|
|
381
|
+
hint="%/yr above inflation; ~2% in mid-2026."
|
|
382
|
+
learn={LEARN.spendingBudget}
|
|
383
|
+
step={0.1}
|
|
384
|
+
min={-2}
|
|
385
|
+
max={8}
|
|
386
|
+
value={e.spendingPolicy.abw?.bondRealYieldPct ?? 2}
|
|
387
|
+
onCommit={(v) => setAbw((abw) => void (abw.bondRealYieldPct = v ?? 2))}
|
|
388
|
+
/>
|
|
389
|
+
) : null}
|
|
390
|
+
<SelectField
|
|
391
|
+
label="Amortize to"
|
|
392
|
+
help="The horizon the balance is spread over. Planning age uses the household's plan horizon. The survival percentiles amortize to the age you (for couples: either of you) have a 25% or 10% chance of reaching — the unadjusted SSA life table, with no health-questionnaire adjustment even if your planning age used one — a shorter, spendier horizon than a conservative planning age."
|
|
393
|
+
learn={LEARN.longevity}
|
|
394
|
+
value={e.spendingPolicy.abw?.horizon ?? 'planningAge'}
|
|
395
|
+
options={[
|
|
396
|
+
{ value: 'planningAge', label: 'Planning age (plan horizon)' },
|
|
397
|
+
{ value: 'survival25', label: 'Age with 25% chance of reaching' },
|
|
398
|
+
{ value: 'survival10', label: 'Age with 10% chance of reaching' },
|
|
399
|
+
]}
|
|
400
|
+
onCommit={(v) => setAbw((abw) => void (abw.horizon = v))}
|
|
401
|
+
/>
|
|
402
|
+
<PercentField
|
|
403
|
+
label="Spending tilt"
|
|
404
|
+
help="Planned real change in spending per year. Negative front-loads spending into early retirement (consistent with the observed decline in real retiree spending); positive defers it. 0 plans level real spending."
|
|
405
|
+
hint="−1 to −1.5%/yr matches observed spending declines."
|
|
406
|
+
learn={LEARN.spendingProfiles}
|
|
407
|
+
step={0.5}
|
|
408
|
+
min={-5}
|
|
409
|
+
max={5}
|
|
410
|
+
value={e.spendingPolicy.abw?.tiltPct ?? 0}
|
|
411
|
+
onCommit={(v) => setAbw((abw) => void (abw.tiltPct = v ?? 0))}
|
|
412
|
+
/>
|
|
413
|
+
</>
|
|
414
|
+
) : null}
|
|
415
|
+
</div>
|
|
416
|
+
{e.spendingPolicy?.mode === 'abw' ? (
|
|
417
|
+
<div className="callout callout--info">
|
|
418
|
+
<p className="card-hint">
|
|
419
|
+
Amortized spending replaces the baseline, retirement phases, and required/ideal/excess layers: each
|
|
420
|
+
year's recurring lifestyle spending is the amortized payment from the actual start-of-year
|
|
421
|
+
portfolio. Healthcare, debt payments, property costs, insurance premiums, and one-time goals stay
|
|
422
|
+
separately modeled on top. Because the payment is recomputed every year, spending self-corrects after
|
|
423
|
+
market surprises instead of failing — the trade-off is a variable budget.{' '}
|
|
424
|
+
<button
|
|
425
|
+
type="button"
|
|
426
|
+
className="btn btn-secondary btn-small"
|
|
427
|
+
onClick={() =>
|
|
428
|
+
update((d) => {
|
|
429
|
+
d.expenses.spendingPolicy = {
|
|
430
|
+
mode: 'abw',
|
|
431
|
+
abw: { returnSource: 'fixed', fixedRealReturnPct: 3.8, horizon: 'planningAge', tiltPct: 0 },
|
|
432
|
+
}
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
>
|
|
436
|
+
Apply the VPW preset
|
|
437
|
+
</button>
|
|
438
|
+
</p>
|
|
439
|
+
</div>
|
|
440
|
+
) : null}
|
|
441
|
+
{e.spendingPolicy?.mode === 'riskBasedGuardrails' ? (
|
|
442
|
+
<div className="callout callout--info">
|
|
443
|
+
{e.spendingPolicy.lowerBalanceThresholdPct !== undefined ||
|
|
444
|
+
e.spendingPolicy.upperBalanceThresholdPct !== undefined ? (
|
|
445
|
+
<p className="card-hint">
|
|
446
|
+
Solved dollar guardrails for the {e.spendingPolicy.targetSuccessLowerPct ?? 70}–
|
|
447
|
+
{e.spendingPolicy.targetSuccessUpperPct ?? 95}% success band:{' '}
|
|
448
|
+
{e.spendingPolicy.lowerBalanceThresholdPct !== undefined ? (
|
|
449
|
+
<>
|
|
450
|
+
cut spending if the portfolio falls below{' '}
|
|
451
|
+
<strong>{fmtMoney((e.spendingPolicy.lowerBalanceThresholdPct / 100) * startingInvestableOf(plan))}</strong>
|
|
452
|
+
</>
|
|
453
|
+
) : (
|
|
454
|
+
<>no cut threshold was solved for this band</>
|
|
455
|
+
)}
|
|
456
|
+
{'; '}
|
|
457
|
+
{e.spendingPolicy.upperBalanceThresholdPct !== undefined ? (
|
|
458
|
+
<>
|
|
459
|
+
raise if it rises above{' '}
|
|
460
|
+
<strong>{fmtMoney((e.spendingPolicy.upperBalanceThresholdPct / 100) * startingInvestableOf(plan))}</strong>
|
|
461
|
+
</>
|
|
462
|
+
) : (
|
|
463
|
+
<>no raise threshold was solved for this band</>
|
|
464
|
+
)}
|
|
465
|
+
. Thresholds are in today's dollars, solved under the standard smooth-randomness market model
|
|
466
|
+
(12% return volatility, 60/40 weighting) with your plan's inflation — custom Monte Carlo page
|
|
467
|
+
model settings are not reflected here. Re-solve after meaningful plan changes.
|
|
468
|
+
</p>
|
|
469
|
+
) : (
|
|
470
|
+
<p className="card-hint">
|
|
471
|
+
No dollar thresholds solved yet — until they are computed, this policy holds spending steady (it
|
|
472
|
+
behaves like fixed target). Solving runs a bounded Monte Carlo search in the background under the
|
|
473
|
+
standard smooth-randomness market model.
|
|
474
|
+
</p>
|
|
475
|
+
)}
|
|
476
|
+
{thresholdSolution?.lowerOutcome === 'never-reaches-band' ? (
|
|
477
|
+
<p className="card-hint">
|
|
478
|
+
<strong>Heads up:</strong> the plan's success probability stays below your{' '}
|
|
479
|
+
{thresholdSolution.lowerBandPct}% cut edge even with several times the current portfolio, so no
|
|
480
|
+
cut threshold exists — the plan is underfunded for this band, not safe. Consider lower target
|
|
481
|
+
spending or a lower band.
|
|
482
|
+
</p>
|
|
483
|
+
) : null}
|
|
484
|
+
{thresholdSolution?.lowerOutcome === 'always-above-band' ? (
|
|
485
|
+
<p className="card-hint">
|
|
486
|
+
The plan stays above the {thresholdSolution.lowerBandPct}% cut edge even at very low balances
|
|
487
|
+
(guaranteed income carries it), so no cut trigger is needed.
|
|
488
|
+
</p>
|
|
489
|
+
) : null}
|
|
490
|
+
{thresholdSolution?.suggestedCut || thresholdSolution?.suggestedRaise ? (
|
|
491
|
+
<p className="card-hint">
|
|
492
|
+
{thresholdSolution.suggestedCut ? (
|
|
493
|
+
<>
|
|
494
|
+
At the cut threshold, trimming about{' '}
|
|
495
|
+
<strong>{fmtMoney(thresholdSolution.suggestedCut.monthlyDollars)}/mo</strong> restores the middle of
|
|
496
|
+
the band.{' '}
|
|
497
|
+
</>
|
|
498
|
+
) : null}
|
|
499
|
+
{thresholdSolution.suggestedRaise ? (
|
|
500
|
+
<>
|
|
501
|
+
At the raise threshold, roughly{' '}
|
|
502
|
+
<strong>{fmtMoney(thresholdSolution.suggestedRaise.monthlyDollars)}/mo</strong> of extra spending
|
|
503
|
+
still keeps the plan above the middle of the band.
|
|
504
|
+
</>
|
|
505
|
+
) : null}
|
|
506
|
+
</p>
|
|
507
|
+
) : null}
|
|
508
|
+
<button
|
|
509
|
+
type="button"
|
|
510
|
+
className="btn btn-secondary btn-small"
|
|
511
|
+
disabled={solvingThresholds}
|
|
512
|
+
onClick={solveThresholds}
|
|
513
|
+
>
|
|
514
|
+
{solvingThresholds
|
|
515
|
+
? 'Solving thresholds…'
|
|
516
|
+
: e.spendingPolicy.lowerBalanceThresholdPct !== undefined ||
|
|
517
|
+
e.spendingPolicy.upperBalanceThresholdPct !== undefined
|
|
518
|
+
? 'Re-solve dollar thresholds'
|
|
519
|
+
: 'Solve dollar thresholds'}
|
|
520
|
+
</button>
|
|
521
|
+
{thresholdSolveError ? <p className="card-hint error-text">{thresholdSolveError}</p> : null}
|
|
522
|
+
</div>
|
|
523
|
+
) : null}
|
|
524
|
+
|
|
525
|
+
<h3>Retirement phases</h3>
|
|
526
|
+
<p className="card-hint">
|
|
527
|
+
Spending multipliers by the primary person's age (go-go / slow-go / no-go).{' '}
|
|
528
|
+
<LearnLink {...LEARN.spendingProfiles} />
|
|
529
|
+
</p>
|
|
530
|
+
{e.phases.map((p, i) => (
|
|
531
|
+
<div className="item-row" key={i}>
|
|
532
|
+
<div className="item-row-head">
|
|
533
|
+
<span className="item-row-title"><span className="type-chip">Phase</span>from age {p.fromAge}</span>
|
|
534
|
+
<button type="button" className="btn-ghost btn-ghost-danger" onClick={() => update((d) => void d.expenses.phases.splice(i, 1))}>Remove</button>
|
|
535
|
+
</div>
|
|
536
|
+
<div className="form-grid">
|
|
537
|
+
<NumberField
|
|
538
|
+
label="From age"
|
|
539
|
+
help="The first age when this phase applies, using the primary person's age as the clock."
|
|
540
|
+
learn={LEARN.spendingProfiles}
|
|
541
|
+
value={p.fromAge}
|
|
542
|
+
min={40}
|
|
543
|
+
max={110}
|
|
544
|
+
onCommit={(v) => update((d) => void (d.expenses.phases[i]!.fromAge = Math.round(v ?? 65)))}
|
|
545
|
+
/>
|
|
546
|
+
<NumberField
|
|
547
|
+
label="Multiplier"
|
|
548
|
+
help="Multiplies baseline spending from this age forward. For example, 0.90 means recurring lifestyle spending is 10% lower before inflation."
|
|
549
|
+
hint="1.00 = no change."
|
|
550
|
+
learn={LEARN.spendingProfiles}
|
|
551
|
+
step={0.05}
|
|
552
|
+
value={p.multiplier}
|
|
553
|
+
min={0}
|
|
554
|
+
max={3}
|
|
555
|
+
onCommit={(v) => update((d) => void (d.expenses.phases[i]!.multiplier = v ?? 1))}
|
|
556
|
+
/>
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
559
|
+
))}
|
|
560
|
+
<div className="add-row">
|
|
561
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={() => update((d) => void d.expenses.phases.push({ fromAge: 75, multiplier: 0.9 }))}>+ Phase</button>
|
|
562
|
+
</div>
|
|
563
|
+
<p className="field-hint" style={{ margin: '0.6rem 0 0.25rem' }}>
|
|
564
|
+
Profiles write ordinary phase rows you can edit afterwards{e.phases.length > 0 ? ' (replacing the phases above)' : ''}:{' '}<LearnLink {...LEARN.spendingProfiles} />
|
|
565
|
+
</p>
|
|
566
|
+
<div className="add-row">
|
|
567
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={() => applyProfile('flat')}>
|
|
568
|
+
Constant-real (no phases)
|
|
569
|
+
</button>
|
|
570
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={() => applyProfile('smile')}>
|
|
571
|
+
Retirement smile (−10% at 75, −20% at 85)
|
|
572
|
+
</button>
|
|
573
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={() => applyProfile('smirk')}>
|
|
574
|
+
Retirement smirk (−1%/yr real)
|
|
575
|
+
</button>
|
|
576
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={() => applyProfile('frontLoaded')}>
|
|
577
|
+
Front-loaded travel (+10% until 75)
|
|
578
|
+
</button>
|
|
579
|
+
</div>
|
|
580
|
+
<p className="field-hint" style={{ margin: '0.6rem 0 0.25rem' }}>
|
|
581
|
+
The smile is the shape of <em>average</em> retiree spending — a decline that late healthcare partly
|
|
582
|
+
reverses (the preset approximates it as two downward steps); the smirk is the <em>median</em> — a steady
|
|
583
|
+
real decline with no late rise, per Blanchett's spending research. Or pick your own annual real
|
|
584
|
+
drift, compiled to 5-year phase steps:
|
|
585
|
+
</p>
|
|
586
|
+
<div className="add-row" style={{ alignItems: 'end' }}>
|
|
587
|
+
<PercentField
|
|
588
|
+
label="Custom real change per year"
|
|
589
|
+
help="Your own steady real spending drift, applied from retirement and compiled into 5-year phase steps (the compounded multiplier at each step age, to age 100). Research on actual retirees clusters around −1% to −2%/yr; a positive value plans rising real spending."
|
|
590
|
+
hint="Negative = declining real spending."
|
|
591
|
+
learn={LEARN.spendingProfiles}
|
|
592
|
+
step={0.5}
|
|
593
|
+
min={-5}
|
|
594
|
+
max={5}
|
|
595
|
+
value={customDeltaPct}
|
|
596
|
+
onCommit={(v) => setCustomDeltaPct(v ?? -1.5)}
|
|
597
|
+
/>
|
|
598
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={applyCustomDelta}>
|
|
599
|
+
Apply custom shape
|
|
600
|
+
</button>
|
|
601
|
+
</div>
|
|
602
|
+
|
|
603
|
+
<h3>One-time goals</h3>
|
|
604
|
+
<p className="card-hint">
|
|
605
|
+
Big, named purchases or gifts that should happen once instead of becoming part of everyday spending.{' '}
|
|
606
|
+
<LearnLink {...LEARN.spendingBudget} />
|
|
607
|
+
</p>
|
|
608
|
+
<div className="add-row">
|
|
609
|
+
<button
|
|
610
|
+
type="button"
|
|
611
|
+
className="btn btn-secondary btn-small"
|
|
612
|
+
onClick={() =>
|
|
613
|
+
update((d) => {
|
|
614
|
+
const targetYear = new Date().getFullYear() + 3
|
|
615
|
+
d.expenses.oneTimeGoals.push({
|
|
616
|
+
id: newId(),
|
|
617
|
+
label: 'Car replacement',
|
|
618
|
+
year: targetYear,
|
|
619
|
+
amount: 35_000,
|
|
620
|
+
classification: 'target',
|
|
621
|
+
flexibility: 'movable',
|
|
622
|
+
earliestYear: targetYear - 2,
|
|
623
|
+
latestYear: targetYear,
|
|
624
|
+
})
|
|
625
|
+
})
|
|
626
|
+
}
|
|
627
|
+
>
|
|
628
|
+
Car replacement
|
|
629
|
+
</button>
|
|
630
|
+
<button
|
|
631
|
+
type="button"
|
|
632
|
+
className="btn btn-secondary btn-small"
|
|
633
|
+
onClick={() =>
|
|
634
|
+
update((d) => {
|
|
635
|
+
const targetYear = new Date().getFullYear() + 2
|
|
636
|
+
d.expenses.oneTimeGoals.push({
|
|
637
|
+
id: newId(),
|
|
638
|
+
label: 'Home improvement',
|
|
639
|
+
year: targetYear,
|
|
640
|
+
amount: 50_000,
|
|
641
|
+
classification: 'ideal',
|
|
642
|
+
flexibility: 'movable',
|
|
643
|
+
earliestYear: targetYear,
|
|
644
|
+
latestYear: targetYear + 2,
|
|
645
|
+
allowPartialFunding: true,
|
|
646
|
+
minFundingPct: 50,
|
|
647
|
+
})
|
|
648
|
+
})
|
|
649
|
+
}
|
|
650
|
+
>
|
|
651
|
+
Home improvement
|
|
652
|
+
</button>
|
|
653
|
+
<button
|
|
654
|
+
type="button"
|
|
655
|
+
className="btn btn-secondary btn-small"
|
|
656
|
+
onClick={() =>
|
|
657
|
+
update((d) => {
|
|
658
|
+
const targetYear = new Date().getFullYear() + 1
|
|
659
|
+
d.expenses.oneTimeGoals.push({
|
|
660
|
+
id: newId(),
|
|
661
|
+
label: 'Big trip',
|
|
662
|
+
year: targetYear,
|
|
663
|
+
amount: 12_000,
|
|
664
|
+
classification: 'excess',
|
|
665
|
+
flexibility: 'skippable',
|
|
666
|
+
earliestYear: targetYear,
|
|
667
|
+
latestYear: targetYear + 1,
|
|
668
|
+
})
|
|
669
|
+
})
|
|
670
|
+
}
|
|
671
|
+
>
|
|
672
|
+
Big trip
|
|
673
|
+
</button>
|
|
674
|
+
</div>
|
|
675
|
+
{e.oneTimeGoals.map((g, i) => (
|
|
676
|
+
<div className="item-row" key={g.id}>
|
|
677
|
+
<div className="item-row-head">
|
|
678
|
+
<span className="item-row-title"><span className="type-chip">Goal</span>{g.label}</span>
|
|
679
|
+
<button type="button" className="btn-ghost btn-ghost-danger" onClick={() => update((d) => void d.expenses.oneTimeGoals.splice(i, 1))}>Remove</button>
|
|
680
|
+
</div>
|
|
681
|
+
<div className="form-grid">
|
|
682
|
+
<TextField
|
|
683
|
+
label="Label"
|
|
684
|
+
help="Name the goal so it is recognizable in Results, such as travel, car replacement, wedding gift, or home project."
|
|
685
|
+
learn={LEARN.spendingBudget}
|
|
686
|
+
value={g.label}
|
|
687
|
+
onCommit={(v) => update((d) => void (d.expenses.oneTimeGoals[i]!.label = v || 'Goal'))}
|
|
688
|
+
/>
|
|
689
|
+
<NumberField
|
|
690
|
+
label="Year"
|
|
691
|
+
help="The calendar year the goal is funded. The amount is inflated from today's dollars to that year."
|
|
692
|
+
learn={LEARN.spendingBudget}
|
|
693
|
+
value={g.year}
|
|
694
|
+
min={1900}
|
|
695
|
+
max={2200}
|
|
696
|
+
onCommit={(v) =>
|
|
697
|
+
update((d) => {
|
|
698
|
+
const goal = d.expenses.oneTimeGoals[i]!
|
|
699
|
+
goal.year = Math.round(v ?? g.year)
|
|
700
|
+
if (goal.earliestYear !== undefined) goal.earliestYear = Math.min(goal.earliestYear, goal.year)
|
|
701
|
+
if (goal.latestYear !== undefined) goal.latestYear = Math.max(goal.latestYear, goal.year)
|
|
702
|
+
})
|
|
703
|
+
}
|
|
704
|
+
/>
|
|
705
|
+
<MoneyField
|
|
706
|
+
label="Amount (today's $)"
|
|
707
|
+
help="The one-time cost in today's dollars. Keep recurring lifestyle costs in baseline spending instead."
|
|
708
|
+
learn={LEARN.spendingBudget}
|
|
709
|
+
value={g.amount}
|
|
710
|
+
onCommit={(v) => update((d) => void (d.expenses.oneTimeGoals[i]!.amount = v ?? 0))}
|
|
711
|
+
/>
|
|
712
|
+
<SelectField<SpendingClassification>
|
|
713
|
+
label="Layer"
|
|
714
|
+
help="Required goals are part of the must-fund floor. Target goals are intended lifestyle. Ideal and excess goals are flexible upside funded after target spending."
|
|
715
|
+
learn={LEARN.spendingBudget}
|
|
716
|
+
value={g.classification ?? 'target'}
|
|
717
|
+
options={[
|
|
718
|
+
{ value: 'required', label: 'Required' },
|
|
719
|
+
{ value: 'target', label: 'Target' },
|
|
720
|
+
{ value: 'ideal', label: 'Ideal' },
|
|
721
|
+
{ value: 'excess', label: 'Excess' },
|
|
722
|
+
]}
|
|
723
|
+
onCommit={(classification) =>
|
|
724
|
+
update((d) => {
|
|
725
|
+
const goal = d.expenses.oneTimeGoals[i]!
|
|
726
|
+
if (classification === 'target') delete goal.classification
|
|
727
|
+
else goal.classification = classification
|
|
728
|
+
})
|
|
729
|
+
}
|
|
730
|
+
/>
|
|
731
|
+
<SelectField
|
|
732
|
+
label="Flexibility"
|
|
733
|
+
help="Fixed happens in its year no matter what. Movable funds in its year normally, but is delayed up to the latest year while guardrails are cutting discretionary spending. Skippable is the same but dropped entirely if it is still unaffordable at the latest year. Only matters when Spending guardrails are on."
|
|
734
|
+
learn={LEARN.spendingBudget}
|
|
735
|
+
value={g.flexibility ?? 'fixed'}
|
|
736
|
+
options={[
|
|
737
|
+
{ value: 'fixed', label: 'Fixed (happens in its year)' },
|
|
738
|
+
{ value: 'movable', label: 'Movable (delay under a cut)' },
|
|
739
|
+
{ value: 'skippable', label: 'Skippable (drop if unaffordable)' },
|
|
740
|
+
]}
|
|
741
|
+
onCommit={(flex: GoalFlexibility) =>
|
|
742
|
+
update((d) => {
|
|
743
|
+
const goal = d.expenses.oneTimeGoals[i]!
|
|
744
|
+
if (flex === 'fixed') {
|
|
745
|
+
delete goal.flexibility
|
|
746
|
+
delete goal.earliestYear
|
|
747
|
+
delete goal.latestYear
|
|
748
|
+
delete goal.priority
|
|
749
|
+
delete goal.minFundingPct
|
|
750
|
+
delete goal.allowPartialFunding
|
|
751
|
+
} else {
|
|
752
|
+
goal.flexibility = flex
|
|
753
|
+
goal.earliestYear ??= goal.year
|
|
754
|
+
goal.latestYear ??= goal.year
|
|
755
|
+
}
|
|
756
|
+
})
|
|
757
|
+
}
|
|
758
|
+
/>
|
|
759
|
+
{g.flexibility && g.flexibility !== 'fixed' ? (
|
|
760
|
+
<>
|
|
761
|
+
<NumberField
|
|
762
|
+
label="Earliest year"
|
|
763
|
+
help="The earliest year a strong guardrail path may pull this goal forward. Leave equal to the target year to prevent acceleration."
|
|
764
|
+
learn={LEARN.spendingBudget}
|
|
765
|
+
value={g.earliestYear ?? g.year}
|
|
766
|
+
min={1900}
|
|
767
|
+
max={2200}
|
|
768
|
+
onCommit={(v) =>
|
|
769
|
+
update((d) => void (d.expenses.oneTimeGoals[i]!.earliestYear = Math.min(Math.round(v ?? g.year), g.year)))
|
|
770
|
+
}
|
|
771
|
+
/>
|
|
772
|
+
<NumberField
|
|
773
|
+
label="Latest year"
|
|
774
|
+
help="The latest year this goal may be delayed to. If it cannot be funded by then, the unfunded amount is reported as a layer shortfall."
|
|
775
|
+
learn={LEARN.spendingBudget}
|
|
776
|
+
value={g.latestYear ?? g.year}
|
|
777
|
+
min={1900}
|
|
778
|
+
max={2200}
|
|
779
|
+
onCommit={(v) =>
|
|
780
|
+
update((d) => void (d.expenses.oneTimeGoals[i]!.latestYear = Math.max(Math.round(v ?? g.year), g.year)))
|
|
781
|
+
}
|
|
782
|
+
/>
|
|
783
|
+
<NumberField
|
|
784
|
+
label="Priority"
|
|
785
|
+
help="Lower numbers fund first within the same spending layer. Required goals still outrank target, ideal, and excess goals."
|
|
786
|
+
learn={LEARN.spendingBudget}
|
|
787
|
+
value={g.priority ?? i}
|
|
788
|
+
min={0}
|
|
789
|
+
max={999}
|
|
790
|
+
onCommit={(v) => update((d) => void (d.expenses.oneTimeGoals[i]!.priority = Math.round(v ?? i)))}
|
|
791
|
+
/>
|
|
792
|
+
<CheckboxField
|
|
793
|
+
label="Allow partial funding"
|
|
794
|
+
help="When enabled, a flexible goal can resolve with less than the full amount if the hard flexible-goal budget clears the minimum funding percent."
|
|
795
|
+
learn={LEARN.spendingBudget}
|
|
796
|
+
value={g.allowPartialFunding ?? false}
|
|
797
|
+
onCommit={(v) =>
|
|
798
|
+
update((d) => {
|
|
799
|
+
const goal = d.expenses.oneTimeGoals[i]!
|
|
800
|
+
if (!v) {
|
|
801
|
+
delete goal.allowPartialFunding
|
|
802
|
+
delete goal.minFundingPct
|
|
803
|
+
} else {
|
|
804
|
+
goal.allowPartialFunding = true
|
|
805
|
+
goal.minFundingPct ??= 50
|
|
806
|
+
}
|
|
807
|
+
})
|
|
808
|
+
}
|
|
809
|
+
/>
|
|
810
|
+
{g.allowPartialFunding ? (
|
|
811
|
+
<PercentField
|
|
812
|
+
label="Minimum funding"
|
|
813
|
+
help="The smallest percent of the goal that must be available before RetireGolden records it as partially funded instead of deferred or skipped."
|
|
814
|
+
learn={LEARN.spendingBudget}
|
|
815
|
+
step={5}
|
|
816
|
+
min={0}
|
|
817
|
+
max={95}
|
|
818
|
+
value={g.minFundingPct ?? 50}
|
|
819
|
+
onCommit={(v) =>
|
|
820
|
+
update((d) => void (d.expenses.oneTimeGoals[i]!.minFundingPct = Math.min(95, Math.max(0, v ?? 50))))
|
|
821
|
+
}
|
|
822
|
+
/>
|
|
823
|
+
) : null}
|
|
824
|
+
</>
|
|
825
|
+
) : null}
|
|
826
|
+
</div>
|
|
827
|
+
</div>
|
|
828
|
+
))}
|
|
829
|
+
<div className="add-row">
|
|
830
|
+
<button
|
|
831
|
+
type="button"
|
|
832
|
+
className="btn btn-secondary btn-small"
|
|
833
|
+
onClick={() => update((d) => void d.expenses.oneTimeGoals.push({ id: newId(), label: 'New goal', year: new Date().getFullYear() + 2, amount: 10_000 }))}
|
|
834
|
+
>
|
|
835
|
+
+ Goal
|
|
836
|
+
</button>
|
|
837
|
+
</div>
|
|
838
|
+
|
|
839
|
+
<h3>Healthcare</h3>
|
|
840
|
+
<div className="form-grid">
|
|
841
|
+
<MoneyField
|
|
842
|
+
label="Pre-65 premium / person / month"
|
|
843
|
+
help="Enter the full unsubsidized monthly premium before any ACA credit. As a rough 2026 check, KFF's national benchmark Silver premium for a 40-year-old is $625/month, but older retirees and local quotes can differ a lot."
|
|
844
|
+
learn={LEARN.healthcareBefore65}
|
|
845
|
+
hint="Full (unsubsidized) marketplace premium."
|
|
846
|
+
value={e.healthcare.pre65MonthlyPremiumPerPerson}
|
|
847
|
+
onCommit={(v) => update((d) => void (d.expenses.healthcare.pre65MonthlyPremiumPerPerson = v ?? 0))}
|
|
848
|
+
/>
|
|
849
|
+
<MoneyField
|
|
850
|
+
label="Medicare extras / person / month"
|
|
851
|
+
help="Enter recurring post-65 coverage costs beyond standard Part B: Part D, Medigap, Medicare Advantage, dental, vision, or similar premiums. RetireGolden adds the 2026 Part B base premium ($202.90/month) and IRMAA separately."
|
|
852
|
+
learn={LEARN.healthcareAfter65}
|
|
853
|
+
hint="Part D, Medigap/Advantage; Part B + IRMAA added automatically."
|
|
854
|
+
value={e.healthcare.medicareExtrasMonthlyPerPerson}
|
|
855
|
+
onCommit={(v) => update((d) => void (d.expenses.healthcare.medicareExtrasMonthlyPerPerson = v ?? 0))}
|
|
856
|
+
/>
|
|
857
|
+
<CheckboxField
|
|
858
|
+
label="Apply ACA premium credit"
|
|
859
|
+
help="Applies a simplified ACA premium tax credit before Medicare eligibility, based on household size and MAGI. Use only for Marketplace coverage that can receive the credit."
|
|
860
|
+
learn={LEARN.acaCredit}
|
|
861
|
+
hint="Models the 400% FPL cliff against your MAGI."
|
|
862
|
+
value={e.healthcare.applyAcaCredit}
|
|
863
|
+
onCommit={(v) => update((d) => void (d.expenses.healthcare.applyAcaCredit = v))}
|
|
864
|
+
/>
|
|
865
|
+
{plan.household.filingStatus === 'marriedFilingJointly' && plan.household.people.length === 2 ? (
|
|
866
|
+
<CheckboxField
|
|
867
|
+
label="Model SSA-44 IRMAA relief in survivor years"
|
|
868
|
+
help="Form SSA-44 lets a surviving spouse ask Social Security to base Medicare's income surcharge (IRMAA) on the current year's lower income instead of the usual two-year lookback. This models the effect of a granted redetermination in the two years after the first death; filing the form itself is up to you."
|
|
869
|
+
learn={LEARN.ssa44}
|
|
870
|
+
hint="Death of spouse is a qualifying life-changing event."
|
|
871
|
+
value={e.healthcare.ssa44?.survivorYears ?? false}
|
|
872
|
+
onCommit={(v) =>
|
|
873
|
+
update((d) => {
|
|
874
|
+
const prior = d.expenses.healthcare.ssa44
|
|
875
|
+
d.expenses.healthcare.ssa44 = { survivorYears: v, retirementYears: prior?.retirementYears ?? false }
|
|
876
|
+
})
|
|
877
|
+
}
|
|
878
|
+
/>
|
|
879
|
+
) : null}
|
|
880
|
+
<CheckboxField
|
|
881
|
+
label="Model SSA-44 IRMAA relief in retirement years"
|
|
882
|
+
help="Stopping work is also an SSA-44 qualifying event. This applies the same redetermination in the two years after each person's retirement year, when the lookback still reflects working income."
|
|
883
|
+
learn={LEARN.ssa44}
|
|
884
|
+
hint="Work stoppage is a qualifying life-changing event."
|
|
885
|
+
value={e.healthcare.ssa44?.retirementYears ?? false}
|
|
886
|
+
onCommit={(v) =>
|
|
887
|
+
update((d) => {
|
|
888
|
+
const prior = d.expenses.healthcare.ssa44
|
|
889
|
+
d.expenses.healthcare.ssa44 = { survivorYears: prior?.survivorYears ?? false, retirementYears: v }
|
|
890
|
+
})
|
|
891
|
+
}
|
|
892
|
+
/>
|
|
893
|
+
</div>
|
|
894
|
+
<Issues />
|
|
895
|
+
</div>
|
|
896
|
+
<LearnAboutScreen route="/plan/:planId/spending" />
|
|
897
|
+
</section>
|
|
898
|
+
)
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// ---------------------------------------------------------------------------
|
|
902
|
+
// Strategy
|
|
903
|
+
// ---------------------------------------------------------------------------
|
|
904
|
+
|