@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.
- package/LICENSE +661 -0
- package/README.md +79 -0
- package/dist/allocation/assetClasses.d.ts +94 -0
- package/dist/allocation/assetClasses.js +215 -0
- package/dist/decisions/annuitization.d.ts +72 -0
- package/dist/decisions/annuitization.js +184 -0
- package/dist/decisions/decisionFixtures.d.ts +48 -0
- package/dist/decisions/decisionFixtures.js +253 -0
- package/dist/decisions/evaluateCandidate.d.ts +61 -0
- package/dist/decisions/evaluateCandidate.js +193 -0
- package/dist/decisions/generators.d.ts +110 -0
- package/dist/decisions/generators.js +667 -0
- package/dist/decisions/index.d.ts +17 -0
- package/dist/decisions/index.js +17 -0
- package/dist/decisions/insightsAdapter.d.ts +27 -0
- package/dist/decisions/insightsAdapter.js +51 -0
- package/dist/decisions/objectives.d.ts +96 -0
- package/dist/decisions/objectives.js +319 -0
- package/dist/decisions/pensionElection.d.ts +112 -0
- package/dist/decisions/pensionElection.js +239 -0
- package/dist/decisions/search.d.ts +51 -0
- package/dist/decisions/search.js +110 -0
- package/dist/decisions/spendingSolver.d.ts +70 -0
- package/dist/decisions/spendingSolver.js +180 -0
- package/dist/decisions/spiaQuotes.d.ts +21 -0
- package/dist/decisions/spiaQuotes.js +43 -0
- package/dist/decisions/stochastic.d.ts +11 -0
- package/dist/decisions/stochastic.js +66 -0
- package/dist/decisions/swrComparator.d.ts +54 -0
- package/dist/decisions/swrComparator.js +89 -0
- package/dist/decisions/tournament.d.ts +53 -0
- package/dist/decisions/tournament.js +104 -0
- package/dist/decisions/types.d.ts +125 -0
- package/dist/decisions/types.js +12 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +12 -0
- package/dist/insights/detectors/annuitizationHeadroom.d.ts +11 -0
- package/dist/insights/detectors/annuitizationHeadroom.js +77 -0
- package/dist/insights/detectors/assetLocation.d.ts +9 -0
- package/dist/insights/detectors/assetLocation.js +86 -0
- package/dist/insights/detectors/hecmBufferCandidate.d.ts +12 -0
- package/dist/insights/detectors/hecmBufferCandidate.js +76 -0
- package/dist/insights/detectors/incomeFloorFunded.d.ts +8 -0
- package/dist/insights/detectors/incomeFloorFunded.js +52 -0
- package/dist/insights/detectors/irmaaTierEdge.d.ts +2 -0
- package/dist/insights/detectors/irmaaTierEdge.js +102 -0
- package/dist/insights/detectors/pensionElectionPending.d.ts +11 -0
- package/dist/insights/detectors/pensionElectionPending.js +62 -0
- package/dist/insights/detectors/qcdEfficiency.d.ts +2 -0
- package/dist/insights/detectors/qcdEfficiency.js +62 -0
- package/dist/insights/detectors/rothBridgeHeadroom.d.ts +2 -0
- package/dist/insights/detectors/rothBridgeHeadroom.js +77 -0
- package/dist/insights/detectors/spendingGuardrails.d.ts +2 -0
- package/dist/insights/detectors/spendingGuardrails.js +77 -0
- package/dist/insights/detectors/spendingHeadroom.d.ts +12 -0
- package/dist/insights/detectors/spendingHeadroom.js +92 -0
- package/dist/insights/detectors/ssBridgeGap.d.ts +9 -0
- package/dist/insights/detectors/ssBridgeGap.js +92 -0
- package/dist/insights/detectors/stateRelocation.d.ts +12 -0
- package/dist/insights/detectors/stateRelocation.js +99 -0
- package/dist/insights/detectors/widowsPenalty.d.ts +12 -0
- package/dist/insights/detectors/widowsPenalty.js +145 -0
- package/dist/insights/registry.d.ts +4 -0
- package/dist/insights/registry.js +62 -0
- package/dist/insights/runInsights.d.ts +6 -0
- package/dist/insights/runInsights.js +15 -0
- package/dist/insights/types.d.ts +70 -0
- package/dist/insights/types.js +1 -0
- package/dist/ladder/bridge.d.ts +72 -0
- package/dist/ladder/bridge.js +59 -0
- package/dist/ladder/fedInvest.d.ts +55 -0
- package/dist/ladder/fedInvest.js +77 -0
- package/dist/ladder/fundedRatio.d.ts +41 -0
- package/dist/ladder/fundedRatio.js +46 -0
- package/dist/ladder/ladderMath.d.ts +88 -0
- package/dist/ladder/ladderMath.js +140 -0
- package/dist/longevity/ssaPeriod2022.d.ts +10 -0
- package/dist/longevity/ssaPeriod2022.js +51 -0
- package/dist/longevity/types.d.ts +39 -0
- package/dist/longevity/types.js +1 -0
- package/dist/model/migrations.d.ts +20 -0
- package/dist/model/migrations.js +67 -0
- package/dist/model/plan.d.ts +3108 -0
- package/dist/model/plan.js +1514 -0
- package/dist/montecarlo/frontiers.d.ts +22 -0
- package/dist/montecarlo/frontiers.js +61 -0
- package/dist/montecarlo/historicalReturns.d.ts +27 -0
- package/dist/montecarlo/historicalReturns.js +124 -0
- package/dist/montecarlo/historicalSuites.d.ts +45 -0
- package/dist/montecarlo/historicalSuites.js +102 -0
- package/dist/montecarlo/ltcShock.d.ts +42 -0
- package/dist/montecarlo/ltcShock.js +62 -0
- package/dist/montecarlo/marketModels.d.ts +248 -0
- package/dist/montecarlo/marketModels.js +672 -0
- package/dist/montecarlo/mortality.d.ts +31 -0
- package/dist/montecarlo/mortality.js +61 -0
- package/dist/montecarlo/riskBasedGuardrails.d.ts +93 -0
- package/dist/montecarlo/riskBasedGuardrails.js +200 -0
- package/dist/montecarlo/rng.d.ts +18 -0
- package/dist/montecarlo/rng.js +44 -0
- package/dist/montecarlo/run.d.ts +210 -0
- package/dist/montecarlo/run.js +353 -0
- package/dist/montecarlo/sharedPaths.d.ts +43 -0
- package/dist/montecarlo/sharedPaths.js +28 -0
- package/dist/montecarlo/survival.d.ts +49 -0
- package/dist/montecarlo/survival.js +126 -0
- package/dist/params/data/realYieldCurve2026.d.ts +17 -0
- package/dist/params/data/realYieldCurve2026.js +26 -0
- package/dist/params/data/year2026.d.ts +14 -0
- package/dist/params/data/year2026.js +199 -0
- package/dist/params/index.d.ts +63 -0
- package/dist/params/index.js +134 -0
- package/dist/params/provenance.d.ts +29 -0
- package/dist/params/provenance.js +123 -0
- package/dist/params/state/data/year2026.d.ts +18 -0
- package/dist/params/state/data/year2026.js +581 -0
- package/dist/params/state/index.d.ts +13 -0
- package/dist/params/state/index.js +26 -0
- package/dist/params/state/types.d.ts +73 -0
- package/dist/params/state/types.js +10 -0
- package/dist/params/types.d.ts +201 -0
- package/dist/params/types.js +7 -0
- package/dist/projection/annuityForms.d.ts +62 -0
- package/dist/projection/annuityForms.js +87 -0
- package/dist/projection/compare.d.ts +96 -0
- package/dist/projection/compare.js +216 -0
- package/dist/projection/flatTax.d.ts +7 -0
- package/dist/projection/flatTax.js +14 -0
- package/dist/projection/optimizePlan.d.ts +328 -0
- package/dist/projection/optimizePlan.js +1039 -0
- package/dist/projection/relocation.d.ts +149 -0
- package/dist/projection/relocation.js +317 -0
- package/dist/projection/simulate.d.ts +61 -0
- package/dist/projection/simulate.js +2754 -0
- package/dist/projection/types.d.ts +381 -0
- package/dist/projection/types.js +9 -0
- package/dist/rmd/jointLifeTable.d.ts +8 -0
- package/dist/rmd/jointLifeTable.js +663 -0
- package/dist/rmd/rmd.d.ts +29 -0
- package/dist/rmd/rmd.js +34 -0
- package/dist/scenarios/scenarios.d.ts +70 -0
- package/dist/scenarios/scenarios.js +130 -0
- package/dist/socialSecurity/benefitFactor.d.ts +13 -0
- package/dist/socialSecurity/benefitFactor.js +40 -0
- package/dist/socialSecurity/claimFactor.d.ts +33 -0
- package/dist/socialSecurity/claimFactor.js +58 -0
- package/dist/socialSecurity/disability.d.ts +36 -0
- package/dist/socialSecurity/disability.js +42 -0
- package/dist/socialSecurity/familyMaximum.d.ts +38 -0
- package/dist/socialSecurity/familyMaximum.js +48 -0
- package/dist/socialSecurity/maritalBenefits.d.ts +57 -0
- package/dist/socialSecurity/maritalBenefits.js +87 -0
- package/dist/socialSecurity/nra.d.ts +30 -0
- package/dist/socialSecurity/nra.js +83 -0
- package/dist/socialSecurity/piaFromEarnings.d.ts +100 -0
- package/dist/socialSecurity/piaFromEarnings.js +202 -0
- package/dist/socialSecurity/ssaWageData.d.ts +68 -0
- package/dist/socialSecurity/ssaWageData.js +306 -0
- package/dist/socialSecurity/survivorBenefit.d.ts +61 -0
- package/dist/socialSecurity/survivorBenefit.js +61 -0
- package/dist/spending/abw.d.ts +57 -0
- package/dist/spending/abw.js +80 -0
- package/dist/spending/flexibleGoals.d.ts +62 -0
- package/dist/spending/flexibleGoals.js +136 -0
- package/dist/spending/guardrails.d.ts +78 -0
- package/dist/spending/guardrails.js +100 -0
- package/dist/spending/layers.d.ts +81 -0
- package/dist/spending/layers.js +67 -0
- package/dist/spending/shapePresets.d.ts +42 -0
- package/dist/spending/shapePresets.js +77 -0
- package/dist/strategies/accountEligibility.d.ts +81 -0
- package/dist/strategies/accountEligibility.js +98 -0
- package/dist/strategies/inheritedIra.d.ts +45 -0
- package/dist/strategies/inheritedIra.js +43 -0
- package/dist/strategies/iraBasis.d.ts +48 -0
- package/dist/strategies/iraBasis.js +46 -0
- package/dist/strategies/optimizer.d.ts +307 -0
- package/dist/strategies/optimizer.js +551 -0
- package/dist/strategies/rothBasis.d.ts +67 -0
- package/dist/strategies/rothBasis.js +83 -0
- package/dist/strategies/rothConversion.d.ts +55 -0
- package/dist/strategies/rothConversion.js +83 -0
- package/dist/strategies/sepp.d.ts +38 -0
- package/dist/strategies/sepp.js +53 -0
- package/dist/tax/aca.d.ts +40 -0
- package/dist/tax/aca.js +82 -0
- package/dist/tax/federalTax.d.ts +110 -0
- package/dist/tax/federalTax.js +249 -0
- package/dist/tax/medicare.d.ts +20 -0
- package/dist/tax/medicare.js +36 -0
- package/dist/tax/propertySale.d.ts +46 -0
- package/dist/tax/propertySale.js +35 -0
- package/dist/tax/stateTax.d.ts +66 -0
- package/dist/tax/stateTax.js +198 -0
- package/dist/testing/money.d.ts +7 -0
- package/dist/testing/money.js +17 -0
- package/dist/testing/planFixtures.d.ts +29 -0
- package/dist/testing/planFixtures.js +141 -0
- 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
|
+
}
|