The source files can be found in examples/.

Pareto surface

This page extends the efficient frontier to two risk measures and the return. We compute the surface with NearOptimalCentering, which the efficient-frontier page introduced.

When to reach for this

Reach for a Pareto surface when one frontier of risk and return is not enough, because you trade off more than two criteria at once, such as two risk measures and the return. The efficient frontier is a curve. With three criteria, the portfolios that no other portfolio beats on every criterion form a surface, and with more they form a hypersurface. Read the efficient-frontier example first, and use it if two criteria are enough.

using PortfolioOptimisers, PrettyTablestsfmt = (v, i, j) -> begin    if j == 1        return Date(v)    else        return v    endend;resfmt = (v, i, j) -> begin    if j == 1        return v    else        return isa(v, Number) ? "$(round(v*100, digits=3)) %" : v    endend;

1. Data

We use the same data as the previous example.

using CSV, TimeSeries, DataFramesX = TimeArray(CSV.File(joinpath(@__DIR__, "..", "SP500.csv.gz")); timestamp = :Date)[(end - 252):end]pretty_table(X[(end - 5):end]; formatters = [tsfmt])rd = prices_to_returns(X)
ReturnsResult
    nx ┼ 20-element Vector{String}
     X ┼ 252×20 Matrix{Float64}
    nf ┼ nothing
     F ┼ nothing
    nb ┼ nothing
     B ┼ nothing
    ne ┼ nothing
     E ┼ nothing
    ts ┼ 252-element Vector{Date}
    iv ┼ nothing
  ivpa ┼ nothing
   pnl ┼ AssetPanel
       │     pf ┼ Vector{AbstractPanelField}: AbstractPanelField[]
       │   amsk ┼ 252×20 AllTrueMask
       │   emsk ┴ 252×20 AllTrueMask

2. Solvers for the Pareto surface

The optimisation on this page is harder and has more constraints, so we pass a vector of seven solvers. The first uses the default settings, and the next six set max_step_fraction from 0.95 down to 0.70 in steps of 0.05.

using Clarabelslv = [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

The risk measures on this page are of high order, so the prior needs the high-order moments. HighOrderPriorEstimator computes them, and it takes a second prior estimator for the low-order moments. One year of data is short, so we denoise the positive definite matrices, the covariance and the cokurtosis. SpectralDenoise sets to zero the eigenvalues that random noise explains. The coskewness gives a matrix that is not positive definite, and on this data its denoised form puts a negative number under a square root, so we leave it as it is.

dn = Denoise(; alg = SpectralDenoise(;))mp = MatrixProcessing(; dn = dn)pe = HighOrderPriorEstimator(;                             pe = EmpiricalPrior(;                                                 ce = PortfolioOptimisersCovariance(;                                                                                    mp = mp)),                             kte = Cokurtosis(; mp = mp), ske = Coskewness())
HighOrderPriorEstimator
     pe ┼ EmpiricalPrior
        │           ce ┼ PortfolioOptimisersCovariance
        │              │   ce ┼ Covariance
        │              │      │    me ┼ SimpleExpectedReturns
        │              │      │       │   w ┴ nothing
        │              │      │    ce ┼ GeneralCovariance
        │              │      │       │   ce ┼ SimpleCovariance: SimpleCovariance(true)
        │              │      │       │    w ┴ nothing
        │              │      │   alg ┼ FullMoment()
        │              │      │     w ┴ nothing
        │              │   mp ┼ MatrixProcessing
        │              │      │     pdm ┼ Posdef
        │              │      │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
        │              │      │         │   kwargs ┴ @NamedTuple{}: NamedTuple()
        │              │      │      dn ┼ Denoise
        │              │      │         │      pdm ┼ Posdef
        │              │      │         │          │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
        │              │      │         │          │   kwargs ┴ @NamedTuple{}: NamedTuple()
        │              │      │         │      alg ┼ SpectralDenoise()
        │              │      │         │     args ┼ Tuple{}: ()
        │              │      │         │   kwargs ┼ @NamedTuple{}: NamedTuple()
        │              │      │         │   kernel ┼ typeof(AverageShiftedHistograms.Kernels.gaussian): AverageShiftedHistograms.Kernels.gaussian
        │              │      │         │        m ┼ Int64: 10
        │              │      │         │        n ┴ Int64: 1000
        │              │      │      dt ┼ nothing
        │              │      │     alg ┼ nothing
        │              │      │   order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)
        │           me ┼ SimpleExpectedReturns
        │              │   w ┴ nothing
        │      horizon ┼ nothing
        │   fill_limit ┼ nothing
        │        x_src ┴ nothing
    kte ┼ Cokurtosis
        │      me ┼ SimpleExpectedReturns
        │         │   w ┴ nothing
        │      mp ┼ MatrixProcessing
        │         │     pdm ┼ Posdef
        │         │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
        │         │         │   kwargs ┴ @NamedTuple{}: NamedTuple()
        │         │      dn ┼ Denoise
        │         │         │      pdm ┼ Posdef
        │         │         │          │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
        │         │         │          │   kwargs ┴ @NamedTuple{}: NamedTuple()
        │         │         │      alg ┼ SpectralDenoise()
        │         │         │     args ┼ Tuple{}: ()
        │         │         │   kwargs ┼ @NamedTuple{}: NamedTuple()
        │         │         │   kernel ┼ typeof(AverageShiftedHistograms.Kernels.gaussian): AverageShiftedHistograms.Kernels.gaussian
        │         │         │        m ┼ Int64: 10
        │         │         │        n ┴ Int64: 1000
        │         │      dt ┼ nothing
        │         │     alg ┼ nothing
        │         │   order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)
        │     alg ┼ FullMoment()
        │       w ┼ nothing
        │   cache ┴ nothing
    ske ┼ Coskewness
        │      me ┼ SimpleExpectedReturns
        │         │   w ┴ nothing
        │      mp ┼ MatrixProcessing
        │         │     pdm ┼ Posdef
        │         │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
        │         │         │   kwargs ┴ @NamedTuple{}: NamedTuple()
        │         │      dn ┼ nothing
        │         │      dt ┼ nothing
        │         │     alg ┼ nothing
        │         │   order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)
        │     alg ┼ FullMoment()
        │       w ┼ nothing
        │   cache ┴ nothing
  x_src ┴ nothing

