The demo below shows a simulated daily return distribution for a hypothetical portfolio. Returns have been negated so losses appear on the positive side of the axis — positive numbers are bad days.
<!-- VaR demo — confidence slider + distribution selector + resample button -->
<div class="controls">
<label for="conf">Confidence level: <strong id="conf-val">95%</strong></label>
<input type="range" id="conf" min="80" max="99" value="95" step="1">
<label for="dist-select">Distribution:</label>
<select id="dist-select">
<option value="normal">Normal returns</option>
<option value="skewed">Right-skewed (rare large losses)</option>
<option value="heavy">Heavy-tailed (extreme events)</option>
</select>
<button id="resample" type="button">Resample</button>
</div>
<canvas id="chart" width="560" height="280"></canvas>
<div class="stats" id="stats"></div>
<p class="note">Red bars = tail losses beyond VaR. VaR is blind to how large they get.</p>
/* Styles for the VaR distribution chart demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem .9rem; align-items: center; margin-bottom: .6rem; }
label { font-size: .88rem; font-weight: 600; }
input[type=range] { width: 160px; cursor: pointer; }
select { font: .88rem system-ui, sans-serif; padding: .2rem .4rem; border: 1px solid #b0bec5; border-radius: 6px; }
button { font: 600 .82rem system-ui, sans-serif; padding: .3rem .75rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
canvas { display: block; width: 100%; max-width: 560px; }
.stats { font-size: .9rem; margin: .4rem 0; min-height: 1.6em; }
.stats span { font-weight: 700; }
.note { font-size: .8rem; color: #555; margin: .3rem 0 0; line-height: 1.4; }
@media (prefers-color-scheme: dark) {
body { color: #ddd; background: #1a1a2e; }
select { background: #222; color: #ddd; border-color: #555; }
button { background: #3a5a8a; border-color: #3a5a8a; }
.note { color: #aaa; }
}
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// VaR demo — draws a histogram, marks the VaR quantile line, computes Expected Shortfall
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { top: 20, right: 20, bottom: 36, left: 46 };
// Deterministic pseudo-random number generator (xorshift) seeded for reproducibility
function rng(seed) {
let s = seed >>> 0;
return function() {
s ^= s << 13; s ^= s >>> 17; s ^= s << 5;
return (s >>> 0) / 4294967296;
};
}
// Box-Muller transform to generate normally-distributed values
function boxMuller(r) {
const u1 = r(), u2 = r();
return Math.sqrt(-2 * Math.log(u1 + 1e-10)) * Math.cos(2 * Math.PI * u2);
}
// Generate synthetic daily losses (positive = loss) for the chosen distribution type
function generateLosses(dist, seed) {
const r = rng(seed);
const n = 500;
const losses = [];
for (let i = 0; i < n; i++) {
let val;
if (dist === 'normal') {
// Gaussian loss centered slightly positive (small daily drift toward losses)
val = boxMuller(r) * 1.5 + 0.3;
} else if (dist === 'skewed') {
// Normal base + occasional jump (models fat-right tail from rare large events)
val = boxMuller(r) * 1.2 + 0.5 + (r() < 0.08 ? r() * 6 : 0);
} else {
// 2% chance of a severe tail event; otherwise near-normal
const u = r();
val = u < 0.02 ? (r() * 12 + 4) : (boxMuller(r) * 1.3 + 0.4);
}
losses.push(val);
}
return losses;
}
// VaR = alpha-quantile of the sorted loss distribution
function computeVaR(losses, conf) {
const sorted = [...losses].sort((a, b) => a - b);
const idx = Math.floor(conf * sorted.length);
return sorted[Math.min(idx, sorted.length - 1)];
}
// Expected Shortfall = average loss in the worst (1-alpha) fraction of days
function computeES(losses, conf) {
const sorted = [...losses].sort((a, b) => a - b);
const idx = Math.floor(conf * sorted.length);
const tail = sorted.slice(idx);
if (!tail.length) return sorted[sorted.length - 1];
return tail.reduce((a, b) => a + b, 0) / tail.length;
}
// Build a fixed-bin histogram from the loss sample
function buildHistogram(losses, bins) {
const min = Math.min(...losses), max = Math.max(...losses);
const bw = (max - min) / bins;
const counts = new Array(bins).fill(0);
for (const l of losses) {
let b = Math.floor((l - min) / bw);
if (b >= bins) b = bins - 1;
counts[b]++;
}
return { counts, min, max, bw };
}
let currentSeed = 42;
let losses = generateLosses('normal', currentSeed);
function draw() {
const conf = parseInt(document.getElementById('conf').value) / 100;
const var95 = computeVaR(losses, conf);
const es = computeES(losses, conf);
const pct = Math.round(conf * 100);
const { counts, min, max, bw } = buildHistogram(losses, 40);
const maxCount = Math.max(...counts);
ctx.clearRect(0, 0, W, H);
const plotW = W - PAD.left - PAD.right;
const plotH = H - PAD.top - PAD.bottom;
// Shade the tail region beyond VaR in light red
const varX = PAD.left + ((var95 - min) / (max - min)) * plotW;
ctx.fillStyle = 'rgba(231,76,60,0.18)';
ctx.fillRect(varX, PAD.top, W - PAD.right - varX, plotH);
// Draw histogram bars, coloring tail bars red
for (let i = 0; i < counts.length; i++) {
const x = PAD.left + (i / counts.length) * plotW;
const bRight = PAD.left + ((i + 1) / counts.length) * plotW;
const barH = (counts[i] / maxCount) * plotH;
const y = PAD.top + plotH - barH;
const barCenter = min + (i + 0.5) * bw;
ctx.fillStyle = barCenter > var95 ? '#e74c3c' : '#3a7abf';
ctx.fillRect(x + 1, y, bRight - x - 2, barH);
}
// Draw the vertical VaR threshold line
ctx.strokeStyle = '#c0392b';
ctx.lineWidth = 2;
ctx.setLineDash([5, 3]);
ctx.beginPath();
ctx.moveTo(varX, PAD.top);
ctx.lineTo(varX, PAD.top + plotH);
ctx.stroke();
ctx.setLineDash([]);
// Label the VaR line with the confidence percentage
ctx.fillStyle = '#c0392b';
ctx.font = 'bold 11px system-ui, sans-serif';
const labelX = varX + 4;
ctx.fillText('VaR ' + pct + '%', labelX > W - 70 ? varX - 56 : labelX, PAD.top + 14);
// Draw chart axes
ctx.strokeStyle = '#aaa';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.left, PAD.top);
ctx.lineTo(PAD.left, PAD.top + plotH);
ctx.lineTo(PAD.left + plotW, PAD.top + plotH);
ctx.stroke();
// Draw x-axis tick labels (loss percentage values)
ctx.fillStyle = '#666';
ctx.font = '10px system-ui, sans-serif';
ctx.textAlign = 'center';
for (let t = 0; t <= 4; t++) {
const val = min + (t / 4) * (max - min);
const tx = PAD.left + (t / 4) * plotW;
ctx.fillText(val.toFixed(1) + '%', tx, PAD.top + plotH + 14);
}
ctx.textAlign = 'left';
// Draw rotated y-axis label
ctx.save();
ctx.translate(12, PAD.top + plotH / 2);
ctx.rotate(-Math.PI / 2);
ctx.textAlign = 'center';
ctx.fillStyle = '#888';
ctx.font = '10px system-ui, sans-serif';
ctx.fillText('Frequency', 0, 0);
ctx.restore();
// Update the stats text below the chart
const statsEl = document.getElementById('stats');
statsEl.innerHTML = i18nFmt('VaR<sub>%pct%</sub> = <span>%var%%</span> | Expected Shortfall = <span>%es%%</span>', {
pct: pct,
var: var95.toFixed(2),
es: es.toFixed(2)
});
document.getElementById('conf-val').textContent = pct + '%';
}
document.getElementById('conf').addEventListener('input', draw);
document.getElementById('dist-select').addEventListener('change', function() {
losses = generateLosses(this.value, currentSeed);
draw();
});
document.getElementById('resample').addEventListener('click', function() {
currentSeed = (currentSeed * 1664525 + 1013904223) >>> 0;
const dist = document.getElementById('dist-select').value;
losses = generateLosses(dist, currentSeed);
draw();
});
draw();
<!-- VaR demo — confidence slider + distribution selector + resample button -->
<div class="controls">
<label for="conf">Confidence level: <strong id="conf-val">95%</strong></label>
<input type="range" id="conf" min="80" max="99" value="95" step="1">
<label for="dist-select">Distribution:</label>
<select id="dist-select">
<option value="normal">Normal returns</option>
<option value="skewed">Right-skewed (rare large losses)</option>
<option value="heavy">Heavy-tailed (extreme events)</option>
</select>
<button id="resample" type="button">Resample</button>
</div>
<canvas id="chart" width="560" height="280"></canvas>
<div class="stats" id="stats"></div>
<p class="note">Red bars = tail losses beyond VaR. VaR is blind to how large they get.</p>
/* Styles for the VaR distribution chart demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem .9rem; align-items: center; margin-bottom: .6rem; }
label { font-size: .88rem; font-weight: 600; }
input[type=range] { width: 160px; cursor: pointer; }
select { font: .88rem system-ui, sans-serif; padding: .2rem .4rem; border: 1px solid #b0bec5; border-radius: 6px; }
button { font: 600 .82rem system-ui, sans-serif; padding: .3rem .75rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
canvas { display: block; width: 100%; max-width: 560px; }
.stats { font-size: .9rem; margin: .4rem 0; min-height: 1.6em; }
.stats span { font-weight: 700; }
.note { font-size: .8rem; color: #555; margin: .3rem 0 0; line-height: 1.4; }
@media (prefers-color-scheme: dark) {
body { color: #ddd; background: #1a1a2e; }
select { background: #222; color: #ddd; border-color: #555; }
button { background: #3a5a8a; border-color: #3a5a8a; }
.note { color: #aaa; }
}
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// VaR demo — draws a histogram, marks the VaR quantile line, computes Expected Shortfall
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { top: 20, right: 20, bottom: 36, left: 46 };
// Deterministic pseudo-random number generator (xorshift) seeded for reproducibility
function rng(seed) {
let s = seed >>> 0;
return function() {
s ^= s << 13; s ^= s >>> 17; s ^= s << 5;
return (s >>> 0) / 4294967296;
};
}
// Box-Muller transform to generate normally-distributed values
function boxMuller(r) {
const u1 = r(), u2 = r();
return Math.sqrt(-2 * Math.log(u1 + 1e-10)) * Math.cos(2 * Math.PI * u2);
}
// Generate synthetic daily losses (positive = loss) for the chosen distribution type
function generateLosses(dist, seed) {
const r = rng(seed);
const n = 500;
const losses = [];
for (let i = 0; i < n; i++) {
let val;
if (dist === 'normal') {
// Gaussian loss centered slightly positive (small daily drift toward losses)
val = boxMuller(r) * 1.5 + 0.3;
} else if (dist === 'skewed') {
// Normal base + occasional jump (models fat-right tail from rare large events)
val = boxMuller(r) * 1.2 + 0.5 + (r() < 0.08 ? r() * 6 : 0);
} else {
// 2% chance of a severe tail event; otherwise near-normal
const u = r();
val = u < 0.02 ? (r() * 12 + 4) : (boxMuller(r) * 1.3 + 0.4);
}
losses.push(val);
}
return losses;
}
// VaR = alpha-quantile of the sorted loss distribution
function computeVaR(losses, conf) {
const sorted = [...losses].sort((a, b) => a - b);
const idx = Math.floor(conf * sorted.length);
return sorted[Math.min(idx, sorted.length - 1)];
}
// Expected Shortfall = average loss in the worst (1-alpha) fraction of days
function computeES(losses, conf) {
const sorted = [...losses].sort((a, b) => a - b);
const idx = Math.floor(conf * sorted.length);
const tail = sorted.slice(idx);
if (!tail.length) return sorted[sorted.length - 1];
return tail.reduce((a, b) => a + b, 0) / tail.length;
}
// Build a fixed-bin histogram from the loss sample
function buildHistogram(losses, bins) {
const min = Math.min(...losses), max = Math.max(...losses);
const bw = (max - min) / bins;
const counts = new Array(bins).fill(0);
for (const l of losses) {
let b = Math.floor((l - min) / bw);
if (b >= bins) b = bins - 1;
counts[b]++;
}
return { counts, min, max, bw };
}
let currentSeed = 42;
let losses = generateLosses('normal', currentSeed);
function draw() {
const conf = parseInt(document.getElementById('conf').value) / 100;
const var95 = computeVaR(losses, conf);
const es = computeES(losses, conf);
const pct = Math.round(conf * 100);
const { counts, min, max, bw } = buildHistogram(losses, 40);
const maxCount = Math.max(...counts);
ctx.clearRect(0, 0, W, H);
const plotW = W - PAD.left - PAD.right;
const plotH = H - PAD.top - PAD.bottom;
// Shade the tail region beyond VaR in light red
const varX = PAD.left + ((var95 - min) / (max - min)) * plotW;
ctx.fillStyle = 'rgba(231,76,60,0.18)';
ctx.fillRect(varX, PAD.top, W - PAD.right - varX, plotH);
// Draw histogram bars, coloring tail bars red
for (let i = 0; i < counts.length; i++) {
const x = PAD.left + (i / counts.length) * plotW;
const bRight = PAD.left + ((i + 1) / counts.length) * plotW;
const barH = (counts[i] / maxCount) * plotH;
const y = PAD.top + plotH - barH;
const barCenter = min + (i + 0.5) * bw;
ctx.fillStyle = barCenter > var95 ? '#e74c3c' : '#3a7abf';
ctx.fillRect(x + 1, y, bRight - x - 2, barH);
}
// Draw the vertical VaR threshold line
ctx.strokeStyle = '#c0392b';
ctx.lineWidth = 2;
ctx.setLineDash([5, 3]);
ctx.beginPath();
ctx.moveTo(varX, PAD.top);
ctx.lineTo(varX, PAD.top + plotH);
ctx.stroke();
ctx.setLineDash([]);
// Label the VaR line with the confidence percentage
ctx.fillStyle = '#c0392b';
ctx.font = 'bold 11px system-ui, sans-serif';
const labelX = varX + 4;
ctx.fillText('VaR ' + pct + '%', labelX > W - 70 ? varX - 56 : labelX, PAD.top + 14);
// Draw chart axes
ctx.strokeStyle = '#aaa';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.left, PAD.top);
ctx.lineTo(PAD.left, PAD.top + plotH);
ctx.lineTo(PAD.left + plotW, PAD.top + plotH);
ctx.stroke();
// Draw x-axis tick labels (loss percentage values)
ctx.fillStyle = '#666';
ctx.font = '10px system-ui, sans-serif';
ctx.textAlign = 'center';
for (let t = 0; t <= 4; t++) {
const val = min + (t / 4) * (max - min);
const tx = PAD.left + (t / 4) * plotW;
ctx.fillText(val.toFixed(1) + '%', tx, PAD.top + plotH + 14);
}
ctx.textAlign = 'left';
// Draw rotated y-axis label
ctx.save();
ctx.translate(12, PAD.top + plotH / 2);
ctx.rotate(-Math.PI / 2);
ctx.textAlign = 'center';
ctx.fillStyle = '#888';
ctx.font = '10px system-ui, sans-serif';
ctx.fillText('Frequency', 0, 0);
ctx.restore();
// Update the stats text below the chart
const statsEl = document.getElementById('stats');
statsEl.innerHTML = i18nFmt('VaR<sub>%pct%</sub> = <span>%var%%</span> | Expected Shortfall = <span>%es%%</span>', {
pct: pct,
var: var95.toFixed(2),
es: es.toFixed(2)
});
document.getElementById('conf-val').textContent = pct + '%';
}
document.getElementById('conf').addEventListener('input', draw);
document.getElementById('dist-select').addEventListener('change', function() {
losses = generateLosses(this.value, currentSeed);
draw();
});
document.getElementById('resample').addEventListener('click', function() {
currentSeed = (currentSeed * 1664525 + 1013904223) >>> 0;
const dist = document.getElementById('dist-select').value;
losses = generateLosses(dist, currentSeed);
draw();
});
draw();
Comments
Loading comments...