Skip to content
5

The source files for all examples can be found in /examples.

Example 4: Pareto surface

This example kicks up the complexity a couple of notches. We will introduce a new optimisation estimator, NearOptimalCentering optimiser.

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

julia
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. Preparing solvers for pareto surface

The pareto surface is a generalisation of the efficient frontier, in fact, we can even think of hypersurfaces if we provide more parameters, but that would be difficult to visualise, so we will stick to a 2D surface in 3D space.

We'll provide a vector of solvers because the optimisation type we'll be using is more complex, and will contain various constraints.

julia
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.95),
              check_sol = (; allow_local = true, allow_almost = true)),
       Solver(; name = :clarabel3, solver = Clarabel.Optimizer,
              settings = Dict("verbose" => false, "max_step_fraction" => 0.9),
              check_sol = (; allow_local = true, allow_almost = true)),
       Solver(; name = :clarabel4, solver = Clarabel.Optimizer,
              settings = Dict("verbose" => false, "max_step_fraction" => 0.85),
              check_sol = (; allow_local = true, allow_almost = true)),
       Solver(; name = :clarabel5, solver = Clarabel.Optimizer,
              settings = Dict("verbose" => false, "max_step_fraction" => 0.8),
              check_sol = (; allow_local = true, allow_almost = true)),
       Solver(; name = :clarabel6, solver = Clarabel.Optimizer,
              settings = Dict("verbose" => false, "max_step_fraction" => 0.75),
              check_sol = (; allow_local = true, allow_almost = true)),
       Solver(; name = :clarabel7, solver = Clarabel.Optimizer,
              settings = Dict("verbose" => false, "max_step_fraction" => 0.70),
              check_sol = (; allow_local = true, allow_almost = true))];

3. High order prior statistics

We will once again precompute the prior statistics because otherwise they'd have to be recomputed a few times.

We will be using high order risk measures, so we need to compute high order moments, we can do this with a HighOrderPriorEstimator, which needs a prior estimator that computes low order moments. Since we are only using a year of data, we will denoise our positive definite matrices by eliminating the eigenvalues corresponding to random noise. Denoising the non-positive definite matrix for the data we're using creates a negative square root, so we will not denoise it.

Note how many options this estimator contains.

julia
de = Denoise(; alg = SpectralDenoise(;))
mp = DefaultMatrixProcessing(; denoise = de)
pe = HighOrderPriorEstimator(;
                             # Prior estimator for low order moments
                             pe = EmpiricalPrior(;
                                                 ce = PortfolioOptimisersCovariance(;
                                                                                    mp = mp)),
                             # Estimator for cokurtosis
                             kte = Cokurtosis(; mp = mp),
                             # Estimator for coskewness
                             ske = Coskewness())