We compute the prior once, because the page uses it several times.

pr = prior(pe, rd)
HighOrderPrior
    pr ┼ LowOrderPrior
       │         X ┼ 252×20 Matrix{Float64}
       │       o_X ┼ nothing
       │        mu ┼ 20-element Vector{Float64}
       │     sigma ┼ 20×20 Matrix{Float64}
       │      chol ┼ nothing
       │         w ┼ nothing
       │       ens ┼ nothing
       │       kld ┼ nothing
       │        ow ┼ nothing
       │        rr ┼ nothing
       │       fpr ┼ nothing
       │     x_src ┼ nothing
       │   ss_gaps ┼ nothing
       │        pc ┴ nothing
    kt ┼ 400×400 Matrix{Float64}
    D2 ┼ 400×210 SparseArrays.SparseMatrixCSC{Int64, Int64}
    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 ┼ MatrixProcessing
       │     pdm ┼ Posdef
       │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
       │         │   kwargs ┴ @NamedTuple{}: NamedTuple()
       │      dn ┼ nothing
       │      dt ┼ nothing
       │     alg ┼ nothing
       │   order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)
   fpr ┴ nothing

A Pareto surface needs more dimensions than a frontier. Each risk measure with a range of bounds adds one, and the product of the ranges gives a mesh of points. The library builds the mesh for any number of risk measures. More measures give a hypersurface, which is hard to plot, so we use two, which give a 2D surface that we plot in 3D.

The two measures are the square root of the negative skewness, NegativeSkewness, and the square root of the kurtosis, Kurtosis.

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 ┼ FullMoment()
      alg2 ┼ SOCRiskExpr()
        pe ┼ nothing
  mtx_sqrt ┴ EigenFallbackSquareRoot()

4. The Pareto surface with near-optimal centring

First we find the bounds of the surface. There are many ways to find them, and the two simplest are:

  • Minimise the risk under both risk measures at once, with any constraints you choose.
  • Maximise the return, the utility or the ratio, with any constraints you choose.

