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.
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 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.
- The default covariance, denoised with the spectral algorithm.
- The Gerber covariance, whose default algorithm is
Gerber1. - The Smyth-Broby covariance with the
SmythBroby1algorithm. - The mutual information covariance.
- 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, GraphRecipesThe dendrogram shows the hierarchical clustering of the assets.
plot_dendrogram(clr, rd.nx)The heatmap shows the correlation matrix in the order of the clusters, with a box around each cluster.
plot_clusters(clr, rd.nx)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)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)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.013323346719772058We 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.002262226567821208This page was generated using Literate.jl.