The source files can be found in examples/.

Risk budgeting

RiskBudgeting takes a different stance from MeanRisk. It does not trade expected return against risk through an objective function. It divides the risk itself, and finds the portfolio whose per-asset or per-factor risk contributions come as close to a budget you supply as they can.

The best-known special case is the equal risk contribution portfolio, or ERC, where every asset carries the same share of total risk. Risk budgeting extends it to any budget vector, and to risk measured by any of the risk measures MeanRisk accepts.

When to reach for this

Reach for risk budgeting when you care about how the risk is divided rather than about the trade-off between return and risk. It spreads risk rather than capital, so it avoids the concentration a minimum-variance portfolio often shows, and it lets you state a view such as "this group of assets carries 30% of the risk". If you want the best return for a given amount of risk instead, use MeanRisk.

using PortfolioOptimisers, PrettyTablesresfmt = (v, i, j) -> begin    if j == 1        return v    else        return isa(v, Number) ? "$(round(v*100, digits=3)) %" : v    endend;

1. Data

We use one year of S&P 500 constituents. Section 4 adds the factor returns.

using CSV, TimeSeries, DataFramesX = TimeArray(CSV.File(joinpath(@__DIR__, "..", "SP500.csv.gz")); timestamp = :Date)[(end - 252):end]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

Every optimisation below reads the same data, so we compute the prior statistics once with EmpiricalPrior and hand them to each optimiser.

using Clarabelslv = Solver(; name = :clarabel, solver = Clarabel.Optimizer,             settings = Dict("verbose" => false),             check_sol = (; allow_local = true, allow_almost = true))pr = prior(EmpiricalPrior(), rd)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 ┼ Solver
          │          name ┼ Symbol: :clarabel
          │        solver ┼ UnionAll: Clarabel.MOIwrapper.Optimizer
          │      settings ┼ Dict{String, Bool}: Dict{String, Bool}("verbose" => 0)
          │     check_sol ┼ @NamedTuple{allow_local::Bool, allow_almost::Bool}: (allow_local = true, allow_almost = true)
          │   add_bridges ┴ Bool: true
       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

We budget variance throughout this example. Any risk measure MeanRisk accepts works here, and the budget divides whichever measure you pass.

r = Variance()N = length(rd.nx)
20

2. Asset risk budgeting

AssetRiskBudgeting divides risk across assets. You pass the budget through the rkb keyword as a RiskBudget, and the vector does not have to sum to one. The alg keyword picks the formulation. LogRiskBudgeting is a log-barrier and the default, and MixedIntegerRiskBudgeting needs a mixed-integer solver.

We build two budgets. The equal budget gives the ERC portfolio. The linearly increasing budget asks each asset to carry more of the risk than the asset before it.

