Skip to content

Commit d07d439

Browse files
committed
optimized.
1 parent 5480924 commit d07d439

File tree

4 files changed

+382
-8
lines changed

4 files changed

+382
-8
lines changed

monte-carlo-projection/all_tests.js

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -446,9 +446,88 @@ function testSymmetricCapping() {
446446
console.log(` Result: ${isSymmetric && lowBias ? 'PASS ✓' : 'FAIL ✗'} (symmetric capping with minimal bias)`);
447447
}
448448

449-
// Test 1.11: Zero Withdrawal Depletion Test
449+
// Test 1.11: Test Dynamic Depletion Threshold
450+
function testDynamicDepletionThreshold() {
451+
console.log('\n1.11 Testing Dynamic Depletion Threshold:');
452+
453+
// Test that depletion threshold is based on withdrawal amount
454+
const initial = 1000000;
455+
const mu = 0.137;
456+
const sigma = 0.40;
457+
const years = 50;
458+
const simulations = 10000;
459+
460+
// Helper function for simulation
461+
function simulateWithThreshold(withdrawalRate) {
462+
let depletedCount = 0;
463+
let nearThresholdCount = 0;
464+
const threshold = initial * withdrawalRate;
465+
466+
for (let i = 0; i < simulations; i++) {
467+
let value = initial;
468+
let depleted = false;
469+
470+
for (let year = 1; year <= years; year++) {
471+
// Check if below threshold (one year's withdrawal)
472+
if (value < threshold) {
473+
depleted = true;
474+
value = 0;
475+
break;
476+
}
477+
478+
// Generate return
479+
const u1 = Math.random();
480+
const u2 = Math.random();
481+
const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
482+
let annualReturn = mu + sigma * z;
483+
const zScore = Math.abs((-1 - mu) / sigma);
484+
const upperBound = mu + zScore * sigma;
485+
annualReturn = Math.max(-0.9999, Math.min(upperBound, annualReturn));
486+
487+
// Apply return and withdrawal
488+
value *= (1 + annualReturn);
489+
if (value > 0) {
490+
const withdrawal = value * withdrawalRate;
491+
value -= withdrawal;
492+
}
493+
}
494+
495+
if (depleted) depletedCount++;
496+
if (value < threshold * 2) nearThresholdCount++;
497+
}
498+
499+
return {
500+
depletionRate: (depletedCount / simulations) * 100,
501+
nearThresholdRate: (nearThresholdCount / simulations) * 100
502+
};
503+
}
504+
505+
// Test different withdrawal rates
506+
console.log(' Testing with different withdrawal rates:');
507+
508+
const result5pct = simulateWithThreshold(0.05);
509+
console.log(` 5% withdrawal: ${result5pct.depletionRate.toFixed(2)}% depleted`);
510+
511+
const result10pct = simulateWithThreshold(0.10);
512+
console.log(` 10% withdrawal: ${result10pct.depletionRate.toFixed(2)}% depleted`);
513+
514+
const result15pct = simulateWithThreshold(0.15);
515+
console.log(` 15% withdrawal: ${result15pct.depletionRate.toFixed(2)}% depleted`);
516+
517+
// Check that higher withdrawal rates have higher depletion
518+
const increasing = result15pct.depletionRate > result10pct.depletionRate &&
519+
result10pct.depletionRate > result5pct.depletionRate;
520+
521+
console.log(` ✓ Depletion increases with withdrawal rate: ${increasing ? 'PASS' : 'FAIL'}`);
522+
523+
// Check that 10% withdrawal with 40% volatility has meaningful depletion
524+
const meaningful = result10pct.depletionRate > 10;
525+
console.log(` ✓ 10% withdrawal has >10% depletion rate: ${meaningful ? 'PASS' : 'FAIL'}`);
526+
}
527+
528+
// Test 1.12: Zero Withdrawal Depletion Test
450529
function testZeroWithdrawalDepletion() {
451-
console.log('\n1.11 Testing Zero Withdrawal Depletion:');
530+
console.log('\n1.12 Testing Zero Withdrawal Depletion:');
452531

453532
const initial = 10000;
454533
const mu = 0.3493; // 34.93% expected return
@@ -712,6 +791,7 @@ async function runAllTests() {
712791
testBoxMuller();
713792
testReturnRateRange();
714793
testSymmetricCapping();
794+
testDynamicDepletionThreshold();
715795
testZeroWithdrawalDepletion();
716796

717797
testHistoricalStats();
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Monte Carlo Projection</title>
7+
<link rel="stylesheet" href="styles.css">
8+
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js"></script>
9+
<script>
10+
function switchTab(method) {
11+
// Update tab buttons
12+
const tabButtons = document.querySelectorAll('.tab-button');
13+
tabButtons.forEach(btn => btn.classList.remove('active'));
14+
event.target.classList.add('active');
15+
16+
// Update tab panels
17+
const percentageTab = document.getElementById('percentageTab');
18+
const fixedTab = document.getElementById('fixedTab');
19+
20+
if (method === 'percentage') {
21+
percentageTab.style.display = 'block';
22+
fixedTab.style.display = 'none';
23+
document.getElementById('percentageRadio').checked = true;
24+
} else {
25+
percentageTab.style.display = 'none';
26+
fixedTab.style.display = 'block';
27+
document.getElementById('fixedRadio').checked = true;
28+
}
29+
}
30+
</script>
31+
</head>
32+
<body>
33+
<div class="container">
34+
<h1>Monte Carlo Projection</h1>
35+
<div class="subtitle">Investment Projection with 100,000 Simulations</div>
36+
37+
<div class="controls">
38+
<!-- Investment Parameters Section -->
39+
<h3>Investment Parameters</h3>
40+
<div class="investment-params">
41+
<div class="params-row">
42+
<div class="control-group">
43+
<label for="initialInvestment">Initial Investment ($)</label>
44+
<input type="text" id="initialInvestment" value="1,000,000">
45+
</div>
46+
<div class="control-group">
47+
<label for="expectedReturn">Expected Annual Return (%)</label>
48+
<input type="number" id="expectedReturn" value="17" step="0.1" min="0">
49+
</div>
50+
<div class="control-group">
51+
<label for="volatility">Annual Volatility (σ) (%)</label>
52+
<input type="number" id="volatility" value="20" step="0.1" min="0">
53+
</div>
54+
</div>
55+
<div class="params-row" style="margin-top: 20px;">
56+
<div class="control-group">
57+
<label for="yearsProjection">Years to Project</label>
58+
<input type="number" id="yearsProjection" value="30" min="1" max="50">
59+
</div>
60+
<div class="control-group">
61+
<label for="withdrawalStartYear">Withdraw from the end of __ years</label>
62+
<input type="number" id="withdrawalStartYear" value="6" min="1" max="50" title="Enter the year when withdrawals should start. Year 1 means withdrawals start at the end of the first year.">
63+
<small style="color: #636e72; font-size: 0.8em; margin-top: 2px;">Year 1 = end of first year</small>
64+
</div>
65+
</div>
66+
</div>
67+
68+
<!-- Withdrawal Method Tabs -->
69+
<div class="withdrawal-section">
70+
<h3>Withdrawal Method</h3>
71+
<div class="tabs">
72+
<button class="tab-button active" onclick="switchTab('percentage')">Percentage of Portfolio</button>
73+
<button class="tab-button" onclick="switchTab('fixed')">Fixed Amount with Inflation</button>
74+
</div>
75+
76+
<!-- Hidden radio buttons for maintaining compatibility -->
77+
<div style="display: none;">
78+
<input type="radio" name="withdrawalMethod" value="percentage" id="percentageRadio" checked>
79+
<input type="radio" name="withdrawalMethod" value="fixed" id="fixedRadio">
80+
</div>
81+
82+
<div class="tab-content">
83+
<div id="percentageTab" class="tab-panel active">
84+
<div class="control-group">
85+
<label for="withdrawalRate">Annual Withdrawal Rate (%)</label>
86+
<input type="number" id="withdrawalRate" value="3" step="0.1" min="0" max="100">
87+
</div>
88+
</div>
89+
90+
<div id="fixedTab" class="tab-panel" style="display: none;">
91+
<div class="control-group">
92+
<label for="fixedWithdrawalAmount">Starting Annual Withdrawal ($)</label>
93+
<input type="text" id="fixedWithdrawalAmount" value="40,000">
94+
</div>
95+
<div class="control-group">
96+
<label for="inflationRate">Annual Inflation Rate (%)</label>
97+
<input type="number" id="inflationRate" value="2.5" step="0.1" min="0" max="20">
98+
</div>
99+
</div>
100+
</div>
101+
</div>
102+
</div>
103+
104+
<div style="text-align: center; margin-top: 30px;">
105+
<button onclick="runSimulation()">Run Simulation</button>
106+
</div>
107+
108+
<div id="loading" class="loading" style="display: none;">
109+
Running 100,000 simulations... Please wait...
110+
</div>
111+
112+
<div id="results" style="display: none;">
113+
<div class="results">
114+
<div class="stat-card">
115+
<div class="stat-label">Median Outcome (50th percentile)</div>
116+
<div class="stat-value" id="median">-</div>
117+
</div>
118+
<div class="stat-card">
119+
<div class="stat-label">Mean Outcome</div>
120+
<div class="stat-value" id="mean">-</div>
121+
</div>
122+
<div class="stat-card">
123+
<div class="stat-label">Best Case (95th percentile)</div>
124+
<div class="stat-value" id="best">-</div>
125+
</div>
126+
<div class="stat-card">
127+
<div class="stat-label">Worst Case (5th percentile)</div>
128+
<div class="stat-value" id="worst">-</div>
129+
</div>
130+
<div class="stat-card">
131+
<div class="stat-label">Withdrawal Strategy</div>
132+
<div class="stat-value" id="annualWithdrawalRate">-</div>
133+
</div>
134+
<div class="stat-card">
135+
<div class="stat-label">Withdrawal Start Year</div>
136+
<div class="stat-value" id="withdrawalStartYearDisplay">-</div>
137+
</div>
138+
<div class="stat-card" style="grid-column: span 2;">
139+
<div class="stat-label">Portfolio Depletion Rate</div>
140+
<div class="stat-value" id="depletionRate">-</div>
141+
</div>
142+
<div class="stat-card" style="grid-column: span 2;">
143+
<div class="stat-label">Total Withdrawn (Median Case)</div>
144+
<div class="stat-value" id="totalWithdrawn">-</div>
145+
</div>
146+
</div>
147+
148+
<div class="charts-container">
149+
<div class="chart-wrapper">
150+
<div class="chart-title">Distribution of Final Values</div>
151+
<canvas id="histogramChart"></canvas>
152+
</div>
153+
<div class="chart-wrapper">
154+
<div class="chart-title">Sample Simulation Paths (100 paths)</div>
155+
<canvas id="pathsChart"></canvas>
156+
</div>
157+
</div>
158+
159+
<div class="probability-table">
160+
<div class="chart-title">Median Outcome Details</div>
161+
<table>
162+
<thead>
163+
<tr>
164+
<th>Year</th>
165+
<th>Value</th>
166+
<th>Return Rate</th>
167+
<th>Withdrawal Amount</th>
168+
<th>Actual Withdrawal Rate</th>
169+
<th>Total Withdrawn</th>
170+
</tr>
171+
</thead>
172+
<tbody id="medianTableBody">
173+
</tbody>
174+
</table>
175+
</div>
176+
177+
<div class="probability-table">
178+
<div class="chart-title">Probability Analysis</div>
179+
<table>
180+
<thead>
181+
<tr>
182+
<th>Target Value</th>
183+
<th>Probability of Exceeding</th>
184+
<th>Multiple of Initial Investment</th>
185+
</tr>
186+
</thead>
187+
<tbody id="probabilityTableBody">
188+
</tbody>
189+
</table>
190+
</div>
191+
</div>
192+
</div>
193+
194+
<script src="script.js"></script>
195+
</body>
196+
</html>

monte-carlo-projection/script.js

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,15 @@ function monteCarloSimulation(initial, mu, sigma, years, withdrawalMethod, withd
246246
const depleted = [];
247247
let medianPath = null;
248248

249+
// Calculate depletion threshold based on one year's withdrawal amount
250+
// If withdrawal rate is 0, use 1% of initial as threshold
251+
let baseDepletionThreshold;
252+
if (withdrawalMethod === 'percentage') {
253+
baseDepletionThreshold = withdrawalRate > 0 ? initial * withdrawalRate : initial * 0.01;
254+
} else {
255+
baseDepletionThreshold = fixedAmount > 0 ? fixedAmount : initial * 0.01;
256+
}
257+
249258
// Pre-calculate inflation multipliers
250259
const inflationMultipliers = new Float64Array(years + 1);
251260
inflationMultipliers[0] = 1;
@@ -263,9 +272,45 @@ function monteCarloSimulation(initial, mu, sigma, years, withdrawalMethod, withd
263272
const yearlyData = [];
264273

265274
for (let year = 1; year <= years; year++) {
275+
// Calculate current year's depletion threshold
276+
let currentThreshold;
277+
if (withdrawalMethod === 'percentage') {
278+
// For percentage, threshold is based on current portfolio value
279+
currentThreshold = value * withdrawalRate;
280+
// But use base threshold as minimum
281+
if (currentThreshold < baseDepletionThreshold || withdrawalRate === 0) {
282+
currentThreshold = baseDepletionThreshold;
283+
}
284+
} else {
285+
// For fixed, threshold is inflation-adjusted withdrawal amount
286+
if (year >= withdrawalStartYear) {
287+
currentThreshold = fixedAmount * inflationMultipliers[year - withdrawalStartYear];
288+
} else {
289+
currentThreshold = baseDepletionThreshold;
290+
}
291+
}
292+
293+
// Skip if portfolio is already depleted (can't cover one year's withdrawal)
294+
if (value < currentThreshold) {
295+
yearlyData.push({
296+
year,
297+
value: 0,
298+
returnRate: 0,
299+
withdrawal: 0,
300+
totalWithdrawn
301+
});
302+
if (depletedYear === null) {
303+
depletedYear = year;
304+
}
305+
value = 0; // Ensure it stays at 0
306+
continue;
307+
}
308+
309+
// Apply returns
266310
const annualReturn = generateNormalRandom(mu, sigma);
267311
value *= (1 + annualReturn);
268312

313+
// Apply withdrawal
269314
let withdrawal = 0;
270315
if (year >= withdrawalStartYear && value > 0) {
271316
if (withdrawalMethod === 'percentage') {
@@ -275,7 +320,15 @@ function monteCarloSimulation(initial, mu, sigma, years, withdrawalMethod, withd
275320
withdrawal = Math.min(inflationAdjusted, value);
276321
}
277322
totalWithdrawn += withdrawal;
278-
value = Math.max(0, value - withdrawal);
323+
value = value - withdrawal;
324+
325+
// Check for depletion after withdrawal
326+
if (value < currentThreshold) {
327+
value = 0;
328+
if (depletedYear === null) {
329+
depletedYear = year;
330+
}
331+
}
279332
}
280333

281334
yearlyData.push({
@@ -285,10 +338,6 @@ function monteCarloSimulation(initial, mu, sigma, years, withdrawalMethod, withd
285338
withdrawal,
286339
totalWithdrawn
287340
});
288-
289-
if (value === 0 && depletedYear === null) {
290-
depletedYear = year;
291-
}
292341
}
293342

294343
finalValues[sim] = value;

0 commit comments

Comments
 (0)