<!-- CAPM Security Market Line chart: canvas + sliders for rf and market risk premium -->
<div class="controls">
<label>Risk-free rate (rₐ) <span id="rfVal">2%</span>
<input type="range" id="rf" min="0" max="8" step="0.5" value="2">
</label>
<label>Market risk premium (rₘ − rₐ) <span id="mrpVal">6%</span>
<input type="range" id="mrp" min="2" max="12" step="0.5" value="6">
</label>
</div>
<canvas id="sml" width="560" height="340"></canvas>
<div id="legend" class="legend"></div>
<div id="info" class="info"></div>
/* Layout: controls above canvas, legend below */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: #fff; }
.controls { display: flex; flex-wrap: wrap; gap: .7rem 1.4rem; margin-bottom: .8rem; }
label { font-size: .85rem; color: #444; display: flex; flex-direction: column; gap: .15rem; }
label span { font-weight: 700; color: #1d3557; }
input[type=range] { width: 140px; accent-color: #1d3557; }
canvas { border: 1px solid #dde3ea; border-radius: 8px; max-width: 100%; display: block; }
.legend { display: flex; flex-wrap: wrap; gap: .4rem .8rem; margin-top: .5rem; font-size: .8rem; }
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 4px; }
.info { min-height: 1.2em; font-size: .82rem; color: #555; margin-top: .35rem; font-style: italic; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Sample assets: beta values span 0 to 1.9; two 'special' assets sit off the SML (alpha != 0) */
const ASSETS = [
{ name: 'T-Bill', beta: 0.00, color: '#2a9d8f' },
{ name: 'Utility', beta: 0.45, color: '#457b9d' },
{ name: 'Consumer', beta: 0.80, color: '#1d3557' },
{ name: 'Market', beta: 1.00, color: '#6c757d' },
{ name: 'Tech', beta: 1.40, color: '#e9c46a' },
{ name: 'Biotech',beta: 1.90, color: '#e76f51' },
{ name: 'Above SML', beta: 1.10, color: '#0a7d33', special: 'above' },
{ name: 'Below SML', beta: 0.70, color: '#c92f3c', special: 'below' }
];
const canvas = document.getElementById('sml');
const ctx = canvas.getContext('2d');
const rfSlider = document.getElementById('rf');
const mrpSlider = document.getElementById('mrp');
const rfVal = document.getElementById('rfVal');
const mrpVal = document.getElementById('mrpVal');
const legend = document.getElementById('legend');
const info = document.getElementById('info');
/* chart drawing dimensions with padding for axes */
const PAD = { left: 54, right: 20, top: 20, bottom: 44 };
const W = canvas.width, H = canvas.height;
const CW = W - PAD.left - PAD.right;
const CH = H - PAD.top - PAD.bottom;
/* helpers to map beta and return% to canvas pixels */
function toX(beta) { return PAD.left + (beta / 2.5) * CW; }
function toY(ret) { return PAD.top + CH - (ret / 22) * CH; }
function capm(rf, mrp, beta) { return rf + beta * mrp; }
function draw() {
const rf = parseFloat(rfSlider.value);
const mrp = parseFloat(mrpSlider.value);
rfVal.textContent = rf + '%';
mrpVal.textContent = mrp + '%';
ctx.clearRect(0, 0, W, H);
/* light grid lines */
ctx.strokeStyle = '#e8eef3';
ctx.lineWidth = 1;
for (let b = 0; b <= 2.5; b += 0.5) {
ctx.beginPath(); ctx.moveTo(toX(b), PAD.top); ctx.lineTo(toX(b), PAD.top + CH); ctx.stroke();
}
for (let r = 0; r <= 20; r += 4) {
ctx.beginPath(); ctx.moveTo(PAD.left, toY(r)); ctx.lineTo(PAD.left + CW, toY(r)); ctx.stroke();
}
/* x and y axes */
ctx.strokeStyle = '#adb5bd';
ctx.lineWidth = 1.4;
ctx.beginPath(); ctx.moveTo(PAD.left, PAD.top); ctx.lineTo(PAD.left, PAD.top + CH); ctx.stroke();
ctx.beginPath(); ctx.moveTo(PAD.left, PAD.top + CH); ctx.lineTo(PAD.left + CW, PAD.top + CH); ctx.stroke();
/* numeric tick labels */
ctx.fillStyle = '#555';
ctx.font = '11px system-ui, sans-serif';
ctx.textAlign = 'center';
for (let b = 0; b <= 2.5; b += 0.5) {
ctx.fillText(b.toFixed(1), toX(b), PAD.top + CH + 16);
}
ctx.textAlign = 'right';
for (let r = 0; r <= 20; r += 4) {
ctx.fillText(r + '%', PAD.left - 6, toY(r) + 4);
}
ctx.save();
ctx.translate(14, PAD.top + CH / 2);
ctx.rotate(-Math.PI / 2);
ctx.textAlign = 'center';
ctx.fillText('Expected return (%)', 0, 0);
ctx.restore();
ctx.textAlign = 'center';
ctx.fillText('Beta (β)', PAD.left + CW / 2, H - 4);
/* draw the Security Market Line */
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2.2;
ctx.setLineDash([]);
ctx.beginPath();
ctx.moveTo(toX(0), toY(capm(rf, mrp, 0)));
ctx.lineTo(toX(2.5), toY(capm(rf, mrp, 2.5)));
ctx.stroke();
/* label the SML line */
ctx.fillStyle = '#1d3557';
ctx.font = 'bold 11px system-ui, sans-serif';
ctx.textAlign = 'left';
ctx.fillText('SML', toX(2.1), toY(capm(rf, mrp, 2.1)) - 8);
/* draw each asset dot, with optional alpha dashed line */
ASSETS.forEach(a => {
const smlReturn = capm(rf, mrp, a.beta);
/* expected return on SML at this beta */
const dispReturn = a.special ? smlReturn + (a.special === 'above' ? 2.5 : -2.5) : smlReturn;
const x = toX(a.beta);
const y = toY(dispReturn);
/* dashed vertical line shows alpha (distance above/below SML) */
if (a.special) {
ctx.strokeStyle = a.color;
ctx.lineWidth = 1.2;
ctx.setLineDash([4, 3]);
ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x, toY(smlReturn)); ctx.stroke();
ctx.setLineDash([]);
}
/* filled circle for the asset */
ctx.beginPath();
ctx.arc(x, y, 7, 0, Math.PI * 2);
ctx.fillStyle = a.color;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
});
/* build the color legend below the chart */
legend.innerHTML = ASSETS.map(a => {
let note = '';
if (a.special === 'above') note = ' (' + 'positive α' + ')';
if (a.special === 'below') note = ' (' + 'negative α' + ')';
return '<span><span class="dot" style="background:' + a.color + '"></span>' + a.name + note + '</span>';
}).join('');
/* show the SML equation in the info bar */
const rm = rf + mrp;
info.textContent = i18nFmt('SML: E(r) = %rf%% + β × %mrp%% — market return rₘ = %rm%%', { rf: rf.toFixed(1), mrp: mrp.toFixed(1), rm: rm.toFixed(1) });
}
rfSlider.addEventListener('input', draw);
mrpSlider.addEventListener('input', draw);
/* on hover show the asset name, beta, expected return and alpha */
canvas.addEventListener('mousemove', function(e) {
const rect = canvas.getBoundingClientRect();
const mx = (e.clientX - rect.left) * (W / rect.width);
const my = (e.clientY - rect.top) * (H / rect.height);
const rf = parseFloat(rfSlider.value);
const mrp = parseFloat(mrpSlider.value);
let hit = null;
ASSETS.forEach(a => {
const smlReturn = capm(rf, mrp, a.beta);
const dispReturn = a.special ? smlReturn + (a.special === 'above' ? 2.5 : -2.5) : smlReturn;
const dx = mx - toX(a.beta), dy = my - toY(dispReturn);
if (Math.sqrt(dx*dx + dy*dy) < 10) hit = { asset: a, dispReturn, smlReturn };
});
if (hit) {
const alpha = (hit.dispReturn - hit.smlReturn).toFixed(1);
const alphaStr = alpha > 0 ? '+' + alpha : alpha;
info.textContent = i18nFmt('%name% — β = %beta%, E(r) = %ret%%, α = %alpha%%', {
name: hit.asset.name,
beta: hit.asset.beta.toFixed(2),
ret: hit.dispReturn.toFixed(1),
alpha: alphaStr
});
}
});
canvas.addEventListener('mouseleave', function() {
const rf = parseFloat(rfSlider.value);
const mrp = parseFloat(mrpSlider.value);
const rm = rf + mrp;
info.textContent = i18nFmt('SML: E(r) = %rf%% + β × %mrp%% — market return rₘ = %rm%%', { rf: rf.toFixed(1), mrp: mrp.toFixed(1), rm: rm.toFixed(1) });
});
draw();
<!-- CAPM Security Market Line chart: canvas + sliders for rf and market risk premium -->
<div class="controls">
<label>Risk-free rate (rₐ) <span id="rfVal">2%</span>
<input type="range" id="rf" min="0" max="8" step="0.5" value="2">
</label>
<label>Market risk premium (rₘ − rₐ) <span id="mrpVal">6%</span>
<input type="range" id="mrp" min="2" max="12" step="0.5" value="6">
</label>
</div>
<canvas id="sml" width="560" height="340"></canvas>
<div id="legend" class="legend"></div>
<div id="info" class="info"></div>
/* Layout: controls above canvas, legend below */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: #fff; }
.controls { display: flex; flex-wrap: wrap; gap: .7rem 1.4rem; margin-bottom: .8rem; }
label { font-size: .85rem; color: #444; display: flex; flex-direction: column; gap: .15rem; }
label span { font-weight: 700; color: #1d3557; }
input[type=range] { width: 140px; accent-color: #1d3557; }
canvas { border: 1px solid #dde3ea; border-radius: 8px; max-width: 100%; display: block; }
.legend { display: flex; flex-wrap: wrap; gap: .4rem .8rem; margin-top: .5rem; font-size: .8rem; }
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 4px; }
.info { min-height: 1.2em; font-size: .82rem; color: #555; margin-top: .35rem; font-style: italic; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Sample assets: beta values span 0 to 1.9; two 'special' assets sit off the SML (alpha != 0) */
const ASSETS = [
{ name: 'T-Bill', beta: 0.00, color: '#2a9d8f' },
{ name: 'Utility', beta: 0.45, color: '#457b9d' },
{ name: 'Consumer', beta: 0.80, color: '#1d3557' },
{ name: 'Market', beta: 1.00, color: '#6c757d' },
{ name: 'Tech', beta: 1.40, color: '#e9c46a' },
{ name: 'Biotech',beta: 1.90, color: '#e76f51' },
{ name: 'Above SML', beta: 1.10, color: '#0a7d33', special: 'above' },
{ name: 'Below SML', beta: 0.70, color: '#c92f3c', special: 'below' }
];
const canvas = document.getElementById('sml');
const ctx = canvas.getContext('2d');
const rfSlider = document.getElementById('rf');
const mrpSlider = document.getElementById('mrp');
const rfVal = document.getElementById('rfVal');
const mrpVal = document.getElementById('mrpVal');
const legend = document.getElementById('legend');
const info = document.getElementById('info');
/* chart drawing dimensions with padding for axes */
const PAD = { left: 54, right: 20, top: 20, bottom: 44 };
const W = canvas.width, H = canvas.height;
const CW = W - PAD.left - PAD.right;
const CH = H - PAD.top - PAD.bottom;
/* helpers to map beta and return% to canvas pixels */
function toX(beta) { return PAD.left + (beta / 2.5) * CW; }
function toY(ret) { return PAD.top + CH - (ret / 22) * CH; }
function capm(rf, mrp, beta) { return rf + beta * mrp; }
function draw() {
const rf = parseFloat(rfSlider.value);
const mrp = parseFloat(mrpSlider.value);
rfVal.textContent = rf + '%';
mrpVal.textContent = mrp + '%';
ctx.clearRect(0, 0, W, H);
/* light grid lines */
ctx.strokeStyle = '#e8eef3';
ctx.lineWidth = 1;
for (let b = 0; b <= 2.5; b += 0.5) {
ctx.beginPath(); ctx.moveTo(toX(b), PAD.top); ctx.lineTo(toX(b), PAD.top + CH); ctx.stroke();
}
for (let r = 0; r <= 20; r += 4) {
ctx.beginPath(); ctx.moveTo(PAD.left, toY(r)); ctx.lineTo(PAD.left + CW, toY(r)); ctx.stroke();
}
/* x and y axes */
ctx.strokeStyle = '#adb5bd';
ctx.lineWidth = 1.4;
ctx.beginPath(); ctx.moveTo(PAD.left, PAD.top); ctx.lineTo(PAD.left, PAD.top + CH); ctx.stroke();
ctx.beginPath(); ctx.moveTo(PAD.left, PAD.top + CH); ctx.lineTo(PAD.left + CW, PAD.top + CH); ctx.stroke();
/* numeric tick labels */
ctx.fillStyle = '#555';
ctx.font = '11px system-ui, sans-serif';
ctx.textAlign = 'center';
for (let b = 0; b <= 2.5; b += 0.5) {
ctx.fillText(b.toFixed(1), toX(b), PAD.top + CH + 16);
}
ctx.textAlign = 'right';
for (let r = 0; r <= 20; r += 4) {
ctx.fillText(r + '%', PAD.left - 6, toY(r) + 4);
}
ctx.save();
ctx.translate(14, PAD.top + CH / 2);
ctx.rotate(-Math.PI / 2);
ctx.textAlign = 'center';
ctx.fillText('Expected return (%)', 0, 0);
ctx.restore();
ctx.textAlign = 'center';
ctx.fillText('Beta (β)', PAD.left + CW / 2, H - 4);
/* draw the Security Market Line */
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2.2;
ctx.setLineDash([]);
ctx.beginPath();
ctx.moveTo(toX(0), toY(capm(rf, mrp, 0)));
ctx.lineTo(toX(2.5), toY(capm(rf, mrp, 2.5)));
ctx.stroke();
/* label the SML line */
ctx.fillStyle = '#1d3557';
ctx.font = 'bold 11px system-ui, sans-serif';
ctx.textAlign = 'left';
ctx.fillText('SML', toX(2.1), toY(capm(rf, mrp, 2.1)) - 8);
/* draw each asset dot, with optional alpha dashed line */
ASSETS.forEach(a => {
const smlReturn = capm(rf, mrp, a.beta);
/* expected return on SML at this beta */
const dispReturn = a.special ? smlReturn + (a.special === 'above' ? 2.5 : -2.5) : smlReturn;
const x = toX(a.beta);
const y = toY(dispReturn);
/* dashed vertical line shows alpha (distance above/below SML) */
if (a.special) {
ctx.strokeStyle = a.color;
ctx.lineWidth = 1.2;
ctx.setLineDash([4, 3]);
ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x, toY(smlReturn)); ctx.stroke();
ctx.setLineDash([]);
}
/* filled circle for the asset */
ctx.beginPath();
ctx.arc(x, y, 7, 0, Math.PI * 2);
ctx.fillStyle = a.color;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
});
/* build the color legend below the chart */
legend.innerHTML = ASSETS.map(a => {
let note = '';
if (a.special === 'above') note = ' (' + 'positive α' + ')';
if (a.special === 'below') note = ' (' + 'negative α' + ')';
return '<span><span class="dot" style="background:' + a.color + '"></span>' + a.name + note + '</span>';
}).join('');
/* show the SML equation in the info bar */
const rm = rf + mrp;
info.textContent = i18nFmt('SML: E(r) = %rf%% + β × %mrp%% — market return rₘ = %rm%%', { rf: rf.toFixed(1), mrp: mrp.toFixed(1), rm: rm.toFixed(1) });
}
rfSlider.addEventListener('input', draw);
mrpSlider.addEventListener('input', draw);
/* on hover show the asset name, beta, expected return and alpha */
canvas.addEventListener('mousemove', function(e) {
const rect = canvas.getBoundingClientRect();
const mx = (e.clientX - rect.left) * (W / rect.width);
const my = (e.clientY - rect.top) * (H / rect.height);
const rf = parseFloat(rfSlider.value);
const mrp = parseFloat(mrpSlider.value);
let hit = null;
ASSETS.forEach(a => {
const smlReturn = capm(rf, mrp, a.beta);
const dispReturn = a.special ? smlReturn + (a.special === 'above' ? 2.5 : -2.5) : smlReturn;
const dx = mx - toX(a.beta), dy = my - toY(dispReturn);
if (Math.sqrt(dx*dx + dy*dy) < 10) hit = { asset: a, dispReturn, smlReturn };
});
if (hit) {
const alpha = (hit.dispReturn - hit.smlReturn).toFixed(1);
const alphaStr = alpha > 0 ? '+' + alpha : alpha;
info.textContent = i18nFmt('%name% — β = %beta%, E(r) = %ret%%, α = %alpha%%', {
name: hit.asset.name,
beta: hit.asset.beta.toFixed(2),
ret: hit.dispReturn.toFixed(1),
alpha: alphaStr
});
}
});
canvas.addEventListener('mouseleave', function() {
const rf = parseFloat(rfSlider.value);
const mrp = parseFloat(mrpSlider.value);
const rm = rf + mrp;
info.textContent = i18nFmt('SML: E(r) = %rf%% + β × %mrp%% — market return rₘ = %rm%%', { rf: rf.toFixed(1), mrp: mrp.toFixed(1), rm: rm.toFixed(1) });
});
draw();
Comments
Loading comments...