HighOrderPriorEstimator
   pe ┼ EmpiricalPrior
      │        ce ┼ PortfolioOptimisersCovariance
      │           │   ce ┼ Covariance
      │           │      │    me ┼ SimpleExpectedReturns
      │           │      │       │   w ┴ nothing
      │           │      │    ce ┼ GeneralCovariance
      │           │      │       │   ce ┼ SimpleCovariance: SimpleCovariance(true)
      │           │      │       │    w ┴ nothing
      │           │      │   alg ┴ Full()
      │           │   mp ┼ DefaultMatrixProcessing
      │           │      │       pdm ┼ Posdef
      │           │      │           │   alg ┴ UnionAll: NearestCorrelationMatrix.Newton
      │           │      │   denoise ┼ Denoise
      │           │      │           │      alg ┼ SpectralDenoise()
      │           │      │           │     args ┼ Tuple{}: ()
      │           │      │           │   kwargs ┼ @NamedTuple{}: NamedTuple()
      │           │      │           │   kernel ┼ typeof(AverageShiftedHistograms.Kernels.gaussian): AverageShiftedHistograms.Kernels.gaussian
      │           │      │           │        m ┼ Int64: 10
      │           │      │           │        n ┴ Int64: 1000
      │           │      │    detone ┼ nothing
      │           │      │       alg ┴ nothing
      │        me ┼ SimpleExpectedReturns
      │           │   w ┴ nothing
      │   horizon ┴ nothing
  kte ┼ Cokurtosis
      │    me ┼ SimpleExpectedReturns
      │       │   w ┴ nothing
      │    mp ┼ DefaultMatrixProcessing
      │       │       pdm ┼ Posdef
      │       │           │   alg ┴ UnionAll: NearestCorrelationMatrix.Newton
      │       │   denoise ┼ Denoise
      │       │           │      alg ┼ SpectralDenoise()
      │       │           │     args ┼ Tuple{}: ()
      │       │           │   kwargs ┼ @NamedTuple{}: NamedTuple()
      │       │           │   kernel ┼ typeof(AverageShiftedHistograms.Kernels.gaussian): AverageShiftedHistograms.Kernels.gaussian
      │       │           │        m ┼ Int64: 10
      │       │           │        n ┴ Int64: 1000
      │       │    detone ┼ nothing
      │       │       alg ┴ nothing
      │   alg ┴ Full()
  ske ┼ Coskewness
      │    me ┼ SimpleExpectedReturns
      │       │   w ┴ nothing
      │    mp ┼ DefaultMatrixProcessing
      │       │       pdm ┼ Posdef
      │       │           │   alg ┴ UnionAll: NearestCorrelationMatrix.Newton
      │       │   denoise ┼ nothing
      │       │    detone ┼ nothing
      │       │       alg ┴ nothing
      │   alg ┴ Full()

Let's compute the prior statistics.

julia
pr = prior(pe, rd)
HighOrderPrior
    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
    kt ┼ 400×400 Matrix{Float64}
    L2 ┼ 210×400 SparseArrays.SparseMatrixCSC{Int64, Int64}
    S2 ┼ 210×400 SparseArrays.SparseMatrixCSC{Int64, Int64}
    sk ┼ 20×400 Matrix{Float64}
     V ┼ 20×20 Matrix{Float64}
  skmp ┼ DefaultMatrixProcessing
       │       pdm ┼ Posdef
       │           │   alg ┴ UnionAll: NearestCorrelationMatrix.Newton
       │   denoise ┼ nothing
       │    detone ┼ nothing
       │       alg ┴ nothing

In order to generate a pareto surface/hyper-surface, we need more dimensions than we've previously explored. We can do this by adding more risk measure sweeps (and taking their product) to generate a mesh. PortfolioOptimisers does this internally and generally, but we will limit ourselves to two risk measures. This will generate a 2D surface which we can visualise in 3D.

We will use the square root NegativeSkewness and Kurtosis.

julia
r1 = NegativeSkewness()
r2 = Kurtosis()
Kurtosis
  settings ┼ RiskMeasureSettings
           │   scale ┼ Float64: 1.0
           │      ub ┼ nothing
           │     rke ┴ Bool: true
         w ┼ nothing
        mu ┼ nothing
        kt ┼ nothing
         N ┼ nothing
      alg1 ┼ Full()
      alg2 ┴ SOCRiskExpr()

4. Near optimal centering pareto surface

First we need to get the bounds of our pareto surface. We can do this in many different ways, the simplest are:

  • Minimise the risk using both risk measures simultaneously subject to optional constraints.

  • Maximise the return, utility or ratio subject to optional constraints.

We will simply maximise the risk-return ratio for both risk measures on their own with no added constraints. This will not give a complete surface, but it will give us a reasonable range of values.

The NearOptimalCentering estimator will not return the portfolio which satisfies the traditional MeanRisk constraints, but rather a portfolio which is at the centre of an analytical region (neighbourhood) around the optimal solution. The region is parametrised by binning the efficient frontier, we will use the automatic bins here, but it is possible to define them manually.

