Below is a symmetric random walk: at each step, your fortune goes up $1 or down $1 with equal probability. Run as many independent walks as you like and watch the sample average (blue line) hover around zero — while individual paths (grey) scatter wildly.
<!-- Martingale demo: controls, canvas, status line, and action buttons. -->
<p class="hint">Each grey line is an independent fair random walk (+1 or -1 at every step). The blue line is the <b>sample average</b> across all walks — it stays near zero, illustrating the martingale property. Use <em>Stop early</em> to check optional stopping.</p>
<div class="controls">
<label>Walks: <input id="numWalks" type="number" min="1" max="50" value="10"></label>
<label>Steps: <input id="numSteps" type="number" min="10" max="200" value="50"></label>
<label>Stop at step: <input id="stopAt" type="number" min="1" max="200" value="25"></label>
</div>
<canvas id="canvas" width="460" height="260"></canvas>
<div class="status" id="status"></div>
<div class="btns">
<button id="btnRun" type="button">Run walks</button>
<button id="btnStop" type="button">Stop early</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
/* Layout and styling for the martingale random walk demo. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem .9rem; margin-bottom: .6rem; align-items: center; }
.controls label { font-size: .85rem; color: #333; display: flex; gap: .3rem; align-items: center; }
.controls input { width: 60px; padding: .2rem .4rem; border: 1px solid #bbb; border-radius: 5px; font-size: .85rem; }
canvas { display: block; border: 1px solid #d0d8e0; border-radius: 8px; background: #f8fafc; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
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 fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Martingale demo: symmetric random walk visualizer.
// A martingale satisfies E[M_{t+1} | M_0,...,M_t] = M_t for all t.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const statusEl = document.getElementById('status');
// Shared state: array of walk paths and optional stop line.
let paths = [];
let stopLine = null;
function getParams() {
// Read input values with safe clamping.
const n = Math.max(1, Math.min(50, parseInt(document.getElementById('numWalks').value) || 10));
const s = Math.max(10, Math.min(200, parseInt(document.getElementById('numSteps').value) || 50));
const t = Math.max(1, Math.min(s, parseInt(document.getElementById('stopAt').value) || 25));
return { n, s, t };
}
function generateWalks(n, s) {
// Generate n independent symmetric random walks of length s.
const result = [];
for (let i = 0; i < n; i++) {
const path = [0];
for (let k = 1; k <= s; k++) {
// Fair coin: +1 or -1 with equal probability.
path.push(path[k - 1] + (Math.random() < 0.5 ? 1 : -1));
}
result.push(path);
}
return result;
}
function averageAtStep(paths, step) {
// Compute sample average of all walks at a given step index.
const total = paths.reduce((sum, p) => sum + p[Math.min(step, p.length - 1)], 0);
return total / paths.length;
}
function draw() {
const { n, s, t } = getParams();
const W = canvas.width, H = canvas.height;
const PAD = { top: 20, bottom: 28, left: 36, right: 16 };
const plotW = W - PAD.left - PAD.right;
const plotH = H - PAD.top - PAD.bottom;
// Compute y-axis scale from max absolute value across all paths.
let maxAbs = 1;
for (const p of paths) for (const v of p) if (Math.abs(v) > maxAbs) maxAbs = Math.abs(v);
maxAbs = Math.ceil(maxAbs * 1.15);
function xOf(step) { return PAD.left + (step / s) * plotW; }
function yOf(val) { return PAD.top + (1 - (val + maxAbs) / (2 * maxAbs)) * plotH; }
ctx.clearRect(0, 0, W, H);
// Draw horizontal grid lines.
ctx.strokeStyle = '#dde4ec';
ctx.lineWidth = 1;
for (let g = -maxAbs; g <= maxAbs; g += Math.ceil(maxAbs / 4)) {
ctx.beginPath(); ctx.moveTo(PAD.left, yOf(g)); ctx.lineTo(W - PAD.right, yOf(g)); ctx.stroke();
ctx.fillStyle = '#888'; ctx.font = '11px system-ui';
ctx.fillText(g, 2, yOf(g) + 4);
}
// Draw the zero (mean) axis.
ctx.strokeStyle = '#b0bec8'; ctx.lineWidth = 1.2;
ctx.beginPath(); ctx.moveTo(PAD.left, yOf(0)); ctx.lineTo(W - PAD.right, yOf(0)); ctx.stroke();
// Draw individual walk paths in light grey.
for (const p of paths) {
ctx.strokeStyle = 'rgba(160,175,190,0.45)'; ctx.lineWidth = 1;
ctx.beginPath();
for (let k = 0; k <= s && k < p.length; k++) {
k === 0 ? ctx.moveTo(xOf(k), yOf(p[k])) : ctx.lineTo(xOf(k), yOf(p[k]));
}
ctx.stroke();
}
// Draw sample average path in blue.
if (paths.length > 0) {
ctx.strokeStyle = '#2563eb'; ctx.lineWidth = 2.2;
ctx.beginPath();
for (let k = 0; k <= s; k++) {
const avg = averageAtStep(paths, k);
k === 0 ? ctx.moveTo(xOf(k), yOf(avg)) : ctx.lineTo(xOf(k), yOf(avg));
}
ctx.stroke();
}
// Draw optional stopping time as a red dashed vertical line.
if (stopLine !== null) {
ctx.strokeStyle = '#e63946'; ctx.lineWidth = 1.8;
ctx.setLineDash([5, 4]);
ctx.beginPath(); ctx.moveTo(xOf(stopLine), PAD.top); ctx.lineTo(xOf(stopLine), H - PAD.bottom); ctx.stroke();
ctx.setLineDash([]);
const avgAtStop = averageAtStep(paths, stopLine);
ctx.fillStyle = '#e63946'; ctx.font = 'bold 11px system-ui';
ctx.fillText('avg=' + avgAtStop.toFixed(2), xOf(stopLine) + 4, PAD.top + 14);
}
}
function setStatus(msg, cls) {
statusEl.textContent = msg; statusEl.className = 'status ' + cls;
}
document.getElementById('btnRun').addEventListener('click', () => {
// Generate fresh walks and redraw.
const { n, s } = getParams();
paths = generateWalks(n, s);
stopLine = null;
draw();
const avg = averageAtStep(paths, s);
setStatus(fmt('Ran %n% walks of %s% steps. Average at end: %avg% (should be near 0)', { n: n, s: s, avg: avg.toFixed(3) }), 'ok');
console.log('Generated walks:', n, 'steps', s, '| avg at end:', avg.toFixed(4));
});
document.getElementById('btnStop').addEventListener('click', () => {
// Apply optional stopping: draw marker and report average.
if (paths.length === 0) { setStatus('Run some walks first.', 'info'); return; }
const { t, s } = getParams();
stopLine = Math.min(t, s);
draw();
const avg = averageAtStep(paths, stopLine);
setStatus(fmt('Stopped at step %stopLine%. Average at stop: %avg% (martingale property holds)', { stopLine: stopLine, avg: avg.toFixed(3) }), 'ok');
});
document.getElementById('btnReset').addEventListener('click', () => {
// Clear all walks and redraw blank canvas.
paths = []; stopLine = null;
draw();
setStatus('', '');
});
// Initial blank canvas draw.
draw();
<!-- Martingale demo: controls, canvas, status line, and action buttons. -->
<p class="hint">Each grey line is an independent fair random walk (+1 or -1 at every step). The blue line is the <b>sample average</b> across all walks — it stays near zero, illustrating the martingale property. Use <em>Stop early</em> to check optional stopping.</p>
<div class="controls">
<label>Walks: <input id="numWalks" type="number" min="1" max="50" value="10"></label>
<label>Steps: <input id="numSteps" type="number" min="10" max="200" value="50"></label>
<label>Stop at step: <input id="stopAt" type="number" min="1" max="200" value="25"></label>
</div>
<canvas id="canvas" width="460" height="260"></canvas>
<div class="status" id="status"></div>
<div class="btns">
<button id="btnRun" type="button">Run walks</button>
<button id="btnStop" type="button">Stop early</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
/* Layout and styling for the martingale random walk demo. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem .9rem; margin-bottom: .6rem; align-items: center; }
.controls label { font-size: .85rem; color: #333; display: flex; gap: .3rem; align-items: center; }
.controls input { width: 60px; padding: .2rem .4rem; border: 1px solid #bbb; border-radius: 5px; font-size: .85rem; }
canvas { display: block; border: 1px solid #d0d8e0; border-radius: 8px; background: #f8fafc; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
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 fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Martingale demo: symmetric random walk visualizer.
// A martingale satisfies E[M_{t+1} | M_0,...,M_t] = M_t for all t.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const statusEl = document.getElementById('status');
// Shared state: array of walk paths and optional stop line.
let paths = [];
let stopLine = null;
function getParams() {
// Read input values with safe clamping.
const n = Math.max(1, Math.min(50, parseInt(document.getElementById('numWalks').value) || 10));
const s = Math.max(10, Math.min(200, parseInt(document.getElementById('numSteps').value) || 50));
const t = Math.max(1, Math.min(s, parseInt(document.getElementById('stopAt').value) || 25));
return { n, s, t };
}
function generateWalks(n, s) {
// Generate n independent symmetric random walks of length s.
const result = [];
for (let i = 0; i < n; i++) {
const path = [0];
for (let k = 1; k <= s; k++) {
// Fair coin: +1 or -1 with equal probability.
path.push(path[k - 1] + (Math.random() < 0.5 ? 1 : -1));
}
result.push(path);
}
return result;
}
function averageAtStep(paths, step) {
// Compute sample average of all walks at a given step index.
const total = paths.reduce((sum, p) => sum + p[Math.min(step, p.length - 1)], 0);
return total / paths.length;
}
function draw() {
const { n, s, t } = getParams();
const W = canvas.width, H = canvas.height;
const PAD = { top: 20, bottom: 28, left: 36, right: 16 };
const plotW = W - PAD.left - PAD.right;
const plotH = H - PAD.top - PAD.bottom;
// Compute y-axis scale from max absolute value across all paths.
let maxAbs = 1;
for (const p of paths) for (const v of p) if (Math.abs(v) > maxAbs) maxAbs = Math.abs(v);
maxAbs = Math.ceil(maxAbs * 1.15);
function xOf(step) { return PAD.left + (step / s) * plotW; }
function yOf(val) { return PAD.top + (1 - (val + maxAbs) / (2 * maxAbs)) * plotH; }
ctx.clearRect(0, 0, W, H);
// Draw horizontal grid lines.
ctx.strokeStyle = '#dde4ec';
ctx.lineWidth = 1;
for (let g = -maxAbs; g <= maxAbs; g += Math.ceil(maxAbs / 4)) {
ctx.beginPath(); ctx.moveTo(PAD.left, yOf(g)); ctx.lineTo(W - PAD.right, yOf(g)); ctx.stroke();
ctx.fillStyle = '#888'; ctx.font = '11px system-ui';
ctx.fillText(g, 2, yOf(g) + 4);
}
// Draw the zero (mean) axis.
ctx.strokeStyle = '#b0bec8'; ctx.lineWidth = 1.2;
ctx.beginPath(); ctx.moveTo(PAD.left, yOf(0)); ctx.lineTo(W - PAD.right, yOf(0)); ctx.stroke();
// Draw individual walk paths in light grey.
for (const p of paths) {
ctx.strokeStyle = 'rgba(160,175,190,0.45)'; ctx.lineWidth = 1;
ctx.beginPath();
for (let k = 0; k <= s && k < p.length; k++) {
k === 0 ? ctx.moveTo(xOf(k), yOf(p[k])) : ctx.lineTo(xOf(k), yOf(p[k]));
}
ctx.stroke();
}
// Draw sample average path in blue.
if (paths.length > 0) {
ctx.strokeStyle = '#2563eb'; ctx.lineWidth = 2.2;
ctx.beginPath();
for (let k = 0; k <= s; k++) {
const avg = averageAtStep(paths, k);
k === 0 ? ctx.moveTo(xOf(k), yOf(avg)) : ctx.lineTo(xOf(k), yOf(avg));
}
ctx.stroke();
}
// Draw optional stopping time as a red dashed vertical line.
if (stopLine !== null) {
ctx.strokeStyle = '#e63946'; ctx.lineWidth = 1.8;
ctx.setLineDash([5, 4]);
ctx.beginPath(); ctx.moveTo(xOf(stopLine), PAD.top); ctx.lineTo(xOf(stopLine), H - PAD.bottom); ctx.stroke();
ctx.setLineDash([]);
const avgAtStop = averageAtStep(paths, stopLine);
ctx.fillStyle = '#e63946'; ctx.font = 'bold 11px system-ui';
ctx.fillText('avg=' + avgAtStop.toFixed(2), xOf(stopLine) + 4, PAD.top + 14);
}
}
function setStatus(msg, cls) {
statusEl.textContent = msg; statusEl.className = 'status ' + cls;
}
document.getElementById('btnRun').addEventListener('click', () => {
// Generate fresh walks and redraw.
const { n, s } = getParams();
paths = generateWalks(n, s);
stopLine = null;
draw();
const avg = averageAtStep(paths, s);
setStatus(fmt('Ran %n% walks of %s% steps. Average at end: %avg% (should be near 0)', { n: n, s: s, avg: avg.toFixed(3) }), 'ok');
console.log('Generated walks:', n, 'steps', s, '| avg at end:', avg.toFixed(4));
});
document.getElementById('btnStop').addEventListener('click', () => {
// Apply optional stopping: draw marker and report average.
if (paths.length === 0) { setStatus('Run some walks first.', 'info'); return; }
const { t, s } = getParams();
stopLine = Math.min(t, s);
draw();
const avg = averageAtStep(paths, stopLine);
setStatus(fmt('Stopped at step %stopLine%. Average at stop: %avg% (martingale property holds)', { stopLine: stopLine, avg: avg.toFixed(3) }), 'ok');
});
document.getElementById('btnReset').addEventListener('click', () => {
// Clear all walks and redraw blank canvas.
paths = []; stopLine = null;
draw();
setStatus('', '');
});
// Initial blank canvas draw.
draw();
Comments
Loading comments...