Hierarchical Equal Risk Contribution

PortfolioOptimisers.HierarchicalEqualRiskContributionType
struct HierarchicalEqualRiskContribution{__T_opt, __T_ri, __T_ro, __T_scai, __T_scao, __T_ex, __T_fb} <: ClusteringOptimisationEstimator

Splits weight down the dendrogram between clusters by their outer risk ro, then splits each cluster's share between its assets by their inner risk ri.

This is the Hierarchical Equal Risk Contribution algorithm. It differs from HierarchicalRiskParity in two ways: it stops at the optimal number of clusters rather than at the individual assets, and it follows the dendrogram's own branch structure rather than halving a leaf order. The name is the source's; the resulting portfolio is not a risk parity portfolio, and the clusters do not contribute equally to risk.

Mathematical definition

Cut the dendrogram at opt.cle's optimal number of clusters $K$. Every asset starts at $w_i = 1$. The algorithm then runs two independent allocations.

Inside a cluster, each asset takes a share in inverse proportion to its own inner risk:

\[\begin{align} \rho_i(\{j\}) &= \textrm{risk of asset } j \textrm{ held alone under } \rho_i\,,\\ w_j &= \frac{\rho_i(\{j\})^{-1}}{\sum_{l \in C_k} \rho_i(\{l\})^{-1}} \quad \forall\, j \in C_k\,. \end{align}\]

Between clusters, the algorithm walks the $K - 1$ internal nodes of the dendrogram in order of falling height. At each node it splits the weight of everything below that node between its two branches:

\[\begin{align} \tilde{w}_j(C_k) &= \frac{\rho_o(\{j\})^{-1}}{\sum_{l \in C_k} \rho_o(\{l\})^{-1}} \quad \forall\, j \in C_k\,,\\ \tilde{\rho}_o(C_k) &= \rho_o\left(\tilde{\boldsymbol{w}}(C_k)\right)\,,\\ \alpha &= \frac{\sum_{C_k \subseteq B_2} \tilde{\rho}_o(C_k)}{\sum_{C_k \subseteq B_1} \tilde{\rho}_o(C_k) + \sum_{C_k \subseteq B_2} \tilde{\rho}_o(C_k)}\,,\\ \boldsymbol{w}_{B_1} &\leftarrow \alpha \, \boldsymbol{w}_{B_1}\,,\\ \boldsymbol{w}_{B_2} &\leftarrow (1 - \alpha) \, \boldsymbol{w}_{B_2}\,. \end{align}\]

Where:

  • $\rho_i$, $\rho_o$: The inner measure ri and the outer measure ro, resolved by factory.
  • $C_k$: The $k$-th cluster. $K$ of them partition the universe.
  • $B_1$, $B_2$: The left and right branches of the dendrogram node being split. Each is a union of whole clusters.
  • $\tilde{\boldsymbol{w}}(C_k)$: Naive risk parity weights inside cluster $C_k$ under $\rho_o$, zero outside it.
  • $\tilde{\rho}_o(C_k)$: Risk of that sub-portfolio, the cluster's contribution to a branch's risk.
  • $\alpha$: Fraction of the node's weight that goes to $B_1$.
  • $w_j$: Weight of asset $j$. The final vector is normalised to sum to one.

The two allocations multiply: an asset's final weight is its share inside its cluster, times every split factor on the path from the root to that cluster. This is not a flat inverse-risk allocation over the $K$ clusters. A recursion of binary splits agrees with one only when $K = 2$. On a twelve-asset sample that clusters into three, the recursion gives cluster weights $[0.7024, 0.1603, 0.1373]$ where a flat allocation would give $[0.3697, 0.3394, 0.2908]$.

When ri or ro is a vector, herc_scalarised_risk_i! and herc_scalarised_risk_o! combine the measures with scai and scao before the weights are formed.

