The source files can be found in examples/.

Risk contribution

This example focuses on where risk lands rather than only on how much risk a portfolio takes. It walks through two related workflows:

  • asset risk contribution under the variance measure, using the rc field to constrain per-asset contributions while comparing multiple objective functions;
  • factor risk contribution optimisation, where we constrain the contribution of specific factors and solve across multiple objectives.
When to reach for this

Reach for risk-contribution workflows when the allocation itself is not the point. They are useful when you want to see concentration in the realised risk profile, impose contribution limits directly in risk space, and compare how objective functions reshape the final risk profile under the same rc constraints.

using PortfolioOptimisers, PrettyTables# Format for pretty tables.resfmt = (v, i, j) -> begin    if j == 1        return v    else        return isa(v, Number) ? "$(round(v * 100, digits = 3)) %" : v    endend;

1. ReturnsResult data

We use one year of S&P 500 prices and the matching factor returns slice used in the test suite. The factor block is required for the factor-risk-contribution section.

using CSV, TimeSeries, DataFrames, ClarabelX = TimeArray(CSV.File(joinpath(@__DIR__, "..", "SP500.csv.gz")); timestamp = :Date)[(end - 252):end]F = TimeArray(CSV.File(joinpath(@__DIR__, "..", "Factors.csv.gz")); timestamp = :Date)[(end - 252):end]rd = prices_to_returns(X, F)
ReturnsResult
    nx ┼ 20-element Vector{String}
     X ┼ 252×20 Matrix{Float64}
    nf ┼ Vector{String}: ["MTUM", "QUAL", "SIZE", "USMV", "VLUE"]
     F ┼ 252×5 Matrix{Float64}
    nb ┼ nothing
     B ┼ nothing
    ts ┼ 252-element Vector{Date}
    iv ┼ nothing
  ivpa ┼ nothing
    nz ┼ nothing
     Z ┴ nothing

2. Shared optimiser setup

We pass a vector of solver configurations so the examples can fall back cleanly if the first Clarabel configuration stalls on this data slice.

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))]pr = prior(EmpiricalPrior(), rd)opt = JuMPOptimiser(; pe = pr, slv = slv)sets_asset = UniverseSets(; dict = Dict("nx" => rd.nx))opt_asset = JuMPOptimiser(; pe = pr, slv = slv, sets = sets_asset)
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
          │       Z ┴ 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
     xbgt ┼ Bool: false
       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 ┼ UniverseSets
          │    xkey ┼ String: "nx"
          │   uxkey ┼ String: "ux"
          │    fkey ┼ String: "nf"
          │   ufkey ┼ String: "uf"
          │    zkey ┼ String: "nz"
          │    dict ┴ Dict{String, Vector{String}}: Dict("nx" => ["AAPL", "AMD", "BAC", "BBY", "CVX", "GE", "HD", "JNJ", "JPM", "KO", "LLY", "MRK", "MSFT", "PEP", "PFE", "PG", "RRC", "UNH", "WMT", "XOM"])
       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
      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
     linf ┼ nothing
       lp ┼ nothing
      brt ┼ Bool: false
    x_src ┼ Symbol: :prior
    z_src ┼ Symbol: :data
   strict ┴ Bool: false

3. Asset risk contribution with rc constraints

risk_contribution decomposes the total risk of a portfolio into per-asset contributions. Instead of using risk budgeting, we constrain contributions directly via Variance using its rc field.

The same risk constraints are then solved under three different objectives to show how objective choice changes weights while still respecting the contribution limits.