rb_eq = RiskBudgeting(; r = r, opt = opt,                      rba = AssetRiskBudgeting(; rkb = RiskBudget(; val = fill(1.0, N)),                                               alg = LogRiskBudgeting()))rb_inc = RiskBudgeting(; r = r, opt = opt,                       rba = AssetRiskBudgeting(; rkb = RiskBudget(; val = 1:N),                                                alg = LogRiskBudgeting()))res_eq, res_inc = optimise(rb_eq), optimise(rb_inc)
(RiskBudgetingResult
   jr ┼ JuMPOptimisationResult
      │        pa ┼ ProcessedJuMPOptimiserAttributes
      │           │        pr ┼ 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
      │           │        wb ┼ WeightBounds
      │           │           │   lb ┼ 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
      │           │           │   ub ┴ 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
      │           │        lt ┼ nothing
      │           │        st ┼ nothing
      │           │      lcsr ┼ nothing
      │           │       ctr ┼ nothing
      │           │    gcardr ┼ nothing
      │           │   sgcardr ┼ nothing
      │           │      smtx ┼ nothing
      │           │     sgmtx ┼ nothing
      │           │       slt ┼ nothing
      │           │       sst ┼ nothing
      │           │      sglt ┼ nothing
      │           │      sgst ┼ nothing
      │           │        tn ┼ nothing
      │           │      fees ┼ nothing
      │           │       plr ┼ nothing
      │           │       ret ┼ ArithmeticReturn
      │           │           │   settings ┼ JuMPReturnsSettings
      │           │           │            │   scale ┼ Float64: 1.0
      │           │           │            │      lb ┼ nothing
      │           │           │            │     rte ┼ Bool: true
      │           │           │            │     fee ┼ Bool: true
      │           │           │            │     mic ┴ Bool: true
      │           │           │        ucs ┼ nothing
      │           │           │         mu ┼ 20-element Vector{Float64}
      │           │           │   mtx_sqrt ┴ EigenFallbackSquareRoot()
      │           │       sca ┼ SumScalariser()
      │           │      imsk ┴ nothing
      │   retcode ┼ OptimisationSuccess
      │           │   res ┴ Dict{Any, Any}: Dict{Any, Any}()
      │       sol ┼ JuMPOptimisationSolution
      │           │   w ┴ 20-element Vector{Float64}
      │     model ┼ A JuMP Model
      │           │ ├ solver: Clarabel
      │           │ ├ objective_sense: MIN_SENSE
      │           │ │ └ objective_function_type: QuadExpr
      │           │ ├ num_variables: 42
      │           │ ├ num_constraints: 25
      │           │ │ ├ AffExpr in MOI.EqualTo{Float64}: 1
      │           │ │ ├ AffExpr in MOI.GreaterThan{Float64}: 1
      │           │ │ ├ Vector{AffExpr} in MOI.Nonnegatives: 1
      │           │ │ ├ Vector{AffExpr} in MOI.Nonpositives: 1
      │           │ │ ├ Vector{AffExpr} in MOI.SecondOrderCone: 1
      │           │ │ └ Vector{AffExpr} in MOI.ExponentialCone: 20
      │           │ └ Names registered in the model
      │           │   └ :G, :T, :bgt, :cdev_soc_1, :clog_w, :crkb, :dev_1, :k, :log_w, :lw, :obj_expr, :ret, :ret_1, :ret_vec, :risk, :risk_minimised, :risk_vec, :sc, :so, :unit_budget, :variance_flag, :variance_risk_1, :w, :w_gross_ub, :w_lb, :w_ub
    r ┼ 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
  prb ┼ ProcessedAssetRiskBudgetingAttributes
      │   rkb ┼ RiskBudget
      │       │   val ┴ 20-element Vector{Float64}
   fb ┴ nothing
, RiskBudgetingResult
   jr ┼ JuMPOptimisationResult
      │        pa ┼ ProcessedJuMPOptimiserAttributes
      │           │        pr ┼ 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
      │           │        wb ┼ WeightBounds
      │           │           │   lb ┼ 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
      │           │           │   ub ┴ 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
      │           │        lt ┼ nothing
      │           │        st ┼ nothing
      │           │      lcsr ┼ nothing
      │           │       ctr ┼ nothing
      │           │    gcardr ┼ nothing
      │           │   sgcardr ┼ nothing
      │           │      smtx ┼ nothing
      │           │     sgmtx ┼ nothing
      │           │       slt ┼ nothing
      │           │       sst ┼ nothing
      │           │      sglt ┼ nothing
      │           │      sgst ┼ nothing
      │           │        tn ┼ nothing
      │           │      fees ┼ nothing
      │           │       plr ┼ nothing
      │           │       ret ┼ ArithmeticReturn
      │           │           │   settings ┼ JuMPReturnsSettings
      │           │           │            │   scale ┼ Float64: 1.0
      │           │           │            │      lb ┼ nothing
      │           │           │            │     rte ┼ Bool: true
      │           │           │            │     fee ┼ Bool: true
      │           │           │            │     mic ┴ Bool: true
      │           │           │        ucs ┼ nothing
      │           │           │         mu ┼ 20-element Vector{Float64}
      │           │           │   mtx_sqrt ┴ EigenFallbackSquareRoot()
      │           │       sca ┼ SumScalariser()
      │           │      imsk ┴ nothing
      │   retcode ┼ OptimisationSuccess
      │           │   res ┴ Dict{Any, Any}: Dict{Any, Any}()
      │       sol ┼ JuMPOptimisationSolution
      │           │   w ┴ 20-element Vector{Float64}
      │     model ┼ A JuMP Model
      │           │ ├ solver: Clarabel
      │           │ ├ objective_sense: MIN_SENSE
      │           │ │ └ objective_function_type: QuadExpr
      │           │ ├ num_variables: 42
      │           │ ├ num_constraints: 25
      │           │ │ ├ AffExpr in MOI.EqualTo{Float64}: 1
      │           │ │ ├ AffExpr in MOI.GreaterThan{Float64}: 1
      │           │ │ ├ Vector{AffExpr} in MOI.Nonnegatives: 1
      │           │ │ ├ Vector{AffExpr} in MOI.Nonpositives: 1
      │           │ │ ├ Vector{AffExpr} in MOI.SecondOrderCone: 1
      │           │ │ └ Vector{AffExpr} in MOI.ExponentialCone: 20
      │           │ └ Names registered in the model
      │           │   └ :G, :T, :bgt, :cdev_soc_1, :clog_w, :crkb, :dev_1, :k, :log_w, :lw, :obj_expr, :ret, :ret_1, :ret_vec, :risk, :risk_minimised, :risk_vec, :sc, :so, :unit_budget, :variance_flag, :variance_risk_1, :w, :w_gross_ub, :w_lb, :w_ub
    r ┼ 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
  prb ┼ ProcessedAssetRiskBudgetingAttributes
      │   rkb ┼ RiskBudget
      │       │   val ┴ 20-element UnitRange{Int64}
   fb ┴ nothing
)

