The source files for all examples can be found in /examples.
Example 3: Efficient frontier
In this example we will show how to compute efficient frontiers using the MeanRisk
and NearOptimalCentering
estimators.
using PortfolioOptimisers, PrettyTables
# Format for pretty tables.
tsfmt = (v, i, j) -> begin
if j == 1
return Date(v)
else
return v
end
end;
resfmt = (v, i, j) -> begin
if j == 1
return v
else
return isa(v, Number) ? "$(round(v*100, digits=3)) %" : v
end
end;
1. ReturnsResult data
We will use the same data as the previous example.
using CSV, TimeSeries, DataFrames
X = TimeArray(CSV.File(joinpath(@__DIR__, "SP500.csv.gz")); timestamp = :Date)[(end - 252):end]
pretty_table(X[(end - 5):end]; formatters = tsfmt)
# Compute the returns
rd = prices_to_returns(X)
ReturnsResult
nx | 20-element Vector{String}
X | 252×20 Matrix{Float64}
nf | nothing
F | nothing
ts | 252-element Vector{Dates.Date}
iv | nothing
ivpa | nothing
2. Efficient frontier
We have two mutually exclusive ways to compute the efficient frontier. We can do so from the perspective of minimising the risk with a return lower bound, or maximising the return with a risk upper bound. It is possible to provide explicit bounds, or a Frontier
object which automatically computes the bounds based on the problem and constraints. All four combinations have their use cases. In this example we will only show the use of Frontier
as a lower bound on the portfolio return.
Since we will be performing various optimistions, we will provide a vector of solver settings because we don't know if a single set of settings will work in all cases.
using Clarabel
slv = [Solver(; name = :clarabel1, solver = Clarabel.Optimizer,
settings = Dict("verbose" => false),
check_sol = (; allow_local = true, allow_almost = true)),
Solver(; name = :clarabel2, solver = Clarabel.Optimizer,
settings = Dict("verbose" => false, "max_step_fraction" => 0.75),
check_sol = (; allow_local = true, allow_almost = true))]
2-element Vector{Solver{Symbol, UnionAll, T3, @NamedTuple{allow_local::Bool, allow_almost::Bool}, Bool} where T3}:
Solver
name | Symbol: :clarabel1
solver | UnionAll: Clarabel.MOIwrapper.Optimizer
settings | Dict{String, Bool}: Dict{String, Bool}("verbose" => 0)
check_sol | @NamedTuple{allow_local::Bool, allow_almost::Bool}: (allow_local = true, allow_almost = true)
add_bridges | Bool: true
Solver
name | Symbol: :clarabel2
solver | UnionAll: Clarabel.MOIwrapper.Optimizer
settings | Dict{String, Real}: Dict{String, Real}("verbose" => false, "max_step_fraction" => 0.75)
check_sol | @NamedTuple{allow_local::Bool, allow_almost::Bool}: (allow_local = true, allow_almost = true)
add_bridges | Bool: true
This time we will use the ConditionalValueatRisk
measure and we will once again precompute prior.
r = ConditionalValueatRisk()
pr = prior(EmpiricalPrior(), rd)
LowOrderPrior
X | 252×20 Matrix{Float64}
mu | 20-element Vector{Float64}
sigma | 20×20 Matrix{Float64}
chol | nothing
w | nothing
ens | nothing
kld | nothing
ow | nothing
rr | nothing
f_mu | nothing
f_sigma | nothing
f_w | nothing
Lets create the efficient frontier by setting returns lower bounds and minimising the risk. We will compute a 30-point frontier.
opt = JuMPOptimiser(; pe = pr, slv = slv, ret = ArithmeticReturn(; lb = Frontier(; N = 30)))
JuMPOptimiser
pe | LowOrderPrior
| X | 252×20 Matrix{Float64}
| mu | 20-element Vector{Float64}
| sigma | 20×20 Matrix{Float64}
| chol | nothing
| w | nothing
| ens | nothing
| kld | nothing
| ow | nothing
| rr | nothing
| f_mu | nothing
| f_sigma | nothing
| f_w | nothing
slv | Vector{Solver{Symbol, UnionAll, T3, @NamedTuple{allow_local::Bool, allow_almost::Bool}, Bool} where T3}: Solver{Symbol, UnionAll, T3, @NamedTuple{allow_local::Bool, allow_almost::Bool}, Bool} where T3[Solver
name | Symbol: :clarabel1
solver | UnionAll: Clarabel.MOIwrapper.Optimizer
settings | Dict{String, Bool}: Dict{String, Bool}("verbose" => 0)
check_sol | @NamedTuple{allow_local::Bool, allow_almost::Bool}: (allow_local = true, allow_almost = true)
add_bridges | Bool: true
, Solver
name | Symbol: :clarabel2
solver | UnionAll: Clarabel.MOIwrapper.Optimizer
settings | Dict{String, Real}: Dict{String, Real}("verbose" => false, "max_step_fraction" => 0.75)
check_sol | @NamedTuple{allow_local::Bool, allow_almost::Bool}: (allow_local = true, allow_almost = true)
add_bridges | Bool: true
]
wb | WeightBounds
| lb | Float64: 0.0
| ub | Float64: 1.0
bgt | Float64: 1.0
sbgt | nothing
lt | nothing
st | nothing
lcs | nothing
lcm | nothing
cent | nothing
gcard | nothing
sgcard | nothing
smtx | nothing
sgmtx | nothing
slt | nothing
sst | nothing
sglt | nothing
sgst | nothing
sets | nothing
nplg | nothing
cplg | nothing
tn | nothing
te | nothing
fees | nothing
ret | ArithmeticReturn
| ucs | nothing
| lb | Frontier
| | N | Int64: 30
| | factor | Int64: 1
| | flag | Bool: true
sce | SumScalariser: SumScalariser()
ccnt | nothing
cobj | nothing
sc | Int64: 1
so | Int64: 1
card | nothing
scard | nothing
nea | nothing
l1 | nothing
l2 | nothing
ss | nothing
strict | Bool: false
We can now use opt
to create the MeanRisk
estimator. In order to get the entire frontier, we need to minimise the risk (which is the default value).
mr = MeanRisk(; opt = opt, r = r)
res1 = optimise!(mr)
JuMPOptimisation
oe | DataType: MeanRisk
pa | ProcessedJuMPOptimiserAttributes
| pr | LowOrderPrior
| | X | 252×20 Matrix{Float64}
| | mu | 20-element Vector{Float64}
| | sigma | 20×20 Matrix{Float64}
| | chol | nothing
| | w | nothing
| | ens | nothing
| | kld | nothing
| | ow | nothing
| | rr | nothing
| | f_mu | nothing
| | f_sigma | nothing
| | f_w | nothing
| wb | WeightBounds
| | lb | 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
| | ub | 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
| lt | nothing
| st | nothing
| lcs | nothing
| cent | nothing
| gcard | nothing
| sgcard | nothing
| smtx | nothing
| sgmtx | nothing
| slt | nothing
| sst | nothing
| sglt | nothing
| sgst | nothing
| nplg | nothing
| cplg | nothing
| tn | nothing
| fees | nothing
| ret | ArithmeticReturn
| | ucs | nothing
| | lb | Frontier
| | | N | Int64: 30
| | | factor | Int64: 1
| | | flag | Bool: true
retcode | 30-element Vector{PortfolioOptimisers.OptimisationReturnCode}
sol | 30-element Vector{PortfolioOptimisers.JuMPOptimisationSolution}
model | A JuMP Model
| ├ solver: Clarabel
| ├ objective_sense: MIN_SENSE
| │ └ objective_function_type: JuMP.AffExpr
| ├ num_variables: 273
| ├ num_constraints: 257
| │ ├ JuMP.AffExpr in MOI.EqualTo{Float64}: 1
| │ ├ JuMP.AffExpr in MOI.GreaterThan{Float64}: 1
| │ ├ Vector{JuMP.AffExpr} in MOI.Nonnegatives: 2
| │ ├ Vector{JuMP.AffExpr} in MOI.Nonpositives: 1
| │ └ JuMP.VariableRef in MOI.GreaterThan{Float64}: 252
| └ Names registered in the model
| └ :X, :bgt, :ccvar_1, :cvar_risk_1, :k, :lw, :net_X, :obj_expr, :ret, :ret_frontier, :ret_lb, :risk, :risk_vec, :sc, :so, :var_1, :w, :w_lb, :w_ub, :z_cvar_1
Note that retcode
and sol
are now vectors. This is because there is one per point in the frontier. Since we didn't get any warnings that any optimisations failed we can proceed without checking the return codes. Regardless, lets check that all optimisations succeeded.
all(x -> isa(x, OptimisationSuccess), res1.retcode)
true
We can view how the weights evolve along the frontier.
pretty_table(DataFrame([rd.nx hcat(res1.w...)], Symbol.([:assets; 1:30]));
formatters = resfmt)
┌────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬─────────┬─────────┬─────────┐
│ assets │ 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 │
│ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │ Any │
├────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼─────────┼─────────┼─────────┤
│ AAPL │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ 0.0 % │
│ AMD │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ 0.0 % │
│ BAC │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ 0.0 % │
│ BBY │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ 0.0 % │
│ CVX │ 13.167 % │ 12.757 % │ 9.552 % │ 3.037 % │ 1.821 % │ 0.159 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ GE │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ 0.0 % │
│ HD │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ 0.0 % │
│ JNJ │ 45.342 % │ 40.238 % │ 37.261 % │ 32.596 % │ 29.384 % │ 26.772 % │ 25.171 % │ 20.067 % │ 16.324 % │ 14.245 % │ 12.166 % │ 10.087 % │ 8.008 % │ 6.101 % │ 1.996 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ JPM │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ 0.0 % │
│ KO │ 13.285 % │ 14.244 % │ 14.767 % │ 18.327 % │ 17.772 % │ 17.332 % │ 15.194 % │ 16.637 % │ 16.595 % │ 14.902 % │ 13.209 % │ 11.516 % │ 9.823 % │ 8.064 % │ 8.415 % │ 6.517 % │ 2.705 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ LLY │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ MRK │ 20.556 % │ 25.205 % │ 27.696 % │ 29.897 % │ 33.915 % │ 36.555 % │ 39.437 % │ 42.723 % │ 46.016 % │ 48.955 % │ 51.893 % │ 54.832 % │ 57.77 % │ 60.441 % │ 63.737 % │ 66.926 % │ 69.546 % │ 69.717 % │ 63.907 % │ 58.098 % │ 52.288 % │ 46.478 % │ 40.668 % │ 34.859 % │ 29.049 % │ 23.239 % │ 17.429 % │ 11.62 % │ 5.81 % │ 0.0 % │
│ MSFT │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ 0.0 % │
│ PEP │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ PFE │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ -0.0 % │ 0.0 % │
│ PG │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ RRC │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ UNH │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ WMT │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ XOM │ 7.65 % │ 7.556 % │ 10.724 % │ 16.142 % │ 17.108 % │ 19.183 % │ 20.197 % │ 20.573 % │ 21.065 % │ 21.898 % │ 22.732 % │ 23.565 % │ 24.399 % │ 25.394 % │ 25.853 % │ 26.557 % │ 27.749 % │ 30.283 % │ 36.093 % │ 41.902 % │ 47.712 % │ 53.522 % │ 59.332 % │ 65.141 % │ 70.951 % │ 76.761 % │ 82.571 % │ 88.38 % │ 94.19 % │ 100.0 % │
└────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴─────────┴─────────┴─────────┘
3. Visualising the efficient frontier
Perhaps it is time to introduce some visualisations, which are implemented as a package extesion. For this we need to import the Plots
and GraphRecipes
packages.
using StatsPlots, GraphRecipes
plot_stacked_area_composition(res1.w, rd.nx)
The efficient frontier is just a special case of a pareto front, we have a function that can plot pareto fronts and surfaces. We have to provide the weights and the prior. There are optional keyword parameters for the risk measure for the X-axis, Y-axis, Z-axis, and colourbar. Here we will use the Conditional Value at Risk as the X-axis, the arithmetic return, and the risk-return ratio as the colourbar.
# Risk-free rate of 4.2/100/252
plot_measures(res1.w, res1.pr; x = r, y = ReturnRiskMeasure(; rt = res1.ret),
c = RatioRiskMeasure(; rt = res1.ret, rk = r, rf = 4.2 / 100 / 252),
title = "Efficient Frontier", xlabel = "CVaR", ylabel = "Arithmetic Return",
colorbar_title = "\nRisk/Return Ratio", right_margin = 6Plots.mm)
The plot_measures
function can plot all sorts of pareto fronts. We can even use the ratio of two risk measures as the colourbar.
plot_measures(res1.w, res1.pr; x = r, y = ConditionalDrawdownatRisk(),
c = RiskRatioRiskMeasure(; r1 = ConditionalDrawdownatRisk(), r2 = r),
title = "Pareto Front", xlabel = "CVaR", ylabel = "CDaR",
colorbar_title = "\nCDaR/CVaR Ratio", right_margin = 6Plots.mm)
This page was generated using Literate.jl.