julia
# Risk-free rate of 4.2/100/252
rf = 4.2 / 100 / 252
opt = JuMPOptimiser(; pe = pr, slv = slv)
obj = MaximumRatio(; rf = rf)
opt1 = NearOptimalCentering(; r = r1, obj = obj, opt = opt)
opt2 = NearOptimalCentering(; r = r2, obj = obj, opt = opt)
NearOptimalCentering
        opt ┼ JuMPOptimiser
            │       pe ┼ HighOrderPrior
            │          │     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
            │          │     kt ┼ 400×400 Matrix{Float64}
            │          │     L2 ┼ 210×400 SparseArrays.SparseMatrixCSC{Int64, Int64}
            │          │     S2 ┼ 210×400 SparseArrays.SparseMatrixCSC{Int64, Int64}
            │          │     sk ┼ 20×400 Matrix{Float64}
            │          │      V ┼ 20×20 Matrix{Float64}
            │          │   skmp ┼ DefaultMatrixProcessing
            │          │        │       pdm ┼ Posdef
            │          │        │           │   alg ┴ UnionAll: NearestCorrelationMatrix.Newton
            │          │        │   denoise ┼ nothing
            │          │        │    detone ┼ nothing
            │          │        │       alg ┴ nothing
            │      slv ┼ 7-element Vector{Solver{Symbol, UnionAll, T3, @NamedTuple{allow_local::Bool, allow_almost::Bool}, Bool} where T3}
            │       wb ┼ WeightBounds
            │          │   lb ┼ Float64: 0.0
            │          │   ub ┴ Float64: 1.0
            │      bgt ┼ Float64: 1.0
            │     sbgt ┼ nothing
            │       lt ┼ nothing
            │       st ┼ nothing
            │      lcs ┼ nothing
            │     cent ┼ nothing
            │    gcard ┼ nothing
            │   sgcard ┼ nothing
            │     smtx ┼ nothing
            │    sgmtx ┼ nothing
            │      slt ┼ nothing
            │      sst ┼ nothing
            │     sglt ┼ nothing
            │     sgst ┼ nothing
            │     sets ┼ nothing
            │      plg ┼ nothing
            │       tn ┼ nothing
            │       te ┼ nothing
            │     fees ┼ nothing
            │      ret ┼ ArithmeticReturn
            │          │   ucs ┼ nothing
            │          │    lb ┴ nothing
            │      sce ┼ SumScalariser()
            │     ccnt ┼ nothing
            │     cobj ┼ nothing
            │       sc ┼ Int64: 1
            │       so ┼ Int64: 1
            │       ss ┼ nothing
            │     card ┼ nothing
            │    scard ┼ nothing
            │      nea ┼ nothing
            │       l1 ┼ nothing
            │       l2 ┼ nothing
            │   strict ┴ Bool: false
          r ┼ Kurtosis
            │   settings ┼ RiskMeasureSettings
            │            │   scale ┼ Float64: 1.0
            │            │      ub ┼ nothing
            │            │     rke ┴ Bool: true
            │          w ┼ nothing
            │         mu ┼ nothing
            │         kt ┼ nothing
            │          N ┼ nothing
            │       alg1 ┼ Full()
            │       alg2 ┴ SOCRiskExpr()
        obj ┼ MaximumRatio
            │    rf ┼ Float64: 0.0001666666666666667
            │   ohf ┴ nothing
       bins ┼ nothing
      w_min ┼ nothing
  w_min_ini ┼ nothing
      w_opt ┼ nothing
  w_opt_ini ┼ nothing
      w_max ┼ nothing
  w_max_ini ┼ nothing
   ucs_flag ┼ Bool: true
        alg ┼ UnconstrainedNearOptimalCentering()
         fb ┴ nothing

Note the number of options in the estimator. In particular the alg property. Which in this case means the NearOptimalCentering alg will not have any external constraints applied to it.

Let's optimise the portfolios.