lcs_asset = LinearConstraintEstimator(; val = ["$a <= 0.2" for a in rd.nx])r_asset = Variance(; rc = lcs_asset)rf_asset = factory(r_asset, pr)obj_specs = [(:min_risk, MinimumRisk()), (:max_utility, MaximumUtility()),             (:max_ratio, MaximumRatio())]asset_df = DataFrame(; assets = rd.nx)asset_res = Dict{Symbol, Any}()for (name, obj) in obj_specs    res = optimise(MeanRisk(; r = r_asset, obj = obj, opt = opt_asset))    asset_res[name] = res    rcs = risk_contribution(rf_asset, res.w, pr.X)    rcs ./= sum(rcs)    asset_df[!, Symbol("$(name)_weight")] = res.w    asset_df[!, Symbol("$(name)_risk")] = rcsendpretty_table(asset_df; formatters = [resfmt])
┌────────┬─────────────────┬───────────────┬────────────────────┬──────────────────┬──────────────────┬────────────────┐
│ assets  min_risk_weight  min_risk_risk  max_utility_weight  max_utility_risk  max_ratio_weight  max_ratio_risk │
│ String          Float64        Float64             Float64           Float64           Float64         Float64 │
├────────┼─────────────────┼───────────────┼────────────────────┼──────────────────┼──────────────────┼────────────────┤
│   AAPL │           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 % │
│    BAC │           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 % │
│    CVX │         9.212 % │       9.707 % │              0.0 % │            0.0 % │         14.354 % │       19.972 % │
│     GE │         0.407 % │       0.425 % │              0.0 % │            0.0 % │            0.0 % │          0.0 % │
│     HD │           0.0 % │         0.0 % │              0.0 % │            0.0 % │            0.0 % │          0.0 % │
│    JNJ │        21.939 % │        20.0 % │              0.0 % │            0.0 % │         11.744 % │        7.931 % │
│    JPM │         1.652 % │       1.715 % │              0.0 % │            0.0 % │            0.0 % │          0.0 % │
│     KO │        13.302 % │      13.797 % │              0.0 % │            0.0 % │         22.528 % │       17.582 % │
│    LLY │           0.0 % │         0.0 % │              0.0 % │            0.0 % │         11.993 % │       13.099 % │
│    MRK │        20.466 % │        20.0 % │           58.664 % │         41.028 % │         23.533 % │         20.0 % │
│   MSFT │           0.0 % │         0.0 % │              0.0 % │            0.0 % │            0.0 % │          0.0 % │
│    PEP │        12.847 % │      13.311 % │              0.0 % │            0.0 % │          0.697 % │        0.508 % │
│    PFE │           0.0 % │         0.0 % │              0.0 % │            0.0 % │            0.0 % │          0.0 % │
│     PG │         5.642 % │       5.824 % │              0.0 % │            0.0 % │            0.0 % │          0.0 % │
│    RRC │           0.0 % │         0.0 % │              0.0 % │            0.0 % │            0.0 % │          0.0 % │
│    UNH │           0.0 % │         0.0 % │              0.0 % │            0.0 % │            0.0 % │          0.0 % │
│    WMT │        10.438 % │      10.898 % │              0.0 % │            0.0 % │          1.418 % │        0.908 % │
│    XOM │         4.095 % │       4.322 % │           41.336 % │         58.972 % │         13.733 % │         20.0 % │
└────────┴─────────────────┴───────────────┴────────────────────┴──────────────────┴──────────────────┴────────────────┘

Even when all solutions satisfy the same per-asset contribution caps, objective functions still produce different weights and risk-contribution shapes.

using StatsPlots, GraphRecipesfor (name, _) in obj_specs    display(plot_risk_contribution(rf_asset, asset_res[name], rd;                                   title = "Asset RC - $(name)"))end

4. Factor risk contribution optimisation

FactorRiskContribution works one layer higher: instead of decomposing risk across assets only, it uses factor regression to impose or inspect contribution targets on named factors. The regression estimator needs the factor-return data at optimisation time, so we pass rd directly to optimise.

The factor side mirrors the same idea: rc constraints are fixed, while objectives vary.

sets = UniverseSets(; dict = Dict("nx" => rd.nf))lcs = LinearConstraintEstimator(; val = ["VLUE <= 0.74", "QUAL >= -0.07", "MTUM==0.09"])r_fac = Variance(; rc = lcs)rf_fac = factory(r_fac, pr)factor_df = DataFrame(; factor = [rd.nf; "Intercept"])factor_res = Dict{Symbol, Any}()for (name, obj) in obj_specs    res = optimise(FactorRiskContribution(; r = r_fac, obj = obj, opt = opt, sets = sets),                   rd)    factor_res[name] = res    frc_risk = factor_risk_contribution(rf_fac, res.w, pr.X; rd = rd)    frc_risk ./= sum(frc_risk)    factor_df[!, Symbol("$(name)_risk")] = frc_riskendpretty_table(factor_df; formatters = [resfmt])for (name, _) in obj_specs    display(plot_factor_risk_contribution(rf_fac, factor_res[name], rd;                                          title = "Factor RC - $(name)"))end
┌───────────┬───────────────┬──────────────────┬────────────────┐
│    factor  min_risk_risk  max_utility_risk  max_ratio_risk │
│    String        Float64           Float64         Float64 │
├───────────┼───────────────┼──────────────────┼────────────────┤
│      MTUM │       5.299 % │         10.532 % │          9.0 % │
│      QUAL │       9.545 % │        -13.214 % │       -6.459 % │
│      SIZE │     -60.711 % │        -42.157 % │      -48.675 % │
│      USMV │      90.242 % │          64.72 % │       73.014 % │
│      VLUE │      55.626 % │         80.119 % │        73.12 % │
│ Intercept │        -0.0 % │           -0.0 % │         -0.0 % │
└───────────┴───────────────┴──────────────────┴────────────────┘

Summary

Risk contribution workflows answer a different question from plain mean-risk optimisation:


This page was generated using Literate.jl.