We maximise the risk-return ratio under each risk measure alone, with no other constraints. This does not give the whole surface, but it gives a useful range of values.

NearOptimalCentering does not return the optimal MeanRisk portfolio. It returns the portfolio at the analytic centre of a region around the optimum. The size of the region comes from splitting the efficient frontier into bins. We keep the default number of bins, the number of observations divided by the number of assets, and you can set it with the bins field.

rf = 4.2 / 100 / 252opt = 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}
            │           │        │       o_X ┼ nothing
            │           │        │        mu ┼ 20-element Vector{Float64}
            │           │        │     sigma ┼ 20×20 Matrix{Float64}
            │           │        │      chol ┼ nothing
            │           │        │         w ┼ nothing
            │           │        │       ens ┼ nothing
            │           │        │       kld ┼ nothing
            │           │        │        ow ┼ nothing
            │           │        │        rr ┼ nothing
            │           │        │       fpr ┼ nothing
            │           │        │     x_src ┼ nothing
            │           │        │   ss_gaps ┼ nothing
            │           │        │        pc ┴ nothing
            │           │     kt ┼ 400×400 Matrix{Float64}
            │           │     D2 ┼ 400×210 SparseArrays.SparseMatrixCSC{Int64, Int64}
            │           │     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 ┼ MatrixProcessing
            │           │        │     pdm ┼ Posdef
            │           │        │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
            │           │        │         │   kwargs ┴ @NamedTuple{}: NamedTuple()
            │           │        │      dn ┼ nothing
            │           │        │      dt ┼ nothing
            │           │        │     alg ┼ nothing
            │           │        │   order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)
            │           │    fpr ┴ nothing
            │       slv ┼ 7-element Vector{Solver}
            │           │ Solver ⋯
            │           │ Solver ⋯
            │           │ Solver ⋯
            │           │ Solver ⋯
            │           │ Solver ⋯
            │           │ Solver ⋯
            │           │ Solver ⋯
            │        wb ┼ WeightBounds
            │           │   lb ┼ Float64: 0.0
            │           │   ub ┴ Float64: 1.0
            │       bgt ┼ Float64: 1.0
            │      sbgt ┼ nothing
            │      gbgt ┼ nothing
            │        lt ┼ nothing
            │        st ┼ nothing
            │      lcse ┼ nothing
            │       cte ┼ nothing
            │    gcarde ┼ nothing
            │   sgcarde ┼ nothing
            │      smtx ┼ nothing
            │     sgmtx ┼ nothing
            │       slt ┼ nothing
            │       sst ┼ nothing
            │      sglt ┼ nothing
            │      sgst ┼ nothing
            │        tn ┼ nothing
            │      fees ┼ nothing
            │      sets ┼ nothing
            │        tr ┼ nothing
            │       ple ┼ nothing
            │       ret ┼ ArithmeticReturn
            │           │   settings ┼ JuMPReturnsSettings
            │           │            │   scale ┼ Float64: 1.0
            │           │            │      lb ┼ nothing
            │           │            │     rte ┼ Bool: true
            │           │            │     fee ┼ Bool: true
            │           │            │     mic ┴ Bool: true
            │           │        ucs ┼ nothing
            │           │         mu ┼ nothing
            │           │   mtx_sqrt ┴ EigenFallbackSquareRoot()
            │       sca ┼ SumScalariser()
            │      ccnt ┼ nothing
            │      cobj ┼ nothing
            │        sc ┼ Int64: 1
            │        so ┼ Int64: 1
            │        ss ┼ nothing
            │      card ┼ nothing
            │     scard ┼ nothing
            │       l2c ┼ nothing
            │       lpc ┼ nothing
            │     linfc ┼ nothing
            │        l1 ┼ nothing
            │        l2 ┼ nothing
            │        lp ┼ nothing
            │      linf ┼ nothing
            │      xbgt ┼ Bool: false
            │    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 ┼ FullMoment()
            │       alg2 ┼ SOCRiskExpr()
            │         pe ┼ nothing
            │   mtx_sqrt ┴ EigenFallbackSquareRoot()
        obj ┼ MaximumRatio
            │     rf ┼ Float64: 0.0001666666666666667
            │    ohf ┼ nothing
            │   kmin ┴ 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