We compute the realised risk contributions, which you read against the budgets we asked for. factory first gives the risk measure the covariance of the prior.

rf = factory(r, pr)rc_eq = risk_contribution(rf, res_eq.w, pr.X);rc_eq ./= sum(rc_eq);rc_inc = risk_contribution(rf, res_inc.w, pr.X);rc_inc ./= sum(rc_inc);pretty_table(DataFrame(; :assets => rd.nx, Symbol("Equal budget weight") => res_eq.w,                       Symbol("Equal budget risk share") => rc_eq,                       Symbol("Increasing budget weight") => res_inc.w,                       Symbol("Increasing budget risk share") => rc_inc);             formatters = [resfmt])
┌────────┬─────────────────────┬─────────────────────────┬──────────────────────────┬──────────────────────────────┐
│ assets │ Equal budget weight │ Equal budget risk share │ Increasing budget weight │ Increasing budget risk share │
│ String │             Float64 │                 Float64 │                  Float64 │                      Float64 │
├────────┼─────────────────────┼─────────────────────────┼──────────────────────────┼──────────────────────────────┤
│   AAPL │             3.313 % │                   5.0 % │                  0.329 % │                      0.476 % │
│    AMD │             2.277 % │                   5.0 % │                  0.483 % │                      0.952 % │
│    BAC │             3.979 % │                   5.0 % │                  1.206 % │                      1.429 % │
│    BBY │             3.245 % │                   5.0 % │                  1.337 % │                      1.904 % │
│    CVX │             5.173 % │                 5.001 % │                  2.313 % │                      2.381 % │
│     GE │              3.97 % │                   5.0 % │                  2.417 % │                      2.857 % │
│     HD │             4.182 % │                   5.0 % │                  2.845 % │                      3.333 % │
│    JNJ │             8.222 % │                   5.0 % │                  5.829 % │                       3.81 % │
│    JPM │             4.261 % │                   5.0 % │                  3.824 % │                      4.286 % │
│     KO │             6.254 % │                   5.0 % │                  5.608 % │                      4.762 % │
│    LLY │             5.376 % │                 4.999 % │                  5.238 % │                      5.238 % │
│    MRK │             8.027 % │                   5.0 % │                  8.242 % │                      5.715 % │
│   MSFT │             3.516 % │                   5.0 % │                  4.482 % │                      6.191 % │
│    PEP │             6.306 % │                   5.0 % │                  7.814 % │                      6.668 % │
│    PFE │             5.567 % │                   5.0 % │                  7.297 % │                      7.144 % │
│     PG │             6.353 % │                   5.0 % │                   8.91 % │                      7.619 % │
│    RRC │             2.935 % │                   5.0 % │                  4.282 % │                      8.095 % │
│    UNH │             5.375 % │                   5.0 % │                  8.405 % │                       8.57 % │
│    WMT │             6.561 % │                   5.0 % │                 10.412 % │                      9.048 % │
│    XOM │             5.108 % │                   5.0 % │                  8.726 % │                      9.523 % │
└────────┴─────────────────────┴─────────────────────────┴──────────────────────────┴──────────────────────────────┘

