@calculator53295/backtest-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 +21 -0
- package/README.md +161 -0
- package/dist/canonical/allocation.d.ts +64 -0
- package/dist/canonical/allocation.d.ts.map +1 -0
- package/dist/canonical/allocation.js +91 -0
- package/dist/canonical/allocation.js.map +1 -0
- package/dist/canonical/dates.d.ts +45 -0
- package/dist/canonical/dates.d.ts.map +1 -0
- package/dist/canonical/dates.js +86 -0
- package/dist/canonical/dates.js.map +1 -0
- package/dist/canonical/index.d.ts +12 -0
- package/dist/canonical/index.d.ts.map +1 -0
- package/dist/canonical/index.js +12 -0
- package/dist/canonical/index.js.map +1 -0
- package/dist/canonical/rebalance.d.ts +35 -0
- package/dist/canonical/rebalance.d.ts.map +1 -0
- package/dist/canonical/rebalance.js +56 -0
- package/dist/canonical/rebalance.js.map +1 -0
- package/dist/canonical/returns.d.ts +61 -0
- package/dist/canonical/returns.d.ts.map +1 -0
- package/dist/canonical/returns.js +65 -0
- package/dist/canonical/returns.js.map +1 -0
- package/dist/canonical/strategy.d.ts +62 -0
- package/dist/canonical/strategy.d.ts.map +1 -0
- package/dist/canonical/strategy.js +103 -0
- package/dist/canonical/strategy.js.map +1 -0
- package/dist/engine/align.d.ts +34 -0
- package/dist/engine/align.d.ts.map +1 -0
- package/dist/engine/align.js +97 -0
- package/dist/engine/align.js.map +1 -0
- package/dist/engine/backtest.d.ts +7 -0
- package/dist/engine/backtest.d.ts.map +1 -0
- package/dist/engine/backtest.js +95 -0
- package/dist/engine/backtest.js.map +1 -0
- package/dist/engine/frontier.d.ts +25 -0
- package/dist/engine/frontier.d.ts.map +1 -0
- package/dist/engine/frontier.js +40 -0
- package/dist/engine/frontier.js.map +1 -0
- package/dist/engine/index.d.ts +6 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +5 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/metrics.d.ts +27 -0
- package/dist/engine/metrics.d.ts.map +1 -0
- package/dist/engine/metrics.js +202 -0
- package/dist/engine/metrics.js.map +1 -0
- package/dist/engine/types.d.ts +118 -0
- package/dist/engine/types.d.ts.map +1 -0
- package/dist/engine/types.js +2 -0
- package/dist/engine/types.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/montecarlo/data.d.ts +17 -0
- package/dist/montecarlo/data.d.ts.map +1 -0
- package/dist/montecarlo/data.js +38 -0
- package/dist/montecarlo/data.js.map +1 -0
- package/dist/montecarlo/simulate.d.ts +106 -0
- package/dist/montecarlo/simulate.d.ts.map +1 -0
- package/dist/montecarlo/simulate.js +264 -0
- package/dist/montecarlo/simulate.js.map +1 -0
- package/dist/retirement/bucketRules.d.ts +3 -0
- package/dist/retirement/bucketRules.d.ts.map +1 -0
- package/dist/retirement/bucketRules.js +37 -0
- package/dist/retirement/bucketRules.js.map +1 -0
- package/dist/retirement/engine.d.ts +7 -0
- package/dist/retirement/engine.d.ts.map +1 -0
- package/dist/retirement/engine.js +297 -0
- package/dist/retirement/engine.js.map +1 -0
- package/dist/retirement/index.d.ts +6 -0
- package/dist/retirement/index.d.ts.map +1 -0
- package/dist/retirement/index.js +6 -0
- package/dist/retirement/index.js.map +1 -0
- package/dist/retirement/runner.d.ts +15 -0
- package/dist/retirement/runner.d.ts.map +1 -0
- package/dist/retirement/runner.js +178 -0
- package/dist/retirement/runner.js.map +1 -0
- package/dist/retirement/strategies/index.d.ts +30 -0
- package/dist/retirement/strategies/index.d.ts.map +1 -0
- package/dist/retirement/strategies/index.js +270 -0
- package/dist/retirement/strategies/index.js.map +1 -0
- package/dist/retirement/types.d.ts +206 -0
- package/dist/retirement/types.d.ts.map +1 -0
- package/dist/retirement/types.js +2 -0
- package/dist/retirement/types.js.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monte Carlo retirement / withdrawal simulator. Pure TS, no deps.
|
|
3
|
+
*
|
|
4
|
+
* Everything runs in REAL (inflation-adjusted) terms — the only honest way to
|
|
5
|
+
* ask "will my purchasing power last." Nominal returns from the asset-class
|
|
6
|
+
* data are deflated by CPI, and a fixed-real withdrawal is therefore constant
|
|
7
|
+
* in the series' units. Balances are reported in today's dollars.
|
|
8
|
+
*
|
|
9
|
+
* Convention (matches the Trinity study / FiCalc): withdraw once at the START
|
|
10
|
+
* of each year, then compound 12 months of returns. A trial "fails" if the
|
|
11
|
+
* balance is exhausted before the horizon ends — except VPW's final-year
|
|
12
|
+
* spend-to-zero, which is the plan working, not a failure.
|
|
13
|
+
*/
|
|
14
|
+
export type WithdrawalStrategy = 'fixedReal' | 'fixedPercent' | 'vpw' | 'guardrails';
|
|
15
|
+
export interface SimParams {
|
|
16
|
+
initialBalance: number;
|
|
17
|
+
/**
|
|
18
|
+
* Annual withdrawal rate (fraction). fixedReal/guardrails: × the balance at
|
|
19
|
+
* RETIREMENT (start of the withdrawal phase); fixedPercent: × current
|
|
20
|
+
* balance each year.
|
|
21
|
+
*/
|
|
22
|
+
withdrawalRate: number;
|
|
23
|
+
strategy: WithdrawalStrategy;
|
|
24
|
+
/** Years of withdrawals (the retirement horizon). */
|
|
25
|
+
horizonYears: number;
|
|
26
|
+
/** Annual expense ratio / advisory fee (fraction), applied monthly. */
|
|
27
|
+
feeRate: number;
|
|
28
|
+
/** VPW assumed real return (fraction); ignored by other strategies. */
|
|
29
|
+
vpwReturn?: number;
|
|
30
|
+
/** Years of saving BEFORE withdrawals begin (0 = retire immediately). */
|
|
31
|
+
accumulationYears?: number;
|
|
32
|
+
/** Real dollars contributed per year during accumulation (monthly 1/12). */
|
|
33
|
+
annualContribution?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface RealReturnSeries {
|
|
36
|
+
/** yyyy-mm labels aligned to `returns` (the month each return occurs in). */
|
|
37
|
+
dates: string[];
|
|
38
|
+
/** Real monthly total returns (fractions). */
|
|
39
|
+
returns: number[];
|
|
40
|
+
}
|
|
41
|
+
export interface SimResult {
|
|
42
|
+
mode: 'historical' | 'bootstrap';
|
|
43
|
+
trials: number;
|
|
44
|
+
successRate: number;
|
|
45
|
+
horizonYears: number;
|
|
46
|
+
accumulationYears: number;
|
|
47
|
+
/** Per-year (0..acc+horizon) balance percentiles across trials, today's dollars. */
|
|
48
|
+
percentiles: {
|
|
49
|
+
p5: number[];
|
|
50
|
+
p25: number[];
|
|
51
|
+
p50: number[];
|
|
52
|
+
p75: number[];
|
|
53
|
+
p95: number[];
|
|
54
|
+
};
|
|
55
|
+
/** Sorted ending balances for the distribution view. */
|
|
56
|
+
endingBalances: number[];
|
|
57
|
+
medianEnding: number;
|
|
58
|
+
/** Historical mode only: the worst starting years by outcome. */
|
|
59
|
+
worstStarts: Array<{
|
|
60
|
+
label: string;
|
|
61
|
+
endingBalance: number;
|
|
62
|
+
depletedYear: number | null;
|
|
63
|
+
}>;
|
|
64
|
+
/**
|
|
65
|
+
* Income variability across trials (real dollars): each trial's worst
|
|
66
|
+
* withdrawal year, summarized. For fixedReal this equals the fixed amount
|
|
67
|
+
* in successful trials; for variable strategies it shows the pay cut risk.
|
|
68
|
+
*/
|
|
69
|
+
income: {
|
|
70
|
+
firstYearMedian: number;
|
|
71
|
+
worstYearMedian: number;
|
|
72
|
+
worstYearP5: number;
|
|
73
|
+
/** Fraction of trials where income ever fell below the first year's. */
|
|
74
|
+
cutProbability: number;
|
|
75
|
+
/** Median count of retirement years spent below the starting income. */
|
|
76
|
+
yearsBelowStartMedian: number;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Per-retirement-year income percentiles across trials (real dollars,
|
|
80
|
+
* actual amounts withdrawn — a depleted trial contributes $0). Arrays are
|
|
81
|
+
* indexed by retirement year 1..horizonYears.
|
|
82
|
+
*/
|
|
83
|
+
incomeByYear: {
|
|
84
|
+
p5: number[];
|
|
85
|
+
p25: number[];
|
|
86
|
+
p50: number[];
|
|
87
|
+
p75: number[];
|
|
88
|
+
p95: number[];
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export declare function mulberry32(seed: number): () => number;
|
|
92
|
+
/** Total months a trial spans (accumulation + withdrawal phases). */
|
|
93
|
+
export declare function trialMonths(params: SimParams): number;
|
|
94
|
+
export declare function runHistoricalSequence(series: RealReturnSeries, params: SimParams): SimResult;
|
|
95
|
+
/**
|
|
96
|
+
* Resample the return series in contiguous blocks (default 24 months) to
|
|
97
|
+
* preserve short-run autocorrelation, building `trials` synthetic sequences.
|
|
98
|
+
*/
|
|
99
|
+
export declare function runBootstrap(series: RealReturnSeries, params: SimParams, opts?: {
|
|
100
|
+
trials: number;
|
|
101
|
+
blockMonths?: number;
|
|
102
|
+
rng?: () => number;
|
|
103
|
+
}): SimResult;
|
|
104
|
+
/** Solve the max withdrawal rate meeting a target success rate (historical). */
|
|
105
|
+
export declare function maxSafeWithdrawal(series: RealReturnSeries, params: Omit<SimParams, 'withdrawalRate'>, targetSuccess?: number): number;
|
|
106
|
+
//# sourceMappingURL=simulate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"simulate.d.ts","sourceRoot":"","sources":["../../src/montecarlo/simulate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monte Carlo retirement / withdrawal simulator. Pure TS, no deps.
|
|
3
|
+
*
|
|
4
|
+
* Everything runs in REAL (inflation-adjusted) terms — the only honest way to
|
|
5
|
+
* ask "will my purchasing power last." Nominal returns from the asset-class
|
|
6
|
+
* data are deflated by CPI, and a fixed-real withdrawal is therefore constant
|
|
7
|
+
* in the series' units. Balances are reported in today's dollars.
|
|
8
|
+
*
|
|
9
|
+
* Convention (matches the Trinity study / FiCalc): withdraw once at the START
|
|
10
|
+
* of each year, then compound 12 months of returns. A trial "fails" if the
|
|
11
|
+
* balance is exhausted before the horizon ends — except VPW's final-year
|
|
12
|
+
* spend-to-zero, which is the plan working, not a failure.
|
|
13
|
+
*/
|
|
14
|
+
// ---- seedable RNG (deterministic tests; Math.random in the worker) ----------
|
|
15
|
+
export function mulberry32(seed) {
|
|
16
|
+
let a = seed >>> 0;
|
|
17
|
+
return () => {
|
|
18
|
+
a |= 0;
|
|
19
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
20
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
21
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
22
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Total months a trial spans (accumulation + withdrawal phases). */
|
|
26
|
+
export function trialMonths(params) {
|
|
27
|
+
return ((params.accumulationYears ?? 0) + params.horizonYears) * 12;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Run one trial. Phases:
|
|
31
|
+
* 1) Accumulation (optional): contribute annualContribution/12 at each
|
|
32
|
+
* month start, compound.
|
|
33
|
+
* 2) Withdrawals: withdraw 1/12 of the year's amount at each month start
|
|
34
|
+
* (FiCalc convention — kinder than a full year up front), compound.
|
|
35
|
+
* Strategy semantics: fixedReal/guardrails anchor to the balance at
|
|
36
|
+
* retirement; guardrails then cuts the real withdrawal 10% when its current
|
|
37
|
+
* rate drifts 20% above the initial rate, and raises it 10% when 20% below
|
|
38
|
+
* (simplified Guyton-Klinger, decision applied at each year start).
|
|
39
|
+
* Everything is in real terms; a fee drags returns monthly.
|
|
40
|
+
*/
|
|
41
|
+
function runTrial(params, monthly, offset) {
|
|
42
|
+
const { initialBalance, horizonYears, feeRate } = params;
|
|
43
|
+
const accYears = params.accumulationYears ?? 0;
|
|
44
|
+
const contribMonthly = (params.annualContribution ?? 0) / 12;
|
|
45
|
+
const monthlyFeeFactor = (1 - feeRate) ** (1 / 12);
|
|
46
|
+
const totalYears = accYears + horizonYears;
|
|
47
|
+
let balance = initialBalance;
|
|
48
|
+
const path = new Array(totalYears + 1);
|
|
49
|
+
path[0] = balance;
|
|
50
|
+
const withdrawals = new Array(horizonYears).fill(0);
|
|
51
|
+
let depletedYear = null;
|
|
52
|
+
let m = offset;
|
|
53
|
+
// Guardrails / fixed-real state, set at retirement.
|
|
54
|
+
let anchorW = 0;
|
|
55
|
+
let initialRate = 0;
|
|
56
|
+
let firstYearW = 0;
|
|
57
|
+
let worstYearW = Infinity;
|
|
58
|
+
for (let y = 0; y < totalYears; y++) {
|
|
59
|
+
if (depletedYear !== null) {
|
|
60
|
+
path[y + 1] = 0;
|
|
61
|
+
m += 12;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const inAccumulation = y < accYears;
|
|
65
|
+
let wMonthly = 0;
|
|
66
|
+
if (!inAccumulation) {
|
|
67
|
+
const yearsRetired = y - accYears;
|
|
68
|
+
if (yearsRetired === 0) {
|
|
69
|
+
anchorW = balance * params.withdrawalRate;
|
|
70
|
+
initialRate = params.withdrawalRate;
|
|
71
|
+
}
|
|
72
|
+
let w;
|
|
73
|
+
switch (params.strategy) {
|
|
74
|
+
case 'fixedReal':
|
|
75
|
+
w = anchorW;
|
|
76
|
+
break;
|
|
77
|
+
case 'fixedPercent':
|
|
78
|
+
w = balance * params.withdrawalRate;
|
|
79
|
+
break;
|
|
80
|
+
case 'guardrails': {
|
|
81
|
+
if (yearsRetired > 0 && balance > 0) {
|
|
82
|
+
const currentRate = anchorW / balance;
|
|
83
|
+
if (currentRate > initialRate * 1.2)
|
|
84
|
+
anchorW *= 0.9;
|
|
85
|
+
else if (currentRate < initialRate * 0.8)
|
|
86
|
+
anchorW *= 1.1;
|
|
87
|
+
}
|
|
88
|
+
w = anchorW;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
default: {
|
|
92
|
+
// VPW: amortize over remaining years at an assumed real return.
|
|
93
|
+
const n = horizonYears - yearsRetired;
|
|
94
|
+
const r = params.vpwReturn ?? 0.025;
|
|
95
|
+
const rate = r === 0 ? 1 / n : r / (1 - (1 + r) ** -n);
|
|
96
|
+
w = balance * rate;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (yearsRetired === 0)
|
|
100
|
+
firstYearW = w;
|
|
101
|
+
if (w < worstYearW)
|
|
102
|
+
worstYearW = w;
|
|
103
|
+
wMonthly = w / 12;
|
|
104
|
+
}
|
|
105
|
+
let drawn = 0;
|
|
106
|
+
for (let k = 0; k < 12; k++) {
|
|
107
|
+
if (inAccumulation) {
|
|
108
|
+
balance += contribMonthly;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
drawn += Math.min(wMonthly, balance);
|
|
112
|
+
balance -= wMonthly;
|
|
113
|
+
if (balance <= 0) {
|
|
114
|
+
balance = 0;
|
|
115
|
+
depletedYear = y + 1;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
balance *= (1 + monthly[m + k]) * monthlyFeeFactor;
|
|
120
|
+
}
|
|
121
|
+
if (!inAccumulation)
|
|
122
|
+
withdrawals[y - accYears] = drawn;
|
|
123
|
+
m += 12;
|
|
124
|
+
path[y + 1] = balance;
|
|
125
|
+
}
|
|
126
|
+
if (!Number.isFinite(worstYearW))
|
|
127
|
+
worstYearW = 0;
|
|
128
|
+
const failed = depletedYear !== null && !(params.strategy === 'vpw' && depletedYear === totalYears);
|
|
129
|
+
return { path, ending: balance, depletedYear, failed, firstYearW, worstYearW, withdrawals };
|
|
130
|
+
}
|
|
131
|
+
// ---- percentile helper ------------------------------------------------------
|
|
132
|
+
function percentile(sorted, p) {
|
|
133
|
+
if (sorted.length === 0)
|
|
134
|
+
return 0;
|
|
135
|
+
const idx = (p / 100) * (sorted.length - 1);
|
|
136
|
+
const lo = Math.floor(idx);
|
|
137
|
+
const hi = Math.ceil(idx);
|
|
138
|
+
if (lo === hi)
|
|
139
|
+
return sorted[lo];
|
|
140
|
+
return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo);
|
|
141
|
+
}
|
|
142
|
+
function summarize(mode, trialsOut, labels, params) {
|
|
143
|
+
const accumulationYears = params.accumulationYears ?? 0;
|
|
144
|
+
const totalYears = accumulationYears + params.horizonYears;
|
|
145
|
+
const trials = trialsOut.length;
|
|
146
|
+
const endings = trialsOut.map((t) => t.path[totalYears]);
|
|
147
|
+
const successes = trialsOut.filter((t) => !t.failed).length;
|
|
148
|
+
// Per-year percentiles.
|
|
149
|
+
const pByYear = { p5: [], p25: [], p50: [], p75: [], p95: [] };
|
|
150
|
+
for (let y = 0; y <= totalYears; y++) {
|
|
151
|
+
const col = trialsOut.map((t) => t.path[y]).sort((a, b) => a - b);
|
|
152
|
+
pByYear.p5.push(percentile(col, 5));
|
|
153
|
+
pByYear.p25.push(percentile(col, 25));
|
|
154
|
+
pByYear.p50.push(percentile(col, 50));
|
|
155
|
+
pByYear.p75.push(percentile(col, 75));
|
|
156
|
+
pByYear.p95.push(percentile(col, 95));
|
|
157
|
+
}
|
|
158
|
+
const sortedEndings = [...endings].sort((a, b) => a - b);
|
|
159
|
+
const firstW = trialsOut.map((t) => t.firstYearW).sort((a, b) => a - b);
|
|
160
|
+
const worstW = trialsOut.map((t) => t.worstYearW).sort((a, b) => a - b);
|
|
161
|
+
// Per-retirement-year income distribution + pay-cut stats. "Below start"
|
|
162
|
+
// uses a 0.5% tolerance so float noise doesn't register as a cut; a
|
|
163
|
+
// depleted trial's $0 years count, which is the honest reading.
|
|
164
|
+
const incomeByYear = { p5: [], p25: [], p50: [], p75: [], p95: [] };
|
|
165
|
+
for (let y = 0; y < params.horizonYears; y++) {
|
|
166
|
+
const col = trialsOut.map((t) => t.withdrawals[y]).sort((a, b) => a - b);
|
|
167
|
+
incomeByYear.p5.push(percentile(col, 5));
|
|
168
|
+
incomeByYear.p25.push(percentile(col, 25));
|
|
169
|
+
incomeByYear.p50.push(percentile(col, 50));
|
|
170
|
+
incomeByYear.p75.push(percentile(col, 75));
|
|
171
|
+
incomeByYear.p95.push(percentile(col, 95));
|
|
172
|
+
}
|
|
173
|
+
let cutTrials = 0;
|
|
174
|
+
const yearsBelow = [];
|
|
175
|
+
for (const t of trialsOut) {
|
|
176
|
+
const floor = t.firstYearW * 0.995;
|
|
177
|
+
const below = t.withdrawals.filter((w) => w < floor).length;
|
|
178
|
+
if (below > 0)
|
|
179
|
+
cutTrials++;
|
|
180
|
+
yearsBelow.push(below);
|
|
181
|
+
}
|
|
182
|
+
yearsBelow.sort((a, b) => a - b);
|
|
183
|
+
const worstStarts = labels.length
|
|
184
|
+
? trialsOut
|
|
185
|
+
.map((t, i) => ({
|
|
186
|
+
label: labels[i],
|
|
187
|
+
endingBalance: t.path[totalYears],
|
|
188
|
+
depletedYear: t.failed ? t.path.findIndex((b, y) => y > 0 && b === 0) : null,
|
|
189
|
+
}))
|
|
190
|
+
.sort((a, b) => a.endingBalance - b.endingBalance)
|
|
191
|
+
.slice(0, 10)
|
|
192
|
+
: [];
|
|
193
|
+
return {
|
|
194
|
+
mode,
|
|
195
|
+
trials,
|
|
196
|
+
successRate: trials ? successes / trials : 0,
|
|
197
|
+
horizonYears: params.horizonYears,
|
|
198
|
+
accumulationYears,
|
|
199
|
+
percentiles: pByYear,
|
|
200
|
+
endingBalances: sortedEndings,
|
|
201
|
+
medianEnding: percentile(sortedEndings, 50),
|
|
202
|
+
worstStarts,
|
|
203
|
+
income: {
|
|
204
|
+
firstYearMedian: percentile(firstW, 50),
|
|
205
|
+
worstYearMedian: percentile(worstW, 50),
|
|
206
|
+
worstYearP5: percentile(worstW, 5),
|
|
207
|
+
cutProbability: trials ? cutTrials / trials : 0,
|
|
208
|
+
yearsBelowStartMedian: percentile(yearsBelow, 50),
|
|
209
|
+
},
|
|
210
|
+
incomeByYear,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
// ---- historical sequence ----------------------------------------------------
|
|
214
|
+
export function runHistoricalSequence(series, params) {
|
|
215
|
+
const months = trialMonths(params);
|
|
216
|
+
const out = [];
|
|
217
|
+
const labels = [];
|
|
218
|
+
for (let start = 0; start + months <= series.returns.length; start++) {
|
|
219
|
+
out.push(runTrial(params, series.returns, start));
|
|
220
|
+
labels.push(series.dates[start]);
|
|
221
|
+
}
|
|
222
|
+
return summarize('historical', out, labels, params);
|
|
223
|
+
}
|
|
224
|
+
// ---- block bootstrap --------------------------------------------------------
|
|
225
|
+
/**
|
|
226
|
+
* Resample the return series in contiguous blocks (default 24 months) to
|
|
227
|
+
* preserve short-run autocorrelation, building `trials` synthetic sequences.
|
|
228
|
+
*/
|
|
229
|
+
export function runBootstrap(series, params, opts = { trials: 10000 }) {
|
|
230
|
+
const months = trialMonths(params);
|
|
231
|
+
const block = opts.blockMonths ?? 24;
|
|
232
|
+
const rng = opts.rng ?? Math.random;
|
|
233
|
+
const src = series.returns;
|
|
234
|
+
const maxStart = src.length - block;
|
|
235
|
+
const out = [];
|
|
236
|
+
for (let t = 0; t < opts.trials; t++) {
|
|
237
|
+
const seq = new Array(months);
|
|
238
|
+
let filled = 0;
|
|
239
|
+
while (filled < months) {
|
|
240
|
+
const start = Math.floor(rng() * (maxStart + 1));
|
|
241
|
+
const len = Math.min(block, months - filled);
|
|
242
|
+
for (let i = 0; i < len; i++)
|
|
243
|
+
seq[filled + i] = src[start + i];
|
|
244
|
+
filled += len;
|
|
245
|
+
}
|
|
246
|
+
out.push(runTrial(params, seq, 0));
|
|
247
|
+
}
|
|
248
|
+
return summarize('bootstrap', out, [], params);
|
|
249
|
+
}
|
|
250
|
+
/** Solve the max withdrawal rate meeting a target success rate (historical). */
|
|
251
|
+
export function maxSafeWithdrawal(series, params, targetSuccess = 0.95) {
|
|
252
|
+
let lo = 0;
|
|
253
|
+
let hi = 0.2;
|
|
254
|
+
for (let i = 0; i < 24; i++) {
|
|
255
|
+
const mid = (lo + hi) / 2;
|
|
256
|
+
const r = runHistoricalSequence(series, { ...params, withdrawalRate: mid });
|
|
257
|
+
if (r.successRate >= targetSuccess)
|
|
258
|
+
lo = mid;
|
|
259
|
+
else
|
|
260
|
+
hi = mid;
|
|
261
|
+
}
|
|
262
|
+
return lo;
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=simulate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"simulate.js","sourceRoot":"","sources":["../../src/montecarlo/simulate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bucketRules.d.ts","sourceRoot":"","sources":["../../src/retirement/bucketRules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAQnE,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,OAAO,EACjB,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,cAAc,GACnB,OAAO,CA8BT"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const ORDER = {
|
|
2
|
+
'cash-first': ['cash', 'bonds', 'stocks'],
|
|
3
|
+
'stocks-first': ['stocks', 'bonds', 'cash'],
|
|
4
|
+
'bonds-first': ['bonds', 'cash', 'stocks'],
|
|
5
|
+
};
|
|
6
|
+
export function withdrawFromBuckets(amount, balances, returns, rule) {
|
|
7
|
+
const w = { stocks: 0, bonds: 0, cash: 0 };
|
|
8
|
+
const total = balances.stocks + balances.bonds + balances.cash;
|
|
9
|
+
if (total <= 0 || amount <= 0)
|
|
10
|
+
return w;
|
|
11
|
+
if (rule === 'proportional') {
|
|
12
|
+
const a = Math.min(amount, total);
|
|
13
|
+
w.stocks = a * (balances.stocks / total);
|
|
14
|
+
w.bonds = a * (balances.bonds / total);
|
|
15
|
+
w.cash = a * (balances.cash / total);
|
|
16
|
+
return w;
|
|
17
|
+
}
|
|
18
|
+
let order;
|
|
19
|
+
if (rule === 'best-performer') {
|
|
20
|
+
order = ['stocks', 'bonds', 'cash']
|
|
21
|
+
.slice()
|
|
22
|
+
.sort((a, b) => returns[b] - returns[a]);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
order = ORDER[rule];
|
|
26
|
+
}
|
|
27
|
+
let remaining = amount;
|
|
28
|
+
for (const b of order) {
|
|
29
|
+
const take = Math.min(remaining, balances[b]);
|
|
30
|
+
w[b] = take;
|
|
31
|
+
remaining -= take;
|
|
32
|
+
if (remaining <= 0)
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
return w;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=bucketRules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bucketRules.js","sourceRoot":"","sources":["../../src/retirement/bucketRules.ts"],"names":[],"mappings":"AAEA,MAAM,KAAK,GAA0F;IACnG,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC;IACzC,cAAc,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC;IAC3C,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC;CAC3C,CAAC;AAEF,MAAM,UAAU,mBAAmB,CACjC,MAAc,EACd,QAAiB,EACjB,OAAgB,EAChB,IAAoB;IAEpB,MAAM,CAAC,GAAY,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IACpD,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC;IAC/D,IAAI,KAAK,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IAExC,IAAI,IAAI,KAAK,cAAc,EAAE,CAAC;QAC5B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAClC,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC;QACzC,CAAC,CAAC,KAAK,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC;QACvC,CAAC,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC;QACrC,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,KAAwB,CAAC;IAC7B,IAAI,IAAI,KAAK,gBAAgB,EAAE,CAAC;QAC9B,KAAK,GAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAW;aAC3C,KAAK,EAAE;aACP,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC;SAAM,CAAC;QACN,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IACtB,CAAC;IAED,IAAI,SAAS,GAAG,MAAM,CAAC;IACvB,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;QACZ,SAAS,IAAI,IAAI,CAAC;QAClB,IAAI,SAAS,IAAI,CAAC;YAAE,MAAM;IAC5B,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HistoricalRow, SimConfig, TrialResult } from './types.js';
|
|
2
|
+
export declare function aggregateToAnnual(data: HistoricalRow[]): HistoricalRow[];
|
|
3
|
+
export declare function simulate(config: SimConfig, monthly: HistoricalRow[]): TrialResult;
|
|
4
|
+
/** Caller-supplied random source returning a value in [0, 1). */
|
|
5
|
+
export type RandomSource = () => number;
|
|
6
|
+
export declare function pickRandomStart(data: HistoricalRow[], horizonYears: number, freq: 'monthly' | 'annual', random: RandomSource): string;
|
|
7
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/retirement/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EAAE,SAAS,EAAE,WAAW,EAEtC,MAAM,YAAY,CAAC;AAUpB,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAwBxE;AAgID,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,aAAa,EAAE,GAAG,WAAW,CAoIjF;AAQD,iEAAiE;AACjE,MAAM,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC;AAExC,wBAAgB,eAAe,CAC7B,IAAI,EAAE,aAAa,EAAE,EACrB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,SAAS,GAAG,QAAQ,EAC1B,MAAM,EAAE,YAAY,GACnB,MAAM,CAWR"}
|