The printout lists the fields of the estimator. Its alg field is UnconstrainedNearOptimalCentering(), so the centring step applies only the weight bounds and the budgets of the optimiser.

We optimise the two portfolios.

res1 = optimise(opt1)res2 = optimise(opt2)
NearOptimalCenteringResult
             jr ┼ JuMPOptimisationResult
                │        pa ┼ ProcessedJuMPOptimiserAttributes
                │           │        pr ┼ HighOrderPrior
                │           │           │     pr ┼ LowOrderPrior
                │           │           │        │         X ┼ 252×20 Matrix{Float64}
                │           │           │        │       o_X ┼ nothing
                │           │           │        │        mu ┼ 20-element Vector{Float64}
                │           │           │        │     sigma ┼ 20×20 Matrix{Float64}
                │           │           │        │      chol ┼ nothing
                │           │           │        │         w ┼ nothing
                │           │           │        │       ens ┼ nothing
                │           │           │        │       kld ┼ nothing
                │           │           │        │        ow ┼ nothing
                │           │           │        │        rr ┼ nothing
                │           │           │        │       fpr ┼ nothing
                │           │           │        │     x_src ┼ nothing
                │           │           │        │   ss_gaps ┼ nothing
                │           │           │        │        pc ┴ nothing
                │           │           │     kt ┼ 400×400 Matrix{Float64}
                │           │           │     D2 ┼ 400×210 SparseArrays.SparseMatrixCSC{Int64, Int64}
                │           │           │     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 ┼ MatrixProcessing
                │           │           │        │     pdm ┼ Posdef
                │           │           │        │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
                │           │           │        │         │   kwargs ┴ @NamedTuple{}: NamedTuple()
                │           │           │        │      dn ┼ nothing
                │           │           │        │      dt ┼ nothing
                │           │           │        │     alg ┼ nothing
                │           │           │        │   order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)
                │           │           │    fpr ┴ 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
                │           │      lcsr ┼ nothing
                │           │       ctr ┼ nothing
                │           │    gcardr ┼ nothing
                │           │   sgcardr ┼ nothing
                │           │      smtx ┼ nothing
                │           │     sgmtx ┼ nothing
                │           │       slt ┼ nothing
                │           │       sst ┼ nothing
                │           │      sglt ┼ nothing
                │           │      sgst ┼ nothing
                │           │        tn ┼ nothing
                │           │      fees ┼ nothing
                │           │       plr ┼ nothing
                │           │       ret ┼ ArithmeticReturn
                │           │           │   settings ┼ JuMPReturnsSettings
                │           │           │            │   scale ┼ Float64: 1.0
                │           │           │            │      lb ┼ nothing
                │           │           │            │     rte ┼ Bool: true
                │           │           │            │     fee ┼ Bool: true
                │           │           │            │     mic ┴ Bool: true
                │           │           │        ucs ┼ nothing
                │           │           │         mu ┼ 20-element Vector{Float64}
                │           │           │   mtx_sqrt ┴ EigenFallbackSquareRoot()
                │           │       sca ┼ SumScalariser()
                │           │      imsk ┴ nothing
                │   retcode ┼ OptimisationSuccess
                │           │   res ┴ nothing
                │       sol ┼ JuMPOptimisationSolution
                │           │   w ┴ 20-element Vector{Float64}
                │     model ┼ A JuMP Model
                │           │ ├ solver: Clarabel
                │           │ ├ objective_sense: MIN_SENSE
                │           │ │ └ objective_function_type: AffExpr
                │           │ ├ num_variables: 273
                │           │ ├ num_constraints: 47
                │           │ │ ├ AffExpr in MOI.EqualTo{Float64}: 1
                │           │ │ ├ Vector{AffExpr} in MOI.Nonnegatives: 1
                │           │ │ ├ Vector{AffExpr} in MOI.Nonpositives: 1
                │           │ │ ├ Vector{AffExpr} in MOI.SecondOrderCone: 1
                │           │ │ ├ Vector{AffExpr} in MOI.ExponentialCone: 42
                │           │ │ └ Vector{AffExpr} in MOI.PositiveSemidefiniteConeSquare: 1
                │           │ └ Names registered in the model
                │           │   └ :Gkt, :L2W, :M, :M_PSD, :T, :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, :noc_rk, :noc_rt, :obj_expr, :ret, :ret_1, :ret_vec, :risk, :risk_vec, :sc, :so, :w, :w_gross_ub, :w_lb, :w_ub, :x_kurt_1
              r ┼ 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 ┼ FullMoment()
                │       alg2 ┼ SOCRiskExpr()
                │         pe ┼ nothing
                │   mtx_sqrt ┴ EigenFallbackSquareRoot()
  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}()
             fb ┴ nothing

