Adjust the drift μ and volatility σ, then click Simulate to generate a random GBM price path of 252 daily steps (one trading year). Each run uses a fresh set of standard normal random increments.
<!-- GBM demo: simulate a stock price path with drift (mu) and volatility (sigma) -->
<div class="controls">
<label>Drift μ (annual) <span id="mu-val">0.10</span>
<input type="range" id="mu" min="-0.50" max="0.50" step="0.01" value="0.10">
</label>
<label>Volatility σ (annual) <span id="sigma-val">0.20</span>
<input type="range" id="sigma" min="0.01" max="1.00" step="0.01" value="0.20">
</label>
<button id="run" type="button">Simulate</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<canvas id="chart" width="560" height="260"></canvas>
<div class="status" id="status">Set drift and volatility, then click Simulate.</div>
/* Layout for GBM simulation controls and canvas */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 4px; }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; margin-bottom: .6rem; }
label { display: flex; flex-direction: column; font-size: .82rem; color: #444; gap: 2px; min-width: 120px; }
label span { font-weight: 700; font-size: .9rem; color: #1d3557; }
input[type=range] { width: 120px; accent-color: #1d3557; cursor: pointer; }
canvas { display: block; width: 100%; max-width: 560px; border: 1px solid #cdd9e3; border-radius: 8px; background: #f7f9fb; }
.status { font-size: .9rem; color: #444; margin-top: .5rem; min-height: 1.3em; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* GBM: dS = mu*S*dt + sigma*S*dW, solved as S(t) = S0*exp((mu - 0.5*sigma^2)*t + sigma*W(t)) */
/* Box-Muller transform produces standard normal increments for the Wiener process */
const STEPS = 252; /* 252 trading days = 1 year */
const S0 = 100; /* starting price */
/* Box-Muller: generate standard normal random variable */
function randn() {
let u = 0, v = 0;
while (u === 0) u = Math.random();
while (v === 0) v = Math.random();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
/* Simulate one GBM path of STEPS daily increments */
function simulate(mu, sigma) {
const dt = 1 / 252; /* daily time step = 1/252 years */
const path = [S0];
for (let i = 0; i < STEPS; i++) {
const prev = path[path.length - 1];
const dW = randn() * Math.sqrt(dt);
const next = prev * Math.exp((mu - 0.5 * sigma * sigma) * dt + sigma * dW);
path.push(next);
}
return path;
}
/* Draw the simulated path and the expected-value trend line */
function draw(path, mu, sigma) {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const pad = { top: 20, bottom: 30, left: 48, right: 16 };
ctx.clearRect(0, 0, W, H);
const minP = Math.min(...path);
const maxP = Math.max(...path);
const range = maxP - minP || 1;
/* map step index to canvas x, price to canvas y */
const toX = (i) => pad.left + (i / STEPS) * (W - pad.left - pad.right);
const toY = (p) => pad.top + (1 - (p - minP) / range) * (H - pad.top - pad.bottom);
/* draw light grid lines */
ctx.strokeStyle = '#dde4ea';
ctx.lineWidth = 1;
for (let t = 0; t <= 4; t++) {
const x = toX(t * STEPS / 4);
ctx.beginPath(); ctx.moveTo(x, pad.top); ctx.lineTo(x, H - pad.bottom); ctx.stroke();
}
for (let g = 0; g <= 4; g++) {
const y = pad.top + g * (H - pad.top - pad.bottom) / 4;
ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(W - pad.right, y); ctx.stroke();
const val = maxP - g * range / 4;
ctx.fillStyle = '#888';
ctx.font = '10px system-ui';
ctx.textAlign = 'right';
ctx.fillText(val.toFixed(1), pad.left - 4, y + 3);
}
/* draw the simulated GBM price path in dark blue */
ctx.beginPath();
ctx.moveTo(toX(0), toY(path[0]));
for (let i = 1; i < path.length; i++) ctx.lineTo(toX(i), toY(path[i]));
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 1.6;
ctx.stroke();
/* draw the expected-value line S0*exp(mu*t) in red dashed */
const expEnd = S0 * Math.exp(mu * 1);
ctx.beginPath();
ctx.moveTo(toX(0), toY(S0));
ctx.lineTo(toX(STEPS), toY(expEnd));
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 1.2;
ctx.setLineDash([5, 4]);
ctx.stroke();
ctx.setLineDash([]);
/* quarter labels along the x axis */
ctx.fillStyle = '#888';
ctx.font = '10px system-ui';
ctx.textAlign = 'center';
['Q1','Q2','Q3','Year'].forEach((lbl, i) => {
ctx.fillText(lbl, toX((i + 1) * STEPS / 4), H - 6);
});
}
const muSlider = document.getElementById('mu');
const sigmaSlider = document.getElementById('sigma');
const muVal = document.getElementById('mu-val');
const sigmaVal = document.getElementById('sigma-val');
const status = document.getElementById('status');
muSlider.addEventListener('input', () => { muVal.textContent = (+muSlider.value).toFixed(2); });
sigmaSlider.addEventListener('input', () => { sigmaVal.textContent = (+sigmaSlider.value).toFixed(2); });
document.getElementById('run').addEventListener('click', () => {
const mu = +muSlider.value;
const sigma = +sigmaSlider.value;
const path = simulate(mu, sigma);
draw(path, mu, sigma);
const finalPrice = path[path.length - 1];
const pct = ((finalPrice - S0) / S0 * 100).toFixed(1);
const dir = finalPrice >= S0 ? 'up' : 'down';
status.textContent = i18nFmt('Final price: $%final% (%pct%% %dir% from $100)', { final: finalPrice.toFixed(2), pct: Math.abs(pct), dir });
});
document.getElementById('reset').addEventListener('click', () => {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
status.textContent = 'Set drift and volatility, then click Simulate.';
muSlider.value = '0.10'; muVal.textContent = '0.10';
sigmaSlider.value = '0.20'; sigmaVal.textContent = '0.20';
});
<!-- GBM demo: simulate a stock price path with drift (mu) and volatility (sigma) -->
<div class="controls">
<label>Drift μ (annual) <span id="mu-val">0.10</span>
<input type="range" id="mu" min="-0.50" max="0.50" step="0.01" value="0.10">
</label>
<label>Volatility σ (annual) <span id="sigma-val">0.20</span>
<input type="range" id="sigma" min="0.01" max="1.00" step="0.01" value="0.20">
</label>
<button id="run" type="button">Simulate</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<canvas id="chart" width="560" height="260"></canvas>
<div class="status" id="status">Set drift and volatility, then click Simulate.</div>
/* Layout for GBM simulation controls and canvas */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 4px; }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; margin-bottom: .6rem; }
label { display: flex; flex-direction: column; font-size: .82rem; color: #444; gap: 2px; min-width: 120px; }
label span { font-weight: 700; font-size: .9rem; color: #1d3557; }
input[type=range] { width: 120px; accent-color: #1d3557; cursor: pointer; }
canvas { display: block; width: 100%; max-width: 560px; border: 1px solid #cdd9e3; border-radius: 8px; background: #f7f9fb; }
.status { font-size: .9rem; color: #444; margin-top: .5rem; min-height: 1.3em; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* GBM: dS = mu*S*dt + sigma*S*dW, solved as S(t) = S0*exp((mu - 0.5*sigma^2)*t + sigma*W(t)) */
/* Box-Muller transform produces standard normal increments for the Wiener process */
const STEPS = 252; /* 252 trading days = 1 year */
const S0 = 100; /* starting price */
/* Box-Muller: generate standard normal random variable */
function randn() {
let u = 0, v = 0;
while (u === 0) u = Math.random();
while (v === 0) v = Math.random();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
/* Simulate one GBM path of STEPS daily increments */
function simulate(mu, sigma) {
const dt = 1 / 252; /* daily time step = 1/252 years */
const path = [S0];
for (let i = 0; i < STEPS; i++) {
const prev = path[path.length - 1];
const dW = randn() * Math.sqrt(dt);
const next = prev * Math.exp((mu - 0.5 * sigma * sigma) * dt + sigma * dW);
path.push(next);
}
return path;
}
/* Draw the simulated path and the expected-value trend line */
function draw(path, mu, sigma) {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const pad = { top: 20, bottom: 30, left: 48, right: 16 };
ctx.clearRect(0, 0, W, H);
const minP = Math.min(...path);
const maxP = Math.max(...path);
const range = maxP - minP || 1;
/* map step index to canvas x, price to canvas y */
const toX = (i) => pad.left + (i / STEPS) * (W - pad.left - pad.right);
const toY = (p) => pad.top + (1 - (p - minP) / range) * (H - pad.top - pad.bottom);
/* draw light grid lines */
ctx.strokeStyle = '#dde4ea';
ctx.lineWidth = 1;
for (let t = 0; t <= 4; t++) {
const x = toX(t * STEPS / 4);
ctx.beginPath(); ctx.moveTo(x, pad.top); ctx.lineTo(x, H - pad.bottom); ctx.stroke();
}
for (let g = 0; g <= 4; g++) {
const y = pad.top + g * (H - pad.top - pad.bottom) / 4;
ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(W - pad.right, y); ctx.stroke();
const val = maxP - g * range / 4;
ctx.fillStyle = '#888';
ctx.font = '10px system-ui';
ctx.textAlign = 'right';
ctx.fillText(val.toFixed(1), pad.left - 4, y + 3);
}
/* draw the simulated GBM price path in dark blue */
ctx.beginPath();
ctx.moveTo(toX(0), toY(path[0]));
for (let i = 1; i < path.length; i++) ctx.lineTo(toX(i), toY(path[i]));
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 1.6;
ctx.stroke();
/* draw the expected-value line S0*exp(mu*t) in red dashed */
const expEnd = S0 * Math.exp(mu * 1);
ctx.beginPath();
ctx.moveTo(toX(0), toY(S0));
ctx.lineTo(toX(STEPS), toY(expEnd));
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 1.2;
ctx.setLineDash([5, 4]);
ctx.stroke();
ctx.setLineDash([]);
/* quarter labels along the x axis */
ctx.fillStyle = '#888';
ctx.font = '10px system-ui';
ctx.textAlign = 'center';
['Q1','Q2','Q3','Year'].forEach((lbl, i) => {
ctx.fillText(lbl, toX((i + 1) * STEPS / 4), H - 6);
});
}
const muSlider = document.getElementById('mu');
const sigmaSlider = document.getElementById('sigma');
const muVal = document.getElementById('mu-val');
const sigmaVal = document.getElementById('sigma-val');
const status = document.getElementById('status');
muSlider.addEventListener('input', () => { muVal.textContent = (+muSlider.value).toFixed(2); });
sigmaSlider.addEventListener('input', () => { sigmaVal.textContent = (+sigmaSlider.value).toFixed(2); });
document.getElementById('run').addEventListener('click', () => {
const mu = +muSlider.value;
const sigma = +sigmaSlider.value;
const path = simulate(mu, sigma);
draw(path, mu, sigma);
const finalPrice = path[path.length - 1];
const pct = ((finalPrice - S0) / S0 * 100).toFixed(1);
const dir = finalPrice >= S0 ? 'up' : 'down';
status.textContent = i18nFmt('Final price: $%final% (%pct%% %dir% from $100)', { final: finalPrice.toFixed(2), pct: Math.abs(pct), dir });
});
document.getElementById('reset').addEventListener('click', () => {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
status.textContent = 'Set drift and volatility, then click Simulate.';
muSlider.value = '0.10'; muVal.textContent = '0.10';
sigmaSlider.value = '0.20'; sigmaVal.textContent = '0.20';
});
Notice how a high drift with low volatility produces a smooth climb, while high volatility makes the path jagged and unpredictable — sometimes dipping well below the starting price even when the drift is positive. That tension between trend and noise is the essence of GBM.
Comments
Loading comments...