julia
res1 = optimise(opt1)
res2 = optimise(opt2)
NearOptimalCenteringOptimisation
             oe ┼ DataType: DataType
             pa ┼ ProcessedJuMPOptimiserAttributes
                │       pr ┼ HighOrderPrior
                │          │     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
                │          │     kt ┼ 400×400 Matrix{Float64}
                │          │     L2 ┼ 210×400 SparseArrays.SparseMatrixCSC{Int64, Int64}
                │          │     S2 ┼ 210×400 SparseArrays.SparseMatrixCSC{Int64, Int64}
                │          │     sk ┼ 20×400 Matrix{Float64}
                │          │      V ┼ 20×20 Matrix{Float64}
                │          │   skmp ┼ DefaultMatrixProcessing
                │          │        │       pdm ┼ Posdef
                │          │        │           │   alg ┴ UnionAll: NearestCorrelationMatrix.Newton
                │          │        │   denoise ┼ nothing
                │          │        │    detone ┼ nothing
                │          │        │       alg ┴ 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
                │      plg ┼ nothing
                │       tn ┼ nothing
                │     fees ┼ nothing
                │      ret ┼ ArithmeticReturn
                │          │   ucs ┼ nothing
                │          │    lb ┴ nothing
  w_min_retcode ┼ OptimisationSuccess
                │   res ┴ Dict{Any, Any}: Dict{Any, Any}()
  w_opt_retcode ┼ OptimisationSuccess
                │   res ┴ Dict{Any, Any}: Dict{Any, Any}()
  w_max_retcode ┼ OptimisationSuccess
                │   res ┴ Dict{Any, Any}: Dict{Any, Any}()
    noc_retcode ┼ OptimisationSuccess
                │   res ┴ Dict{Any, Any}: Dict{Any, Any}(:clarabel1 => Dict{Symbol, Any}(:settings => Dict{String, Bool}("verbose" => 0), :err => solution_summary(; result = 1, verbose = false)
                │ ├ solver_name          : Clarabel
                │ ├ Termination
                │ │ ├ termination_status : SLOW_PROGRESS
                │ │ ├ result_count       : 1
                │ │ └ raw_status         : INSUFFICIENT_PROGRESS
                │ ├ Solution (result = 1)
                │ │ ├ primal_status        : OTHER_RESULT_STATUS
                │ │ ├ dual_status          : OTHER_RESULT_STATUS
                │ │ ├ objective_value      : 9.29399e+01
                │ │ └ dual_objective_value : 9.29049e+01
                │ └ Work counters
                │   ├ solve_time (sec)   : 2.81466e-01
                │   └ barrier_iterations : 35))
        retcode ┼ OptimisationSuccess
                │   res ┴ nothing
            sol ┼ JuMPOptimisationSolution
                │   w ┴ 20-element Vector{Float64}
          model ┼ A JuMP Model
                │ ├ solver: Clarabel
                │ ├ objective_sense: MIN_SENSE
                │ │ └ objective_function_type: JuMP.AffExpr
                │ ├ num_variables: 273
                │ ├ num_constraints: 47
                │ │ ├ JuMP.AffExpr in MOI.EqualTo{Float64}: 1
                │ │ ├ Vector{JuMP.AffExpr} in MOI.Nonnegatives: 1
                │ │ ├ Vector{JuMP.AffExpr} in MOI.Nonpositives: 1
                │ │ ├ Vector{JuMP.AffExpr} in MOI.SecondOrderCone: 1
                │ │ ├ Vector{JuMP.AffExpr} in MOI.ExponentialCone: 42
                │ │ └ Vector{JuMP.AffExpr} in MOI.PositiveSemidefiniteConeSquare: 1
                │ └ Names registered in the model
                │   └ :Gkt, :L2W, :M, :M_PSD, :W, :bgt, :ckurt_soc_1, :clog_delta_w, :clog_ret, :clog_risk, :clog_w, :k, :kurtosis_risk_1, :log_delta_w, :log_ret, :log_risk, :log_w, :lw, :obj_expr, :ret, :risk, :risk_vec, :sc, :so, :w, :w_lb, :w_ub, :x_kurt_1
             fb ┴ nothing

In order to allow for multiple risk measures in optimisations, certain measures can take different parameters. In this case, NegativeSkewness and Kurtosis take the moment matrices, which are used to compute the risk measures. We can use the factory function to create a new risk measure with the same parameters as the original, but with the moment matrices from the prior. Other risk measures require a solver, and this function is also used in those cases.

