@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,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire types between the Monte Carlo pool and its workers (roadmap V4).
|
|
3
|
+
* Everything here must survive structured clone: plain objects, arrays, and
|
|
4
|
+
* Float64Array only.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Plan } from '@retiregolden/engine/model/plan'
|
|
8
|
+
import type { AnnuitizationSweep } from '@retiregolden/engine/decisions/annuitization'
|
|
9
|
+
import type { StochasticFrontierPoint } from '@retiregolden/engine/montecarlo/frontiers'
|
|
10
|
+
import type { HistoricalStressSuiteKind } from '@retiregolden/engine/montecarlo/historicalSuites'
|
|
11
|
+
import type { LtcShockParams } from '@retiregolden/engine/montecarlo/ltcShock'
|
|
12
|
+
import type { MarketModelConfig } from '@retiregolden/engine/montecarlo/marketModels'
|
|
13
|
+
import type { RiskBasedGuardrailSolution } from '@retiregolden/engine/montecarlo/riskBasedGuardrails'
|
|
14
|
+
import type { MonteCarloPathsResult } from '@retiregolden/engine/montecarlo/run'
|
|
15
|
+
|
|
16
|
+
export interface McWorkerRequest {
|
|
17
|
+
kind: 'monteCarlo'
|
|
18
|
+
plan: Plan
|
|
19
|
+
startYear: number
|
|
20
|
+
seed: number
|
|
21
|
+
model: MarketModelConfig
|
|
22
|
+
pathCount: number
|
|
23
|
+
/** Global index of this worker's first path (keeps seeding partition-independent). */
|
|
24
|
+
firstPathIndex: number
|
|
25
|
+
/** Post a progress message every N completed paths. */
|
|
26
|
+
progressEvery: number
|
|
27
|
+
/** Sample lifespans from the mortality table per path (roadmap V6). */
|
|
28
|
+
stochasticLongevity?: boolean
|
|
29
|
+
/** Probabilistic LTC care episode per path (roadmap V6); omitted/null = off. */
|
|
30
|
+
ltcShock?: LtcShockParams | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FrontierWorkerRequest {
|
|
34
|
+
kind: 'frontiers'
|
|
35
|
+
plan: Plan
|
|
36
|
+
startYear: number
|
|
37
|
+
seed: number
|
|
38
|
+
model: MarketModelConfig
|
|
39
|
+
pathCount: number
|
|
40
|
+
stochasticLongevity?: boolean
|
|
41
|
+
ltcShock?: LtcShockParams | null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface FrontierWorkerResult {
|
|
45
|
+
spending: StochasticFrontierPoint[]
|
|
46
|
+
retirement: StochasticFrontierPoint[]
|
|
47
|
+
/** Annuitization sweep (annuity-pension-and-home-equity, step 2). */
|
|
48
|
+
annuitization: AnnuitizationSweep
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RiskBasedWorkerRequest {
|
|
52
|
+
kind: 'riskBasedGuardrails'
|
|
53
|
+
plan: Plan
|
|
54
|
+
startYear: number
|
|
55
|
+
seed: number
|
|
56
|
+
model: MarketModelConfig
|
|
57
|
+
pathCount: number
|
|
58
|
+
stochasticLongevity?: boolean
|
|
59
|
+
ltcShock?: LtcShockParams | null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface HistoricalWorkerRequest {
|
|
63
|
+
kind: 'historicalSuites'
|
|
64
|
+
plan: Plan
|
|
65
|
+
startYear: number
|
|
66
|
+
equityWeightPct: number
|
|
67
|
+
classShocks?: boolean
|
|
68
|
+
worstWindowCount?: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface HistoricalStressWindowView {
|
|
72
|
+
suite: HistoricalStressSuiteKind
|
|
73
|
+
label: string
|
|
74
|
+
startHistoricalYear: number
|
|
75
|
+
endHistoricalYear: number
|
|
76
|
+
reversed: boolean
|
|
77
|
+
success: boolean
|
|
78
|
+
projection: { depletionYear: number | null }
|
|
79
|
+
summary: { endingAfterTaxEstate: number }
|
|
80
|
+
totalShortfall: number
|
|
81
|
+
totalRequiredShortfall: number
|
|
82
|
+
totalTargetShortfall: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface HistoricalStressSuiteView {
|
|
86
|
+
kind: HistoricalStressSuiteKind
|
|
87
|
+
name: string
|
|
88
|
+
windowLengthYears: number
|
|
89
|
+
worstByEndingAfterTaxEstate: HistoricalStressWindowView[]
|
|
90
|
+
worstByTotalShortfall: HistoricalStressWindowView[]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface HistoricalStressSuiteViewResult {
|
|
94
|
+
windowLengthYears: number
|
|
95
|
+
suites: HistoricalStressSuiteView[]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type WorkerRequest = McWorkerRequest | FrontierWorkerRequest | HistoricalWorkerRequest | RiskBasedWorkerRequest
|
|
99
|
+
|
|
100
|
+
export type McWorkerResponse =
|
|
101
|
+
| { type: 'progress'; completed: number }
|
|
102
|
+
| { type: 'done'; result: MonteCarloPathsResult }
|
|
103
|
+
| { type: 'error'; message: string }
|
|
104
|
+
|
|
105
|
+
export type FrontierWorkerResponse =
|
|
106
|
+
| { type: 'frontiersDone'; result: FrontierWorkerResult }
|
|
107
|
+
| { type: 'error'; message: string }
|
|
108
|
+
|
|
109
|
+
export type HistoricalWorkerResponse =
|
|
110
|
+
| { type: 'historicalSuitesDone'; result: HistoricalStressSuiteViewResult }
|
|
111
|
+
| { type: 'error'; message: string }
|
|
112
|
+
|
|
113
|
+
export type RiskBasedWorkerResponse =
|
|
114
|
+
| { type: 'progress'; completed: number }
|
|
115
|
+
| { type: 'riskBasedDone'; result: RiskBasedGuardrailSolution }
|
|
116
|
+
| { type: 'error'; message: string }
|
|
117
|
+
|
|
118
|
+
export type WorkerResponse = McWorkerResponse | FrontierWorkerResponse | HistoricalWorkerResponse | RiskBasedWorkerResponse
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Worker entry for Monte Carlo paths (roadmap V4). One request per
|
|
3
|
+
* worker run: simulate the assigned slice of paths, stream progress, post
|
|
4
|
+
* the reduced per-path results back (Float64Array buffers transferred, not
|
|
5
|
+
* copied).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { WorkerRequest, WorkerResponse } from './messages'
|
|
9
|
+
import {
|
|
10
|
+
runFrontierRequest,
|
|
11
|
+
runHistoricalStressSuiteRequest,
|
|
12
|
+
runMcRequest,
|
|
13
|
+
runRiskBasedGuardrailRequest,
|
|
14
|
+
} from './runRequest'
|
|
15
|
+
|
|
16
|
+
const post = (msg: WorkerResponse, transfer: Transferable[] = []) =>
|
|
17
|
+
(self as unknown as Worker).postMessage(msg, transfer)
|
|
18
|
+
|
|
19
|
+
self.onmessage = (event: MessageEvent<WorkerRequest>) => {
|
|
20
|
+
const req = event.data
|
|
21
|
+
try {
|
|
22
|
+
if (req.kind === 'frontiers') {
|
|
23
|
+
post({ type: 'frontiersDone', result: runFrontierRequest(req) })
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
if (req.kind === 'historicalSuites') {
|
|
27
|
+
post({ type: 'historicalSuitesDone', result: runHistoricalStressSuiteRequest(req) })
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
if (req.kind === 'riskBasedGuardrails') {
|
|
31
|
+
post({ type: 'riskBasedDone', result: runRiskBasedGuardrailRequest(req, (completed) => post({ type: 'progress', completed })) })
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
const result = runMcRequest(req, (completed) => {
|
|
35
|
+
if (completed % req.progressEvery === 0) post({ type: 'progress', completed })
|
|
36
|
+
})
|
|
37
|
+
post(
|
|
38
|
+
{ type: 'done', result },
|
|
39
|
+
result.paths.map((p) => p.investableByYear.buffer),
|
|
40
|
+
)
|
|
41
|
+
} catch (err) {
|
|
42
|
+
post({ type: 'error', message: err instanceof Error ? err.message : String(err) })
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/mc/pool.ts
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-pool Monte Carlo runner (roadmap V4, feature catalog §11).
|
|
3
|
+
*
|
|
4
|
+
* Splits the requested paths across a pool of Web Workers (one per core by
|
|
5
|
+
* default, capped at 8) and merges the results. Per-path seeds derive from
|
|
6
|
+
* (seed, globalPathIndex) inside the engine, so the outcome is identical for
|
|
7
|
+
* any worker count — including the synchronous fallback used where Worker is
|
|
8
|
+
* unavailable (tests, very old browsers).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Plan } from '@retiregolden/engine/model/plan'
|
|
12
|
+
import type { LtcShockParams } from '@retiregolden/engine/montecarlo/ltcShock'
|
|
13
|
+
import type { MarketModelConfig } from '@retiregolden/engine/montecarlo/marketModels'
|
|
14
|
+
import {
|
|
15
|
+
aggregateMonteCarlo,
|
|
16
|
+
mergePathResults,
|
|
17
|
+
type MonteCarloPathsResult,
|
|
18
|
+
type MonteCarloSummary,
|
|
19
|
+
} from '@retiregolden/engine/montecarlo/run'
|
|
20
|
+
import type { RiskBasedGuardrailSolution } from '@retiregolden/engine/montecarlo/riskBasedGuardrails'
|
|
21
|
+
import type {
|
|
22
|
+
FrontierWorkerRequest,
|
|
23
|
+
FrontierWorkerResponse,
|
|
24
|
+
FrontierWorkerResult,
|
|
25
|
+
HistoricalStressSuiteViewResult,
|
|
26
|
+
HistoricalWorkerRequest,
|
|
27
|
+
HistoricalWorkerResponse,
|
|
28
|
+
McWorkerRequest,
|
|
29
|
+
McWorkerResponse,
|
|
30
|
+
RiskBasedWorkerRequest,
|
|
31
|
+
RiskBasedWorkerResponse,
|
|
32
|
+
} from './messages'
|
|
33
|
+
import {
|
|
34
|
+
runFrontierRequest,
|
|
35
|
+
runHistoricalStressSuiteRequest,
|
|
36
|
+
runMcRequest,
|
|
37
|
+
runRiskBasedGuardrailRequest,
|
|
38
|
+
} from './runRequest'
|
|
39
|
+
import { runWorkerRequest } from '../workers/run'
|
|
40
|
+
|
|
41
|
+
export interface MonteCarloRunOptions {
|
|
42
|
+
startYear: number
|
|
43
|
+
/** 1,000 default; 10,000 on demand. */
|
|
44
|
+
pathCount: number
|
|
45
|
+
seed: number
|
|
46
|
+
model: MarketModelConfig
|
|
47
|
+
/** Default: hardwareConcurrency − 1, clamped to [1, 8]. */
|
|
48
|
+
workerCount?: number
|
|
49
|
+
/** Sample lifespans from the mortality table per path (roadmap V6). */
|
|
50
|
+
stochasticLongevity?: boolean
|
|
51
|
+
/** Probabilistic LTC care episode per path (roadmap V6); omitted/null = off. */
|
|
52
|
+
ltcShock?: LtcShockParams | null
|
|
53
|
+
onProgress?: (completedPaths: number, totalPaths: number) => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const DEFAULT_PATH_COUNT = 1000
|
|
57
|
+
export const ON_DEMAND_PATH_COUNT = 10_000
|
|
58
|
+
|
|
59
|
+
function defaultWorkerCount(): number {
|
|
60
|
+
const cores = typeof navigator !== 'undefined' ? (navigator.hardwareConcurrency ?? 4) : 4
|
|
61
|
+
return Math.max(1, Math.min(8, cores - 1))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function makeRequest(plan: Plan, opts: MonteCarloRunOptions, firstPathIndex: number, pathCount: number): McWorkerRequest {
|
|
65
|
+
return {
|
|
66
|
+
kind: 'monteCarlo',
|
|
67
|
+
plan,
|
|
68
|
+
startYear: opts.startYear,
|
|
69
|
+
seed: opts.seed,
|
|
70
|
+
model: opts.model,
|
|
71
|
+
pathCount,
|
|
72
|
+
firstPathIndex,
|
|
73
|
+
progressEvery: Math.max(1, Math.floor(opts.pathCount / 100)),
|
|
74
|
+
stochasticLongevity: opts.stochasticLongevity,
|
|
75
|
+
ltcShock: opts.ltcShock,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface StochasticFrontierRunOptions {
|
|
80
|
+
startYear: number
|
|
81
|
+
pathCount: number
|
|
82
|
+
seed: number
|
|
83
|
+
model: MarketModelConfig
|
|
84
|
+
stochasticLongevity?: boolean
|
|
85
|
+
ltcShock?: LtcShockParams | null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface HistoricalStressSuiteRunOptions {
|
|
89
|
+
startYear: number
|
|
90
|
+
equityWeightPct: number
|
|
91
|
+
classShocks?: boolean
|
|
92
|
+
worstWindowCount?: number
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type { HistoricalStressSuiteViewResult } from './messages'
|
|
96
|
+
|
|
97
|
+
function runInWorker(
|
|
98
|
+
req: McWorkerRequest,
|
|
99
|
+
onProgress: (workerCompleted: number) => void,
|
|
100
|
+
onSpawn?: (worker: Worker) => void,
|
|
101
|
+
): Promise<MonteCarloPathsResult> {
|
|
102
|
+
return runWorkerRequest<McWorkerRequest, McWorkerResponse, MonteCarloPathsResult>({
|
|
103
|
+
request: req,
|
|
104
|
+
createWorker: () => new Worker(new URL('./monteCarlo.worker.ts', import.meta.url), { type: 'module' }),
|
|
105
|
+
interpret: (msg) => {
|
|
106
|
+
if (msg.type === 'progress') {
|
|
107
|
+
onProgress(msg.completed)
|
|
108
|
+
return { kind: 'progress' }
|
|
109
|
+
}
|
|
110
|
+
if (msg.type === 'done') return { kind: 'done', result: msg.result }
|
|
111
|
+
return { kind: 'error', message: msg.message }
|
|
112
|
+
},
|
|
113
|
+
errorLabel: 'Monte Carlo worker failed',
|
|
114
|
+
onSpawn,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function runFrontiersInWorker(req: FrontierWorkerRequest): Promise<FrontierWorkerResult> {
|
|
119
|
+
return runWorkerRequest<FrontierWorkerRequest, FrontierWorkerResponse, FrontierWorkerResult>({
|
|
120
|
+
request: req,
|
|
121
|
+
createWorker: () => new Worker(new URL('./monteCarlo.worker.ts', import.meta.url), { type: 'module' }),
|
|
122
|
+
interpret: (msg) =>
|
|
123
|
+
msg.type === 'frontiersDone' ? { kind: 'done', result: msg.result } : { kind: 'error', message: msg.message },
|
|
124
|
+
errorLabel: 'Frontier worker failed',
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function runHistoricalSuitesInWorker(req: HistoricalWorkerRequest): Promise<HistoricalStressSuiteViewResult> {
|
|
129
|
+
return runWorkerRequest<HistoricalWorkerRequest, HistoricalWorkerResponse, HistoricalStressSuiteViewResult>({
|
|
130
|
+
request: req,
|
|
131
|
+
createWorker: () => new Worker(new URL('./monteCarlo.worker.ts', import.meta.url), { type: 'module' }),
|
|
132
|
+
interpret: (msg) =>
|
|
133
|
+
msg.type === 'historicalSuitesDone'
|
|
134
|
+
? { kind: 'done', result: msg.result }
|
|
135
|
+
: { kind: 'error', message: msg.message },
|
|
136
|
+
errorLabel: 'Historical stress worker failed',
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Run the full Monte Carlo and aggregate it. Reproducible per (plan, seed, model). */
|
|
141
|
+
export async function runMonteCarlo(plan: Plan, opts: MonteCarloRunOptions): Promise<MonteCarloSummary> {
|
|
142
|
+
const total = opts.pathCount
|
|
143
|
+
if (typeof Worker === 'undefined') {
|
|
144
|
+
const result = runMcRequest(makeRequest(plan, opts, 0, total), (completed) => opts.onProgress?.(completed, total))
|
|
145
|
+
return aggregateMonteCarlo(result)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const workerCount = Math.max(1, Math.min(opts.workerCount ?? defaultWorkerCount(), total))
|
|
149
|
+
const base = Math.floor(total / workerCount)
|
|
150
|
+
const extra = total % workerCount
|
|
151
|
+
const completedByWorker = new Array<number>(workerCount).fill(0)
|
|
152
|
+
const reportProgress = () => {
|
|
153
|
+
opts.onProgress?.(
|
|
154
|
+
completedByWorker.reduce((a, b) => a + b, 0),
|
|
155
|
+
total,
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const jobs: Promise<MonteCarloPathsResult>[] = []
|
|
160
|
+
const liveWorkers: Worker[] = []
|
|
161
|
+
let firstPathIndex = 0
|
|
162
|
+
for (let w = 0; w < workerCount; w++) {
|
|
163
|
+
const slice = base + (w < extra ? 1 : 0)
|
|
164
|
+
const req = makeRequest(plan, opts, firstPathIndex, slice)
|
|
165
|
+
firstPathIndex += slice
|
|
166
|
+
const workerIndex = w
|
|
167
|
+
jobs.push(
|
|
168
|
+
runInWorker(
|
|
169
|
+
req,
|
|
170
|
+
(completed) => {
|
|
171
|
+
completedByWorker[workerIndex] = completed
|
|
172
|
+
reportProgress()
|
|
173
|
+
},
|
|
174
|
+
(worker) => liveWorkers.push(worker),
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
let parts: MonteCarloPathsResult[]
|
|
179
|
+
try {
|
|
180
|
+
parts = await Promise.all(jobs)
|
|
181
|
+
} catch (error) {
|
|
182
|
+
// One job failed: stop the siblings instead of letting them burn CPU on a
|
|
183
|
+
// run whose result will be discarded (terminate is a no-op once settled).
|
|
184
|
+
for (const worker of liveWorkers) worker.terminate()
|
|
185
|
+
// The killed siblings' promises still reject (their onerror fires after
|
|
186
|
+
// terminate); swallow those so only this rethrow surfaces.
|
|
187
|
+
for (const job of jobs) job.catch(() => {})
|
|
188
|
+
throw error
|
|
189
|
+
}
|
|
190
|
+
opts.onProgress?.(total, total)
|
|
191
|
+
return aggregateMonteCarlo(mergePathResults(parts))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface RiskBasedGuardrailRunOptions {
|
|
195
|
+
startYear: number
|
|
196
|
+
pathCount: number
|
|
197
|
+
seed: number
|
|
198
|
+
model: MarketModelConfig
|
|
199
|
+
stochasticLongevity?: boolean
|
|
200
|
+
ltcShock?: LtcShockParams | null
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Solve the risk-based guardrail thresholds (dollar balance levels matching the
|
|
205
|
+
* plan's target success band) on a single worker. Deterministic per
|
|
206
|
+
* (plan, seed, model, pathCount).
|
|
207
|
+
*/
|
|
208
|
+
export async function runRiskBasedGuardrailSolve(
|
|
209
|
+
plan: Plan,
|
|
210
|
+
opts: RiskBasedGuardrailRunOptions,
|
|
211
|
+
): Promise<RiskBasedGuardrailSolution> {
|
|
212
|
+
const req: RiskBasedWorkerRequest = {
|
|
213
|
+
kind: 'riskBasedGuardrails',
|
|
214
|
+
plan,
|
|
215
|
+
startYear: opts.startYear,
|
|
216
|
+
seed: opts.seed,
|
|
217
|
+
model: opts.model,
|
|
218
|
+
pathCount: opts.pathCount,
|
|
219
|
+
stochasticLongevity: opts.stochasticLongevity,
|
|
220
|
+
ltcShock: opts.ltcShock,
|
|
221
|
+
}
|
|
222
|
+
if (typeof Worker === 'undefined') return runRiskBasedGuardrailRequest(req)
|
|
223
|
+
return runWorkerRequest<RiskBasedWorkerRequest, RiskBasedWorkerResponse, RiskBasedGuardrailSolution>({
|
|
224
|
+
request: req,
|
|
225
|
+
createWorker: () => new Worker(new URL('./monteCarlo.worker.ts', import.meta.url), { type: 'module' }),
|
|
226
|
+
interpret: (msg) => {
|
|
227
|
+
if (msg.type === 'progress') return { kind: 'progress' }
|
|
228
|
+
if (msg.type === 'riskBasedDone') return { kind: 'done', result: msg.result }
|
|
229
|
+
return { kind: 'error', message: msg.message }
|
|
230
|
+
},
|
|
231
|
+
errorLabel: 'Risk-based guardrail solver failed',
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function runStochasticFrontiers(
|
|
236
|
+
plan: Plan,
|
|
237
|
+
opts: StochasticFrontierRunOptions,
|
|
238
|
+
): Promise<FrontierWorkerResult> {
|
|
239
|
+
const req: FrontierWorkerRequest = {
|
|
240
|
+
kind: 'frontiers',
|
|
241
|
+
plan,
|
|
242
|
+
startYear: opts.startYear,
|
|
243
|
+
seed: opts.seed,
|
|
244
|
+
model: opts.model,
|
|
245
|
+
pathCount: opts.pathCount,
|
|
246
|
+
stochasticLongevity: opts.stochasticLongevity,
|
|
247
|
+
ltcShock: opts.ltcShock,
|
|
248
|
+
}
|
|
249
|
+
if (typeof Worker === 'undefined') return runFrontierRequest(req)
|
|
250
|
+
return runFrontiersInWorker(req)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function runHistoricalStressSuiteViews(
|
|
254
|
+
plan: Plan,
|
|
255
|
+
opts: HistoricalStressSuiteRunOptions,
|
|
256
|
+
): Promise<HistoricalStressSuiteViewResult> {
|
|
257
|
+
const req: HistoricalWorkerRequest = {
|
|
258
|
+
kind: 'historicalSuites',
|
|
259
|
+
plan,
|
|
260
|
+
startYear: opts.startYear,
|
|
261
|
+
equityWeightPct: opts.equityWeightPct,
|
|
262
|
+
classShocks: opts.classShocks,
|
|
263
|
+
worstWindowCount: opts.worstWindowCount,
|
|
264
|
+
}
|
|
265
|
+
if (typeof Worker === 'undefined') return runHistoricalStressSuiteRequest(req)
|
|
266
|
+
return runHistoricalSuitesInWorker(req)
|
|
267
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executes one Monte Carlo work unit. Shared by the Web Worker entry and the
|
|
3
|
+
* pool's synchronous fallback so both paths run the identical tax stack:
|
|
4
|
+
* the federal engine plus the per-state engine (with the plan's flat rate as
|
|
5
|
+
* an override).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Plan } from '@retiregolden/engine/model/plan'
|
|
9
|
+
import { buildAnnuitizationSweep } from '@retiregolden/engine/decisions/annuitization'
|
|
10
|
+
import { buildRetirementAgeSuccessFrontier, buildSpendingSuccessFrontier } from '@retiregolden/engine/montecarlo/frontiers'
|
|
11
|
+
import {
|
|
12
|
+
runHistoricalStressSuites,
|
|
13
|
+
type HistoricalStressWindow,
|
|
14
|
+
} from '@retiregolden/engine/montecarlo/historicalSuites'
|
|
15
|
+
import { createMarketModel } from '@retiregolden/engine/montecarlo/marketModels'
|
|
16
|
+
import {
|
|
17
|
+
solveRiskBasedGuardrails,
|
|
18
|
+
type RiskBasedGuardrailSolution,
|
|
19
|
+
} from '@retiregolden/engine/montecarlo/riskBasedGuardrails'
|
|
20
|
+
import { runMonteCarloPaths, type MonteCarloPathsResult } from '@retiregolden/engine/montecarlo/run'
|
|
21
|
+
import { combineTaxCalculators, createFederalTaxCalculator } from '@retiregolden/engine/tax/federalTax'
|
|
22
|
+
import { createStateTaxCalculator } from '@retiregolden/engine/tax/stateTax'
|
|
23
|
+
import type {
|
|
24
|
+
FrontierWorkerRequest,
|
|
25
|
+
FrontierWorkerResult,
|
|
26
|
+
HistoricalStressSuiteViewResult,
|
|
27
|
+
HistoricalStressWindowView,
|
|
28
|
+
HistoricalWorkerRequest,
|
|
29
|
+
McWorkerRequest,
|
|
30
|
+
RiskBasedWorkerRequest,
|
|
31
|
+
} from './messages'
|
|
32
|
+
|
|
33
|
+
function taxCalculatorForPlan(plan: Plan) {
|
|
34
|
+
return combineTaxCalculators(
|
|
35
|
+
createFederalTaxCalculator(),
|
|
36
|
+
createStateTaxCalculator({
|
|
37
|
+
overridePct: plan.assumptions.stateEffectiveTaxPct,
|
|
38
|
+
localPct: plan.assumptions.localIncomeTaxPct,
|
|
39
|
+
}),
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function runMcRequest(req: McWorkerRequest, onPathDone?: (completed: number) => void): MonteCarloPathsResult {
|
|
44
|
+
const taxCalculator = taxCalculatorForPlan(req.plan)
|
|
45
|
+
return runMonteCarloPaths(req.plan, {
|
|
46
|
+
startYear: req.startYear,
|
|
47
|
+
taxCalculator,
|
|
48
|
+
model: createMarketModel(req.model),
|
|
49
|
+
seed: req.seed,
|
|
50
|
+
pathCount: req.pathCount,
|
|
51
|
+
firstPathIndex: req.firstPathIndex,
|
|
52
|
+
stochasticLongevity: req.stochasticLongevity,
|
|
53
|
+
ltcShock: req.ltcShock,
|
|
54
|
+
onPathDone,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function runRiskBasedGuardrailRequest(
|
|
59
|
+
req: RiskBasedWorkerRequest,
|
|
60
|
+
onProbeDone?: (completed: number, total: number) => void,
|
|
61
|
+
): RiskBasedGuardrailSolution {
|
|
62
|
+
return solveRiskBasedGuardrails(req.plan, {
|
|
63
|
+
startYear: req.startYear,
|
|
64
|
+
taxCalculator: taxCalculatorForPlan(req.plan),
|
|
65
|
+
model: req.model,
|
|
66
|
+
pathCount: req.pathCount,
|
|
67
|
+
seed: req.seed,
|
|
68
|
+
stochasticLongevity: req.stochasticLongevity,
|
|
69
|
+
ltcShock: req.ltcShock,
|
|
70
|
+
onProbeDone,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function runFrontierRequest(req: FrontierWorkerRequest): FrontierWorkerResult {
|
|
75
|
+
const opts = {
|
|
76
|
+
startYear: req.startYear,
|
|
77
|
+
taxCalculator: taxCalculatorForPlan(req.plan),
|
|
78
|
+
model: req.model,
|
|
79
|
+
pathCount: req.pathCount,
|
|
80
|
+
seed: req.seed,
|
|
81
|
+
stochasticLongevity: req.stochasticLongevity,
|
|
82
|
+
ltcShock: req.ltcShock,
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
spending: buildSpendingSuccessFrontier(req.plan, opts),
|
|
86
|
+
retirement: buildRetirementAgeSuccessFrontier(req.plan, opts),
|
|
87
|
+
annuitization: buildAnnuitizationSweep(req.plan, opts),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function windowView(window: HistoricalStressWindow): HistoricalStressWindowView {
|
|
92
|
+
return {
|
|
93
|
+
suite: window.suite,
|
|
94
|
+
label: window.label,
|
|
95
|
+
startHistoricalYear: window.startHistoricalYear,
|
|
96
|
+
endHistoricalYear: window.endHistoricalYear,
|
|
97
|
+
reversed: window.reversed,
|
|
98
|
+
success: window.success,
|
|
99
|
+
projection: { depletionYear: window.projection.depletionYear },
|
|
100
|
+
summary: { endingAfterTaxEstate: window.summary.endingAfterTaxEstate },
|
|
101
|
+
totalShortfall: window.totalShortfall,
|
|
102
|
+
totalRequiredShortfall: window.totalRequiredShortfall,
|
|
103
|
+
totalTargetShortfall: window.totalTargetShortfall,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function runHistoricalStressSuiteRequest(req: HistoricalWorkerRequest): HistoricalStressSuiteViewResult {
|
|
108
|
+
const result = runHistoricalStressSuites(req.plan, {
|
|
109
|
+
startYear: req.startYear,
|
|
110
|
+
taxCalculator: taxCalculatorForPlan(req.plan),
|
|
111
|
+
equityWeightPct: req.equityWeightPct,
|
|
112
|
+
classShocks: req.classShocks,
|
|
113
|
+
worstWindowCount: req.worstWindowCount,
|
|
114
|
+
})
|
|
115
|
+
return {
|
|
116
|
+
windowLengthYears: result.windowLengthYears,
|
|
117
|
+
suites: result.suites.map((suite) => ({
|
|
118
|
+
kind: suite.kind,
|
|
119
|
+
name: suite.name,
|
|
120
|
+
windowLengthYears: suite.windowLengthYears,
|
|
121
|
+
worstByEndingAfterTaxEstate: suite.worstByEndingAfterTaxEstate.map(windowView),
|
|
122
|
+
worstByTotalShortfall: suite.worstByTotalShortfall.map(windowView),
|
|
123
|
+
})),
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire types between the Optimize UI and its worker (roadmap V8). Everything
|
|
3
|
+
* here must survive structured clone — note the tax calculator is NOT passed
|
|
4
|
+
* (it's a closure); the worker rebuilds the standard stack from the plan, just
|
|
5
|
+
* like the Monte Carlo worker (see src/mc/runRequest.ts).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ObjectivePolicyId } from '@retiregolden/engine/decisions'
|
|
9
|
+
import type { Plan } from '@retiregolden/engine/model/plan'
|
|
10
|
+
import type {
|
|
11
|
+
ClaimAgeCoOptimization,
|
|
12
|
+
ExactLedgerConvergenceDiagnostics,
|
|
13
|
+
ExactLedgerRecommendationSchedule,
|
|
14
|
+
ExactLedgerScheduleAdjustment,
|
|
15
|
+
ExactLedgerTournament,
|
|
16
|
+
ExactLedgerValidation,
|
|
17
|
+
} from '@retiregolden/engine/projection/optimizePlan'
|
|
18
|
+
import type { OptimizedSchedule } from '@retiregolden/engine/strategies/optimizer'
|
|
19
|
+
|
|
20
|
+
export interface OptimizeRequest {
|
|
21
|
+
plan: Plan
|
|
22
|
+
startYear: number
|
|
23
|
+
/** Haircut on leftover traditional; defaults to the plan's heirTaxRatePct. */
|
|
24
|
+
liquidationRatePct?: number
|
|
25
|
+
/** HiGHS time limit (seconds). */
|
|
26
|
+
timeLimitSec?: number
|
|
27
|
+
/**
|
|
28
|
+
* Exact-ledger local-search budget for refining winning schedules. Omitted =
|
|
29
|
+
* default 96 simulations. This is a PER-SEED budget: search refines the top
|
|
30
|
+
* two candidates (and a winning MILP schedule), so total search simulations
|
|
31
|
+
* can reach ~2–3× this value. 0 or negative disables search.
|
|
32
|
+
*/
|
|
33
|
+
searchSimulationBudget?: number
|
|
34
|
+
/**
|
|
35
|
+
* Objective policy the tournament ranks by. Omitted = `max-after-tax-estate`
|
|
36
|
+
* (the original behavior). Floor-style policies pick up the plan's bequest
|
|
37
|
+
* target inside the worker (`objectivePolicyForPlan`).
|
|
38
|
+
*/
|
|
39
|
+
objectivePolicyId?: ObjectivePolicyId
|
|
40
|
+
/**
|
|
41
|
+
* Exact-ledger convergence-loop iteration cap (optimizer-exact-ledger-convergence Track 1, Step 1).
|
|
42
|
+
* Omitted = worker default; `≤ 1` disables the loop (single solve = the
|
|
43
|
+
* pre-convergence schedule).
|
|
44
|
+
*/
|
|
45
|
+
convergenceIterations?: number
|
|
46
|
+
/**
|
|
47
|
+
* Co-optimize the SS claim age with the conversion optimum
|
|
48
|
+
* (optimizer-exact-ledger-convergence Track 1, Step 5). Off by default: it runs a bounded alternate-minimization over the
|
|
49
|
+
* claim grid (a small multiple of a single optimize) and can recommend a claim
|
|
50
|
+
* change alongside the schedule, so the caller must surface `claimAge` when on.
|
|
51
|
+
*/
|
|
52
|
+
coOptimizeClaimAge?: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface OptimizeResult {
|
|
56
|
+
schedule: OptimizedSchedule
|
|
57
|
+
postProcessed: OptimizePostProcessing | null
|
|
58
|
+
/** Bounded simple-candidate tournament; contains no ProjectionResult, so it is structured-clone safe as-is. */
|
|
59
|
+
tournament: ExactLedgerTournament
|
|
60
|
+
/** Convergence-loop diagnostics (Step 1); plain scalars, structured-clone safe. */
|
|
61
|
+
convergence: ExactLedgerConvergenceDiagnostics
|
|
62
|
+
/**
|
|
63
|
+
* Claim-age co-optimization diagnostics (Step 5); null unless requested. The
|
|
64
|
+
* `winningClaimPatch` (a plain incomes array) is structured-clone safe.
|
|
65
|
+
*/
|
|
66
|
+
claimAge: ClaimAgeCoOptimization | null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface OptimizePostProcessing {
|
|
70
|
+
rawSchedule: OptimizedSchedule
|
|
71
|
+
cleanedSchedule: OptimizedSchedule
|
|
72
|
+
rawValidation: ExactLedgerValidation
|
|
73
|
+
cleanedValidation: ExactLedgerValidation
|
|
74
|
+
adjustments: ExactLedgerScheduleAdjustment[]
|
|
75
|
+
stabilized: boolean
|
|
76
|
+
iterationCount: number
|
|
77
|
+
pruneIterationCount: number
|
|
78
|
+
recommendationSchedule: ExactLedgerRecommendationSchedule
|
|
79
|
+
cleanedExecutionByYear: { year: number; rothConversion: number }[]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type OptimizeResponse =
|
|
83
|
+
| { type: 'done'; result: OptimizeResult }
|
|
84
|
+
| { type: 'error'; message: string }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Worker entry for the V8 optimizer (roadmap V8). One request per run:
|
|
3
|
+
* solve the plan's conversion MILP off the main thread and post the schedule
|
|
4
|
+
* back. HiGHS-WASM is loaded lazily inside the worker — the ~3 MB wasm is
|
|
5
|
+
* emitted as a separate Vite asset (`?url`) and only fetched when Optimize is
|
|
6
|
+
* actually invoked, never in the main bundle.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// `highs/runtime` is the package's exported alias for build/highs.wasm; `?url`
|
|
10
|
+
// makes Vite emit it as a separate asset fetched lazily by the worker.
|
|
11
|
+
import wasmUrl from 'highs/runtime?url'
|
|
12
|
+
import type { OptimizeRequest, OptimizeResponse } from './messages'
|
|
13
|
+
import { runOptimizeRequest } from './runOptimize'
|
|
14
|
+
|
|
15
|
+
// Deployment marker: keeps the hashed worker asset moving when worker-level
|
|
16
|
+
// serving headers need to be refreshed through immutable CDN caches.
|
|
17
|
+
;(self as unknown as { __retiregoldenOptimizerWorkerBuild?: string }).__retiregoldenOptimizerWorkerBuild =
|
|
18
|
+
'optimizer-worker-csp-2026-06-25'
|
|
19
|
+
|
|
20
|
+
const post = (msg: OptimizeResponse) => (self as unknown as Worker).postMessage(msg)
|
|
21
|
+
|
|
22
|
+
self.onmessage = async (event: MessageEvent<OptimizeRequest>) => {
|
|
23
|
+
try {
|
|
24
|
+
const result = await runOptimizeRequest(event.data, () => wasmUrl)
|
|
25
|
+
post({ type: 'done', result })
|
|
26
|
+
} catch (err) {
|
|
27
|
+
post({ type: 'error', message: err instanceof Error ? err.message : String(err) })
|
|
28
|
+
}
|
|
29
|
+
}
|