Skip to content

Commit 6a422d3

Browse files
authored
[docs] add multiobjective portfolio example (#3227)
1 parent 50234cb commit 6a422d3

File tree

3 files changed

+146
-64
lines changed

3 files changed

+146
-64
lines changed

docs/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ JSON = "0.21"
3535
JSONSchema = "1"
3636
Literate = "2.8"
3737
MathOptInterface = "=1.12.0"
38-
MultiObjectiveAlgorithms = "=0.1.1"
38+
MultiObjectiveAlgorithms = "=0.1.3"
3939
Plots = "1"
4040
SCS = "=1.1.3"
4141
SQLite = "1"

docs/src/tutorials/nonlinear/introduction.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ will help you know where to look for certain things.
3939
new to JuMP.
4040
* [Rocket Control](@ref)
4141
* [Optimal control for a Space Shuttle reentry trajectory](@ref)
42-
* [Quadratic portfolio optimization](@ref)
42+
* [Portfolio optimization](@ref)
4343
* The [Tips and tricks](@ref nonlinear_tips_and_tricks) tutorial contains a
4444
number of helpful reformulations and tricks you can use when modeling
4545
nonlinear programs. Look here if you are stuck trying to formulate a problem

docs/src/tutorials/nonlinear/portfolio.jl

Lines changed: 144 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #src
1919
# SOFTWARE. #src
2020

21-
# # Quadratic portfolio optimization
21+
# # Portfolio optimization
2222

2323
# **Originally Contributed by**: Arpit Bhatia
2424

@@ -29,10 +29,19 @@
2929
# This tutorial solves the famous Markowitz Portfolio Optimization problem with
3030
# data from [lecture notes from a course taught at Georgia Tech by Shabbir Ahmed](https://www2.isye.gatech.edu/~sahmed/isye6669/).
3131

32+
# ## Required packages
33+
3234
# This tutorial uses the following packages
35+
3336
using JuMP
37+
import DataFrames
3438
import Ipopt
39+
import MultiObjectiveAlgorithms as MOA
40+
import Plots
3541
import Statistics
42+
import StatsPlots
43+
44+
# ## Formulation
3645

3746
# Suppose we are considering investing 1000 dollars in three non-dividend paying
3847
# stocks, IBM (IBM), Walmart (WMT), and Southern Electric (SEHI), for a
@@ -88,7 +97,7 @@ import Statistics
8897
# this form. We can also write this equation as:
8998

9099
# ```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
92101
# ```
93102

94103
# Where $Q$ is the covariance matrix for the random vector $\tilde{r}$.
@@ -97,77 +106,150 @@ import Statistics
97106

98107
# ```math
99108
# \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 \\
103112
# x \geq 0
104113
# \end{aligned}
105114
# ```
106115

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.
150120

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+
)
152139

153-
r = Statistics.mean(stock_returns; dims = 1)
140+
# Next, we compute the percentage return for the stock in each month:
154141

155-
# Calculating the covariance matrix Q
142+
returns = diff(Matrix(df); dims = 1) ./ Matrix(df[1:end-1, :])
156143

157-
Q = Statistics.cov(stock_returns)
144+
# The expected monthly return is:
158145

159-
# JuMP Model
146+
r = vec(Statistics.mean(returns; dims = 1))
160147

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:
168149

169-
objective_value(portfolio)
150+
Q = Statistics.cov(returns)
170151

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:
172164

173165
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

Comments
 (0)