The source files can be found in examples/.

Multiple risk measures

This example shows how to use multiple risk measures.

When to reach for this

Reach for multiple risk measures when no single measure captures everything you care about and you want one optimisation to answer to several at once — for example bounding variance while also controlling a tail measure like CVaR or drawdown. Any MeanRisk optimisation accepts a vector of risk measures, each with its own settings, so the objective and constraints can blend them rather than forcing you to pick one. If you instead want to trade several criteria off against each other across many portfolios, see the Pareto-surface example.

The return side takes several terms too

JuMPOptimiser's ret field mirrors r: one return term or a vector of them, each with its own JuMPReturnsSettings. The one asymmetry is deliberate — return terms are always a weighted sum, so no scalariser applies to them (ADR 0052). See ℓ1 uncertainty sets.

using PortfolioOptimisers, PrettyTables# Format for pretty tables.tsfmt = (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. ReturnsResult data

We will 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])# Compute the returnsrd = prices_to_returns(X)
ReturnsResult
    nx ┼ 20-element Vector{String}
     X ┼ 252×20 Matrix{Float64}
    nf ┼ nothing
     F ┼ nothing
    nb ┼ nothing
     B ┼ nothing
    ts ┼ 252-element Vector{Date}
    iv ┼ nothing
  ivpa ┼ nothing
    nz ┼ nothing
     Z ┴ nothing

2. Preparatory steps

We'll provide a vector of continuous solvers as a failsafe.

using Clarabelslv = [Solver(; name = :clarabel1, solver = Clarabel.Optimizer,              settings = Dict("verbose" => false),              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 = :clarabel5, solver = Clarabel.Optimizer,              settings = Dict("verbose" => false, "max_step_fraction" => 0.8),              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. Multiple risk measures

3.1 Equally weighted sum

Some risk measures can use precomputed prior statistics which take precedence over the ones in PriorResult. We can make use of this to minimise the variance with different covariance matrices simultaneously.

We will also precompute the prior statistics to minimise redundant work. First let's create a vector of Variances onto which we will push the different variances. We'll use 5 variance estimators, and their equally weighted sum.

  1. Denoised covariance using the spectral algorithm.
  2. Gerber 1 covariance.
  3. Smyth Broby 1 covariance.
  4. Mutual Information covariance.
  5. Distance covariance.
  6. Equally weighted sum of all the above covariances.

For the multi risk measure optimisation, we will weigh each risk measure equally. It should give the same result as adding all covariances together, but not the same as averaging the weights of the individual optimisations.

pr = prior(HighOrderPriorEstimator(), rd.X)ces = [PortfolioOptimisersCovariance(;                                     mp = MatrixProcessing(;                                                           dn = Denoise(;                                                                        alg = SpectralDenoise()))),       PortfolioOptimisersCovariance(; ce = GerberCovariance()),       PortfolioOptimisersCovariance(; ce = SmythBrobyCovariance(; alg = SmythBroby1())),       PortfolioOptimisersCovariance(; ce = MutualInfoCovariance()),       PortfolioOptimisersCovariance(; ce = DistanceCovariance())]
5-element Vector{PortfolioOptimisersCovariance}:
 PortfolioOptimisersCovariance
  ce ┼ Covariance
     │    me ┼ SimpleExpectedReturns
     │       │   w ┴ nothing
     │    ce ┼ GeneralCovariance
     │       │   ce ┼ SimpleCovariance: SimpleCovariance(true)
     │       │    w ┴ nothing
     │   alg ┴ FullMoment()
  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)

 PortfolioOptimisersCovariance
  ce ┼ GerberCovariance
     │    ve ┼ SimpleVariance
     │       │          me ┼ SimpleExpectedReturns
     │       │             │   w ┴ nothing
     │       │           w ┼ nothing
     │       │   corrected ┴ Bool: true
     │    me ┼ SimpleExpectedReturns
     │       │   w ┴ nothing
     │   pdm ┼ Posdef
     │       │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
     │       │   kwargs ┴ @NamedTuple{}: NamedTuple()
     │     t ┼ Float64: 0.5
     │   alg ┴ Gerber1()
  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)

 PortfolioOptimisersCovariance
  ce ┼ SmythBrobyCovariance
     │    ve ┼ SimpleVariance
     │       │          me ┼ SimpleExpectedReturns
     │       │             │   w ┴ nothing
     │       │           w ┼ nothing
     │       │   corrected ┴ Bool: true
     │    me ┼ SimpleExpectedReturns
     │       │   w ┴ nothing
     │   pdm ┼ Posdef
     │       │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton
     │       │   kwargs ┴ @NamedTuple{}: NamedTuple()
     │    c1 ┼ Float64: 0.5
     │    c2 ┼ Float64: 0.5
     │    c3 ┼ Int64: 4
     │     n ┼ Int64: 2
     │   alg ┼ SmythBroby1()
     │    ex ┴ Transducers.ThreadedEx{@NamedTuple{}}: Transducers.ThreadedEx()
  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)

 PortfolioOptimisersCovariance
  ce ┼ MutualInfoCovariance
     │          ve ┼ SimpleVariance
     │             │          me ┼ SimpleExpectedReturns
     │             │             │   w ┴ nothing
     │             │           w ┼ nothing
     │             │   corrected ┴ Bool: true
     │        bins ┼ HacineGharbiRavier()
     │   normalise ┴ Bool: true
  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)

 PortfolioOptimisersCovariance
  ce ┼ DistanceCovariance
     │   metric ┼ Distances.Euclidean: Distances.Euclidean(0.0)
     │     args ┼ Tuple{}: ()
     │   kwargs ┼ @NamedTuple{}: NamedTuple()
     │        w ┼ nothing
     │       ex ┴ Transducers.ThreadedEx{@NamedTuple{}}: Transducers.ThreadedEx()
  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)

