Skip to content

Commit

Permalink
[docs] add multiobjective portfolio example (#3227)
Browse files Browse the repository at this point in the history
  • Loading branch information
odow authored Feb 22, 2023
1 parent 50234cb commit 6a422d3
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 64 deletions.
2 changes: 1 addition & 1 deletion docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ JSON = "0.21"
JSONSchema = "1"
Literate = "2.8"
MathOptInterface = "=1.12.0"
MultiObjectiveAlgorithms = "=0.1.1"
MultiObjectiveAlgorithms = "=0.1.3"
Plots = "1"
SCS = "=1.1.3"
SQLite = "1"
Expand Down
2 changes: 1 addition & 1 deletion docs/src/tutorials/nonlinear/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ will help you know where to look for certain things.
new to JuMP.
* [Rocket Control](@ref)
* [Optimal control for a Space Shuttle reentry trajectory](@ref)
* [Quadratic portfolio optimization](@ref)
* [Portfolio optimization](@ref)
* The [Tips and tricks](@ref nonlinear_tips_and_tricks) tutorial contains a
number of helpful reformulations and tricks you can use when modeling
nonlinear programs. Look here if you are stuck trying to formulate a problem
Expand Down
206 changes: 144 additions & 62 deletions docs/src/tutorials/nonlinear/portfolio.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #src
# SOFTWARE. #src

# # Quadratic portfolio optimization
# # Portfolio optimization

# **Originally Contributed by**: Arpit Bhatia

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

# ## Required packages

# This tutorial uses the following packages

using JuMP
import DataFrames
import Ipopt
import MultiObjectiveAlgorithms as MOA
import Plots
import Statistics
import StatsPlots

# ## Formulation

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

# ```math
# \operatorname{Var}\left[\sum_{i=1}^{3} \tilde{r}_{i} x_{i}\right] =x^{T} Q x
# \operatorname{Var}\left[\sum_{i=1}^{3} \tilde{r}_{i} x_{i}\right] =x^\top Q x
# ```

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

# ```math
# \begin{aligned}
# \min x^{T} Q x \\
# \text { s.t. } \sum_{i=1}^{3} x_{i} \leq 1000.00 \\
# \overline{r}^{T} x \geq 50.00 \\
# \min x^\top Q x \\
# \text { s.t. } \sum_{i=1}^{3} x_{i} \leq 1000 \\
# \overline{r}^\top x \geq 50 \\
# x \geq 0
# \end{aligned}
# ```

# After that long discussion, let's now use JuMP to solve the portfolio
# optimization problem for the data given below.

# | Month | IBM | WMT | SEHI |
# |--------------|----------|---------|--------|
# | November-00 | 93.043 | 51.826 | 1.063 |
# | December-00 | 84.585 | 52.823 | 0.938 |
# | January-01 | 111.453 | 56.477 | 1.000 |
# | February-01 | 99.525 | 49.805 | 0.938 |
# | March-01 | 95.819 | 50.287 | 1.438 |
# | April-01 | 114.708 | 51.521 | 1.700 |
# | May-01 | 111.515 | 51.531 | 2.540 |
# | June-01 | 113.211 | 48.664 | 2.390 |
# | July-01 | 104.942 | 55.744 | 3.120 |
# | August-01 | 99.827 | 47.916 | 2.980 |
# | September-01 | 91.607 | 49.438 | 1.900 |
# | October-01 | 107.937 | 51.336 | 1.750 |
# | November-01 | 115.590 | 55.081 | 1.800 |

stock_data = [
93.043 51.826 1.063
84.585 52.823 0.938
111.453 56.477 1.000
99.525 49.805 0.938
95.819 50.287 1.438
114.708 51.521 1.700
111.515 51.531 2.540
113.211 48.664 2.390
104.942 55.744 3.120
99.827 47.916 2.980
91.607 49.438 1.900
107.937 51.336 1.750
115.590 55.081 1.800
]

# Calculating stock returns

stock_returns = Array{Float64}(undef, 12, 3)
for i in 1:12
stock_returns[i, :] =
(stock_data[i+1, :] .- stock_data[i, :]) ./ stock_data[i, :]
end
stock_returns
# ## Data

# For the data in our problem, we use the stock prices given below, in monthly
# values from November 2000, through November 2001.

# Calculating the expected value of monthly return:
df = DataFrames.DataFrame(
[
93.043 51.826 1.063
84.585 52.823 0.938
111.453 56.477 1.000
99.525 49.805 0.938
95.819 50.287 1.438
114.708 51.521 1.700
111.515 51.531 2.540
113.211 48.664 2.390
104.942 55.744 3.120
99.827 47.916 2.980
91.607 49.438 1.900
107.937 51.336 1.750
115.590 55.081 1.800
],
[:IBM, :WMT, :SEHI],
)

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

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

Q = Statistics.cov(stock_returns)
# The expected monthly return is:

# JuMP Model
r = vec(Statistics.mean(returns; dims = 1))

portfolio = Model(Ipopt.Optimizer)
set_silent(portfolio)
@variable(portfolio, x[1:3] >= 0)
@objective(portfolio, Min, x' * Q * x)
@constraint(portfolio, sum(x) <= 1000)
@constraint(portfolio, sum(r[i] * x[i] for i in 1:3) >= 50)
optimize!(portfolio)
# and the covariance matrix is:

objective_value(portfolio)
Q = Statistics.cov(returns)

#-
# ## JuMP formulation

model = Model(Ipopt.Optimizer)
set_silent(model)
@variable(model, x[1:3] >= 0)
@objective(model, Min, x' * Q * x)
@constraint(model, sum(x) <= 1000)
@constraint(model, r' * x >= 50)
optimize!(model)
solution_summary(model)

# The optimal allocation of our assets is:

value.(x)

# So we spend \$497 on IBM, and \$503 on SEHI. This results in a variance of:

scalar_variance = value(x' * Q * x)

# and an expected return of:

scalar_return = value(r' * x)

# ## Multi-objective portfolio optimization

# The previous model returned a single solution that minimized the variance,
# ensuring that our expected return was at least \$50. In practice, we might
# be willing to accept a slightly higher variance if it meant a much increased
# expected return. To explore this problem space, we can instead formulate our
# portfolio optimization problem with two objectives:
#
# 1. to minimize the variance
# 2. to maximize the expected return
#
# The solution to this biobjective problem is the
# [efficient frontier](https://en.wikipedia.org/wiki/Efficient_frontier) of
# modern portfolio theory, and each point in the solution is a point with the
# best return for a fixed level of risk.

model = Model(() -> MOA.Optimizer(Ipopt.Optimizer))
set_silent(model)

# We also need to choose a solution algorithm for `MOA`. For our problem, the
# efficient frontier will have an infinite number of solutions. Since we cannot
# find all of the solutions, we choose an approximation algorithm and limit the
# number of solution points that are returned:

set_optimizer_attribute(model, MOA.Algorithm(), MOA.EpsilonConstraint())
set_optimizer_attribute(model, MOA.SolutionLimit(), 50)

# Now we can define the rest of the model:

@variable(model, x[1:3] >= 0)
@constraint(model, sum(x) <= 1000)
@expression(model, variance, x' * Q * x)
@expression(model, expected_return, r' * x)
## We want to minimize variance and maximize expected return, but we must pick
## a single objective sense `Min`, and negate any `Max` objectives:
@objective(model, Min, [variance, -expected_return])
optimize!(model)
solution_summary(model)

# The algorithm found 50 different solutions. Let's plot them to see how they
# differ:

objective_space = Plots.hline(
[scalar_return];
label = "Single-objective solution",
linecolor = :red,
)
Plots.vline!(objective_space, [scalar_variance]; label = "", linecolor = :red)
Plots.scatter!(
objective_space,
[value(variance; result = i) for i in 1:result_count(model)],
[value(expected_return; result = i) for i in 1:result_count(model)];
xlabel = "Variance",
ylabel = "Expected Return",
label = "",
title = "Objective space",
markercolor = "white",
markersize = 5,
legend = :bottomright,
)
for i in 1:result_count(model)
y = objective_value(model; result = i)
Plots.annotate!(objective_space, y[1], -y[2], (i, 3))
end

decision_space = StatsPlots.groupedbar(
vcat([value.(x; result = i)' for i in 1:result_count(model)]...);
bar_position = :stack,
label = ["IBM" "WMT" "SEHI"],
xlabel = "Solution #",
ylabel = "Investment (\$)",
title = "Decision space",
)
Plots.plot(objective_space, decision_space; layout = (2, 1), size = (600, 600))

# Perhaps our trade-off wasn't so bad after all! Our original solution
# corresponded to picking a solution #17. If we buy more SEHI, we can increase
# the return, but the variance also increases. If we buy less SEHI, such as a
# solution like #5 or #6, then we can achieve the corresponding return without
# deploying all of our capital. We should also note that at no point should we
# buy WMT.

0 comments on commit 6a422d3

Please sign in to comment.