# D14-F02-A05 — Hierarchical Equal Risk Contribution

> **Domain:** D14 — Portfolio Construction  
> **Family:** D14-F02 — Risk Allocation  
> **Canonical profile:** fixed cluster count, correlation-profile distance,
> deterministic single linkage, variance risk  
> **Status:** Released source package; visitor validation is recorded separately.

## Executive summary

Hierarchical Equal Risk Contribution (HERC) separates a portfolio decision
into two questions: which assets belong together, and how should capital move
down the resulting tree? It cuts a hierarchy into terminal clusters, builds a
simple inverse-risk allocation within each terminal cluster, then follows the
dendrogram from the root and balances a risk proxy between each pair of
branches.

That structure is the point. HERC is not ordinary equal risk contribution
performed on every asset, and it is not Hierarchical Risk Parity (HRP) with a
different name. The cluster stopping point is explicit, the natural shape of
the dendrogram controls recursion, and “equal risk” applies locally at each
branch split. Final asset component-risk shares need not be equal.

This package freezes one dependency-free profile: the caller supplies the
terminal cluster count, the tree uses correlation-profile distance and
deterministic single linkage, terminal clusters use inverse-variance weights,
and branch risk is variance. Ward linkage, Gap-statistic cluster selection,
downside risk, bounds, turnover and cost models remain named alternatives.

## Why this topic matters

A flat allocation can conceal duplicated exposures. Two banks, two technology
stocks, or two funds may look like four holdings but behave like two risk
groups. A hierarchy makes that structure inspectable. Early stopping then asks
an important practical question: should allocation continue all the way to
individual assets, or should closely related instruments remain one terminal
decision unit?

HERC is useful when an analyst wants a transparent allocation heuristic that
does not invert the full covariance matrix. That does not remove estimation
risk. The covariance, distance, linkage, cluster count and risk measure can all
change the tree or weights. The output is an auditable response to declared
inputs—not proof of future diversification or performance.

## Learning objectives

After completing this topic, a reader can:

- convert covariance into correlation-profile distances and a deterministic
  hierarchy;
- cut that hierarchy at an explicit number of terminal clusters;
- calculate inverse-variance terminal weights and branch variance proxies;
- follow the dendrogram and verify every inverse-risk branch split;
- reconcile final capital weights, portfolio volatility and signed component
  risk;
- distinguish HERC from HRP, flat ERC and risk budgeting; and
- identify bad price, corporate-action, quantity, currency and FX inputs that
  must be resolved before covariance reaches the allocator.

## Prerequisites

- [Inverse-Volatility Weighting](../D14-F02-A01-inverse-volatility-weighting/README.md)
  for normalized reciprocal-risk scores;
- [Equal Risk Contribution](../D14-F02-A02-equal-risk-contribution/README.md)
  for component-risk identities;
- [Risk Budgeting](../D14-F02-A03-risk-budgeting/README.md) for the difference
  between target and realized risk shares; and
- [Hierarchical Risk Parity](../D14-F02-A04-hierarchical-risk-parity/README.md)
  for correlation profiles, single linkage and recursive tree allocation.

## Definitions and terminology

| Term | Meaning in this package |
|---|---|
| Pair profile | Column `i` of `D`, where `D_ij=sqrt((1-rho_ij)/2)` records asset `i` against the full universe. |
| Profile distance | Euclidean distance between two pair-profile columns. |
| Terminal cluster | One group after cutting the hierarchy at the supplied `clusterCount`. Recursion stops at this group. |
| Intra-cluster allocation | Normalized inverse-variance weights inside a terminal cluster. |
| Cluster risk proxy | Variance of a terminal cluster's inverse-variance portfolio. |
| Branch risk | Sum of terminal-cluster variance proxies on one side of a dendrogram split. |
| Branch balance | The equality `alpha_L v_L = alpha_R v_R` created by inverse-risk split factors. |
| Component-risk share | `w_i (Sigma w)_i / (w^T Sigma w)` for a final asset. It is an audit output, not a HERC equality target. |

## Scope and variants

