Module 11 · Financial Risk and Performance Statistics Module demo

Portfolio Risk Report

A tear sheet for two quiet funds.

6:21 clipUses lessons 101–110Watch on YouTube

Transcript

46 sentences · select one to jump there

Code lab

Run it yourself

The demo source in one language. Edit it, run TypeScript and Python right here, and compare with the expected output.

demo-portfolio-risk-report.ts
Start from GitHub
/**
 * 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.