daniel-dobos commited on
Commit
12a852f
·
1 Parent(s): be8071f

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +11 -0
  2. app.ipynb +304 -0
  3. requirements.txt +7 -0
Dockerfile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9
2
+
3
+ WORKDIR /code
4
+
5
+ COPY ./requirements.txt /code/requirements.txt
6
+
7
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
8
+
9
+ COPY . .
10
+
11
+ CMD ["mnn", "serve", "/code/app.ipynb", "--address", "0.0.0.0", "--port", "7860", "--allow-websocket-origin", "*"]
app.ipynb ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "metadata": {},
7
+ "outputs": [],
8
+ "source": [
9
+ "import manganite\n",
10
+ "%load_ext manganite"
11
+ ]
12
+ },
13
+ {
14
+ "cell_type": "markdown",
15
+ "metadata": {},
16
+ "source": [
17
+ "# Portfolio Selection Optimization\n",
18
+ "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",
19
+ "\n",
20
+ "**Download the Repository** <br /> \n",
21
+ "You can download the repository containing this and other examples by clicking [here](https://github.com/Gurobi/modeling-examples/archive/master.zip). \n",
22
+ "\n",
23
+ "**Gurobi License** <br /> \n",
24
+ "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",
25
+ "\n",
26
+ "\n",
27
+ "## Model Formulation\n",
28
+ "### Parameters\n",
29
+ "\n",
30
+ "We use the [Greek values](https://en.wikipedia.org/wiki/Greeks_\\(finance\\)) that are traditional in finance:\n",
31
+ "\n",
32
+ "- $$ \\delta $$: n-element vector measuring the change in price for each stock\n",
33
+ "- $$ \\sigma $$: n x n matrix measuring the covariance among stocks\n",
34
+ "\n",
35
+ "There is one additional parameter when solving the model parametrically:\n",
36
+ "\n",
37
+ "- r: target return\n",
38
+ "\n",
39
+ "\n",
40
+ "### Decision Variables\n",
41
+ "- $$ x \\ge 0 $$ : n-element vector where each element represents the fraction of the porfolio to invest in each stock\n",
42
+ "\n",
43
+ "### Objective Function\n",
44
+ "Minimize the total risk, a convex quadratic function:\n",
45
+ "<pre>\n",
46
+ "$$\n",
47
+ "\\begin{equation}\n",
48
+ "\\min x^t \\cdot \\sigma \\cdot x\n",
49
+ "\\end{equation}\n",
50
+ "$$\n",
51
+ "</pre>\n",
52
+ "\n",
53
+ "### Constraints\n",
54
+ "\n",
55
+ "Allocate the entire portfolio: the total investments should be 1.0 (100%), where $e$ is a unit vector (all 1's):\n",
56
+ "<pre>\n",
57
+ "$$\n",
58
+ "\\begin{equation}\n",
59
+ "e \\cdot x = 1\n",
60
+ "\\end{equation}\n",
61
+ "$$\n",
62
+ "</pre>\n",
63
+ "Return: When we solve the model parametrically for different return values $r$, we add a constraint on the target return:\n",
64
+ "<pre>\n",
65
+ "$$\n",
66
+ "\\begin{equation}\n",
67
+ "\\delta \\cdot x = r\n",
68
+ "\\end{equation}\n",
69
+ "$$\n",
70
+ "</pre>"
71
+ ]
72
+ },
73
+ {
74
+ "cell_type": "markdown",
75
+ "metadata": {},
76
+ "source": [
77
+ "## Python Implementation\n",
78
+ "### Stock data\n",
79
+ "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).\n",
80
+ "### Dashboard\n",
81
+ "Use manganite package to create a beautiful dashoard from the jupyter notebook"
82
+ ]
83
+ },
84
+ {
85
+ "cell_type": "code",
86
+ "execution_count": null,
87
+ "metadata": {
88
+ "scrolled": true
89
+ },
90
+ "outputs": [],
91
+ "source": [
92
+ "import yfinance as yf\n",
93
+ "import numpy as np\n",
94
+ "\n",
95
+ "import gurobipy as gp\n",
96
+ "from gurobipy import GRB\n",
97
+ "from math import sqrt\n",
98
+ "\n",
99
+ "import pandas as pd\n",
100
+ "import matplotlib.pyplot as plt\n",
101
+ "import plotly.express as px\n",
102
+ "import plotly.graph_objects as go\n",
103
+ "\n",
104
+ "import plotly.io as pio\n",
105
+ "pio.templates.default = 'plotly_white'"
106
+ ]
107
+ },
108
+ {
109
+ "cell_type": "code",
110
+ "execution_count": null,
111
+ "metadata": {},
112
+ "outputs": [],
113
+ "source": [
114
+ "%%mnn widget --type text --tab \"Stock Selector\" --header \"Select your stocks\" --var stocks --position 0 0 3\n",
115
+ "stocks = 'AAPL, MSFT, JPM, GOOG, BAC, INTC, WFC, Meta'"
116
+ ]
117
+ },
118
+ {
119
+ "cell_type": "code",
120
+ "execution_count": null,
121
+ "metadata": {},
122
+ "outputs": [],
123
+ "source": [
124
+ "# Split the input string into a list using the column separator\n",
125
+ "tick_marks_list = stocks.split(',')\n",
126
+ "\n",
127
+ "# Remove unwanted characters and spaces from each element\n",
128
+ "clean_tick_marks = [tick.strip() for tick in tick_marks_list]\n",
129
+ "\n",
130
+ "data = yf.download(clean_tick_marks, period='2y')"
131
+ ]
132
+ },
133
+ {
134
+ "cell_type": "code",
135
+ "execution_count": null,
136
+ "metadata": {},
137
+ "outputs": [],
138
+ "source": [
139
+ "%%mnn widget --type plot --var fig_stocks --tab \"Stock Selector\" --position 1 0 4 --header \"Stock prices\"\n",
140
+ "\n",
141
+ "df_closing = data['Close']\n",
142
+ "fig_stocks = px.line(df_closing, x=df_closing.index, y=df_closing.columns,\n",
143
+ " # hover_data={\"date\": \"|%B %d, %Y\"},\n",
144
+ " )\n",
145
+ "\n",
146
+ "# Update the layout to customize axis labels\n",
147
+ "fig_stocks.update_yaxes(title_text='Stock Price ($)')\n",
148
+ "\n",
149
+ "fig_stocks.update_xaxes(\n",
150
+ " title_text='',\n",
151
+ " dtick=\"M1\",\n",
152
+ " tickformat=\"%b\\n%Y\",\n",
153
+ " ticklabelmode=\"period\")\n",
154
+ "\n",
155
+ "fig_stocks.update_layout(legend_title=\"Stock\",)"
156
+ ]
157
+ },
158
+ {
159
+ "cell_type": "code",
160
+ "execution_count": null,
161
+ "metadata": {},
162
+ "outputs": [],
163
+ "source": [
164
+ "%%mnn execute --on button \"Optimize Portfolio\" --returns solution\n",
165
+ "\n",
166
+ "#calculating greeks\n",
167
+ "closes = np.transpose(np.array(data.Close)) # matrix of daily closing prices\n",
168
+ "absdiff = np.diff(closes) # change in closing price each day\n",
169
+ "reldiff = np.divide(absdiff, closes[:,:-1]) # relative change in daily closing price\n",
170
+ "delta = np.mean(reldiff, axis=1) # mean price change\n",
171
+ "sigma = np.cov(reldiff) # covariance (standard deviations)\n",
172
+ "std = np.std(reldiff, axis=1) # standard deviation\n",
173
+ "df_plot = pd.DataFrame({'std': std, 'delta': delta, 'stocks':clean_tick_marks})\n",
174
+ "print('solving QP model')\n",
175
+ "# Create an empty model\n",
176
+ "m = gp.Model('portfolio')\n",
177
+ "\n",
178
+ "# Add matrix variable for the stocks\n",
179
+ "x = m.addMVar(len(clean_tick_marks))\n",
180
+ "\n",
181
+ "# Objective is to minimize risk (squared). This is modeled using the\n",
182
+ "# covariance matrix, which measures the historical correlation between stocks\n",
183
+ "portfolio_risk = x @ sigma @ x\n",
184
+ "m.setObjective(portfolio_risk, GRB.MINIMIZE)\n",
185
+ "\n",
186
+ "# Fix budget with a constraint\n",
187
+ "m.addConstr(x.sum() == 1, 'budget')\n",
188
+ "\n",
189
+ "# Verify model formulation\n",
190
+ "# m.write('portfolio_selection_optimization.lp')\n",
191
+ "\n",
192
+ "# Optimize model to find the minimum risk portfolio\n",
193
+ "m.optimize()\n",
194
+ "\n",
195
+ "minrisk_volatility = sqrt(m.ObjVal)\n",
196
+ "minrisk_return = delta @ x.X\n",
197
+ "\n",
198
+ "# Create an expression representing the expected return for the portfolio\n",
199
+ "portfolio_return = delta @ x\n",
200
+ "\n",
201
+ "target = m.addConstr(portfolio_return == minrisk_return, 'target')\n",
202
+ "\n",
203
+ "# solution = pd.DataFrame(data=np.append(x.X, [minrisk_volatility, minrisk_return]),\n",
204
+ "# index=clean_tick_marks + ['Volatility', 'Expected Return'],\n",
205
+ "# columns=['Minimum Risk Portfolio'])\n",
206
+ "\n",
207
+ "solution = pd.DataFrame(data=x.X,\n",
208
+ " index=clean_tick_marks,\n",
209
+ " columns=['Minimum Risk Portfolio'])\n",
210
+ "\n",
211
+ "exp_return = minrisk_return\n",
212
+ "exp_volatility = minrisk_volatility\n",
213
+ "\n",
214
+ "# Solve for efficient frontier by varying target return\n",
215
+ "frontier = np.empty((2,0))\n",
216
+ "for r in np.linspace(delta.min(), delta.max(), 25):\n",
217
+ " target.rhs = r\n",
218
+ " m.optimize()\n",
219
+ " frontier = np.append(frontier, [[sqrt(m.ObjVal)],[r]], axis=1)\n",
220
+ "\n"
221
+ ]
222
+ },
223
+ {
224
+ "cell_type": "code",
225
+ "execution_count": null,
226
+ "metadata": {},
227
+ "outputs": [],
228
+ "source": [
229
+ "%%mnn widget --type plot --var fig_bar --tab \"Portfolio\" --position 0 0 3 --header \"Minimum Risk Portfolio\"\n",
230
+ "# Create a pie chart\n",
231
+ "fig_pie = px.pie(solution, values='Minimum Risk Portfolio', names=solution.index,\n",
232
+ " title=f'Your Portfolio Distribution<br>Expected Return: {round(exp_return,6)}<br>Volatility: {round(exp_volatility,4)}')\n",
233
+ "fig_pie.update_traces(textposition='inside', textinfo='percent+label')\n",
234
+ "\n",
235
+ "# Create a bar chart\n",
236
+ "fig_bar = px.bar(solution, x=solution.index, y='Minimum Risk Portfolio',\n",
237
+ "title=f'Your Portfolio Distribution<br>Expected Return: {round(exp_return,6)}<br>Volatility: {round(exp_volatility,4)}',\n",
238
+ ")\n",
239
+ "\n",
240
+ "# Hide x-axis and y-axis labels\n",
241
+ "fig_bar.update_xaxes(title_text='')\n",
242
+ "fig_bar.update_yaxes(title_text='')"
243
+ ]
244
+ },
245
+ {
246
+ "cell_type": "code",
247
+ "execution_count": null,
248
+ "metadata": {},
249
+ "outputs": [],
250
+ "source": [
251
+ "%%mnn widget --type plot --var fig --tab \"Portfolio\" --position 0 3 3 --header \"Efficient Frontier\"\n",
252
+ "\n",
253
+ "update = solution\n",
254
+ "# Plot volatility versus expected return for individual stocks\n",
255
+ "fig1 = px.scatter(df_plot, x=\"std\", y=\"delta\" ,\n",
256
+ " labels = { \"std\": \"Volatility (standard deviation)\", \"delta\": \"Expected Return\"}, text=\"stocks\" )\n",
257
+ "fig1.update_traces(textposition=\"bottom right\")\n",
258
+ "\n",
259
+ "# Plot volatility versus expected return for minimum risk portfolio\n",
260
+ "\n",
261
+ "fig2 = px.scatter(x=[minrisk_volatility], y=[minrisk_return],text = [\"Minimum Risk<br>Portfolio\"])\n",
262
+ "fig2.update_traces(textposition=\"bottom right\")\n",
263
+ "fig = go.Figure(data=fig1.data + fig2.data)\n",
264
+ "\n",
265
+ "# Plot efficient frontier\n",
266
+ "\n",
267
+ "fig.add_trace(go.Scatter(x=frontier[0], y=frontier[1], mode='lines', name='Efficient Frontier'))\n",
268
+ "\n",
269
+ "# Set x and y labels using update_xaxes and update_yaxes\n",
270
+ "fig.update_xaxes(title_text=\"Volatility (standard deviation)\")\n",
271
+ "fig.update_yaxes(title_text=\"Expected Return\")\n",
272
+ "\n",
273
+ "fig.update_layout(legend=dict(\n",
274
+ " yanchor=\"bottom\",\n",
275
+ " y=0.01,\n",
276
+ " xanchor=\"right\",\n",
277
+ " x=0.99\n",
278
+ "))\n",
279
+ "\n"
280
+ ]
281
+ }
282
+ ],
283
+ "metadata": {
284
+ "kernelspec": {
285
+ "display_name": "Python 3 (ipykernel)",
286
+ "language": "python",
287
+ "name": "python3"
288
+ },
289
+ "language_info": {
290
+ "codemirror_mode": {
291
+ "name": "ipython",
292
+ "version": 3
293
+ },
294
+ "file_extension": ".py",
295
+ "mimetype": "text/x-python",
296
+ "name": "python",
297
+ "nbconvert_exporter": "python",
298
+ "pygments_lexer": "ipython3",
299
+ "version": "3.11.4"
300
+ }
301
+ },
302
+ "nbformat": 4,
303
+ "nbformat_minor": 4
304
+ }
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ manganite
2
+ yfinance
3
+ numpy
4
+ pandas
5
+ plotly
6
+ gurobipy
7
+ scipy