Risk budget estimators

PortfolioOptimisers.RiskBudgetType
struct RiskBudget{__T_val} <: AbstractConstraintResult

Carries 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 val field.
  • $\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_constraints normalises 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) -> RiskBudget

Keywords correspond to the struct's fields. The keyword form forwards to the positional one, so both run the same checks.

Validation

  • !isempty(val), IsEmptyError otherwise.
  • all(x -> zero(x) <= x, val), DomainError otherwise. 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:

Examples

julia> RiskBudget(; val = [0.2, 0.3, 0.5])RiskBudget  val ┴ Vector{Float64}: [0.2, 0.3, 0.5]

Related

References

  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 10.1.3.
source
PortfolioOptimisers.RiskBudgetEstimatorType
struct RiskBudgetEstimator{__T_val, __T_dval} <: AbstractConstraintEstimator

Resolves 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 to RiskBudget(1.0) whatever the scalar was, so only a one-entry axis can consume it. Write the uniform budget as nothing.
  • dval: Default value for assets not specified in val.

Constructors

RiskBudgetEstimator(;    val::EstValType{<:VectorAbstractEstimatorValueAlgorithm},    dval::Option{<:Number} = nothing) -> RiskBudgetEstimator

Keywords correspond to the struct's fields.

Validation

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 ┴ nothing

Two 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

  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 10.1.3.
source
PortfolioOptimisers.risk_budget_constraintsFunction
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

  1. Compute iN, the reciprocal of N.
  2. Build the constant range of length N whose start and stop are both iN.
  3. 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 length N, with each entry equal to 1/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

source
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

  1. Return rb. The method reads none of its other arguments and none of its keywords.

Arguments

  • rb: An existing RiskBudget object.
  • args...: Additional positional arguments (ignored).
  • kwargs...: Additional keyword arguments (ignored).

Returns

  • rb::RiskBudget: The input RiskBudget object, 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

source
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

  1. Take dval as the fill value. When it is nothing, use 1/length of the universe that key selects, or of sets.xkey's universe when key is nothing too.
  2. Resolve rb against sets with estimator_to_val, giving val. 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.
  3. Divide val by sum(val), giving the normalised budget.
  4. 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: A UniverseSets object specifying the universe and groupings.
  • dval: Default value to use for names not found in rb. If nothing, a default value of 1/length(sets.dict[key]) is used.
  • key: Key to specify the universe in sets.dict that names resolve against. If nothing, the key is taken from sets.xkey — or, where the caller is written against another declared axis, from that axis' key. FactorRiskBudgeting passes the factor axis key of its own re, because its budget is written in factor names.
  • strict: If true, throws an error if a key in rb is not found in sets; if false, issues a warning.

Validation

  • A name in rb that names neither an asset nor a group of sets raises ArgumentError when strict is true. 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 NaN vector fails RiskBudget's own non-negativity check, so the call raises DomainError rather than returning a NaN budget. RiskBudget admits 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

source
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

  1. Call risk_budget_constraints(rb.val, sets, rb.dval, key; strict = strict, kwargs...), which resolves the mapping, normalises it and builds the RiskBudget.

Arguments

  • rb: A RiskBudgetEstimator carrying the mapping and the default value.
  • sets: A UniverseSets object specifying the universe and groupings.
  • key: Key to specify the universe in sets.dict that names resolve against. If nothing, the key is taken from sets.xkey — or, where the caller is written against another declared axis, from that axis' key. FactorRiskBudgeting passes the factor axis key of its own re, because its budget is written in factor names.
  • strict: If true, throws an error if a name in rb.val is not found in sets; if false, 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

source

References

[5]
D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).