Let's define a vector of variance risk measure using each of the different covariance matrices.

rs = [Variance(; sigma = cov(ce, rd.X)) for ce in ces]all_sigmas = zeros(length(rd.nx), length(rd.nx))for r in rs    all_sigmas .+= r.sigmaendpush!(rs, Variance(; sigma = all_sigmas))
6-element Vector{Variance{RiskMeasureSettings{Float64, Nothing, Bool}, Matrix{Float64}, Nothing, Nothing, SquaredSOCRiskExpr}}:
 Variance
  settings ┼ RiskMeasureSettings
           │   scale ┼ Float64: 1.0
           │      ub ┼ nothing
           │     rke ┴ Bool: true
     sigma ┼ 20×20 Matrix{Float64}
      chol ┼ nothing
        rc ┼ nothing
       alg ┴ SquaredSOCRiskExpr()

 Variance
  settings ┼ RiskMeasureSettings
           │   scale ┼ Float64: 1.0
           │      ub ┼ nothing
           │     rke ┴ Bool: true
     sigma ┼ 20×20 Matrix{Float64}
      chol ┼ nothing
        rc ┼ nothing
       alg ┴ SquaredSOCRiskExpr()

 Variance
  settings ┼ RiskMeasureSettings
           │   scale ┼ Float64: 1.0
           │      ub ┼ nothing
           │     rke ┴ Bool: true
     sigma ┼ 20×20 Matrix{Float64}
      chol ┼ nothing
        rc ┼ nothing
       alg ┴ SquaredSOCRiskExpr()

 Variance
  settings ┼ RiskMeasureSettings
           │   scale ┼ Float64: 1.0
           │      ub ┼ nothing
           │     rke ┴ Bool: true
     sigma ┼ 20×20 Matrix{Float64}
      chol ┼ nothing
        rc ┼ nothing
       alg ┴ SquaredSOCRiskExpr()

 Variance
  settings ┼ RiskMeasureSettings
           │   scale ┼ Float64: 1.0
           │      ub ┼ nothing
           │     rke ┴ Bool: true
     sigma ┼ 20×20 Matrix{Float64}
      chol ┼ nothing
        rc ┼ nothing
       alg ┴ SquaredSOCRiskExpr()

 Variance
  settings ┼ RiskMeasureSettings
           │   scale ┼ Float64: 1.0
           │      ub ┼ nothing
           │     rke ┴ Bool: true
     sigma ┼ 20×20 Matrix{Float64}
      chol ┼ nothing
        rc ┼ nothing
       alg ┴ SquaredSOCRiskExpr()

We'll minimise the variance for each individual risk measure and then we'll minimise the equally weighted sum of all risk measures.

