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$.
  • GenericValueatRiskRange combines 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.

When to reach for this

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.

A conic solver is enough

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)
Example block output

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)
Example block output

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))
Example block output

Summary

The library has a family of coherent tail measures that weight the largest losses more than CVaR does.

  • EntropicValueatRisk is the Chernoff upper bound on VaR and CVaR, over the exponential cone.
  • RelativisticValueatRisk generalises EVaR, and $\kappa$ moves it from EVaR ($\kappa \to 0$) to the worst realisation ($\kappa \to 1$), over the power cone.
  • PowerNormValueatRisk moves toward the worst realisation as its power $p \ge 1$ grows, and it equals the worst realisation once $\alpha T^{1/p} \le 1$.
  • GenericValueatRiskRange combines 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.