The source files can be found in examples/.
Tail risk measures that weight the worst losses more than CVaR
ConditionalValueatRisk, CVaR, is the most common coherent tail measure. It is the expected loss over the worst fraction $\alpha$ of outcomes. Because it averages over the tail, two distributions with the same tail average and very different tail shapes have the same CVaR. The library has a family of coherent tail measures that weight the largest losses more than CVaR does.
EntropicValueatRisk, EVaR, is the tightest upper bound on VaR and CVaR that the Chernoff inequality gives, built from the moment-generating function of the losses. It is at least as large as CVaR, and the solver works over the exponential cone.RelativisticValueatRisk, RLVaR, generalises EVaR with the Kaniadakis entropy, which has a deformation parameter $\kappa \in (0, 1)$. It moves from EVaR as $\kappa \to 0$ to the worst realisation as $\kappa \to 1$, so $\kappa$ sets how much the largest losses count. The solver works over the power cone.PowerNormValueatRisk, PNVaR, generalises EVaR by replacing the moment-generating function with a power norm of power $p \ge 1$. The solver also works over the power cone, and the measure also moves toward the worst realisation as $p$ grows. On a finite sample it reaches the worst realisation at a finite $p$.GenericValueatRiskRangecombines any two of these measures into a range, one measure on the losses and another on the gains.
The measures are ordered, $\mathrm{CVaR} \le \mathrm{EVaR} \le \mathrm{RLVaR}$, and each takes the tail fraction $\alpha$ as CVaR does.
Reach for these measures when CVaR does not penalise the extreme tail enough. They suit you when the few worst outcomes matter more than the mean of the worst 5 %, and you want a measure that still counts more than the single worst day. Raise $\kappa$ for RLVaR to move from EVaR toward the worst case, or $p$ for PNVaR to move from CVaR toward it.
EVaR needs the exponential cone, and RLVaR and PNVaR need the power cone. Clarabel supports both, so the Clarabel solver in opt handles both cones, and no other solver is needed. The previous page solves VarianceSkewKurtosis with SCS, because it builds a large semidefinite problem. These measures also take an slv field. You need it only when you evaluate a measure alone with expected_risk, as we do below, and not when the measure is the objective of a MeanRisk.
using PortfolioOptimisers, PrettyTables, DataFrames, Statisticsresfmt = (v, i, j) -> begin if j == 1 return v else return isa(v, Number) ? "$(round(v * 100, digits = 3)) %" : v endend;1. Data and shared setup
using CSV, TimeSeries, ClarabelX = TimeArray(CSV.File(joinpath(@__DIR__, "..", "SP500.csv.gz")); timestamp = :Date)[(end - 252):end]rd = prices_to_returns(X)pr = prior(EmpiricalPrior(), rd)slv = [Solver(; name = :clarabel1, solver = Clarabel.Optimizer, settings = Dict("verbose" => false), check_sol = (; allow_local = true, allow_almost = true)), Solver(; name = :clarabel2, solver = Clarabel.Optimizer, settings = Dict("verbose" => false, "max_step_fraction" => 0.95), check_sol = (; allow_local = true, allow_almost = true)), Solver(; name = :clarabel3, solver = Clarabel.Optimizer, settings = Dict("verbose" => false, "max_step_fraction" => 0.9), check_sol = (; allow_local = true, allow_almost = true))]opt = JuMPOptimiser(; pe = pr, slv = slv)JuMPOptimiser
pe ┼ LowOrderPrior
│ X ┼ 252×20 Matrix{Float64}
│ o_X ┼ nothing
│ mu ┼ 20-element Vector{Float64}
│ sigma ┼ 20×20 Matrix{Float64}
│ chol ┼ nothing
│ w ┼ nothing
│ ens ┼ nothing
│ kld ┼ nothing
│ ow ┼ nothing
│ rr ┼ nothing
│ fpr ┼ nothing
│ x_src ┼ nothing
│ ss_gaps ┼ nothing
│ pc ┴ nothing
slv ┼ 3-element Vector{Solver}
│ Solver ⋯
│ Solver ⋯
│ Solver ⋯
wb ┼ WeightBounds
│ lb ┼ Float64: 0.0
│ ub ┴ Float64: 1.0
bgt ┼ Float64: 1.0
sbgt ┼ nothing
gbgt ┼ nothing
lt ┼ nothing
st ┼ nothing
lcse ┼ nothing
cte ┼ nothing
gcarde ┼ nothing
sgcarde ┼ nothing
smtx ┼ nothing
sgmtx ┼ nothing
slt ┼ nothing
sst ┼ nothing
sglt ┼ nothing
sgst ┼ nothing
tn ┼ nothing
fees ┼ nothing
sets ┼ nothing
tr ┼ nothing
ple ┼ nothing
ret ┼ ArithmeticReturn
│ settings ┼ JuMPReturnsSettings
│ │ scale ┼ Float64: 1.0
│ │ lb ┼ nothing
│ │ rte ┼ Bool: true
│ │ fee ┼ Bool: true
│ │ mic ┴ Bool: true
│ ucs ┼ nothing
│ mu ┼ nothing
│ mtx_sqrt ┴ EigenFallbackSquareRoot()
sca ┼ SumScalariser()
ccnt ┼ nothing
cobj ┼ nothing
sc ┼ Int64: 1
so ┼ Int64: 1
ss ┼ nothing
card ┼ nothing
scard ┼ nothing
l2c ┼ nothing
lpc ┼ nothing
linfc ┼ nothing
l1 ┼ nothing
l2 ┼ nothing
lp ┼ nothing
linf ┼ nothing
xbgt ┼ Bool: false
strict ┴ Bool: false
2. Minimising each tail measure
We minimise each measure with MeanRisk. We use the defaults alpha = 0.05 for every measure and kappa = 0.3 for RLVaR. For PNVaR we use p = 1.5, not the default p = 2.0. Section 5 explains why, on 252 observations, PNVaR at p = 2.0 is the worst realisation.
measures = ["CVaR" => ConditionalValueatRisk(), "EVaR" => EntropicValueatRisk(), "RLVaR" => RelativisticValueatRisk(), "PNVaR" => PowerNormValueatRisk(; p = 1.5)]results = [optimise(MeanRisk(; r = r, opt = opt)) for (_, r) in measures]names_r = first.(measures)pretty_table(DataFrame(hcat(rd.nx, [r.w for r in results]...), [:assets; Symbol.(names_r)...]); formatters = [resfmt])┌────────┬──────────┬──────────┬──────────┬──────────┐
│ assets │ CVaR │ EVaR │ RLVaR │ PNVaR │
│ Any │ Any │ Any │ Any │ Any │
├────────┼──────────┼──────────┼──────────┼──────────┤
│ AAPL │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ AMD │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ BAC │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ BBY │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ CVX │ 13.167 % │ 21.386 % │ 19.13 % │ 20.329 % │
│ GE │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ HD │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ JNJ │ 45.342 % │ 55.414 % │ 47.541 % │ 62.965 % │
│ JPM │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ KO │ 13.285 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ LLY │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ MRK │ 20.556 % │ 21.207 % │ 27.934 % │ 15.798 % │
│ MSFT │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ PEP │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ PFE │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ PG │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ RRC │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ UNH │ 0.0 % │ 0.0 % │ 0.0 % │ 0.0 % │
│ WMT │ 0.0 % │ 1.993 % │ 5.396 % │ 0.908 % │
│ XOM │ 7.65 % │ 0.0 % │ 0.0 % │ 0.0 % │
└────────┴──────────┴──────────┴──────────┴──────────┘The portfolios differ. CVaR is the mean loss over the worst 5 % of days. EVaR and RLVaR also put more weight on the worst of those days.
using StatsPlots, GraphRecipesplot_stacked_bar_composition(results, rd)3. How different is each measure from CVaR?
We evaluate each minimum-risk portfolio under every measure. Each row of the table is one portfolio, and each column one measure. We pass slv here, because we evaluate the measures alone.
evals = ["CVaR" => ConditionalValueatRisk(), "EVaR" => EntropicValueatRisk(; slv = slv), "RLVaR" => RelativisticValueatRisk(; slv = slv), "PNVaR" => PowerNormValueatRisk(; slv = slv, p = 1.5)]cross = DataFrame(; minimises = names_r)for (mname, m) in evals cross[!, Symbol(mname)] = [expected_risk(m, r.w, rd.X) for r in results]endpretty_table(cross; formatters = [resfmt])┌───────────┬─────────┬─────────┬─────────┬─────────┐
│ minimises │ CVaR │ EVaR │ RLVaR │ PNVaR │
│ String │ Float64 │ Float64 │ Float64 │ Float64 │
├───────────┼─────────┼─────────┼─────────┼─────────┤
│ CVaR │ 1.764 % │ 2.062 % │ 2.218 % │ 2.346 % │
│ EVaR │ 1.81 % │ 1.97 % │ 2.08 % │ 2.171 % │
│ RLVaR │ 1.826 % │ 1.977 % │ 2.074 % │ 2.184 % │
│ PNVaR │ 1.814 % │ 1.978 % │ 2.099 % │ 2.158 % │
└───────────┴─────────┴─────────┴─────────┴─────────┘In each column, the smallest value is on the diagonal, where the portfolio minimised that measure. The portfolio that is best under one measure is not the best under the others. Along each row CVaR ≤ EVaR ≤ RLVaR, because each puts more weight on the largest losses than the one before it.
4. The parameter $\kappa$ of RLVaR, from EVaR to the worst realisation
$\kappa$ moves RelativisticValueatRisk between two measures you know. We take one portfolio, the one that minimises CVaR, and evaluate RLVaR on it for five values of $\kappa$. We also evaluate EVaR and WorstRealisation, the two limits.
w_fixed = results[1].wkappas = [0.01, 0.1, 0.3, 0.6, 0.99]rlvar_curve = [expected_risk(RelativisticValueatRisk(; slv = slv, kappa = k), w_fixed, rd.X) for k in kappas]evar_ref = expected_risk(EntropicValueatRisk(; slv = slv), w_fixed, rd.X)wr_ref = expected_risk(WorstRealisation(), w_fixed, rd.X)pretty_table(DataFrame(; :kappa => kappas, :RLVaR => rlvar_curve); formatters = [resfmt])┌─────────┬─────────┐
│ kappa │ RLVaR │
│ Float64 │ Float64 │
├─────────┼─────────┤
│ 0.01 │ 2.062 % │
│ 0.1 │ 2.079 % │
│ 0.3 │ 2.218 % │
│ 0.6 │ 2.458 % │
│ 0.99 │ 2.479 % │
└─────────┴─────────┘RLVaR rises with $\kappa$. We plot the curve with EVaR and the worst realisation as horizontal lines. The plot shows RLVaR near EVaR at $\kappa = 0.01$ and near the worst realisation at $\kappa = 0.99$.
plot(kappas, rlvar_curve; seriestype = :path, marker = (:circle, 5), label = "RLVaR(κ)", xlabel = "κ", ylabel = "Realised tail risk", legend = :topleft, title = "RLVaR of the minimum-CVaR portfolio for five values of κ")hline!([evar_ref]; label = "EVaR", linestyle = :dash)hline!([wr_ref]; label = "Worst realisation", linestyle = :dot)5. The power p of the power-norm VaR
PowerNormValueatRisk has a power $p \ge 1$ with a similar effect. A larger p moves the measure toward the worst realisation. We evaluate it on the same portfolio, beside the worst realisation wr_ref of section 4.
ps = [1.25, 1.5, 1.75, 1.8]pnvar_curve = [expected_risk(PowerNormValueatRisk(; slv = slv, p = p), w_fixed, rd.X) for p in ps]pretty_table(DataFrame(; :p => ps, :PNVaR => pnvar_curve, :worst_realisation => wr_ref); formatters = [resfmt])┌─────────┬─────────┬───────────────────┐
│ p │ PNVaR │ worst_realisation │
│ Float64 │ Float64 │ Float64 │
├─────────┼─────────┼───────────────────┤
│ 1.25 │ 2.082 % │ 2.479 % │
│ 1.5 │ 2.346 % │ 2.479 % │
│ 1.75 │ 2.472 % │ 2.479 % │
│ 1.8 │ 2.475 % │ 2.479 % │
└─────────┴─────────┴───────────────────┘PNVaR rises with p. Unlike RLVaR, it reaches the worst realisation at a finite p. With $T$ equally weighted observations, PNVaR equals the worst realisation when $\alpha T^{1/p} \le 1$, that is when $p \ge \log T / \log(1/\alpha)$. For our 252 observations at $\alpha = 0.05$ this is $p \ge 1.85$, so the sweep stops at p = 1.8. At the default p = 2 the measure is the worst realisation. Raise alpha or use more observations to keep a larger p below that point.
We start above p = 1 on purpose. The constructor accepts p = 1, but the power cone degenerates at that value, and the solver stops without a solution.
6. Both tails with GenericValueatRiskRange
GenericValueatRiskRange combines any two of these tail measures into a range. One measure applies to the returns, for the losses, and the other to the negated returns, for the gains. This lets you treat the two tails differently, for example with EVaR on the losses, which penalises large losses more, and CVaR on the gains. We compare it with the symmetric ConditionalValueatRiskRange.
r_asym = GenericValueatRiskRange(; loss = EntropicValueatRisk(), gain = ConditionalValueatRisk())res_asym = optimise(MeanRisk(; r = r_asym, opt = opt))res_sym = optimise(MeanRisk(; r = ConditionalValueatRiskRange(), opt = opt))pretty_table(DataFrame(; :assets => rd.nx, :EVaR_loss_CVaR_gain => res_asym.w, :CVaR_range => res_sym.w); formatters = [resfmt])┌────────┬─────────────────────┬────────────┐
│ assets │ EVaR_loss_CVaR_gain │ CVaR_range │
│ String │ Float64 │ Float64 │
├────────┼─────────────────────┼────────────┤
│ AAPL │ 0.0 % │ 0.0 % │
│ AMD │ 0.0 % │ 0.0 % │
│ BAC │ 0.0 % │ 0.0 % │
│ BBY │ 0.0 % │ 0.0 % │
│ CVX │ 0.0 % │ 10.966 % │
│ GE │ 0.065 % │ 0.0 % │
│ HD │ 0.0 % │ 0.0 % │
│ JNJ │ 45.358 % │ 45.582 % │
│ JPM │ 0.0 % │ 0.0 % │
│ KO │ 0.958 % │ 12.099 % │
│ LLY │ 0.0 % │ 0.0 % │
│ MRK │ 28.527 % │ 12.347 % │
│ MSFT │ 0.0 % │ 0.0 % │
│ PEP │ 0.0 % │ 0.0 % │
│ PFE │ 0.0 % │ 0.0 % │
│ PG │ 0.085 % │ 3.365 % │
│ RRC │ 4.442 % │ 1.242 % │
│ UNH │ 0.0 % │ 0.0 % │
│ WMT │ 10.941 % │ 14.399 % │
│ XOM │ 9.623 % │ 0.0 % │
└────────┴─────────────────────┴────────────┘The asymmetric range weights the largest losses more than the symmetric CVaR range does. Both ranges measure the gains with CVaR at the same alpha, so the two columns differ only by the measure on the loss side.
We plot the four portfolios of section 2 again, with the name of each measure under its bar.
plot_stacked_bar_composition(results, rd; xticks = ([1, 2, 3, 4], names_r))Summary
The library has a family of coherent tail measures that weight the largest losses more than CVaR does.
EntropicValueatRiskis the Chernoff upper bound on VaR and CVaR, over the exponential cone.RelativisticValueatRiskgeneralises EVaR, and $\kappa$ moves it from EVaR ($\kappa \to 0$) to the worst realisation ($\kappa \to 1$), over the power cone.PowerNormValueatRiskmoves toward the worst realisation as its power $p \ge 1$ grows, and it equals the worst realisation once $\alpha T^{1/p} \le 1$.GenericValueatRiskRangecombines any two of them into one measure of both tails, with a different measure on each side.
Every one of them is convex. We minimised all of them with Clarabel on this page.
This page was generated using Literate.jl.