results = [optimise(MeanRisk(; r = r, opt = JuMPOptimiser(; pe = pr, slv = slv)))           for r in rs]mean_w = zeros(length(results[1].w))for res in results[1:5]    mean_w .+= res.wendmean_w ./= 5res = optimise(MeanRisk(; r = rs, opt = JuMPOptimiser(; pe = pr, slv = slv)))pretty_table(DataFrame(:assets => rd.nx, :dn => results[1].w, :gerber1 => results[2].w,                       :smyth_broby1 => results[3].w, :mutual_info => results[4].w,                       :distance => results[5].w, :mean_w => mean_w,                       :sum_covs => results[6].w, :multi_risk => res.w);             formatters = [resfmt])
┌────────┬──────────┬──────────┬──────────────┬─────────────┬──────────┬──────────┬──────────┬────────────┐
│ assets        dn   gerber1  smyth_broby1  mutual_info  distance    mean_w  sum_covs  multi_risk │
│ String   Float64   Float64       Float64      Float64   Float64   Float64   Float64     Float64 │
├────────┼──────────┼──────────┼──────────────┼─────────────┼──────────┼──────────┼──────────┼────────────┤
│   AAPL │    0.0 % │  0.001 % │        0.0 % │     1.263 % │    0.0 % │  0.253 % │    0.0 % │      0.0 % │
│    AMD │    0.0 % │    0.0 % │        0.0 % │       0.0 % │    0.0 % │    0.0 % │    0.0 % │      0.0 % │
│    BAC │    0.0 % │  0.517 % │      0.001 % │     2.166 % │  2.279 % │  0.993 % │  1.678 % │     1.68 % │
│    BBY │    0.0 % │    0.0 % │        0.0 % │      0.74 % │    0.0 % │  0.148 % │    0.0 % │      0.0 % │
│    CVX │ 18.188 % │  9.564 % │     13.135 % │     4.007 % │  9.961 % │ 10.971 % │ 10.335 % │   10.334 % │
│     GE │    0.0 % │  2.421 % │      0.222 % │     2.702 % │  4.347 % │  1.939 % │  3.856 % │    3.856 % │
│     HD │    0.0 % │  0.022 % │        0.0 % │     2.713 % │  3.792 % │  1.305 % │   3.39 % │     3.39 % │
│    JNJ │ 81.812 % │  24.63 % │     30.058 % │    17.458 % │  17.28 % │ 34.247 % │  18.71 % │    18.71 % │
│    JPM │    0.0 % │  1.074 % │      0.262 % │     2.859 % │  1.284 % │  1.095 % │  1.727 % │    1.726 % │
│     KO │    0.0 % │ 11.992 % │     11.914 % │     9.807 % │  9.243 % │  8.591 % │  9.531 % │    9.531 % │
│    LLY │    0.0 % │  0.001 % │        0.0 % │     4.874 % │  0.241 % │  1.023 % │    0.0 % │      0.0 % │
│    MRK │    0.0 % │ 16.944 % │     14.527 % │    14.056 % │  18.66 % │ 12.837 % │ 18.642 % │   18.642 % │
│   MSFT │    0.0 % │  0.002 % │        0.0 % │     0.805 % │    0.0 % │  0.161 % │    0.0 % │      0.0 % │
│    PEP │    0.0 % │ 15.273 % │     18.248 % │     8.543 % │  8.089 % │ 10.031 % │  9.096 % │    9.095 % │
│    PFE │    0.0 % │  0.508 % │        0.0 % │     4.166 % │    0.0 % │  0.935 % │    0.0 % │      0.0 % │
│     PG │    0.0 % │  6.332 % │       5.96 % │     6.709 % │  3.699 % │   4.54 % │  4.031 % │    4.032 % │
│    RRC │    0.0 % │    0.0 % │        0.0 % │     0.434 % │    0.0 % │  0.087 % │    0.0 % │      0.0 % │
│    UNH │    0.0 % │  3.968 % │      2.182 % │      6.69 % │  1.612 % │   2.89 % │  1.343 % │    1.344 % │
│    WMT │    0.0 % │  4.945 % │      3.489 % │     6.549 % │ 12.797 % │  5.556 % │ 11.414 % │   11.414 % │
│    XOM │    0.0 % │  1.809 % │        0.0 % │      3.46 % │  6.717 % │  2.397 % │  6.247 % │    6.247 % │
└────────┴──────────┴──────────┴──────────────┴─────────────┴──────────┴──────────┴──────────┴────────────┘

