-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathportfolio_selection_optimization.ipynb.txt
More file actions
259 lines (259 loc) · 9.03 KB
/
portfolio_selection_optimization.ipynb.txt
File metadata and controls
259 lines (259 loc) · 9.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"scrolled": true
},
"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** <br /> \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",
"**Gurobi License** <br /> \n",
"In order to run this Jupyter Notebook properly, you must have a Gurobi license. If you do not have one, you can request an [evaluation license](https://www.gurobi.com/downloads/request-an-evaluation-license/?utm_source=3PW&utm_medium=OT&utm_campaign=WW-MU-MUI-OR-O_LEA-PR_NO-Q3_FY20_WW_JPME_Lost_Luggage_Distribution_COM_EVAL_GitHub&utm_term=Lost%20Luggage%20Distribution&utm_content=C_JPM) as a *commercial user*, or download a [free license](https://www.gurobi.com/academia/academic-program-and-licenses/?utm_source=3PW&utm_medium=OT&utm_campaign=WW-MU-EDU-OR-O_LEA-PR_NO-Q3_FY20_WW_JPME_Lost_Luggage_Distribution_COM_EVAL_GitHub&utm_term=Lost%20Luggage%20Distribution&utm_content=C_JPM) as an *academic user*.\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": {
"scrolled": true
},
"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": null,
"metadata": {
"scrolled": true
},
"outputs": [],
"source": [
"import yfinance as yf\n",
"\n",
"stocks = ['BRK-A', 'AAPL', 'MSFT', 'JPM', 'GOOG', 'BAC', 'INTC', 'WFC',\n",
" 'C', 'VZ', 'FB', '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": null,
"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": null,
"metadata": {},
"outputs": [],
"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": null,
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"minrisk_volatility = sqrt(m.ObjVal)\n",
"minrisk_return = delta @ x.X\n",
"pd.DataFrame(data=np.append(x.X, [minrisk_volatility, minrisk_return]),\n",
" index=stocks + ['Volatility', 'Expected Return'],\n",
" columns=['Minimum Risk Portfolio'])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Compute the efficient frontier\n",
"Solve the QP parametrically to find the lowest risk portfolio for different expected returns."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Create an expression representing the expected return for the portfolio\n",
"portfolio_return = delta @ x\n",
"target = m.addConstr(portfolio_return == minrisk_return, 'target')\n",
"\n",
"# Solve for efficient frontier by varying target return\n",
"frontier = np.empty((2,0))\n",
"for r in np.linspace(delta.min(), delta.max(), 25):\n",
" target[0].rhs = r\n",
" m.optimize()\n",
" frontier = np.append(frontier, [[sqrt(m.ObjVal)],[r]], axis=1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Plot results\n",
"Use the matplot library to plot the optimized solutions, along with the individual stocks:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"#plt.figure(figsize=(10,10))\n",
"\n",
"fig, ax = plt.subplots(figsize=(10,8))\n",
"\n",
"# Plot volatility versus expected return for individual stocks\n",
"ax.scatter(x=std, y=delta,\n",
" color='Blue', label='Individual Stocks')\n",
"for i, stock in enumerate(stocks):\n",
" ax.annotate(stock, (std[i], delta[i]))\n",
"\n",
"# Plot volatility versus expected return for minimum risk portfolio\n",
"ax.scatter(x=minrisk_volatility, y=minrisk_return, color='DarkGreen')\n",
"ax.annotate('Minimum\\nRisk\\nPortfolio', (minrisk_volatility, minrisk_return),\n",
" horizontalalignment='right')\n",
"\n",
"# Plot efficient frontier\n",
"ax.plot(frontier[0], frontier[1], label='Efficient Frontier', color='DarkGreen')\n",
"\n",
"# Format and display the final plot\n",
"ax.axis([frontier[0].min()*0.7, frontier[0].max()*1.3, delta.min()*1.2, delta.max()*1.2])\n",
"ax.set_xlabel('Volatility (standard deviation)')\n",
"ax.set_ylabel('Expected Return')\n",
"ax.legend()\n",
"ax.grid()\n",
"plt.show()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"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.8.2"
}
},
"nbformat": 4,
"nbformat_minor": 2
}