{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Portfolio Selection Optimization\n",
"This model is an example of the classic [Markowitz portfolio selection optimization model](https://en.wikipedia.org/wiki/Markowitz_model). We want to find the fraction of the portfolio to invest among a set of stocks that balances risk and return. It is a Quadratic Programming (QP) model with vector and matrix data for returns and risk, respectively. This is best suited to a matrix formulation, so we use the Gurobi Python *matrix* interface. The basic model is fairly simple, so we also solve it parametrically to find the efficient frontier.\n",
"\n",
"**Download the Repository**
\n",
"You can download the repository containing this and other examples by clicking [here](https://github.com/Gurobi/modeling-examples/archive/master.zip). \n",
"\n",
"\n",
"## Model Formulation\n",
"### Parameters\n",
"\n",
"We use the [Greek values](https://en.wikipedia.org/wiki/Greeks_\\(finance\\)) that are traditional in finance:\n",
"\n",
"- $\\delta$: n-element vector measuring the change in price for each stock\n",
"- $\\sigma$: n x n matrix measuring the covariance among stocks\n",
"\n",
"There is one additional parameter when solving the model parametrically:\n",
"\n",
"- r: target return\n",
"\n",
"\n",
"### Decision Variables\n",
"- $x \\ge 0$: n-element vector where each element represents the fraction of the porfolio to invest in each stock\n",
"\n",
"### Objective Function\n",
"Minimize the total risk, a convex quadratic function:\n",
"\n",
"\\begin{equation}\n",
"\\min x^t \\cdot \\sigma \\cdot x\n",
"\\end{equation}\n",
"\n",
"### Constraints\n",
"\n",
"Allocate the entire portfolio: the total investments should be 1.0 (100%), where $e$ is a unit vector (all 1's):\n",
"\n",
"\\begin{equation}\n",
"e \\cdot x = 1\n",
"\\end{equation}\n",
"\n",
"\n",
"Return: When we solve the model parametrically for different return values $r$, we add a constraint on the target return:\n",
"\n",
"\\begin{equation}\n",
"\\delta \\cdot x = r\n",
"\\end{equation}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Python Implementation\n",
"### Stock data\n",
"Use [yfinance](https://pypi.org/project/yfinance/) library to get the latest 2 years of _actual stock data_ from the 20 most profitable US companies, [according to Wikipedia in April 2021](https://en.wikipedia.org/wiki/List_of_largest_companies_in_the_United_States_by_revenue#List_of_companies_by_profit)."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Requirement already satisfied: gurobipy in c:\\users\\pancho\\anaconda3\\envs\\proyectonestle\\lib\\site-packages (10.0.3)Note: you may need to restart the kernel to use updated packages.\n",
"\n",
"Collecting yfinance\n",
" Obtaining dependency information for yfinance from https://files.pythonhosted.org/packages/e6/b3/388ab967a387cc92926f70e97688dd9a7189b29a0773db815ffc5289e2b5/yfinance-0.2.31-py2.py3-none-any.whl.metadata\n",
" Downloading yfinance-0.2.31-py2.py3-none-any.whl.metadata (11 kB)\n",
"Requirement already satisfied: pandas>=1.3.0 in c:\\users\\pancho\\anaconda3\\envs\\proyectonestle\\lib\\site-packages (from yfinance) (1.5.3)\n",
"Requirement already satisfied: numpy>=1.16.5 in c:\\users\\pancho\\anaconda3\\envs\\proyectonestle\\lib\\site-packages (from yfinance) (1.24.3)\n",
"Requirement already satisfied: requests>=2.31 in c:\\users\\pancho\\anaconda3\\envs\\proyectonestle\\lib\\site-packages (from yfinance) (2.31.0)\n",
"Collecting multitasking>=0.0.7 (from yfinance)\n",
" Downloading multitasking-0.0.11-py3-none-any.whl (8.5 kB)\n",
"Requirement already satisfied: lxml>=4.9.1 in c:\\users\\pancho\\anaconda3\\envs\\proyectonestle\\lib\\site-packages (from yfinance) (4.9.3)\n",
"Requirement already satisfied: appdirs>=1.4.4 in c:\\users\\pancho\\anaconda3\\envs\\proyectonestle\\lib\\site-packages (from yfinance) (1.4.4)\n",
"Requirement already satisfied: pytz>=2022.5 in c:\\users\\pancho\\anaconda3\\envs\\proyectonestle\\lib\\site-packages (from yfinance) (2022.7)\n",
"Collecting frozendict>=2.3.4 (from yfinance)\n",
" Downloading frozendict-2.3.8-py311-none-any.whl (14 kB)\n",
"Collecting peewee>=3.16.2 (from yfinance)\n",
" Downloading peewee-3.17.0.tar.gz (2.9 MB)\n",
" ---------------------------------------- 0.0/2.9 MB ? eta -:--:--\n",
" --------------------------------------- 0.0/2.9 MB 991.0 kB/s eta 0:00:03\n",
" -- ------------------------------------- 0.2/2.9 MB 2.4 MB/s eta 0:00:02\n",
" ------ --------------------------------- 0.5/2.9 MB 3.5 MB/s eta 0:00:01\n",
" --------- ------------------------------ 0.7/2.9 MB 4.2 MB/s eta 0:00:01\n",
" ------------- -------------------------- 1.0/2.9 MB 4.2 MB/s eta 0:00:01\n",
" ----------------- ---------------------- 1.3/2.9 MB 4.8 MB/s eta 0:00:01\n",
" --------------------- ------------------ 1.6/2.9 MB 4.9 MB/s eta 0:00:01\n",
" --------------------------- ------------ 2.0/2.9 MB 5.3 MB/s eta 0:00:01\n",
" ------------------------------- -------- 2.3/2.9 MB 5.5 MB/s eta 0:00:01\n",
" ----------------------------------- ---- 2.6/2.9 MB 5.8 MB/s eta 0:00:01\n",
" ------------------------------------- -- 2.8/2.9 MB 5.5 MB/s eta 0:00:01\n",
" ---------------------------------------- 2.9/2.9 MB 5.4 MB/s eta 0:00:00\n",
" Installing build dependencies: started\n",
" Installing build dependencies: finished with status 'done'\n",
" Getting requirements to build wheel: started\n",
" Getting requirements to build wheel: finished with status 'done'\n",
" Preparing metadata (pyproject.toml): started\n",
" Preparing metadata (pyproject.toml): finished with status 'done'\n",
"Requirement already satisfied: beautifulsoup4>=4.11.1 in c:\\users\\pancho\\anaconda3\\envs\\proyectonestle\\lib\\site-packages (from yfinance) (4.12.2)\n",
"Collecting html5lib>=1.1 (from yfinance)\n",
" Downloading html5lib-1.1-py2.py3-none-any.whl (112 kB)\n",
" ---------------------------------------- 0.0/112.2 kB ? eta -:--:--\n",
" -------------------------------------- 112.2/112.2 kB 3.3 MB/s eta 0:00:00\n",
"Requirement already satisfied: soupsieve>1.2 in c:\\users\\pancho\\anaconda3\\envs\\proyectonestle\\lib\\site-packages (from beautifulsoup4>=4.11.1->yfinance) (2.4)\n",
"Requirement already satisfied: six>=1.9 in c:\\users\\pancho\\anaconda3\\envs\\proyectonestle\\lib\\site-packages (from html5lib>=1.1->yfinance) (1.16.0)\n",
"Requirement already satisfied: webencodings in c:\\users\\pancho\\anaconda3\\envs\\proyectonestle\\lib\\site-packages (from html5lib>=1.1->yfinance) (0.5.1)\n",
"Requirement already satisfied: python-dateutil>=2.8.1 in c:\\users\\pancho\\anaconda3\\envs\\proyectonestle\\lib\\site-packages (from pandas>=1.3.0->yfinance) (2.8.2)\n",
"Requirement already satisfied: charset-normalizer<4,>=2 in c:\\users\\pancho\\anaconda3\\envs\\proyectonestle\\lib\\site-packages (from requests>=2.31->yfinance) (2.0.4)\n",
"Requirement already satisfied: idna<4,>=2.5 in c:\\users\\pancho\\anaconda3\\envs\\proyectonestle\\lib\\site-packages (from requests>=2.31->yfinance) (3.4)\n",
"Requirement already satisfied: urllib3<3,>=1.21.1 in c:\\users\\pancho\\anaconda3\\envs\\proyectonestle\\lib\\site-packages (from requests>=2.31->yfinance) (1.26.16)\n",
"Requirement already satisfied: certifi>=2017.4.17 in c:\\users\\pancho\\anaconda3\\envs\\proyectonestle\\lib\\site-packages (from requests>=2.31->yfinance) (2023.7.22)\n",
"Downloading yfinance-0.2.31-py2.py3-none-any.whl (65 kB)\n",
" ---------------------------------------- 0.0/65.6 kB ? eta -:--:--\n",
" ---------------------------------------- 65.6/65.6 kB 3.5 MB/s eta 0:00:00\n",
"Building wheels for collected packages: peewee\n",
" Building wheel for peewee (pyproject.toml): started\n",
" Building wheel for peewee (pyproject.toml): finished with status 'done'\n",
" Created wheel for peewee: filename=peewee-3.17.0-py3-none-any.whl size=135766 sha256=5974096996eb451920df562db0e487aaaadf1e6bcbdf3cb7d241d48eb0a55d81\n",
" Stored in directory: c:\\users\\pancho\\appdata\\local\\pip\\cache\\wheels\\02\\20\\23\\74a10d0cd31f5d41c19b92ddf4c138ceff01b9f4675f19dbf5\n",
"Successfully built peewee\n",
"Installing collected packages: peewee, multitasking, html5lib, frozendict, yfinance\n",
"Successfully installed frozendict-2.3.8 html5lib-1.1 multitasking-0.0.11 peewee-3.17.0 yfinance-0.2.31\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
" WARNING: The script sample.exe is installed in 'C:\\Users\\Pancho\\anaconda3\\envs\\ProyectoNestle\\Scripts' which is not on PATH.\n",
" Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.\n"
]
}
],
"source": [
"%pip install gurobipy yfinance"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[*********************100%%**********************] 20 of 20 completed\n"
]
}
],
"source": [
"import yfinance as yf\n",
"\n",
"stocks = ['BRK-A', 'AAPL', 'MSFT', 'JPM', 'GOOG', 'BAC', 'INTC', 'WFC',\n",
" 'C', 'VZ', 'META', 'PFE', 'JNJ', 'WMT', 'XOM',\n",
" 'FNMA', 'T', 'UNH', 'CMCSA', 'V' ]\n",
"\n",
"data = yf.download(stocks, period='2y')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Compute Greeks\n",
"Using the downloaded stock data, find the delta (return), sigma (covariance) and standard deviation values for stock prices:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"closes = np.transpose(np.array(data.Close)) # matrix of daily closing prices\n",
"absdiff = np.diff(closes) # change in closing price each day\n",
"reldiff = np.divide(absdiff, closes[:,:-1]) # relative change in daily closing price\n",
"delta = np.mean(reldiff, axis=1) # mean price change\n",
"sigma = np.cov(reldiff) # covariance (standard deviations)\n",
"std = np.std(reldiff, axis=1) # standard deviation"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Minimize risk by solving QP model"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Set parameter Username\n",
"Academic license - for non-commercial use only - expires 2023-12-07\n",
"Gurobi Optimizer version 10.0.3 build v10.0.3rc0 (win64)\n",
"\n",
"CPU model: AMD Ryzen 9 5900HX with Radeon Graphics, instruction set [SSE2|AVX|AVX2]\n",
"Thread count: 8 physical cores, 16 logical processors, using up to 16 threads\n",
"\n",
"Optimize a model with 1 rows, 20 columns and 20 nonzeros\n",
"Model fingerprint: 0x8083dace\n",
"Model has 210 quadratic objective terms\n",
"Coefficient statistics:\n",
" Matrix range [1e+00, 1e+00]\n",
" Objective range [0e+00, 0e+00]\n",
" QObjective range [8e-06, 4e-03]\n",
" Bounds range [0e+00, 0e+00]\n",
" RHS range [1e+00, 1e+00]\n",
"Presolve time: 0.02s\n",
"Presolved: 1 rows, 20 columns, 20 nonzeros\n",
"Presolved model has 210 quadratic objective terms\n",
"Ordering time: 0.00s\n",
"\n",
"Barrier statistics:\n",
" Free vars : 19\n",
" AA' NZ : 1.900e+02\n",
" Factor NZ : 2.100e+02\n",
" Factor Ops : 2.870e+03 (less than 1 second per iteration)\n",
" Threads : 1\n",
"\n",
" Objective Residual\n",
"Iter Primal Dual Primal Dual Compl Time\n",
" 0 2.39659952e+05 -2.39659952e+05 1.24e+04 8.21e-07 1.00e+06 0s\n",
" 1 1.05586674e+05 -1.05609528e+05 1.15e+03 7.60e-08 1.02e+05 0s\n",
" 2 1.75003400e+03 -1.77843491e+03 3.01e+01 2.00e-09 2.93e+03 0s\n",
" 3 3.17477419e-02 -3.04635273e+01 6.52e-02 4.33e-12 7.88e+00 0s\n",
" 4 1.21242809e-03 -1.99285264e+01 6.52e-08 4.55e-18 9.96e-01 0s\n",
" 5 1.21218354e-03 -2.11465359e-02 7.96e-12 3.47e-18 1.12e-03 0s\n",
" 6 9.50377488e-04 -1.04906728e-03 2.19e-13 4.34e-19 1.00e-04 0s\n",
" 7 1.92539823e-04 -7.14636096e-04 6.94e-18 2.08e-17 4.54e-05 0s\n",
" 8 1.03056536e-04 2.93648920e-06 1.04e-17 1.63e-17 5.01e-06 0s\n",
" 9 7.56748365e-05 4.67875996e-05 3.30e-17 8.67e-19 1.44e-06 0s\n",
" 10 7.11581951e-05 6.86860873e-05 3.47e-18 8.67e-19 1.24e-07 0s\n",
" 11 7.04392044e-05 7.01891298e-05 7.89e-17 5.53e-18 1.25e-08 0s\n",
" 12 7.03463170e-05 7.03378302e-05 1.73e-17 1.62e-18 4.24e-10 0s\n",
"\n",
"Barrier solved model in 12 iterations and 0.06 seconds (0.00 work units)\n",
"Optimal objective 7.03463170e-05\n",
"\n"
]
}
],
"source": [
"import gurobipy as gp\n",
"from gurobipy import GRB\n",
"from math import sqrt\n",
"\n",
"# Create an empty model\n",
"m = gp.Model('portfolio')\n",
"\n",
"# Add matrix variable for the stocks\n",
"x = m.addMVar(len(stocks))\n",
"\n",
"# Objective is to minimize risk (squared). This is modeled using the\n",
"# covariance matrix, which measures the historical correlation between stocks\n",
"portfolio_risk = x @ sigma @ x\n",
"m.setObjective(portfolio_risk, GRB.MINIMIZE)\n",
"\n",
"# Fix budget with a constraint\n",
"m.addConstr(x.sum() == 1, 'budget')\n",
"\n",
"# Verify model formulation\n",
"m.write('portfolio_selection_optimization.lp')\n",
"\n",
"# Optimize model to find the minimum risk portfolio\n",
"m.optimize()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Display minimum risk portfolio using Pandas"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"
\n", " | Minimum Risk Portfolio | \n", "
---|---|
BRK-A | \n", "0.000017 | \n", "
AAPL | \n", "0.000024 | \n", "
MSFT | \n", "0.054089 | \n", "
JPM | \n", "0.000286 | \n", "
GOOG | \n", "0.024058 | \n", "
BAC | \n", "0.025590 | \n", "
INTC | \n", "0.000066 | \n", "
WFC | \n", "0.000074 | \n", "
C | \n", "0.345902 | \n", "
VZ | \n", "0.010441 | \n", "
META | \n", "0.000019 | \n", "
PFE | \n", "0.018616 | \n", "
JNJ | \n", "0.044215 | \n", "
WMT | \n", "0.022510 | \n", "
XOM | \n", "0.064354 | \n", "
FNMA | \n", "0.029768 | \n", "
T | \n", "0.111170 | \n", "
UNH | \n", "0.000046 | \n", "
CMCSA | \n", "0.172376 | \n", "
V | \n", "0.076379 | \n", "
Volatility | \n", "0.008387 | \n", "
Expected Return | \n", "0.000076 | \n", "