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
rcfield 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.
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)"))end4. 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:
Variancewithrcconstraints directly limits realised risk contribution, either by asset or by factor.MeanRiskandFactorRiskContributioncan be run with the same constraints and different objective functions to compare allocations and concentration.risk_contributionandfactor_risk_contributionverify whether the solved portfolio's realised profile matches your intended contribution policy.
This page was generated using Literate.jl.