For extra credit we can do the same but maximising the risk-adjusted return ratio.

results = [optimise(MeanRisk(; r = r, obj = MaximumRatio(),                             opt = JuMPOptimiser(; pe = pr, slv = slv))) for r in rs]mean_w = zeros(length(results[1].w))for res in results[1:5]    mean_w .+= res.wendmean_w ./= 5res = optimise(MeanRisk(; r = rs, obj = MaximumRatio(),                        opt = JuMPOptimiser(; pe = pr, slv = slv)))pretty_table(DataFrame(:assets => rd.nx, :dn => results[1].w, :gerber1 => results[2].w,                       :smyth_broby1 => results[3].w, :mutual_info => results[4].w,                       :distance => results[5].w, :mean_w => mean_w,                       :sum_covs => results[6].w, :multi_risk => res.w);             formatters = [resfmt])
┌────────┬──────────┬──────────┬──────────────┬─────────────┬──────────┬──────────┬──────────┬────────────┐
│ assets        dn   gerber1  smyth_broby1  mutual_info  distance    mean_w  sum_covs  multi_risk │
│ String   Float64   Float64       Float64      Float64   Float64   Float64   Float64     Float64 │
├────────┼──────────┼──────────┼──────────────┼─────────────┼──────────┼──────────┼──────────┼────────────┤
│   AAPL │    0.0 % │    0.0 % │        0.0 % │       0.0 % │    0.0 % │    0.0 % │    0.0 % │      0.0 % │
│    AMD │    0.0 % │    0.0 % │        0.0 % │       0.0 % │    0.0 % │    0.0 % │    0.0 % │      0.0 % │
│    BAC │    0.0 % │    0.0 % │        0.0 % │       0.0 % │    0.0 % │    0.0 % │    0.0 % │      0.0 % │
│    BBY │    0.0 % │    0.0 % │        0.0 % │       0.0 % │    0.0 % │    0.0 % │    0.0 % │      0.0 % │
│    CVX │    0.0 % │    6.2 % │      0.001 % │     9.888 % │    0.0 % │  3.218 % │    0.0 % │      0.0 % │
│     GE │    0.0 % │    0.0 % │        0.0 % │       0.0 % │    0.0 % │    0.0 % │    0.0 % │      0.0 % │
│     HD │    0.0 % │    0.0 % │        0.0 % │       0.0 % │    0.0 % │    0.0 % │    0.0 % │      0.0 % │
│    JNJ │    0.0 % │    0.0 % │        0.0 % │       0.0 % │    0.0 % │    0.0 % │    0.0 % │      0.0 % │
│    JPM │    0.0 % │    0.0 % │        0.0 % │       0.0 % │    0.0 % │    0.0 % │    0.0 % │      0.0 % │
│     KO │    0.0 % │  0.002 % │        0.0 % │     5.688 % │    0.0 % │  1.138 % │    0.0 % │      0.0 % │
│    LLY │    0.0 % │  7.503 % │      2.465 % │    14.783 % │  1.936 % │  5.337 % │  1.817 % │    1.813 % │
│    MRK │ 68.452 % │ 59.478 % │      65.42 % │    46.763 % │ 50.398 % │ 58.102 % │ 52.198 % │   52.204 % │
│   MSFT │    0.0 % │    0.0 % │        0.0 % │       0.0 % │    0.0 % │    0.0 % │    0.0 % │      0.0 % │
│    PEP │    0.0 % │  0.001 % │        0.0 % │     0.002 % │    0.0 % │  0.001 % │    0.0 % │      0.0 % │
│    PFE │    0.0 % │    0.0 % │        0.0 % │       0.0 % │    0.0 % │    0.0 % │    0.0 % │      0.0 % │
│     PG │    0.0 % │    0.0 % │        0.0 % │       0.0 % │    0.0 % │    0.0 % │    0.0 % │      0.0 % │
│    RRC │    0.0 % │    0.0 % │        0.0 % │     2.215 % │    0.0 % │  0.443 % │    0.0 % │      0.0 % │
│    UNH │    0.0 % │    0.0 % │        0.0 % │       0.0 % │    0.0 % │    0.0 % │    0.0 % │      0.0 % │
│    WMT │    0.0 % │    0.0 % │        0.0 % │       0.0 % │    0.0 % │    0.0 % │    0.0 % │      0.0 % │
│    XOM │ 31.547 % │ 26.814 % │     32.113 % │    20.659 % │ 47.666 % │  31.76 % │ 45.985 % │   45.983 % │
└────────┴──────────┴──────────┴──────────────┴─────────────┴──────────┴──────────┴──────────┴────────────┘