Read the two risk columns of the table. The equal-budget portfolio carries the same $1/N$ share of variance on every asset. The risk contributions of the increasing-budget portfolio rise from the first asset to the last.

The bar plot below draws the risk contributions of the equal-budget portfolio.

using StatsPlots, GraphRecipesplot_risk_contribution(rf, res_eq, rd)
Example block output

We draw the same plot for the increasing-budget portfolio.

plot_risk_contribution(rf, res_inc, rd)
Example block output

3. Relaxed risk budgeting

RelaxedRiskBudgeting (RRB) replaces the non-convex risk-parity constraint with a second-order cone relaxation. It uses no logarithm and no integer variable, so a solver handles it faster, which matters on a large universe. It builds the cone on the Cholesky factor of the covariance, so it works on variance alone and takes no r. Three algorithms trade exactness for regularisation: BasicRelaxedRiskBudgeting, RegularisedRelaxedRiskBudgeting, and RegularisedPenalisedRelaxedRiskBudgeting.

A relaxation does not hold the target budget as tightly as the exact log-barrier and mixed-integer formulations of section 2. Where the covariance is ill-conditioned or the budget is extreme, the realised contributions can sit well away from the target. In exchange the problem stays convex, so further constraints add to it directly. That suits a risk budget that is one goal among several. Use RiskBudgeting when the budget has to hold.