Fields

  • opt: Base hierarchical optimiser configuration.
  • ri: Inner risk measure.
  • ro: Outer risk measure.
  • scai: Inner scalariser.
  • scao: Outer scalariser.
  • ex: Parallel execution strategy.
  • fb: Fallback result or estimator.

Constructors

HierarchicalEqualRiskContribution(;    opt::HierarchicalOptimiser = HierarchicalOptimiser(),    ri::TD{<:OptRM_VecOptRM} = Variance(),    ro::TD{<:OptRM_VecOptRM} = ri,    scai::TD{<:Scalariser} = SumScalariser(),    scao::TD{<:Scalariser} = scai,    ex::FLoops.Transducers.Executor = FLoops.ThreadedEx(),    fb::TDO_Option{<:OptE_Opt} = nothing) -> HierarchicalEqualRiskContribution

Keywords correspond to the struct's fields. Fields typed TD or TDO_Option may hold a TimeDependent per-fold schedule instead of a static value: the inner/outer risk measures, their scalarisers and the fallback are problem definition, so a cross-validation fold loop resolves them per fold, and a fold-less optimise runs with each at its static default (nothing for fb). The executor ex is execution control and stays static.

Validation

  • If ri or ro is a vector: !isempty(ri) / !isempty(ro).
  • fb schedules: bind !== :nearest.

Propagated parameters

When factory is called on this type, the following @fprop-tagged fields are automatically propagated:

  • opt: Recursively updated via factory.
  • ri: Recursively updated via factory.
  • ro: Recursively updated via factory.
  • fb: Recursively updated via factory.

View parameters

HierarchicalEqualRiskContribution defines its own port_opt_view method rather than deriving one from field tags.

  • The method reads the returns matrix X as its third argument. When opt.pe already holds a prior result, the method replaces X with opt.pe.X, so the children are viewed against the prior's own observations rather than the caller's matrix.
  • ri and ro recurse through port_opt_view with that matrix. opt recurses with the index alone.
  • When ri and ro are the same object, the method views it once and shares the result, so the two fields stay aliased on the subset as they were on the whole universe.
  • scai, scao, ex and fb are carried through unchanged.

Examples

