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. A budget built by hand keeps the sum it was written with, and still reaches the same weights as its normalised twin, because the barrier row that reads it is homogeneous in the budget.
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. The barrier is positively homogeneous of degree one in $\boldsymbol{b}$, so $c\, \boldsymbol{b}^\intercal \ln(\boldsymbol{y}) \geq 0$ and $\boldsymbol{b}^\intercal \ln(\boldsymbol{y}) \geq 0$ cut the same set for every $c > 0$, and the multiplier $\lambda$ of the risk-contribution identity absorbs the scale.
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) -> RiskBudgetRiskBudget(; val::Num_VecNum) -> RiskBudgetKeywords correspond to the struct's fields. The keyword form forwards to the positional one, so both run the same checks.
Validation
!isempty(val),IsEmptyErrorotherwise.all(x -> zero(x) <= x, val),DomainErrorotherwise. A zero entry is admitted; only a negative one raises.
The inner constructor is typed on Num_VecNum rather than on VecNum, and that width is load-bearing. @concrete emits a generic RiskBudget(val::__T_val) where __T_val, which every argument matches. A VecNum-typed inner method does not apply to a scalar at all, so a scalar would reach the generic method and pass neither check. Num_VecNum applies, and it is more specific than the unbounded generic, so both routes run both checks.
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, because the risk-budgeting builder checks the budget's length against the weight count.
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_constraints: normalises the budget to sum to one on its estimator branch only. The identity branch returns this object untouched, so a hand-built budget keeps its own sum.AbstractConstraintResultAssetRiskBudgetingFactorRiskBudgetingport_opt_view
References
- [5] 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.
This estimator carries no key field. risk_budget_constraints takes the key as a positional argument instead, and resolves a nothing key to sets.xkey inside the verb. ThresholdEstimator is the same shape with the key held as a field. A dval of nothing becomes 1/length of the axis the key selects, so an unnamed entry starts at the uniform share of that axis rather than at zero — on a universe of four assets and two factors, a factor budget naming only F1 fills F2 with 0.5.
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{<:VectorAbstractEstimatorValueAlgorithm}, 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
RiskBudgetrisk_budget_constraintsUniverseSetsThresholdEstimator: the same shape with the universe key held as a field.
References
- [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 10.1.3.
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.
This is the one branch whose budget sums to exactly one. sum of a range reads the arithmetic series rather than the entries, so it lands on exactly 1.0 for any N, where summing the collected entries would accumulate rounding error instead. The estimator branch divides by a computed sum instead, and lands within round-off of one rather than on it.
Algorithm
- Compute
iN, the reciprocal ofN. - Build the constant range of length
Nwhose start and stop are bothiN. - Wrap it in a
RiskBudget, which checks that it is non-empty and non-negative.
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.
It does not normalise, unlike the estimator branch, so a hand-built RiskBudget(; val = [1, 2, 3]) reaches an optimiser summing to 6. That is harmless: the barrier row the optimiser writes is homogeneous in the budget, and the same three-asset variance model returns the same weights for [1, 2, 3] as for [1/6, 2/6, 3/6] to within 3.5e-5, which is solver tolerance.
Algorithm
- Return
rb. The method reads none of its other arguments and none of its keywords.
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]julia> risk_budget_constraints(RiskBudget(; val = [1, 2, 3]))RiskBudget val ┴ Vector{Int64}: [1, 2, 3]Related
risk_budget_constraints(rb::EstValType{<:VectorAbstractEstimatorValueAlgorithm},
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.
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.
The normalisation divides by a computed sum, so the result lands within round-off of one rather than on it. Only the nothing branch sums to exactly one.
Algorithm
- Take
dvalas the fill value. When it isnothing, use1/lengthof the universe thatkeyselects, or ofsets.xkey's universe whenkeyisnothingtoo. - Resolve
rbagainstsetswithestimator_to_val, givingval. A name that names an asset writes one entry, a name that names a group writes one entry per member of the group, and every unnamed entry keeps the fill value of step 1. - Divide
valbysum(val), giving the normalised budget. - Wrap it in a
RiskBudget, which checks that it is non-empty and non-negative.
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.FactorRiskBudgetingpasses the factor axis key of its ownre, because its budget is written in factor names.strict: Iftrue, throws an error if a key inrbis not found insets; iffalse, issues a warning.
Validation
- A name in
rbthat names neither an asset nor a group ofsetsraisesArgumentErrorwhenstrictistrue. A warning is issued otherwise, and the name writes nothing. - A budget whose entries are all zero divides by zero in step 3, and the resulting
NaNvector failsRiskBudget's own non-negativity check, so the call raisesDomainErrorrather than returning aNaNbudget.RiskBudgetadmits a zero entry, and only an all-zero budget reaches this raise.
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(["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.tfkey)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 unpacks a RiskBudgetEstimator and calls the mapping method with its two fields. It is used for type stability and to give a uniform interface for processing constraint estimators.
The key is a positional argument here, not a field of the estimator. ThresholdEstimator holds its key instead, so threshold_constraints needs no such argument. Both routes end at the same estimator_to_val, and a key naming the factor axis resolves on either: on a universe of four assets and two factors, a budget naming only F1 returns a two-entry vector.
There is no vector method. A vector of estimators raises MethodError, and no broader method takes it silently. RkbE_Rkb states the decision that governs the gap.
Algorithm
- Call
risk_budget_constraints(rb.val, sets, rb.dval, key; strict = strict, kwargs...), which resolves the mapping, normalises it and builds theRiskBudget.
Arguments
rb: ARiskBudgetEstimatorcarrying the mapping and the default value.sets: AUniverseSetsobject specifying the universe and groupings.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.FactorRiskBudgetingpasses the factor axis key of its ownre, because its budget is written in factor names.strict: Iftrue, throws an error if a name inrb.valis not found insets; iffalse, issues a warning.kwargs...: Additional keyword arguments forwarded to the mapping method.
Returns
rb::RiskBudget: A result object containing the normalised risk budget vector.
Examples
julia> sets = UniverseSets(; dict = Dict("nx" => ["A", "B", "C", "D"], "nf" => ["F1", "F2"]));julia> risk_budget_constraints(RiskBudgetEstimator(; val = Dict("F1" => 0.4)), sets, sets.tfkey)RiskBudget val ┴ Vector{Float64}: [0.4444444444444445, 0.5555555555555556]Related
References
- [5]
- D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).