rba_eq = AssetRiskBudgeting(; rkb = RiskBudget(; val = fill(1.0, N)))rrb_basic = RelaxedRiskBudgeting(; opt = opt, rba = rba_eq,                                 alg = BasicRelaxedRiskBudgeting())rrb_reg = RelaxedRiskBudgeting(; opt = opt, rba = rba_eq,                               alg = RegularisedRelaxedRiskBudgeting())res_b, res_r = optimise(rrb_basic), optimise(rrb_reg)
(RelaxedRiskBudgetingResult
   jr ┼ JuMPOptimisationResult
      │        pa ┼ ProcessedJuMPOptimiserAttributes
      │           │        pr ┼ 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
      │           │        wb ┼ WeightBounds
      │           │           │   lb ┼ 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
      │           │           │   ub ┴ 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
      │           │        lt ┼ nothing
      │           │        st ┼ nothing
      │           │      lcsr ┼ nothing
      │           │       ctr ┼ nothing
      │           │    gcardr ┼ nothing
      │           │   sgcardr ┼ nothing
      │           │      smtx ┼ nothing
      │           │     sgmtx ┼ nothing
      │           │       slt ┼ nothing
      │           │       sst ┼ nothing
      │           │      sglt ┼ nothing
      │           │      sgst ┼ nothing
      │           │        tn ┼ nothing
      │           │      fees ┼ nothing
      │           │       plr ┼ nothing
      │           │       ret ┼ ArithmeticReturn
      │           │           │   settings ┼ JuMPReturnsSettings
      │           │           │            │   scale ┼ Float64: 1.0
      │           │           │            │      lb ┼ nothing
      │           │           │            │     rte ┼ Bool: true
      │           │           │            │     fee ┼ Bool: true
      │           │           │            │     mic ┴ Bool: true
      │           │           │        ucs ┼ nothing
      │           │           │         mu ┼ 20-element Vector{Float64}
      │           │           │   mtx_sqrt ┴ EigenFallbackSquareRoot()
      │           │       sca ┼ SumScalariser()
      │           │      imsk ┴ nothing
      │   retcode ┼ OptimisationSuccess
      │           │   res ┴ Dict{Any, Any}: Dict{Any, Any}()
      │       sol ┼ JuMPOptimisationSolution
      │           │   w ┴ 20-element Vector{Float64}
      │     model ┼ A JuMP Model
      │           │ ├ solver: Clarabel
      │           │ ├ objective_sense: MIN_SENSE
      │           │ │ └ objective_function_type: AffExpr
      │           │ ├ num_variables: 42
      │           │ ├ num_constraints: 47
      │           │ │ ├ AffExpr in MOI.EqualTo{Float64}: 1
      │           │ │ ├ Vector{AffExpr} in MOI.Zeros: 1
      │           │ │ ├ Vector{AffExpr} in MOI.Nonnegatives: 1
      │           │ │ ├ Vector{AffExpr} in MOI.Nonpositives: 1
      │           │ │ ├ Vector{AffExpr} in MOI.SecondOrderCone: 21
      │           │ │ └ VariableRef in MOI.GreaterThan{Float64}: 22
      │           │ └ Names registered in the model
      │           │   └ :T, :bgt, :cbasic_rrp, :crrp, :crrp_soc, :gamma, :k, :lw, :obj_expr, :psi, :ret, :ret_1, :ret_vec, :risk, :risk_minimised, :sc, :so, :w, :w_gross_ub, :w_lb, :w_ub, :zeta
  prb ┼ ProcessedAssetRiskBudgetingAttributes
      │   rkb ┼ RiskBudget
      │       │   val ┴ 20-element Vector{Float64}
   fb ┴ nothing
, RelaxedRiskBudgetingResult
   jr ┼ JuMPOptimisationResult
      │        pa ┼ ProcessedJuMPOptimiserAttributes
      │           │        pr ┼ 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
      │           │        wb ┼ WeightBounds
      │           │           │   lb ┼ 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
      │           │           │   ub ┴ 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
      │           │        lt ┼ nothing
      │           │        st ┼ nothing
      │           │      lcsr ┼ nothing
      │           │       ctr ┼ nothing
      │           │    gcardr ┼ nothing
      │           │   sgcardr ┼ nothing
      │           │      smtx ┼ nothing
      │           │     sgmtx ┼ nothing
      │           │       slt ┼ nothing
      │           │       sst ┼ nothing
      │           │      sglt ┼ nothing
      │           │      sgst ┼ nothing
      │           │        tn ┼ nothing
      │           │      fees ┼ nothing
      │           │       plr ┼ nothing
      │           │       ret ┼ ArithmeticReturn
      │           │           │   settings ┼ JuMPReturnsSettings
      │           │           │            │   scale ┼ Float64: 1.0
      │           │           │            │      lb ┼ nothing
      │           │           │            │     rte ┼ Bool: true
      │           │           │            │     fee ┼ Bool: true
      │           │           │            │     mic ┴ Bool: true
      │           │           │        ucs ┼ nothing
      │           │           │         mu ┼ 20-element Vector{Float64}
      │           │           │   mtx_sqrt ┴ EigenFallbackSquareRoot()
      │           │       sca ┼ SumScalariser()
      │           │      imsk ┴ nothing
      │   retcode ┼ OptimisationSuccess
      │           │   res ┴ Dict{Any, Any}: Dict{Any, Any}()
      │       sol ┼ JuMPOptimisationSolution
      │           │   w ┴ 20-element Vector{Float64}
      │     model ┼ A JuMP Model
      │           │ ├ solver: Clarabel
      │           │ ├ objective_sense: MIN_SENSE
      │           │ │ └ objective_function_type: AffExpr
      │           │ ├ num_variables: 43
      │           │ ├ num_constraints: 49
      │           │ │ ├ AffExpr in MOI.EqualTo{Float64}: 1
      │           │ │ ├ Vector{AffExpr} in MOI.Zeros: 1
      │           │ │ ├ Vector{AffExpr} in MOI.Nonnegatives: 1
      │           │ │ ├ Vector{AffExpr} in MOI.Nonpositives: 1
      │           │ │ ├ Vector{AffExpr} in MOI.SecondOrderCone: 22
      │           │ │ └ VariableRef in MOI.GreaterThan{Float64}: 23
      │           │ └ Names registered in the model
      │           │   └ :T, :bgt, :creg_rrp_soc_1, :creg_rrp_soc_2, :crrp, :crrp_soc, :gamma, :k, :lw, :obj_expr, :psi, :ret, :ret_1, :ret_vec, :rho, :risk, :risk_minimised, :sc, :so, :w, :w_gross_ub, :w_lb, :w_ub, :zeta
  prb ┼ ProcessedAssetRiskBudgetingAttributes
      │   rkb ┼ RiskBudget
      │       │   val ┴ 20-element Vector{Float64}
   fb ┴ nothing
)

