The demo below runs 200 coin flips with a 60 % heads coin and tracks three bettors simultaneously: one who follows Kelly exactly (20 %), one who bets only 10 % (underbetting), and one who bets 40 % (overbetting).
<div class="controls">
<label>Win probability (p): <span id="p-val">0.60</span>
<input type="range" id="prob" min="0.51" max="0.75" step="0.01" value="0.60">
</label>
<label>Flips: <span id="n-val">200</span>
<input type="range" id="flips" min="50" max="500" step="50" value="200">
</label>
<button id="run" type="button">Run</button>
</div>
<canvas id="chart" width="560" height="280"></canvas>
<div class="legend">
<span class="dot kelly"></span> Kelly (f*)
<span class="dot under"></span> Under-bet (f*/2)
<span class="dot over"></span> Over-bet (2·f*)
</div>
<div id="result" class="result"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; align-items: center; margin-bottom: .7rem; font-size: .88rem; }
label { display: flex; flex-direction: column; gap: .15rem; }
label span { font-weight: 700; font-size: .92rem; }
input[type=range] { width: 120px; }
button { font: 700 14px system-ui; padding: .4rem 1rem; background: #1d3557; color: #fff; border: none; border-radius: 8px; cursor: pointer; }
canvas { width: 100%; height: auto; border: 1px solid #dde3ea; border-radius: 8px; background: #f8fafc; display: block; }
.legend { display: flex; gap: 1.2rem; font-size: .82rem; margin-top: .4rem; }
.dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; margin-right: .3rem; }
.dot.kelly { background: #2a9d8f; }
.dot.under { background: #e9c46a; }
.dot.over { background: #e63946; }
.result { font-size: .88rem; font-weight: 600; margin-top: .5rem; min-height: 1.2em; color: #444; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Kelly criterion simulation: compare Kelly, under-bet, and over-bet strategies
// over N coin flips with win probability p.
// Kelly fraction: f* = p - q/b (for b=1 even money: f* = p - (1-p) = 2p - 1)
// Growth rate: G(f) = p*log(1+f) + (1-p)*log(1-f) [maximised at f*]
const probSlider = document.getElementById('prob');
const flipsSlider = document.getElementById('flips');
const pVal = document.getElementById('p-val');
const nVal = document.getElementById('n-val');
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const resultEl = document.getElementById('result');
probSlider.addEventListener('input', () => { pVal.textContent = (+probSlider.value).toFixed(2); simulate(); });
flipsSlider.addEventListener('input', () => { nVal.textContent = flipsSlider.value; simulate(); });
document.getElementById('run').addEventListener('click', simulate);
function kellyFraction(p) { return 2 * p - 1; } // b=1 even money
function simulate() {
const p = +probSlider.value;
const N = +flipsSlider.value;
const fK = kellyFraction(p); // Kelly
const fU = fK / 2; // under-bet
const fO = Math.min(fK * 2, 0.99); // over-bet (cap at 99%)
// Generate one sequence of coin flips (shared across all three strategies)
const flips = Array.from({length: N}, () => Math.random() < p ? 1 : -1);
// Simulate bankroll trajectories
function trajectory(f) {
let w = 1.0;
const hist = [w];
for (const outcome of flips) {
if (outcome === 1) w *= (1 + f);
else w *= (1 - f);
if (w < 1e-6) w = 1e-6; // floor to avoid log(0)
hist.push(w);
}
return hist;
}
const tK = trajectory(fK);
const tU = trajectory(fU);
const tO = trajectory(fO);
// Draw chart (log scale on y-axis)
drawChart(tK, tU, tO, N, fK);
// Summary
const fmt = v => v >= 1000 ? (v/1000).toFixed(1)+'k×'
: v >= 1 ? v.toFixed(1)+'×'
: v.toFixed(3)+'×';
resultEl.textContent =
i18nFmt('After %N% flips — — Kelly %v%%: %fmt% | Under-bet %v2%%: %fmt2% | Over-bet %v3%%: %fmt3%', { N: N, v: fK.toFixed(0)*100|0, fmt: fmt(tK[N]), v2: (fU*100).toFixed(0), fmt2: fmt(tU[N]), v3: (fO*100).toFixed(0), fmt3: fmt(tO[N]) });
console.log('Kelly f*=', fK.toFixed(3), '| | final wealth K=', tK[N].toFixed(2),
'U=', tU[N].toFixed(2), 'O=', tO[N].toFixed(2));
}
function drawChart(tK, tU, tO, N, fK) {
const W = canvas.width, H = canvas.height;
const pad = { top: 20, right: 20, bottom: 30, left: 52 };
ctx.clearRect(0, 0, W, H);
// Determine y range in log space
const allVals = [...tK, ...tU, ...tO].filter(v => v > 0);
const logMin = Math.min(...allVals.map(Math.log10));
const logMax = Math.max(...allVals.map(Math.log10));
const yPad = (logMax - logMin) * 0.08 || 0.5;
const yLo = logMin - yPad;
const yHi = logMax + yPad;
function xPx(i) { return pad.left + (i / N) * (W - pad.left - pad.right); }
function yPx(val) {
const lv = Math.log10(Math.max(val, 1e-9));
return pad.top + (1 - (lv - yLo) / (yHi - yLo)) * (H - pad.top - pad.bottom);
}
// Grid lines at powers of 10 and halves
ctx.strokeStyle = '#e0e6ed'; ctx.lineWidth = 1;
const ticks = [];
for (let e = Math.floor(yLo); e <= Math.ceil(yHi) + 1; e++) {
ticks.push(Math.pow(10, e));
ticks.push(Math.pow(10, e) * 3);
}
for (const t of ticks) {
const y = yPx(t);
if (y < pad.top || y > H - pad.bottom) continue;
ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(W - pad.right, y); ctx.stroke();
ctx.fillStyle = '#8899aa'; ctx.font = '10px system-ui'; ctx.textAlign = 'right';
const label = t >= 1000 ? (t/1000).toFixed(0)+'k' : t >= 1 ? t.toFixed(t<2?1:0) : t.toFixed(2);
ctx.fillText(label+'×', pad.left - 4, y + 3.5);
}
// Baseline at 1×
const y1 = yPx(1);
ctx.strokeStyle = '#b0bec8'; ctx.lineWidth = 1; ctx.setLineDash([4,3]);
ctx.beginPath(); ctx.moveTo(pad.left, y1); ctx.lineTo(W-pad.right, y1); ctx.stroke();
ctx.setLineDash([]);
// X axis labels
ctx.fillStyle = '#8899aa'; ctx.font = '10px system-ui'; ctx.textAlign = 'center';
for (let i = 0; i <= N; i += Math.round(N/4)) {
ctx.fillText(i, xPx(i), H - pad.bottom + 14);
}
// Draw each trajectory
function drawLine(traj, color) {
ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.setLineDash([]);
ctx.beginPath();
for (let i = 0; i <= N; i++) {
const x = xPx(i), y = yPx(traj[i]);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
}
drawLine(tO, '#e63946'); // over-bet first (behind)
drawLine(tU, '#e9c46a'); // under-bet
drawLine(tK, '#2a9d8f'); // Kelly on top
}
simulate();
<div class="controls">
<label>Win probability (p): <span id="p-val">0.60</span>
<input type="range" id="prob" min="0.51" max="0.75" step="0.01" value="0.60">
</label>
<label>Flips: <span id="n-val">200</span>
<input type="range" id="flips" min="50" max="500" step="50" value="200">
</label>
<button id="run" type="button">Run</button>
</div>
<canvas id="chart" width="560" height="280"></canvas>
<div class="legend">
<span class="dot kelly"></span> Kelly (f*)
<span class="dot under"></span> Under-bet (f*/2)
<span class="dot over"></span> Over-bet (2·f*)
</div>
<div id="result" class="result"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; align-items: center; margin-bottom: .7rem; font-size: .88rem; }
label { display: flex; flex-direction: column; gap: .15rem; }
label span { font-weight: 700; font-size: .92rem; }
input[type=range] { width: 120px; }
button { font: 700 14px system-ui; padding: .4rem 1rem; background: #1d3557; color: #fff; border: none; border-radius: 8px; cursor: pointer; }
canvas { width: 100%; height: auto; border: 1px solid #dde3ea; border-radius: 8px; background: #f8fafc; display: block; }
.legend { display: flex; gap: 1.2rem; font-size: .82rem; margin-top: .4rem; }
.dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; margin-right: .3rem; }
.dot.kelly { background: #2a9d8f; }
.dot.under { background: #e9c46a; }
.dot.over { background: #e63946; }
.result { font-size: .88rem; font-weight: 600; margin-top: .5rem; min-height: 1.2em; color: #444; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Kelly criterion simulation: compare Kelly, under-bet, and over-bet strategies
// over N coin flips with win probability p.
// Kelly fraction: f* = p - q/b (for b=1 even money: f* = p - (1-p) = 2p - 1)
// Growth rate: G(f) = p*log(1+f) + (1-p)*log(1-f) [maximised at f*]
const probSlider = document.getElementById('prob');
const flipsSlider = document.getElementById('flips');
const pVal = document.getElementById('p-val');
const nVal = document.getElementById('n-val');
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const resultEl = document.getElementById('result');
probSlider.addEventListener('input', () => { pVal.textContent = (+probSlider.value).toFixed(2); simulate(); });
flipsSlider.addEventListener('input', () => { nVal.textContent = flipsSlider.value; simulate(); });
document.getElementById('run').addEventListener('click', simulate);
function kellyFraction(p) { return 2 * p - 1; } // b=1 even money
function simulate() {
const p = +probSlider.value;
const N = +flipsSlider.value;
const fK = kellyFraction(p); // Kelly
const fU = fK / 2; // under-bet
const fO = Math.min(fK * 2, 0.99); // over-bet (cap at 99%)
// Generate one sequence of coin flips (shared across all three strategies)
const flips = Array.from({length: N}, () => Math.random() < p ? 1 : -1);
// Simulate bankroll trajectories
function trajectory(f) {
let w = 1.0;
const hist = [w];
for (const outcome of flips) {
if (outcome === 1) w *= (1 + f);
else w *= (1 - f);
if (w < 1e-6) w = 1e-6; // floor to avoid log(0)
hist.push(w);
}
return hist;
}
const tK = trajectory(fK);
const tU = trajectory(fU);
const tO = trajectory(fO);
// Draw chart (log scale on y-axis)
drawChart(tK, tU, tO, N, fK);
// Summary
const fmt = v => v >= 1000 ? (v/1000).toFixed(1)+'k×'
: v >= 1 ? v.toFixed(1)+'×'
: v.toFixed(3)+'×';
resultEl.textContent =
i18nFmt('After %N% flips — — Kelly %v%%: %fmt% | Under-bet %v2%%: %fmt2% | Over-bet %v3%%: %fmt3%', { N: N, v: fK.toFixed(0)*100|0, fmt: fmt(tK[N]), v2: (fU*100).toFixed(0), fmt2: fmt(tU[N]), v3: (fO*100).toFixed(0), fmt3: fmt(tO[N]) });
console.log('Kelly f*=', fK.toFixed(3), '| | final wealth K=', tK[N].toFixed(2),
'U=', tU[N].toFixed(2), 'O=', tO[N].toFixed(2));
}
function drawChart(tK, tU, tO, N, fK) {
const W = canvas.width, H = canvas.height;
const pad = { top: 20, right: 20, bottom: 30, left: 52 };
ctx.clearRect(0, 0, W, H);
// Determine y range in log space
const allVals = [...tK, ...tU, ...tO].filter(v => v > 0);
const logMin = Math.min(...allVals.map(Math.log10));
const logMax = Math.max(...allVals.map(Math.log10));
const yPad = (logMax - logMin) * 0.08 || 0.5;
const yLo = logMin - yPad;
const yHi = logMax + yPad;
function xPx(i) { return pad.left + (i / N) * (W - pad.left - pad.right); }
function yPx(val) {
const lv = Math.log10(Math.max(val, 1e-9));
return pad.top + (1 - (lv - yLo) / (yHi - yLo)) * (H - pad.top - pad.bottom);
}
// Grid lines at powers of 10 and halves
ctx.strokeStyle = '#e0e6ed'; ctx.lineWidth = 1;
const ticks = [];
for (let e = Math.floor(yLo); e <= Math.ceil(yHi) + 1; e++) {
ticks.push(Math.pow(10, e));
ticks.push(Math.pow(10, e) * 3);
}
for (const t of ticks) {
const y = yPx(t);
if (y < pad.top || y > H - pad.bottom) continue;
ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(W - pad.right, y); ctx.stroke();
ctx.fillStyle = '#8899aa'; ctx.font = '10px system-ui'; ctx.textAlign = 'right';
const label = t >= 1000 ? (t/1000).toFixed(0)+'k' : t >= 1 ? t.toFixed(t<2?1:0) : t.toFixed(2);
ctx.fillText(label+'×', pad.left - 4, y + 3.5);
}
// Baseline at 1×
const y1 = yPx(1);
ctx.strokeStyle = '#b0bec8'; ctx.lineWidth = 1; ctx.setLineDash([4,3]);
ctx.beginPath(); ctx.moveTo(pad.left, y1); ctx.lineTo(W-pad.right, y1); ctx.stroke();
ctx.setLineDash([]);
// X axis labels
ctx.fillStyle = '#8899aa'; ctx.font = '10px system-ui'; ctx.textAlign = 'center';
for (let i = 0; i <= N; i += Math.round(N/4)) {
ctx.fillText(i, xPx(i), H - pad.bottom + 14);
}
// Draw each trajectory
function drawLine(traj, color) {
ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.setLineDash([]);
ctx.beginPath();
for (let i = 0; i <= N; i++) {
const x = xPx(i), y = yPx(traj[i]);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
}
drawLine(tO, '#e63946'); // over-bet first (behind)
drawLine(tU, '#e9c46a'); // under-bet
drawLine(tK, '#2a9d8f'); // Kelly on top
}
simulate();
Hit Run to play out a fresh sequence. Notice how the overbetter's curve collapses — even when it briefly overtakes Kelly, it eventually ruins itself. Hit Run many times to build intuition: Kelly wins in the long run almost every time, while the overbetter oscillates wildly and trends downward.
Comments
Loading comments...