julia
r1 = factory(r1, pr)
r2 = factory(r2, pr)
Kurtosis
  settings ┼ RiskMeasureSettings
           │   scale ┼ Float64: 1.0
           │      ub ┼ nothing
           │     rke ┴ Bool: true
         w ┼ nothing
        mu ┼ 20-element Vector{Float64}
        kt ┼ 400×400 Matrix{Float64}
         N ┼ nothing
      alg1 ┼ Full()
      alg2 ┴ SOCRiskExpr()

Let's compute the risk bounds for the pareto surface. We need to compute four risks because we have two risk measures and two optimisations. This will let us pick the lower and upper bounds for each risk measure, as we explore the pareto surface from one optimisation to the other.

julia
sk_rk1 = expected_risk(r1, res1.w, pr.X);
kt_rk1 = expected_risk(r2, res1.w, pr.X);
sk_rk2 = expected_risk(r1, res2.w, pr.X);
kt_rk2 = expected_risk(r2, res2.w, pr.X);

We will now create new risk measures bounded by these values. We will also use factories from the get-go. The optimisation procedure prioritises the parameters in the risk measures over the ones in the prior. This lets users provide the same risk measure with different parameters in the same optimisation. We will use two ranges of 5. The total number of points in the pareto surface will be the product of the points of each range.

Since we don't know which sk_rk1 or sk_r2, kt_rk1 or kt_rk2 is bigger or smaller, we need to use min, max.

julia
r1 = factory(NegativeSkewness(;
                              settings = RiskMeasureSettings(;
                                                             # Risk upper bounds go from the minimum to maximum risk given the optimisations.
                                                             ub = range(;
                                                                        start = min(sk_rk1,
                                                                                    sk_rk2),
                                                                        stop = max(sk_rk1,
                                                                                   sk_rk2),
                                                                        length = 5))), pr);
r2 = factory(Kurtosis(;
                      settings = RiskMeasureSettings(;
                                                     ub = range(;
                                                                start = min(kt_rk1, kt_rk2),
                                                                stop = max(kt_rk1, kt_rk2),
                                                                length = 5))), pr);

Now we only need to maximise the return given both risk measures. Internally, the optimisation will generate the mesh as a product of the ranges in the order in which the risk measures were provided. This also works with the MeanRisk estimator, in fact, NearOptimalCentering uses it internally.

Since we are using an unconstrained NearOptimalCentering, the risk bound constraints will not be satisfied by the solution. If we wish to satisfy them, we can provide alg = ConstrainedNearOptimalCentering(), but would also make the optimisations harder, which may cause them to fail.