julia> HierarchicalEqualRiskContribution()HierarchicalEqualRiskContribution   opt ┼ HierarchicalOptimiser       │       pe ┼ EmpiricalPrior       │          │           ce ┼ PortfolioOptimisersCovariance       │          │              │   ce ┼ Covariance       │          │              │      │    me ┼ SimpleExpectedReturns       │          │              │      │       │   w ┴ nothing       │          │              │      │    ce ┼ GeneralCovariance       │          │              │      │       │   ce ┼ StatsBase.SimpleCovariance: StatsBase.SimpleCovariance(true)       │          │              │      │       │    w ┴ nothing       │          │              │      │   alg ┼ FullMoment()       │          │              │      │     w ┴ nothing       │          │              │   mp ┼ MatrixProcessing       │          │              │      │     pdm ┼ Posdef       │          │              │      │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton       │          │              │      │         │   kwargs ┴ @NamedTuple{}: NamedTuple()       │          │              │      │      dn ┼ nothing       │          │              │      │      dt ┼ nothing       │          │              │      │     alg ┼ nothing       │          │              │      │   order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)       │          │           me ┼ SimpleExpectedReturns       │          │              │   w ┴ nothing       │          │      horizon ┼ nothing       │          │   fill_limit ┴ nothing       │      cle ┼ ClustersEstimator       │          │    ce ┼ PortfolioOptimisersCovariance       │          │       │   ce ┼ Covariance       │          │       │      │    me ┼ SimpleExpectedReturns       │          │       │      │       │   w ┴ nothing       │          │       │      │    ce ┼ GeneralCovariance       │          │       │      │       │   ce ┼ StatsBase.SimpleCovariance: StatsBase.SimpleCovariance(true)       │          │       │      │       │    w ┴ nothing       │          │       │      │   alg ┼ FullMoment()       │          │       │      │     w ┴ nothing       │          │       │   mp ┼ MatrixProcessing       │          │       │      │     pdm ┼ Posdef       │          │       │      │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton       │          │       │      │         │   kwargs ┴ @NamedTuple{}: NamedTuple()       │          │       │      │      dn ┼ nothing       │          │       │      │      dt ┼ nothing       │          │       │      │     alg ┼ nothing       │          │       │      │   order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)       │          │    de ┼ Distance       │          │       │   power ┼ nothing       │          │       │     alg ┴ CanonicalDistance()       │          │   alg ┼ HClustAlgorithm       │          │       │   linkage ┴ Symbol: :ward       │          │   onc ┼ OptimalNumberClusters       │          │       │   max_k ┼ nothing       │          │       │     alg ┼ SecondOrderDifference       │          │       │         │   alg ┼ StandardisedValue       │          │       │         │       │   mv ┼ MeanValue       │          │       │         │       │      │   w ┴ nothing       │          │       │         │       │   sv ┼ StdValue       │          │       │         │       │      │           w ┼ nothing       │          │       │         │       │      │   corrected ┴ Bool: true       │      slv ┼ nothing       │       wb ┼ WeightBounds       │          │   lb ┼ Float64: 0.0       │          │   ub ┴ Float64: 1.0       │     fees ┼ nothing       │     sets ┼ nothing       │       wf ┼ IterativeWeightFinaliser       │          │   iter ┴ Int64: 100       │      brt ┼ Bool: false       │    x_src ┼ Symbol: :prior       │   strict ┴ Bool: false    ri ┼ Variance       │   settings ┼ RiskMeasureSettings       │            │   scale ┼ Float64: 1.0       │            │      ub ┼ nothing       │            │     rke ┴ Bool: true       │      sigma ┼ nothing       │       chol ┼ nothing       │         rc ┼ nothing       │        alg ┴ SquaredSOCRiskExpr()    ro ┼ Variance       │   settings ┼ RiskMeasureSettings       │            │   scale ┼ Float64: 1.0       │            │      ub ┼ nothing       │            │     rke ┴ Bool: true       │      sigma ┼ nothing       │       chol ┼ nothing       │         rc ┼ nothing       │        alg ┴ SquaredSOCRiskExpr()  scai ┼ SumScalariser()  scao ┼ SumScalariser()    ex ┼ Transducers.ThreadedEx{@NamedTuple{}}: Transducers.ThreadedEx()    fb ┴ nothing

Related

References

  • [122] T. Raffinot. Hierarchical clustering-based asset allocation. The Journal of Portfolio Management 44, 89–99 (2017).
  • [123] T. Raffinot. The hierarchical equal risk contribution portfolio. SSRN Electronic Journal (2018).
  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 12.2.
source
PortfolioOptimisers.factoryMethod
factory(a::Union{Nothing, <:AbstractEstimator, <:AbstractAlgorithm,
                 <:AbstractResult}, args...; kwargs...) -> a
factory(a::AbstractVector{<:Union{Nothing, <:AbstractEstimator, <:AbstractAlgorithm,
                                  <:AbstractResult}}, args...; kwargs...) -> Vector

No-op factory function for constructing objects with a uniform interface.

Defining methods which dispatch on the first argument allows for a consistent factory interface across different types.

factory and port_opt_view are the two propagation mechanisms in this library. They are duals: factory threads runtime values (prior moments, observation weights, previous portfolio weights) down through a composed struct tree; port_opt_view threads an index selection (a subset of assets or observations) down through the same tree.

The vector method is the one forwarding contract for every vector-valued propagation field: it applies factory to each element and forwards args... and kwargs... unchanged, so a family that admits a vector of estimators, algorithms, or results needs no method of its own. A family that needs more than the forward, such as a concrete element type (concrete_typed_array_if_abstract), defines its own more specific method.

Algorithm

