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.

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. 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 beacause the optimisation type we'll be using is more complex, and will contain various constraints.

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.

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 | GeneralWeightedCovariance
      |           |      |       |   ce | StatsBase.SimpleCovariance: StatsBase.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 | NonPositiveDefiniteMatrixProcessing
      |       |   denoise | nothing
      |       |    detone | nothing
      |       |       alg | nothing
      |   alg | Full()

Lets compute the prior statistics.

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 | NonPositiveDefiniteMatrixProcessing
       |   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 SquareRootKurtosis.

r1 = NegativeSkewness()
r2 = SquareRootKurtosis()
SquareRootKurtosis
  settings | RiskMeasureSettings
           |   scale | Float64: 1.0
           |      ub | nothing
           |     rke | Bool: true
         w | nothing
        mu | nothing
        kt | nothing
         N | nothing
       alg | Full()

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.

# 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 | NonPositiveDefiniteMatrixProcessing
            |          |        |   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
            |      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 | nothing
            |      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
          r | SquareRootKurtosis
            |   settings | RiskMeasureSettings
            |            |   scale | Float64: 1.0
            |            |      ub | nothing
            |            |     rke | Bool: true
            |          w | nothing
            |         mu | nothing
            |         kt | nothing
            |          N | nothing
            |        alg | Full()
        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()

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.

Lets optimise the portfolios.

res1 = optimise!(opt1)
res2 = optimise!(opt2)
NearOptimalCenteringOptimisation
             oe | DataType: NearOptimalCentering
             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 | NonPositiveDefiniteMatrixProcessing
                |          |        |   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
                |     nplg | nothing
                |     cplg | 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.29400e+01
                | │ └ dual_objective_value : 9.29050e+01
                | └ Work counters
                |   ├ solve_time (sec)   : 2.61538e-01
                |   └ barrier_iterations : 35))
        retcode | OptimisationSuccess
                |   res | nothing
            sol | PortfolioOptimisers.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
                |   └ :M_PSD, :W, :W_M, :bgt, :ckurt_soc_1, :clog_delta_w, :clog_ret, :clog_risk, :clog_w, :k, :log_delta_w, :log_ret, :log_risk, :log_w, :lw, :obj_expr, :ret, :risk, :risk_vec, :sc, :so, :sqrt_kurtosis_risk_1, :w, :w_lb, :w_ub, :zkurt_1

In order to allow for multiple risk measures in optimisations, certain measures can take different parameters. In this case, NegativeSkewness and SquareRootKurtosis 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.

r1 = factory(r1, pr)
r2 = factory(r2, pr)
SquareRootKurtosis
  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
       alg | Full()

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

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.

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(SquareRootKurtosis(;
                                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 estimatro, 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.

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 | NonPositiveDefiniteMatrixProcessing
            |          |        |   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
            |      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 | nothing
            |      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
          r | Vector{PortfolioOptimisers.RiskMeasure}: PortfolioOptimisers.RiskMeasure[NegativeSkewness
  settings | RiskMeasureSettings
           |   scale | Float64: 1.0
           |      ub | StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}: 0.0014407874342906597:0.00010182732908744173:0.0018480967506404266
           |     rke | Bool: true
        mp | NonPositiveDefiniteMatrixProcessing
           |   denoise | nothing
           |    detone | nothing
           |       alg | nothing
        sk | 20×400 Matrix{Float64}
         V | 20×20 Matrix{Float64}
       alg | SqrtRiskExpr()
, SquareRootKurtosis
  settings | RiskMeasureSettings
           |   scale | Float64: 1.0
           |      ub | StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}: 0.00023763521983569873:7.758896959421372e-6:0.0002686708076733842
           |     rke | Bool: true
         w | nothing
        mu | 20-element Vector{Float64}
        kt | 400×400 Matrix{Float64}
         N | nothing
       alg | Full()
]
        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()

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

res3 = optimise!(opt3)
NearOptimalCenteringOptimisation
             oe | DataType: NearOptimalCentering
             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 | NonPositiveDefiniteMatrixProcessing
                |          |        |   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
                |     nplg | nothing
                |     cplg | 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{PortfolioOptimisers.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
                |   └ :M_PSD, :W, :W_M, :bgt, :ckurt_soc_2, :clog_delta_w, :clog_ret, :clog_risk, :clog_w, :cnskew_soc_1, :k, :log_delta_w, :log_ret, :log_risk, :log_w, :lw, :nskew_risk_1, :obj_expr, :ret, :risk, :risk_vec, :sc, :so, :sqrt_kurtosis_risk_2, :w, :w_lb, :w_ub, :zkurt_2

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.

isa(res3.retcode, OptimisationSuccess)
true

5. Visualising the pareto surface

Lets view how the weights evolve along the pareto surface.

using StatsPlots, GraphRecipes
plot_stacked_area_composition(res3.w, rd.nx)
Example block output

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

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 NSke", ylabel = "Sqrt Kt",
              zlabel = "CDaR/Return")
Example block output

We can view it in 2D as well.

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

This page was generated using Literate.jl.