We print the realised contributions of the two relaxed portfolios next to the exact log-barrier ERC of section 2. On this data the risk contributions of the relaxed portfolios move away from the flat $1/N$ target, and two assets, JNJ and MRK, carry the largest shares. Check the contributions whenever you use a relaxed form.

rc_b = risk_contribution(rf, res_b.w, pr.X);rc_b ./= sum(rc_b);rc_r = risk_contribution(rf, res_r.w, pr.X);rc_r ./= sum(rc_r);pretty_table(DataFrame(; :assets => rd.nx, Symbol("Log ERC risk") => rc_eq,                       Symbol("Basic RRB risk") => rc_b,                       Symbol("Regularised RRB risk") => rc_r); formatters = [resfmt])
┌────────┬──────────────┬────────────────┬──────────────────────┐
│ assets │ Log ERC risk │ Basic RRB risk │ Regularised RRB risk │
│ String │      Float64 │        Float64 │              Float64 │
├────────┼──────────────┼────────────────┼──────────────────────┤
│   AAPL │        5.0 % │          5.0 % │                5.0 % │
│    AMD │        5.0 % │          5.0 % │                5.0 % │
│    BAC │        5.0 % │          5.0 % │                5.0 % │
│    BBY │        5.0 % │          5.0 % │                5.0 % │
│    CVX │      5.001 % │          5.0 % │                5.0 % │
│     GE │        5.0 % │          5.0 % │                5.0 % │
│     HD │        5.0 % │          5.0 % │                5.0 % │
│    JNJ │        5.0 % │          5.0 % │                5.0 % │
│    JPM │        5.0 % │          5.0 % │                5.0 % │
│     KO │        5.0 % │          5.0 % │                5.0 % │
│    LLY │      4.999 % │          5.0 % │                5.0 % │
│    MRK │        5.0 % │          5.0 % │                5.0 % │
│   MSFT │        5.0 % │          5.0 % │                5.0 % │
│    PEP │        5.0 % │          5.0 % │                5.0 % │
│    PFE │        5.0 % │          5.0 % │                5.0 % │
│     PG │        5.0 % │          5.0 % │                5.0 % │
│    RRC │        5.0 % │          5.0 % │                5.0 % │
│    UNH │        5.0 % │          5.0 % │                5.0 % │
│    WMT │        5.0 % │          5.0 % │                5.0 % │
│    XOM │        5.0 % │          5.0 % │                5.0 % │
└────────┴──────────────┴────────────────┴──────────────────────┘

4. Factor risk budgeting

FactorRiskBudgeting divides risk across factors rather than assets. It regresses the asset returns onto the factor returns. It needs the factor returns, and it does not need a factor prior, so an EmpiricalPrior is enough.

