Below are three assets, each with its own expected return and risk, and they don't all move together. Drag the target return slider: for every target, the demo computes the mix of weights that achieves it with the least possible risk, and plots that point.
<p class="hint">Three assets with different expected returns and risks that don't all move together. Drag the <b>target return</b>: the demo solves for the mix of weights that hits it with the <b>least risk</b>, and plots the point. Sweep it to draw the <b>efficient frontier</b>.</p>
<div class="wrap">
<canvas id="plot" width="360" height="300"></canvas>
<div class="panel">
<label>Target return: <b id="tval">8.0%</b></label>
<input id="target" type="range" min="40" max="150" value="80">
<div class="row"><span class="dot a"></span>Asset A <span id="wa">0%</span></div>
<div class="row"><span class="dot b"></span>Asset B <span id="wb">0%</span></div>
<div class="row"><span class="dot c"></span>Asset C <span id="wc">0%</span></div>
<div class="risk">Portfolio risk (st.dev): <b id="rval">–</b></div>
<button id="trace" type="button">Trace whole frontier</button>
</div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.wrap { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-start; }
canvas { background: #f7f9fb; border: 1px solid #d6dee6; border-radius: 8px; }
.panel { flex: 1; min-width: 180px; }
label { font-size: .9rem; font-weight: 600; display: block; margin-bottom: .3rem; }
input[type=range] { width: 100%; }
.row { font-size: .92rem; margin: .35rem 0; display: flex; align-items: center; }
.dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; margin-right: .4rem; }
.dot.a { background: #e63946; } .dot.b { background: #1d3557; } .dot.c { background: #2a9d8f; }
.risk { font-size: .95rem; margin: .6rem 0; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; margin-top: .3rem; }
// Markowitz mean-variance optimization for 3 assets.
// Minimize portfolio variance wᵀΣw subject to: μᵀw = r and Σw = 1.
// With equality constraints only, the convex QP has a closed-form solution via
// Lagrange multipliers — a unique global optimum for each target return r.
const mu = [0.06, 0.10, 0.14]; // expected returns of A, B, C
const sd = [0.08, 0.15, 0.25]; // st.devs
const rho = [[1,0.2,0.0],[0.2,1,0.3],[0.0,0.3,1]]; // correlations
const S = [[0,0,0],[0,0,0],[0,0,0]]; // covariance matrix Σ
for(let i=0;i<3;i++)for(let j=0;j<3;j++) S[i][j]=rho[i][j]*sd[i]*sd[j];
// ----- small linear algebra (3x3) -----
function inv3(m){
const a=m[0][0],b=m[0][1],c=m[0][2],d=m[1][0],e=m[1][1],f=m[1][2],g=m[2][0],h=m[2][1],i=m[2][2];
const A=e*i-f*h,B=-(d*i-f*g),C=d*h-e*g,D=-(b*i-c*h),E=a*i-c*g,F=-(a*h-b*g),G=b*f-c*e,H=-(a*f-c*d),I=a*e-b*d;
const det=a*A+b*B+c*C;
return [[A/det,D/det,G/det],[B/det,E/det,H/det],[C/det,F/det,I/det]];
}
function mv(m,v){ return m.map(row=>row[0]*v[0]+row[1]*v[1]+row[2]*v[2]); }
function dot(a,b){ return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]; }
const Sinv = inv3(S);
const one = [1,1,1];
// Frontier constants: A=1ᵀΣ⁻¹1, B=1ᵀΣ⁻¹μ, C=μᵀΣ⁻¹μ, D=AC−B²
const Si1 = mv(Sinv, one), Simu = mv(Sinv, mu);
const Acon = dot(one, Si1), Bcon = dot(one, Simu), Ccon = dot(mu, Simu);
const Dcon = Acon*Ccon - Bcon*Bcon;
// Min-variance weights for a target return r (closed form).
function weights(r){
const l = (Ccon - Bcon*r)/Dcon; // multiplier for Σw=1
const g = (Acon*r - Bcon)/Dcon; // multiplier for μᵀw=r
// w = Σ⁻¹(l·1 + g·μ)
const rhs = [l*one[0]+g*mu[0], l*one[1]+g*mu[1], l*one[2]+g*mu[2]];
return mv(Sinv, rhs);
}
function variance(w){ // wᵀΣw
let v=0; for(let i=0;i<3;i++)for(let j=0;j<3;j++) v+=w[i]*S[i][j]*w[j]; return v;
}
const ctx = document.getElementById('plot').getContext('2d');
const W=360,H=300, PADL=44, PADB=34;
// axes scales: x = risk(st.dev) 0..0.30, y = return 0.04..0.16
const xMin=0, xMax=0.30, yMin=0.04, yMax=0.16;
const px = x => PADL + (x-xMin)/(xMax-xMin)*(W-PADL-12);
const py = y => H-PADB - (y-yMin)/(yMax-yMin)*(H-PADB-12);
function axes(){
ctx.clearRect(0,0,W,H);
ctx.strokeStyle='#aab4c0'; ctx.lineWidth=1; ctx.beginPath();
ctx.moveTo(PADL,12); ctx.lineTo(PADL,H-PADB); ctx.lineTo(W-12,H-PADB); ctx.stroke();
ctx.fillStyle='#556'; ctx.font='11px system-ui';
ctx.fillText('return →', 6, 20); ctx.fillText('risk (st.dev) →', W-110, H-10);
// the three assets as colored dots
const cols=['#e63946','#1d3557','#2a9d8f'], labs=['A','B','C'];
for(let i=0;i<3;i++){ ctx.fillStyle=cols[i]; ctx.beginPath();
ctx.arc(px(sd[i]),py(mu[i]),5,0,7); ctx.fill();
ctx.fillStyle='#333'; ctx.fillText(labs[i], px(sd[i])+7, py(mu[i])+3); }
}
let drawn=false;
function frontier(){
ctx.strokeStyle='#7a8aa0'; ctx.lineWidth=2; ctx.beginPath();
let first=true;
for(let r=yMin; r<=yMax+1e-9; r+=0.002){
const w=weights(r); const risk=Math.sqrt(Math.max(variance(w),0));
const X=px(risk), Y=py(r);
if(first){ctx.moveTo(X,Y);first=false;} else ctx.lineTo(X,Y);
}
ctx.stroke(); drawn=true;
}
function update(){
const r = (+document.getElementById('target').value)/1000;
const w = weights(r); const risk=Math.sqrt(Math.max(variance(w),0));
axes(); if(drawn) frontier();
// current point
ctx.fillStyle='#e9a000'; ctx.beginPath(); ctx.arc(px(risk),py(r),6,0,7); ctx.fill();
ctx.strokeStyle='#9a6b00'; ctx.stroke();
document.getElementById('tval').textContent=(r*100).toFixed(1)+'%';
document.getElementById('wa').textContent=(w[0]*100).toFixed(0)+'%';
document.getElementById('wb').textContent=(w[1]*100).toFixed(0)+'%';
document.getElementById('wc').textContent=(w[2]*100).toFixed(0)+'%';
document.getElementById('rval').textContent=(risk*100).toFixed(1)+'%';
console.log('target',(r*100).toFixed(1)+'%','weights',w.map(x=>x.toFixed(2)).join('/'),'risk',(risk*100).toFixed(2)+'%');
}
document.getElementById('target').addEventListener('input',update);
document.getElementById('trace').addEventListener('click',()=>{ drawn=true; update(); });
axes(); update();
<p class="hint">Three assets with different expected returns and risks that don't all move together. Drag the <b>target return</b>: the demo solves for the mix of weights that hits it with the <b>least risk</b>, and plots the point. Sweep it to draw the <b>efficient frontier</b>.</p>
<div class="wrap">
<canvas id="plot" width="360" height="300"></canvas>
<div class="panel">
<label>Target return: <b id="tval">8.0%</b></label>
<input id="target" type="range" min="40" max="150" value="80">
<div class="row"><span class="dot a"></span>Asset A <span id="wa">0%</span></div>
<div class="row"><span class="dot b"></span>Asset B <span id="wb">0%</span></div>
<div class="row"><span class="dot c"></span>Asset C <span id="wc">0%</span></div>
<div class="risk">Portfolio risk (st.dev): <b id="rval">–</b></div>
<button id="trace" type="button">Trace whole frontier</button>
</div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.wrap { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-start; }
canvas { background: #f7f9fb; border: 1px solid #d6dee6; border-radius: 8px; }
.panel { flex: 1; min-width: 180px; }
label { font-size: .9rem; font-weight: 600; display: block; margin-bottom: .3rem; }
input[type=range] { width: 100%; }
.row { font-size: .92rem; margin: .35rem 0; display: flex; align-items: center; }
.dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; margin-right: .4rem; }
.dot.a { background: #e63946; } .dot.b { background: #1d3557; } .dot.c { background: #2a9d8f; }
.risk { font-size: .95rem; margin: .6rem 0; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; margin-top: .3rem; }
// Markowitz mean-variance optimization for 3 assets.
// Minimize portfolio variance wᵀΣw subject to: μᵀw = r and Σw = 1.
// With equality constraints only, the convex QP has a closed-form solution via
// Lagrange multipliers — a unique global optimum for each target return r.
const mu = [0.06, 0.10, 0.14]; // expected returns of A, B, C
const sd = [0.08, 0.15, 0.25]; // st.devs
const rho = [[1,0.2,0.0],[0.2,1,0.3],[0.0,0.3,1]]; // correlations
const S = [[0,0,0],[0,0,0],[0,0,0]]; // covariance matrix Σ
for(let i=0;i<3;i++)for(let j=0;j<3;j++) S[i][j]=rho[i][j]*sd[i]*sd[j];
// ----- small linear algebra (3x3) -----
function inv3(m){
const a=m[0][0],b=m[0][1],c=m[0][2],d=m[1][0],e=m[1][1],f=m[1][2],g=m[2][0],h=m[2][1],i=m[2][2];
const A=e*i-f*h,B=-(d*i-f*g),C=d*h-e*g,D=-(b*i-c*h),E=a*i-c*g,F=-(a*h-b*g),G=b*f-c*e,H=-(a*f-c*d),I=a*e-b*d;
const det=a*A+b*B+c*C;
return [[A/det,D/det,G/det],[B/det,E/det,H/det],[C/det,F/det,I/det]];
}
function mv(m,v){ return m.map(row=>row[0]*v[0]+row[1]*v[1]+row[2]*v[2]); }
function dot(a,b){ return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]; }
const Sinv = inv3(S);
const one = [1,1,1];
// Frontier constants: A=1ᵀΣ⁻¹1, B=1ᵀΣ⁻¹μ, C=μᵀΣ⁻¹μ, D=AC−B²
const Si1 = mv(Sinv, one), Simu = mv(Sinv, mu);
const Acon = dot(one, Si1), Bcon = dot(one, Simu), Ccon = dot(mu, Simu);
const Dcon = Acon*Ccon - Bcon*Bcon;
// Min-variance weights for a target return r (closed form).
function weights(r){
const l = (Ccon - Bcon*r)/Dcon; // multiplier for Σw=1
const g = (Acon*r - Bcon)/Dcon; // multiplier for μᵀw=r
// w = Σ⁻¹(l·1 + g·μ)
const rhs = [l*one[0]+g*mu[0], l*one[1]+g*mu[1], l*one[2]+g*mu[2]];
return mv(Sinv, rhs);
}
function variance(w){ // wᵀΣw
let v=0; for(let i=0;i<3;i++)for(let j=0;j<3;j++) v+=w[i]*S[i][j]*w[j]; return v;
}
const ctx = document.getElementById('plot').getContext('2d');
const W=360,H=300, PADL=44, PADB=34;
// axes scales: x = risk(st.dev) 0..0.30, y = return 0.04..0.16
const xMin=0, xMax=0.30, yMin=0.04, yMax=0.16;
const px = x => PADL + (x-xMin)/(xMax-xMin)*(W-PADL-12);
const py = y => H-PADB - (y-yMin)/(yMax-yMin)*(H-PADB-12);
function axes(){
ctx.clearRect(0,0,W,H);
ctx.strokeStyle='#aab4c0'; ctx.lineWidth=1; ctx.beginPath();
ctx.moveTo(PADL,12); ctx.lineTo(PADL,H-PADB); ctx.lineTo(W-12,H-PADB); ctx.stroke();
ctx.fillStyle='#556'; ctx.font='11px system-ui';
ctx.fillText('return →', 6, 20); ctx.fillText('risk (st.dev) →', W-110, H-10);
// the three assets as colored dots
const cols=['#e63946','#1d3557','#2a9d8f'], labs=['A','B','C'];
for(let i=0;i<3;i++){ ctx.fillStyle=cols[i]; ctx.beginPath();
ctx.arc(px(sd[i]),py(mu[i]),5,0,7); ctx.fill();
ctx.fillStyle='#333'; ctx.fillText(labs[i], px(sd[i])+7, py(mu[i])+3); }
}
let drawn=false;
function frontier(){
ctx.strokeStyle='#7a8aa0'; ctx.lineWidth=2; ctx.beginPath();
let first=true;
for(let r=yMin; r<=yMax+1e-9; r+=0.002){
const w=weights(r); const risk=Math.sqrt(Math.max(variance(w),0));
const X=px(risk), Y=py(r);
if(first){ctx.moveTo(X,Y);first=false;} else ctx.lineTo(X,Y);
}
ctx.stroke(); drawn=true;
}
function update(){
const r = (+document.getElementById('target').value)/1000;
const w = weights(r); const risk=Math.sqrt(Math.max(variance(w),0));
axes(); if(drawn) frontier();
// current point
ctx.fillStyle='#e9a000'; ctx.beginPath(); ctx.arc(px(risk),py(r),6,0,7); ctx.fill();
ctx.strokeStyle='#9a6b00'; ctx.stroke();
document.getElementById('tval').textContent=(r*100).toFixed(1)+'%';
document.getElementById('wa').textContent=(w[0]*100).toFixed(0)+'%';
document.getElementById('wb').textContent=(w[1]*100).toFixed(0)+'%';
document.getElementById('wc').textContent=(w[2]*100).toFixed(0)+'%';
document.getElementById('rval').textContent=(risk*100).toFixed(1)+'%';
console.log('target',(r*100).toFixed(1)+'%','weights',w.map(x=>x.toFixed(2)).join('/'),'risk',(risk*100).toFixed(2)+'%');
}
document.getElementById('target').addEventListener('input',update);
document.getElementById('trace').addEventListener('click',()=>{ drawn=true; update(); });
axes(); update();
Sweep the slider from low to high and the minimum-risk points trace a smooth curve — the efficient frontier. Every portfolio on it is optimal: you cannot get more return without taking on more risk. Notice there's no guessing and no getting stuck: because the problem is convex, there is a single best answer for each target, and the computer finds it instantly. Compare that with the Traveling Salesman Problem, where finding the true optimum can take forever.
Comments
Loading comments...