The source files can be found in examples/.

Multiple risk measures

This page puts several risk measures into one optimisation. It combines them in the objective of MeanRisk and of a hierarchical optimiser, and it reports the risk of a portfolio under a vector of risk measures.

When to reach for this

Reach for several risk measures when one measure does not cover what you care about, and you want one optimisation to take all of them into account. For example, you can bound the variance and also limit a tail measure such as CVaR or a drawdown. A MeanRisk optimisation takes a vector of risk measures, each with its own settings, so the objective and the constraints can combine them. If you want to trade several criteria against each other over many portfolios instead, see the Pareto surface example.

The return side takes several terms too

The ret field of JuMPOptimiser takes one return term or a vector of them, as the r field of MeanRisk does, each with its own JuMPReturnsSettings. The optimiser combines several risk measures with a scalariser, the rule that turns their values into one number, and you choose it. It always adds the return terms as a weighted sum, with no scalariser. See ℓ1 uncertainty sets.

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 load the same prices as the previous examples.

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

We pass a vector of four solvers. When one fails to converge, the optimiser tries the next.

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 hold their own prior statistics, and the optimisation uses those instead of the ones in the prior result. We use this to minimise the variance under several covariance matrices at once.

We compute the prior once, and define five covariance estimators.

  1. The default covariance, denoised with the spectral algorithm.
  2. The Gerber covariance, whose default algorithm is Gerber1.
  3. The Smyth-Broby covariance with the SmythBroby1 algorithm.
  4. The mutual information covariance.
  5. The distance covariance.
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{__T_ce, __T_mp, Nothing} where {__T_ce, __T_mp}}:
 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)

 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)

We build one Variance from each covariance matrix, and a sixth from the sum of the five 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, EigenFallbackSquareRoot, SquareRootRatio, Nothing}}:
 Variance
   settings ┼ RiskMeasureSettings
            │   scale ┼ Float64: 1.0
            │      ub ┼ nothing
            │     rke ┴ Bool: true
      sigma ┼ 20×20 Matrix{Float64}
       chol ┼ nothing
         rc ┼ nothing
        alg ┼ SquaredSOCRiskExpr()
   mtx_sqrt ┼ EigenFallbackSquareRoot()
  ratio_alg ┼ SquareRootRatio()
     corner ┴ nothing

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

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

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

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

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

We minimise each of the six variances alone, and average the weights of the first five portfolios. Then we minimise all six variances in one optimisation, whose default scalariser adds them.

That sum is twice the variance under the summed covariance, so the multi_risk column shows the same portfolio as the sum_covs column, up to the tolerance of the solver. The mean_w column averages the weights of the five single portfolios, and that is a different portfolio.

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 % │
└────────┴──────────┴──────────┴──────────────┴─────────────┴──────────┴──────────┴──────────┴────────────┘

We repeat the comparison with the objective that maximises 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

MeanRisk, NearOptimalCentering, RiskBudgeting, FactorRiskContribution, HierarchicalRiskParity and HierarchicalEqualRiskContribution each take a vector of risk measures. You can give each measure its own weight, and choose one of four scalarisers to combine them. SumScalariser, MaxScalariser and LogSumExpScalariser work with all six, and MinScalariser works only with the two hierarchical optimisers.

The restriction on MinScalariser comes from the optimisation model. Minimising a minimum has no convex form, so a JuMP optimiser does not accept MinScalariser. When you only combine risk values you already computed, no model is built, and all four scalarisers work. Section 5 reports one portfolio's risk under each of them.

In a hierarchical optimiser the scalariser applies to each sub-optimisation separately. Under MaxScalariser, for example, the larger measure in one cluster need not be the larger measure in another cluster, or in the whole portfolio. This cannot be avoided. A hierarchical optimiser aims at a trade-off between risk and diversification, not at the lowest possible risk.

You can mix any risk measures that the optimiser accepts. We mix the variance with the negative skewness, and we scale the negative skewness down so that the larger of the two measures differs from cluster to cluster.

