{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "LaTeX macros (hidden cell)\n", "$\n", "\\newcommand{\\Q}{\\mathcal{Q}}\n", "\\newcommand{\\ECov}{\\boldsymbol{\\Sigma}}\n", "\\newcommand{\\EMean}{\\boldsymbol{\\mu}}\n", "\\newcommand{\\EAlpha}{\\boldsymbol{\\alpha}}\n", "\\newcommand{\\EBeta}{\\boldsymbol{\\beta}}\n", "$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Imports and configuration" ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "scrolled": false }, "outputs": [], "source": [ "import sys\n", "import os\n", "import re\n", "import glob\n", "import datetime as dt\n", "\n", "import numpy as np\n", "import pandas as pd\n", "%matplotlib inline\n", "import matplotlib\n", "import matplotlib.pyplot as plt\n", "from matplotlib.colors import LinearSegmentedColormap\n", "\n", "from mosek.fusion import *\n", "import mosek.fusion.pythonic # Requires MOSEK >= 10.2\n", "\n", "from notebook.services.config import ConfigManager\n", "\n", "from portfolio_tools import data_download, DataReader" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3.6.9 (default, Jan 26 2021, 15:33:00) \n", "[GCC 8.4.0]\n", "matplotlib: 3.3.4\n" ] } ], "source": [ "# Version checks\n", "print(sys.version)\n", "print('matplotlib: {}'.format(matplotlib.__version__))\n", "\n", "# Jupyter configuration\n", "c = ConfigManager()\n", "c.update('notebook', {\"CodeCell\": {\"cm_config\": {\"autoCloseBrackets\": False}}}) \n", "\n", "# Numpy options\n", "np.set_printoptions(precision=5, linewidth=120, suppress=True)\n", "\n", "# Pandas options\n", "pd.set_option('display.max_rows', None)\n", "\n", "# Matplotlib options\n", "plt.rcParams['figure.figsize'] = [12, 8]\n", "plt.rcParams['figure.dpi'] = 200" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Prepare input data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this example, the input data is given. It consists of the vector $\\EMean$ of expected returns, and the covariance matrix $\\ECov$." ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [], "source": [ "# Linear return statistics on the investment horizon\n", "mu = np.array([0.07197349, 0.15518171, 0.17535435, 0.0898094 , 0.42895777, 0.39291844, 0.32170722, 0.18378628])\n", "Sigma = np.array([\n", " [0.09460323, 0.03735969, 0.03488376, 0.03483838, 0.05420885, 0.03682539, 0.03209623, 0.03271886],\n", " [0.03735969, 0.07746293, 0.03868215, 0.03670678, 0.03816653, 0.03634422, 0.0356449 , 0.03422235],\n", " [0.03488376, 0.03868215, 0.06241065, 0.03364444, 0.03949475, 0.03690811, 0.03383847, 0.02433733],\n", " [0.03483838, 0.03670678, 0.03364444, 0.06824955, 0.04017978, 0.03348263, 0.04360484, 0.03713009],\n", " [0.05420885, 0.03816653, 0.03949475, 0.04017978, 0.17243352, 0.07886889, 0.06999607, 0.05010711],\n", " [0.03682539, 0.03634422, 0.03690811, 0.03348263, 0.07886889, 0.09093307, 0.05364518, 0.04489357],\n", " [0.03209623, 0.0356449 , 0.03383847, 0.04360484, 0.06999607, 0.05364518, 0.09649728, 0.04419974],\n", " [0.03271886, 0.03422235, 0.02433733, 0.03713009, 0.05010711, 0.04489357, 0.04419974, 0.08159633]\n", " ])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Define the optimization model" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The optimization problem we would like to solve is \n", "$$\n", " \\begin{array}{lrcl}\n", " \\mbox{maximize} & \\EMean^\\mathsf{T}\\mathbf{x} & &\\\\\n", " \\mbox{subject to} & \\left(\\gamma^2, \\frac{1}{2}, \\mathbf{G}^\\mathsf{T}\\mathbf{x}\\right) & \\in & \\Q_\\mathrm{r}^{N+2},\\\\\n", " & \\mathbf{1}^\\mathsf{T}\\mathbf{x} & = & 1,\\\\\n", " & \\mathbf{x} & \\geq & 0.\\\\\n", " \\end{array}\n", "$$\n", "\n", "Here we define this model in MOSEK Fusion." ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [], "source": [ "# Define function solving the optimization model\n", "def Markowitz(N, m, G, gamma2):\n", " with Model(\"markowitz\") as M:\n", " # Settings\n", " M.setLogHandler(sys.stdout) \n", "\n", " # Decision variable (fraction of holdings in each security)\n", " # The variable x is restricted to be positive, which imposes the constraint of no short-selling. \n", " x = M.variable(\"x\", N, Domain.greaterThan(0.0)) \n", "\n", " # Budget constraint\n", " M.constraint('budget', Expr.sum(x) == 1)\n", "\n", " # Objective \n", " M.objective('obj', ObjectiveSense.Maximize, x.T @ m)\n", "\n", " # Imposes a bound on the risk\n", " M.constraint('risk', Expr.vstack(gamma2, 0.5, G.T @ x), Domain.inRotatedQCone())\n", "\n", " # Solve optimization\n", " M.solve()\n", " \n", " # Check if the solution is an optimal point\n", " solsta = M.getPrimalSolutionStatus()\n", " if (solsta != SolutionStatus.Optimal):\n", " # See https://docs.mosek.com/latest/pythonfusion/accessing-solution.html about handling solution statuses.\n", " raise Exception(\"Unexpected solution status!\") \n", " \n", " returns = M.primalObjValue()\n", " portfolio = x.level()\n", " \n", " return returns, portfolio" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Run the optimization" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Define the parameters" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The problem parameters are the number of securities $N$ and the risk limit $\\gamma^2$. " ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [], "source": [ "N = mu.shape[0] # Number of securities\n", "gamma2 = 0.05 # Risk limit (variance)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Factorize the covariance matrix" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here we factorize $\\ECov$ because the model is defined in conic form, and it expects a matrix $G$ such that $\\ECov = GG^\\mathsf{T}$." ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [], "source": [ "G = np.linalg.cholesky(Sigma) # Cholesky factor of S to use in conic risk constraint " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Solve the optimization problem" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next we call the function that defines the Fusion model and runs the optimization." ] }, { "cell_type": "code", "execution_count": 35, "metadata": { "scrolled": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Problem\n", " Name : markowitz \n", " Objective sense : maximize \n", " Type : CONIC (conic optimization problem)\n", " Constraints : 1 \n", " Affine conic cons. : 1 \n", " Disjunctive cons. : 0 \n", " Cones : 0 \n", " Scalar variables : 9 \n", " Matrix variables : 0 \n", " Integer variables : 0 \n", "\n", "Optimizer started.\n", "Presolve started.\n", "Eliminator started.\n", "Freed constraints in eliminator : 0\n", "Eliminator terminated.\n", "Linear dependency checker started.\n", "Linear dependency checker terminated.\n", "Eliminator started.\n", "Freed constraints in eliminator : 0\n", "Eliminator terminated.\n", "Eliminator - tries : 2 time : 0.00 \n", "Lin. dep. - tries : 1 time : 0.00 \n", "Lin. dep. - number : 0 \n", "Presolve terminated. Time: 0.00 \n", "Problem\n", " Name : markowitz \n", " Objective sense : maximize \n", " Type : CONIC (conic optimization problem)\n", " Constraints : 1 \n", " Affine conic cons. : 1 \n", " Disjunctive cons. : 0 \n", " Cones : 0 \n", " Scalar variables : 9 \n", " Matrix variables : 0 \n", " Integer variables : 0 \n", "\n", "Optimizer - threads : 20 \n", "Optimizer - solved problem : the primal \n", "Optimizer - Constraints : 8\n", "Optimizer - Cones : 1\n", "Optimizer - Scalar variables : 16 conic : 9 \n", "Optimizer - Semi-definite variables: 0 scalarized : 0 \n", "Factor - setup time : 0.00 dense det. time : 0.00 \n", "Factor - ML order time : 0.00 GP order time : 0.00 \n", "Factor - nonzeros before factor : 36 after factor : 36 \n", "Factor - dense dim. : 0 flops : 6.00e+02 \n", "ITE PFEAS DFEAS GFEAS PRSTATUS POBJ DOBJ MU TIME \n", "0 1.0e+00 1.6e+00 1.2e+00 0.00e+00 0.000000000e+00 2.236067977e-01 1.0e+00 0.01 \n", "1 1.9e-01 3.1e-01 5.2e-02 7.17e-01 3.603783622e-01 5.403581465e-01 1.9e-01 0.02 \n", "2 7.2e-02 1.1e-01 1.1e-02 2.13e+00 3.359129710e-01 3.727555242e-01 7.2e-02 0.02 \n", "3 3.9e-02 6.1e-02 4.4e-03 1.55e+00 2.940729650e-01 3.097931685e-01 3.9e-02 0.03 \n", "4 9.7e-03 1.5e-02 5.2e-04 1.34e+00 2.834101970e-01 2.867604611e-01 9.7e-03 0.03 \n", "5 3.2e-03 5.1e-03 1.0e-04 1.09e+00 2.789097434e-01 2.799847756e-01 3.2e-03 0.03 \n", "6 5.2e-04 8.2e-04 6.7e-06 9.93e-01 2.770268510e-01 2.771990381e-01 5.2e-04 0.03 \n", "7 1.1e-04 1.8e-04 6.6e-07 9.95e-01 2.767639389e-01 2.768013245e-01 1.1e-04 0.03 \n", "8 1.9e-06 3.0e-06 1.4e-09 1.00e+00 2.767188047e-01 2.767194670e-01 1.9e-06 0.03 \n", "9 1.9e-08 3.0e-08 1.4e-12 1.00e+00 2.767173194e-01 2.767173259e-01 1.9e-08 0.03 \n", "Optimizer terminated. Time: 0.04 \n", "\n", "\n", "Interior-point solution summary\n", " Problem status : PRIMAL_AND_DUAL_FEASIBLE\n", " Solution status : OPTIMAL\n", " Primal. obj: 2.7671731936e-01 nrm: 1e+00 Viol. con: 8e-09 var: 0e+00 acc: 2e-09 \n", " Dual. obj: 2.7671732594e-01 nrm: 6e+00 Viol. con: 0e+00 var: 4e-09 acc: 0e+00 \n", "========================\n", "\n", "RESULTS:\n", "Optimal expected portfolio return: 27.6717%\n", "Optimal portfolio weights: [0. 0.09127 0.2691 0. 0.02529 0.32163 0.17653 0.11618]\n", "Sum of weights: 0.9999999915763336\n" ] } ], "source": [ "# Run optimization \n", "f, x = Markowitz(N, mu, G, gamma2)\n", "print(\"========================\\n\")\n", "print(\"RESULTS:\")\n", "print(f\"Optimal expected portfolio return: {f*100:.4f}%\")\n", "print(f\"Optimal portfolio weights: {x}\")\n", "print(f\"Sum of weights: {np.sum(x)}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Test result" ] }, { "cell_type": "code", "execution_count": 36, "metadata": { "scrolled": true }, "outputs": [], "source": [ "expected_x = np.array([0., 0.09126, 0.26911, 0., 0.02531, 0.32162, 0.17652, 0.11618])\n", "diff = np.sum(np.abs(expected_x - x))\n", "assert diff < 1e-4, f\"Resulting portfolio does not match expected one. Difference is {diff}\"" ] } ], "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.9.7" } }, "nbformat": 4, "nbformat_minor": 2 }