Variance-Covariance VaR for a Multi-Asset Portfolio
Matrix-form portfolio VaR and ES for a linear portfolio of four asset classes with editable positions, volatilities, and correlations
For a linear portfolio with \(n\) positions, the dollar change in value is \(\Delta P = \sum_{i=1}^{n} \delta_i \, \Delta x_i\), where \(\delta_i\) is the dollar sensitivity of position \(i\) to risk factor \(i\) and \(\Delta x_i\) is the proportional change in that risk factor. Assuming the factor changes are jointly normal with covariance matrix \(\Sigma_{t+1}\), the portfolio variance is
where \(\delta = (\delta_1, \dots, \delta_n)^{\top}\) is the column vector of dollar sensitivities, \(\sigma_{i,t+1}\) is the one-day-ahead standard deviation of factor \(i\), and \(\rho_{ij,t+1}\) is the correlation between factors \(i\) and \(j\). The \(T\)-trading-day VaR and ES at tail probability \(p\) follow from the standard-normal quantile (Hull 2023, chap. 13)
where \(\sigma_{P,t+1} = \sqrt{\sigma_{P,t+1}^{2}}\) is the one-day portfolio standard deviation, \(\Phi_p^{-1}\) is the \(p\)-quantile of the standard normal distribution, and \(\phi(\cdot)\) is the standard normal density.
The defaults below describe a mixed portfolio of equities, government bonds, gold, and a foreign-exchange exposure. All risk measures are dollar amounts.
Tip
How to experiment
Start from the default allocation and change one correlation at a time. Drive the equity-bond correlation from the defensive \(-0.3\) towards \(+0.8\): the portfolio volatility jumps as the hedge breaks down. Then flip the sign of a position (short the S&P) and watch both correlations and the diversification benefit flip as well. The correlation matrix is validated: an entry that would violate positive semi-definiteness is highlighted.
normalPDF = x =>Math.exp(-x * x /2) /Math.sqrt(2*Math.PI)
viewof vcHorizon = Inputs.range([1,20], { label:"Horizon T (trading days)",step:1,value:10 })
viewof vcTail = Inputs.range([0.1,10], { label:"Tail probability p (%)",step:0.1,value:1.0 })
vcNames = ["Equities","Bonds","Gold","EUR/USD"]
vcCore = {const delta = [vcPosSPX, vcPosBond, vcPosGold, vcPosEUR].map(x => x *1e6)const sigDaily = [vcSig1, vcSig2, vcSig3, vcSig4].map(x => x /100)const rho = [ [1, vcRho12, vcRho13, vcRho14], [vcRho12,1, vcRho23, vcRho24], [vcRho13, vcRho23,1, vcRho34], [vcRho14, vcRho24, vcRho34,1 ] ]const n =4const cov =Array.from({length: n}, () =>newArray(n).fill(0))for (let i =0; i < n; i++)for (let j =0; j < n; j++) cov[i][j] = sigDaily[i] * sigDaily[j] * rho[i][j]// Portfolio variance (daily)let pv =0for (let i =0; i < n; i++)for (let j =0; j < n; j++) pv += delta[i] * delta[j] * cov[i][j]const pSigma =Math.sqrt(Math.max(0, pv))const p = vcTail /100const T = vcHorizonconst zp =Math.abs(qnorm(p))const esMult =normalPDF(qnorm(p)) / pconst pVaR = pSigma *Math.sqrt(T) * zpconst pES = pSigma *Math.sqrt(T) * esMult// Individual dollar VaRs / ESs (stand-alone)const indVaR = delta.map((d, i) =>Math.abs(d) * sigDaily[i] *Math.sqrt(T) * zp)const indES = delta.map((d, i) =>Math.abs(d) * sigDaily[i] *Math.sqrt(T) * esMult)const sumVaR = indVaR.reduce((a, b) => a + b,0)const sumES = indES.reduce((a, b) => a + b,0)// Component VaR: C_i = (δ_i * (Σ δ)_i / σ_P) × √T × z_pconst Sd =newArray(n).fill(0)for (let i =0; i < n; i++)for (let j =0; j < n; j++) Sd[i] += cov[i][j] * delta[j]const compVaR = delta.map((d, i) => pSigma >0? (d * Sd[i] / pSigma) *Math.sqrt(T) * zp :0 )const compES = delta.map((d, i) => pSigma >0? (d * Sd[i] / pSigma) *Math.sqrt(T) * esMult :0 )// Cholesky-based PSD check: attempt L L^T = rho; fails iff any leading// principal minor is not positive definite. Sufficient condition for PSD,// unlike a determinant check which can be positive with two negative// eigenvalues. Also returns the smallest pivot, as a proxy for "distance"// from the PSD boundary.const cholesky = (A) => {const nn = A.lengthconst L =Array.from({length: nn}, () =>newArray(nn).fill(0))let minPivot =Infinityfor (let i =0; i < nn; i++) {for (let j =0; j <= i; j++) {let sum =0for (let k =0; k < j; k++) sum += L[i][k] * L[j][k]if (i === j) {const pivot = A[i][i] - sumif (pivot < minPivot) minPivot = pivotif (pivot <-1e-10) return { ok:false, minPivot } L[i][j] =Math.sqrt(Math.max(pivot,0)) } else { L[i][j] = L[j][j] >1e-12? (A[i][j] - sum) / L[j][j] :0 } } }return { ok:true, minPivot } }const chol =cholesky(rho)return { delta, sigDaily, rho, cov,pVar: pv, pSigma, pVaR, pES, indVaR, indES, sumVaR, sumES, compVaR, compES,divBenVaR: sumVaR - pVaR,divBenES: sumES - pES,divBenVaRPct: sumVaR >0?100* (sumVaR - pVaR) / sumVaR :0,divBenESPct: sumES >0?100* (sumES - pES) / sumES :0, T, zp, esMult, p,psdOK: chol.ok,minPivot: chol.minPivot }}
{if (vcCore.psdOK) returnhtml`<div></div>`returnhtml`<div style="padding:10px 14px; border-left:4px solid #c0392b; background:#fdecea; color:#922; margin:8px 0;"> <strong>⚠ Correlation matrix is not positive semi-definite</strong><br/> <span style="font-size:0.88rem;">The Cholesky decomposition failed (smallest pivot ${fmt(vcCore.minPivot,4)}, should be ≥ 0). The implied covariance matrix is inconsistent: some portfolio variance is negative. Adjust the correlations until the matrix becomes valid.</span> </div>`}
html`<p style="color:#666;font-size:0.85rem;">Grey bars show each position's <strong>stand-alone</strong> VaR, computed as if the asset were the only thing in the portfolio. Blue bars show the <strong>component VaR</strong>, which sums to the total portfolio VaR (Euler decomposition) and reflects the marginal contribution of each position after accounting for correlations. Component VaRs can be negative when a position hedges the rest of the book.</p>`
html`<p style="color:#666;font-size:0.85rem;">Cells show pairwise correlations. Blue indicates positive correlation, red negative. Negative correlations (equity-bond, equity-gold) are the source of most of the diversification benefit in the default allocation.</p>`
{const el =html`<p style="color:#666;font-size:0.85rem;">The covariance matrix is \\(\\sigma_{i,t+1}\\sigma_{j,t+1}\\rho_{ij,t+1}\\) for \\(i,j = 1,\\dots,4\\). Portfolio variance is \\(\\delta^\\top\\Sigma_{t+1}\\delta\\), where \\(\\delta\\) is the dollar position vector. The component VaR column uses Euler's theorem: each column sums to the portfolio VaR and gives the sensitivity of VaR to a proportional change in each position.</p>`if (window.MathJax&& MathJax.typesetPromise) MathJax.typesetPromise([el])return el}
{const c = vcCoreconst el =html`<table class="table" style="width:100%;"> <thead><tr> <th>Measure</th> <th>Portfolio</th> <th>Sum of stand-alone</th> <th>Diversification benefit</th> </tr></thead> <tbody> <tr> <td style="font-weight:500;">σ (daily)</td> <td style="font-weight:700;">${fmtMoney(c.pSigma)}</td> <td>—</td> <td>—</td> </tr> <tr> <td style="font-weight:500;">${c.T}-day VaR (${fmt(c.p*100,1)}%)</td> <td style="font-weight:700;color:#2f71d5;">${fmtMoney(c.pVaR)}</td> <td>${fmtMoney(c.sumVaR)}</td> <td>${fmtMoney(c.divBenVaR)} (${fmt(c.divBenVaRPct,1)}%)</td> </tr> <tr> <td style="font-weight:500;">${c.T}-day ES (${fmt(c.p*100,1)}%)</td> <td style="font-weight:700;color:#2f71d5;">${fmtMoney(c.pES)}</td> <td>${fmtMoney(c.sumES)}</td> <td>${fmtMoney(c.divBenES)} (${fmt(c.divBenESPct,1)}%)</td> </tr> </tbody></table> <p style="color:#666;font-size:0.85rem;">Computed under multivariate normality with tail probability ${fmt(c.p*100,1)}% and a ${c.T}-trading-day horizon. Portfolio σ is the square root of \\(\\delta^\\top\\Sigma_{t+1}\\delta\\).</p>`if (window.MathJax&& MathJax.typesetPromise) MathJax.typesetPromise([el])return el}
Note
Limitations. The variance-covariance approach assumes the \(\Delta x_i\) are jointly normal and that the portfolio is linear in the factors. It underestimates tail risk when returns are fat-tailed (see Stylized facts of asset returns) and breaks down entirely when the portfolio contains options (see the next three pages on delta, gamma, and full-valuation methods).
References
Hull, John. 2023. Risk Management and Financial Institutions. 6th ed. John Wiley & Sons.