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.
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)202. 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)We draw the same plot for the increasing-budget portfolio.
plot_risk_contribution(rf, res_inc, rd)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)Summary
Risk budgeting sets how the risk is divided, not the trade-off between return and risk.
AssetRiskBudgetingdivides risk across assets. An equal budget gives the ERC portfolio, and any other budget states how you want the risk divided.RelaxedRiskBudgetingis convex and cheaper to solve. Read the realised contributions, because a relaxation need not reach exact risk parity.FactorRiskBudgetingdivides risk across factors instead of assets. Unlessreholds aRegressionresult or the prior carries its own loadings, it needs the returns data at optimise time.
This page was generated using Literate.jl.