{ "cells": [ { "cell_type": "markdown", "id": "86e024bf", "metadata": {}, "source": [ "# Leverage by Borrowing Cash\n", "\n", "The *standard mean-variance (Markowitz) portfolio selection model* determines an optimal investment portfolio that balances risk and expected return. In this notebook, we maximize the portfolio's expected return while constraining the admissible variance (risk) to a given maximum level. Please refer to the [annotated list of references](../literature.rst#portfolio-optimization) for more background information on portfolio optimization.\n", "\n", "To this basic model, we add *leverage*. Leverage means borrowing capital from a third party to buy more assets (and paying interest on the borrowed capital). This magnifies both the potential upside and downside." ] }, { "cell_type": "code", "execution_count": 1, "id": "256b3463", "metadata": { "execution": { "iopub.execute_input": "2026-07-03T11:04:06.493025Z", "iopub.status.busy": "2026-07-03T11:04:06.492763Z", "iopub.status.idle": "2026-07-03T11:04:07.075053Z", "shell.execute_reply": "2026-07-03T11:04:07.073938Z" }, "nbsphinx": "hidden" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Requirement already satisfied: numpy in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (2.4.6)\r\n", "Requirement already satisfied: scipy in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (1.17.1)\r\n", "Requirement already satisfied: gurobipy in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (13.0.2)\r\n", "Requirement already satisfied: pandas in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (3.0.3)\r\n", "Requirement already satisfied: matplotlib in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (3.11.0)\r\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Requirement already satisfied: python-dateutil>=2.8.2 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from pandas) (2.9.0.post0)\r\n", "Requirement already satisfied: contourpy>=1.0.1 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from matplotlib) (1.3.3)\r\n", "Requirement already satisfied: cycler>=0.10 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from matplotlib) (0.12.1)\r\n", "Requirement already satisfied: fonttools>=4.22.0 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from matplotlib) (4.63.0)\r\n", "Requirement already satisfied: kiwisolver>=1.3.1 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from matplotlib) (1.5.0)\r\n", "Requirement already satisfied: packaging>=20.0 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from matplotlib) (26.2)\r\n", "Requirement already satisfied: pillow>=9 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from matplotlib) (12.3.0)\r\n", "Requirement already satisfied: pyparsing>=3 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from matplotlib) (3.3.2)\r\n", "Requirement already satisfied: six>=1.5 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)\r\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Note: you may need to restart the kernel to use updated packages.\n" ] } ], "source": [ "# Install dependencies\n", "%pip install numpy scipy gurobipy pandas matplotlib" ] }, { "cell_type": "code", "execution_count": 2, "id": "6cea9bcb", "metadata": { "execution": { "iopub.execute_input": "2026-07-03T11:04:07.077288Z", "iopub.status.busy": "2026-07-03T11:04:07.077081Z", "iopub.status.idle": "2026-07-03T11:04:07.672256Z", "shell.execute_reply": "2026-07-03T11:04:07.670917Z" } }, "outputs": [], "source": [ "import gurobipy as gp\n", "import pandas as pd\n", "import numpy as np\n", "import matplotlib.pyplot as plt" ] }, { "cell_type": "code", "execution_count": 3, "id": "6ebd068e", "metadata": { "execution": { "iopub.execute_input": "2026-07-03T11:04:07.674259Z", "iopub.status.busy": "2026-07-03T11:04:07.673977Z", "iopub.status.idle": "2026-07-03T11:04:07.683794Z", "shell.execute_reply": "2026-07-03T11:04:07.682608Z" }, "nbsphinx": "hidden" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Set parameter WLSAccessID\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Set parameter WLSSecret\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Set parameter LicenseID to value 2443533\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "WLS license 2443533 - registered to Gurobi GmbH\n" ] } ], "source": [ "# Hidden cell to avoid licensing messages\n", "# when docs are generated.\n", "with gp.Model():\n", " pass" ] }, { "cell_type": "markdown", "id": "2785c8a5", "metadata": {}, "source": [ "## Input Data\n", "\n", "The following input data is used within the model:\n", "\n", "- $S$: set of stocks\n", "- $\\mu$: vector of expected returns\n", "- $\\Sigma$: PSD variance-covariance matrix\n", " - $\\sigma_{ij}$ covariance between returns of assets $i$ and $j$\n", " - $\\sigma_{ii}$ variance of return of asset $i$" ] }, { "cell_type": "code", "execution_count": 4, "id": "e32d9d5c", "metadata": { "execution": { "iopub.execute_input": "2026-07-03T11:04:07.685547Z", "iopub.status.busy": "2026-07-03T11:04:07.685369Z", "iopub.status.idle": "2026-07-03T11:04:07.691126Z", "shell.execute_reply": "2026-07-03T11:04:07.690235Z" } }, "outputs": [], "source": [ "# Import some example data set\n", "Sigma = pd.read_pickle(\"sigma.pkl\")\n", "mu = pd.read_pickle(\"mu.pkl\")" ] }, { "cell_type": "markdown", "id": "4a9d3c5b", "metadata": {}, "source": [ "## Formulation\n", "Mathematically, this results in a convex quadratically constrained optimization problem.\n", "\n", "### Model Parameters\n", "\n", "The following parameters are used within the model:\n", "\n", "- $\\bar\\sigma^2$: maximal admissible variance for the portfolio return\n", "- $c_\\text{rf}$: interest on the risk-free asset. For simplicity, we assume the same interest rate for lending and borrowing.\n", "- $\\ell_\\text{rf}$: maximal short on risk-free asset\n", "- $u_\\text{rf}$: maximal investment in risk-free asset" ] }, { "cell_type": "code", "execution_count": 5, "id": "13f2f98d", "metadata": { "execution": { "iopub.execute_input": "2026-07-03T11:04:07.692747Z", "iopub.status.busy": "2026-07-03T11:04:07.692593Z", "iopub.status.idle": "2026-07-03T11:04:07.696153Z", "shell.execute_reply": "2026-07-03T11:04:07.695384Z" } }, "outputs": [], "source": [ "# Values for the model parameters:\n", "V = 4.0 # Maximal admissible variance (sigma^2)\n", "c_rf = 2 / 52 # interest rate on risk-free asset\n", "l_rf = -0.3 # maximal borrowing of risk-free asset\n", "u_rf = 1 # maximal investment in risk-free asset" ] }, { "cell_type": "markdown", "id": "e3a880eb", "metadata": {}, "source": [ "### Decision Variables\n", "We require two types of decision variables:\n", "\n", "1. The proportions of capital invested among the considered stocks. The corresponding vector of positions is denoted by $x$ with its component $x_i$ denoting the proportion of capital invested in stock $i$.\n", "\n", "2. The proportion $x_\\text{rf}$ invested in the risk-free asset. This may be positive or negative. If positive, we gain a risk-free return; if negative, we pay interest on the borrowed amount.\n", "\n", "\n", "### Variable Bounds\n", "\n", "Each position must be nonnegative:\n", "\n", "$$ x_i\\geq 0 \\;, \\, i \\in S$$\n", "\n", "The risk-free position must be within its bounds:\n", "\n", "$$ \\ell_\\text{rf} \\leq x_\\text{rf} \\leq u_\\text{rf} $$\n", "\n", "Setting the upper bound $u_\\text{rf}=1$ means the portfolio is allowed to be fully invested in the risk-free asset." ] }, { "cell_type": "code", "execution_count": 6, "id": "8b819c98", "metadata": { "execution": { "iopub.execute_input": "2026-07-03T11:04:07.697883Z", "iopub.status.busy": "2026-07-03T11:04:07.697706Z", "iopub.status.idle": "2026-07-03T11:04:07.702697Z", "shell.execute_reply": "2026-07-03T11:04:07.701782Z" } }, "outputs": [], "source": [ "%%capture\n", "# Create an empty optimization model\n", "m = gp.Model()\n", "\n", "# Add variables: x[i] denotes the proportion invested in stock i\n", "x = m.addMVar(len(mu), name=\"x\")\n", "\n", "# Risk-free allocation\n", "x_rf = m.addVar(lb=l_rf, ub=u_rf, name=\"x_rf\")" ] }, { "cell_type": "markdown", "id": "26503978", "metadata": {}, "source": [ "### Constraints\n", "\n", "The budget constraint ensures that all capital (both initial and borrowed) is invested:\n", "\n", "$$\\sum_{i \\in S} x_i + x_\\text{rf} = 1$$\n", "\n", "The estimated risk must not exceed a prespecified maximal admissible level of variance $\\bar\\sigma^2$:\n", "\n", "$$x^\\top \\Sigma x \\leq \\bar\\sigma^2$$" ] }, { "cell_type": "code", "execution_count": 7, "id": "aa4ac569", "metadata": { "execution": { "iopub.execute_input": "2026-07-03T11:04:07.704456Z", "iopub.status.busy": "2026-07-03T11:04:07.704288Z", "iopub.status.idle": "2026-07-03T11:04:07.831521Z", "shell.execute_reply": "2026-07-03T11:04:07.830566Z" } }, "outputs": [], "source": [ "%%capture\n", "# Budget constraint: all investments sum up to 1\n", "m.addConstr(x.sum() + x_rf == 1, name=\"Budget_Constraint\")\n", "\n", "# Upper bound on variance\n", "risk_constr = m.addConstr(x @ Sigma.to_numpy() @ x <= V, name=\"Variance\")" ] }, { "cell_type": "markdown", "id": "1df9520a", "metadata": {}, "source": [ "### Objective Function\n", "\n", "The objective is to maximize the expected return of the portfolio. We need to account for risk-free returns and costs for borrowing cash:\n", "\\begin{equation*}\n", "\\max_x \\underbrace{c_\\text{rf} x_\\text{rf}}_{\\substack{\\text{cost for borrowing}\\\\\\text{or risk-free return}}} + \\underbrace{\\mu^\\top x}_\\text{expected return from stocks}\n", "\\end{equation*}" ] }, { "cell_type": "code", "execution_count": 8, "id": "c71fbd74", "metadata": { "execution": { "iopub.execute_input": "2026-07-03T11:04:07.833813Z", "iopub.status.busy": "2026-07-03T11:04:07.833564Z", "iopub.status.idle": "2026-07-03T11:04:07.837904Z", "shell.execute_reply": "2026-07-03T11:04:07.837057Z" } }, "outputs": [], "source": [ "m.setObjective(c_rf * x_rf + mu.to_numpy() @ x, gp.GRB.MAXIMIZE)" ] }, { "cell_type": "markdown", "id": "5eb02213", "metadata": {}, "source": [ "We now solve the optimization problem:" ] }, { "cell_type": "code", "execution_count": 9, "id": "9d88095d", "metadata": { "execution": { "iopub.execute_input": "2026-07-03T11:04:07.839594Z", "iopub.status.busy": "2026-07-03T11:04:07.839423Z", "iopub.status.idle": "2026-07-03T11:04:08.101793Z", "shell.execute_reply": "2026-07-03T11:04:08.100858Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Gurobi Optimizer version 13.0.2 build v13.0.2rc1 (linux64 - \"Ubuntu 24.04.4 LTS\")\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "CPU model: AMD EPYC 7763 64-Core Processor, instruction set [SSE2|AVX|AVX2]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Thread count: 1 physical cores, 2 logical processors, using up to 2 threads\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "WLS license 2443533 - registered to Gurobi GmbH\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Optimize a model with 1 rows, 463 columns and 463 nonzeros (Max)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Model fingerprint: 0xc5359fbf\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Model has 463 linear objective coefficients\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Model has 1 quadratic constraint\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Coefficient statistics:\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " Matrix range [1e+00, 1e+00]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " QMatrix range [3e-03, 1e+02]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " Objective range [4e-02, 6e-01]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " Bounds range [3e-01, 1e+00]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " RHS range [1e+00, 1e+00]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " QRHS range [4e+00, 4e+00]\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Presolve time: 0.03s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Presolved: 463 rows, 926 columns, 107878 nonzeros\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Presolved model has 1 second-order cone constraint\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Ordering time: 0.01s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Barrier statistics:\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " AA' NZ : 1.070e+05\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " Factor NZ : 1.074e+05 (roughly 1 MB of memory)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " Factor Ops : 3.319e+07 (less than 1 second per iteration)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " Threads : 1\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " Objective Residual\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Iter Primal Dual Primal Dual Compl Time\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 0 1.35263322e+01 1.38517017e+00 5.74e+01 5.93e-02 4.06e-02 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 1 1.72294179e+00 1.45034312e+00 5.96e+00 6.53e-08 5.06e-03 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 2 5.65980685e-01 7.13794601e-01 1.23e+00 5.20e-09 1.14e-03 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 3 2.76492884e-01 4.52651953e-01 1.35e-06 9.08e-10 1.90e-04 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 4 3.04811140e-01 3.88375383e-01 1.49e-12 2.76e-10 9.00e-05 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 5 3.32848350e-01 3.72022345e-01 1.44e-15 4.16e-16 4.22e-05 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 6 3.60987566e-01 3.62373983e-01 3.11e-15 2.78e-17 1.49e-06 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 7 3.61633548e-01 3.61707202e-01 2.89e-15 1.11e-16 7.94e-08 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 8 3.61665207e-01 3.61667012e-01 8.88e-16 1.58e-15 1.94e-09 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 9 3.61665648e-01 3.61666258e-01 6.48e-14 1.36e-13 6.56e-10 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Barrier solved model in 9 iterations and 0.25 seconds (0.59 work units)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Optimal objective 3.61665648e-01\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "m.optimize()" ] }, { "cell_type": "markdown", "id": "76a91575", "metadata": {}, "source": [ "Display basic solution data for all non-negligible positions; for clarity we've rounded all solution quantities to five digits." ] }, { "cell_type": "code", "execution_count": 10, "id": "7fbbcd56", "metadata": { "execution": { "iopub.execute_input": "2026-07-03T11:04:08.103598Z", "iopub.status.busy": "2026-07-03T11:04:08.103425Z", "iopub.status.idle": "2026-07-03T11:04:08.120231Z", "shell.execute_reply": "2026-07-03T11:04:08.119459Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Expected return: 0.361666\n", "Variance: 3.999995\n", "Solution time: 0.25 seconds\n", "\n", "Total investment: 1.180835\n", "Risk-free allocation: -0.180836\n", "Number of positions: 31\n" ] }, { "data": { "text/html": [ "
| \n", " | x | \n", "
|---|---|
| LLY | \n", "0.234514 | \n", "
| PGR | \n", "0.130495 | \n", "
| KDP | \n", "0.109867 | \n", "
| TMUS | \n", "0.061023 | \n", "
| NVDA | \n", "0.059568 | \n", "
| KR | \n", "0.059165 | \n", "
| DPZ | \n", "0.058688 | \n", "
| TTWO | \n", "0.052528 | \n", "
| WM | \n", "0.049898 | \n", "
| NOC | \n", "0.049281 | \n", "
| ODFL | \n", "0.039805 | \n", "
| ORLY | \n", "0.035924 | \n", "
| AVGO | \n", "0.034771 | \n", "
| WST | \n", "0.029062 | \n", "
| MSFT | \n", "0.027460 | \n", "
| ED | \n", "0.018787 | \n", "
| MKTX | \n", "0.018729 | \n", "
| AZO | \n", "0.018145 | \n", "
| MNST | \n", "0.015005 | \n", "
| CLX | \n", "0.014785 | \n", "
| META | \n", "0.013694 | \n", "
| HRL | \n", "0.010083 | \n", "
| NFLX | \n", "0.009696 | \n", "
| WMT | \n", "0.008265 | \n", "
| UNH | \n", "0.007742 | \n", "
| XEL | \n", "0.004691 | \n", "
| DXCM | \n", "0.004483 | \n", "
| CBOE | \n", "0.003388 | \n", "
| MOH | \n", "0.000747 | \n", "
| WEC | \n", "0.000449 | \n", "
| CME | \n", "0.000096 | \n", "