expected_risk needs the moment matrices inside the measure, and NegativeSkewness and Kurtosis hold none yet. factory returns a copy of a risk measure with those matrices taken from the prior. For a risk measure that needs a solver, it fills that in too.

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 ┼ FullMoment()
      alg2 ┼ SOCRiskExpr()
        pe ┼ nothing
  mtx_sqrt ┴ EigenFallbackSquareRoot()

We compute the risk bounds of the surface. Two risk measures on two portfolios give four risks. From them we take the lower and the upper bound of each risk measure, so the surface spans the space between the two portfolios.

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 build new risk measures with these bounds, and pass each one through factory immediately, so that each takes its matrices from the prior. Each range has 5 values, and the surface has one point per pair of values.

We do not know which of sk_rk1 and sk_rk2 is the larger, or which of kt_rk1 and kt_rk2, so we use min and max.

r1 = factory(NegativeSkewness(;                              settings = RiskMeasureSettings(;                                                             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);

We maximise the return under both risk measures. The optimisation builds the mesh as the product of the ranges, in the order in which you give the risk measures. MeanRisk builds the mesh the same way, and NearOptimalCentering solves MeanRisk problems to find its region.

Our NearOptimalCentering is the unconstrained variant, so its portfolios need not stay inside the risk bounds. To keep them inside, set alg to ConstrainedNearOptimalCentering. The optimisations are then harder, and some of them can fail.

opt3 = NearOptimalCentering(; r = [r1, r2], obj = MaximumReturn(), opt = opt)
NearOptimalCentering
        opt ┼ JuMPOptimiser
            │        pe ┼ HighOrderPrior
            │           │     pr ┼ LowOrderPrior
            │           │        │         X ┼ 252×20 Matrix{Float64}
            │           │        │       o_X ┼ nothing
            │           │        │        mu ┼ 20-element Vector{Float64}
            │           │        │     sigma ┼ 20×20 Matrix{Float64}
            │           │        │      chol ┼ nothing
            │           │        │         w ┼ nothing
            │           │        │       ens ┼ nothing
            │           │        │       kld ┼ nothing
            │           │        │        ow ┼ nothing
            │           │        │        rr ┼ nothing
            │           │        │       fpr ┼ nothing
            │           │        │     x_src ┼ nothing
            │           │        │   ss_gaps ┼ nothing
            │           │        │        pc ┴ nothing
            │           │     kt ┼ 400×400 Matrix{Float64}
            │           │     D2 ┼ 400×210 SparseArrays.SparseMatrixCSC{Int64, Int64}
            │           │     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 ┼ MatrixProcessing
            │           │        │     pdm ┼ Posdef
            │           │        │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
            │           │        │         │   kwargs ┴ @NamedTuple{}: NamedTuple()
            │           │        │      dn ┼ nothing
            │           │        │      dt ┼ nothing
            │           │        │     alg ┼ nothing
            │           │        │   order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)
            │           │    fpr ┴ nothing
            │       slv ┼ 7-element Vector{Solver}
            │           │ Solver ⋯
            │           │ Solver ⋯
            │           │ Solver ⋯
            │           │ Solver ⋯
            │           │ Solver ⋯
            │           │ Solver ⋯
            │           │ Solver ⋯
            │        wb ┼ WeightBounds
            │           │   lb ┼ Float64: 0.0
            │           │   ub ┴ Float64: 1.0
            │       bgt ┼ Float64: 1.0
            │      sbgt ┼ nothing
            │      gbgt ┼ nothing
            │        lt ┼ nothing
            │        st ┼ nothing
            │      lcse ┼ nothing
            │       cte ┼ nothing
            │    gcarde ┼ nothing
            │   sgcarde ┼ nothing
            │      smtx ┼ nothing
            │     sgmtx ┼ nothing
            │       slt ┼ nothing
            │       sst ┼ nothing
            │      sglt ┼ nothing
            │      sgst ┼ nothing
            │        tn ┼ nothing
            │      fees ┼ nothing
            │      sets ┼ nothing
            │        tr ┼ nothing
            │       ple ┼ nothing
            │       ret ┼ ArithmeticReturn
            │           │   settings ┼ JuMPReturnsSettings
            │           │            │   scale ┼ Float64: 1.0
            │           │            │      lb ┼ nothing
            │           │            │     rte ┼ Bool: true
            │           │            │     fee ┼ Bool: true
            │           │            │     mic ┴ Bool: true
            │           │        ucs ┼ nothing
            │           │         mu ┼ nothing
            │           │   mtx_sqrt ┴ EigenFallbackSquareRoot()
            │       sca ┼ SumScalariser()
            │      ccnt ┼ nothing
            │      cobj ┼ nothing
            │        sc ┼ Int64: 1
            │        so ┼ Int64: 1
            │        ss ┼ nothing
            │      card ┼ nothing
            │     scard ┼ nothing
            │       l2c ┼ nothing
            │       lpc ┼ nothing
            │     linfc ┼ nothing
            │        l1 ┼ nothing
            │        l2 ┼ nothing
            │        lp ┼ nothing
            │      linf ┼ nothing
            │      xbgt ┼ Bool: false
            │    strict ┴ Bool: false
          r ┼ 2-element Vector{RiskMeasure}
            │ NegativeSkewness ⋯
            │ Kurtosis ⋯
        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

In the printout, r is a vector of two risk measures, each with its bounds and its moment matrices filled in. We optimise the portfolios.

res3 = optimise(opt3)
NearOptimalCenteringResult
             jr ┼ JuMPOptimisationResult
                │        pa ┼ ProcessedJuMPOptimiserAttributes
                │           │        pr ┼ HighOrderPrior
                │           │           │     pr ┼ LowOrderPrior
                │           │           │        │         X ┼ 252×20 Matrix{Float64}
                │           │           │        │       o_X ┼ nothing
                │           │           │        │        mu ┼ 20-element Vector{Float64}
                │           │           │        │     sigma ┼ 20×20 Matrix{Float64}
                │           │           │        │      chol ┼ nothing
                │           │           │        │         w ┼ nothing
                │           │           │        │       ens ┼ nothing
                │           │           │        │       kld ┼ nothing
                │           │           │        │        ow ┼ nothing
                │           │           │        │        rr ┼ nothing
                │           │           │        │       fpr ┼ nothing
                │           │           │        │     x_src ┼ nothing
                │           │           │        │   ss_gaps ┼ nothing
                │           │           │        │        pc ┴ nothing
                │           │           │     kt ┼ 400×400 Matrix{Float64}
                │           │           │     D2 ┼ 400×210 SparseArrays.SparseMatrixCSC{Int64, Int64}
                │           │           │     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 ┼ MatrixProcessing
                │           │           │        │     pdm ┼ Posdef
                │           │           │        │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
                │           │           │        │         │   kwargs ┴ @NamedTuple{}: NamedTuple()
                │           │           │        │      dn ┼ nothing
                │           │           │        │      dt ┼ nothing
                │           │           │        │     alg ┼ nothing
                │           │           │        │   order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)
                │           │           │    fpr ┴ 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
                │           │      lcsr ┼ nothing
                │           │       ctr ┼ nothing
                │           │    gcardr ┼ nothing
                │           │   sgcardr ┼ nothing
                │           │      smtx ┼ nothing
                │           │     sgmtx ┼ nothing
                │           │       slt ┼ nothing
                │           │       sst ┼ nothing
                │           │      sglt ┼ nothing
                │           │      sgst ┼ nothing
                │           │        tn ┼ nothing
                │           │      fees ┼ nothing
                │           │       plr ┼ nothing
                │           │       ret ┼ ArithmeticReturn
                │           │           │   settings ┼ JuMPReturnsSettings
                │           │           │            │   scale ┼ Float64: 1.0
                │           │           │            │      lb ┼ nothing
                │           │           │            │     rte ┼ Bool: true
                │           │           │            │     fee ┼ Bool: true
                │           │           │            │     mic ┴ Bool: true
                │           │           │        ucs ┼ nothing
                │           │           │         mu ┼ 20-element Vector{Float64}
                │           │           │   mtx_sqrt ┴ EigenFallbackSquareRoot()
                │           │       sca ┼ SumScalariser()
                │           │      imsk ┴ nothing
                │   retcode ┼ OptimisationSuccess
                │           │   res ┴ nothing
                │       sol ┼ 25-element Vector{JuMPOptimisationSolution}
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │           │ JuMPOptimisationSolution ⋯
                │     model ┼ A JuMP Model
                │           │ ├ solver: Clarabel
                │           │ ├ objective_sense: MIN_SENSE
                │           │ │ └ objective_function_type: AffExpr
                │           │ ├ num_variables: 276
                │           │ ├ num_constraints: 50
                │           │ │ ├ AffExpr in MOI.EqualTo{Float64}: 1
                │           │ │ ├ Vector{AffExpr} in MOI.Nonnegatives: 1
                │           │ │ ├ Vector{AffExpr} in MOI.Nonpositives: 1
                │           │ │ ├ Vector{AffExpr} in MOI.SecondOrderCone: 2
                │           │ │ ├ Vector{AffExpr} in MOI.ExponentialCone: 42
                │           │ │ ├ Vector{AffExpr} in MOI.PositiveSemidefiniteConeSquare: 1
                │           │ │ └ VariableRef in MOI.Parameter{Float64}: 2
                │           │ └ Names registered in the model
                │           │   └ :L2W, :M, :M_PSD, :T, :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, :noc_rk, :noc_rt, :nskew_risk_1, :obj_expr, :ret, :ret_1, :ret_vec, :risk, :risk_vec, :sc, :so, :w, :w_gross_ub, :w_lb, :w_ub, :x_kurt_2
              r ┼ 2-element Vector{RiskMeasure}
                │ NegativeSkewness ⋯
                │ Kurtosis ⋯
  w_min_retcode ┼ OptimisationSuccess
                │   res ┴ Dict{Any, Any}: Dict{Any, Any}()
  w_opt_retcode ┼ 25-element Vector{OptimisationSuccess}
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
  w_max_retcode ┼ OptimisationSuccess
                │   res ┴ Dict{Any, Any}: Dict{Any, Any}()
    noc_retcode ┼ 25-element Vector{OptimisationSuccess}
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
                │ OptimisationSuccess ⋯
             fb ┴ nothing

