@retiregolden/engine 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.
Files changed (199) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +79 -0
  3. package/dist/allocation/assetClasses.d.ts +94 -0
  4. package/dist/allocation/assetClasses.js +215 -0
  5. package/dist/decisions/annuitization.d.ts +72 -0
  6. package/dist/decisions/annuitization.js +184 -0
  7. package/dist/decisions/decisionFixtures.d.ts +48 -0
  8. package/dist/decisions/decisionFixtures.js +253 -0
  9. package/dist/decisions/evaluateCandidate.d.ts +61 -0
  10. package/dist/decisions/evaluateCandidate.js +193 -0
  11. package/dist/decisions/generators.d.ts +110 -0
  12. package/dist/decisions/generators.js +667 -0
  13. package/dist/decisions/index.d.ts +17 -0
  14. package/dist/decisions/index.js +17 -0
  15. package/dist/decisions/insightsAdapter.d.ts +27 -0
  16. package/dist/decisions/insightsAdapter.js +51 -0
  17. package/dist/decisions/objectives.d.ts +96 -0
  18. package/dist/decisions/objectives.js +319 -0
  19. package/dist/decisions/pensionElection.d.ts +112 -0
  20. package/dist/decisions/pensionElection.js +239 -0
  21. package/dist/decisions/search.d.ts +51 -0
  22. package/dist/decisions/search.js +110 -0
  23. package/dist/decisions/spendingSolver.d.ts +70 -0
  24. package/dist/decisions/spendingSolver.js +180 -0
  25. package/dist/decisions/spiaQuotes.d.ts +21 -0
  26. package/dist/decisions/spiaQuotes.js +43 -0
  27. package/dist/decisions/stochastic.d.ts +11 -0
  28. package/dist/decisions/stochastic.js +66 -0
  29. package/dist/decisions/swrComparator.d.ts +54 -0
  30. package/dist/decisions/swrComparator.js +89 -0
  31. package/dist/decisions/tournament.d.ts +53 -0
  32. package/dist/decisions/tournament.js +104 -0
  33. package/dist/decisions/types.d.ts +125 -0
  34. package/dist/decisions/types.js +12 -0
  35. package/dist/index.d.ts +13 -0
  36. package/dist/index.js +12 -0
  37. package/dist/insights/detectors/annuitizationHeadroom.d.ts +11 -0
  38. package/dist/insights/detectors/annuitizationHeadroom.js +77 -0
  39. package/dist/insights/detectors/assetLocation.d.ts +9 -0
  40. package/dist/insights/detectors/assetLocation.js +86 -0
  41. package/dist/insights/detectors/hecmBufferCandidate.d.ts +12 -0
  42. package/dist/insights/detectors/hecmBufferCandidate.js +76 -0
  43. package/dist/insights/detectors/incomeFloorFunded.d.ts +8 -0
  44. package/dist/insights/detectors/incomeFloorFunded.js +52 -0
  45. package/dist/insights/detectors/irmaaTierEdge.d.ts +2 -0
  46. package/dist/insights/detectors/irmaaTierEdge.js +102 -0
  47. package/dist/insights/detectors/pensionElectionPending.d.ts +11 -0
  48. package/dist/insights/detectors/pensionElectionPending.js +62 -0
  49. package/dist/insights/detectors/qcdEfficiency.d.ts +2 -0
  50. package/dist/insights/detectors/qcdEfficiency.js +62 -0
  51. package/dist/insights/detectors/rothBridgeHeadroom.d.ts +2 -0
  52. package/dist/insights/detectors/rothBridgeHeadroom.js +77 -0
  53. package/dist/insights/detectors/spendingGuardrails.d.ts +2 -0
  54. package/dist/insights/detectors/spendingGuardrails.js +77 -0
  55. package/dist/insights/detectors/spendingHeadroom.d.ts +12 -0
  56. package/dist/insights/detectors/spendingHeadroom.js +92 -0
  57. package/dist/insights/detectors/ssBridgeGap.d.ts +9 -0
  58. package/dist/insights/detectors/ssBridgeGap.js +92 -0
  59. package/dist/insights/detectors/stateRelocation.d.ts +12 -0
  60. package/dist/insights/detectors/stateRelocation.js +99 -0
  61. package/dist/insights/detectors/widowsPenalty.d.ts +12 -0
  62. package/dist/insights/detectors/widowsPenalty.js +145 -0
  63. package/dist/insights/registry.d.ts +4 -0
  64. package/dist/insights/registry.js +62 -0
  65. package/dist/insights/runInsights.d.ts +6 -0
  66. package/dist/insights/runInsights.js +15 -0
  67. package/dist/insights/types.d.ts +70 -0
  68. package/dist/insights/types.js +1 -0
  69. package/dist/ladder/bridge.d.ts +72 -0
  70. package/dist/ladder/bridge.js +59 -0
  71. package/dist/ladder/fedInvest.d.ts +55 -0
  72. package/dist/ladder/fedInvest.js +77 -0
  73. package/dist/ladder/fundedRatio.d.ts +41 -0
  74. package/dist/ladder/fundedRatio.js +46 -0
  75. package/dist/ladder/ladderMath.d.ts +88 -0
  76. package/dist/ladder/ladderMath.js +140 -0
  77. package/dist/longevity/ssaPeriod2022.d.ts +10 -0
  78. package/dist/longevity/ssaPeriod2022.js +51 -0
  79. package/dist/longevity/types.d.ts +39 -0
  80. package/dist/longevity/types.js +1 -0
  81. package/dist/model/migrations.d.ts +20 -0
  82. package/dist/model/migrations.js +67 -0
  83. package/dist/model/plan.d.ts +3108 -0
  84. package/dist/model/plan.js +1514 -0
  85. package/dist/montecarlo/frontiers.d.ts +22 -0
  86. package/dist/montecarlo/frontiers.js +61 -0
  87. package/dist/montecarlo/historicalReturns.d.ts +27 -0
  88. package/dist/montecarlo/historicalReturns.js +124 -0
  89. package/dist/montecarlo/historicalSuites.d.ts +45 -0
  90. package/dist/montecarlo/historicalSuites.js +102 -0
  91. package/dist/montecarlo/ltcShock.d.ts +42 -0
  92. package/dist/montecarlo/ltcShock.js +62 -0
  93. package/dist/montecarlo/marketModels.d.ts +248 -0
  94. package/dist/montecarlo/marketModels.js +672 -0
  95. package/dist/montecarlo/mortality.d.ts +31 -0
  96. package/dist/montecarlo/mortality.js +61 -0
  97. package/dist/montecarlo/riskBasedGuardrails.d.ts +93 -0
  98. package/dist/montecarlo/riskBasedGuardrails.js +200 -0
  99. package/dist/montecarlo/rng.d.ts +18 -0
  100. package/dist/montecarlo/rng.js +44 -0
  101. package/dist/montecarlo/run.d.ts +210 -0
  102. package/dist/montecarlo/run.js +353 -0
  103. package/dist/montecarlo/sharedPaths.d.ts +43 -0
  104. package/dist/montecarlo/sharedPaths.js +28 -0
  105. package/dist/montecarlo/survival.d.ts +49 -0
  106. package/dist/montecarlo/survival.js +126 -0
  107. package/dist/params/data/realYieldCurve2026.d.ts +17 -0
  108. package/dist/params/data/realYieldCurve2026.js +26 -0
  109. package/dist/params/data/year2026.d.ts +14 -0
  110. package/dist/params/data/year2026.js +199 -0
  111. package/dist/params/index.d.ts +63 -0
  112. package/dist/params/index.js +134 -0
  113. package/dist/params/provenance.d.ts +29 -0
  114. package/dist/params/provenance.js +123 -0
  115. package/dist/params/state/data/year2026.d.ts +18 -0
  116. package/dist/params/state/data/year2026.js +581 -0
  117. package/dist/params/state/index.d.ts +13 -0
  118. package/dist/params/state/index.js +26 -0
  119. package/dist/params/state/types.d.ts +73 -0
  120. package/dist/params/state/types.js +10 -0
  121. package/dist/params/types.d.ts +201 -0
  122. package/dist/params/types.js +7 -0
  123. package/dist/projection/annuityForms.d.ts +62 -0
  124. package/dist/projection/annuityForms.js +87 -0
  125. package/dist/projection/compare.d.ts +96 -0
  126. package/dist/projection/compare.js +216 -0
  127. package/dist/projection/flatTax.d.ts +7 -0
  128. package/dist/projection/flatTax.js +14 -0
  129. package/dist/projection/optimizePlan.d.ts +328 -0
  130. package/dist/projection/optimizePlan.js +1039 -0
  131. package/dist/projection/relocation.d.ts +149 -0
  132. package/dist/projection/relocation.js +317 -0
  133. package/dist/projection/simulate.d.ts +61 -0
  134. package/dist/projection/simulate.js +2754 -0
  135. package/dist/projection/types.d.ts +381 -0
  136. package/dist/projection/types.js +9 -0
  137. package/dist/rmd/jointLifeTable.d.ts +8 -0
  138. package/dist/rmd/jointLifeTable.js +663 -0
  139. package/dist/rmd/rmd.d.ts +29 -0
  140. package/dist/rmd/rmd.js +34 -0
  141. package/dist/scenarios/scenarios.d.ts +70 -0
  142. package/dist/scenarios/scenarios.js +130 -0
  143. package/dist/socialSecurity/benefitFactor.d.ts +13 -0
  144. package/dist/socialSecurity/benefitFactor.js +40 -0
  145. package/dist/socialSecurity/claimFactor.d.ts +33 -0
  146. package/dist/socialSecurity/claimFactor.js +58 -0
  147. package/dist/socialSecurity/disability.d.ts +36 -0
  148. package/dist/socialSecurity/disability.js +42 -0
  149. package/dist/socialSecurity/familyMaximum.d.ts +38 -0
  150. package/dist/socialSecurity/familyMaximum.js +48 -0
  151. package/dist/socialSecurity/maritalBenefits.d.ts +57 -0
  152. package/dist/socialSecurity/maritalBenefits.js +87 -0
  153. package/dist/socialSecurity/nra.d.ts +30 -0
  154. package/dist/socialSecurity/nra.js +83 -0
  155. package/dist/socialSecurity/piaFromEarnings.d.ts +100 -0
  156. package/dist/socialSecurity/piaFromEarnings.js +202 -0
  157. package/dist/socialSecurity/ssaWageData.d.ts +68 -0
  158. package/dist/socialSecurity/ssaWageData.js +306 -0
  159. package/dist/socialSecurity/survivorBenefit.d.ts +61 -0
  160. package/dist/socialSecurity/survivorBenefit.js +61 -0
  161. package/dist/spending/abw.d.ts +57 -0
  162. package/dist/spending/abw.js +80 -0
  163. package/dist/spending/flexibleGoals.d.ts +62 -0
  164. package/dist/spending/flexibleGoals.js +136 -0
  165. package/dist/spending/guardrails.d.ts +78 -0
  166. package/dist/spending/guardrails.js +100 -0
  167. package/dist/spending/layers.d.ts +81 -0
  168. package/dist/spending/layers.js +67 -0
  169. package/dist/spending/shapePresets.d.ts +42 -0
  170. package/dist/spending/shapePresets.js +77 -0
  171. package/dist/strategies/accountEligibility.d.ts +81 -0
  172. package/dist/strategies/accountEligibility.js +98 -0
  173. package/dist/strategies/inheritedIra.d.ts +45 -0
  174. package/dist/strategies/inheritedIra.js +43 -0
  175. package/dist/strategies/iraBasis.d.ts +48 -0
  176. package/dist/strategies/iraBasis.js +46 -0
  177. package/dist/strategies/optimizer.d.ts +307 -0
  178. package/dist/strategies/optimizer.js +551 -0
  179. package/dist/strategies/rothBasis.d.ts +67 -0
  180. package/dist/strategies/rothBasis.js +83 -0
  181. package/dist/strategies/rothConversion.d.ts +55 -0
  182. package/dist/strategies/rothConversion.js +83 -0
  183. package/dist/strategies/sepp.d.ts +38 -0
  184. package/dist/strategies/sepp.js +53 -0
  185. package/dist/tax/aca.d.ts +40 -0
  186. package/dist/tax/aca.js +82 -0
  187. package/dist/tax/federalTax.d.ts +110 -0
  188. package/dist/tax/federalTax.js +249 -0
  189. package/dist/tax/medicare.d.ts +20 -0
  190. package/dist/tax/medicare.js +36 -0
  191. package/dist/tax/propertySale.d.ts +46 -0
  192. package/dist/tax/propertySale.js +35 -0
  193. package/dist/tax/stateTax.d.ts +66 -0
  194. package/dist/tax/stateTax.js +198 -0
  195. package/dist/testing/money.d.ts +7 -0
  196. package/dist/testing/money.js +17 -0
  197. package/dist/testing/planFixtures.d.ts +29 -0
  198. package/dist/testing/planFixtures.js +141 -0
  199. package/package.json +75 -0
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Bounded stochastic frontier sweeps.
3
+ *
4
+ * The helpers build ordinary plan variants and evaluate them through the
5
+ * shared-path primitive. They do not search; callers provide small fixed grids.
6
+ */
7
+ import type { Plan } from '../model/plan.js';
8
+ import { type SharedPathComparisonOptions } from './sharedPaths.js';
9
+ export declare const MAX_FRONTIER_POINTS = 15;
10
+ export interface StochasticFrontierPoint {
11
+ id: string;
12
+ label: string;
13
+ x: number;
14
+ successRate: number;
15
+ requiredFloorSuccessRate: number;
16
+ targetLifestyleSuccessRate: number;
17
+ p10EndingAfterTaxEstate: number;
18
+ medianEndingAfterTaxEstate: number;
19
+ expectedShortfallDollars: number;
20
+ }
21
+ export declare function buildSpendingSuccessFrontier(plan: Plan, opts: SharedPathComparisonOptions, multipliers?: readonly number[]): StochasticFrontierPoint[];
22
+ export declare function buildRetirementAgeSuccessFrontier(plan: Plan, opts: SharedPathComparisonOptions, deltas?: readonly number[]): StochasticFrontierPoint[];
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Bounded stochastic frontier sweeps.
3
+ *
4
+ * The helpers build ordinary plan variants and evaluate them through the
5
+ * shared-path primitive. They do not search; callers provide small fixed grids.
6
+ */
7
+ import { comparePlansOnSharedMarketPaths, } from './sharedPaths.js';
8
+ export const MAX_FRONTIER_POINTS = 15;
9
+ function assertBounded(pointCount) {
10
+ if (pointCount > MAX_FRONTIER_POINTS) {
11
+ throw new Error(`Frontier sweeps are capped at ${MAX_FRONTIER_POINTS} points; received ${pointCount}.`);
12
+ }
13
+ }
14
+ function pointFromRow(id, label, x, row) {
15
+ return {
16
+ id,
17
+ label,
18
+ x,
19
+ successRate: row.summary.successRate,
20
+ requiredFloorSuccessRate: row.summary.requiredFloorSuccessRate,
21
+ targetLifestyleSuccessRate: row.summary.targetLifestyleSuccessRate,
22
+ p10EndingAfterTaxEstate: row.summary.endingAfterTaxEstate.percentiles.p10,
23
+ medianEndingAfterTaxEstate: row.summary.endingAfterTaxEstate.percentiles.p50,
24
+ expectedShortfallDollars: row.summary.downsideRisk.expectedShortfallDollars,
25
+ };
26
+ }
27
+ export function buildSpendingSuccessFrontier(plan, opts, multipliers = [0.85, 0.9, 0.95, 1, 1.05, 1.1, 1.15]) {
28
+ assertBounded(multipliers.length);
29
+ const variants = multipliers.map((multiplier) => {
30
+ const baseAnnual = Math.max(0, Math.round(plan.expenses.baseAnnual * multiplier));
31
+ const pct = Math.round(multiplier * 100);
32
+ return {
33
+ id: `spending-${pct}`,
34
+ label: `${pct}% spending`,
35
+ plan: { ...plan, expenses: { ...plan.expenses, baseAnnual } },
36
+ };
37
+ });
38
+ const comparison = comparePlansOnSharedMarketPaths(variants, opts);
39
+ return comparison.rows.map((row, index) => pointFromRow(row.id, row.label, variants[index].plan.expenses.baseAnnual, row));
40
+ }
41
+ export function buildRetirementAgeSuccessFrontier(plan, opts, deltas = [-2, -1, 0, 1, 2]) {
42
+ assertBounded(deltas.length);
43
+ const variants = deltas.map((delta) => {
44
+ const people = plan.household.people.map((person) => ({
45
+ ...person,
46
+ retirementAge: person.retirementAge === null ? null : Math.min(80, Math.max(30, person.retirementAge + delta)),
47
+ }));
48
+ const changedAges = people
49
+ .map((person) => person.retirementAge)
50
+ .filter((age) => age !== null);
51
+ const x = changedAges.length > 0 ? Math.min(...changedAges) : 0;
52
+ return {
53
+ id: `retirement-${delta >= 0 ? 'plus' : 'minus'}-${Math.abs(delta)}`,
54
+ label: delta === 0 ? 'Current retirement age' : `${Math.abs(delta)}y ${delta < 0 ? 'earlier' : 'later'}`,
55
+ plan: { ...plan, household: { ...plan.household, people } },
56
+ x,
57
+ };
58
+ });
59
+ const comparison = comparePlansOnSharedMarketPaths(variants, opts);
60
+ return comparison.rows.map((row, index) => pointFromRow(row.id, row.label, variants[index].x ?? 0, row));
61
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Annual US market history for the bootstrap return models (roadmap V4).
3
+ *
4
+ * Values are approximate (≈0.1–0.5pp), transcribed from the public Damodaran
5
+ * (NYU Stern) "Historical Returns on Stocks, Bonds and Bills" dataset, which
6
+ * itself derives from Shiller's annual series: S&P 500 total return, 10-year
7
+ * Treasury total return, and calendar-year CPI inflation. Bootstrap sampling
8
+ * cares about the joint distribution and sequencing of these series, not
9
+ * basis-point precision. Refresh alongside the annual parameter-pack
10
+ * workstream (see DOCS/maintenance-schedule.md, standing workstreams).
11
+ *
12
+ * Source: https://pages.stern.nyu.edu/~adamodar/New_Home_Page/datafile/histretSP.html
13
+ */
14
+ export interface HistoricalYear {
15
+ year: number;
16
+ /** S&P 500 total return, percent. */
17
+ stocksPct: number;
18
+ /** 10-year US Treasury total return, percent. */
19
+ bondsPct: number;
20
+ /** CPI-U calendar-year inflation, percent. */
21
+ inflationPct: number;
22
+ }
23
+ export declare const HISTORICAL_YEARS: readonly HistoricalYear[];
24
+ /** Blended nominal portfolio return for one historical year. */
25
+ export declare function portfolioReturnPct(year: HistoricalYear, equityWeightPct: number): number;
26
+ /** Mean blended return across the dataset (centers bootstrap shocks at zero). */
27
+ export declare function meanPortfolioReturnPct(equityWeightPct: number): number;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Annual US market history for the bootstrap return models (roadmap V4).
3
+ *
4
+ * Values are approximate (≈0.1–0.5pp), transcribed from the public Damodaran
5
+ * (NYU Stern) "Historical Returns on Stocks, Bonds and Bills" dataset, which
6
+ * itself derives from Shiller's annual series: S&P 500 total return, 10-year
7
+ * Treasury total return, and calendar-year CPI inflation. Bootstrap sampling
8
+ * cares about the joint distribution and sequencing of these series, not
9
+ * basis-point precision. Refresh alongside the annual parameter-pack
10
+ * workstream (see DOCS/maintenance-schedule.md, standing workstreams).
11
+ *
12
+ * Source: https://pages.stern.nyu.edu/~adamodar/New_Home_Page/datafile/histretSP.html
13
+ */
14
+ // prettier-ignore
15
+ export const HISTORICAL_YEARS = [
16
+ { year: 1928, stocksPct: 43.8, bondsPct: 0.8, inflationPct: -1.2 },
17
+ { year: 1929, stocksPct: -8.3, bondsPct: 4.2, inflationPct: 0.6 },
18
+ { year: 1930, stocksPct: -25.1, bondsPct: 4.5, inflationPct: -6.4 },
19
+ { year: 1931, stocksPct: -43.8, bondsPct: -2.6, inflationPct: -9.3 },
20
+ { year: 1932, stocksPct: -8.6, bondsPct: 8.8, inflationPct: -10.3 },
21
+ { year: 1933, stocksPct: 50.0, bondsPct: 1.9, inflationPct: 0.8 },
22
+ { year: 1934, stocksPct: -1.2, bondsPct: 8.0, inflationPct: 1.5 },
23
+ { year: 1935, stocksPct: 46.7, bondsPct: 4.5, inflationPct: 3.0 },
24
+ { year: 1936, stocksPct: 31.9, bondsPct: 5.0, inflationPct: 1.4 },
25
+ { year: 1937, stocksPct: -35.3, bondsPct: 1.4, inflationPct: 2.9 },
26
+ { year: 1938, stocksPct: 29.3, bondsPct: 4.2, inflationPct: -2.8 },
27
+ { year: 1939, stocksPct: -1.1, bondsPct: 4.4, inflationPct: 0.0 },
28
+ { year: 1940, stocksPct: -10.7, bondsPct: 5.4, inflationPct: 0.7 },
29
+ { year: 1941, stocksPct: -12.8, bondsPct: -2.0, inflationPct: 9.9 },
30
+ { year: 1942, stocksPct: 19.2, bondsPct: 2.3, inflationPct: 9.0 },
31
+ { year: 1943, stocksPct: 25.1, bondsPct: 2.5, inflationPct: 3.0 },
32
+ { year: 1944, stocksPct: 19.0, bondsPct: 2.6, inflationPct: 2.3 },
33
+ { year: 1945, stocksPct: 35.8, bondsPct: 3.8, inflationPct: 2.2 },
34
+ { year: 1946, stocksPct: -8.4, bondsPct: 3.1, inflationPct: 18.1 },
35
+ { year: 1947, stocksPct: 5.2, bondsPct: 0.9, inflationPct: 8.8 },
36
+ { year: 1948, stocksPct: 5.7, bondsPct: 2.0, inflationPct: 3.0 },
37
+ { year: 1949, stocksPct: 18.3, bondsPct: 4.7, inflationPct: -2.1 },
38
+ { year: 1950, stocksPct: 30.8, bondsPct: 0.4, inflationPct: 5.9 },
39
+ { year: 1951, stocksPct: 23.7, bondsPct: -0.3, inflationPct: 6.0 },
40
+ { year: 1952, stocksPct: 18.2, bondsPct: 2.3, inflationPct: 0.8 },
41
+ { year: 1953, stocksPct: -1.2, bondsPct: 4.1, inflationPct: 0.7 },
42
+ { year: 1954, stocksPct: 52.6, bondsPct: 3.3, inflationPct: -0.7 },
43
+ { year: 1955, stocksPct: 32.6, bondsPct: -1.3, inflationPct: 0.4 },
44
+ { year: 1956, stocksPct: 7.4, bondsPct: -2.3, inflationPct: 3.0 },
45
+ { year: 1957, stocksPct: -10.5, bondsPct: 6.8, inflationPct: 2.9 },
46
+ { year: 1958, stocksPct: 43.7, bondsPct: -2.1, inflationPct: 1.8 },
47
+ { year: 1959, stocksPct: 12.1, bondsPct: -2.6, inflationPct: 1.7 },
48
+ { year: 1960, stocksPct: 0.3, bondsPct: 11.6, inflationPct: 1.4 },
49
+ { year: 1961, stocksPct: 26.6, bondsPct: 2.1, inflationPct: 0.7 },
50
+ { year: 1962, stocksPct: -8.8, bondsPct: 5.7, inflationPct: 1.3 },
51
+ { year: 1963, stocksPct: 22.6, bondsPct: 1.7, inflationPct: 1.6 },
52
+ { year: 1964, stocksPct: 16.4, bondsPct: 3.7, inflationPct: 1.0 },
53
+ { year: 1965, stocksPct: 12.4, bondsPct: 0.7, inflationPct: 1.9 },
54
+ { year: 1966, stocksPct: -10.0, bondsPct: 2.9, inflationPct: 3.5 },
55
+ { year: 1967, stocksPct: 23.8, bondsPct: -1.6, inflationPct: 3.0 },
56
+ { year: 1968, stocksPct: 10.8, bondsPct: 3.3, inflationPct: 4.7 },
57
+ { year: 1969, stocksPct: -8.2, bondsPct: -5.0, inflationPct: 6.2 },
58
+ { year: 1970, stocksPct: 3.6, bondsPct: 16.8, inflationPct: 5.6 },
59
+ { year: 1971, stocksPct: 14.2, bondsPct: 9.8, inflationPct: 3.3 },
60
+ { year: 1972, stocksPct: 18.8, bondsPct: 2.8, inflationPct: 3.4 },
61
+ { year: 1973, stocksPct: -14.3, bondsPct: 3.7, inflationPct: 8.7 },
62
+ { year: 1974, stocksPct: -25.9, bondsPct: 2.0, inflationPct: 12.3 },
63
+ { year: 1975, stocksPct: 37.0, bondsPct: 3.6, inflationPct: 6.9 },
64
+ { year: 1976, stocksPct: 23.8, bondsPct: 16.0, inflationPct: 4.9 },
65
+ { year: 1977, stocksPct: -7.0, bondsPct: 1.3, inflationPct: 6.7 },
66
+ { year: 1978, stocksPct: 6.5, bondsPct: -0.8, inflationPct: 9.0 },
67
+ { year: 1979, stocksPct: 18.5, bondsPct: 0.7, inflationPct: 13.3 },
68
+ { year: 1980, stocksPct: 31.7, bondsPct: -3.0, inflationPct: 12.5 },
69
+ { year: 1981, stocksPct: -4.7, bondsPct: 8.2, inflationPct: 8.9 },
70
+ { year: 1982, stocksPct: 20.4, bondsPct: 32.8, inflationPct: 3.8 },
71
+ { year: 1983, stocksPct: 22.3, bondsPct: 3.2, inflationPct: 3.8 },
72
+ { year: 1984, stocksPct: 6.1, bondsPct: 13.7, inflationPct: 3.9 },
73
+ { year: 1985, stocksPct: 31.2, bondsPct: 25.7, inflationPct: 3.8 },
74
+ { year: 1986, stocksPct: 18.5, bondsPct: 24.3, inflationPct: 1.1 },
75
+ { year: 1987, stocksPct: 5.8, bondsPct: -5.0, inflationPct: 4.4 },
76
+ { year: 1988, stocksPct: 16.5, bondsPct: 8.2, inflationPct: 4.4 },
77
+ { year: 1989, stocksPct: 31.5, bondsPct: 17.7, inflationPct: 4.6 },
78
+ { year: 1990, stocksPct: -3.1, bondsPct: 6.2, inflationPct: 6.1 },
79
+ { year: 1991, stocksPct: 30.2, bondsPct: 15.0, inflationPct: 3.1 },
80
+ { year: 1992, stocksPct: 7.5, bondsPct: 9.4, inflationPct: 2.9 },
81
+ { year: 1993, stocksPct: 10.0, bondsPct: 14.2, inflationPct: 2.7 },
82
+ { year: 1994, stocksPct: 1.3, bondsPct: -8.0, inflationPct: 2.7 },
83
+ { year: 1995, stocksPct: 37.2, bondsPct: 23.5, inflationPct: 2.5 },
84
+ { year: 1996, stocksPct: 22.7, bondsPct: 1.4, inflationPct: 3.3 },
85
+ { year: 1997, stocksPct: 33.1, bondsPct: 9.9, inflationPct: 1.7 },
86
+ { year: 1998, stocksPct: 28.3, bondsPct: 14.9, inflationPct: 1.6 },
87
+ { year: 1999, stocksPct: 20.9, bondsPct: -8.3, inflationPct: 2.7 },
88
+ { year: 2000, stocksPct: -9.0, bondsPct: 16.7, inflationPct: 3.4 },
89
+ { year: 2001, stocksPct: -11.9, bondsPct: 5.6, inflationPct: 1.6 },
90
+ { year: 2002, stocksPct: -22.0, bondsPct: 15.1, inflationPct: 2.4 },
91
+ { year: 2003, stocksPct: 28.4, bondsPct: 0.4, inflationPct: 1.9 },
92
+ { year: 2004, stocksPct: 10.7, bondsPct: 4.5, inflationPct: 3.3 },
93
+ { year: 2005, stocksPct: 4.8, bondsPct: 2.9, inflationPct: 3.4 },
94
+ { year: 2006, stocksPct: 15.6, bondsPct: 2.0, inflationPct: 2.5 },
95
+ { year: 2007, stocksPct: 5.5, bondsPct: 10.2, inflationPct: 4.1 },
96
+ { year: 2008, stocksPct: -36.6, bondsPct: 20.1, inflationPct: 0.1 },
97
+ { year: 2009, stocksPct: 25.9, bondsPct: -11.1, inflationPct: 2.7 },
98
+ { year: 2010, stocksPct: 14.8, bondsPct: 8.5, inflationPct: 1.5 },
99
+ { year: 2011, stocksPct: 2.1, bondsPct: 16.0, inflationPct: 3.0 },
100
+ { year: 2012, stocksPct: 15.9, bondsPct: 3.0, inflationPct: 1.7 },
101
+ { year: 2013, stocksPct: 32.2, bondsPct: -9.1, inflationPct: 1.5 },
102
+ { year: 2014, stocksPct: 13.5, bondsPct: 10.7, inflationPct: 0.8 },
103
+ { year: 2015, stocksPct: 1.4, bondsPct: 1.3, inflationPct: 0.7 },
104
+ { year: 2016, stocksPct: 11.8, bondsPct: 0.7, inflationPct: 2.1 },
105
+ { year: 2017, stocksPct: 21.6, bondsPct: 2.8, inflationPct: 2.1 },
106
+ { year: 2018, stocksPct: -4.2, bondsPct: 0.0, inflationPct: 1.9 },
107
+ { year: 2019, stocksPct: 31.2, bondsPct: 9.6, inflationPct: 2.3 },
108
+ { year: 2020, stocksPct: 18.0, bondsPct: 11.3, inflationPct: 1.4 },
109
+ { year: 2021, stocksPct: 28.5, bondsPct: -4.4, inflationPct: 7.0 },
110
+ { year: 2022, stocksPct: -18.0, bondsPct: -17.8, inflationPct: 6.5 },
111
+ { year: 2023, stocksPct: 26.1, bondsPct: 3.9, inflationPct: 3.4 },
112
+ ];
113
+ /** Blended nominal portfolio return for one historical year. */
114
+ export function portfolioReturnPct(year, equityWeightPct) {
115
+ const w = equityWeightPct / 100;
116
+ return year.stocksPct * w + year.bondsPct * (1 - w);
117
+ }
118
+ /** Mean blended return across the dataset (centers bootstrap shocks at zero). */
119
+ export function meanPortfolioReturnPct(equityWeightPct) {
120
+ let sum = 0;
121
+ for (const y of HISTORICAL_YEARS)
122
+ sum += portfolioReturnPct(y, equityWeightPct);
123
+ return sum / HISTORICAL_YEARS.length;
124
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Deterministic historical stress suites.
3
+ *
4
+ * These are not random Monte Carlo runs. They replay every rolling historical
5
+ * window, plus optional reversed windows, through the same projection ledger.
6
+ */
7
+ import { type Plan } from '../model/plan.js';
8
+ import { type ProjectionSummary } from '../projection/compare.js';
9
+ import { type SimulateOptions } from '../projection/simulate.js';
10
+ import type { ProjectionResult } from '../projection/types.js';
11
+ export type HistoricalStressSuiteKind = 'rolling' | 'reversed';
12
+ export interface HistoricalStressWindow {
13
+ suite: HistoricalStressSuiteKind;
14
+ label: string;
15
+ startHistoricalYear: number;
16
+ endHistoricalYear: number;
17
+ reversed: boolean;
18
+ marketYears: number[];
19
+ projection: ProjectionResult;
20
+ summary: ProjectionSummary;
21
+ success: boolean;
22
+ totalShortfall: number;
23
+ totalRequiredShortfall: number;
24
+ totalTargetShortfall: number;
25
+ }
26
+ export interface HistoricalStressSuite {
27
+ kind: HistoricalStressSuiteKind;
28
+ name: string;
29
+ windowLengthYears: number;
30
+ windows: HistoricalStressWindow[];
31
+ worstByEndingAfterTaxEstate: HistoricalStressWindow[];
32
+ worstByTotalShortfall: HistoricalStressWindow[];
33
+ }
34
+ export interface HistoricalStressSuiteResult {
35
+ windowLengthYears: number;
36
+ suites: HistoricalStressSuite[];
37
+ }
38
+ export interface HistoricalStressSuiteOptions extends SimulateOptions {
39
+ equityWeightPct?: number;
40
+ windowLengthYears?: number;
41
+ suites?: readonly HistoricalStressSuiteKind[];
42
+ classShocks?: boolean;
43
+ worstWindowCount?: number;
44
+ }
45
+ export declare function runHistoricalStressSuites(plan: Plan, opts: HistoricalStressSuiteOptions): HistoricalStressSuiteResult;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Deterministic historical stress suites.
3
+ *
4
+ * These are not random Monte Carlo runs. They replay every rolling historical
5
+ * window, plus optional reversed windows, through the same projection ledger.
6
+ */
7
+ import { ASSET_CLASS_IDS } from '../model/plan.js';
8
+ import { summarizeProjection } from '../projection/compare.js';
9
+ import { simulatePlan } from '../projection/simulate.js';
10
+ import { HISTORICAL_YEARS, meanPortfolioReturnPct, portfolioReturnPct } from './historicalReturns.js';
11
+ function historicalReplaySeries(args) {
12
+ const mean = meanPortfolioReturnPct(args.equityWeightPct);
13
+ const meanStocks = meanPortfolioReturnPct(100);
14
+ const meanBonds = meanPortfolioReturnPct(0);
15
+ const returnShockPct = new Array(args.projectionYears);
16
+ const inflationPct = new Array(args.projectionYears);
17
+ const classSeries = args.classShocks
18
+ ? Object.fromEntries(ASSET_CLASS_IDS.map((id) => [id, new Array(args.projectionYears)]))
19
+ : null;
20
+ const marketYears = [];
21
+ for (let i = 0; i < args.projectionYears; i++) {
22
+ const offset = i % args.windowLength;
23
+ const historicalIndex = args.reversed
24
+ ? args.startIndex + args.windowLength - 1 - offset
25
+ : args.startIndex + offset;
26
+ const sample = HISTORICAL_YEARS[historicalIndex];
27
+ marketYears.push(sample.year);
28
+ returnShockPct[i] = portfolioReturnPct(sample, args.equityWeightPct) - mean;
29
+ inflationPct[i] = sample.inflationPct;
30
+ if (classSeries) {
31
+ const stockShock = sample.stocksPct - meanStocks;
32
+ classSeries.usStocks[i] = stockShock;
33
+ classSeries.intlStocks[i] = stockShock;
34
+ classSeries.bonds[i] = sample.bondsPct - meanBonds;
35
+ classSeries.cash[i] = 0;
36
+ }
37
+ }
38
+ return {
39
+ market: classSeries ? { returnShockPct, inflationPct, classReturnShockPct: classSeries } : { returnShockPct, inflationPct },
40
+ marketYears,
41
+ };
42
+ }
43
+ function total(result, pick) {
44
+ return result.years.reduce((sum, year) => sum + pick(year), 0);
45
+ }
46
+ function suiteName(kind, windowLength) {
47
+ return kind === 'rolling' ? `Rolling ${windowLength}-year historical windows` : `Reversed ${windowLength}-year historical windows`;
48
+ }
49
+ export function runHistoricalStressSuites(plan, opts) {
50
+ const baseline = simulatePlan(plan, { startYear: opts.startYear, taxCalculator: opts.taxCalculator });
51
+ const projectionYears = baseline.years.length;
52
+ const windowLength = Math.max(1, Math.min(opts.windowLengthYears ?? projectionYears, HISTORICAL_YEARS.length));
53
+ const kinds = opts.suites ?? ['rolling', 'reversed'];
54
+ const lastStart = HISTORICAL_YEARS.length - windowLength;
55
+ const worstWindowCount = Math.max(1, opts.worstWindowCount ?? 5);
56
+ const suites = [];
57
+ for (const kind of kinds) {
58
+ const reversed = kind === 'reversed';
59
+ const windows = [];
60
+ for (let startIndex = 0; startIndex <= lastStart; startIndex++) {
61
+ const first = HISTORICAL_YEARS[startIndex];
62
+ const last = HISTORICAL_YEARS[startIndex + windowLength - 1];
63
+ const { market, marketYears } = historicalReplaySeries({
64
+ startIndex,
65
+ windowLength,
66
+ projectionYears,
67
+ equityWeightPct: opts.equityWeightPct ?? 60,
68
+ reversed,
69
+ classShocks: opts.classShocks ?? false,
70
+ });
71
+ const projection = simulatePlan(plan, { startYear: opts.startYear, taxCalculator: opts.taxCalculator, market });
72
+ const summary = summarizeProjection(plan, projection);
73
+ windows.push({
74
+ suite: kind,
75
+ label: reversed ? `${first.year}-${last.year} reversed` : `${first.year}-${last.year}`,
76
+ startHistoricalYear: first.year,
77
+ endHistoricalYear: last.year,
78
+ reversed,
79
+ marketYears,
80
+ projection,
81
+ summary,
82
+ success: projection.depletionYear === null,
83
+ totalShortfall: total(projection, (year) => year.shortfall),
84
+ totalRequiredShortfall: total(projection, (year) => year.requiredShortfall),
85
+ totalTargetShortfall: total(projection, (year) => year.targetShortfall),
86
+ });
87
+ }
88
+ suites.push({
89
+ kind,
90
+ name: suiteName(kind, windowLength),
91
+ windowLengthYears: windowLength,
92
+ windows,
93
+ worstByEndingAfterTaxEstate: [...windows]
94
+ .sort((a, b) => a.summary.endingAfterTaxEstate - b.summary.endingAfterTaxEstate)
95
+ .slice(0, worstWindowCount),
96
+ worstByTotalShortfall: [...windows]
97
+ .sort((a, b) => b.totalShortfall - a.totalShortfall || a.summary.endingAfterTaxEstate - b.summary.endingAfterTaxEstate)
98
+ .slice(0, worstWindowCount),
99
+ });
100
+ }
101
+ return { windowLengthYears: windowLength, suites };
102
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Probabilistic long-term-care shock for Monte Carlo (roadmap V6 §4).
3
+ *
4
+ * Each path may draw a paid-care episode per person — incidence, onset age, and
5
+ * duration — so an LTC policy's risk reduction shows up in the success-rate and
6
+ * ending-balance distribution, not just the single deterministic "LTC stress"
7
+ * what-if. The sampled episode is injected as a plan careEvent, so the existing
8
+ * engine offset (monthly cap × inflation rider, elimination period, benefit
9
+ * period) applies unchanged.
10
+ *
11
+ * Default distribution (planning precision, editable in the UI):
12
+ * - incidence: ~50% of people 65+ need a significant period of *paid* LTSS
13
+ * (HHS/ASPE, "Long-Term Services and Supports for Older Americans").
14
+ * - duration: right-skewed — most need <2 years, a meaningful tail needs 5+.
15
+ * - annual cost: ~$75k, near the national median for assisted living / a year
16
+ * of substantial home care (Genworth Cost of Care survey range).
17
+ */
18
+ import type { CareEvent } from '../model/plan.js';
19
+ import type { Rng } from './rng.js';
20
+ export interface LtcShockParams {
21
+ /** Probability a person experiences a paid-care episode in late life. */
22
+ incidence: number;
23
+ /** Onset age sampled uniformly in [minOnsetAge, maxOnsetAge]. */
24
+ minOnsetAge: number;
25
+ maxOnsetAge: number;
26
+ /** Discrete duration distribution (years → relative weight). */
27
+ durations: ReadonlyArray<{
28
+ years: number;
29
+ weight: number;
30
+ }>;
31
+ /** Annual care cost, today's dollars. */
32
+ annualCost: number;
33
+ }
34
+ export declare const DEFAULT_LTC_SHOCK: LtcShockParams;
35
+ /**
36
+ * Sample zero or one care episode per person for one path. Onset is clamped to
37
+ * be no earlier than the person's current age so the spike lands in the future.
38
+ */
39
+ export declare function sampleCareEvents(rng: Rng, people: ReadonlyArray<{
40
+ id: string;
41
+ dob: string;
42
+ }>, startYear: number, params: LtcShockParams): CareEvent[];
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Probabilistic long-term-care shock for Monte Carlo (roadmap V6 §4).
3
+ *
4
+ * Each path may draw a paid-care episode per person — incidence, onset age, and
5
+ * duration — so an LTC policy's risk reduction shows up in the success-rate and
6
+ * ending-balance distribution, not just the single deterministic "LTC stress"
7
+ * what-if. The sampled episode is injected as a plan careEvent, so the existing
8
+ * engine offset (monthly cap × inflation rider, elimination period, benefit
9
+ * period) applies unchanged.
10
+ *
11
+ * Default distribution (planning precision, editable in the UI):
12
+ * - incidence: ~50% of people 65+ need a significant period of *paid* LTSS
13
+ * (HHS/ASPE, "Long-Term Services and Supports for Older Americans").
14
+ * - duration: right-skewed — most need <2 years, a meaningful tail needs 5+.
15
+ * - annual cost: ~$75k, near the national median for assisted living / a year
16
+ * of substantial home care (Genworth Cost of Care survey range).
17
+ */
18
+ export const DEFAULT_LTC_SHOCK = {
19
+ incidence: 0.5,
20
+ minOnsetAge: 80,
21
+ maxOnsetAge: 90,
22
+ durations: [
23
+ { years: 1, weight: 0.3 },
24
+ { years: 2, weight: 0.3 },
25
+ { years: 3, weight: 0.2 },
26
+ { years: 5, weight: 0.15 },
27
+ { years: 8, weight: 0.05 },
28
+ ],
29
+ annualCost: 75_000,
30
+ };
31
+ function pickDuration(rng, durations) {
32
+ const total = durations.reduce((sum, d) => sum + d.weight, 0);
33
+ let r = rng.next() * total;
34
+ for (const d of durations) {
35
+ r -= d.weight;
36
+ if (r <= 0)
37
+ return d.years;
38
+ }
39
+ return durations[durations.length - 1]?.years ?? 1;
40
+ }
41
+ /**
42
+ * Sample zero or one care episode per person for one path. Onset is clamped to
43
+ * be no earlier than the person's current age so the spike lands in the future.
44
+ */
45
+ export function sampleCareEvents(rng, people, startYear, params) {
46
+ const events = [];
47
+ for (const p of people) {
48
+ if (rng.next() >= params.incidence)
49
+ continue;
50
+ const span = Math.max(0, params.maxOnsetAge - params.minOnsetAge);
51
+ const currentAge = startYear - Number(p.dob.slice(0, 4));
52
+ const startAge = Math.max(currentAge, params.minOnsetAge + rng.nextInt(span + 1));
53
+ events.push({
54
+ id: `ltc-shock-${p.id}`,
55
+ personId: p.id,
56
+ startAge,
57
+ durationYears: pickDuration(rng, params.durations),
58
+ annualCost: params.annualCost,
59
+ });
60
+ }
61
+ return events;
62
+ }