| Design choice | Executable profile | Important alternative |
|---|---|---|
| Risk | Variance | Volatility, CVaR, CDaR and other downside measures |
| Linkage | Single linkage with deterministic ties | Ward, complete or average linkage |
| Cluster count | Required integer supplied by caller | Gap statistic or another selection policy |
| Terminal weights | Inverse variance | Equal weight, inverse volatility or optimized ERC |
| Tree split | Dendrogram-following inverse branch risk | HRP's ordered bisection or flat cluster allocation |
| Constraints | Long-only and fully invested | Bounds, leverage and post-allocation projection |

The [definition contract](./research/DEFINITION-CONTRACT.md) freezes the exact
profile. Official maintained HERC implementations expose different combinations
of these choices, which is why a result should travel with its method metadata
([CRAN HierPortfolios](https://search.r-project.org/CRAN/refmans/HierPortfolios/html/HERC_Portfolio.html),
[skfolio](https://skfolio.org/generated/skfolio.optimization.HierarchicalEqualRiskContribution.html)).

## Input data contract

| Field | Type | Unit | Nullable | Constraint |
|---|---|---|:---:|---|
| `assetIds` | ordered string array | identifier | No | Nonempty, unique, nonblank; order aligns every covariance row/column. |
| `covariance` | `N x N` numeric matrix | return squared for one horizon | No | Finite, exactly symmetric, strictly positive diagonal and strictly positive definite. |
| `clusterCount` | integer | count | No | From 1 through N. |

The covariance must use one observation horizon, timestamp alignment, return
definition, corporate-action basis and currency basis. Missing values,
pairwise-deletion policy, winsorization, shrinkage, annualization and repair
belong to the estimator upstream and must not be silently inferred here.

## Output contract

| Field | Meaning | Invariant |
|---|---|---|
| `weights` | Final capital weights in asset order | Positive and sum to 1 within `1e-12`. |
| `clusters` | Terminal clusters as zero-based asset indices | Exactly `clusterCount` disjoint groups covering all assets. |
| `withinClusterWeights` | Inverse-variance weights within each terminal cluster | Each row is positive and sums to 1. |
| `clusterWeights` | Capital assigned to terminal clusters after all branch splits | Positive and sum to 1. |
| `clusterRiskProxies` | Terminal-cluster variances in input covariance units | Positive and finite. |
| `branchSplits` | Node assets, child clusters, risks, allocations and residual | Each split balances local branch proxy products within `1e-12`. |
| `componentRiskContributions` | `w_i(Sigma w)_i/sigma_p` | Signed values sum to portfolio volatility. |
| `componentRiskShares` | Contribution divided by portfolio volatility | Signed values sum to 1. |
| `portfolioVariance`, `portfolioVolatility` | Risk on the input covariance scale | Positive and finite. |
| Method fields | Linkage, distance, allocation and variant labels | Match the frozen definition. |

## Mathematical formulation

First convert covariance to correlation and build the pair-profile matrix:

$$
\rho_{ij}=\frac{\Sigma_{ij}}{\sqrt{\Sigma_{ii}\Sigma_{jj}}},
\qquad D_{ij}=\sqrt{\frac{1-\rho_{ij}}{2}}.
$$

Clustering uses the full relationship profile, not only the direct `i,j`
entry:

$$
d(i,j)=\left\lVert D_{\cdot i}-D_{\cdot j}\right\rVert_2.
$$

After single linkage, cut the hierarchy at `K` terminal clusters. Within a
terminal cluster `C`, use normalized inverse variances:

$$
u_i^{(C)}=\frac{1/\Sigma_{ii}}{\sum_{j\in C}1/\Sigma_{jj}},
\qquad
v_C={u^{(C)}}^\mathsf{T}\Sigma_Cu^{(C)}.
$$

At one natural dendrogram split, collect the terminal clusters below the left
and right children. Let their aggregate proxies be

$$
v_L=\sum_{C\in L}v_C,
\qquad
v_R=\sum_{C\in R}v_C.
$$

Allocate the parent capital inversely to those risks:

$$
\alpha_L=\frac{v_R}{v_L+v_R},
\qquad
\alpha_R=\frac{v_L}{v_L+v_R}.
$$

This makes `alpha_L v_L = alpha_R v_R` at that split. Recurse until each
terminal cluster is reached, then multiply its capital by its internal
inverse-variance weights.

## Algorithm

1. Validate identifiers, covariance and cluster count without mutating inputs.
2. Scale covariance by its largest absolute entry for stable factorization.
3. Compute correlations, pair profiles and profile distances.
4. Run single linkage; break exact ties lexicographically for reproducibility.
5. Replay the first `N-K` merges to obtain `K` terminal clusters.
6. Calculate inverse-variance weights and variance proxy for every terminal
   cluster.
7. Walk the dendrogram from the root. Stop when a selected terminal cluster is
   reached; otherwise balance left/right proxy risk and recurse.
8. Combine cluster capital with terminal weights, restore covariance scale and
   calculate component-risk diagnostics.

```text
validate(assetIds, covariance, K)
D <- sqrt((1 - correlation(covariance)) / 2)
distance <- Euclidean distance between columns of D
tree <- deterministic_single_linkage(distance)
clusters <- cut_tree(tree, K)

for cluster C:
    u[C] <- normalize(1 / diagonal(covariance[C,C]))
    v[C] <- transpose(u[C]) * covariance[C,C] * u[C]

walk(tree.root):
    if node equals a terminal cluster: stop
    vLeft  <- sum(v[C] for terminal clusters below node.left)
    vRight <- sum(v[C] for terminal clusters below node.right)
    scale left descendants by vRight / (vLeft + vRight)
    scale right descendants by vLeft / (vLeft + vRight)
    walk(node.left); walk(node.right)

weight[i in C] <- clusterWeight[C] * u[C][i]
return weights, tree, clusters, branch audit, portfolio risk
```

## Worked synthetic example

Use four labeled assets and one common-horizon synthetic covariance:

$$
\Sigma=\begin{bmatrix}
0.04&0.036&0.004&0.002\\
0.036&0.04&0.003&0.001\\
0.004&0.003&0.01&0.008\\
0.002&0.001&0.008&0.0225
\end{bmatrix}.
$$

Single linkage merges `(A,B)` and `(C,D)`. Set `clusterCount=3`, so the
terminal clusters are `(A,B)`, `C`, and `D`. Their inverse-variance variance
proxies are `0.038000`, `0.010000`, and `0.022500`.

At the root, left risk is `0.038` and right risk is `0.0325`, producing
`46.0993% / 53.9007%`. The right branch then splits `C / D` by
`69.2308% / 30.7692%`. The final result is:

| Asset | Terminal cluster | Final capital weight | Final component-risk share |
|---|---|---:|---:|
| A | A/B | 23.0496% | 35.6359% |
| B | A/B | 23.0496% | 34.6428% |
| C | C | 37.3159% | 19.9003% |
| D | D | 16.5848% | 9.8209% |

Portfolio volatility is `0.111850951366`. The final risk shares are not equal;
what balances is each local branch proxy product. Full precision and the
independent arithmetic are in [CANONICAL-EXAMPLE.md](./CANONICAL-EXAMPLE.md).

![How a three-cluster HERC cut becomes final weights](./visuals/static/herc-three-cluster-allocation.svg)

The SVG shows the early stopping decision and both risk-balanced splits. The
[guided HERC lab](./visuals/animated/playground.html) lets the reader compare
one through four terminal clusters and inspect a rejected covariance.

```mermaid
flowchart LR
    A[Validated covariance and K] --> B[Correlation profiles]
    B --> C[Deterministic single-linkage tree]
    C --> D[Cut tree at K terminal clusters]
    D --> E[Inverse-variance weights and variance per cluster]
    E --> F[Follow natural dendrogram branches]
    F --> G[Balance variance proxies at every split]
    G --> H[Asset weights and component-risk audit]
```

## What changes when the cluster count changes?

The same covariance produces four intentionally different states:

| K | Terminal interpretation | Weight vector |
|---:|---|---|
| 1 | One terminal cluster; only its internal inverse-variance allocation remains | `(12.8571%, 12.8571%, 51.4286%, 22.8571%)` |
| 2 | Two terminal pairs; for this balanced tree the result coincides with the related HRP example | `(10.6881%, 10.6881%, 54.4319%, 24.1920%)` |
| 3 | A/B stops together while C and D split separately | `(23.0496%, 23.0496%, 37.3159%, 16.5848%)` |
| 4 | Every asset is terminal, but allocation still follows natural tree branches | `(14.4444%, 14.4444%, 49.2308%, 21.8803%)` |

These are not four claims about which portfolio will perform better. They show
that the stopping rule is part of the algorithm, not harmless UI decoration.

## Evidence and historical-example decision

The historical-example decision is **not useful** for the core calculation.
A named market portfolio would require an exact rebalancing date, licensed
point-in-time returns, adjustment and FX bases, covariance estimator, cluster
selection rule and knowledge cutoff. A clean synthetic matrix makes the
method, the branch arithmetic and the failure modes reproducible.

The operational boundary still matters. Islam Baraka has encountered old
portfolio marks that were impossible after a split, cash dividends that made a
holding appear to lose wealth, questionable price/quantity combinations,
changing units and portfolios spanning Egypt, Japan, the United States and
Saudi Arabia. Those incidents belong **before** HERC:

- reconcile splits and cash distributions before calculating total-return
  histories;
- validate that an imported mark was possible for the instrument and date;
- keep quantity, local price, FX and base-currency market value distinct; and
- align asynchronous markets before interpreting cross-market covariance.

A hierarchy cannot repair those errors. It can instead make a corrupted
correlation pattern look structurally convincing. The fixture therefore
contains metadata-only regression boundaries forbidding the core from
repairing prices, treating missing quotes as zero, or mixing return currencies.

## Implementation guide

The reference functions are
`implementations/python/hierarchical_equal_risk_contribution.py` and
`implementations/typescript/hierarchical-equal-risk-contribution.ts`. Both are
pure, dependency-free and return the same structures and reason codes. The
straightforward clustering scan is `O(N^3)` and matrix storage is `O(N^2)`;
production universes may use optimized clustering while preserving the same
contract.

Run the examples:

```powershell
python examples/python_example.py
npx tsx examples/typescript_example.ts
```

## Testing and validation

- Four shared success cases cover every explicit cluster count from 1 to 4.
- Nine shared invalid cases cover empty/duplicate IDs, invalid cluster counts,
  singular/asymmetric/negative/non-numeric covariance.
- Three metadata-only cases preserve upstream split/dividend, stale-price and
  quantity/FX boundaries.
- Python tests independently reconstruct the canonical terminal variances and
  branch weights without calling implementation helpers.
- TypeScript tests launch the Python JSON-lines CLI and compare 13 success/error
  outcomes directly.
- Invariants cover weight and risk sums, branch balance, nonmutation,
  permutation identity and extreme covariance scales.

## Edge cases and failure modes

| Case | Behavior | Why |
|---|---|---|
| `K=1` | Return inverse-variance weights for the one terminal cluster | No inter-cluster branch is active. |
| `K=N` | Follow every natural dendrogram branch to singleton terminals | This is not automatically global ERC or HRP ordered bisection. |
| Exact linkage tie | Use lexicographic cluster order | Reproducible input-order convention. |
| Singular covariance | Reject `invalid-covariance` | Frozen core requires strict positive definiteness. |
| Tiny positive variances | Normalize scale and avoid avoidable products | Preserve representable values without silently flooring risk. |
| Negative asset component contribution | Preserve its sign | Diversification can make marginal contribution negative. |
| Bad or mixed-basis returns | Reject upstream; do not “fix” in HERC | A valid matrix shape does not prove valid economic evidence. |

HERC remains sensitive to the covariance sample, distance metric, linkage,
cluster count and risk measure. Single linkage can chain. An explicit `K` can
be chosen badly. There are no weight bounds, trading costs, taxes or turnover
controls here. Those limitations should be evaluated before using the output
as a rebalance target.

## Related catalog topics

- [Inverse-Volatility Weighting](../D14-F02-A01-inverse-volatility-weighting/README.md): reciprocal volatility without hierarchy.
- [Equal Risk Contribution](../D14-F02-A02-equal-risk-contribution/README.md): global asset-level risk equalization.
- [Risk Budgeting](../D14-F02-A03-risk-budgeting/README.md): explicit target risk shares.
- [Hierarchical Risk Parity](../D14-F02-A04-hierarchical-risk-parity/README.md): quasi-diagonal ordered bisection without an explicit terminal-cluster cut.
- D14-F03-A01 — Black–Litterman: the next catalog family introduces views and Bayesian allocation rather than another risk-only hierarchy.

## References

See [REFERENCES.md](./REFERENCES.md) and the per-claim roles in
[CLAIM-LEDGER.md](./research/CLAIM-LEDGER.md).
