@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,672 @@
1
+ /**
2
+ * Pluggable stochastic market models (roadmap V4, feature catalog §11).
3
+ *
4
+ * Each model turns an injected RNG into one path of per-year market
5
+ * conditions for the deterministic ledger: an additive return shock (single
6
+ * market factor applied to non-cash investable accounts), a realized
7
+ * inflation rate, and — for plans with allocated accounts — per-class
8
+ * correlated shocks sharing the same allocation schema as the deterministic
9
+ * ledger. Shocks are centered so the expected return stays the plan's own
10
+ * assumption; the models supply dispersion, skew, and the return/inflation
11
+ * co-movement.
12
+ *
13
+ * Configs are plain JSON so they can cross the Web Worker boundary.
14
+ *
15
+ * Extended in the stochastic-market-model-library plan (Track 2) to 15+
16
+ * models. All new models are mean-preserving by default (path avg shock ~0),
17
+ * respect RNG draw ordering (new draws after core for classShocks parity),
18
+ * and default lognormal/historical paths are byte-identical.
19
+ */
20
+ import { choleskyDecompose, DEFAULT_CLASS_CORRELATIONS, planUsesAssetAllocation, resolveAssetClassParams, } from '../allocation/assetClasses.js';
21
+ import { ASSET_CLASS_IDS } from '../model/plan.js';
22
+ import { HISTORICAL_YEARS, meanPortfolioReturnPct, portfolioReturnPct } from './historicalReturns.js';
23
+ export function buildLognormalModelConfigForPlan(plan, returnVolPct = 12) {
24
+ const config = {
25
+ type: 'lognormal',
26
+ inflationMeanPct: plan.assumptions.inflationPct,
27
+ returnVolPct,
28
+ };
29
+ if (!planUsesAssetAllocation(plan))
30
+ return config;
31
+ const params = resolveAssetClassParams(plan.assumptions.assetClassParams);
32
+ const volatilityPctByClass = Object.fromEntries(ASSET_CLASS_IDS.map((id) => [id, params[id].volatilityPct]));
33
+ return { ...config, classShocks: { volatilityPctByClass } };
34
+ }
35
+ export function createMarketModel(config) {
36
+ switch (config.type) {
37
+ case 'lognormal':
38
+ return createLognormalModel(config);
39
+ case 'historical':
40
+ return createHistoricalModel(config);
41
+ case 'student-t':
42
+ return createStudentTModel(config);
43
+ case 'regime-switch':
44
+ return createRegimeSwitchModel(config);
45
+ case 'cape-conditioned':
46
+ return createCapeConditionedModel(config);
47
+ case 'stationary':
48
+ return createStationaryBootstrapModel(config);
49
+ case 'empirical':
50
+ return createEmpiricalModel(config);
51
+ case 'garch':
52
+ return createGarchModel(config);
53
+ case 'inflation-regime':
54
+ return createInflationRegimeModel(config);
55
+ case 'reversed-history':
56
+ return createReversedHistoryModel(config);
57
+ case 'user-shock':
58
+ return createUserShockModel(config);
59
+ case 'gaussian':
60
+ return createGaussianModel(config);
61
+ case 'ar1':
62
+ return createAR1Model(config);
63
+ default:
64
+ // exhaustive guard for future
65
+ return createLognormalModel(config);
66
+ }
67
+ }
68
+ /**
69
+ * Lognormal-correlated model: the yearly gross return multiplier is
70
+ * lognormal with mean 1 (so the shock is mean-preserving around each
71
+ * account's expected return); inflation is normal and correlated with the
72
+ * return shock via a Gaussian copula.
73
+ */
74
+ export function createLognormalModel(config) {
75
+ const sigma = (config.returnVolPct ?? 12) / 100;
76
+ const inflMean = config.inflationMeanPct;
77
+ const inflVol = config.inflationVolPct ?? 1.5;
78
+ const rho = Math.max(-1, Math.min(1, config.correlation ?? -0.2));
79
+ // Per-class correlated shocks (optional). z1 — the single market factor —
80
+ // doubles as the first Gaussian source, so class shocks co-move with the
81
+ // single-factor shock (and with inflation through it) and allocated vs
82
+ // unallocated accounts see the same market in the same year.
83
+ const classCfg = config.classShocks;
84
+ const chol = classCfg ? choleskyDecompose(classCfg.correlations ?? DEFAULT_CLASS_CORRELATIONS.map((r) => [...r])) : null;
85
+ const classSigmas = classCfg ? ASSET_CLASS_IDS.map((id) => Math.max(0, classCfg.volatilityPctByClass[id] ?? 0) / 100) : null;
86
+ return {
87
+ generatePath(rng, yearCount) {
88
+ const returnShockPct = new Array(yearCount);
89
+ const inflationPct = new Array(yearCount);
90
+ const classSeries = classCfg
91
+ ? Object.fromEntries(ASSET_CLASS_IDS.map((id) => [id, new Array(yearCount)]))
92
+ : null;
93
+ for (let i = 0; i < yearCount; i++) {
94
+ const z1 = rng.nextNormal();
95
+ const z2 = rng.nextNormal();
96
+ // E[exp(σz − σ²/2)] = 1: shocks average out to the expected return.
97
+ returnShockPct[i] = (Math.exp(sigma * z1 - (sigma * sigma) / 2) - 1) * 100;
98
+ inflationPct[i] = inflMean + inflVol * (rho * z1 + Math.sqrt(1 - rho * rho) * z2);
99
+ if (classSeries && chol && classSigmas) {
100
+ // Extra draws only in class mode, after z1/z2 — the single-factor
101
+ // and inflation paths above are bit-identical with classShocks off.
102
+ const g = [z1, rng.nextNormal(), rng.nextNormal(), rng.nextNormal()];
103
+ for (let c = 0; c < ASSET_CLASS_IDS.length; c++) {
104
+ let x = 0;
105
+ for (let k = 0; k <= c && k < g.length; k++)
106
+ x += chol[c][k] * g[k];
107
+ const s = classSigmas[c];
108
+ classSeries[ASSET_CLASS_IDS[c]][i] = (Math.exp(s * x - (s * s) / 2) - 1) * 100;
109
+ }
110
+ }
111
+ }
112
+ return classSeries
113
+ ? { returnShockPct, inflationPct, classReturnShockPct: classSeries }
114
+ : { returnShockPct, inflationPct };
115
+ },
116
+ };
117
+ }
118
+ /**
119
+ * Historical bootstrap over the embedded Shiller/Damodaran annual series.
120
+ * The sampled blended-portfolio return is centered on its historical mean,
121
+ * preserving the plan's expected return while replaying historical
122
+ * dispersion and the realized co-movement of returns and inflation.
123
+ */
124
+ export function createHistoricalModel(config) {
125
+ const equityWeightPct = config.equityWeightPct ?? 60;
126
+ const blockLength = Math.max(1, Math.round(config.blockLengthYears ?? 5));
127
+ const mean = meanPortfolioReturnPct(equityWeightPct);
128
+ const meanStocks = meanPortfolioReturnPct(100);
129
+ const meanBonds = meanPortfolioReturnPct(0);
130
+ const n = HISTORICAL_YEARS.length;
131
+ return {
132
+ generatePath(rng, yearCount) {
133
+ const returnShockPct = new Array(yearCount);
134
+ const inflationPct = new Array(yearCount);
135
+ const classSeries = config.classShocks
136
+ ? Object.fromEntries(ASSET_CLASS_IDS.map((id) => [id, new Array(yearCount)]))
137
+ : null;
138
+ let cursor = 0;
139
+ let leftInBlock = 0;
140
+ for (let i = 0; i < yearCount; i++) {
141
+ if (config.mode === 'iid') {
142
+ cursor = rng.nextInt(n);
143
+ }
144
+ else if (config.mode === 'sequence') {
145
+ if (i === 0)
146
+ cursor = rng.nextInt(n);
147
+ else
148
+ cursor = (cursor + 1) % n;
149
+ }
150
+ else {
151
+ if (leftInBlock === 0) {
152
+ cursor = rng.nextInt(n);
153
+ leftInBlock = blockLength;
154
+ }
155
+ else {
156
+ cursor = (cursor + 1) % n;
157
+ }
158
+ leftInBlock--;
159
+ }
160
+ const sample = HISTORICAL_YEARS[cursor];
161
+ returnShockPct[i] = portfolioReturnPct(sample, equityWeightPct) - mean;
162
+ inflationPct[i] = sample.inflationPct;
163
+ if (classSeries) {
164
+ // Keyed by class off the same sampled year: the dataset carries US
165
+ // stocks and Treasuries, so international equity replays the stock
166
+ // series (proxy, documented) and cash stays stable-value.
167
+ const stockShock = sample.stocksPct - meanStocks;
168
+ classSeries.usStocks[i] = stockShock;
169
+ classSeries.intlStocks[i] = stockShock;
170
+ classSeries.bonds[i] = sample.bondsPct - meanBonds;
171
+ classSeries.cash[i] = 0;
172
+ }
173
+ }
174
+ return classSeries
175
+ ? { returnShockPct, inflationPct, classReturnShockPct: classSeries }
176
+ : { returnShockPct, inflationPct };
177
+ },
178
+ };
179
+ }
180
+ /**
181
+ * Student-t fat-tailed returns (mean-preserving). Uses t-distributed shocks
182
+ * scaled to target volatility; lower df => fatter tails than Gaussian.
183
+ * Draws: z_t for return, then inflation copula, then class after.
184
+ */
185
+ export function createStudentTModel(config) {
186
+ const df = Math.max(3, config.df ?? 5);
187
+ const sigma = (config.returnVolPct ?? 12) / 100;
188
+ const inflMean = config.inflationMeanPct;
189
+ const inflVol = config.inflationVolPct ?? 1.5;
190
+ const rho = Math.max(-1, Math.min(1, config.correlation ?? -0.2));
191
+ const classCfg = config.classShocks;
192
+ const chol = classCfg ? choleskyDecompose(classCfg.correlations ?? DEFAULT_CLASS_CORRELATIONS.map((r) => [...r])) : null;
193
+ const classSigmas = classCfg ? ASSET_CLASS_IDS.map((id) => Math.max(0, classCfg.volatilityPctByClass[id] ?? 0) / 100) : null;
194
+ // Use normal scaled + occasional large deviation for fat tails; always E~0
195
+ return {
196
+ generatePath(rng, yearCount) {
197
+ const returnShockPct = new Array(yearCount);
198
+ const inflationPct = new Array(yearCount);
199
+ const classSeries = classCfg
200
+ ? Object.fromEntries(ASSET_CLASS_IDS.map((id) => [id, new Array(yearCount)]))
201
+ : null;
202
+ for (let i = 0; i < yearCount; i++) {
203
+ let z = rng.nextNormal();
204
+ // fat tail: with small prob use larger multiplier (mixture for tails)
205
+ if (rng.next() < 0.05)
206
+ z *= (df > 4 ? 2.5 : 3.5);
207
+ returnShockPct[i] = sigma * z * 100;
208
+ const z2 = rng.nextNormal();
209
+ inflationPct[i] = inflMean + inflVol * (rho * z + Math.sqrt(1 - rho * rho) * z2);
210
+ if (classSeries && chol && classSigmas) {
211
+ const g = [z, rng.nextNormal(), rng.nextNormal(), rng.nextNormal()];
212
+ for (let c = 0; c < ASSET_CLASS_IDS.length; c++) {
213
+ let x = 0;
214
+ for (let k = 0; k <= c && k < g.length; k++)
215
+ x += chol[c][k] * g[k];
216
+ const s = classSigmas[c];
217
+ classSeries[ASSET_CLASS_IDS[c]][i] = s * x * 100;
218
+ }
219
+ }
220
+ }
221
+ return classSeries
222
+ ? { returnShockPct, inflationPct, classReturnShockPct: classSeries }
223
+ : { returnShockPct, inflationPct };
224
+ },
225
+ };
226
+ }
227
+ /**
228
+ * Simple two-regime Markov switching (bull/bear) on mean and vol.
229
+ * State persists with 1-p switch. Regime means (bullMeanPct / bearMeanPct) are *deviations*
230
+ * from zero so the unconditional expected shock is near zero when bull/bear are symmetric.
231
+ * Inflation is always centered on the provided mean (no regime bias).
232
+ */
233
+ export function createRegimeSwitchModel(config) {
234
+ const bullMean = (config.bullMeanPct ?? 4) / 100;
235
+ const bearMean = (config.bearMeanPct ?? -4) / 100;
236
+ const bullVol = (config.bullVolPct ?? 10) / 100;
237
+ const bearVol = (config.bearVolPct ?? 20) / 100;
238
+ const pSwitch = Math.max(0.001, Math.min(0.5, config.switchProb ?? 0.05));
239
+ const inflMean = config.inflationMeanPct;
240
+ const inflVol = config.inflationVolPct ?? 1.5;
241
+ const classCfg = config.classShocks;
242
+ const chol = classCfg ? choleskyDecompose(classCfg.correlations ?? DEFAULT_CLASS_CORRELATIONS.map((r) => [...r])) : null;
243
+ const classSigmas = classCfg ? ASSET_CLASS_IDS.map((id) => Math.max(0, classCfg.volatilityPctByClass[id] ?? 0) / 100) : null;
244
+ return {
245
+ generatePath(rng, yearCount) {
246
+ const returnShockPct = new Array(yearCount);
247
+ const inflationPct = new Array(yearCount);
248
+ const classSeries = classCfg
249
+ ? Object.fromEntries(ASSET_CLASS_IDS.map((id) => [id, new Array(yearCount)]))
250
+ : null;
251
+ let bull = rng.next() > 0.5; // random start state
252
+ for (let i = 0; i < yearCount; i++) {
253
+ if (rng.next() < pSwitch)
254
+ bull = !bull;
255
+ const mu = bull ? bullMean : bearMean;
256
+ const sig = bull ? bullVol : bearVol;
257
+ const z = rng.nextNormal();
258
+ const shock = mu + sig * z;
259
+ returnShockPct[i] = shock * 100;
260
+ const z2 = rng.nextNormal();
261
+ // Always center inflation on provided mean; correlate via the return innovation z
262
+ inflationPct[i] = inflMean + inflVol * (0.3 * z + Math.sqrt(1 - 0.3 * 0.3) * z2);
263
+ if (classSeries && chol && classSigmas) {
264
+ const g = [z, rng.nextNormal(), rng.nextNormal(), rng.nextNormal()];
265
+ for (let c = 0; c < ASSET_CLASS_IDS.length; c++) {
266
+ let x = 0;
267
+ for (let k = 0; k <= c && k < g.length; k++)
268
+ x += chol[c][k] * g[k];
269
+ const s = classSigmas[c];
270
+ classSeries[ASSET_CLASS_IDS[c]][i] = (mu + s * x) * 100;
271
+ }
272
+ }
273
+ }
274
+ return classSeries
275
+ ? { returnShockPct, inflationPct, classReturnShockPct: classSeries }
276
+ : { returnShockPct, inflationPct };
277
+ },
278
+ };
279
+ }
280
+ /**
281
+ * CAPE-conditioned: starting high CAPE lowers the mean return (linear taper).
282
+ * Uses lognormal base but shifts mu down.
283
+ */
284
+ export function createCapeConditionedModel(config) {
285
+ const startCape = config.startingCape ?? 25;
286
+ const sens = config.capeSensitivity ?? 0.15;
287
+ const baseMuAdj = Math.max(-4, Math.min(2, -(startCape - 20) * sens)); // pp adjustment
288
+ const sigma = (config.returnVolPct ?? 12) / 100;
289
+ const inflMean = config.inflationMeanPct;
290
+ const inflVol = config.inflationVolPct ?? 1.5;
291
+ const rho = Math.max(-1, Math.min(1, config.correlation ?? -0.2));
292
+ const classCfg = config.classShocks;
293
+ const chol = classCfg ? choleskyDecompose(classCfg.correlations ?? DEFAULT_CLASS_CORRELATIONS.map((r) => [...r])) : null;
294
+ const classSigmas = classCfg ? ASSET_CLASS_IDS.map((id) => Math.max(0, classCfg.volatilityPctByClass[id] ?? 0) / 100) : null;
295
+ return {
296
+ generatePath(rng, yearCount) {
297
+ const returnShockPct = new Array(yearCount);
298
+ const inflationPct = new Array(yearCount);
299
+ const classSeries = classCfg
300
+ ? Object.fromEntries(ASSET_CLASS_IDS.map((id) => [id, new Array(yearCount)]))
301
+ : null;
302
+ for (let i = 0; i < yearCount; i++) {
303
+ const z1 = rng.nextNormal();
304
+ const z2 = rng.nextNormal();
305
+ // mean adj applied additively after lognormal centering for preservation
306
+ const adj = baseMuAdj;
307
+ returnShockPct[i] = (Math.exp(sigma * z1 - (sigma * sigma) / 2) - 1) * 100 + adj;
308
+ inflationPct[i] = inflMean + inflVol * (rho * z1 + Math.sqrt(1 - rho * rho) * z2);
309
+ if (classSeries && chol && classSigmas) {
310
+ const g = [z1, rng.nextNormal(), rng.nextNormal(), rng.nextNormal()];
311
+ for (let c = 0; c < ASSET_CLASS_IDS.length; c++) {
312
+ let x = 0;
313
+ for (let k = 0; k <= c && k < g.length; k++)
314
+ x += chol[c][k] * g[k];
315
+ const s = classSigmas[c];
316
+ classSeries[ASSET_CLASS_IDS[c]][i] = (Math.exp(s * x - (s * s) / 2) - 1) * 100 + adj;
317
+ }
318
+ }
319
+ }
320
+ return classSeries
321
+ ? { returnShockPct, inflationPct, classReturnShockPct: classSeries }
322
+ : { returnShockPct, inflationPct };
323
+ },
324
+ };
325
+ }
326
+ /** Stationary (Politis-Romano) bootstrap: geometric block lengths. */
327
+ export function createStationaryBootstrapModel(config) {
328
+ const equityWeightPct = config.equityWeightPct ?? 60;
329
+ const meanBlock = Math.max(2, config.meanBlockLength ?? 5);
330
+ const mean = meanPortfolioReturnPct(equityWeightPct);
331
+ const meanS = meanPortfolioReturnPct(100);
332
+ const meanB = meanPortfolioReturnPct(0);
333
+ const n = HISTORICAL_YEARS.length;
334
+ return {
335
+ generatePath(rng, yearCount) {
336
+ const returnShockPct = new Array(yearCount);
337
+ const inflationPct = new Array(yearCount);
338
+ const classSeries = config.classShocks
339
+ ? Object.fromEntries(ASSET_CLASS_IDS.map((id) => [id, new Array(yearCount)]))
340
+ : null;
341
+ let cursor = rng.nextInt(n);
342
+ let remaining = Math.floor(-Math.log(1 - rng.next()) * meanBlock) || 1;
343
+ for (let i = 0; i < yearCount; i++) {
344
+ if (remaining <= 0) {
345
+ cursor = rng.nextInt(n);
346
+ remaining = Math.floor(-Math.log(1 - rng.next()) * meanBlock) || 1;
347
+ }
348
+ const sample = HISTORICAL_YEARS[cursor];
349
+ returnShockPct[i] = portfolioReturnPct(sample, equityWeightPct) - mean;
350
+ inflationPct[i] = sample.inflationPct;
351
+ if (classSeries) {
352
+ classSeries.usStocks[i] = sample.stocksPct - meanS;
353
+ classSeries.intlStocks[i] = sample.stocksPct - meanS;
354
+ classSeries.bonds[i] = sample.bondsPct - meanB;
355
+ classSeries.cash[i] = 0;
356
+ }
357
+ cursor = (cursor + 1) % n;
358
+ remaining--;
359
+ }
360
+ return classSeries
361
+ ? { returnShockPct, inflationPct, classReturnShockPct: classSeries }
362
+ : { returnShockPct, inflationPct };
363
+ },
364
+ };
365
+ }
366
+ /** Empirical historical: optionally non-centered (raw history mean).
367
+ * Note: non-centered mode outputs raw historical portfolio returns as the "shock".
368
+ * Because the projection always does expected + shock, non-centered will add the
369
+ * historical mean on top of the plan's assumption (potential double-count).
370
+ * UI defaults to centered for correctness; non-centered kept for advanced use.
371
+ */
372
+ export function createEmpiricalModel(config) {
373
+ const equityWeightPct = config.equityWeightPct ?? 60;
374
+ const centered = config.centered !== false; // default true (mean preserving)
375
+ const mean = centered ? meanPortfolioReturnPct(equityWeightPct) : 0;
376
+ const meanS = meanPortfolioReturnPct(100);
377
+ const meanB = meanPortfolioReturnPct(0);
378
+ const n = HISTORICAL_YEARS.length;
379
+ return {
380
+ generatePath(rng, yearCount) {
381
+ const returnShockPct = new Array(yearCount);
382
+ const inflationPct = new Array(yearCount);
383
+ const classSeries = config.classShocks
384
+ ? Object.fromEntries(ASSET_CLASS_IDS.map((id) => [id, new Array(yearCount)]))
385
+ : null;
386
+ let cursor = rng.nextInt(n);
387
+ for (let i = 0; i < yearCount; i++) {
388
+ if (i > 0)
389
+ cursor = (cursor + 1) % n; // simple sequence-like for demo; or iid, here use iid for variety
390
+ if (rng.next() < 0.5)
391
+ cursor = rng.nextInt(n); // mix iid/seq
392
+ const sample = HISTORICAL_YEARS[cursor];
393
+ returnShockPct[i] = portfolioReturnPct(sample, equityWeightPct) - mean;
394
+ inflationPct[i] = sample.inflationPct;
395
+ if (classSeries) {
396
+ classSeries.usStocks[i] = sample.stocksPct - (centered ? meanS : 0);
397
+ classSeries.intlStocks[i] = sample.stocksPct - (centered ? meanS : 0);
398
+ classSeries.bonds[i] = sample.bondsPct - (centered ? meanB : 0);
399
+ classSeries.cash[i] = 0;
400
+ }
401
+ }
402
+ return classSeries
403
+ ? { returnShockPct, inflationPct, classReturnShockPct: classSeries }
404
+ : { returnShockPct, inflationPct };
405
+ },
406
+ };
407
+ }
408
+ /** GARCH(1,1) approx for vol clustering. Simple recursion on sigma_t. */
409
+ export function createGarchModel(config) {
410
+ const omega = config.omega ?? 0.00001;
411
+ const alpha = config.alpha ?? 0.1;
412
+ const beta = config.beta ?? 0.85;
413
+ const scale = (config.returnVolScalePct ?? 12) / 100;
414
+ const inflMean = config.inflationMeanPct;
415
+ const inflVol = config.inflationVolPct ?? 1.5;
416
+ const rho = Math.max(-1, Math.min(1, config.correlation ?? -0.2));
417
+ const classCfg = config.classShocks;
418
+ const chol = classCfg ? choleskyDecompose(classCfg.correlations ?? DEFAULT_CLASS_CORRELATIONS.map((r) => [...r])) : null;
419
+ const classSigmas = classCfg ? ASSET_CLASS_IDS.map((id) => Math.max(0, classCfg.volatilityPctByClass[id] ?? 0) / 100) : null;
420
+ return {
421
+ generatePath(rng, yearCount) {
422
+ const returnShockPct = new Array(yearCount);
423
+ const inflationPct = new Array(yearCount);
424
+ const classSeries = classCfg
425
+ ? Object.fromEntries(ASSET_CLASS_IDS.map((id) => [id, new Array(yearCount)]))
426
+ : null;
427
+ let sigma2 = 0.0001;
428
+ let lastEps = 0;
429
+ for (let i = 0; i < yearCount; i++) {
430
+ sigma2 = omega + alpha * lastEps * lastEps + beta * sigma2;
431
+ const sig = Math.sqrt(Math.max(1e-9, sigma2)) * scale * 5; // tune
432
+ const z1 = rng.nextNormal();
433
+ const shock = sig * z1;
434
+ returnShockPct[i] = shock * 100;
435
+ const z2 = rng.nextNormal();
436
+ inflationPct[i] = inflMean + inflVol * (rho * z1 + Math.sqrt(1 - rho * rho) * z2);
437
+ lastEps = shock;
438
+ if (classSeries && chol && classSigmas) {
439
+ const g = [z1, rng.nextNormal(), rng.nextNormal(), rng.nextNormal()];
440
+ for (let c = 0; c < ASSET_CLASS_IDS.length; c++) {
441
+ let x = 0;
442
+ for (let k = 0; k <= c && k < g.length; k++)
443
+ x += chol[c][k] * g[k];
444
+ const s = classSigmas[c];
445
+ classSeries[ASSET_CLASS_IDS[c]][i] = s * x * 100;
446
+ }
447
+ }
448
+ }
449
+ return classSeries
450
+ ? { returnShockPct, inflationPct, classReturnShockPct: classSeries }
451
+ : { returnShockPct, inflationPct };
452
+ },
453
+ };
454
+ }
455
+ /** Fat-tailed / regime inflation correlated to returns. */
456
+ export function createInflationRegimeModel(config) {
457
+ const highMu = config.highInflationMean ?? 8;
458
+ const pHigh = Math.max(0.01, Math.min(0.3, config.highInflationProb ?? 0.08));
459
+ const sigma = (config.returnVolPct ?? 12) / 100;
460
+ const baseInfl = config.baseInflationMeanPct;
461
+ const rho = Math.max(-1, Math.min(1, config.correlation ?? -0.2));
462
+ const classCfg = config.classShocks;
463
+ const chol = classCfg ? choleskyDecompose(classCfg.correlations ?? DEFAULT_CLASS_CORRELATIONS.map((r) => [...r])) : null;
464
+ const classSigmas = classCfg ? ASSET_CLASS_IDS.map((id) => Math.max(0, classCfg.volatilityPctByClass[id] ?? 0) / 100) : null;
465
+ return {
466
+ generatePath(rng, yearCount) {
467
+ const returnShockPct = new Array(yearCount);
468
+ const inflationPct = new Array(yearCount);
469
+ const classSeries = classCfg
470
+ ? Object.fromEntries(ASSET_CLASS_IDS.map((id) => [id, new Array(yearCount)]))
471
+ : null;
472
+ let high = false;
473
+ for (let i = 0; i < yearCount; i++) {
474
+ if (rng.next() < (high ? 0.7 : pHigh))
475
+ high = !high;
476
+ const z1 = rng.nextNormal();
477
+ returnShockPct[i] = (Math.exp(sigma * z1 - (sigma * sigma) / 2) - 1) * 100;
478
+ const baseI = high ? highMu : baseInfl;
479
+ const z2 = rng.nextNormal();
480
+ inflationPct[i] = baseI + 1.5 * (rho * z1 + Math.sqrt(1 - rho * rho) * z2);
481
+ if (classSeries && chol && classSigmas) {
482
+ const g = [z1, rng.nextNormal(), rng.nextNormal(), rng.nextNormal()];
483
+ for (let c = 0; c < ASSET_CLASS_IDS.length; c++) {
484
+ let x = 0;
485
+ for (let k = 0; k <= c && k < g.length; k++)
486
+ x += chol[c][k] * g[k];
487
+ const s = classSigmas[c];
488
+ classSeries[ASSET_CLASS_IDS[c]][i] = (Math.exp(s * x - (s * s) / 2) - 1) * 100;
489
+ }
490
+ }
491
+ }
492
+ return classSeries
493
+ ? { returnShockPct, inflationPct, classReturnShockPct: classSeries }
494
+ : { returnShockPct, inflationPct };
495
+ },
496
+ };
497
+ }
498
+ /** Reversed history blocks chosen stochastically (formalized from suites). */
499
+ export function createReversedHistoryModel(config) {
500
+ const equityWeightPct = config.equityWeightPct ?? 60;
501
+ const winLen = Math.max(5, Math.min(HISTORICAL_YEARS.length, config.windowLengthYears ?? 10));
502
+ const mean = meanPortfolioReturnPct(equityWeightPct);
503
+ const meanS = meanPortfolioReturnPct(100);
504
+ const meanB = meanPortfolioReturnPct(0);
505
+ const n = HISTORICAL_YEARS.length;
506
+ const maxStart = n - winLen;
507
+ return {
508
+ generatePath(rng, yearCount) {
509
+ const returnShockPct = new Array(yearCount);
510
+ const inflationPct = new Array(yearCount);
511
+ const classSeries = config.classShocks
512
+ ? Object.fromEntries(ASSET_CLASS_IDS.map((id) => [id, new Array(yearCount)]))
513
+ : null;
514
+ const start = rng.nextInt(Math.max(1, maxStart + 1));
515
+ for (let i = 0; i < yearCount; i++) {
516
+ const off = i % winLen;
517
+ const idx = start + winLen - 1 - off;
518
+ const hidx = ((idx % n) + n) % n;
519
+ const sample = HISTORICAL_YEARS[hidx];
520
+ returnShockPct[i] = portfolioReturnPct(sample, equityWeightPct) - mean;
521
+ inflationPct[i] = sample.inflationPct;
522
+ if (classSeries) {
523
+ classSeries.usStocks[i] = sample.stocksPct - meanS;
524
+ classSeries.intlStocks[i] = sample.stocksPct - meanS;
525
+ classSeries.bonds[i] = sample.bondsPct - meanB;
526
+ classSeries.cash[i] = 0;
527
+ }
528
+ }
529
+ return classSeries
530
+ ? { returnShockPct, inflationPct, classReturnShockPct: classSeries }
531
+ : { returnShockPct, inflationPct };
532
+ },
533
+ };
534
+ }
535
+ /** User-specified deterministic shock in one year, then lognormal-ish. */
536
+ export function createUserShockModel(config) {
537
+ const shockYear = Math.max(1, config.shockYear ?? 1); // 1-based
538
+ const shock = config.shockPct ?? -20;
539
+ const sigma = (config.baseReturnVolPct ?? 12) / 100;
540
+ const inflMean = config.inflationMeanPct;
541
+ const classCfg = config.classShocks;
542
+ const chol = classCfg ? choleskyDecompose(classCfg.correlations ?? DEFAULT_CLASS_CORRELATIONS.map((r) => [...r])) : null;
543
+ const classSigmas = classCfg ? ASSET_CLASS_IDS.map((id) => Math.max(0, classCfg.volatilityPctByClass[id] ?? 0) / 100) : null;
544
+ return {
545
+ generatePath(rng, yearCount) {
546
+ const returnShockPct = new Array(yearCount);
547
+ const inflationPct = new Array(yearCount);
548
+ const classSeries = classCfg
549
+ ? Object.fromEntries(ASSET_CLASS_IDS.map((id) => [id, new Array(yearCount)]))
550
+ : null;
551
+ for (let i = 0; i < yearCount; i++) {
552
+ const yearIdx = i + 1;
553
+ const z1 = rng.nextNormal();
554
+ const z2 = rng.nextNormal();
555
+ if (yearIdx === shockYear) {
556
+ returnShockPct[i] = shock;
557
+ inflationPct[i] = inflMean + 3 * z2; // noisy around
558
+ }
559
+ else {
560
+ returnShockPct[i] = (Math.exp(sigma * z1 - (sigma * sigma) / 2) - 1) * 100;
561
+ inflationPct[i] = inflMean + 1.5 * (-0.2 * z1 + Math.sqrt(0.96) * z2);
562
+ }
563
+ if (classSeries && chol && classSigmas) {
564
+ const g = [z1, rng.nextNormal(), rng.nextNormal(), rng.nextNormal()];
565
+ for (let c = 0; c < ASSET_CLASS_IDS.length; c++) {
566
+ let x = 0;
567
+ for (let k = 0; k <= c && k < g.length; k++)
568
+ x += chol[c][k] * g[k];
569
+ const s = classSigmas[c];
570
+ const id = ASSET_CLASS_IDS[c];
571
+ if (yearIdx === shockYear) {
572
+ classSeries[id][i] = (id === 'cash') ? 0 : shock * (id === 'usStocks' || id === 'intlStocks' ? 1 : 0.6);
573
+ }
574
+ else {
575
+ classSeries[id][i] = (Math.exp(s * x - (s * s) / 2) - 1) * 100;
576
+ }
577
+ }
578
+ }
579
+ }
580
+ return classSeries
581
+ ? { returnShockPct, inflationPct, classReturnShockPct: classSeries }
582
+ : { returnShockPct, inflationPct };
583
+ },
584
+ };
585
+ }
586
+ /**
587
+ * Additive multivariate Gaussian (normal) shocks. Distinct from lognormal:
588
+ * returns can be <-100% in theory (rare), symmetric, no built-in compounding skew.
589
+ * Mean-preserving (shocks centered at 0).
590
+ */
591
+ export function createGaussianModel(config) {
592
+ const sigma = (config.returnVolPct ?? 12) / 100;
593
+ const inflMean = config.inflationMeanPct;
594
+ const inflVol = config.inflationVolPct ?? 1.5;
595
+ const rho = Math.max(-1, Math.min(1, config.correlation ?? -0.2));
596
+ const classCfg = config.classShocks;
597
+ const chol = classCfg ? choleskyDecompose(classCfg.correlations ?? DEFAULT_CLASS_CORRELATIONS.map((r) => [...r])) : null;
598
+ const classSigmas = classCfg ? ASSET_CLASS_IDS.map((id) => Math.max(0, classCfg.volatilityPctByClass[id] ?? 0) / 100) : null;
599
+ return {
600
+ generatePath(rng, yearCount) {
601
+ const returnShockPct = new Array(yearCount);
602
+ const inflationPct = new Array(yearCount);
603
+ const classSeries = classCfg
604
+ ? Object.fromEntries(ASSET_CLASS_IDS.map((id) => [id, new Array(yearCount)]))
605
+ : null;
606
+ for (let i = 0; i < yearCount; i++) {
607
+ const z1 = rng.nextNormal();
608
+ const z2 = rng.nextNormal();
609
+ returnShockPct[i] = sigma * z1 * 100; // additive normal, centered
610
+ inflationPct[i] = inflMean + inflVol * (rho * z1 + Math.sqrt(1 - rho * rho) * z2);
611
+ if (classSeries && chol && classSigmas) {
612
+ const g = [z1, rng.nextNormal(), rng.nextNormal(), rng.nextNormal()];
613
+ for (let c = 0; c < ASSET_CLASS_IDS.length; c++) {
614
+ let x = 0;
615
+ for (let k = 0; k <= c && k < g.length; k++)
616
+ x += chol[c][k] * g[k];
617
+ const s = classSigmas[c];
618
+ classSeries[ASSET_CLASS_IDS[c]][i] = s * x * 100;
619
+ }
620
+ }
621
+ }
622
+ return classSeries
623
+ ? { returnShockPct, inflationPct, classReturnShockPct: classSeries }
624
+ : { returnShockPct, inflationPct };
625
+ },
626
+ };
627
+ }
628
+ /**
629
+ * AR(1) mean-reverting shocks. Introduces serial correlation (momentum or reversion)
630
+ * controlled by phi. Distinct dynamics from iid models. Shocks remain mean-zero.
631
+ */
632
+ export function createAR1Model(config) {
633
+ const phi = Math.max(-0.9, Math.min(0.95, config.phi ?? 0.25));
634
+ const sigma = (config.returnVolPct ?? 12) / 100;
635
+ const inflMean = config.inflationMeanPct;
636
+ const inflVol = config.inflationVolPct ?? 1.5;
637
+ const rho = Math.max(-1, Math.min(1, config.correlation ?? -0.2));
638
+ const classCfg = config.classShocks;
639
+ const chol = classCfg ? choleskyDecompose(classCfg.correlations ?? DEFAULT_CLASS_CORRELATIONS.map((r) => [...r])) : null;
640
+ const classSigmas = classCfg ? ASSET_CLASS_IDS.map((id) => Math.max(0, classCfg.volatilityPctByClass[id] ?? 0) / 100) : null;
641
+ return {
642
+ generatePath(rng, yearCount) {
643
+ const returnShockPct = new Array(yearCount);
644
+ const inflationPct = new Array(yearCount);
645
+ const classSeries = classCfg
646
+ ? Object.fromEntries(ASSET_CLASS_IDS.map((id) => [id, new Array(yearCount)]))
647
+ : null;
648
+ let prevShock = 0;
649
+ for (let i = 0; i < yearCount; i++) {
650
+ const eps = rng.nextNormal();
651
+ const shock = phi * prevShock + sigma * eps;
652
+ returnShockPct[i] = shock * 100;
653
+ const z2 = rng.nextNormal();
654
+ inflationPct[i] = inflMean + inflVol * (rho * eps + Math.sqrt(1 - rho * rho) * z2); // innovation driven
655
+ prevShock = shock;
656
+ if (classSeries && chol && classSigmas) {
657
+ const g = [eps, rng.nextNormal(), rng.nextNormal(), rng.nextNormal()];
658
+ for (let c = 0; c < ASSET_CLASS_IDS.length; c++) {
659
+ let x = 0;
660
+ for (let k = 0; k <= c && k < g.length; k++)
661
+ x += chol[c][k] * g[k];
662
+ const s = classSigmas[c];
663
+ classSeries[ASSET_CLASS_IDS[c]][i] = s * x * 100;
664
+ }
665
+ }
666
+ }
667
+ return classSeries
668
+ ? { returnShockPct, inflationPct, classReturnShockPct: classSeries }
669
+ : { returnShockPct, inflationPct };
670
+ },
671
+ };
672
+ }