-
Notifications
You must be signed in to change notification settings - Fork 0
/
script.js
173 lines (157 loc) · 7.03 KB
/
script.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
function updateSliderValue(sliderId, displayId) {
var slider = document.getElementById(sliderId);
var display = document.getElementById(displayId);
display.textContent = slider.value;
}
function toggleMonthlyInput() {
var investmentType = document.getElementById('investmentType').value;
var monthlyInvestmentInput = document.getElementById('monthlyInvestmentInput');
// Check if the investment type is 'monthly' and show/hide the monthly investment input accordingly
if (investmentType === 'monthly') {
// Remove the 'hidden' class if it exists
if (monthlyInvestmentInput.classList.contains('hidden')) {
monthlyInvestmentInput.classList.remove('hidden');
}
} else {
// Add the 'hidden' class if it's not already there
if (!monthlyInvestmentInput.classList.contains('hidden')) {
monthlyInvestmentInput.classList.add('hidden');
}
}
}
function getColorForPercentile(percentile) {
// Red to yellow gradient: interpolate between red (255,0,0) and yellow (255,255,0)
var greenValue = Math.round(255 * (percentile / 10)); // Interpolate green channel
return `rgb(255, ${greenValue}, 0)`;
}
function gaussianRandom(mean=0, stdev=1) {
// Standard Normal variate using Box-Muller transform. Credits to Maxwell Collard's answer in https://stackoverflow.com/questions/25582882/javascript-math-random-normal-distribution-gaussian-bell-curve
const u = 1 - Math.random(); // Converting [0,1) to (0,1]
const v = Math.random();
const z = Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v );
// Transform to the desired mean and standard deviation:
return z * stdev + mean;
}
function runSimulations() {
// Retrieve input values
var amountInvested = parseFloat(document.getElementById('amountInvested').value);
var investmentType = document.getElementById('investmentType').value; // Get the investment type
var monthlyInvestment = investmentType === 'monthly' ? parseFloat(document.getElementById('monthlyInvestment').value) : 0;
var cagr = parseFloat(document.getElementById('cagr').value) / 100; // Convert percentage to decimal
var volatility = parseFloat(document.getElementById('volatility').value) / 100; // Convert percentage to decimal
var timeHorizon = parseInt(document.getElementById('timeHorizon').value);
var monteCarloSimulations = parseInt(document.getElementById('monteCarloSimulations').value);
// Derived values
var numMonths = timeHorizon*12;
var monthlyReturn = Math.log(1 + cagr)/12; // like cagr/12 but adjusted for compounding
var monthlyVolatility = volatility/Math.sqrt(12); // lognormal process, volatility grows with sqrt(time)
// Calculate the timeseries of investment contributions
var investmentContributions = [amountInvested]; // Start with the initial amount invested
for (let month = 1; month <= numMonths; month++) {
// Each month, add the monthly investment to the last value
let lastValue = investmentContributions[month - 1];
let newValue = lastValue + monthlyInvestment;
investmentContributions.push(newValue);
}
// Run all simulations and store final values with corresponding simulation data
var simulationResults = [];
for (let sim = 0; sim < monteCarloSimulations; sim++) {
let data = [amountInvested];
for (let month = 1; month <= numMonths; month++) {
let lastValue = data[month - 1];
// Simulate the monthly growth with volatility
let monthlyGrowth = monthlyReturn + monthlyVolatility*gaussianRandom(mean=0, stdev=1)
let newValue = lastValue * (1 + monthlyGrowth) + monthlyInvestment;
data.push(newValue);
}
simulationResults.push({ finalValue: data[numMonths], data: data });
}
// Sort simulation results by final value
simulationResults.sort((a, b) => a.finalValue - b.finalValue);
// Select the 10-percentiles
var percentiles = [];
for (let i = 1; i <= 9; i++) {
let index = Math.floor(monteCarloSimulations * i / 10) - 1;
percentiles.push(simulationResults[index].data);
}
// Clear previous chart if it exists and create a new canvas
var resultElement = document.getElementById('simulationResult');
resultElement.innerHTML = '<canvas id="monteCarloChart"></canvas>';
var ctx = document.getElementById('monteCarloChart').getContext('2d');
// Prepare datasets array for Chart.js
var datasets = percentiles.map((data, index) => ({
label: `Percentile ${(index + 1)*10}`,
data: data,
borderColor: getColorForPercentile(index + 1),
backgroundColor: getColorForPercentile(index + 1),
borderWidth: 1,
pointRadius: 0, // Hide points for a cleaner look
}));
// Add the blue dashed line dataset for the cumulative investments
datasets.push({
label: 'Cumulative Investment',
data: investmentContributions,
borderColor: 'blue',
backgroundColor: 'rgba(0, 0, 255, 0.1)', // Light blue fill under the line
borderWidth: 1,
borderDash: [5, 5], // Dashed line: 5px dash, 5px gap
pointRadius: 0, // Hide points for a cleaner look
fill: true // Fill the area under the line
});
// Create Chart.js chart
new Chart(ctx, {
type: 'line',
data: {
labels: Array.from({length: numMonths + 1}, (_, i) => (i / 12).toFixed(1)), // [0, 0.1, 0.2, ..., numMonths/12]
datasets: datasets
},
options: {
aspectRatio: 3 / 4, // Custom aspect ratio (3:4)
animation: true,
scales: {
x: {
title: {
display: true,
text: 'Year'
}
},
y: {
title: {
display: true,
text: 'Value'
}
}
},
plugins: {
legend: {
display: true, // Display the legend
position: 'top', // Position the legend at the top
align: 'start', // Align the legend to the start (left)
labels: {
boxWidth: 10, // Width of the color box
padding: 10, // Padding between rows
usePointStyle: true, // Use point style instead of box
},
},
tooltip: {
enabled: true // Enable tooltips
}
},
elements: {
line: {
tension: 0 // Disable bezier curves
}
}
}
});
// Scroll to the chart element
resultElement.scrollIntoView({
behavior: 'smooth'
});
}
// Initialization
document.addEventListener('DOMContentLoaded', function() {
updateSliderValue('cagr', 'cagrValue');
updateSliderValue('volatility', 'volatilityValue');
toggleMonthlyInput();
});