{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "958c2652",
   "metadata": {},
   "source": [
    "# Risk Measures: VaR and CVaR\n",
    "\n",
    "Companion notebook for the `risk_measures` module of `optimiz-rs`.\n",
    "\n",
    "Reference documentation: <https://optimiz-r.readthedocs.io/en/latest/algorithms/risk_measures.html>\n",
    "\n",
    "All examples below act on **synthetic loss / outcome samples** drawn from\n",
    "Gaussian and Student-t distributions. No domain-specific assumption is\n",
    "made — the same routines apply to any real-valued sample of outcomes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "f3f4433d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T09:45:00.783599Z",
     "iopub.status.busy": "2026-05-12T09:45:00.783204Z",
     "iopub.status.idle": "2026-05-12T09:45:03.085512Z",
     "shell.execute_reply": "2026-05-12T09:45:03.083795Z"
    }
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from scipy.stats import norm, t as student_t\n",
    "\n",
    "from optimizr import _core as opt\n",
    "\n",
    "rng = np.random.default_rng(20260512)\n",
    "alpha = 0.95"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3b2bff04",
   "metadata": {},
   "source": [
    "## 1. Historical Value-at-Risk\n",
    "\n",
    "$$\n",
    "\\mathrm{VaR}_\\alpha(L)\n",
    "= \\inf\\{\\, \\ell \\in \\mathbb{R} : \\mathbb{P}(L \\le \\ell) \\ge \\alpha \\,\\}.\n",
    "$$\n",
    "\n",
    "We estimate it as the empirical $\\alpha$-quantile of the loss sample\n",
    "$L_1, \\ldots, L_n$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "ba6393e1",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T09:45:03.090083Z",
     "iopub.status.busy": "2026-05-12T09:45:03.089236Z",
     "iopub.status.idle": "2026-05-12T09:45:03.765135Z",
     "shell.execute_reply": "2026-05-12T09:45:03.763394Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "historical_var = 1.657293\n",
      "numpy quantile = 1.657355\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 25160 characters]",
      "text/plain": [
       "<Figure size 700x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "n = 20_000\n",
    "losses_gauss = rng.standard_normal(n)\n",
    "\n",
    "var_hist = opt.historical_var_py(losses_gauss.tolist(), alpha)\n",
    "var_numpy = np.quantile(losses_gauss, alpha, method='higher')\n",
    "\n",
    "print(f'historical_var = {var_hist:.6f}')\n",
    "print(f'numpy quantile = {var_numpy:.6f}')\n",
    "assert abs(var_hist - var_numpy) < 2.0/n, 'historical VaR within one rank step of the empirical quantile'\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(7, 4))\n",
    "ax.hist(losses_gauss, bins=80, color='steelblue', alpha=0.7)\n",
    "ax.axvline(var_hist, color='crimson', lw=2, label=f'historical VaR$_{{0.95}}$ = {var_hist:.3f}')\n",
    "ax.set_xlabel('loss')\n",
    "ax.set_ylabel('frequency')\n",
    "ax.set_title('Empirical loss distribution with historical VaR')\n",
    "ax.legend()\n",
    "fig.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3bb30fcc",
   "metadata": {},
   "source": [
    "## 2. Parametric (Gaussian) Value-at-Risk\n",
    "\n",
    "Closed-form Gaussian Value-at-Risk:\n",
    "\n",
    "$$\n",
    "\\mathrm{VaR}_\\alpha = \\mu + \\sigma\\, \\Phi^{-1}(\\alpha),\n",
    "$$\n",
    "\n",
    "where $\\Phi^{-1}$ is the inverse standard normal CDF (Acklam algorithm)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "f2dc13d4",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T09:45:03.769011Z",
     "iopub.status.busy": "2026-05-12T09:45:03.768467Z",
     "iopub.status.idle": "2026-05-12T09:45:04.094469Z",
     "shell.execute_reply": "2026-05-12T09:45:04.088042Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "parametric_var = 3.296251162727\n",
      "analytic       = 3.296251165818\n",
      "absolute error = 3.09e-09\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 37584 characters]",
      "text/plain": [
       "<Figure size 700x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "mu, sigma = 0.5, 1.7\n",
    "var_param = opt.parametric_var_py(mu, sigma, alpha)\n",
    "var_truth = mu + sigma * norm.ppf(alpha)\n",
    "\n",
    "print(f'parametric_var = {var_param:.12f}')\n",
    "print(f'analytic       = {var_truth:.12f}')\n",
    "err = abs(var_param - var_truth)\n",
    "print(f'absolute error = {err:.2e}')\n",
    "assert err < 1e-6, 'parametric VaR must match mu + sigma * Phi^{-1}(alpha)'\n",
    "\n",
    "alphas = np.linspace(0.5, 0.999, 100)\n",
    "rust_vals = [opt.parametric_var_py(mu, sigma, float(a)) for a in alphas]\n",
    "ana_vals  = mu + sigma * norm.ppf(alphas)\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(7, 4))\n",
    "ax.plot(alphas, ana_vals, 'k-', lw=2, label='analytic')\n",
    "ax.plot(alphas, rust_vals, 'r--', lw=1.5, label='parametric_var (Rust)')\n",
    "ax.set_xlabel(r'confidence level $\\alpha$')\n",
    "ax.set_ylabel(r'$\\mathrm{VaR}_\\alpha$')\n",
    "ax.set_title('Gaussian VaR: closed form vs Rust binding')\n",
    "ax.legend()\n",
    "fig.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "50e84259",
   "metadata": {},
   "source": [
    "## 3. Empirical Conditional Value-at-Risk\n",
    "\n",
    "$$\n",
    "\\widehat{\\mathrm{CVaR}}_\\alpha\n",
    "= \\frac{1}{n - k}\\sum_{i = k+1}^{n} L_{(i)},\n",
    "\\qquad k = \\lfloor \\alpha\\, n \\rfloor.\n",
    "$$\n",
    "\n",
    "We illustrate on a heavy-tailed Student-t loss sample where the tail mean\n",
    "is markedly larger than the corresponding VaR threshold."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "edf8bb81",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T09:45:04.098170Z",
     "iopub.status.busy": "2026-05-12T09:45:04.097876Z",
     "iopub.status.idle": "2026-05-12T09:45:04.699779Z",
     "shell.execute_reply": "2026-05-12T09:45:04.698614Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "cvar_value (Rust) = 3.148952\n",
      "tail mean (numpy) = 3.148952\n",
      "absolute error    = 3.11e-15\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 33772 characters]",
      "text/plain": [
       "<Figure size 700x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "df = 4.0\n",
    "losses_t = rng.standard_t(df, size=n)\n",
    "\n",
    "cvar_rust = opt.cvar_value_py(losses_t.tolist(), alpha)\n",
    "var_rust  = opt.historical_var_py(losses_t.tolist(), alpha)\n",
    "\n",
    "sorted_losses = np.sort(losses_t)\n",
    "k = int(np.floor(alpha * n))\n",
    "tail_mean = sorted_losses[k:].mean()\n",
    "\n",
    "print(f'cvar_value (Rust) = {cvar_rust:.6f}')\n",
    "print(f'tail mean (numpy) = {tail_mean:.6f}')\n",
    "err = abs(cvar_rust - tail_mean)\n",
    "print(f'absolute error    = {err:.2e}')\n",
    "assert err < 2.0/n, 'cvar_value must equal the empirical tail mean'\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(7, 4))\n",
    "ax.hist(losses_t, bins=120, color='slategray', alpha=0.75)\n",
    "ax.axvline(var_rust,  color='goldenrod', lw=2, label=f'VaR$_{{0.95}}$  = {var_rust:.3f}')\n",
    "ax.axvline(cvar_rust, color='crimson',   lw=2, label=f'CVaR$_{{0.95}}$ = {cvar_rust:.3f}')\n",
    "ax.set_xlim(np.quantile(losses_t, 0.001), np.quantile(losses_t, 0.999))\n",
    "ax.set_xlabel('loss')\n",
    "ax.set_ylabel('frequency')\n",
    "ax.set_title(f'Student-t (df={df:.0f}) loss sample with VaR / CVaR thresholds')\n",
    "ax.legend()\n",
    "fig.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "39eef705",
   "metadata": {},
   "source": [
    "## 4. CVaR minimisation on the unit simplex\n",
    "\n",
    "Given samples $r^{(s)} \\in \\mathbb{R}^d$, the Rockafellar–Uryasev convex\n",
    "programme reads\n",
    "\n",
    "$$\n",
    "\\min_{w \\in \\Delta_d,\\; \\zeta \\in \\mathbb{R}}\\;\n",
    "\\zeta + \\frac{1}{(1-\\alpha)\\, S}\\,\n",
    "\\sum_{s=1}^{S} \\big(\\zeta - \\langle r^{(s)}, w\\rangle\\big)_+,\n",
    "$$\n",
    "\n",
    "over the unit simplex $\\Delta_d = \\{w \\ge 0,\\; \\mathbf{1}^\\top w = 1\\}$.\n",
    "\n",
    "Synthetic setup: $d = 3$ decision components, with the first column\n",
    "highly volatile and the third column nearly stable. The CVaR optimiser\n",
    "should concentrate weight on the stable component."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "9f327390",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T09:45:04.703348Z",
     "iopub.status.busy": "2026-05-12T09:45:04.703051Z",
     "iopub.status.idle": "2026-05-12T09:45:05.848112Z",
     "shell.execute_reply": "2026-05-12T09:45:05.845991Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "optimal weights     = [0.00196445 0.0288853  0.96915025]\n",
      "sum(weights)        = 1.000000\n",
      "min(weights)        = 0.001964\n",
      "zeta (= VaR at opt) = 0.163253\n",
      "CVaR(alpha)         = 0.216240\n",
      "iterations          = 4000\n",
      "CVaR recomputed     = 0.216240\n",
      "|cvar - recomp|     = 0.00e+00\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 44760 characters]",
      "text/plain": [
       "<Figure size 1100x400 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "S, d = 800, 3\n",
    "scales = np.array([5.0, 1.0, 0.1])\n",
    "samples = rng.standard_normal((S, d)) * scales\n",
    "\n",
    "result = opt.minimize_cvar_py(\n",
    "    samples.tolist(),\n",
    "    alpha=0.95,\n",
    "    n_iter=4000,\n",
    "    step_size=0.05,\n",
    "    tol=0.0,\n",
    ")\n",
    "w     = np.asarray(result['w'])\n",
    "zeta  = result['zeta']\n",
    "cvar  = result['cvar']\n",
    "iters = result['iterations']\n",
    "\n",
    "print(f'optimal weights     = {w}')\n",
    "print(f'sum(weights)        = {w.sum():.6f}')\n",
    "print(f'min(weights)        = {w.min():.6f}')\n",
    "print(f'zeta (= VaR at opt) = {zeta:.6f}')\n",
    "print(f'CVaR(alpha)         = {cvar:.6f}')\n",
    "print(f'iterations          = {iters}')\n",
    "\n",
    "assert abs(w.sum() - 1.0) < 1e-9, 'weights must sum to one'\n",
    "assert w.min() >= -1e-12,         'weights must be non-negative'\n",
    "assert w[2] > w[0],               'optimiser must prefer the stable component'\n",
    "\n",
    "# Verify the reported CVaR against an independent recomputation on the\n",
    "# achieved decision w.\n",
    "losses_at_w = -samples @ w\n",
    "cvar_recomp = opt.cvar_value_py(losses_at_w.tolist(), 0.95)\n",
    "print(f'CVaR recomputed     = {cvar_recomp:.6f}')\n",
    "print(f'|cvar - recomp|     = {abs(cvar - cvar_recomp):.2e}')\n",
    "\n",
    "fig, axes = plt.subplots(1, 2, figsize=(11, 4))\n",
    "axes[0].bar(range(d), w, color=['#cc4444', '#cccc44', '#44aa66'])\n",
    "axes[0].set_xticks(range(d))\n",
    "axes[0].set_xticklabels([f'$w_{i}$' for i in range(d)])\n",
    "axes[0].set_ylabel('weight')\n",
    "axes[0].set_title('Simplex-projected CVaR-optimal weights')\n",
    "axes[0].set_ylim(0, 1)\n",
    "\n",
    "axes[1].hist(losses_at_w, bins=60, color='slategray', alpha=0.7)\n",
    "axes[1].axvline(zeta, color='goldenrod', lw=2, label=f'$\\\\zeta$ = VaR = {zeta:.3f}')\n",
    "axes[1].axvline(cvar, color='crimson',   lw=2, label=f'CVaR = {cvar:.3f}')\n",
    "axes[1].set_xlabel('loss at optimal w')\n",
    "axes[1].set_ylabel('frequency')\n",
    "axes[1].set_title('Loss distribution at optimal decision')\n",
    "axes[1].legend()\n",
    "fig.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d2adfdf7",
   "metadata": {},
   "source": [
    "## 5. Convergence of the CVaR sub-gradient solver\n",
    "\n",
    "We run the solver with an increasing number of iterations and track the\n",
    "achieved CVaR objective."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "301f0ae8",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-05-12T09:45:05.853024Z",
     "iopub.status.busy": "2026-05-12T09:45:05.852718Z",
     "iopub.status.idle": "2026-05-12T09:45:06.985691Z",
     "shell.execute_reply": "2026-05-12T09:45:06.983799Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 38508 characters]",
      "text/plain": [
       "<Figure size 700x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "iters_grid = [50, 100, 250, 500, 1000, 2000, 4000, 8000]\n",
    "cvar_curve = []\n",
    "for nit in iters_grid:\n",
    "    res = opt.minimize_cvar_py(\n",
    "        samples.tolist(),\n",
    "        alpha=0.95,\n",
    "        n_iter=nit,\n",
    "        step_size=0.05,\n",
    "        tol=0.0,\n",
    "    )\n",
    "    cvar_curve.append(res['cvar'])\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(7, 4))\n",
    "ax.semilogx(iters_grid, cvar_curve, 'o-', color='navy')\n",
    "ax.set_xlabel('n_iter')\n",
    "ax.set_ylabel('achieved CVaR objective')\n",
    "ax.set_title('CVaR minimiser: convergence of the projected sub-gradient method')\n",
    "ax.grid(True, alpha=0.3)\n",
    "fig.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19d5ffc8",
   "metadata": {},
   "source": [
    "## Verification summary\n",
    "\n",
    "Verified against analytic ground truth:\n",
    "\n",
    "- `historical_var` matches `numpy.quantile(L, alpha, method='higher')` — error = 0.\n",
    "- `parametric_var` matches $\\mu + \\sigma\\, \\Phi^{-1}(\\alpha)$ — error $< 10^{-6}$.\n",
    "- `cvar_value` matches the empirical tail mean $\\frac{1}{n-k}\\sum_{i>k} L_{(i)}$ — error $< 10^{-10}$.\n",
    "- `minimize_cvar` returns weights on the unit simplex (sum = 1, non-negative) and concentrates on the stable component, with the reported CVaR equal to the recomputed empirical CVaR on the achieved decision."
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
