{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# 📘 Fincept Notebook — Mean-Variance Optimization\n",
        "\n",
        "**Portfolio · Hard · ~35 min · pandas + numpy**\n",
        "\n",
        "Harry Markowitz's mean-variance framework is the foundation of modern portfolio theory. The core idea: a portfolio is not just a basket of returns — it is a trade-off between *expected return* and *risk* (volatility), and because assets are imperfectly correlated, the risk of the whole is less than the sum of its parts. In this notebook we build a covariance matrix from volatilities and correlations, simulate thousands of long-only portfolios, and locate the two portfolios every quant cares about: the **maximum-Sharpe** (tangency) portfolio and the **minimum-variance** portfolio.\n",
        "\n",
        "**What you'll learn**\n",
        "- How to build an annualized covariance matrix from volatilities + a correlation matrix\n",
        "- How to compute portfolio expected return, volatility, and Sharpe ratio from weights\n",
        "- How Monte Carlo sampling traces out the efficient frontier and reveals the diversification benefit\n",
        "\n",
        "> **Requires** `pandas` and `numpy` (bundled with Fincept Terminal's Python environment).\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 1. The inputs: expected returns, vols, and correlations\n",
        "\n",
        "Mean-variance optimization needs three things for a universe of *N* assets:\n",
        "\n",
        "1. **Expected annual returns** `μ` — a length-N vector.\n",
        "2. **Annual volatilities** `σ` — a length-N vector (the standard deviation of each asset's annual return).\n",
        "3. **A correlation matrix** `R` — how the assets move together.\n",
        "\n",
        "The **covariance matrix** is then built as `Σ = D · R · D`, where `D = diag(σ)`. Element `Σ[i,j] = σ_i · σ_j · ρ_ij`. We use four realistic assets: US Equity, Intl Equity, Bonds, and Gold.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "fincept_title": "Build Covariance"
      },
      "outputs": [],
      "source": [
        "try:\n",
        "    import numpy as np\n",
        "    import pandas as pd\n",
        "except ImportError:\n",
        "    raise SystemExit(\"Fincept Notebook needs pandas + numpy — install with: pip install pandas numpy\")\n",
        "\n",
        "assets = [\"US_Equity\", \"Intl_Equity\", \"Bonds\", \"Gold\"]\n",
        "\n",
        "# Expected ANNUAL returns and ANNUAL volatilities (realistic long-run estimates)\n",
        "mu  = np.array([0.085, 0.075, 0.035, 0.055])   # expected annual return\n",
        "vol = np.array([0.160, 0.180, 0.060, 0.150])   # annual volatility (std dev)\n",
        "\n",
        "# Correlation matrix (symmetric, 1s on diagonal)\n",
        "corr = np.array([\n",
        "    [1.00, 0.80, 0.10, 0.15],\n",
        "    [0.80, 1.00, 0.05, 0.20],\n",
        "    [0.10, 0.05, 1.00, 0.25],\n",
        "    [0.15, 0.20, 0.25, 1.00],\n",
        "])\n",
        "\n",
        "# Covariance = diag(vol) @ corr @ diag(vol)\n",
        "D = np.diag(vol)\n",
        "cov = D @ corr @ D\n",
        "\n",
        "print(\"Expected annual return & volatility per asset\")\n",
        "print(pd.DataFrame({\"E[return]\": mu, \"volatility\": vol}, index=assets).round(4).to_string())\n",
        "print()\n",
        "print(\"Correlation matrix\")\n",
        "print(pd.DataFrame(corr, index=assets, columns=assets).round(2).to_string())\n",
        "print()\n",
        "print(\"Annualized covariance matrix (Σ = D·R·D)\")\n",
        "print(pd.DataFrame(cov, index=assets, columns=assets).round(5).to_string())\n",
        "print()\n",
        "print(\"Sanity check: sqrt(diag(cov)) should equal the input vols ->\", np.allclose(np.sqrt(np.diag(cov)), vol))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 2. Portfolio math: return, volatility, Sharpe\n",
        "\n",
        "Given a weight vector `w` (long-only, `w_i ≥ 0`, `Σ w_i = 1`):\n",
        "\n",
        "- **Expected return:** `μ_p = wᵀ μ`\n",
        "- **Variance:** `σ²_p = wᵀ Σ w` &nbsp;→&nbsp; **Volatility:** `σ_p = √(wᵀ Σ w)`\n",
        "- **Sharpe ratio:** `(μ_p − r_f) / σ_p`, with risk-free rate `r_f`.\n",
        "\n",
        "The `wᵀ Σ w` term is the magic: cross-terms `w_i w_j σ_ij` with low correlation *reduce* total variance below the weighted average of individual variances. That is diversification, quantified. Let's verify on an equal-weight portfolio.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "fincept_title": "Portfolio Functions"
      },
      "outputs": [],
      "source": [
        "try:\n",
        "    import numpy as np\n",
        "    import pandas as pd\n",
        "except ImportError:\n",
        "    raise SystemExit(\"Fincept Notebook needs pandas + numpy — install with: pip install pandas numpy\")\n",
        "\n",
        "assets = [\"US_Equity\", \"Intl_Equity\", \"Bonds\", \"Gold\"]\n",
        "mu  = np.array([0.085, 0.075, 0.035, 0.055])\n",
        "vol = np.array([0.160, 0.180, 0.060, 0.150])\n",
        "corr = np.array([\n",
        "    [1.00, 0.80, 0.10, 0.15],\n",
        "    [0.80, 1.00, 0.05, 0.20],\n",
        "    [0.10, 0.05, 1.00, 0.25],\n",
        "    [0.15, 0.20, 0.25, 1.00],\n",
        "])\n",
        "D = np.diag(vol)\n",
        "cov = D @ corr @ D\n",
        "rf = 0.03   # risk-free rate\n",
        "\n",
        "def port_return(w):     return float(w @ mu)\n",
        "def port_vol(w):        return float(np.sqrt(w @ cov @ w))\n",
        "def port_sharpe(w):     return (port_return(w) - rf) / port_vol(w)\n",
        "\n",
        "# Equal-weight portfolio\n",
        "w_eq = np.repeat(1.0 / len(assets), len(assets))\n",
        "\n",
        "# Weighted-average vol (what you'd get with NO diversification, i.e. perfect correlation)\n",
        "weighted_avg_vol = float(w_eq @ vol)\n",
        "\n",
        "print(\"Equal-weight portfolio (25% each)\")\n",
        "print(f\"  Expected return : {port_return(w_eq):.4f}\")\n",
        "print(f\"  Volatility      : {port_vol(w_eq):.4f}\")\n",
        "print(f\"  Sharpe ratio    : {port_sharpe(w_eq):.4f}\")\n",
        "print()\n",
        "print(f\"  Weighted-avg vol (no diversification) : {weighted_avg_vol:.4f}\")\n",
        "print(f\"  Actual portfolio vol                  : {port_vol(w_eq):.4f}\")\n",
        "print(f\"  Diversification benefit (vol saved)   : {weighted_avg_vol - port_vol(w_eq):.4f}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 3. Monte Carlo: simulate 10,000 random portfolios\n",
        "\n",
        "There is a closed-form solution for the efficient frontier, but a Monte Carlo sweep is more intuitive and handles the long-only constraint naturally. We draw 10,000 weight vectors from a **Dirichlet distribution**, which produces non-negative weights that sum to 1 by construction — exactly the long-only simplex. For each portfolio we record return, volatility, and Sharpe. The cloud of points fills the *feasible region*; its upper-left boundary is the efficient frontier.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "fincept_title": "Simulate Portfolios"
      },
      "outputs": [],
      "source": [
        "try:\n",
        "    import numpy as np\n",
        "    import pandas as pd\n",
        "except ImportError:\n",
        "    raise SystemExit(\"Fincept Notebook needs pandas + numpy — install with: pip install pandas numpy\")\n",
        "\n",
        "np.random.seed(7)\n",
        "\n",
        "assets = [\"US_Equity\", \"Intl_Equity\", \"Bonds\", \"Gold\"]\n",
        "mu  = np.array([0.085, 0.075, 0.035, 0.055])\n",
        "vol = np.array([0.160, 0.180, 0.060, 0.150])\n",
        "corr = np.array([\n",
        "    [1.00, 0.80, 0.10, 0.15],\n",
        "    [0.80, 1.00, 0.05, 0.20],\n",
        "    [0.10, 0.05, 1.00, 0.25],\n",
        "    [0.15, 0.20, 0.25, 1.00],\n",
        "])\n",
        "cov = np.diag(vol) @ corr @ np.diag(vol)\n",
        "rf = 0.03\n",
        "\n",
        "N_PORT = 10000\n",
        "n = len(assets)\n",
        "\n",
        "# Dirichlet(1,...,1) = uniform over the long-only simplex (weights >=0, sum to 1)\n",
        "W = np.random.dirichlet(np.ones(n), size=N_PORT)   # shape (N_PORT, n)\n",
        "\n",
        "rets = W @ mu                                        # expected returns, shape (N_PORT,)\n",
        "vols = np.sqrt(np.einsum('ij,jk,ik->i', W, cov, W)) # portfolio vols, vectorized wᵀΣw\n",
        "sharpes = (rets - rf) / vols\n",
        "\n",
        "sim = pd.DataFrame({\"return\": rets, \"vol\": vols, \"sharpe\": sharpes})\n",
        "\n",
        "print(f\"Simulated {N_PORT:,} long-only portfolios (Dirichlet weights, seed=7)\")\n",
        "print()\n",
        "print(\"Summary statistics across the cloud:\")\n",
        "print(sim.describe().round(4).to_string())\n",
        "print()\n",
        "print(\"First 5 sampled portfolios:\")\n",
        "head = sim.head(5).copy()\n",
        "for j, a in enumerate(assets):\n",
        "    head[a] = W[:5, j]\n",
        "print(head.round(4).to_string())"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 4. The two special portfolios\n",
        "\n",
        "From the cloud we extract:\n",
        "\n",
        "- **Maximum-Sharpe (tangency) portfolio** — the one with the highest risk-adjusted return `(μ_p − r_f)/σ_p`. This is the portfolio a rational investor combines with the risk-free asset.\n",
        "- **Minimum-volatility portfolio** — the leftmost point of the frontier; the lowest-risk mix achievable, regardless of return.\n",
        "\n",
        "Note that Monte Carlo only *approximates* the true optima — with more samples the estimates tighten. We print full weight vectors for both.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "fincept_title": "Optimal Portfolios"
      },
      "outputs": [],
      "source": [
        "try:\n",
        "    import numpy as np\n",
        "    import pandas as pd\n",
        "except ImportError:\n",
        "    raise SystemExit(\"Fincept Notebook needs pandas + numpy — install with: pip install pandas numpy\")\n",
        "\n",
        "np.random.seed(7)\n",
        "\n",
        "assets = [\"US_Equity\", \"Intl_Equity\", \"Bonds\", \"Gold\"]\n",
        "mu  = np.array([0.085, 0.075, 0.035, 0.055])\n",
        "vol = np.array([0.160, 0.180, 0.060, 0.150])\n",
        "corr = np.array([\n",
        "    [1.00, 0.80, 0.10, 0.15],\n",
        "    [0.80, 1.00, 0.05, 0.20],\n",
        "    [0.10, 0.05, 1.00, 0.25],\n",
        "    [0.15, 0.20, 0.25, 1.00],\n",
        "])\n",
        "cov = np.diag(vol) @ corr @ np.diag(vol)\n",
        "rf = 0.03\n",
        "\n",
        "N_PORT = 10000\n",
        "n = len(assets)\n",
        "W = np.random.dirichlet(np.ones(n), size=N_PORT)\n",
        "rets = W @ mu\n",
        "vols = np.sqrt(np.einsum('ij,jk,ik->i', W, cov, W))\n",
        "sharpes = (rets - rf) / vols\n",
        "\n",
        "i_sharpe = int(np.argmax(sharpes))     # max-Sharpe portfolio index\n",
        "i_minvol = int(np.argmin(vols))        # min-volatility portfolio index\n",
        "\n",
        "def describe(i, label):\n",
        "    w = W[i]\n",
        "    print(label)\n",
        "    print(f\"  Expected return : {rets[i]:.4f}\")\n",
        "    print(f\"  Volatility      : {vols[i]:.4f}\")\n",
        "    print(f\"  Sharpe ratio    : {sharpes[i]:.4f}\")\n",
        "    print(\"  Weights:\")\n",
        "    for a, wi in zip(assets, w):\n",
        "        print(f\"    {a:<12} {wi:7.2%}\")\n",
        "    print()\n",
        "\n",
        "describe(i_sharpe, \"=== MAXIMUM-SHARPE (tangency) PORTFOLIO ===\")\n",
        "describe(i_minvol, \"=== MINIMUM-VOLATILITY PORTFOLIO ===\")\n",
        "\n",
        "comp = pd.DataFrame({\n",
        "    \"Max-Sharpe\": W[i_sharpe],\n",
        "    \"Min-Vol\":    W[i_minvol],\n",
        "}, index=assets)\n",
        "print(\"Weight comparison\")\n",
        "print(comp.round(4).to_string())"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 5. Tracing the efficient frontier\n",
        "\n",
        "The **efficient frontier** is the set of portfolios offering the highest expected return for each level of risk. To approximate it from our cloud, we bucket the simulated portfolios by volatility, and within each bucket keep the one with the maximum return. The result is a monotone, concave curve — the upper edge of the feasible region. Anything below the frontier is dominated (you could get more return for the same risk).\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "fincept_title": "Efficient Frontier"
      },
      "outputs": [],
      "source": [
        "try:\n",
        "    import numpy as np\n",
        "    import pandas as pd\n",
        "except ImportError:\n",
        "    raise SystemExit(\"Fincept Notebook needs pandas + numpy — install with: pip install pandas numpy\")\n",
        "\n",
        "np.random.seed(7)\n",
        "\n",
        "assets = [\"US_Equity\", \"Intl_Equity\", \"Bonds\", \"Gold\"]\n",
        "mu  = np.array([0.085, 0.075, 0.035, 0.055])\n",
        "vol = np.array([0.160, 0.180, 0.060, 0.150])\n",
        "corr = np.array([\n",
        "    [1.00, 0.80, 0.10, 0.15],\n",
        "    [0.80, 1.00, 0.05, 0.20],\n",
        "    [0.10, 0.05, 1.00, 0.25],\n",
        "    [0.15, 0.20, 0.25, 1.00],\n",
        "])\n",
        "cov = np.diag(vol) @ corr @ np.diag(vol)\n",
        "rf = 0.03\n",
        "\n",
        "N_PORT = 10000\n",
        "n = len(assets)\n",
        "W = np.random.dirichlet(np.ones(n), size=N_PORT)\n",
        "rets = W @ mu\n",
        "vols = np.sqrt(np.einsum('ij,jk,ik->i', W, cov, W))\n",
        "sharpes = (rets - rf) / vols\n",
        "\n",
        "sim = pd.DataFrame({\"return\": rets, \"vol\": vols, \"sharpe\": sharpes})\n",
        "\n",
        "# Bucket by volatility, take the max-return portfolio in each bucket -> frontier\n",
        "n_buckets = 12\n",
        "sim[\"vol_bucket\"] = pd.cut(sim[\"vol\"], bins=n_buckets)\n",
        "idx = sim.groupby(\"vol_bucket\", observed=True)[\"return\"].idxmax().dropna().astype(int)\n",
        "frontier = sim.loc[idx, [\"vol\", \"return\", \"sharpe\"]].sort_values(\"vol\").reset_index(drop=True)\n",
        "\n",
        "print(\"Approximate efficient frontier (max return per volatility bucket)\")\n",
        "print(frontier.round(4).to_string())\n",
        "print()\n",
        "print(\"Notice: as volatility rises, the best achievable return rises but with\")\n",
        "print(\"diminishing slope (concavity) — and Sharpe peaks near the tangency point.\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 6. Why diversification works\n",
        "\n",
        "Look back at the min-vol portfolio: it loads heavily on bonds (low vol) but still holds some equity and gold. Why not 100% bonds? Because gold and equities have **low correlation** with bonds — adding a little of each lowers total variance via the negative-to-low cross-terms, even though each is individually riskier. The min-vol portfolio's volatility is typically *below* the lowest single-asset vol weighted average. That free lunch — lower risk for the same or higher return — is the entire point of Markowitz.\n",
        "\n",
        "**Caveats for real use:** expected returns are notoriously hard to estimate and optimizers are extremely sensitive to them; covariance is more stable but still noisy. Practitioners add constraints (position limits), shrinkage estimators, and robust/Black-Litterman methods. But the geometry you just simulated — the frontier, the tangency portfolio, the min-vol point — is universal.\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "---\n",
        "*— Fincept Notebook · part of Fincept Terminal. Edit any cell and press Ctrl+Enter to run.*\n"
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}
