{ "cells": [ { "cell_type": "markdown", "id": "511dff2c", "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": "2bd010ba", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:04:33.265046Z", "iopub.status.busy": "2025-01-31T10:04:33.264815Z", "iopub.status.idle": "2025-01-31T10:04:34.043695Z", "shell.execute_reply": "2025-01-31T10:04:34.042954Z" }, "nbsphinx": "hidden" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Requirement already satisfied: numpy in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (2.2.2)\r\n", "Requirement already satisfied: scipy in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (1.15.1)\r\n", "Requirement already satisfied: gurobipy in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (11.0.3)\r\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Requirement already satisfied: pandas in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (2.2.3)\r\n", "Requirement already satisfied: matplotlib in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (3.10.0)\r\n", "Requirement already satisfied: python-dateutil>=2.8.2 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from pandas) (2.9.0.post0)\r\n", "Requirement already satisfied: pytz>=2020.1 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from pandas) (2025.1)\r\n", "Requirement already satisfied: tzdata>=2022.7 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from pandas) (2025.1)\r\n", "Requirement already satisfied: contourpy>=1.0.1 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from matplotlib) (1.3.1)\r\n", "Requirement already satisfied: cycler>=0.10 in /opt/hostedtoolcache/Python/3.11.11/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.11/x64/lib/python3.11/site-packages (from matplotlib) (4.55.8)\r\n", "Requirement already satisfied: kiwisolver>=1.3.1 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from matplotlib) (1.4.8)\r\n", "Requirement already satisfied: packaging>=20.0 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from matplotlib) (24.2)\r\n", "Requirement already satisfied: pillow>=8 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from matplotlib) (11.1.0)\r\n", "Requirement already satisfied: pyparsing>=2.3.1 in /opt/hostedtoolcache/Python/3.11.11/x64/lib/python3.11/site-packages (from matplotlib) (3.2.1)\r\n", "Requirement already satisfied: six>=1.5 in /opt/hostedtoolcache/Python/3.11.11/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": "4fdd601f", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:04:34.045752Z", "iopub.status.busy": "2025-01-31T10:04:34.045560Z", "iopub.status.idle": "2025-01-31T10:04:34.663728Z", "shell.execute_reply": "2025-01-31T10:04:34.663037Z" } }, "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": "70bcc356", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:04:34.665831Z", "iopub.status.busy": "2025-01-31T10:04:34.665571Z", "iopub.status.idle": "2025-01-31T10:04:34.673938Z", "shell.execute_reply": "2025-01-31T10:04:34.673366Z" }, "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": "38937ca0", "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": "26df3e61", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:04:34.675771Z", "iopub.status.busy": "2025-01-31T10:04:34.675596Z", "iopub.status.idle": "2025-01-31T10:04:34.681425Z", "shell.execute_reply": "2025-01-31T10:04:34.680823Z" } }, "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": "ac692b61", "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": "91c0f177", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:04:34.683373Z", "iopub.status.busy": "2025-01-31T10:04:34.683052Z", "iopub.status.idle": "2025-01-31T10:04:34.685833Z", "shell.execute_reply": "2025-01-31T10:04:34.685439Z" } }, "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": "1b4eb1c9", "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": "100bd681", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:04:34.687511Z", "iopub.status.busy": "2025-01-31T10:04:34.687341Z", "iopub.status.idle": "2025-01-31T10:04:34.692071Z", "shell.execute_reply": "2025-01-31T10:04:34.691495Z" } }, "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": "d7272afa", "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": "2c036aac", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:04:34.693817Z", "iopub.status.busy": "2025-01-31T10:04:34.693630Z", "iopub.status.idle": "2025-01-31T10:04:34.861189Z", "shell.execute_reply": "2025-01-31T10:04:34.860624Z" } }, "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": "2dd9fccd", "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": "83066763", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:04:34.863823Z", "iopub.status.busy": "2025-01-31T10:04:34.863523Z", "iopub.status.idle": "2025-01-31T10:04:34.868447Z", "shell.execute_reply": "2025-01-31T10:04:34.867914Z" } }, "outputs": [], "source": [ "m.setObjective(c_rf * x_rf + mu.to_numpy() @ x, gp.GRB.MAXIMIZE)" ] }, { "cell_type": "markdown", "id": "8257be31", "metadata": {}, "source": [ "We now solve the optimization problem:" ] }, { "cell_type": "code", "execution_count": 9, "id": "7935d52d", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:04:34.871386Z", "iopub.status.busy": "2025-01-31T10:04:34.870629Z", "iopub.status.idle": "2025-01-31T10:04:35.131845Z", "shell.execute_reply": "2025-01-31T10:04:35.131233Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Gurobi Optimizer version 11.0.3 build v11.0.3rc0 (linux64 - \"Ubuntu 24.04.1 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\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Model fingerprint: 0xc5359fbf\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": [ "Presolve time: 0.04s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Presolved: 464 rows, 926 columns, 107879 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.074e+05\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " Factor NZ : 1.079e+05 (roughly 1 MB of memory)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " Factor Ops : 3.341e+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.36004448e+01 1.52173653e-01 5.78e+01 6.79e-01 4.09e-02 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 1 1.85351518e+00 1.75506108e+00 6.76e+00 7.47e-07 6.08e-03 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 2 5.26444573e-01 9.73208598e-01 1.18e+00 1.48e-07 1.50e-03 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 3 2.58180913e-01 5.35781906e-01 1.30e-06 1.75e-08 2.99e-04 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 4 2.74073099e-01 4.36667485e-01 1.43e-12 1.15e-09 1.75e-04 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 5 3.46759821e-01 3.86258876e-01 2.22e-15 2.28e-10 4.26e-05 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 6 3.60280539e-01 3.63495642e-01 1.05e-15 2.37e-11 3.46e-06 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 7 3.61562696e-01 3.61702796e-01 6.49e-15 9.30e-13 1.51e-07 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 8 3.61658821e-01 3.61673013e-01 7.21e-13 2.39e-14 1.53e-08 0s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ " 9 3.61665753e-01 3.61665955e-01 6.12e-13 5.33e-15 2.18e-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.57 work units)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Optimal objective 3.61665753e-01\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "m.optimize()" ] }, { "cell_type": "markdown", "id": "efa095ce", "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": "e35695e4", "metadata": { "execution": { "iopub.execute_input": "2025-01-31T10:04:35.133808Z", "iopub.status.busy": "2025-01-31T10:04:35.133634Z", "iopub.status.idle": "2025-01-31T10:04:35.148785Z", "shell.execute_reply": "2025-01-31T10:04:35.148205Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Expected return: 0.361666\n", "Variance: 3.999999\n", "Solution time: 0.25 seconds\n", "\n", "Total investment: 1.180790\n", "Risk-free allocation: -0.180793\n", "Number of positions: 31\n" ] }, { "data": { "text/html": [ "
\n", " | x | \n", "
---|---|
LLY | \n", "0.234378 | \n", "
PGR | \n", "0.130469 | \n", "
KDP | \n", "0.109984 | \n", "
TMUS | \n", "0.061071 | \n", "
NVDA | \n", "0.059577 | \n", "
KR | \n", "0.059146 | \n", "
DPZ | \n", "0.058659 | \n", "
TTWO | \n", "0.052455 | \n", "
WM | \n", "0.050070 | \n", "
NOC | \n", "0.049313 | \n", "
ODFL | \n", "0.039862 | \n", "
ORLY | \n", "0.035956 | \n", "
AVGO | \n", "0.034829 | \n", "
WST | \n", "0.029040 | \n", "
MSFT | \n", "0.027461 | \n", "
ED | \n", "0.018846 | \n", "
MKTX | \n", "0.018722 | \n", "
AZO | \n", "0.018125 | \n", "
MNST | \n", "0.015005 | \n", "
CLX | \n", "0.014768 | \n", "
META | \n", "0.013696 | \n", "
HRL | \n", "0.010052 | \n", "
NFLX | \n", "0.009693 | \n", "
WMT | \n", "0.008259 | \n", "
UNH | \n", "0.007706 | \n", "
DXCM | \n", "0.004475 | \n", "
XEL | \n", "0.004328 | \n", "
CBOE | \n", "0.003246 | \n", "
MOH | \n", "0.000780 | \n", "
WEC | \n", "0.000628 | \n", "
CME | \n", "0.000189 | \n", "