We use HierarchicalEqualRiskContribution, which takes a risk measure and a scalariser inside each cluster, ri and scai, and a pair between the clusters, ro and scao. We compute the clusters once in advance, with the direct bubble hierarchical tree algorithm, DBHT.

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], [17, 20, 5, 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. Hierarchical optimisation with two risk measures

Before we optimise, we plot the clusters that the estimator found from the correlation matrix.

using StatsPlots, GraphRecipes

The dendrogram shows the hierarchical clustering of the assets.

plot_dendrogram(clr, rd.nx)
Example block output

The heatmap shows the correlation matrix in the order of the clusters, with a box around each cluster.

plot_clusters(clr, rd.nx)
Example block output

We run six optimisations: the variance alone, the negative skewness alone, and both measures under each of the four scalarisers. The negative skewness has a scale of 0.1.

r = [Variance(), NegativeSkewness(; settings = RiskMeasureSettings(; scale = 0.1))]results = [optimise(HierarchicalEqualRiskContribution(; ri = r[1], ro = r[1],                                                      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(),                                                      scao = SumScalariser(),                                                      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 % │      3.13 % │
│    AMD │  0.627 % │    2.3 % │  1.056 % │ 1.115 % │  1.354 % │      2.94 % │
│    BAC │  2.221 % │  6.575 % │  3.636 % │  3.95 % │  3.871 % │     3.199 % │
│    BBY │  1.138 % │  2.166 % │  1.781 % │ 2.024 % │  1.275 % │     3.012 % │
│    CVX │  4.525 % │   4.41 % │  5.338 % │ 6.436 % │  3.246 % │     9.049 % │
│     GE │  1.924 % │  2.356 % │  2.923 % │ 3.423 % │  1.387 % │     3.129 % │
│     HD │  2.386 % │  2.995 % │  3.629 % │ 4.244 % │  1.763 % │     3.202 % │
│    JNJ │ 10.746 % │  7.487 % │ 10.587 % │ 7.821 % │ 10.413 % │     4.468 % │
│    JPM │  2.623 % │  5.682 % │  4.153 % │ 4.666 % │  3.345 % │     3.254 % │
│     KO │  13.86 % │  7.188 % │  9.593 % │ 7.509 % │ 13.431 % │     8.647 % │
│    LLY │  4.388 % │  7.217 % │  4.727 % │ 7.539 % │  4.253 % │     4.011 % │
│    MRK │  8.205 % │   7.77 % │  8.283 % │ 8.117 % │  7.951 % │     4.284 % │
│   MSFT │  1.886 % │    4.4 % │  3.008 % │ 3.356 % │   2.59 % │     3.136 % │
│    PEP │ 14.175 % │  6.616 % │  9.727 % │ 6.911 % │ 13.736 % │     8.684 % │
│    PFE │  4.473 % │   5.51 % │  4.639 % │ 5.756 % │  4.334 % │     4.006 % │
│     PG │  6.756 % │  5.271 % │  6.711 % │ 5.506 % │  6.547 % │     4.165 % │
│    RRC │  1.238 % │  2.674 % │  1.595 % │ 1.761 % │  1.969 % │     7.202 % │
│    UNH │  5.489 % │   4.62 % │  5.485 % │ 4.826 % │  5.319 % │     4.071 % │
│    WMT │  7.513 % │   5.83 % │  5.422 % │  6.09 % │  7.281 % │      7.67 % │
│    XOM │   3.98 % │  4.566 % │  4.759 % │ 5.662 % │  3.362 % │     8.741 % │
└────────┴──────────┴──────────┴──────────┴─────────┴──────────┴─────────────┘

The stacked bars show the six portfolios, with the negative skewness at a scale of 0.1.

plot_stacked_bar_composition(results, rd)
Example block output

We run the same six optimisations with both measures at a scale of 1. If one measure is larger than the other in every cluster, MaxScalariser and MinScalariser each give the portfolio of a single measure.

r = [Variance(), NegativeSkewness()]results = [optimise(HierarchicalEqualRiskContribution(; ri = r[1], ro = r[1],                                                      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(),                                                      scao = SumScalariser(),                                                      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 % │     3.161 % │
│    AMD │  0.627 % │    2.3 % │  1.73 % │   2.3 % │  0.627 % │     2.874 % │
│    BAC │  2.221 % │  6.575 % │ 5.377 % │ 6.575 % │  2.221 % │     3.348 % │
│    BBY │  1.138 % │  2.166 % │ 2.181 % │ 2.166 % │  1.138 % │     2.932 % │
│    CVX │  4.525 % │   4.41 % │ 4.959 % │  4.41 % │  4.525 % │     9.033 % │
│     GE │  1.924 % │  2.356 % │ 3.063 % │ 2.356 % │  1.924 % │     3.048 % │
│     HD │  2.386 % │  2.995 % │ 3.833 % │ 2.995 % │  2.386 % │     3.148 % │
│    JNJ │ 10.746 % │  7.487 % │ 8.942 % │ 7.487 % │ 10.746 % │     4.495 % │
│    JPM │  2.623 % │  5.682 % │ 5.356 % │ 5.682 % │  2.623 % │     3.343 % │
│     KO │  13.86 % │  7.188 % │ 7.697 % │ 7.188 % │  13.86 % │     8.691 % │
│    LLY │  4.388 % │  7.217 % │  5.76 % │ 7.217 % │  4.388 % │     4.093 % │
│    MRK │  8.205 % │   7.77 % │ 7.869 % │  7.77 % │  8.205 % │     4.356 % │
│   MSFT │  1.886 % │    4.4 % │ 4.002 % │   4.4 % │  1.886 % │     3.168 % │
│    PEP │ 14.175 % │  6.616 % │ 7.491 % │ 6.616 % │ 14.175 % │     8.612 % │
│    PFE │  4.473 % │   5.51 % │ 4.935 % │  5.51 % │  4.473 % │     3.986 % │
│     PG │  6.756 % │  5.271 % │ 5.908 % │ 5.271 % │  6.756 % │     4.109 % │
│    RRC │  1.238 % │  2.674 % │ 2.098 % │ 2.674 % │  1.238 % │     7.068 % │
│    UNH │  5.489 % │   4.62 % │ 4.971 % │  4.62 % │  5.489 % │      3.99 % │
│    WMT │  7.513 % │   5.83 % │ 5.175 % │  5.83 % │  7.513 % │     7.693 % │
│    XOM │   3.98 % │  4.566 % │ 4.709 % │ 4.566 % │   3.98 % │     8.854 % │
└────────┴──────────┴──────────┴─────────┴─────────┴──────────┴─────────────┘

The stacked bars show the six portfolios with both measures at a scale of 1.

plot_stacked_bar_composition(results, rd)
Example block output

The MaxScalariser portfolio has the same weights as the negative skewness portfolio, and the MinScalariser portfolio the same weights as the variance portfolio. In every cluster the negative skewness is larger than the variance, so the maximum always picks the negative skewness and the minimum always picks the variance. HierarchicalRiskParity applies its scalariser at each split of the tree, so the same effect can happen there. NearOptimalCentering computes the risk bounds of its frontier with the scalariser, and MaxScalariser takes the maximum at each end of the frontier separately. A different measure can then set each bound.

5. Reporting a vector of risk measures

expected_risk also takes a vector of risk measures. It combines them into one number with the scalariser, and it does not return one number per measure.

res.r is the vector of fitted risk measures of the optimisation, and res.sca is its scalariser. If you pass both from the result, the reported risk is the risk that the optimisation measured.

Here res 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.013323346719772058

We also name the measures and the scalariser by hand, and print both numbers. We name the same vector and the same scalariser, so the two numbers are equal. A different vector, scalariser or fees gives a different number.

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 = "Expected risk of the maximum-ratio portfolio")
Expected risk of the maximum-ratio portfolio
┌─────────────────┬───────────┐
│           route │      risk │
│          String │   Float64 │
├─────────────────┼───────────┤
│ from the result │ 0.0133233 │
│   named by hand │ 0.0133233 │
└─────────────────┴───────────┘

The scalariser is a keyword of expected_risk, so we report the same portfolio under each of the four.

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 = "Risk of the same portfolio under each scalariser")
Risk of the same portfolio under each scalariser
┌────────────┬─────────────┐
│ scalariser │        risk │
│     String │     Float64 │
├────────────┼─────────────┤
│        Sum │   0.0133233 │
│        Max │  0.00666167 │
│        Min │ 0.000152821 │
│  LogSumExp │     1.79398 │
└────────────┴─────────────┘

Sum, Max and Min stay in the units of the measure, and they always satisfy Min ≤ Max ≤ Sum. Each scalariser first multiplies each risk by its own settings.scale. Sum then adds all six positive values, and Max and Min each keep one of them.

LogSumExpScalariser gives a much larger number than the other three. It is a smooth maximum computed in log space, log(Σᵢ exp(γ rᵢ)) / γ with the default γ = 1, so it is not a weighted average of the risks and it is not in their units. When the risks are small, log(N), where N is the number of measures, makes up most of its value. Use it as a smooth form of the maximum, and do not compare its value with the other three.

A hierarchical result also stores its measures, one pair for each level. The result of HierarchicalEqualRiskContribution contains the fitted ri, scai, ro and scao. We report the risk of the SumScalariser portfolio from the second run of section 4 under its outer measures and scalariser, ro and scao.

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

This page was generated using Literate.jl.