@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,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executes one optimize request. Shared by the Web Worker entry and the
|
|
3
|
+
* synchronous fallback (tests / no-Worker environments) so both run the
|
|
4
|
+
* identical tax stack — federal engine + per-state engine with the plan's flat
|
|
5
|
+
* rate as an override, matching src/mc/runRequest.ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { objectivePolicyForPlan } from '@retiregolden/engine/decisions'
|
|
9
|
+
import {
|
|
10
|
+
optimizePlan,
|
|
11
|
+
optimizePlanCoOptimizingClaimAge,
|
|
12
|
+
type ExactLedgerPostProcessing,
|
|
13
|
+
type OptimizePlanOptions,
|
|
14
|
+
} from '@retiregolden/engine/projection/optimizePlan'
|
|
15
|
+
import { combineTaxCalculators, createFederalTaxCalculator } from '@retiregolden/engine/tax/federalTax'
|
|
16
|
+
import { createStateTaxCalculator } from '@retiregolden/engine/tax/stateTax'
|
|
17
|
+
import type { OptimizePostProcessing, OptimizeRequest, OptimizeResult } from './messages'
|
|
18
|
+
|
|
19
|
+
function summarizePostProcessing(postProcessed: ExactLedgerPostProcessing | null): OptimizePostProcessing | null {
|
|
20
|
+
if (!postProcessed) return null
|
|
21
|
+
return {
|
|
22
|
+
rawSchedule: postProcessed.rawSchedule,
|
|
23
|
+
cleanedSchedule: postProcessed.cleanedSchedule,
|
|
24
|
+
rawValidation: postProcessed.rawValidation,
|
|
25
|
+
cleanedValidation: postProcessed.cleanedValidation,
|
|
26
|
+
adjustments: postProcessed.adjustments,
|
|
27
|
+
stabilized: postProcessed.stabilized,
|
|
28
|
+
iterationCount: postProcessed.iterationCount,
|
|
29
|
+
pruneIterationCount: postProcessed.pruneIterationCount,
|
|
30
|
+
recommendationSchedule: postProcessed.recommendationSchedule,
|
|
31
|
+
cleanedExecutionByYear: postProcessed.cleanedResult.years.map((year) => ({
|
|
32
|
+
year: year.year,
|
|
33
|
+
rothConversion: year.rothConversion,
|
|
34
|
+
})),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Production defaults for the optimize pipeline. Exported so the dev-only Owl
|
|
40
|
+
* parity harness (src/cases/owlParity.ts) measures exactly what the worker
|
|
41
|
+
* ships — a retune here automatically retunes the parity gate.
|
|
42
|
+
*/
|
|
43
|
+
export const DEFAULT_OPTIMIZE_SEARCH_BUDGET = 96
|
|
44
|
+
export const DEFAULT_OPTIMIZE_CONVERGENCE_ITERATIONS = 6
|
|
45
|
+
|
|
46
|
+
export async function runOptimizeRequest(
|
|
47
|
+
req: OptimizeRequest,
|
|
48
|
+
locateFile?: (file: string) => string,
|
|
49
|
+
): Promise<OptimizeResult> {
|
|
50
|
+
const taxCalculator = combineTaxCalculators(
|
|
51
|
+
createFederalTaxCalculator(),
|
|
52
|
+
createStateTaxCalculator({
|
|
53
|
+
overridePct: req.plan.assumptions.stateEffectiveTaxPct,
|
|
54
|
+
localPct: req.plan.assumptions.localIncomeTaxPct,
|
|
55
|
+
}),
|
|
56
|
+
)
|
|
57
|
+
// Phase 4 local search: refine the winning schedules on the exact ledger
|
|
58
|
+
// under a bounded, deterministic per-seed simulation budget (enough for the
|
|
59
|
+
// coarse+fine sweeps to also explore the bounded taper-extension years past
|
|
60
|
+
// the seed schedule); zero or negative disables search entirely (the seed
|
|
61
|
+
// evaluation would otherwise still cost one simulation).
|
|
62
|
+
const searchBudget = req.searchSimulationBudget ?? DEFAULT_OPTIMIZE_SEARCH_BUDGET
|
|
63
|
+
// Exact-ledger convergence loop (Step 1): re-solve against exogenous inputs
|
|
64
|
+
// recaptured from the incumbent schedule so the recommendation is optimal on
|
|
65
|
+
// the real projection to tolerance. Bounded iteration cap; the tournament
|
|
66
|
+
// still prices and gates whatever the loop produces. `≤ 1` disables it.
|
|
67
|
+
const convergenceIterations = req.convergenceIterations ?? DEFAULT_OPTIMIZE_CONVERGENCE_ITERATIONS
|
|
68
|
+
const planOptions: OptimizePlanOptions = {
|
|
69
|
+
startYear: req.startYear,
|
|
70
|
+
taxCalculator,
|
|
71
|
+
liquidationRatePct: req.liquidationRatePct,
|
|
72
|
+
solver: { timeLimitSec: req.timeLimitSec, locateFile },
|
|
73
|
+
search: searchBudget > 0 ? { maxSimulations: searchBudget } : false,
|
|
74
|
+
convergence: convergenceIterations > 1 ? { maxIterations: convergenceIterations } : false,
|
|
75
|
+
// Resolved here (not passed as a function) so the policy survives the
|
|
76
|
+
// structured-clone worker boundary; floor policies read the plan's bequest target.
|
|
77
|
+
policy: req.objectivePolicyId ? objectivePolicyForPlan(req.objectivePolicyId, req.plan) : undefined,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Step 5: co-optimize the SS claim age when requested (opt-in; bounded grid).
|
|
81
|
+
// The returned schedule may assume a claim change reported in `claimAge`.
|
|
82
|
+
if (req.coOptimizeClaimAge) {
|
|
83
|
+
const { schedule, postProcessed, tournament, convergence, claimAge } = await optimizePlanCoOptimizingClaimAge(
|
|
84
|
+
req.plan,
|
|
85
|
+
planOptions,
|
|
86
|
+
)
|
|
87
|
+
return { schedule, postProcessed: summarizePostProcessing(postProcessed), tournament, convergence, claimAge }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const { schedule, postProcessed, tournament, convergence } = await optimizePlan(req.plan, planOptions)
|
|
91
|
+
return { schedule, postProcessed: summarizePostProcessing(postProcessed), tournament, convergence, claimAge: null }
|
|
92
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executes one sustainable-spending solve. Shared by the Web Worker entry and
|
|
3
|
+
* the synchronous fallback (tests / no-Worker environments) so both run the
|
|
4
|
+
* identical tax stack — federal engine + per-state engine with the plan's flat
|
|
5
|
+
* rate as an override, matching ./runOptimize.ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createDecisionContext, solveMaxSustainableSpending, SPENDING_SOLVER_UI_BUDGET } from '@retiregolden/engine/decisions'
|
|
9
|
+
import { combineTaxCalculators, createFederalTaxCalculator } from '@retiregolden/engine/tax/federalTax'
|
|
10
|
+
import { createStateTaxCalculator } from '@retiregolden/engine/tax/stateTax'
|
|
11
|
+
import type { SpendingSolveRequest, SpendingSolveResult } from './spendingMessages'
|
|
12
|
+
|
|
13
|
+
export function runSpendingSolveRequest(req: SpendingSolveRequest): SpendingSolveResult {
|
|
14
|
+
const taxCalculator = combineTaxCalculators(
|
|
15
|
+
createFederalTaxCalculator(),
|
|
16
|
+
createStateTaxCalculator({
|
|
17
|
+
overridePct: req.plan.assumptions.stateEffectiveTaxPct,
|
|
18
|
+
localPct: req.plan.assumptions.localIncomeTaxPct,
|
|
19
|
+
}),
|
|
20
|
+
)
|
|
21
|
+
const ctx = createDecisionContext(req.plan, { startYear: req.startYear, taxCalculator })
|
|
22
|
+
const estateFloorTodayDollars = req.plan.expenses.bequestTargetDollars ?? 0
|
|
23
|
+
const solved = solveMaxSustainableSpending(ctx, {
|
|
24
|
+
maxSimulations: req.maxSimulations ?? SPENDING_SOLVER_UI_BUDGET,
|
|
25
|
+
estateFloorTodayDollars,
|
|
26
|
+
})
|
|
27
|
+
const summary = solved.bestEvaluation?.candidateSummary ?? null
|
|
28
|
+
return {
|
|
29
|
+
maxBaseAnnual: solved.maxBaseAnnual,
|
|
30
|
+
spendingSlackDollars: solved.spendingSlackDollars,
|
|
31
|
+
currentBaseAnnual: req.plan.expenses.baseAnnual,
|
|
32
|
+
estateFloorTodayDollars,
|
|
33
|
+
converged: solved.converged,
|
|
34
|
+
limitingConstraint: solved.limitingConstraint,
|
|
35
|
+
simulationCount: solved.simulationCount,
|
|
36
|
+
diagnostics: solved.diagnostics,
|
|
37
|
+
evidence: summary
|
|
38
|
+
? {
|
|
39
|
+
endingAfterTaxEstate: summary.endingAfterTaxEstate,
|
|
40
|
+
endingNetWorth: summary.endingNetWorth,
|
|
41
|
+
lifetimeTaxesAndPenalties: summary.lifetimeTaxesAndPenalties,
|
|
42
|
+
depletionYear: solved.bestEvaluation!.candidateResult.depletionYear,
|
|
43
|
+
endYear: solved.bestEvaluation!.candidateResult.endYear,
|
|
44
|
+
}
|
|
45
|
+
: null,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawns the optimizer in a Web Worker (lazy-loaded only here, so the wasm
|
|
3
|
+
* chunk is fetched only when Optimize is invoked) and resolves the schedule.
|
|
4
|
+
* Falls back to a synchronous in-process solve where Worker is unavailable
|
|
5
|
+
* (tests, very old browsers), mirroring src/mc/pool.ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { OptimizeRequest, OptimizeResponse, OptimizeResult } from './messages'
|
|
9
|
+
import { runOptimizeRequest } from './runOptimize'
|
|
10
|
+
import { runWorkerRequest } from '../workers/run'
|
|
11
|
+
|
|
12
|
+
export function runOptimize(req: OptimizeRequest): Promise<OptimizeResult> {
|
|
13
|
+
if (typeof Worker === 'undefined') return runOptimizeRequest(req)
|
|
14
|
+
return runWorkerRequest<OptimizeRequest, OptimizeResponse, OptimizeResult>({
|
|
15
|
+
request: req,
|
|
16
|
+
createWorker: () => new Worker(new URL('./optimize.worker.ts', import.meta.url), { type: 'module' }),
|
|
17
|
+
interpret: (msg) =>
|
|
18
|
+
msg.type === 'done' ? { kind: 'done', result: msg.result } : { kind: 'error', message: msg.message },
|
|
19
|
+
errorLabel: 'Optimizer worker failed',
|
|
20
|
+
})
|
|
21
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire types between the "How much can I spend?" surface and its worker
|
|
3
|
+
* (sustainable-spending plan, Step 4). Same rules as ./messages.ts: everything
|
|
4
|
+
* must survive structured clone, so the solver's `bestEvaluation` (which holds
|
|
5
|
+
* a full ProjectionResult) is summarized into `evidence` before posting.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Plan } from '@retiregolden/engine/model/plan'
|
|
9
|
+
|
|
10
|
+
export interface SpendingSolveRequest {
|
|
11
|
+
plan: Plan
|
|
12
|
+
startYear: number
|
|
13
|
+
/** Exact-ledger simulation budget; defaults to SPENDING_SOLVER_UI_BUDGET. */
|
|
14
|
+
maxSimulations?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Exact-ledger evidence for the plan run at the solved spending level. */
|
|
18
|
+
export interface SpendingSolveEvidence {
|
|
19
|
+
endingAfterTaxEstate: number
|
|
20
|
+
endingNetWorth: number
|
|
21
|
+
lifetimeTaxesAndPenalties: number
|
|
22
|
+
depletionYear: number | null
|
|
23
|
+
endYear: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SpendingSolveResult {
|
|
27
|
+
/** Highest feasible annual base spending (today's dollars), or null. */
|
|
28
|
+
maxBaseAnnual: number | null
|
|
29
|
+
/** maxBaseAnnual − current base spending (negative ⇒ overspending today). */
|
|
30
|
+
spendingSlackDollars: number | null
|
|
31
|
+
/** The plan's own base spending the slack is measured against. */
|
|
32
|
+
currentBaseAnnual: number
|
|
33
|
+
/** The bequest target the solve enforced (today's dollars; 0 = none). */
|
|
34
|
+
estateFloorTodayDollars: number
|
|
35
|
+
converged: boolean
|
|
36
|
+
limitingConstraint: 'depletion' | 'estate-floor' | null
|
|
37
|
+
simulationCount: number
|
|
38
|
+
diagnostics: string[]
|
|
39
|
+
evidence: SpendingSolveEvidence | null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type SpendingSolveResponse =
|
|
43
|
+
| { type: 'done'; result: SpendingSolveResult }
|
|
44
|
+
| { type: 'error'; message: string }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawns the sustainable-spending solver in a Web Worker (lazy-loaded only
|
|
3
|
+
* here) and resolves the summarized result. Falls back to a synchronous
|
|
4
|
+
* in-process solve where Worker is unavailable (tests, very old browsers),
|
|
5
|
+
* mirroring ./runner.ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SpendingSolveRequest, SpendingSolveResponse, SpendingSolveResult } from './spendingMessages'
|
|
9
|
+
import { runSpendingSolveRequest } from './runSpendingSolve'
|
|
10
|
+
import { runWorkerRequest } from '../workers/run'
|
|
11
|
+
|
|
12
|
+
export function runSpendingSolve(req: SpendingSolveRequest): Promise<SpendingSolveResult> {
|
|
13
|
+
if (typeof Worker === 'undefined') return Promise.resolve(runSpendingSolveRequest(req))
|
|
14
|
+
return runWorkerRequest<SpendingSolveRequest, SpendingSolveResponse, SpendingSolveResult>({
|
|
15
|
+
request: req,
|
|
16
|
+
createWorker: () => new Worker(new URL('./spendingSolve.worker.ts', import.meta.url), { type: 'module' }),
|
|
17
|
+
interpret: (msg) =>
|
|
18
|
+
msg.type === 'done' ? { kind: 'done', result: msg.result } : { kind: 'error', message: msg.message },
|
|
19
|
+
errorLabel: 'Spending solver worker failed',
|
|
20
|
+
})
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Worker entry for the sustainable-spending solver: one request per run,
|
|
3
|
+
* ~25 exact-ledger simulations off the main thread, summarized result posted
|
|
4
|
+
* back. No wasm — the solver is pure `simulatePlan` bisection.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { SpendingSolveRequest, SpendingSolveResponse } from './spendingMessages'
|
|
8
|
+
import { runSpendingSolveRequest } from './runSpendingSolve'
|
|
9
|
+
|
|
10
|
+
const post = (msg: SpendingSolveResponse) => (self as unknown as Worker).postMessage(msg)
|
|
11
|
+
|
|
12
|
+
self.onmessage = (event: MessageEvent<SpendingSolveRequest>) => {
|
|
13
|
+
try {
|
|
14
|
+
post({ type: 'done', result: runSpendingSolveRequest(event.data) })
|
|
15
|
+
} catch (err) {
|
|
16
|
+
post({ type: 'error', message: err instanceof Error ? err.message : String(err) })
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Your assumptions" card (trust-and-transparency-layer, step 1): one screen
|
|
3
|
+
* enumerating every live assumption behind this plan's numbers — economy,
|
|
4
|
+
* account returns, longevity, law toggles, and the parameter pack — each
|
|
5
|
+
* tagged user-set / app default / published source, with a copy-export so the
|
|
6
|
+
* run can be replicated in another tool. This is the direct answer to "why do
|
|
7
|
+
* different planners give different answers?": they differ here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
11
|
+
import { Link } from 'react-router-dom'
|
|
12
|
+
|
|
13
|
+
import { PARAMETER_PROVENANCE } from '@retiregolden/engine/params'
|
|
14
|
+
import { usePlan } from './planContextCore'
|
|
15
|
+
import { currentStartYear } from './useProjection'
|
|
16
|
+
import {
|
|
17
|
+
assumptionsExportJson,
|
|
18
|
+
assumptionsExportText,
|
|
19
|
+
buildAssumptionsSnapshot,
|
|
20
|
+
type AssumptionProvenance,
|
|
21
|
+
} from './assumptionsExport'
|
|
22
|
+
|
|
23
|
+
const PROVENANCE_CHIP: Record<AssumptionProvenance, { label: string; title: string }> = {
|
|
24
|
+
'user-set': { label: 'You set this', title: 'Entered or changed by you — other tools need this value to match your run.' },
|
|
25
|
+
'app-default': { label: 'App default', title: "RetireGolden's shipped default — you have not changed it." },
|
|
26
|
+
'published-source': { label: 'Published source', title: 'Comes from a cited publication (statute, agency figure, or documented dataset).' },
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function CopyButton({ label, copiedLabel, text }: { label: string; copiedLabel: string; text: () => string }) {
|
|
30
|
+
const [state, setState] = useState<'idle' | 'copied' | 'failed'>('idle')
|
|
31
|
+
const resetTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
32
|
+
useEffect(() => () => clearTimeout(resetTimer.current), [])
|
|
33
|
+
const flash = (next: 'copied' | 'failed') => {
|
|
34
|
+
setState(next)
|
|
35
|
+
clearTimeout(resetTimer.current)
|
|
36
|
+
resetTimer.current = setTimeout(() => setState('idle'), 2500)
|
|
37
|
+
}
|
|
38
|
+
const copy = () => {
|
|
39
|
+
// The Clipboard API is absent in insecure contexts and some browsers; say
|
|
40
|
+
// so instead of throwing (or faking success).
|
|
41
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard?.writeText) {
|
|
42
|
+
flash('failed')
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
navigator.clipboard
|
|
46
|
+
.writeText(text())
|
|
47
|
+
.then(() => flash('copied'))
|
|
48
|
+
.catch(() => flash('failed'))
|
|
49
|
+
}
|
|
50
|
+
return (
|
|
51
|
+
<button type="button" className="btn btn-secondary btn-small" onClick={copy}>
|
|
52
|
+
{state === 'copied' ? copiedLabel : state === 'failed' ? 'Clipboard unavailable — copy manually' : label}
|
|
53
|
+
</button>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function AssumptionsCardPage() {
|
|
58
|
+
const { plan } = usePlan()
|
|
59
|
+
const startYear = currentStartYear()
|
|
60
|
+
const snapshot = useMemo(() => buildAssumptionsSnapshot(plan, startYear), [plan, startYear])
|
|
61
|
+
const sourceById = useMemo(() => new Map(PARAMETER_PROVENANCE.map((s) => [s.id, s])), [])
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<section>
|
|
65
|
+
<div className="card">
|
|
66
|
+
<h2>Your assumptions, on one card</h2>
|
|
67
|
+
<p className="card-hint">
|
|
68
|
+
Every number RetireGolden shows follows from the assumptions below — the same plan run through tools with
|
|
69
|
+
different assumptions (or hidden ones) will disagree. Each value is tagged with where it comes from, and the
|
|
70
|
+
copy buttons export the whole card so you can hand your exact assumptions to another tool or a professional
|
|
71
|
+
and see where the inputs differ. (The rest of the plan — incomes, expenses, goals — travels in a plan backup
|
|
72
|
+
from the planner home.)
|
|
73
|
+
</p>
|
|
74
|
+
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
|
|
75
|
+
<CopyButton label="Copy as text" copiedLabel="Copied ✓" text={() => assumptionsExportText(snapshot)} />
|
|
76
|
+
<CopyButton label="Copy as JSON" copiedLabel="Copied ✓" text={() => assumptionsExportJson(snapshot)} />
|
|
77
|
+
</div>
|
|
78
|
+
<p className="field-hint" style={{ marginTop: '0.5rem' }}>
|
|
79
|
+
Applies to the {snapshot.packYear} tax year · parameter figures compiled {snapshot.dataAsOf} ·{' '}
|
|
80
|
+
<Link to="/disclaimer">full source list</Link> · <Link to="/how-tested">how RetireGolden is tested</Link>
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{snapshot.groups.map((group) => (
|
|
85
|
+
<div className="card" key={group.id}>
|
|
86
|
+
<h3 style={{ marginTop: 0 }}>{group.label}</h3>
|
|
87
|
+
<table className="provenance-table">
|
|
88
|
+
<thead>
|
|
89
|
+
<tr>
|
|
90
|
+
<th scope="col">Assumption</th>
|
|
91
|
+
<th scope="col">Where it comes from</th>
|
|
92
|
+
</tr>
|
|
93
|
+
</thead>
|
|
94
|
+
<tbody>
|
|
95
|
+
{group.rows.map((row) => {
|
|
96
|
+
const source = row.sourceId ? sourceById.get(row.sourceId) : undefined
|
|
97
|
+
const chip = PROVENANCE_CHIP[row.provenance]
|
|
98
|
+
return (
|
|
99
|
+
<tr key={row.id}>
|
|
100
|
+
<th scope="row">
|
|
101
|
+
<span className="provenance-label">{row.label}</span>
|
|
102
|
+
<span className="provenance-figures muted small">{row.value}</span>
|
|
103
|
+
</th>
|
|
104
|
+
<td>
|
|
105
|
+
<span className="muted small" title={chip.title}>
|
|
106
|
+
{chip.label}
|
|
107
|
+
</span>
|
|
108
|
+
{source ? (
|
|
109
|
+
<>
|
|
110
|
+
{' · '}
|
|
111
|
+
<a href={source.url} target="_blank" rel="noopener noreferrer">
|
|
112
|
+
{source.publisher} ↗
|
|
113
|
+
</a>
|
|
114
|
+
</>
|
|
115
|
+
) : null}
|
|
116
|
+
</td>
|
|
117
|
+
</tr>
|
|
118
|
+
)
|
|
119
|
+
})}
|
|
120
|
+
</tbody>
|
|
121
|
+
</table>
|
|
122
|
+
</div>
|
|
123
|
+
))}
|
|
124
|
+
|
|
125
|
+
<div className="card">
|
|
126
|
+
<p className="field-hint" style={{ margin: 0 }}>
|
|
127
|
+
Monte Carlo results also depend on the market model, path count, and seed chosen on the{' '}
|
|
128
|
+
<Link to={`/plan/${plan.id}/monte-carlo`}>Monte Carlo page</Link> — your expected returns and inflation above
|
|
129
|
+
stay the center of every model's distribution. To change anything on this card, use{' '}
|
|
130
|
+
<Link to={`/plan/${plan.id}/assumptions`}>Assumptions</Link>, <Link to={`/plan/${plan.id}/accounts`}>Accounts</Link>,
|
|
131
|
+
or <Link to={`/plan/${plan.id}/household`}>Household</Link>.
|
|
132
|
+
</p>
|
|
133
|
+
</div>
|
|
134
|
+
</section>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bucket reporting lens card (spending-paths & SWR-lenses plan, Goal 5): an
|
|
3
|
+
* opt-in re-reading of the same ledger balances as time-segmented buckets —
|
|
4
|
+
* "the next N years of net spending", then growth. Reporting only: nothing
|
|
5
|
+
* feeds back into the engine, and the honest evidence note stays attached
|
|
6
|
+
* because the bucket literature (Estrada; Kitces) finds no systematic benefit
|
|
7
|
+
* over total-return investing — the comfort is the product, not extra return.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useMemo, useState } from 'react'
|
|
11
|
+
import {
|
|
12
|
+
Area,
|
|
13
|
+
AreaChart,
|
|
14
|
+
CartesianGrid,
|
|
15
|
+
Legend,
|
|
16
|
+
ResponsiveContainer,
|
|
17
|
+
Tooltip,
|
|
18
|
+
XAxis,
|
|
19
|
+
YAxis,
|
|
20
|
+
} from 'recharts'
|
|
21
|
+
|
|
22
|
+
import type { ProjectionResult } from '@retiregolden/engine/projection/types'
|
|
23
|
+
import { bucketLens, BUCKET_PRESETS } from './bucketLens'
|
|
24
|
+
import { chartTooltipStyle } from './chartStyle'
|
|
25
|
+
import { frameH } from './chartFrame'
|
|
26
|
+
import { fmtMoney, fmtMoneyCompact } from './format'
|
|
27
|
+
import { SelectField } from './fields'
|
|
28
|
+
|
|
29
|
+
const BUCKET_COLORS = ['var(--chart-5)', 'var(--chart-2)', 'var(--chart-1)']
|
|
30
|
+
|
|
31
|
+
export function BucketLensCard({
|
|
32
|
+
result,
|
|
33
|
+
adj,
|
|
34
|
+
}: {
|
|
35
|
+
result: ProjectionResult
|
|
36
|
+
/** Dollar-mode adapter from ResultsPage: (year, nominal) → displayed dollars. */
|
|
37
|
+
adj: (year: number, v: number) => number
|
|
38
|
+
}) {
|
|
39
|
+
const [presetId, setPresetId] = useState<'off' | 'three' | 'two'>('off')
|
|
40
|
+
const preset = BUCKET_PRESETS.find((p) => p.id === presetId) ?? null
|
|
41
|
+
|
|
42
|
+
const rows = useMemo(() => {
|
|
43
|
+
if (!preset) return []
|
|
44
|
+
return bucketLens(result, preset.spans).map((row) => ({
|
|
45
|
+
year: row.year,
|
|
46
|
+
...Object.fromEntries(row.buckets.map((b, i) => [`b${i}`, adj(row.year, b)])),
|
|
47
|
+
}))
|
|
48
|
+
}, [result, preset, adj])
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="chart-card">
|
|
52
|
+
<h2>Bucket view (a reporting lens)</h2>
|
|
53
|
+
<p className="card-hint">
|
|
54
|
+
Many retirees think in buckets: a couple of years of spending in cash, several more in stable assets, the
|
|
55
|
+
rest invested for growth. This view reads the same projected balances that way — bucket 1 holds the next
|
|
56
|
+
years of <em>net</em> spending (spending plus taxes, minus that year's income), and so on — without
|
|
57
|
+
changing how the plan is invested or simulated. The buckets sum exactly to the investable total every year.
|
|
58
|
+
</p>
|
|
59
|
+
<div className="form-grid">
|
|
60
|
+
<SelectField
|
|
61
|
+
label="Bucket definition"
|
|
62
|
+
help="Purely presentational: pick how the projected balances are segmented. Bucket studies (Estrada; Kitces) find no systematic return or safety benefit from actually managing money as buckets versus a rebalanced total-return portfolio — the value is legibility, so RetireGolden reports buckets but never invests by them."
|
|
63
|
+
value={presetId}
|
|
64
|
+
options={[
|
|
65
|
+
{ value: 'off', label: 'Off' },
|
|
66
|
+
...BUCKET_PRESETS.map((p) => ({ value: p.id, label: p.label })),
|
|
67
|
+
]}
|
|
68
|
+
onCommit={(v) => setPresetId(v as 'off' | 'three' | 'two')}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
{preset ? (
|
|
72
|
+
<>
|
|
73
|
+
<div className="chart-frame" style={frameH(280)}>
|
|
74
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
75
|
+
<AreaChart
|
|
76
|
+
data={rows}
|
|
77
|
+
margin={{ left: 12, right: 8, top: 8 }}
|
|
78
|
+
aria-label="Bucket view: projected balances segmented into spending buckets, year by year"
|
|
79
|
+
>
|
|
80
|
+
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3" />
|
|
81
|
+
<XAxis dataKey="year" interval="equidistantPreserveStart" tick={{ fill: 'var(--muted)', fontSize: 12 }} />
|
|
82
|
+
<YAxis tickFormatter={fmtMoneyCompact} tick={{ fill: 'var(--muted)', fontSize: 12 }} width={70} />
|
|
83
|
+
<Legend />
|
|
84
|
+
{preset.bucketLabels.map((label, i) => (
|
|
85
|
+
<Area
|
|
86
|
+
key={label}
|
|
87
|
+
dataKey={`b${i}`}
|
|
88
|
+
stackId="buckets"
|
|
89
|
+
name={label}
|
|
90
|
+
stroke={BUCKET_COLORS[i % BUCKET_COLORS.length]}
|
|
91
|
+
fill={BUCKET_COLORS[i % BUCKET_COLORS.length]}
|
|
92
|
+
fillOpacity={0.55}
|
|
93
|
+
/>
|
|
94
|
+
))}
|
|
95
|
+
<Tooltip
|
|
96
|
+
formatter={(v: unknown) => fmtMoney(Number(v))}
|
|
97
|
+
contentStyle={chartTooltipStyle}
|
|
98
|
+
wrapperStyle={{ zIndex: 2 }}
|
|
99
|
+
/>
|
|
100
|
+
</AreaChart>
|
|
101
|
+
</ResponsiveContainer>
|
|
102
|
+
</div>
|
|
103
|
+
<p className="field-hint" style={{ marginTop: '0.5rem' }}>
|
|
104
|
+
Honest labeling: the research record (Estrada's bucket studies; Kitces) finds bucket{' '}
|
|
105
|
+
<em>management</em> adds no systematic benefit over a rebalanced total-return portfolio — refilling rules
|
|
106
|
+
are market-timing in disguise. RetireGolden therefore simulates your plan total-return and offers buckets
|
|
107
|
+
only as this view. Years where income covers spending show an empty bucket 1; near the end of the plan
|
|
108
|
+
the leading buckets drain because fewer spending years remain.
|
|
109
|
+
</p>
|
|
110
|
+
</>
|
|
111
|
+
) : null}
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
}
|