julia
opt3 = NearOptimalCentering(; r = [r1, r2], obj = MaximumReturn(), opt = opt)
NearOptimalCentering
        opt ┼ JuMPOptimiser
            │       pe ┼ HighOrderPrior
            │          │     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
            │          │     kt ┼ 400×400 Matrix{Float64}
            │          │     L2 ┼ 210×400 SparseArrays.SparseMatrixCSC{Int64, Int64}
            │          │     S2 ┼ 210×400 SparseArrays.SparseMatrixCSC{Int64, Int64}
            │          │     sk ┼ 20×400 Matrix{Float64}
            │          │      V ┼ 20×20 Matrix{Float64}
            │          │   skmp ┼ DefaultMatrixProcessing
            │          │        │       pdm ┼ Posdef
            │          │        │           │   alg ┴ UnionAll: NearestCorrelationMatrix.Newton
            │          │        │   denoise ┼ nothing
            │          │        │    detone ┼ nothing
            │          │        │       alg ┴ nothing
            │      slv ┼ 7-element Vector{Solver{Symbol, UnionAll, T3, @NamedTuple{allow_local::Bool, allow_almost::Bool}, Bool} where T3}
            │       wb ┼ WeightBounds
            │          │   lb ┼ Float64: 0.0
            │          │   ub ┴ Float64: 1.0
            │      bgt ┼ Float64: 1.0
            │     sbgt ┼ nothing
            │       lt ┼ nothing
            │       st ┼ nothing
            │      lcs ┼ nothing
            │     cent ┼ nothing
            │    gcard ┼ nothing
            │   sgcard ┼ nothing
            │     smtx ┼ nothing
            │    sgmtx ┼ nothing
            │      slt ┼ nothing
            │      sst ┼ nothing
            │     sglt ┼ nothing
            │     sgst ┼ nothing
            │     sets ┼ nothing
            │      plg ┼ nothing
            │       tn ┼ nothing
            │       te ┼ nothing
            │     fees ┼ nothing
            │      ret ┼ ArithmeticReturn
            │          │   ucs ┼ nothing
            │          │    lb ┴ nothing
            │      sce ┼ SumScalariser()
            │     ccnt ┼ nothing
            │     cobj ┼ nothing
            │       sc ┼ Int64: 1
            │       so ┼ Int64: 1
            │       ss ┼ nothing
            │     card ┼ nothing
            │    scard ┼ nothing
            │      nea ┼ nothing
            │       l1 ┼ nothing
            │       l2 ┼ nothing
            │   strict ┴ Bool: false
          r ┼ RiskMeasure[NegativeSkewness
            │   settings ┼ RiskMeasureSettings
            │            │   scale ┼ Float64: 1.0
            │            │      ub ┼ StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}: 0.0023843110534115166:7.19282054030126e-5:0.002672023875023567
            │            │     rke ┴ Bool: true
            │         mp ┼ DefaultMatrixProcessing
            │            │       pdm ┼ Posdef
            │            │           │   alg ┴ UnionAll: NearestCorrelationMatrix.Newton
            │            │   denoise ┼ nothing
            │            │    detone ┼ nothing
            │            │       alg ┴ nothing
            │         sk ┼ 20×400 Matrix{Float64}
            │          V ┼ 20×20 Matrix{Float64}
            │        alg ┴ SOCRiskExpr()
            │ , Kurtosis
            │   settings ┼ RiskMeasureSettings
            │            │   scale ┼ Float64: 1.0
            │            │      ub ┼ StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}: 0.00024459272226908814:6.019201801454017e-6:0.0002686695294749042
            │            │     rke ┴ Bool: true
            │          w ┼ nothing
            │         mu ┼ 20-element Vector{Float64}
            │         kt ┼ 400×400 Matrix{Float64}
            │          N ┼ nothing
            │       alg1 ┼ Full()
            │       alg2 ┴ SOCRiskExpr()
            │ ]
        obj ┼ MaximumReturn()
       bins ┼ nothing
      w_min ┼ nothing
  w_min_ini ┼ nothing
      w_opt ┼ nothing
  w_opt_ini ┼ nothing
      w_max ┼ nothing
  w_max_ini ┼ nothing
   ucs_flag ┼ Bool: true
        alg ┼ UnconstrainedNearOptimalCentering()
         fb ┴ nothing

See how r is a vector of risk measures with populated properties. We can now optimise the portfolios.