F = TimeArray(CSV.File(joinpath(@__DIR__, "..", "Factors.csv.gz")); timestamp = :Date)[(end - 252):end]rdf = prices_to_returns(price_ingestion(PriceIngestion(), X; F = F))prf = prior(EmpiricalPrior(), rdf)optf = JuMPOptimiser(; pe = prf, slv = slv)Nf = length(rdf.nf)frb = RiskBudgeting(; r = Variance(), opt = optf,                    rba = FactorRiskBudgeting(; rkb = RiskBudget(; val = fill(1.0, Nf))))
RiskBudgeting
  opt ┼ 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 ┼ Solver
      │           │          name ┼ Symbol: :clarabel
      │           │        solver ┼ UnionAll: Clarabel.MOIwrapper.Optimizer
      │           │      settings ┼ Dict{String, Bool}: Dict{String, Bool}("verbose" => 0)
      │           │     check_sol ┼ @NamedTuple{allow_local::Bool, allow_almost::Bool}: (allow_local = true, allow_almost = true)
      │           │   add_bridges ┴ Bool: true
      │        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
    r ┼ Variance
      │    settings ┼ RiskMeasureSettings
      │             │   scale ┼ Float64: 1.0
      │             │      ub ┼ nothing
      │             │     rke ┴ Bool: true
      │       sigma ┼ nothing
      │        chol ┼ nothing
      │          rc ┼ nothing
      │         alg ┼ SquaredSOCRiskExpr()
      │    mtx_sqrt ┼ EigenFallbackSquareRoot()
      │   ratio_alg ┼ SquareRootRatio()
      │      corner ┴ nothing
  rba ┼ FactorRiskBudgeting
      │      re ┼ StepwiseRegression
      │         │       crit ┼ PValue
      │         │            │   t ┴ Float64: 0.05
      │         │        alg ┼ ForwardSelection()
      │         │        tgt ┼ LinearModel
      │         │            │   kwargs ┴ @NamedTuple{}: NamedTuple()
      │         │     choice ┼ BatchChoice()
      │         │   included ┼ nothing
      │         │      x_src ┴ nothing
      │     rkb ┼ RiskBudget
      │         │   val ┴ Vector{Float64}: [1.0, 1.0, 1.0, 1.0, 1.0]
      │    sets ┼ nothing
      │    flag ┼ Bool: true
      │   hedge ┴ Bool: false
   wi ┼ nothing
   fb ┴ nothing

re here is a regression estimator, StepwiseRegression by default. An estimator fits the factor model as the optimiser builds the problem, so you pass the returns data to optimise even though the prior is already computed. Pass a Regression result as re instead and the data is not needed. If you leave rd out where it is needed, the error names what is missing.