3.2 Different weights and scalarisers

All optimisations accept multiple risk measures in the same way. We can also provide different weights for each measure and four different scalarisers, SumScalariser, MaxScalariser, LogSumExpScalariser which work for all optimisation estimators, and MinScalariser which only works for hierarchical ones.

That last restriction is the model's, not the number's. Minimising a minimum has no convex JuMP form, so a JuMP optimiser refuses MinScalariser — but at the value level, where the measures have already been evaluated and we are only combining numbers, all four scalarisers are admitted. Section 4 below reports with one of them.

For clustering optimisations, the scalarisers apply to each sub-optimisation, so what may be the choice of risk to "minimise" for one cluster may not be the minimal risk for others, or the overall portfolio. This inconsistency is unavoidable but should not be a problem in practice as the point of hierarchical optimisations is not to provide the absolute minimum risk, but a good trade-off between risk and diversification.

It is also possible to mix any and all compatible risk measures. We will demonstrate this by mixing the variance with the negative skewness.

In this example we have tuned the weight of the negative skewness to demonstrate how clusters may end up with different risk measures due to the choice of scalariser.

We will use the heirarchical equal risk contribution optimisation, precomputing the clustering results using the direct bubble hierarchy tree algorithm.

The HierarchicalEqualRiskContribution optimisation estimator accepts inner and outer risk measures and inner and outer scalarisers.

clr = clusterise(ClustersEstimator(; alg = DBHT()), pr.X)
Clusters
  res ┼ Clustering.Hclust{Float64}([-1 -13; -7 -4; … ; 12 17; 10 18], [0.1, 0.1111111111111111, 0.125, 0.14285714285714285, 0.16666666666666666, 0.2, 0.25, 0.3333333333333333, 0.5, 1.0, 0.125, 0.14285714285714285, 0.16666666666666666, 0.2, 0.25, 0.3333333333333333, 0.5, 1.0, 2.0], [5, 20, 17, 3, 9, 6, 2, 1, 13, 7, 4, 19, 14, 10, 16, 18, 11, 8, 12, 15], :DBHT)
    S ┼ 20×20 Matrix{Float64}
    D ┼ 20×20 Matrix{Float64}
    P ┼ nothing
    k ┴ Int64: 4

4. Visualising cluster structure and results

Before optimising, we can visualise the asset clustering structure derived from the correlation matrix.

Hierarchical clustering dendrogram.

using StatsPlots, GraphRecipes

Reordered correlation heatmap with cluster boundary boxes.