The result holds 5 × 5 = 25 portfolios, one per pair of bounds.

Its retcode shows whether every inner MeanRisk problem succeeded.

isa(res3.retcode, OptimisationSuccess)
true

5. Visualising the Pareto surface

The stacked areas show the weights at each of the 25 points of the surface.

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

We plot the Pareto surface. The z-axis and the colour both show the ratio of the return, net of the risk-free rate, to the conditional drawdown at risk, CDaR. The CDaR is not one of the two optimised measures. We use it to compare the portfolios on a third risk.

plot_measures(res3.w, pr; x = r1, y = r2,              z = ExpectedReturnRiskRatio(; rk = ConditionalDrawdownatRisk(),                                          rt = ArithmeticReturn(), rf = rf),              c = ExpectedReturnRiskRatio(; rk = ConditionalDrawdownatRisk(),                                          rt = ArithmeticReturn(), rf = rf),              title = "Pareto surface", xlabel = "Square root of negative skewness",              ylabel = "Square root of kurtosis", zlabel = "Return/CDaR")
Example block output

We plot the same surface in 2D, with the ratio as the colour.

plot_measures(res3.w, pr; x = r1, y = r2,              c = ExpectedReturnRiskRatio(; rk = ConditionalDrawdownatRisk(),                                          rt = ArithmeticReturn(), rf = rf),              title = "Pareto front", xlabel = "Square root of negative skewness",              ylabel = "Square root of kurtosis", colorbar_title = "\n\nReturn/CDaR",              right_margin = 8Plots.mm)
Example block output

This page was generated using Literate.jl.