The scalar method:

  1. Return a unchanged, and drop args... and kwargs.... This method is the leaf of the recursion, and it is what makes an untagged type safe to call the verb on.

The vector method:

  1. For each element ai of a, call factory on ai, and forward args... and kwargs... unchanged.
  2. Collect the results into a new vector, in the order of a, and return it.

A @propagatable struct with at least one @fprop- or @wprop-tagged field carries a generated method that dominates the scalar method. That method rebuilds the struct with its keyword constructor, sending each @fprop field through factory_child and each @wprop field through _wprop.

Arguments

  • a: Indicates no object should be constructed, or a vector whose elements are rebuilt one by one.
  • args...: Arbitrary positional arguments (ignored by the scalar method, forwarded by the vector method).
  • kwargs...: Arbitrary keyword arguments (ignored by the scalar method, forwarded by the vector method).

Returns

  • a: The input unchanged.
  • v::Vector: The element-wise rebuilds, for the vector method.

Examples

julia> factory(nothing, 1, 2; x = 3)julia> factory(MeanValue())MeanValue  w ┴ nothing

Related

source
factory(
    opt::Union{NonFiniteAllocationOptimisationEstimator, NonFiniteAllocationOptimisationResult},
    _
) -> RandomWeighted{_A, var"#s185", _B, _C, _D, _E, _F, Bool} where {_A, var"#s185"<:AbstractRNG, _B, _C, _D, _E, _F}

Return opt unchanged.

Default pass-through factory for optimisation estimators and results. Overridden for estimators that carry parameters requiring update at each optimisation step.

Related

source
PortfolioOptimisers.port_opt_viewMethod
port_opt_view(
    hec::HierarchicalEqualRiskContribution,
    i,
    X::AbstractMatrix{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}},
    args...
) -> HierarchicalEqualRiskContribution{HierarchicalOptimiser{__T_pe, __T_cle, __T_slv, __T_wb, __T_fees, __T_sets, __T_wf, __T_brt, __T_x_src, __T_strict, __T_cache}, _A, _B, _C, _D, <:Transducers.Executor} where {__T_pe, __T_cle, __T_slv, __T_wb, __T_fees, __T_sets, __T_wf, __T_brt, __T_x_src, __T_strict, __T_cache, _A, _B, _C, _D}

Return a view of HierarchicalEqualRiskContribution hec sliced to asset indices i.

Related

source
PortfolioOptimisers.optimiseMethod
optimise(hec::HierarchicalEqualRiskContribution{
                 <:Any, <:Any, <:Any, <:Any, <:Any, <:Any, Nothing
             },
        rd::ReturnsResult; dims::Int = 1,
        branchorder::Symbol = :optimal, kwargs...) -> HierarchicalEqualRiskContributionResult

Run the Hierarchical Equal Risk Contribution portfolio optimisation.

Arguments

  • hec: The hierarchical equal risk contribution optimiser to use.
  • rd: The returns result to use. If isa(hec.opt.pe, AbstractPriorResult), rd is not necessary if doing a standalone optimisation, but may be required/desired by fallbacks and/or clusterisation.
  • dims: The dimension along which observations advance in time.
  • branchorder: The branch order to use for the clusterisation, this optimisation can use non-optimal branch orders, which make the clustering faster but the dendrogram won't be as nice.
  • kwargs: Additional keyword arguments passed to the optimisation function.

Validation

  • No field in the tree of hec holds an Online. An ArgumentError naming the field is thrown otherwise, through assert_batch_entry: a plain optimise is a batch fit, and a wrapper resolves only at the warm-up of the fold loop's online arm.

Related

source

References

[5]
D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).
[122]
T. Raffinot. Hierarchical clustering-based asset allocation. The Journal of Portfolio Management 44, 89–99 (2017).
[123]
T. Raffinot. The hierarchical equal risk contribution portfolio. SSRN Electronic Journal (2018).