plot_dendrogram(clr, rd.nx)plot_clusters(clr, rd.nx)r = [Variance(), NegativeSkewness(; settings = RiskMeasureSettings(; scale = 0.1))]results = [optimise(HierarchicalEqualRiskContribution(; ri = r[1],# inner (intra-cluster) risk measure                                                      ro = r[1],  # outer (inter-cluster) risk measure                                                      opt = HierarchicalOptimiser(; pe = pr,                                                                                  cle = clr))),           optimise(HierarchicalEqualRiskContribution(; ri = r[2], ro = r[2],                                                      opt = HierarchicalOptimiser(; pe = pr,                                                                                  cle = clr))),           optimise(HierarchicalEqualRiskContribution(; ri = r, ro = r,#                                                      scai = SumScalariser(),# inner (intra-cluster)                                                      scao = SumScalariser(),# outer (inter-cluster)                                                      opt = HierarchicalOptimiser(; pe = pr,                                                                                  cle = clr))),           optimise(HierarchicalEqualRiskContribution(; ri = r, ro = r,                                                      scai = MaxScalariser(),                                                      scao = MaxScalariser(),                                                      opt = HierarchicalOptimiser(; pe = pr,                                                                                  cle = clr))),           optimise(HierarchicalEqualRiskContribution(; ri = r, ro = r,                                                      scai = MinScalariser(),                                                      scao = MinScalariser(),                                                      opt = HierarchicalOptimiser(; pe = pr,                                                                                  cle = clr))),           optimise(HierarchicalEqualRiskContribution(; ri = r, ro = r,                                                      scai = LogSumExpScalariser(),                                                      scao = LogSumExpScalariser(),                                                      opt = HierarchicalOptimiser(; pe = pr,                                                                                  cle = clr)))]pretty_table(DataFrame(:assets => rd.nx, :variance => results[1].w,                       :neg_skew => results[2].w, :sum_sca => results[3].w,                       :max_sca => results[4].w, :min_sca => results[5].w,                       :log_sum_exp => results[6].w); formatters = [resfmt])
┌────────┬──────────┬──────────┬──────────┬─────────┬──────────┬─────────────┐
│ assets  variance  neg_skew   sum_sca  max_sca   min_sca  log_sum_exp │
│ String   Float64   Float64   Float64  Float64   Float64      Float64 │
├────────┼──────────┼──────────┼──────────┼─────────┼──────────┼─────────────┤
│   AAPL │  1.847 % │  4.367 % │  2.949 % │ 3.286 % │  2.571 % │     4.729 % │
│    AMD │  0.627 % │    2.3 % │  1.056 % │ 1.115 % │  1.354 % │     4.442 % │
│    BAC │  2.221 % │  6.575 % │  3.636 % │  3.95 % │  3.871 % │     4.833 % │
│    BBY │  1.138 % │  2.166 % │  1.781 % │ 2.024 % │  1.275 % │     4.552 % │
│    CVX │  4.525 % │   4.41 % │  5.338 % │ 6.436 % │  3.246 % │     5.976 % │
│     GE │  1.924 % │  2.356 % │  2.923 % │ 3.423 % │  1.387 % │     4.727 % │
│     HD │  2.386 % │  2.995 % │  3.629 % │ 4.244 % │  1.763 % │     4.837 % │
│    JNJ │ 10.746 % │  7.487 % │ 10.587 % │ 7.821 % │ 10.413 % │     5.225 % │
│    JPM │  2.623 % │  5.682 % │  4.153 % │ 4.666 % │  3.345 % │     4.916 % │
│     KO │  13.86 % │  7.188 % │  9.593 % │ 7.509 % │ 13.431 % │       5.7 % │
│    LLY │  4.388 % │  7.217 % │  4.727 % │ 7.539 % │  4.253 % │      4.69 % │
│    MRK │  8.205 % │   7.77 % │  8.283 % │ 8.117 % │  7.951 % │     5.009 % │
│   MSFT │  1.886 % │    4.4 % │  3.008 % │ 3.356 % │   2.59 % │     4.738 % │
│    PEP │ 14.175 % │  6.616 % │  9.727 % │ 6.911 % │ 13.736 % │     5.725 % │
│    PFE │  4.473 % │   5.51 % │  4.639 % │ 5.756 % │  4.334 % │     4.684 % │
│     PG │  6.756 % │  5.271 % │  6.711 % │ 5.506 % │  6.547 % │     4.869 % │
│    RRC │  1.238 % │  2.674 % │  1.595 % │ 1.761 % │  1.969 % │     4.756 % │
│    UNH │  5.489 % │   4.62 % │  5.485 % │ 4.826 % │  5.319 % │      4.76 % │
│    WMT │  7.513 % │   5.83 % │  5.422 % │  6.09 % │  7.281 % │     5.057 % │
│    XOM │   3.98 % │  4.566 % │  4.759 % │ 5.662 % │  3.362 % │     5.773 % │
└────────┴──────────┴──────────┴──────────┴─────────┴──────────┴─────────────┘

Compositions across scalarisers with weighted NegativeSkewness (scale = 0.1).

plot_stacked_bar_composition(results, rd)
Example block output

When the weights are different enough that one risk measure domintes over the other in all contexts, then the results of the max and min scalarisers will be as expected, i.e. they will be as if only one risk measure was used.

r = [Variance(), NegativeSkewness()]results = [optimise(HierarchicalEqualRiskContribution(; ri = r[1],# inner (intra-cluster) risk measure                                                      ro = r[1],  # outer (inter-cluster) risk measure                                                      opt = HierarchicalOptimiser(; pe = pr,                                                                                  cle = clr))),           optimise(HierarchicalEqualRiskContribution(; ri = r[2], ro = r[2],                                                      opt = HierarchicalOptimiser(; pe = pr,                                                                                  cle = clr))),           optimise(HierarchicalEqualRiskContribution(; ri = r, ro = r,#                                                      scai = SumScalariser(),# inner (intra-cluster)                                                      scao = SumScalariser(),# outer (inter-cluster)                                                      opt = HierarchicalOptimiser(; pe = pr,                                                                                  cle = clr))),           optimise(HierarchicalEqualRiskContribution(; ri = r, ro = r,                                                      scai = MaxScalariser(),                                                      scao = MaxScalariser(),                                                      opt = HierarchicalOptimiser(; pe = pr,                                                                                  cle = clr))),           optimise(HierarchicalEqualRiskContribution(; ri = r, ro = r,                                                      scai = MinScalariser(),                                                      scao = MinScalariser(),                                                      opt = HierarchicalOptimiser(; pe = pr,                                                                                  cle = clr))),           optimise(HierarchicalEqualRiskContribution(; ri = r, ro = r,                                                      scai = LogSumExpScalariser(),                                                      scao = LogSumExpScalariser(),                                                      opt = HierarchicalOptimiser(; pe = pr,                                                                                  cle = clr)))]pretty_table(DataFrame(:assets => rd.nx, :variance => results[1].w,                       :neg_skew => results[2].w, :sum_sca => results[3].w,                       :max_sca => results[4].w, :min_sca => results[5].w,                       :log_sum_exp => results[6].w); formatters = [resfmt])
┌────────┬──────────┬──────────┬─────────┬─────────┬──────────┬─────────────┐
│ assets  variance  neg_skew  sum_sca  max_sca   min_sca  log_sum_exp │
│ String   Float64   Float64  Float64  Float64   Float64      Float64 │
├────────┼──────────┼──────────┼─────────┼─────────┼──────────┼─────────────┤
│   AAPL │  1.847 % │  4.367 % │ 3.946 % │ 4.367 % │  1.847 % │     4.631 % │
│    AMD │  0.627 % │    2.3 % │  1.73 % │   2.3 % │  0.627 % │      4.21 % │
│    BAC │  2.221 % │  6.575 % │ 5.377 % │ 6.575 % │  2.221 % │     4.905 % │
│    BBY │  1.138 % │  2.166 % │ 2.181 % │ 2.166 % │  1.138 % │     4.295 % │
│    CVX │  4.525 % │   4.41 % │ 4.959 % │  4.41 % │  4.525 % │     6.229 % │
│     GE │  1.924 % │  2.356 % │ 3.063 % │ 2.356 % │  1.924 % │     4.465 % │
│     HD │  2.386 % │  2.995 % │ 3.833 % │ 2.995 % │  2.386 % │     4.612 % │
│    JNJ │ 10.746 % │  7.487 % │ 8.942 % │ 7.487 % │ 10.746 % │      5.19 % │
│    JPM │  2.623 % │  5.682 % │ 5.356 % │ 5.682 % │  2.623 % │     4.898 % │
│     KO │  13.86 % │  7.188 % │ 7.697 % │ 7.188 % │  13.86 % │     5.991 % │
│    LLY │  4.388 % │  7.217 % │  5.76 % │ 7.217 % │  4.388 % │     4.727 % │
│    MRK │  8.205 % │   7.77 % │ 7.869 % │  7.77 % │  8.205 % │      5.03 % │
│   MSFT │  1.886 % │    4.4 % │ 4.002 % │   4.4 % │  1.886 % │     4.641 % │
│    PEP │ 14.175 % │  6.616 % │ 7.491 % │ 6.616 % │ 14.175 % │     5.937 % │
│    PFE │  4.473 % │   5.51 % │ 4.935 % │  5.51 % │  4.473 % │     4.603 % │
│     PG │  6.756 % │  5.271 % │ 5.908 % │ 5.271 % │  6.756 % │     4.745 % │
│    RRC │  1.238 % │  2.674 % │ 2.098 % │ 2.674 % │  1.238 % │     4.874 % │
│    UNH │  5.489 % │   4.62 % │ 4.971 % │  4.62 % │  5.489 % │     4.608 % │
│    WMT │  7.513 % │   5.83 % │ 5.175 % │  5.83 % │  7.513 % │     5.303 % │
│    XOM │   3.98 % │  4.566 % │ 4.709 % │ 4.566 % │   3.98 % │     6.106 % │
└────────┴──────────┴──────────┴─────────┴─────────┴──────────┴─────────────┘

Compositions across scalarisers with equal-weight NegativeSkewness. Notice Max collapses to NegativeSkewness-only and Min collapses to Variance-only.

plot_stacked_bar_composition(results, rd)
Example block output

Note how the max scalariser produced the same weights as the negative skewness and the min scalariser produced the same weights as the variance. This is because in all cases, the same the value of the negative skewness was greater than that of the variance. A similar behaviour can be observed with other clustering optimisers. NearOptimalCentering can also have unintuitive behaviour when computing the risk bounds with an effective frontier MaxScalariser and MinScalariser due to the fact that each point in the efficient frontier can have a different risk measure dominating the others.

4. Reporting a vector of risk measures

Everything above optimises with several measures. Reporting works the same way: expected_risk takes the vector and collapses it to one number, never a per-element breakdown.

We do not need to re-state what we optimised. The result carries it: res.r is the vector of risk measures the optimisation ran under, stored resolved, and res.sca is the scalariser. Reading both off the result is the route that guarantees the reported figure is the optimised one.

res below is the maximum-ratio optimisation over all six variance measures from section 3.1.

rk_opt = expected_risk(res.r, res.w, res.pr; sca = res.sca)
0.013323346727056613

Naming the measures by hand gives the same number, because we happen to name the same vector and the same scalariser. If either differed, so would the figure — and fees and rf carry the same responsibility.

rk_hand = expected_risk(rs, res.w, res.pr; sca = SumScalariser())pretty_table(DataFrame(; :route => ["from the result", "named by hand"],                       :risk => [rk_opt, rk_hand]); title = "Both routes, one figure")
    Both routes, one figure
┌─────────────────┬───────────┐
│           route       risk │
│          String    Float64 │
├─────────────────┼───────────┤
│ from the result │ 0.0133233 │
│   named by hand │ 0.0133233 │
└─────────────────┴───────────┘

The scalariser is a keyword at this level, so we can report the same portfolio under each of them. All four are admitted here, MinScalariser included — the hierarchical-only restriction is the JuMP model's, and no model is being built when we are combining numbers we already have.

scas = [SumScalariser(), MaxScalariser(), MinScalariser(), LogSumExpScalariser()]pretty_table(DataFrame(; :scalariser => ["Sum", "Max", "Min", "LogSumExp"],                       :risk =>                           [expected_risk(res.r, res.w, res.pr; sca = s) for s in scas]);             title = "One portfolio, six variance measures, four scalarisers")
One portfolio, six variance measures, four scalarisers
┌────────────┬─────────────┐
│ scalariser         risk │
│     String      Float64 │
├────────────┼─────────────┤
│        Sum │   0.0133233 │
│        Max │  0.00666167 │
│        Min │ 0.000152821 │
│  LogSumExp │     1.79398 │
└────────────┴─────────────┘

Among the three that stay in the units of the measure, the ordering is forced: Min ≤ Max ≤ Sum, because each element is weighted by its own settings.scale and then Sum adds all six positive contributions while Max and Min keep a single one.

LogSumExpScalariser is the odd one out, and by a wide margin — here it reports 1.79 against Sum's 0.0133. It is a smooth maximum computed in log space, log(Σᵢ exp(wᵢ rᵢ)), so it is not a weighted average of the risks and does not stay in their units. When the risks are small its value is dominated by log(N), the count of measures, rather than by the risks themselves. Read it as a soft-max dial, never as a risk figure comparable with the other three.

A hierarchical result carries its measures the same way, one pair per level. HierarchicalEqualRiskContribution holds an intra-cluster measure and scalariser and an inter-cluster pair, so its result carries ri, scai, ro and scao, all resolved.

res_herc = results[3]rk_herc = expected_risk(res_herc.ro, res_herc.w, res_herc.pr; sca = res_herc.scao)
0.002262226567821218

This page was generated using Literate.jl.