{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-000",
   "metadata": {},
   "source": [
    "# Lesson 8 (Part B) — Capstone: A Financial Simulation Engine\n",
    "\n",
    "**Companion notebook** for the capstone. Build and check the engine here first; then\n",
    "move the functions into `src/foundations/simulation.py` and `risk.py`, and write the\n",
    "report notebook described in the slides."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-001",
   "metadata": {},
   "outputs": [],
   "source": [
    "def check(label, got, expected, tol=1e-6):\n",
    "    \"\"\"Compare your answer with the expected value; prints ✅ or ❌ (never raises).\"\"\"\n",
    "    import math\n",
    "    try:\n",
    "        if isinstance(expected, float) or isinstance(got, float):\n",
    "            ok = math.isclose(float(got), float(expected), rel_tol=tol, abs_tol=tol)\n",
    "        elif isinstance(expected, (list, tuple)) and expected and isinstance(expected[0], str):\n",
    "            ok = list(got) == list(expected)\n",
    "        elif hasattr(expected, \"__len__\") and not isinstance(expected, str):\n",
    "            import numpy as np\n",
    "            ok = np.allclose(np.asarray(got, dtype=float), np.asarray(expected, dtype=float), rtol=tol, atol=tol)\n",
    "        else:\n",
    "            ok = got == expected\n",
    "    except Exception:\n",
    "        ok = False\n",
    "    print((\"✅ \" if ok else \"❌ \") + f\"{label}: got {got!r}, expected {expected!r}\")\n",
    "import numpy as np\n",
    "from scipy import stats"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-002",
   "metadata": {},
   "source": [
    "## 1. The portfolio (given)\n",
    "This is the Lesson 7 driver generator as a function: ages, mileage, and each driver's\n",
    "expected claim frequency."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-003",
   "metadata": {},
   "outputs": [],
   "source": [
    "def make_portfolio(n: int, rng: np.random.Generator, base_freq: float = 0.08) -> dict:\n",
    "    \"\"\"Simulate n driver profiles and their expected claim frequencies.\"\"\"\n",
    "    age_dist = stats.truncnorm((18 - 42) / 14, (85 - 42) / 14, loc=42, scale=14)\n",
    "    ages = np.floor(age_dist.rvs(size=n, random_state=rng)).astype(int)\n",
    "    mileage = stats.lognorm(s=0.5, scale=12_000).rvs(size=n, random_state=rng)\n",
    "    age_factor = np.where(ages < 25, 1.8, np.where(ages >= 70, 1.3, 1.0))\n",
    "    freq = base_freq * (mileage / 12_000) ** 0.5 * age_factor\n",
    "    return {\"ages\": ages, \"mileage\": mileage, \"freq\": freq}\n",
    "\n",
    "\n",
    "portfolio = make_portfolio(2_000, np.random.default_rng(2026))\n",
    "freq = portfolio[\"freq\"]\n",
    "print(f\"expected claims per year: {freq.sum():.2f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-004",
   "metadata": {},
   "source": [
    "## 2. Pooling claims with `np.repeat` and `np.bincount`\n",
    "Draw one claim count per **year**, all severities at once, then add them up by year:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-005",
   "metadata": {},
   "outputs": [],
   "source": [
    "counts = np.array([2, 0, 3])\n",
    "paid_demo = np.array([10.0, 20, 30, 40, 50])\n",
    "year = np.repeat(np.arange(3), counts)\n",
    "print(year, np.bincount(year, weights=paid_demo, minlength=3))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-006",
   "metadata": {},
   "source": [
    "---\n",
    "## Your turn 1 — the engine\n",
    "\n",
    "Complete `simulate_annual_losses`: Poisson claim count per year with rate\n",
    "`freq.sum()`, lognormal severities from `severity.rvs(..., random_state=rng)`, a\n",
    "deductible and a limit, then totals per year."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-007",
   "metadata": {},
   "outputs": [],
   "source": [
    "def simulate_annual_losses(freq, n_years, rng, severity, deductible=500.0, limit=50_000.0):\n",
    "    \"\"\"Total paid losses in each of n_years simulated years.\"\"\"\n",
    "    ...  # your code here"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-008",
   "metadata": {},
   "source": [
    "## Your turn 2 — risk measures\n",
    "\n",
    "* `value_at_risk(losses, level)` — the `level` quantile (use `np.quantile`); raise\n",
    "  `ValueError` unless `0 < level < 1`\n",
    "* `tail_value_at_risk(losses, level)` — the mean of losses **≥** the VaR"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-009",
   "metadata": {},
   "outputs": [],
   "source": [
    "def value_at_risk(losses, level=0.995):\n",
    "    ...  # your code here\n",
    "\n",
    "\n",
    "def tail_value_at_risk(losses, level=0.995):\n",
    "    ...  # your code here\n",
    "\n",
    "\n",
    "check(\"VaR of 1..100 at 95%\", value_at_risk(np.arange(1, 101, dtype=float), 0.95), 95.05)\n",
    "check(\"TVaR of 1..100 at 95%\", tail_value_at_risk(np.arange(1, 101, dtype=float), 0.95), 98.0)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-010",
   "metadata": {},
   "source": [
    "## 3. The base run — compare with the answer key"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-011",
   "metadata": {},
   "outputs": [],
   "source": [
    "severity = stats.lognorm(s=1.2, scale=4_000)\n",
    "losses = simulate_annual_losses(freq, 50_000, np.random.default_rng(1), severity)\n",
    "if losses is not None:\n",
    "    print(f\"mean {losses.mean():,.0f}   sd {losses.std():,.0f}\")\n",
    "check(\"mean annual loss\", None if losses is None else round(losses.mean()), 1245042)\n",
    "check(\"VaR 99.5%\", None if losses is None else round(value_at_risk(losses, 0.995) or 0), 1692831)\n",
    "check(\"TVaR 99.5%\", None if losses is None else round(tail_value_at_risk(losses, 0.995) or 0), 1759408)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-012",
   "metadata": {},
   "source": [
    "## 4. Validation: mean vs. theory"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-013",
   "metadata": {},
   "outputs": [],
   "source": [
    "mean_paid = severity.expect(lambda x: np.minimum(np.maximum(x - 500, 0), 50_000))\n",
    "theory = freq.sum() * mean_paid\n",
    "if losses is not None:\n",
    "    se = losses.std() / np.sqrt(len(losses))\n",
    "    print(f\"theory {theory:,.0f}   simulated {losses.mean():,.0f}   difference {(losses.mean() - theory) / se:+.2f} s.e.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-014",
   "metadata": {},
   "source": [
    "## 5. A report chart\n",
    "Histogram of annual losses with VaR and TVaR marked — a starting point for your report."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-015",
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "if losses is not None:\n",
    "    fig, ax = plt.subplots(figsize=(8, 3.5))\n",
    "    ax.hist(losses, bins=80, color=\"#0e6b6b\", alpha=0.8)\n",
    "    for value, label, color in [(value_at_risk(losses), \"VaR 99.5%\", \"#c26a1b\"),\n",
    "                                 (tail_value_at_risk(losses), \"TVaR 99.5%\", \"#b3372e\")]:\n",
    "        ax.axvline(value, color=color, linewidth=2, label=f\"{label}: {value:,.0f}\")\n",
    "    ax.set(title=\"Simulated annual losses (50,000 years)\", xlabel=\"Annual loss\", ylabel=\"Years\")\n",
    "    ax.legend()\n",
    "    plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-016",
   "metadata": {},
   "source": [
    "**Next:** reproduce the scenario table from the slides (σ = 1.6; limit 250,000; no\n",
    "limit), add a scenario of your own, and write your conclusions."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
