{ "cells": [ { "cell_type": "markdown", "id": "6b0b3c2f-3aaa-43fe-a8d0-2b696df5c1a6", "metadata": { "tags": [] }, "source": [ "```{index} single: application; portfolio\n", "```\n", "```{index} single: application; investment\n", "```\n", "```{index} single: solver; highs\n", "```\n", "```{index} chance constraints\n", "```\n", "# Markowitz portfolio optimization with chance constraints" ] }, { "cell_type": "code", "execution_count": null, "id": "5da22c67-5c34-4c3a-90a4-61222899e855", "metadata": { "tags": [] }, "outputs": [], "source": [ "# Install AMPL and solvers\n", "%pip install -q amplpy pandas\n", "\n", "SOLVER = \"highs\"\n", "\n", "from amplpy import AMPL, ampl_notebook\n", "\n", "ampl = ampl_notebook(\n", " modules=[\"highs\"], # modules to install\n", " license_uuid=\"default\", # license to use\n", ") # instantiate AMPL object and register magics" ] }, { "cell_type": "code", "execution_count": 2, "id": "b13edf26", "metadata": { "tags": [] }, "outputs": [], "source": [ "from IPython.display import Markdown, HTML\n", "import pandas as pd\n", "import numpy as np" ] }, { "cell_type": "markdown", "id": "a82bd88d-7b16-4c82-b0b7-5dbd0d81b71c", "metadata": { "jp-MarkdownHeadingCollapsed": true, "tags": [] }, "source": [ "We consider here another variant of the Markowitz portfolio optimization problem, which we already encountered in the context of convex optimization [here](../05/markowitz_portfolio.ipynb) and in the context of conic optimization [here](../06/markowitz_portfolio_revisited.ipynb).\n", "\n", "Assuming there is an initial unit capital $C$ that needs to be invested in a selection of $n$ possible assets, each of them with a unknown return rate $r_i$, $i=1,\\dots,n$. Let $x$ be the vector whose $i$-th component $x_i$ describes the fraction of the capital invested in asset $i$. The return rate vector $r$ can be modelled by a multivariate Gaussian distribution with mean $\\mu$ and covariance $\\Sigma$. Assume there is also a risk-free asset with guaranteed return rate $R$ and let $\\tilde{x}$ the amount invested in that asset. We want to determine the portfolio that maximizes the _expected_ return $\\mathbb{E} ( R \\tilde{x} + r^\\top x )$, which in view of our assumptions rewrites as $ \\mathbb{E} ( R \\tilde{x} + r^\\top x ) = R \\tilde{x} + \\mu^\\top x$.\n", "\n", "Additionally, we includ a _loss risk chance constraint_ of the form \n", "\n", "$$\n", "\\mathbb{P} ( r^\\top x \\leq \\alpha) \\leq \\beta.\n", "$$ \n", "\n", "Thanks to the assumption that $r$ is multivariate Gaussian, this chance constraint can be equivalently rewritten as\n", "\n", "$$\n", " \\mu^\\top x \\geq \\Phi^{-1}(1-\\beta) \\| \\Sigma^{1/2} r \\|_2 + \\alpha,\n", "$$\n", "\n", "which the theory guarantees to be a convex constraint if $\\beta \\leq 1/2$. The resulting portfolio optimization problem written as a SOCP is\n", "\n", "$$\n", "\\begin{align*}\n", " \\max \\; & R \\tilde{x} + \\mu^\\top x\\\\\n", " \\quad \\text{ s.t. } & \\Phi^{-1}(1-\\beta) \\| \\Sigma^{1/2} x \\|_2 \\leq \\mu^\\top x - \\alpha,\\\\\n", " & \\sum_{i=1}^n x_i + \\tilde{x} = C, \\\\\n", " & \\tilde{x} \\geq 0 \\\\\n", " & x_i \\geq 0 & i=1,\\dots,n.\n", "\\end{align*}\n", "$$\n", "\n", "We now implement an AMPL model and solve it for $n=3$, $\\alpha = 0.6$, $\\beta =0.3$, the given vector $\\mu$ and semi-definite positive covariance matrix $\\Sigma$." ] }, { "cell_type": "code", "execution_count": 3, "id": "1e9ef269", "metadata": {}, "outputs": [], "source": [ "# we import the inverse CDF or quantile function for the standard normal norm.ppf() from scipy.stats\n", "from scipy.stats import norm\n", "\n", "# We set our risk threshold and risk levels (sometimes you may get an infeasible problem if the chance\n", "# constraint becomes too tight!)\n", "alpha = 0.6\n", "beta = 0.3\n", "\n", "# We specify the initial capital, the risk-free return the number of risky assets, their expected returns, and their covariance matrix.\n", "C = 1\n", "R = 1.05\n", "n = 3\n", "mu = np.array([1.25, 1.15, 1.35])\n", "Sigma = np.array([[1.5, 0.5, 2], [0.5, 2, 0], [2, 0, 5]])\n", "\n", "# Check how dramatically the optimal solution changes if we assume i.i.d. deviations for the returns.\n", "# Sigma = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])\n", "\n", "# If you want to change covariance matrix, make sure you input a semi-definite positive one.\n", "# The easiest way to generate a random covariance matrix is first generating a random m x m matrix A\n", "# and then taking the matrix A^T A (which is always semi-definite positive)\n", "# m = 3\n", "# A = np.random.rand(m, m)\n", "# Sigma = A.T @ A" ] }, { "cell_type": "code", "execution_count": 4, "id": "3936f9b5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Overwriting markowitz_chanceconstraints.mod\n" ] } ], "source": [ "%%writefile markowitz_chanceconstraints.mod\n", "\n", "set N;\n", "\n", "param Sigma{N, N};\n", "param mu{N};\n", "param C;\n", "param R;\n", "param gamma;\n", "\n", "param phi_val;\n", "param alpha;\n", "\n", "var x_tilde >= 0;\n", "var x{N} >= 0;\n", "\n", "maximize expected_return:\n", " sum{i in N} mu[i] * x[i];\n", " \n", "subject to bounded_variance:\n", " phi_val * (sum{i in N, j in N} x[i] * Sigma[i, j] * x[j]) <= sum{i in N} mu[i] * x[i] - alpha;\n", "\n", "subject to total_assets:\n", " sum{i in N} x[i] + x_tilde = C;" ] }, { "cell_type": "code", "execution_count": 5, "id": "0e194e8d", "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "HiGHS 1.5.1: HiGHS 1.5.1: optimal solution; objective 1.229721418\n", "860 simplex iterations\n", "5 branching nodes\n", "absmipgap=8.14792e-06, relmipgap=6.62583e-06\n" ] }, { "data": { "text/markdown": [ "**Solver result:** *solved*" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/markdown": [ "**Solution:** $\\tilde x = 0.000$, $x_1 = 0.797$, $x_2 = 0.203$, $x_3 = 0.000$" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/markdown": [ "**Maximizes objective value to:** $1.23$" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "def markowitz_chanceconstraints(alpha, beta, mu, Sigma):\n", " m = AMPL()\n", " m.read(\"markowitz_chanceconstraints.mod\")\n", "\n", " Sigma_df = pd.DataFrame(Sigma, index=range(n), columns=range(n))\n", " phi_val = norm.ppf(1 - beta)\n", "\n", " m.set[\"N\"] = range(n)\n", "\n", " m.param[\"phi_val\"] = phi_val\n", " m.param[\"alpha\"] = alpha\n", " m.param[\"C\"] = C\n", " m.param[\"R\"] = R\n", " m.param[\"mu\"] = mu\n", " m.param[\"Sigma\"] = Sigma_df\n", "\n", " m.option[\"solver\"] = SOLVER\n", "\n", " m.solve()\n", " solve_result = m.get_value(\"solve_result\")\n", "\n", " return solve_result, m\n", "\n", "\n", "result, model = markowitz_chanceconstraints(alpha, beta, mu, Sigma)\n", "\n", "x_tilde = model.var[\"x_tilde\"].value()\n", "x = model.var[\"x\"].to_dict()\n", "\n", "display(Markdown(f\"**Solver result:** *{result}*\"))\n", "display(\n", " Markdown(\n", " f\"**Solution:** $\\\\tilde x = {x_tilde:.3f}$, $x_1 = {x[0]:.3f}$, $x_2 = {x[1]:.3f}$, $x_3 = {x[2]:.3f}$\"\n", " )\n", ")\n", "display(\n", " Markdown(\n", " f\"**Maximizes objective value to:** ${model.obj['expected_return'].value():.2f}$\"\n", " )\n", ")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "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.10.6" }, "varInspector": { "cols": { "lenName": 16, "lenType": 16, "lenVar": 40 }, "kernels_config": { "python": { "delete_cmd_postfix": "", "delete_cmd_prefix": "del ", "library": "var_list.py", "varRefreshCmd": "print(var_dic_list())" }, "r": { "delete_cmd_postfix": ") ", "delete_cmd_prefix": "rm(", "library": "var_list.r", "varRefreshCmd": "cat(var_dic_list()) " } }, "types_to_exclude": [ "module", "function", "builtin_function_or_method", "instance", "_Feature" ], "window_display": false } }, "nbformat": 4, "nbformat_minor": 5 }