julia
res3 = optimise(opt3)
NearOptimalCenteringOptimisation
             oe ┼ DataType: DataType
             pa ┼ ProcessedJuMPOptimiserAttributes
                │       pr ┼ HighOrderPrior
                │          │     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
                │          │     kt ┼ 400×400 Matrix{Float64}
                │          │     L2 ┼ 210×400 SparseArrays.SparseMatrixCSC{Int64, Int64}
                │          │     S2 ┼ 210×400 SparseArrays.SparseMatrixCSC{Int64, Int64}
                │          │     sk ┼ 20×400 Matrix{Float64}
                │          │      V ┼ 20×20 Matrix{Float64}
                │          │   skmp ┼ DefaultMatrixProcessing
                │          │        │       pdm ┼ Posdef
                │          │        │           │   alg ┴ UnionAll: NearestCorrelationMatrix.Newton
                │          │        │   denoise ┼ nothing
                │          │        │    detone ┼ nothing
                │          │        │       alg ┴ 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
                │      plg ┼ nothing
                │       tn ┼ nothing
                │     fees ┼ nothing
                │      ret ┼ ArithmeticReturn
                │          │   ucs ┼ nothing
                │          │    lb ┴ nothing
  w_min_retcode ┼ OptimisationSuccess
                │   res ┴ Dict{Any, Any}: Dict{Any, Any}()
  w_opt_retcode ┼ 25-element Vector{PortfolioOptimisers.OptimisationReturnCode}
  w_max_retcode ┼ OptimisationSuccess
                │   res ┴ Dict{Any, Any}: Dict{Any, Any}()
    noc_retcode ┼ 25-element Vector{PortfolioOptimisers.OptimisationReturnCode}
        retcode ┼ OptimisationSuccess
                │   res ┴ nothing
            sol ┼ 25-element Vector{JuMPOptimisationSolution}
          model ┼ A JuMP Model
                │ ├ solver: Clarabel
                │ ├ objective_sense: MIN_SENSE
                │ │ └ objective_function_type: JuMP.AffExpr
                │ ├ num_variables: 274
                │ ├ num_constraints: 48
                │ │ ├ JuMP.AffExpr in MOI.EqualTo{Float64}: 1
                │ │ ├ Vector{JuMP.AffExpr} in MOI.Nonnegatives: 1
                │ │ ├ Vector{JuMP.AffExpr} in MOI.Nonpositives: 1
                │ │ ├ Vector{JuMP.AffExpr} in MOI.SecondOrderCone: 2
                │ │ ├ Vector{JuMP.AffExpr} in MOI.ExponentialCone: 42
                │ │ └ Vector{JuMP.AffExpr} in MOI.PositiveSemidefiniteConeSquare: 1
                │ └ Names registered in the model
                │   └ :L2W, :M, :M_PSD, :W, :bgt, :ckurt_soc_2, :clog_delta_w, :clog_ret, :clog_risk, :clog_w, :cnskew_soc_1, :k, :kurtosis_risk_2, :log_delta_w, :log_ret, :log_risk, :log_w, :lw, :nskew_risk_1, :obj_expr, :ret, :risk, :risk_vec, :sc, :so, :w, :w_lb, :w_ub, :x_kurt_2
             fb ┴ nothing

As expected, there are 5 × 5 = 25 solutions. Thankfully there are no warnings about failed optimisations, so there is no need to check the solutions.

The NearOptimalCentering estimator contains various return codes because it may need to compute some MeanRisk optimisations, it has a retcode which summarises whether all other optimisations succeeded. We can check this to make sure it was a success.

julia
isa(res3.retcode, OptimisationSuccess)
true

5. Visualising the pareto surface

Let's view how the weights evolve along the pareto surface.

julia
using StatsPlots, GraphRecipes
plot_stacked_area_composition(res3.w, rd.nx)

Now we can view the pareto surface. For the z-axis and colourbar, we will use the conditional drawdown at risk to return ratio.

julia
plot_measures(res3.w, pr; x = r1, y = r2,
              z = RatioRiskMeasure(; rk = ConditionalDrawdownatRisk(),
                                   rt = ArithmeticReturn(), rf = rf),
              c = RatioRiskMeasure(; rk = ConditionalDrawdownatRisk(),
                                   rt = ArithmeticReturn(), rf = rf),
              title = "Pareto Surface", xlabel = "Sqrt NSkew", ylabel = "Sqrt Kurt",
              zlabel = "CDaR/Return")

We can view it in 2D as well.

julia
gr()
plot_measures(res3.w, pr; x = r1, y = r2,
              c = RatioRiskMeasure(; rk = ConditionalDrawdownatRisk(),
                                   rt = ArithmeticReturn(), rf = rf),
              title = "Pareto Front", xlabel = "Sqrt NSkew", ylabel = "Sqrt Kurt",
              colorbar_title = "\n\nCDaR/Return", right_margin = 8Plots.mm)


This page was generated using Literate.jl.