Module 11 · Financial Risk and Performance Statistics Module demo
Portfolio Risk Report
A tear sheet for two quiet funds.
Transcript
46 sentences · select one to jump thereCode lab
Run it yourself
The demo source in one language. Edit it, run TypeScript and Python right here, and compare with the expected output.
/**
* Fintech Math Bootcamp · Module 11 demo · Portfolio Risk Report
* A one-page risk tear sheet for a two-fund portfolio (60% Fund A, 40% Fund B) over twenty synthetic
* trading days, measured against a synthetic benchmark. Each fund on its own is quiet; the report shows
* what happens to the portfolio when the correlation between them flips from negative to positive.
* Lessons 101–110: volatility, downside deviation, drawdown, loss quantiles, VaR, Expected Shortfall,
* beta, Sharpe / Sortino / information ratio, tracking error, covariance matrices and portfolio variance.
* Conventions: simple daily returns, sample SD (n − 1), annualization factor A = 252 trading days,
* loss = −return, loss quantile by linear interpolation between order statistics (type 7).
* Synthetic example; not investment advice.
*/
const A = 252; // declared annualization frequency: trading days per year
const mean = (x: number[]) => x.reduce((s, v) => s + v, 0) / x.length;
// 101 · sample standard deviation (n − 1) and its annualized version σ·√A
export function sampleSD(x: number[]): number {
if (x.length < 2) throw new Error("Need at least two observations");
const m = mean(x);
return Math.sqrt(x.reduce((s, v) => s + (v - m) ** 2, 0) / (x.length - 1));
}
export const annualizedVol = (x: number[]) => sampleSD(x) * Math.sqrt(A);
// 102 · downside deviation below a target: √mean(min(0, r − target)²) over all observations
export function downsideDeviation(x: number[], target = 0): number {
return Math.sqrt(mean(x.map(r => Math.min(0, r - target) ** 2)));
}
// 103 · wealth path, running peak and drawdown = wealth / peak − 1
export function drawdowns(returns: number[], start: number) {
let wealth = start, peak = start;
const values = [wealth], peaks = [peak], dd = [0];
for (const r of returns) {
wealth *= 1 + r;
peak = Math.max(peak, wealth);
values.push(wealth); peaks.push(peak); dd.push(wealth / peak - 1);
}
const maxDrawdown = Math.min(...dd);
return {values, peaks, dd, maxDrawdown, troughDay: dd.indexOf(maxDrawdown)};
}
// 104 · loss quantile with the declared rule: linear interpolation between order statistics (type 7)
export function lossQuantile(sortedLosses: number[], p: number): number {
const h = (sortedLosses.length - 1) * p, i = Math.floor(h);
return i + 1 < sortedLosses.length ? sortedLosses[i] + (h - i) * (sortedLosses[i + 1] - sortedLosses[i]) : sortedLosses[i];
}
// 105 · historical VaR is a loss quantile; 106 · Expected Shortfall averages the losses at or beyond it
export function historicalVaRES(returns: number[], confidence: number) {
const losses = returns.map(r => -r).sort((a, b) => a - b); // loss = −return, ascending
const varLoss = lossQuantile(losses, confidence);
const tail = losses.filter(l => l >= varLoss);
return {losses, varLoss, tail, esLoss: mean(tail), tailShare: tail.length / losses.length};
}
// 107 · beta = Cov(asset, market) / Var(market), both with n − 1
export function beta(asset: number[], market: number[]): number {
const cov = covariance(asset, market), varM = covariance(market, market);
if (varM <= 0) throw new Error("Market variance must be positive");
return cov / varM;
}
// 108 · Sharpe uses excess return over SD; Sortino uses excess over a target divided by downside deviation
export const sharpe = (x: number[], rfDaily: number) => (mean(x) - rfDaily) / sampleSD(x);
export const sortino = (x: number[], target = 0) => (mean(x) - target) / downsideDeviation(x, target);
// 109 · active return on matched days, tracking error = sample SD of active, information ratio = mean / TE
export function activeStats(r: number[], b: number[]) {
if (r.length !== b.length) throw new Error("Returns and benchmark must be matched by day");
const active = r.map((v, i) => v - b[i]);
const te = sampleSD(active);
return {active, meanActive: mean(active), te, informationRatio: mean(active) / te};
}
// 110 · sample covariance, the covariance matrix Σ and portfolio variance wᵀΣw
export function covariance(x: number[], y: number[]): number {
const mx = mean(x), my = mean(y);
return x.reduce((s, v, i) => s + (v - mx) * (y[i] - my), 0) / (x.length - 1);
}
export const correlation = (x: number[], y: number[]) => covariance(x, y) / (sampleSD(x) * sampleSD(y));
export function covMatrix(series: number[][]): number[][] {
return series.map(a => series.map(b => covariance(a, b)));
}
export function portfolioVariance(w: number[], cov: number[][]): number {
return w.reduce((s, wi, i) => s + wi * w.reduce((t, wj, j) => t + cov[i][j] * wj, 0), 0);
}
// the same wᵀΣw with the vols held fixed and only the correlation ρ changed
export function varianceAtRho(w: number[], vol: number[], rho: number): number {
const c = rho * vol[0] * vol[1];
return portfolioVariance(w, [[vol[0] ** 2, c], [c, vol[1] ** 2]]);
}
export function runDemo() {
// twenty synthetic trading days, in percent; days 1–10 the funds offset each other, days 11–20 they move together
const pctA = [0.6, -0.5, 0.8, -0.6, 0.5, -0.3, 0.7, -0.5, 0.6, -0.4, 0.4, -0.8, -0.6, 0.6, -0.9, 0.3, -0.7, 0.5, -0.6, 0.9];
const pctB = [-0.4, 0.7, -0.5, 0.8, -0.3, 0.6, -0.4, 0.7, -0.3, 0.7, 0.5, -0.7, -0.8, 0.5, -0.8, 0.4, -0.6, 0.6, -0.5, 0.8];
const pctM = [0.1, 0.0, 0.1, 0.0, 0.1, 0.0, 0.1, 0.0, 0.1, 0.0, 0.4, -0.6, -0.5, 0.5, -0.7, 0.3, -0.4, 0.5, -0.4, 0.6];
const fundA = pctA.map(v => v / 100), fundB = pctB.map(v => v / 100), bench = pctM.map(v => v / 100);
const w = [0.6, 0.4], exposure = 100000, confidence = 0.9, rfAnnual = 0.02, rfDaily = rfAnnual / A;
const port = fundA.map((a, i) => w[0] * a + w[1] * fundB[i]);
const vol = (x: number[]) => ({daily: sampleSD(x), annual: annualizedVol(x), downside: downsideDeviation(x)});
const dd = drawdowns(port, exposure);
const tail = historicalVaRES(port, confidence);
const act = activeStats(port, bench);
const cov = covMatrix([fundA, fundB]);
const pv = portfolioVariance(w, cov);
const vols = [Math.sqrt(cov[0][0]), Math.sqrt(cov[1][1])];
const half = (x: number[], k: number) => x.slice(k * 10, k * 10 + 10);
const regimes = [0, 1].map(k => {
const a = half(fundA, k), b = half(fundB, k), p = half(port, k);
return {rho: correlation(a, b), volA: annualizedVol(a), volB: annualizedVol(b), volPort: annualizedVol(p), total: p.reduce((g, r) => g * (1 + r), 1) - 1};
});
const rhoGrid = [-1, -0.5, 0, 0.5, 1].map(rho => ({rho, annualVol: Math.sqrt(varianceAtRho(w, vols, rho) * A)}));
return {
setup: {days: port.length, weights: w, exposure, annualization: A, confidence, rfAnnual, rfDaily, target: 0},
series: {fundA, fundB, bench, port},
vol: {fundA: vol(fundA), fundB: vol(fundB), port: vol(port), bench: vol(bench)},
shortfalls: port.map(r => Math.min(0, r)),
downDays: port.filter(r => r < 0).length,
drawdown: {values: dd.values, peaks: dd.peaks, dd: dd.dd, maxDrawdown: dd.maxDrawdown, troughDay: dd.troughDay,
peakDay: dd.values.indexOf(dd.peaks[dd.troughDay]), endValue: dd.values.at(-1)!, maxDrawdownDollars: dd.maxDrawdown * dd.peaks[dd.troughDay]},
tail: {losses: tail.losses, varLoss: tail.varLoss, esLoss: tail.esLoss, tail: tail.tail, tailShare: tail.tailShare,
varDollars: tail.varLoss * exposure, esDollars: tail.esLoss * exposure, worstLoss: tail.losses.at(-1)!},
relative: {
beta: beta(port, bench), betaA: beta(fundA, bench), betaB: beta(fundB, bench),
sharpe: sharpe(port, rfDaily), sortino: sortino(port), sharpeAnnual: sharpe(port, rfDaily) * Math.sqrt(A),
meanDaily: mean(port), active: act.active, meanActive: act.meanActive, te: act.te, teAnnual: act.te * Math.sqrt(A),
informationRatio: act.informationRatio, informationRatioAnnual: act.informationRatio * Math.sqrt(A),
},
diversification: {
cov, corr: correlation(fundA, fundB), portfolioVariance: pv, portfolioVarianceDirect: sampleSD(port) ** 2,
annualVol: Math.sqrt(pv * A), vols, annualVols: vols.map(v => v * Math.sqrt(A)), regimes, rhoGrid,
},
};
}
export const checkedResult = {"setup":{"days":20,"weights":[0.6,0.4],"exposure":100000,"annualization":252,"confidence":0.9,"rfAnnual":0.02,"rfDaily":0.00007936507936507937,"target":0},"series":{"fundA":[0.006,-0.005,0.008,-0.006,0.005,-0.003,0.006999999999999999,-0.005,0.006,-0.004,0.004,-0.008,-0.006,0.006,-0.009000000000000001,0.003,-0.006999999999999999,0.005,-0.006,0.009000000000000001],"fundB":[-0.004,0.006999999999999999,-0.005,0.008,-0.003,0.006,-0.004,0.006999999999999999,-0.003,0.006999999999999999,0.005,-0.006999999999999999,-0.008,0.005,-0.008,0.004,-0.006,0.006,-0.005,0.008],"bench":[0.001,0,0.001,0,0.001,0,0.001,0,0.001,0,0.004,-0.006,-0.005,0.005,-0.006999999999999999,0.003,-0.004,0.005,-0.004,0.006],"port":[0.002,-0.0002000000000000001,0.0027999999999999995,-0.00039999999999999975,0.0018,0.0006000000000000003,0.0026,-0.0002000000000000001,0.0024,0.0004000000000000002,0.004399999999999999,-0.007599999999999999,-0.0068000000000000005,0.0056,-0.0086,0.0034000000000000002,-0.0066,0.0054,-0.0056,0.0086]},"vol":{"fundA":{"daily":0.006299540501204496,"annual":0.1000021052409977,"downside":0.004341658669218483},"fundB":{"daily":0.006160143538997499,"annual":0.09778924706789222,"downside":0.003956008088970496},"port":{"daily":0.0048440957007365925,"annual":0.07689763530687713,"downside":0.003557527231097466},"bench":{"daily":0.00368353377242989,"annual":0.058474285846542476,"downside":0.0026645825188948455}},"shortfalls":[0,-0.0002000000000000001,0,-0.00039999999999999975,0,0,0,-0.0002000000000000001,0,0,0,-0.007599999999999999,-0.0068000000000000005,0,-0.0086,0,-0.0066,0,-0.0056,0],"downDays":8,"drawdown":{"values":[100000,100200,100179.96,100460.463888,100420.27970244481,100601.03620590921,100661.39682763275,100923.11645938459,100902.93183609271,101145.09887249934,101185.55691204833,101630.77336246133,100858.37948490662,100172.54250440926,100733.50874243396,99867.20056724902,100206.74904917767,99545.3845054531,100082.92958178255,99522.46517612456,100378.35837663923],"peaks":[100000,100200,100200,100460.463888,100460.463888,100601.03620590921,100661.39682763275,100923.11645938459,100923.11645938459,101145.09887249934,101185.55691204833,101630.77336246133,101630.77336246133,101630.77336246133,101630.77336246133,101630.77336246133,101630.77336246133,101630.77336246133,101630.77336246133,101630.77336246133,101630.77336246133],"dd":[0,0,-0.00019999999999997797,0,-0.00039999999999995595,0,0,0,-0.00019999999999997797,0,0,0,-0.007600000000000051,-0.014348320000000081,-0.008828670591999987,-0.01735274402490883,-0.014011743354593431,-0.020519265848453094,-0.015230069884034747,-0.020744781492684217,-0.012323186613521386],"maxDrawdown":-0.020744781492684217,"troughDay":19,"peakDay":11,"endValue":100378.35837663923,"maxDrawdownDollars":-2108.308186336772},"tail":{"losses":[-0.0086,-0.0056,-0.0054,-0.004399999999999999,-0.0034000000000000002,-0.0027999999999999995,-0.0026,-0.0024,-0.002,-0.0018,-0.0006000000000000003,-0.0004000000000000002,0.0002000000000000001,0.0002000000000000001,0.00039999999999999975,0.0056,0.0066,0.0068000000000000005,0.007599999999999999,0.0086],"varLoss":0.0068800000000000016,"esLoss":0.0081,"tail":[0.007599999999999999,0.0086],"tailShare":0.1,"varDollars":688.0000000000001,"esDollars":810,"worstLoss":0.0086},"relative":{"beta":1.2963537626066717,"betaA":1.3925523661753298,"betaB":1.1520558572536852,"sharpe":0.024903496563161813,"sortino":0.05621882476449851,"sharpeAnnual":0.3953307520924674,"meanDaily":0.0002,"active":[0.001,-0.0002000000000000001,0.0017999999999999995,-0.00039999999999999975,0.0007999999999999999,0.0006000000000000003,0.0015999999999999999,-0.0002000000000000001,0.0013999999999999998,0.0004000000000000002,0.0003999999999999993,-0.001599999999999999,-0.0018000000000000004,0.0005999999999999998,-0.0016000000000000007,0.0004000000000000002,-0.0026,0.0004000000000000002,-0.0015999999999999999,0.0026],"meanActive":0.00009999999999999996,"te":0.0013618872354586717,"teAnnual":0.02161928963242147,"informationRatio":0.07342751837035967,"informationRatioAnnual":1.1656257179795906},"diversification":{"cov":[[0.00003968421052631579,0.000006473684210526318],[0.000006473684210526318,0.00003794736842105263]],"corr":0.16682141712434242,"portfolioVariance":0.00002346526315789474,"portfolioVarianceDirect":0.00002346526315789474,"annualVol":0.07689763530687713,"vols":[0.006299540501204496,0.006160143538997499],"annualVols":[0.1000021052409977,0.09778924706789222],"regimes":[{"rho":-0.9962622975306197,"volA":0.09360128204250197,"volB":0.09109994511524142,"volPort":0.02010492476981697,"total":0.011855569120483134},{"rho":0.985773126102038,"volA":0.10907428661238176,"volB":0.1058829542466586,"volPort":0.10743131759407963,"total":-0.007977408634621042}],"rhoGrid":[{"rho":-1,"annualVol":0.02088556431744174},{"rho":-0.5,"annualVol":0.052756024634832976},{"rho":0,"annualVol":0.0716253410022194},{"rho":0.5,"annualVol":0.0864706933712071},{"rho":1,"annualVol":0.0991169619717555}]}};
// Run this file directly: npx tsx lessons/11-financial-risk-and-performance-statistics/demo-portfolio-risk-report.ts
if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/").split("/").pop()!)) {
console.log(JSON.stringify(runDemo(), null, 2));
}
Your output
Press Run to execute the code in your browser.
Expected output
{
"setup": {
"days": 20,
"weights": [
0.6,
0.4
],
"exposure": 100000,
"annualization": 252,
"confidence": 0.9,
"rfAnnual": 0.02,
"rfDaily": 0.00007936507936507937,
"target": 0
},
"series": {
"fundA": [
0.006,
-0.005,
0.008,
-0.006,
0.005,
-0.003,
0.006999999999999999,
-0.005,
0.006,
-0.004,
0.004,
-0.008,
-0.006,
0.006,
-0.009000000000000001,
0.003,
-0.006999999999999999,
0.005,
-0.006,
0.009000000000000001
],
"fundB": [
-0.004,
0.006999999999999999,
-0.005,
0.008,
-0.003,
0.006,
-0.004,
0.006999999999999999,
-0.003,
0.006999999999999999,
0.005,
-0.006999999999999999,
-0.008,
0.005,
-0.008,
0.004,
-0.006,
0.006,
-0.005,
0.008
],
"bench": [
0.001,
0,
0.001,
0,
0.001,
0,
0.001,
0,
0.001,
0,
0.004,
-0.006,
-0.005,
0.005,
-0.006999999999999999,
0.003,
-0.004,
0.005,
-0.004,
0.006
],
"port": [
0.002,
-0.0002000000000000001,
0.0027999999999999995,
-0.00039999999999999975,
0.0018,
0.0006000000000000003,
0.0026,
-0.0002000000000000001,
0.0024,
0.0004000000000000002,
0.004399999999999999,
-0.007599999999999999,
-0.0068000000000000005,
0.0056,
-0.0086,
0.0034000000000000002,
-0.0066,
0.0054,
-0.0056,
0.0086
]
},
"vol": {
"fundA": {
"daily": 0.006299540501204496,
"annual": 0.1000021052409977,
"downside": 0.004341658669218483
},
"fundB": {
"daily": 0.006160143538997499,
"annual": 0.09778924706789222,
"downside": 0.003956008088970496
},
"port": {
"daily": 0.0048440957007365925,
"annual": 0.07689763530687713,
"downside": 0.003557527231097466
},
"bench": {
"daily": 0.00368353377242989,
"annual": 0.058474285846542476,
"downside": 0.0026645825188948455
}
},
"shortfalls": [
0,
-0.0002000000000000001,
0,
-0.00039999999999999975,
0,
0,
0,
-0.0002000000000000001,
0,
0,
0,
-0.007599999999999999,
-0.0068000000000000005,
0,
-0.0086,
0,
-0.0066,
0,
-0.0056,
0
],
"downDays": 8,
"drawdown": {
"values": [
100000,
100200,
100179.96,
100460.463888,
100420.27970244481,
100601.03620590921,
100661.39682763275,
100923.11645938459,
100902.93183609271,
101145.09887249934,
101185.55691204833,
101630.77336246133,
100858.37948490662,
100172.54250440926,
100733.50874243396,
99867.20056724902,
100206.74904917767,
99545.3845054531,
100082.92958178255,
99522.46517612456,
100378.35837663923
],
"peaks": [
100000,
100200,
100200,
100460.463888,
100460.463888,
100601.03620590921,
100661.39682763275,
100923.11645938459,
100923.11645938459,
101145.09887249934,
101185.55691204833,
101630.77336246133,
101630.77336246133,
101630.77336246133,
101630.77336246133,
101630.77336246133,
101630.77336246133,
101630.77336246133,
101630.77336246133,
101630.77336246133,
101630.77336246133
],
"dd": [
0,
0,
-0.00019999999999997797,
0,
-0.00039999999999995595,
0,
0,
0,
-0.00019999999999997797,
0,
0,
0,
-0.007600000000000051,
-0.014348320000000081,
-0.008828670591999987,
-0.01735274402490883,
-0.014011743354593431,
-0.020519265848453094,
-0.015230069884034747,
-0.020744781492684217,
-0.012323186613521386
],
"maxDrawdown": -0.020744781492684217,
"troughDay": 19,
"peakDay": 11,
"endValue": 100378.35837663923,
"maxDrawdownDollars": -2108.308186336772
},
"tail": {
"losses": [
-0.0086,
-0.0056,
-0.0054,
-0.004399999999999999,
-0.0034000000000000002,
-0.0027999999999999995,
-0.0026,
-0.0024,
-0.002,
-0.0018,
-0.0006000000000000003,
-0.0004000000000000002,
0.0002000000000000001,
0.0002000000000000001,
0.00039999999999999975,
0.0056,
0.0066,
0.0068000000000000005,
0.007599999999999999,
0.0086
],
"varLoss": 0.0068800000000000016,
"esLoss": 0.0081,
"tail": [
0.007599999999999999,
0.0086
],
"tailShare": 0.1,
"varDollars": 688.0000000000001,
"esDollars": 810,
"worstLoss": 0.0086
},
"relative": {
"beta": 1.2963537626066717,
"betaA": 1.3925523661753298,
"betaB": 1.1520558572536852,
"sharpe": 0.024903496563161813,
"sortino": 0.05621882476449851,
"sharpeAnnual": 0.3953307520924674,
"meanDaily": 0.0002,
"active": [
0.001,
-0.0002000000000000001,
0.0017999999999999995,
-0.00039999999999999975,
0.0007999999999999999,
0.0006000000000000003,
0.0015999999999999999,
-0.0002000000000000001,
0.0013999999999999998,
0.0004000000000000002,
0.0003999999999999993,
-0.001599999999999999,
-0.0018000000000000004,
0.0005999999999999998,
-0.0016000000000000007,
0.0004000000000000002,
-0.0026,
0.0004000000000000002,
-0.0015999999999999999,
0.0026
],
"meanActive": 0.00009999999999999996,
"te": 0.0013618872354586717,
"teAnnual": 0.02161928963242147,
"informationRatio": 0.07342751837035967,
"informationRatioAnnual": 1.1656257179795906
},
"diversification": {
"cov": [
[
0.00003968421052631579,
0.000006473684210526318
],
[
0.000006473684210526318,
0.00003794736842105263
]
],
"corr": 0.16682141712434242,
"portfolioVariance": 0.00002346526315789474,
"portfolioVarianceDirect": 0.00002346526315789474,
"annualVol": 0.07689763530687713,
"vols": [
0.006299540501204496,
0.006160143538997499
],
"annualVols": [
0.1000021052409977,
0.09778924706789222
],
"regimes": [
{
"rho": -0.9962622975306197,
"volA": 0.09360128204250197,
"volB": 0.09109994511524142,
"volPort": 0.02010492476981697,
"total": 0.011855569120483134
},
{
"rho": 0.985773126102038,
"volA": 0.10907428661238176,
"volB": 0.1058829542466586,
"volPort": 0.10743131759407963,
"total": -0.007977408634621042
}
],
"rhoGrid": [
{
"rho": -1,
"annualVol": 0.02088556431744174
},
{
"rho": -0.5,
"annualVol": 0.052756024634832976
},
{
"rho": 0,
"annualVol": 0.0716253410022194
},
{
"rho": 0.5,
"annualVol": 0.0864706933712071
},
{
"rho": 1,
"annualVol": 0.0991169619717555
}
]
}
}Prefer your own machine? Every file is in the course repository · open it in Codespaces.
What the demo does
A one-page risk tear sheet for a synthetic 60/40 two-fund portfolio over twenty trading days: volatility, downside deviation, drawdown, VaR and Expected Shortfall, beta and performance ratios, tracking error, and a covariance matrix that shows diversification appear and disappear as the correlation flips.
Lessons it combines
- Volatility and Annualized Volatility
- Downside Deviation and Target Shortfall
- Drawdown and Maximum Drawdown
- Loss Distributions and Loss Quantiles
- Value at Risk Intuition
- Expected Shortfall Intuition
- Beta and Market-Relative Risk
- Sharpe, Sortino, and Information Ratio Intuition
- Active Return and Tracking Error
- Covariance Matrices, Portfolio Variance, and Diversification