@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,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "How much can I spend?" (sustainable-spending plan, Step 4): runs the
|
|
3
|
+
* exact-ledger max-spending solver (`solveMaxSustainableSpending`) in a Web
|
|
4
|
+
* Worker and presents the answer with its constraints — the no-depletion
|
|
5
|
+
* horizon and the optional bequest target from the Spending screen — plus the
|
|
6
|
+
* exact-ledger evidence at the solved level. Deterministic: identical inputs
|
|
7
|
+
* re-solve to identical answers under the fixed ~25-simulation budget.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
11
|
+
import { Link, useNavigate } from 'react-router-dom'
|
|
12
|
+
|
|
13
|
+
import { compareSwrRules } from '@retiregolden/engine/decisions/swrComparator'
|
|
14
|
+
import { startingInvestableOf } from '@retiregolden/engine/montecarlo/riskBasedGuardrails'
|
|
15
|
+
import { spendingShapePhases, type SpendingShapeId } from '@retiregolden/engine/spending/shapePresets'
|
|
16
|
+
import type { Plan } from '@retiregolden/engine/model/plan'
|
|
17
|
+
import type { SpendingSolveResult } from '../optimize/spendingMessages'
|
|
18
|
+
import { runSpendingSolve } from '../optimize/spendingRunner'
|
|
19
|
+
import { usePlan } from './planContextCore'
|
|
20
|
+
import { HelpTip } from './fields'
|
|
21
|
+
import { LearnAboutScreen } from '../learn/LearnAboutScreen'
|
|
22
|
+
import { LearnLink } from '../learn/LearnLink'
|
|
23
|
+
import { fmtMoney } from './format'
|
|
24
|
+
import { LEARN } from './learnLinks'
|
|
25
|
+
import { currentStartYear, taxCalculatorFor } from './useProjection'
|
|
26
|
+
|
|
27
|
+
function makeScenarioId(): string {
|
|
28
|
+
return typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
29
|
+
? `scenario-${crypto.randomUUID()}`
|
|
30
|
+
: `scenario-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function Stat({
|
|
34
|
+
label,
|
|
35
|
+
value,
|
|
36
|
+
tone,
|
|
37
|
+
help,
|
|
38
|
+
small = false,
|
|
39
|
+
}: {
|
|
40
|
+
label: string
|
|
41
|
+
value: string
|
|
42
|
+
tone: 'good' | 'bad' | 'neutral'
|
|
43
|
+
help: string
|
|
44
|
+
small?: boolean
|
|
45
|
+
}) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="card">
|
|
48
|
+
<span className="field-label-row">
|
|
49
|
+
<span className="field-label">{label}</span>
|
|
50
|
+
<HelpTip text={help} />
|
|
51
|
+
</span>
|
|
52
|
+
<div className={`stat-value${small ? ' stat-value--sm' : ''} stat-value--${tone}`}>{value}</div>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface ShapeRow {
|
|
58
|
+
id: SpendingShapeId
|
|
59
|
+
label: string
|
|
60
|
+
maxBaseAnnual: number | null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const SHAPE_DEFS: { id: SpendingShapeId; label: string }[] = [
|
|
64
|
+
{ id: 'flat', label: 'Constant-real (no decline)' },
|
|
65
|
+
{ id: 'smile', label: 'Smile — average retiree (−10% at 75, −20% at 85)' },
|
|
66
|
+
{ id: 'smirk', label: 'Smirk — median retiree (−1%/yr real)' },
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
export function SpendingSolverPage() {
|
|
70
|
+
const { plan, update } = usePlan()
|
|
71
|
+
const navigate = useNavigate()
|
|
72
|
+
const startYear = currentStartYear()
|
|
73
|
+
|
|
74
|
+
const [result, setResult] = useState<SpendingSolveResult | null>(null)
|
|
75
|
+
const [running, setRunning] = useState(false)
|
|
76
|
+
const [error, setError] = useState<string | null>(null)
|
|
77
|
+
const runToken = useRef(0)
|
|
78
|
+
|
|
79
|
+
// Amortized spending recomputes annual spending from the portfolio, so the
|
|
80
|
+
// fixed-baseline bisection has nothing to move (the engine solver refuses
|
|
81
|
+
// ABW plans with a diagnostic). Skip solving and explain instead; the
|
|
82
|
+
// per-shape and SWR cards below solve fixed-target variants and stay useful.
|
|
83
|
+
const abwActive = plan.expenses.spendingPolicy?.mode === 'abw'
|
|
84
|
+
|
|
85
|
+
// --- solver output per spending shape (on demand; ~25 sims per shape) ----
|
|
86
|
+
// Results are keyed to the plan object they were solved on, so an edit
|
|
87
|
+
// anywhere simply makes them disappear instead of showing stale answers.
|
|
88
|
+
const [shapeState, setShapeState] = useState<{
|
|
89
|
+
forPlan: Plan
|
|
90
|
+
rows: ShapeRow[] | null
|
|
91
|
+
error: string | null
|
|
92
|
+
} | null>(null)
|
|
93
|
+
const [shapesRunning, setShapesRunning] = useState(false)
|
|
94
|
+
const shapeToken = useRef(0)
|
|
95
|
+
const shapeRows = shapeState !== null && shapeState.forPlan === plan ? shapeState.rows : null
|
|
96
|
+
const shapesError = shapeState !== null && shapeState.forPlan === plan ? shapeState.error : null
|
|
97
|
+
const compareShapes = () => {
|
|
98
|
+
const token = ++shapeToken.current
|
|
99
|
+
const forPlan = plan
|
|
100
|
+
setShapesRunning(true)
|
|
101
|
+
setShapeState(null)
|
|
102
|
+
const retirementAge = plan.household.people[0]?.retirementAge ?? 65
|
|
103
|
+
void (async () => {
|
|
104
|
+
try {
|
|
105
|
+
const rows: ShapeRow[] = []
|
|
106
|
+
for (const def of SHAPE_DEFS) {
|
|
107
|
+
const variant: Plan = {
|
|
108
|
+
...forPlan,
|
|
109
|
+
expenses: {
|
|
110
|
+
...forPlan.expenses,
|
|
111
|
+
phases: spendingShapePhases(def.id, retirementAge),
|
|
112
|
+
// Under ABW the ledger ignores baseAnnual/phases entirely, so a
|
|
113
|
+
// per-shape solve must price fixed-target variants; guardrail
|
|
114
|
+
// policies stay (the solver handles them).
|
|
115
|
+
...(forPlan.expenses.spendingPolicy?.mode === 'abw' ? { spendingPolicy: undefined } : {}),
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
const solved = await runSpendingSolve({ plan: variant, startYear })
|
|
119
|
+
rows.push({ ...def, maxBaseAnnual: solved.maxBaseAnnual })
|
|
120
|
+
}
|
|
121
|
+
if (token === shapeToken.current) setShapeState({ forPlan, rows, error: null })
|
|
122
|
+
} catch (e: unknown) {
|
|
123
|
+
if (token === shapeToken.current) {
|
|
124
|
+
setShapeState({ forPlan, rows: null, error: e instanceof Error ? e.message : String(e) })
|
|
125
|
+
}
|
|
126
|
+
} finally {
|
|
127
|
+
if (token === shapeToken.current) setShapesRunning(false)
|
|
128
|
+
}
|
|
129
|
+
})()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// --- published SWR rules on this plan (three deterministic ledger runs) --
|
|
133
|
+
const swrRows = useMemo(
|
|
134
|
+
() => compareSwrRules(plan, { startYear, taxCalculator: taxCalculatorFor(plan) }),
|
|
135
|
+
[plan, startYear],
|
|
136
|
+
)
|
|
137
|
+
const startingInvestable = useMemo(() => startingInvestableOf(plan), [plan])
|
|
138
|
+
|
|
139
|
+
const run = useCallback(() => {
|
|
140
|
+
const token = ++runToken.current
|
|
141
|
+
setRunning(true)
|
|
142
|
+
setError(null)
|
|
143
|
+
runSpendingSolve({ plan, startYear })
|
|
144
|
+
.then((r) => {
|
|
145
|
+
if (token === runToken.current) setResult(r)
|
|
146
|
+
})
|
|
147
|
+
.catch((e: unknown) => {
|
|
148
|
+
if (token === runToken.current) {
|
|
149
|
+
setError(e instanceof Error ? e.message : String(e))
|
|
150
|
+
setResult(null)
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
.finally(() => {
|
|
154
|
+
if (token === runToken.current) setRunning(false)
|
|
155
|
+
})
|
|
156
|
+
}, [plan, startYear])
|
|
157
|
+
|
|
158
|
+
// Auto-run on plan change (debounced), like the Roth & Tax Optimizer.
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (abwActive) return // nothing to solve; the ABW callout explains
|
|
161
|
+
const t = window.setTimeout(run, 300)
|
|
162
|
+
return () => window.clearTimeout(t)
|
|
163
|
+
}, [run, abwActive])
|
|
164
|
+
|
|
165
|
+
// The solver bisects to ~$500 resolution; a to-the-dollar headline claims
|
|
166
|
+
// precision the answer doesn't have. Floor to $100 (still feasible — it's
|
|
167
|
+
// below the level that passed) and use the same number everywhere: display,
|
|
168
|
+
// Apply, and scenarios.
|
|
169
|
+
const solvedRounded = result?.maxBaseAnnual != null ? Math.floor(result.maxBaseAnnual / 100) * 100 : null
|
|
170
|
+
|
|
171
|
+
const applyToSpending = () => {
|
|
172
|
+
if (solvedRounded === null) return
|
|
173
|
+
const solved = solvedRounded
|
|
174
|
+
update((d) => {
|
|
175
|
+
d.expenses.baseAnnual = solved
|
|
176
|
+
})
|
|
177
|
+
// Land on Spending so the applied baseline is visible where it's edited.
|
|
178
|
+
navigate(`/plan/${plan.id}/spending`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const addScenario = () => {
|
|
182
|
+
if (solvedRounded === null) return
|
|
183
|
+
const solved = solvedRounded
|
|
184
|
+
const baseName = `Spend ${fmtMoney(solved)}/yr (max sustainable)`
|
|
185
|
+
const names = new Set(plan.scenarios.map((s) => s.name))
|
|
186
|
+
let name = baseName
|
|
187
|
+
let suffix = 2
|
|
188
|
+
while (names.has(name)) name = `${baseName} (${suffix++})`
|
|
189
|
+
update((d) => {
|
|
190
|
+
d.scenarios.push({ id: makeScenarioId(), name, patch: { expenses: { baseAnnual: solved } } })
|
|
191
|
+
})
|
|
192
|
+
navigate(`/plan/${plan.id}/scenarios`)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Slack measured against the rounded display value so the two tiles agree.
|
|
196
|
+
const slack = result && solvedRounded !== null ? solvedRounded - result.currentBaseAnnual : null
|
|
197
|
+
// Deflate nominal end-of-plan evidence back to today's dollars so it reads
|
|
198
|
+
// on the same scale as the today's-dollars spending answer.
|
|
199
|
+
const deflator =
|
|
200
|
+
result?.evidence != null
|
|
201
|
+
? Math.pow(1 + plan.assumptions.inflationPct / 100, -(result.evidence.endYear - startYear))
|
|
202
|
+
: 1
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<section>
|
|
206
|
+
<div className="card">
|
|
207
|
+
<h2>How much can I spend?</h2>
|
|
208
|
+
<p className="card-hint">
|
|
209
|
+
Finds the highest annual base spending (today's dollars) your exact projection ledger can sustain: the plan
|
|
210
|
+
must never run out of investable money through the full horizon, and the ending after-tax estate must stay at
|
|
211
|
+
or above your bequest target. Every probed level re-runs the whole ledger — taxes, ACA and IRMAA cliffs,
|
|
212
|
+
withdrawal order, healthcare, debts, and survivor years all price in. Phases and one-time goals stay as
|
|
213
|
+
entered; only the baseline level moves. <LearnLink {...LEARN.sustainableSpending} />
|
|
214
|
+
</p>
|
|
215
|
+
{abwActive ? (
|
|
216
|
+
<div className="callout callout--info">
|
|
217
|
+
<p className="card-hint">
|
|
218
|
+
This plan uses <strong>amortized spending (ABW)</strong>: annual spending is recomputed from the actual
|
|
219
|
+
portfolio every year, so there is no fixed base-spending level to solve for — the amortization rule{' '}
|
|
220
|
+
<em>is</em> the answer to "how much can I spend?", and it adjusts itself as markets move. See
|
|
221
|
+
each year's amount on Results, or switch the spending policy back to fixed target under{' '}
|
|
222
|
+
<Link to={`/plan/${plan.id}/spending`}>Spending</Link> to use this solver. The shape and published-rule
|
|
223
|
+
comparisons below solve fixed-target variants of your plan and remain useful.
|
|
224
|
+
</p>
|
|
225
|
+
</div>
|
|
226
|
+
) : (
|
|
227
|
+
<p className="field-hint">
|
|
228
|
+
{result && result.estateFloorTodayDollars > 0 ? (
|
|
229
|
+
<>
|
|
230
|
+
Enforcing your {fmtMoney(result.estateFloorTodayDollars)} bequest target (today's dollars) from{' '}
|
|
231
|
+
<Link to={`/plan/${plan.id}/spending`}>Spending</Link>.
|
|
232
|
+
</>
|
|
233
|
+
) : (
|
|
234
|
+
<>
|
|
235
|
+
No bequest target set — the only constraint is not running out. Set one under{' '}
|
|
236
|
+
<Link to={`/plan/${plan.id}/spending`}>Spending</Link> to protect an estate floor.
|
|
237
|
+
</>
|
|
238
|
+
)}
|
|
239
|
+
</p>
|
|
240
|
+
)}
|
|
241
|
+
{running ? <div className="skeleton" style={{ height: '2rem', marginTop: '0.75rem' }} aria-label="Solving" /> : null}
|
|
242
|
+
{error ? <p style={{ color: 'var(--bad)' }}>Solver error: {error}</p> : null}
|
|
243
|
+
{error && !running ? (
|
|
244
|
+
<div style={{ marginTop: '0.75rem' }}>
|
|
245
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={run}>
|
|
246
|
+
Try again
|
|
247
|
+
</button>
|
|
248
|
+
</div>
|
|
249
|
+
) : null}
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{result && !running ? (
|
|
253
|
+
result.maxBaseAnnual === null ? (
|
|
254
|
+
<div className="card">
|
|
255
|
+
<h2 style={{ color: 'var(--bad)' }}>No sustainable spending level found</h2>
|
|
256
|
+
<p className="muted">
|
|
257
|
+
{result.diagnostics.length > 0
|
|
258
|
+
? result.diagnostics.join(' ')
|
|
259
|
+
: 'Even minimal base spending depletes the portfolio or breaks the bequest target within the plan horizon.'}{' '}
|
|
260
|
+
Fixed costs modeled outside baseline spending (healthcare, debt service, property carrying costs, one-time
|
|
261
|
+
goals) may already exceed what the plan can fund.
|
|
262
|
+
</p>
|
|
263
|
+
</div>
|
|
264
|
+
) : (
|
|
265
|
+
<>
|
|
266
|
+
<div className="mc-hero">
|
|
267
|
+
<div>
|
|
268
|
+
<h2 style={{ margin: '0 0 0.35rem', color: slack !== null && slack >= 0 ? 'var(--good)' : 'var(--bad)' }}>
|
|
269
|
+
Your plan can sustain about {fmtMoney(solvedRounded ?? 0)} of baseline spending per year.
|
|
270
|
+
</h2>
|
|
271
|
+
<p className="muted" style={{ margin: 0 }}>
|
|
272
|
+
{slack !== null && slack >= 0
|
|
273
|
+
? `That is ${fmtMoney(slack)} per year of headroom above your current ${fmtMoney(result.currentBaseAnnual)} baseline (today's dollars).`
|
|
274
|
+
: `That is ${fmtMoney(Math.abs(slack ?? 0))} per year BELOW your current ${fmtMoney(result.currentBaseAnnual)} baseline — your projection cannot sustain today's spending through the horizon.`}
|
|
275
|
+
{!result.converged
|
|
276
|
+
? ' The simulation budget ran out before the answer fully converged, so this is a feasible lower bound.'
|
|
277
|
+
: ''}
|
|
278
|
+
</p>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<div className="stat-grid">
|
|
283
|
+
<Stat
|
|
284
|
+
label="Max sustainable spending"
|
|
285
|
+
value={`${fmtMoney(solvedRounded ?? 0)}/yr`}
|
|
286
|
+
tone="neutral"
|
|
287
|
+
help="Highest annual baseline spending (today's dollars) whose full year-by-year projection never depletes investable assets and keeps the ending after-tax estate at or above your bequest target. Solved by bisection to ~$500 resolution, then shown rounded down to the nearest $100 — the same rounded figure is what Apply and scenarios use."
|
|
288
|
+
/>
|
|
289
|
+
<Stat
|
|
290
|
+
label="Spending slack"
|
|
291
|
+
value={`${slack !== null && slack >= 0 ? '+' : ''}${fmtMoney(slack ?? 0)}/yr`}
|
|
292
|
+
tone={slack !== null && slack > 0 ? 'good' : slack !== null && slack < 0 ? 'bad' : 'neutral'}
|
|
293
|
+
help="Max sustainable spending minus your current baseline. Positive = headroom you are not using; negative = the current baseline overspends what your plan can sustain."
|
|
294
|
+
/>
|
|
295
|
+
<Stat
|
|
296
|
+
label="What limits it"
|
|
297
|
+
value={
|
|
298
|
+
result.limitingConstraint === 'estate-floor'
|
|
299
|
+
? 'Bequest target'
|
|
300
|
+
: result.limitingConstraint === 'depletion'
|
|
301
|
+
? 'Depletion — at higher spending'
|
|
302
|
+
: 'No limit found'
|
|
303
|
+
}
|
|
304
|
+
tone="neutral"
|
|
305
|
+
small
|
|
306
|
+
help="What failed at the next-higher spending level the solver probed — not at the answer above, which passed. 'Depletion' means spending any more than the answer would run the portfolio out of money before the end of the plan; 'Bequest target' means the ending estate would fall below your floor. 'No limit found' means spending appeared unbounded at every probed level (guaranteed income outruns spending)."
|
|
307
|
+
/>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
{result.evidence ? (
|
|
311
|
+
<div className="card">
|
|
312
|
+
<h2>Evidence at that level</h2>
|
|
313
|
+
<p className="card-hint">
|
|
314
|
+
From the full projection run at the solver's exact answer (shown as{' '}
|
|
315
|
+
{fmtMoney(solvedRounded ?? 0)}/yr, rounded down to the nearest $100) — the same year-by-year
|
|
316
|
+
numbers Results shows, not an approximation.
|
|
317
|
+
</p>
|
|
318
|
+
<ul style={{ margin: '0.25rem 0 0.75rem 1.1rem', lineHeight: 1.7 }}>
|
|
319
|
+
<li>
|
|
320
|
+
Money lasts through <strong>{result.evidence.endYear}</strong> (end of plan
|
|
321
|
+
{result.evidence.depletionYear === null ? ', never depleting' : ''}).
|
|
322
|
+
</li>
|
|
323
|
+
<li>
|
|
324
|
+
Ending after-tax estate: <strong>{fmtMoney(result.evidence.endingAfterTaxEstate * deflator)}</strong>{' '}
|
|
325
|
+
today's dollars ({fmtMoney(result.evidence.endingAfterTaxEstate)} nominal)
|
|
326
|
+
{result.estateFloorTodayDollars > 0 ? ` vs. the ${fmtMoney(result.estateFloorTodayDollars)} floor` : ''}.
|
|
327
|
+
</li>
|
|
328
|
+
<li>
|
|
329
|
+
Lifetime taxes and penalties at that spending level:{' '}
|
|
330
|
+
<strong>{fmtMoney(result.evidence.lifetimeTaxesAndPenalties)}</strong> (nominal, summed).
|
|
331
|
+
</li>
|
|
332
|
+
</ul>
|
|
333
|
+
<p className="field-hint">
|
|
334
|
+
Solved in {result.simulationCount} full-plan simulations
|
|
335
|
+
{result.converged ? ', converged to ~$500 resolution.' : ' — budget exhausted, feasible lower bound.'}
|
|
336
|
+
</p>
|
|
337
|
+
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
|
|
338
|
+
<button type="button" className="btn btn-primary btn-small" onClick={applyToSpending}>
|
|
339
|
+
Apply to Spending
|
|
340
|
+
</button>
|
|
341
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={addScenario}>
|
|
342
|
+
Add as scenario
|
|
343
|
+
</button>
|
|
344
|
+
<button type="button" className="btn btn-secondary btn-small" disabled={running} onClick={run}>
|
|
345
|
+
Re-solve
|
|
346
|
+
</button>
|
|
347
|
+
</div>
|
|
348
|
+
<p className="field-hint" style={{ marginTop: '0.5rem' }}>
|
|
349
|
+
"Apply to Spending" sets your plan's baseline spending to {fmtMoney(solvedRounded ?? 0)}/yr and opens
|
|
350
|
+
the Spending screen. "Add as scenario" instead creates a side-by-side scenario under Scenarios without
|
|
351
|
+
changing your plan.
|
|
352
|
+
</p>
|
|
353
|
+
<details className="ss-explainer">
|
|
354
|
+
<summary>Why this number?</summary>
|
|
355
|
+
<p>
|
|
356
|
+
The solver bisects on the baseline spending level: each probe re-runs your entire year-by-year
|
|
357
|
+
projection (taxes, ACA/IRMAA cliffs, withdrawal order, healthcare, debts, survivor years) and
|
|
358
|
+
checks two constraints — investable assets must never deplete before {result.evidence.endYear}, and
|
|
359
|
+
the ending after-tax estate must stay at or above{' '}
|
|
360
|
+
{result.estateFloorTodayDollars > 0
|
|
361
|
+
? `your ${fmtMoney(result.estateFloorTodayDollars)} bequest target`
|
|
362
|
+
: 'zero (no bequest target set)'}
|
|
363
|
+
. The solver's exact answer is the highest level that passed both — it is shown, applied,
|
|
364
|
+
and added to scenarios rounded down to the nearest $100 ({fmtMoney(solvedRounded ?? 0)}), which
|
|
365
|
+
therefore also passes. The next-higher probe failed on{' '}
|
|
366
|
+
{result.limitingConstraint === 'estate-floor'
|
|
367
|
+
? 'the bequest target'
|
|
368
|
+
: result.limitingConstraint === 'depletion'
|
|
369
|
+
? 'running out of money'
|
|
370
|
+
: 'nothing (spending appeared unbounded at every probed level)'}
|
|
371
|
+
. This answer used {result.simulationCount} full-projection runs
|
|
372
|
+
{result.converged ? ' and converged to ~$500 resolution' : ' before the budget ran out (feasible lower bound)'}
|
|
373
|
+
. The assumptions behind every run are on{' '}
|
|
374
|
+
<Link to={`/plan/${plan.id}/assumptions-card`}>your assumptions card</Link>.
|
|
375
|
+
</p>
|
|
376
|
+
</details>
|
|
377
|
+
</div>
|
|
378
|
+
) : null}
|
|
379
|
+
</>
|
|
380
|
+
)
|
|
381
|
+
) : null}
|
|
382
|
+
|
|
383
|
+
<div className="card">
|
|
384
|
+
<h2>What shape of spending?</h2>
|
|
385
|
+
<p className="card-hint">
|
|
386
|
+
The answer above keeps your phases as entered. Research on actual retirees says spending rarely stays
|
|
387
|
+
constant-real: the <em>average</em> path is a "smile" (a slow decline that late-life healthcare
|
|
388
|
+
partly reverses — the preset approximates it as two downward steps) and the <em>median</em> path is a
|
|
389
|
+
"smirk" — a steady ~1%/yr real decline with no late rise at all (Blanchett).
|
|
390
|
+
Shape-aware plans support a higher initial spend from the same portfolio. Solve your plan under each shape
|
|
391
|
+
to see the size of that effect here — apply a shape on the{' '}
|
|
392
|
+
<Link to={`/plan/${plan.id}/spending`}>Spending</Link> screen if you want to keep it.{' '}
|
|
393
|
+
<LearnLink {...LEARN.spendingProfiles} />
|
|
394
|
+
</p>
|
|
395
|
+
{shapeRows === null && !shapesRunning ? (
|
|
396
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={compareShapes}>
|
|
397
|
+
Solve per shape (~75 simulations)
|
|
398
|
+
</button>
|
|
399
|
+
) : null}
|
|
400
|
+
{shapesRunning ? <div className="skeleton" style={{ height: '2rem' }} aria-label="Solving per shape" /> : null}
|
|
401
|
+
{shapesError ? <p style={{ color: 'var(--bad)' }}>Per-shape solve error: {shapesError}</p> : null}
|
|
402
|
+
{shapeRows !== null && !shapesRunning ? (
|
|
403
|
+
<>
|
|
404
|
+
<div className="table-scroll">
|
|
405
|
+
<table>
|
|
406
|
+
<thead>
|
|
407
|
+
<tr>
|
|
408
|
+
<th style={{ textAlign: 'left' }}>Spending shape</th>
|
|
409
|
+
<th style={{ textAlign: 'right' }}>Max sustainable initial spend</th>
|
|
410
|
+
<th style={{ textAlign: 'right' }}>vs constant-real</th>
|
|
411
|
+
</tr>
|
|
412
|
+
</thead>
|
|
413
|
+
<tbody>
|
|
414
|
+
{shapeRows.map((row) => {
|
|
415
|
+
const flat = shapeRows.find((r) => r.id === 'flat')?.maxBaseAnnual ?? null
|
|
416
|
+
const delta = row.maxBaseAnnual !== null && flat !== null ? row.maxBaseAnnual - flat : null
|
|
417
|
+
return (
|
|
418
|
+
<tr key={row.id}>
|
|
419
|
+
<td>{row.label}</td>
|
|
420
|
+
<td style={{ textAlign: 'right' }}>
|
|
421
|
+
{row.maxBaseAnnual !== null ? `${fmtMoney(Math.floor(row.maxBaseAnnual / 100) * 100)}/yr` : '—'}
|
|
422
|
+
</td>
|
|
423
|
+
<td style={{ textAlign: 'right' }}>
|
|
424
|
+
{delta === null || row.id === 'flat' ? '—' : `${delta >= 0 ? '+' : ''}${fmtMoney(delta)}/yr`}
|
|
425
|
+
</td>
|
|
426
|
+
</tr>
|
|
427
|
+
)
|
|
428
|
+
})}
|
|
429
|
+
</tbody>
|
|
430
|
+
</table>
|
|
431
|
+
</div>
|
|
432
|
+
<p className="field-hint" style={{ marginTop: '0.5rem' }}>
|
|
433
|
+
Each row re-solves your full plan with that shape's phase rows (initial spend in today's
|
|
434
|
+
dollars; later years follow the shape). No shape is "the answer" — they are framings of how
|
|
435
|
+
your own later-life spending might behave.
|
|
436
|
+
</p>
|
|
437
|
+
</>
|
|
438
|
+
) : null}
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
<div className="card">
|
|
442
|
+
<h2>Whose 4% rule? Published rules vs this plan</h2>
|
|
443
|
+
<p className="card-hint">
|
|
444
|
+
The community argues about the "right" safe withdrawal rate — Bengen's 2025 book says 4.7%,
|
|
445
|
+
Morningstar's latest study says 3.9%, Early Retirement Now conditions it on market valuations. Here is
|
|
446
|
+
each published rule priced on <em>your</em> plan through the same year-by-year ledger, next to your
|
|
447
|
+
plan's own solved answer above. The rules assume constant-real spending of a fixed fraction of the
|
|
448
|
+
starting portfolio; your solver answer prices your actual phases, taxes, and horizon — which is why they
|
|
449
|
+
differ.
|
|
450
|
+
</p>
|
|
451
|
+
<div className="table-scroll">
|
|
452
|
+
<table>
|
|
453
|
+
<thead>
|
|
454
|
+
<tr>
|
|
455
|
+
<th style={{ textAlign: 'left' }}>Rule</th>
|
|
456
|
+
<th style={{ textAlign: 'right' }}>Rate</th>
|
|
457
|
+
<th style={{ textAlign: 'right' }}>Spending/yr</th>
|
|
458
|
+
<th style={{ textAlign: 'left' }}>On your plan</th>
|
|
459
|
+
<th style={{ textAlign: 'right' }}>Ending estate (today's $)</th>
|
|
460
|
+
</tr>
|
|
461
|
+
</thead>
|
|
462
|
+
<tbody>
|
|
463
|
+
{swrRows.map((row) => {
|
|
464
|
+
const deflate = Math.pow(1 + plan.assumptions.inflationPct / 100, -(row.endYear - startYear))
|
|
465
|
+
return (
|
|
466
|
+
<tr key={row.id}>
|
|
467
|
+
<td>
|
|
468
|
+
{row.label} <HelpTip text={row.citation} />
|
|
469
|
+
</td>
|
|
470
|
+
<td style={{ textAlign: 'right' }}>{row.initialRatePct.toFixed(2)}%</td>
|
|
471
|
+
<td style={{ textAlign: 'right' }}>{fmtMoney(row.initialAnnualSpend)}</td>
|
|
472
|
+
<td>
|
|
473
|
+
{row.depletionYear === null ? (
|
|
474
|
+
<span style={{ color: 'var(--good)' }}>lasts through {row.endYear}</span>
|
|
475
|
+
) : (
|
|
476
|
+
<span style={{ color: 'var(--bad)' }}>runs out in {row.depletionYear}</span>
|
|
477
|
+
)}
|
|
478
|
+
</td>
|
|
479
|
+
<td style={{ textAlign: 'right' }}>
|
|
480
|
+
{row.depletionYear === null ? fmtMoney(row.endingAfterTaxEstate * deflate) : '—'}
|
|
481
|
+
</td>
|
|
482
|
+
</tr>
|
|
483
|
+
)
|
|
484
|
+
})}
|
|
485
|
+
{solvedRounded !== null && startingInvestable > 0 ? (
|
|
486
|
+
<tr>
|
|
487
|
+
<td>
|
|
488
|
+
<strong>This plan's solver</strong>{' '}
|
|
489
|
+
<HelpTip text="The exact-ledger answer from the top of this page, expressed as an initial rate on the same starting investable balance so it can sit in the same table. Unlike the published rules it prices your actual phases, taxes, healthcare, and horizon." />
|
|
490
|
+
</td>
|
|
491
|
+
<td style={{ textAlign: 'right' }}>{((solvedRounded / startingInvestable) * 100).toFixed(2)}%</td>
|
|
492
|
+
<td style={{ textAlign: 'right' }}>{fmtMoney(solvedRounded)}</td>
|
|
493
|
+
<td>solved on your exact plan</td>
|
|
494
|
+
<td style={{ textAlign: 'right' }}>—</td>
|
|
495
|
+
</tr>
|
|
496
|
+
) : null}
|
|
497
|
+
</tbody>
|
|
498
|
+
</table>
|
|
499
|
+
</div>
|
|
500
|
+
<p className="field-hint" style={{ marginTop: '0.5rem' }}>
|
|
501
|
+
Each rule runs with your accounts, taxes, healthcare, goals, and horizon unchanged — only recurring
|
|
502
|
+
lifestyle spending is set to the rule's level (constant-real, as the rules define it). A rule
|
|
503
|
+
"running out" on your plan usually means your horizon is longer than the 30 years the rule was
|
|
504
|
+
derived for, or your fixed costs differ from a generic retiree's — evidence for why a plan-specific
|
|
505
|
+
answer beats a debate about whose rule is right.
|
|
506
|
+
</p>
|
|
507
|
+
</div>
|
|
508
|
+
|
|
509
|
+
<LearnAboutScreen route="/plan/:planId/spending-solver" />
|
|
510
|
+
</section>
|
|
511
|
+
)
|
|
512
|
+
}
|