res_frb = optimise(frb, rdf)
RiskBudgetingResult
   jr ┼ JuMPOptimisationResult
      │        pa ┼ ProcessedJuMPOptimiserAttributes
      │           │        pr ┼ 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
      │           │        wb ┼ WeightBounds
      │           │           │   lb ┼ 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
      │           │           │   ub ┴ 20-element StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
      │           │        lt ┼ nothing
      │           │        st ┼ nothing
      │           │      lcsr ┼ nothing
      │           │       ctr ┼ nothing
      │           │    gcardr ┼ nothing
      │           │   sgcardr ┼ nothing
      │           │      smtx ┼ nothing
      │           │     sgmtx ┼ nothing
      │           │       slt ┼ nothing
      │           │       sst ┼ nothing
      │           │      sglt ┼ nothing
      │           │      sgst ┼ nothing
      │           │        tn ┼ nothing
      │           │      fees ┼ nothing
      │           │       plr ┼ nothing
      │           │       ret ┼ ArithmeticReturn
      │           │           │   settings ┼ JuMPReturnsSettings
      │           │           │            │   scale ┼ Float64: 1.0
      │           │           │            │      lb ┼ nothing
      │           │           │            │     rte ┼ Bool: true
      │           │           │            │     fee ┼ Bool: true
      │           │           │            │     mic ┴ Bool: true
      │           │           │        ucs ┼ nothing
      │           │           │         mu ┼ 20-element Vector{Float64}
      │           │           │   mtx_sqrt ┴ EigenFallbackSquareRoot()
      │           │       sca ┼ SumScalariser()
      │           │      imsk ┴ nothing
      │   retcode ┼ OptimisationSuccess
      │           │   res ┴ Dict{Any, Any}: Dict{Any, Any}()
      │       sol ┼ JuMPOptimisationSolution
      │           │   w ┴ 20-element Vector{Float64}
      │     model ┼ A JuMP Model
      │           │ ├ solver: Clarabel
      │           │ ├ objective_sense: MIN_SENSE
      │           │ │ └ objective_function_type: QuadExpr
      │           │ ├ num_variables: 27
      │           │ ├ num_constraints: 10
      │           │ │ ├ AffExpr in MOI.EqualTo{Float64}: 1
      │           │ │ ├ AffExpr in MOI.GreaterThan{Float64}: 1
      │           │ │ ├ Vector{AffExpr} in MOI.Nonnegatives: 1
      │           │ │ ├ Vector{AffExpr} in MOI.Nonpositives: 1
      │           │ │ ├ Vector{AffExpr} in MOI.SecondOrderCone: 1
      │           │ │ └ Vector{AffExpr} in MOI.ExponentialCone: 5
      │           │ └ Names registered in the model
      │           │   └ :G, :T, :bgt, :cdev_soc_1, :clog_w, :crkb, :dev_1, :k, :log_w, :lw, :obj_expr, :ret, :ret_1, :ret_vec, :risk, :risk_minimised, :risk_vec, :sc, :so, :unit_budget, :variance_flag, :variance_risk_1, :w, :w1, :w2, :w_gross_ub, :w_lb, :w_ub
    r ┼ 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
  prb ┼ ProcessedFactorRiskBudgetingAttributes
      │   rkb ┼ RiskBudget
      │       │   val ┴ Vector{Float64}: [1.0, 1.0, 1.0, 1.0, 1.0]
      │    b1 ┼ 20×5 Matrix{Float64}
      │    rr ┼ Regression
      │       │        M ┼ 20×5 SubArray{Float64, 2, Matrix{Float64}, Tuple{Base.Slice{Base.OneTo{Int64}}, UnitRange{Int64}}, true}
      │       │        L ┼ 20×5 SubArray{Float64, 2, Matrix{Float64}, Tuple{Base.Slice{Base.OneTo{Int64}}, UnitRange{Int64}}, true}
      │       │        b ┼ 20-element SubArray{Float64, 1, Matrix{Float64}, Tuple{Base.Slice{Base.OneTo{Int64}}, Int64}, true}
      │       │   esigma ┼ nothing
      │       │     edof ┼ 20-element Vector{Int64}
      │       │     ediv ┼ nothing
      │       │      idx ┼ nothing
      │       │       ts ┴ nothing
   fb ┴ nothing

We print the factor risk contributions. The last row is the off-factor contribution, the risk that comes from the part of the weights with no exposure to any factor. Read the five factor rows against the equal $1/N_f$ target.

rfk = factory(Variance(), prf)frc = factor_risk_contribution(rfk, res_frb.w, prf.X; rd = rdf)frc ./= sum(frc)pretty_table(DataFrame(; :factor => [rdf.nf; "Off-factor"], :risk => frc);             formatters = [resfmt])plot_factor_risk_contribution(rfk, res_frb, rdf)
Example block output

Summary

Risk budgeting sets how the risk is divided, not the trade-off between return and risk.

  • AssetRiskBudgeting divides risk across assets. An equal budget gives the ERC portfolio, and any other budget states how you want the risk divided.
  • RelaxedRiskBudgeting is convex and cheaper to solve. Read the realised contributions, because a relaxation need not reach exact risk parity.
  • FactorRiskBudgeting divides risk across factors instead of assets. Unless re holds a Regression result or the prior carries its own loadings, it needs the returns data at optimise time.

This page was generated using Literate.jl.