18
18
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #src
19
19
# SOFTWARE. #src
20
20
21
- # # Quadratic portfolio optimization
21
+ # # Portfolio optimization
22
22
23
23
# **Originally Contributed by**: Arpit Bhatia
24
24
29
29
# This tutorial solves the famous Markowitz Portfolio Optimization problem with
30
30
# data from [lecture notes from a course taught at Georgia Tech by Shabbir Ahmed](https://www2.isye.gatech.edu/~sahmed/isye6669/).
31
31
32
+ # ## Required packages
33
+
32
34
# This tutorial uses the following packages
35
+
33
36
using JuMP
37
+ import DataFrames
34
38
import Ipopt
39
+ import MultiObjectiveAlgorithms as MOA
40
+ import Plots
35
41
import Statistics
42
+ import StatsPlots
43
+
44
+ # ## Formulation
36
45
37
46
# Suppose we are considering investing 1000 dollars in three non-dividend paying
38
47
# stocks, IBM (IBM), Walmart (WMT), and Southern Electric (SEHI), for a
@@ -88,7 +97,7 @@ import Statistics
88
97
# this form. We can also write this equation as:
89
98
90
99
# ```math
91
- # \operatorname{Var}\left[\sum_{i=1}^{3} \tilde{r}_{i} x_{i}\right] =x^{T} Q x
100
+ # \operatorname{Var}\left[\sum_{i=1}^{3} \tilde{r}_{i} x_{i}\right] =x^\top Q x
92
101
# ```
93
102
94
103
# Where $Q$ is the covariance matrix for the random vector $\tilde{r}$.
@@ -97,77 +106,150 @@ import Statistics
97
106
98
107
# ```math
99
108
# \begin{aligned}
100
- # \min x^{T} Q x \\
101
- # \text { s.t. } \sum_{i=1}^{3} x_{i} \leq 1000.00 \\
102
- # \overline{r}^{T} x \geq 50.00 \\
109
+ # \min x^\top Q x \\
110
+ # \text { s.t. } \sum_{i=1}^{3} x_{i} \leq 1000 \\
111
+ # \overline{r}^\top x \geq 50 \\
103
112
# x \geq 0
104
113
# \end{aligned}
105
114
# ```
106
115
107
- # After that long discussion, let's now use JuMP to solve the portfolio
108
- # optimization problem for the data given below.
109
-
110
- # | Month | IBM | WMT | SEHI |
111
- # |--------------|----------|---------|--------|
112
- # | November-00 | 93.043 | 51.826 | 1.063 |
113
- # | December-00 | 84.585 | 52.823 | 0.938 |
114
- # | January-01 | 111.453 | 56.477 | 1.000 |
115
- # | February-01 | 99.525 | 49.805 | 0.938 |
116
- # | March-01 | 95.819 | 50.287 | 1.438 |
117
- # | April-01 | 114.708 | 51.521 | 1.700 |
118
- # | May-01 | 111.515 | 51.531 | 2.540 |
119
- # | June-01 | 113.211 | 48.664 | 2.390 |
120
- # | July-01 | 104.942 | 55.744 | 3.120 |
121
- # | August-01 | 99.827 | 47.916 | 2.980 |
122
- # | September-01 | 91.607 | 49.438 | 1.900 |
123
- # | October-01 | 107.937 | 51.336 | 1.750 |
124
- # | November-01 | 115.590 | 55.081 | 1.800 |
125
-
126
- stock_data = [
127
- 93.043 51.826 1.063
128
- 84.585 52.823 0.938
129
- 111.453 56.477 1.000
130
- 99.525 49.805 0.938
131
- 95.819 50.287 1.438
132
- 114.708 51.521 1.700
133
- 111.515 51.531 2.540
134
- 113.211 48.664 2.390
135
- 104.942 55.744 3.120
136
- 99.827 47.916 2.980
137
- 91.607 49.438 1.900
138
- 107.937 51.336 1.750
139
- 115.590 55.081 1.800
140
- ]
141
-
142
- # Calculating stock returns
143
-
144
- stock_returns = Array {Float64} (undef, 12 , 3 )
145
- for i in 1 : 12
146
- stock_returns[i, :] =
147
- (stock_data[i+ 1 , :] .- stock_data[i, :]) ./ stock_data[i, :]
148
- end
149
- stock_returns
116
+ # ## Data
117
+
118
+ # For the data in our problem, we use the stock prices given below, in monthly
119
+ # values from November 2000, through November 2001.
150
120
151
- # Calculating the expected value of monthly return:
121
+ df = DataFrames. DataFrame (
122
+ [
123
+ 93.043 51.826 1.063
124
+ 84.585 52.823 0.938
125
+ 111.453 56.477 1.000
126
+ 99.525 49.805 0.938
127
+ 95.819 50.287 1.438
128
+ 114.708 51.521 1.700
129
+ 111.515 51.531 2.540
130
+ 113.211 48.664 2.390
131
+ 104.942 55.744 3.120
132
+ 99.827 47.916 2.980
133
+ 91.607 49.438 1.900
134
+ 107.937 51.336 1.750
135
+ 115.590 55.081 1.800
136
+ ],
137
+ [:IBM , :WMT , :SEHI ],
138
+ )
152
139
153
- r = Statistics . mean (stock_returns; dims = 1 )
140
+ # Next, we compute the percentage return for the stock in each month:
154
141
155
- # Calculating the covariance matrix Q
142
+ returns = diff ( Matrix (df); dims = 1 ) ./ Matrix (df[ 1 : end - 1 , :])
156
143
157
- Q = Statistics . cov (stock_returns)
144
+ # The expected monthly return is:
158
145
159
- # JuMP Model
146
+ r = vec (Statistics . mean (returns; dims = 1 ))
160
147
161
- portfolio = Model (Ipopt. Optimizer)
162
- set_silent (portfolio)
163
- @variable (portfolio, x[1 : 3 ] >= 0 )
164
- @objective (portfolio, Min, x' * Q * x)
165
- @constraint (portfolio, sum (x) <= 1000 )
166
- @constraint (portfolio, sum (r[i] * x[i] for i in 1 : 3 ) >= 50 )
167
- optimize! (portfolio)
148
+ # and the covariance matrix is:
168
149
169
- objective_value (portfolio )
150
+ Q = Statistics . cov (returns )
170
151
171
- # -
152
+ # ## JuMP formulation
153
+
154
+ model = Model (Ipopt. Optimizer)
155
+ set_silent (model)
156
+ @variable (model, x[1 : 3 ] >= 0 )
157
+ @objective (model, Min, x' * Q * x)
158
+ @constraint (model, sum (x) <= 1000 )
159
+ @constraint (model, r' * x >= 50 )
160
+ optimize! (model)
161
+ solution_summary (model)
162
+
163
+ # The optimal allocation of our assets is:
172
164
173
165
value .(x)
166
+
167
+ # So we spend \$497 on IBM, and \$503 on SEHI. This results in a variance of:
168
+
169
+ scalar_variance = value (x' * Q * x)
170
+
171
+ # and an expected return of:
172
+
173
+ scalar_return = value (r' * x)
174
+
175
+ # ## Multi-objective portfolio optimization
176
+
177
+ # The previous model returned a single solution that minimized the variance,
178
+ # ensuring that our expected return was at least \$50. In practice, we might
179
+ # be willing to accept a slightly higher variance if it meant a much increased
180
+ # expected return. To explore this problem space, we can instead formulate our
181
+ # portfolio optimization problem with two objectives:
182
+ #
183
+ # 1. to minimize the variance
184
+ # 2. to maximize the expected return
185
+ #
186
+ # The solution to this biobjective problem is the
187
+ # [efficient frontier](https://en.wikipedia.org/wiki/Efficient_frontier) of
188
+ # modern portfolio theory, and each point in the solution is a point with the
189
+ # best return for a fixed level of risk.
190
+
191
+ model = Model (() -> MOA. Optimizer (Ipopt. Optimizer))
192
+ set_silent (model)
193
+
194
+ # We also need to choose a solution algorithm for `MOA`. For our problem, the
195
+ # efficient frontier will have an infinite number of solutions. Since we cannot
196
+ # find all of the solutions, we choose an approximation algorithm and limit the
197
+ # number of solution points that are returned:
198
+
199
+ set_optimizer_attribute (model, MOA. Algorithm (), MOA. EpsilonConstraint ())
200
+ set_optimizer_attribute (model, MOA. SolutionLimit (), 50 )
201
+
202
+ # Now we can define the rest of the model:
203
+
204
+ @variable (model, x[1 : 3 ] >= 0 )
205
+ @constraint (model, sum (x) <= 1000 )
206
+ @expression (model, variance, x' * Q * x)
207
+ @expression (model, expected_return, r' * x)
208
+ # # We want to minimize variance and maximize expected return, but we must pick
209
+ # # a single objective sense `Min`, and negate any `Max` objectives:
210
+ @objective (model, Min, [variance, - expected_return])
211
+ optimize! (model)
212
+ solution_summary (model)
213
+
214
+ # The algorithm found 50 different solutions. Let's plot them to see how they
215
+ # differ:
216
+
217
+ objective_space = Plots. hline (
218
+ [scalar_return];
219
+ label = " Single-objective solution" ,
220
+ linecolor = :red ,
221
+ )
222
+ Plots. vline! (objective_space, [scalar_variance]; label = " " , linecolor = :red )
223
+ Plots. scatter! (
224
+ objective_space,
225
+ [value (variance; result = i) for i in 1 : result_count (model)],
226
+ [value (expected_return; result = i) for i in 1 : result_count (model)];
227
+ xlabel = " Variance" ,
228
+ ylabel = " Expected Return" ,
229
+ label = " " ,
230
+ title = " Objective space" ,
231
+ markercolor = " white" ,
232
+ markersize = 5 ,
233
+ legend = :bottomright ,
234
+ )
235
+ for i in 1 : result_count (model)
236
+ y = objective_value (model; result = i)
237
+ Plots. annotate! (objective_space, y[1 ], - y[2 ], (i, 3 ))
238
+ end
239
+
240
+ decision_space = StatsPlots. groupedbar (
241
+ vcat ([value .(x; result = i)' for i in 1 : result_count (model)]. .. );
242
+ bar_position = :stack ,
243
+ label = [" IBM" " WMT" " SEHI" ],
244
+ xlabel = " Solution #" ,
245
+ ylabel = " Investment (\$ )" ,
246
+ title = " Decision space" ,
247
+ )
248
+ Plots. plot (objective_space, decision_space; layout = (2 , 1 ), size = (600 , 600 ))
249
+
250
+ # Perhaps our trade-off wasn't so bad after all! Our original solution
251
+ # corresponded to picking a solution #17. If we buy more SEHI, we can increase
252
+ # the return, but the variance also increases. If we buy less SEHI, such as a
253
+ # solution like #5 or #6, then we can achieve the corresponding return without
254
+ # deploying all of our capital. We should also note that at no point should we
255
+ # buy WMT.
0 commit comments