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)
Example block output

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)
Example block output

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)
Example block output

This page was generated using Literate.jl.