Risk budget estimators
PortfolioOptimisers.RiskBudget — Type
struct RiskBudget{__T_val} <: AbstractConstraintResultCarries the vector of non-negative risk budgets that a risk-budgeting optimiser targets.
The budget names how much of the total risk each entry of an axis is asked to carry. AssetRiskBudgeting writes it over the assets and FactorRiskBudgeting over the factors.
Mathematical definition
A risk-budgeting model minimises the risk of an unnormalised weight vector under a logarithmic barrier, and recovers the portfolio by dividing out the budget variable:
\[\begin{align} \underset{\boldsymbol{y},\, k}{\min}\quad & \phi(\boldsymbol{y})\\ \textrm{s.t.}\quad & \boldsymbol{b}^\intercal \ln(\boldsymbol{y}) \geq 0\,,\\ & \boldsymbol{1}^\intercal \boldsymbol{y} = k\,,\\ & \boldsymbol{y},\, k \geq 0\,,\\ & \boldsymbol{w} = \boldsymbol{y} / k\,. \end{align}\]
The barrier is what states the budget. At the optimum the Karush-Kuhn-Tucker conditions give the risk-contribution identity:
\[\begin{align} y_i \frac{\partial \phi(\boldsymbol{y})}{\partial y_i} &= \lambda\, b_i\,. \end{align}\]
Where:
- $\boldsymbol{b}$: Risk budget vector, the
valfield. - $\boldsymbol{y}$: Unnormalised weight vector.
- $k$: Budget scaling / homogenisation variable.
- $\boldsymbol{w}$: Portfolio weights vector $N \times 1$.
- $\phi$: Risk measure of the optimiser.
- $\lambda$: Multiplier of the barrier constraint.
Only the relative entries of $\boldsymbol{b}$ reach the solution: scaling the whole vector shifts the barrier by a constant. risk_budget_constraints normalises it to sum to one anyway, so a budget that arrives through an estimator always does.
Fields
val: Vector of non-negative risk budgets, one per entry of the axis the budget is written against.risk_budget_constraintsnormalises it to sum to one; a hand-built vector is stored as given, and the model reads it inside a logarithmic barrier, so only its relative entries matter.
Constructors
RiskBudget(; val::Num_VecNum) -> RiskBudgetKeywords correspond to the struct's fields.
Validation
!isempty(val).all(x -> zero(x) <= x, val).
Both checks run in the keyword constructor as well as in the inner one. @concrete emits a generic single-argument constructor that a scalar val matches ahead of the inner VecNum method, so a scalar reaches the struct without passing either check unless the keyword constructor applies them itself.
A scalar val is a real input, not a mistake to refuse: a scalar RiskBudgetEstimator resolves to one, because risk_budget_constraints normalises the budget to sum to one and a scalar divided by itself is 1.0. Such a budget states one allocation over a one-entry axis, and an optimiser accepts it only for a one-asset universe.
View parameters
When port_opt_view is called on this type, the following @vprop-tagged fields are automatically subset to the selected indices:
val: Sliced to the selected indices viaport_opt_view.
Examples
julia> RiskBudget(; val = [0.2, 0.3, 0.5])RiskBudget val ┴ Vector{Float64}: [0.2, 0.3, 0.5]Related
RiskBudgetEstimatorrisk_budget_constraintsAbstractConstraintResultAssetRiskBudgetingFactorRiskBudgetingport_opt_view
References
- [4] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 10.1.3.
PortfolioOptimisers.RiskBudgetEstimator — Type
struct RiskBudgetEstimator{__T_val, __T_dval} <: AbstractConstraintEstimatorResolves a risk budget written in asset or group names against a universe.
risk_budget_constraints turns it into a RiskBudget: every name in val is mapped to its indices in the selected universe, an unnamed asset takes dval, and the result is normalised to sum to one. A group name assigns its value to every asset in the group.
A scalar val is accepted, and resolves to a scalar rather than to a uniform vector. The normalisation divides the scalar by itself, so every scalar resolves to the same RiskBudget(1.0) and the number written here reaches nothing. Only a one-entry axis can consume such a budget: RiskBudgeting reads the budget against an N-vector of weights. Write the uniform budget as nothing, which risk_budget_constraints turns into 1/N over N entries, or as UniformValues, which resolves to the same vector through this estimator.
Fields
val: Mapping of names to risk budget values. A name may be an asset or a group, and a group assigns its value to every asset in it. A scalar is accepted and resolves toRiskBudget(1.0)whatever the scalar was, so only a one-entry axis can consume it. Write the uniform budget asnothing.
dval: Default value for assets not specified inval.
Constructors
RiskBudgetEstimator(; val::EstValType, dval::Option{<:Number} = nothing) -> RiskBudgetEstimatorKeywords correspond to the struct's fields.
Validation
valis validated withassert_nonempty_nonneg_finite_val.
Examples
julia> RiskBudgetEstimator(; val = Dict("A" => 0.2, "B" => 0.3, "C" => 0.5))RiskBudgetEstimator val ┼ Dict{String, Float64}: Dict("B" => 0.3, "A" => 0.2, "C" => 0.5) dval ┴ nothingjulia> RiskBudgetEstimator(; val = ["A" => 0.2, "B" => 0.3, "C" => 0.5])RiskBudgetEstimator val ┼ Vector{Pair{String, Float64}}: ["A" => 0.2, "B" => 0.3, "C" => 0.5] dval ┴ nothingTwo different scalars resolve to the same budget, and the uniform budget over the universe is written as nothing:
julia> sets = UniverseSets(; dict = Dict("nx" => ["A", "B", "C"]));julia> risk_budget_constraints(RiskBudgetEstimator(; val = 0.2), sets)RiskBudget val ┴ Float64: 1.0julia> risk_budget_constraints(RiskBudgetEstimator(; val = 0.9), sets)RiskBudget val ┴ Float64: 1.0julia> risk_budget_constraints(nothing; N = 3)RiskBudget val ┴ StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}: StepRangeLen(0.3333333333333333, 0.0, 3)Related
References
- [4] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 10.1.3.
PortfolioOptimisers.RkbE_Rkb — Type
const RkbE_Rkb = Union{<:RiskBudgetEstimator, <:RiskBudget}Alias for a risk budget estimator or result.
Matches either a RiskBudgetEstimator (specifying how to generate risk budget constraints) or a RiskBudget result (a pre-computed risk budget allocation). Used internally to accept either form in constraint generation dispatch.
There is no vector counterpart, and risk_budget_constraints has no vector method. A risk budget is one allocation over the whole universe, so an optimiser holds exactly one. This is the same reason WbE_Wb and FeesE_Fees are singular, and the reason TnE_Tn, LcE_Lc and PlCE_PlC are not: several turnover, linear or phylogeny constraints can hold at once, and each of those does carry a vector alias and a broadcast method.
Related
PortfolioOptimisers.risk_budget_constraints — Function
risk_budget_constraints(::Nothing, args...; N::Integer, kwargs...)No-op fallback for risk budget constraint generation.
This method returns a uniform risk budget allocation when no explicit risk budget is specified (nothing). It creates a RiskBudget with equal budgets summing to one over N entries. This is useful as a default in workflows where a risk budget is optional or omitted.
The vector is a constant range, not an Array, and its element type is always Float64 because it comes from inv(N). This method takes no datatype keyword; one passed here is swallowed by kwargs... and changes nothing.
Arguments
::Nothing: Indicates that no explicit risk budget is specified.args...: Additional positional arguments (ignored).N::Integer: Number of entries of the budgeted axis (required).kwargs...: Additional keyword arguments (ignored).
Returns
rb::RiskBudget: A result object containing a uniform risk budget vector of lengthN, with each entry equal to1/N.
Examples
julia> risk_budget_constraints(nothing; N = 3)RiskBudget val ┴ StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}: StepRangeLen(0.3333333333333333, 0.0, 3)Related
risk_budget_constraints(rb::RiskBudget, args...; kwargs...)No-op fallback for risk budget constraint propagation.
This method returns the input RiskBudget object unchanged. It is used to pass through an already constructed risk budget allocation result, enabling composability and uniform interface handling in risk budgeting workflows.
Arguments
rb: An existingRiskBudgetobject.args...: Additional positional arguments (ignored).kwargs...: Additional keyword arguments (ignored).
Returns
rb::RiskBudget: The inputRiskBudgetobject, unchanged.
Examples
julia> risk_budget_constraints(RiskBudget(; val = [0.2, 0.3, 0.5]))RiskBudget val ┴ Vector{Float64}: [0.2, 0.3, 0.5]The vector is passed through as given. This method does not normalise it, unlike the estimator method, so a hand-built budget keeps whatever sum it was written with.
Related
risk_budget_constraints(rb::EstValType, sets::UniverseSets,
dval::Option{<:Number} = nothing,
key::Option{<:AbstractString} = nothing; strict::Bool = false,
kwargs...)Generate a risk budget allocation from asset/group mappings and asset sets.
This method constructs a RiskBudget from a mapping of asset or group names to risk budget values, using the provided UniverseSets. The mapping can be a dictionary, a single pair, or a vector of pairs. Names are resolved against the universe key selects, and the resulting risk budget vector is normalised to sum to one.
Arguments
rb: A dictionary, pair, or vector of pairs mapping asset or group names to risk budget values.sets: AUniverseSetsobject specifying the universe and groupings.dval: Default value to use for names not found inrb. Ifnothing, a default value of1/length(sets.dict[key])is used.key: Key to specify the universe insets.dictthat names resolve against. Ifnothing, the key is taken fromsets.xkey— or, where the caller is written against another declared axis, from that axis' key.FactorRiskBudgetingpassessets.fkey, because its budget is written in factor names.strict: Iftrue, throws an error if a key inrbis not found insets; iffalse, issues a warning.
Details
- Names and groups in
rbare mapped to indices in the selected universe usingsets. - If a key is a group, all assets in the group are assigned the specified value.
- The resulting vector is normalised to sum to one.
- If
strictistrue, missing keys cause an error; otherwise, a warning is issued.
A scalar rb resolves to a scalar, not to a uniform vector, so the normalisation returns 1.0 whatever the scalar was. The result is a RiskBudget holding one number, and an optimiser accepts it only for a one-asset universe. Write the uniform budget as nothing, which the no-op method turns into 1/N over N entries.
Returns
rb::RiskBudget: A result object containing the normalised risk budget vector.
Examples
julia> sets = UniverseSets(; xkey = "nx", dict = Dict("nx" => ["A", "B", "C"], "group1" => ["A", "B"]));julia> risk_budget_constraints(Dict("A" => 0.2, "group1" => 0.8), sets)RiskBudget val ┴ Vector{Float64}: [0.41379310344827586, 0.41379310344827586, 0.17241379310344826]A budget written in factor names resolves against the declared factor axis, which is what FactorRiskBudgeting passes — the unspecified factors take the 1/length(sets.dict[key]) default before normalisation:
julia> sets = UniverseSets(; dict = Dict("nx" => ["A", "B", "C"], "nf" => ["F1", "F2"]));julia> risk_budget_constraints(Dict("F1" => 0.25), sets, nothing, sets.fkey)RiskBudget val ┴ Vector{Float64}: [0.3333333333333333, 0.6666666666666666]Related
risk_budget_constraints(rb::RiskBudgetEstimator, sets::UniverseSets,
key::Option{<:AbstractString} = nothing; strict::Bool = false,
kwargs...)This method is a wrapper calling:
risk_budget_constraints(rb.val, sets, rb.dval, key; strict = strict)It is used for type stability and to provide a uniform interface for processing constraint estimators, as well as simplifying the use of multiple estimators simulatneously.
Related
References
- [4]
- D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).