API Reference¶
The public API of pyhrp. Import the main entry points directly from the top-level package:
from pyhrp.hrp import hrp, build_tree, Dendrogram
from pyhrp.algos import risk_parity, one_over_n
from pyhrp.cluster import Cluster, Portfolio
Hierarchical Risk Parity (HRP) allocation entry points.
This module owns the top-level allocation functions: - hrp: Compute HRP portfolio weights from prices - schur_hrp: Compute Schur Complementary Allocation weights from prices
For backward compatibility the supporting building blocks stay importable from
pyhrp.hrp, but they are owned (and exported) by the modules that define them:
- build_tree, Dendrogram: see :mod:pyhrp.dendrogram
- compute_returns, compute_cov, compute_corr: see :mod:pyhrp.covariance
hrp(prices, node=None, method='ward', bisection=False)
¶
Compute the hierarchical risk parity portfolio weights.
This is the main entry point for the HRP algorithm. It calculates returns from prices, builds a hierarchical clustering tree if not provided, and applies risk parity weights.
Note
A node passed in is weighted in place and returned; see the allocator
contract in :mod:pyhrp.algos. Passing dendrogram.root therefore
rewrites the portfolios inside that Dendrogram, even though the
Dendrogram itself is a frozen dataclass.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prices
|
DataFrame
|
Asset price time series (columns are assets, rows are dates) |
required |
node
|
Cluster
|
Root node of the hierarchical clustering tree. If None, a tree will be built from the correlation matrix. |
None
|
method
|
Literal['single', 'complete', 'average', 'ward']
|
Linkage method to use for distance calculation - "single": minimum distance between points (nearest neighbor) - "complete": maximum distance between points (furthest neighbor) - "average": average distance between all points - "ward": Ward variance minimization |
'ward'
|
bisection
|
bool
|
Whether to use bisection method for tree construction |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
Cluster |
Cluster
|
The root cluster with portfolio weights assigned according to HRP |
Source code in src/pyhrp/hrp.py
schur_hrp(prices, node=None, method='ward', bisection=False, gamma=0.5)
¶
Compute Schur Complementary Allocation portfolio weights.
Extends HRP by augmenting each sub-covariance block with off-diagonal information via Schur complements before splitting risk between clusters. Introduced by Peter Cotton (arXiv:2411.05807). At gamma=0 this is identical to HRP; at gamma=1 it recovers the global minimum-variance portfolio through the same recursive hierarchy.
Note
A node passed in is weighted in place and returned; see the allocator
contract in :mod:pyhrp.algos. Passing dendrogram.root therefore
rewrites the portfolios inside that Dendrogram, even though the
Dendrogram itself is a frozen dataclass.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prices
|
DataFrame
|
Asset price time series (columns are assets, rows are dates) |
required |
node
|
Cluster
|
Root node of the hierarchical clustering tree. If None, a tree will be built from the correlation matrix. |
None
|
method
|
Literal['single', 'complete', 'average', 'ward']
|
Linkage method for clustering |
'ward'
|
bisection
|
bool
|
Whether to use bisection method for tree construction |
False
|
gamma
|
float
|
Schur interpolation parameter in [0, 1]. 0 recovers standard HRP; 1 recovers minimum-variance portfolio. |
0.5
|
Returns:
| Name | Type | Description |
|---|---|---|
Cluster |
Cluster
|
The root cluster with portfolio weights assigned |
Source code in src/pyhrp/hrp.py
Portfolio optimization algorithms for hierarchical risk parity.
This module implements various portfolio optimization algorithms: - risk_parity: The main hierarchical risk parity algorithm - schur_risk_parity: Schur Complementary Allocation (Cotton, arXiv:2411.05807) - one_over_n: A simple equal-weight allocation strategy
Allocator contract¶
All three allocators take the same inputs — a Cluster tree (root) plus
the asset names — and none of them changes the shape of that tree: no node is
added, removed or re-parented. They differ in where the weights land.
risk_parity and schur_risk_parity share the recursive _allocate_with
scaffolding and write into the tree they are given: every node's
portfolio is replaced rather than accumulated into, and the same root object
is returned. Replacing makes them idempotent — re-running on an already weighted
tree, with a different covariance matrix or gamma, gives the same answer as
running on a fresh one — but the caller's tree is modified. Note in particular
that Dendrogram is a frozen dataclass while the Cluster it holds is not:
passing dendrogram.root to either allocator rewrites the portfolios inside
that dendrogram. Pass copy.deepcopy(dendrogram.root) to keep the original
tree unweighted.
one_over_n differs on both counts. It accumulates into a local buffer, so it
leaves the input tree untouched entirely, and its output is a generator
yielding the equal-weight portfolio one tree level at a time (see its
docstring), because its purpose is to expose the allocation as it deepens rather
than a single final result.
one_over_n(root, assets)
¶
Generate 1/N (equal-weight) portfolios one tree level at a time.
This implements a hierarchical 1/N strategy where weights are distributed equally among the leaves of each cluster, and the weight budget halves at each successive level of the tree.
Unlike :func:risk_parity and :func:schur_risk_parity — which rebuild a
single final allocation and return the root Cluster — this allocator is
intentionally a generator: its purpose is to expose the equal-weight
allocation as the tree deepens, yielding one portfolio per level. It shares
the sibling input contract (a Cluster tree plus the asset names) and, like
them, does not mutate the tree: weights accumulate in a local buffer, so a
leaf that terminates at a shallow level keeps its weight in the deeper levels
(each yielded portfolio is therefore a complete allocation over all assets),
and re-running on the same tree yields an identical sequence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
Cluster
|
The root node of the cluster tree. |
required |
assets
|
list[str]
|
Asset names; a leaf's value indexes into this list. |
required |
Yields:
| Type | Description |
|---|---|
Generator[tuple[int, Portfolio]]
|
tuple[int, Portfolio]: The level number and the (cumulative) equal-weight |
Generator[tuple[int, Portfolio]]
|
portfolio at that level. |
Examples:
>>> import polars as pl
>>> from pyhrp.hrp import build_tree
>>> from pyhrp.algos import one_over_n
>>> cor = pl.DataFrame({"A": [1.0, 0.3], "B": [0.3, 1.0]})
>>> dg = build_tree(cor, method="ward")
>>> levels = list(one_over_n(dg.root, dg.assets))
>>> len(levels) > 0
True
Source code in src/pyhrp/algos.py
risk_parity(root, cov, assets=None)
¶
Compute hierarchical risk parity weights for a cluster tree.
This is the main algorithm for hierarchical risk parity. It recursively traverses the cluster tree and assigns weights to each node based on the risk parity principle.
Note
The tree is modified in place: the portfolio of every node is rebuilt
from scratch and root itself is returned, so the function is
idempotent and a tree can be reused with a different covariance matrix.
Deep-copy the tree first if you need to keep it unweighted.
The covariance is reached only through block products, one per child of
every node, so a factor or Gram :class:~pyhrp.operators.CovarianceOperator
allocates without forming the n x n matrix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
Cluster
|
The root node of the cluster tree |
required |
cov
|
DataFrame | CovarianceOperator
|
Covariance matrix of asset returns, as a DataFrame or as an operator |
required |
assets
|
Sequence[str]
|
Asset names for an operator |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
Cluster |
Cluster
|
The same root node, with portfolio weights assigned |
Examples:
>>> import polars as pl
>>> from pyhrp.cluster import Cluster
>>> from pyhrp.algos import risk_parity
>>> cov = pl.DataFrame({"A": [4.0, 0.0], "B": [0.0, 1.0]})
>>> root = Cluster(2, left=Cluster(0), right=Cluster(1))
>>> cluster = risk_parity(root=root, cov=cov)
>>> round(cluster.portfolio["B"], 1)
0.8
Source code in src/pyhrp/algos.py
schur_risk_parity(root, cov, gamma=0.5, assets=None)
¶
Compute Schur Complementary Allocation weights for a cluster tree.
An extension of HRP introduced by Peter Cotton (arXiv:2411.05807) that augments sub-covariance matrices with off-diagonal block information via Schur complements. At gamma=0 this recovers standard HRP; at gamma=1 it recovers the minimum-variance portfolio through the same recursive structure.
Note
The tree is modified in place: the portfolio of every node is rebuilt
from scratch and root itself is returned, so the function is
idempotent and a tree can be reused with a different covariance matrix
or gamma. Deep-copy the tree first if you need to keep it unweighted.
The Schur terms need, per node, the two cross-block products
Sigma[L, R] w_R and Sigma[R, L] w_L and one solve against each
child's principal block, which a factor operator does by Woodbury in
O(|C| k**2) rather than O(|C|**3). The block quadratic forms
B D^{-1} B^T are never formed: only their action on the child weights is.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
Cluster
|
The root node of the cluster tree |
required |
cov
|
DataFrame | CovarianceOperator
|
Covariance matrix of asset returns, as a DataFrame or as an operator |
required |
gamma
|
float
|
Interpolation parameter in [0, 1]. 0 = HRP, 1 = minimum variance. |
0.5
|
assets
|
Sequence[str]
|
Asset names for an operator |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
Cluster |
Cluster
|
The same root node, with portfolio weights assigned |
Raises:
| Type | Description |
|---|---|
ValueError
|
If gamma is outside the interval [0, 1]. |
Examples:
>>> import polars as pl
>>> from pyhrp.cluster import Cluster
>>> from pyhrp.algos import schur_risk_parity
>>> cov = pl.DataFrame({"A": [4.0, 0.0], "B": [0.0, 1.0]})
>>> root = Cluster(2, left=Cluster(0), right=Cluster(1))
>>> cluster = schur_risk_parity(root=root, cov=cov, gamma=0.5)
>>> round(cluster.portfolio["B"], 1)
0.8
Source code in src/pyhrp/algos.py
Data structures for hierarchical risk parity portfolio optimization.
This module defines the core data structures used in the hierarchical risk parity algorithm: - Portfolio: Manages a collection of asset weights (strings identify assets) - Cluster: Represents a node in the hierarchical clustering tree
Cluster
¶
Bases: Node[int]
Represents a cluster in the hierarchical clustering tree.
Clusters are the nodes of the graphs we build. Each cluster is aware of the left and the right cluster it is connecting to. Each cluster also has an associated portfolio.
Attributes:
| Name | Type | Description |
|---|---|---|
portfolio |
Portfolio
|
The portfolio associated with this cluster |
After an allocation a node does not hold its portfolio, only its split: the share of its weight that goes to the left child. The portfolio is built from the splits below the node when it is first read, and then kept. Holding every node's portfolio would cost the sum of all cluster sizes, which is quadratic in the number of assets for the chain-like trees single linkage builds.
Source code in src/pyhrp/cluster.py
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 | |
leaves
property
¶
Get all reachable leaf nodes in left-to-right dendrogram order.
Returns:
| Type | Description |
|---|---|
list[Cluster]
|
list[Cluster]: List of all leaf nodes reachable from this cluster |
portfolio
property
writable
¶
The portfolio of this cluster, built from the allocation's splits on first access.
__init__(value, left=None, right=None)
¶
Initialize a new Cluster.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
int
|
The identifier for this cluster |
required |
left
|
Cluster
|
The left child cluster |
None
|
right
|
Cluster
|
The right child cluster |
None
|
Source code in src/pyhrp/cluster.py
Portfolio
dataclass
¶
Container for portfolio asset weights.
This lightweight class stores and manipulates a mapping from asset names to their portfolio weights, and provides convenience helpers for analysis and visualization.
Attributes:
| Name | Type | Description |
|---|---|---|
_weights |
dict[str, float]
|
Internal mapping from asset symbol to weight. |
Source code in src/pyhrp/cluster.py
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | |
assets
property
¶
List of asset names present in the portfolio.
Returns:
| Type | Description |
|---|---|
list[str]
|
list[str]: Asset identifiers in insertion order (Python 3.7+ dict order). |
weights
property
¶
Get all weights as a dict sorted alphabetically by asset name.
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
dict[str, float]: Mapping from asset name to weight, sorted by name. |
__getitem__(item)
¶
Return the weight for a given asset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
item
|
str
|
Asset name/symbol. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
The weight associated with the asset. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If the asset is not present in the portfolio. |
Source code in src/pyhrp/cluster.py
__setitem__(key, value)
¶
Set or update the weight for an asset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
Asset name/symbol. |
required |
value
|
float
|
Portfolio weight for the asset. |
required |
plot(names)
¶
Plot the portfolio weights as a bar chart.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
names
|
list[str]
|
List of asset names to include in the plot |
required |
Returns:
| Type | Description |
|---|---|
Figure
|
go.Figure: The plotly figure |
Note
The plotly dependency is imported lazily so importing the allocation core (which pulls in this module) stays plotly-free.
Source code in src/pyhrp/cluster.py
variance(cov)
¶
Calculate the variance of the portfolio.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cov
|
DataFrame
|
Covariance matrix where columns and rows correspond to assets in the same order as columns list. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
Portfolio variance |
Source code in src/pyhrp/cluster.py
Plotly visualization for hierarchical clustering dendrograms.
Kept separate from the allocation core (hrp.py) so that the optional
plotting dependency (plotly) and the visualization logic do not couple the
algorithm modules. Dendrogram.plot delegates here.
plot_dendrogram(dendrogram, **kwargs)
¶
Build and return a plotly dendrogram figure for a Dendrogram.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dendrogram
|
Dendrogram
|
Clustering result carrying the linkage matrix and asset labels to render. |
required |
**kwargs
|
object
|
Extra keyword arguments forwarded to
|
{}
|
Returns:
| Type | Description |
|---|---|
Figure
|
go.Figure: A plotly figure drawing the dendrogram as line traces. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the dendrogram has no linkage matrix to plot. |
Examples:
>>> import polars as pl
>>> from pyhrp.hrp import build_tree
>>> from pyhrp.plot import plot_dendrogram
>>> cor = pl.DataFrame({"A": [1.0, 0.5], "B": [0.5, 1.0]})
>>> fig = plot_dendrogram(build_tree(cor, method="ward"))
>>> type(fig).__name__
'Figure'
Source code in src/pyhrp/plot.py
A lightweight binary tree implementation to replace the binarytree dependency.
This module provides a simple Node class that can be used to create binary trees. It implements only the functionality needed by the pyhrp package.
Node
¶
Bases: Generic[T]
A binary tree node with left and right children.
This class implements the minimal functionality needed from the binarytree.Node class that is used in the pyhrp package.
Attributes:
| Name | Type | Description |
|---|---|---|
value |
The value of the node |
|
left |
The left child node |
|
right |
The right child node |
Source code in src/pyhrp/treelib.py
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | |
is_leaf
property
¶
Check if this node is a leaf node (has no children).
Returns:
| Name | Type | Description |
|---|---|---|
bool |
bool
|
True if this is a leaf node, False otherwise |
leaf_count
property
¶
Count the number of leaf nodes in the tree.
Returns:
| Name | Type | Description |
|---|---|---|
int |
int
|
Number of leaf nodes |
leaves
property
¶
Get all leaf nodes in the tree rooted at this node.
Returns:
| Type | Description |
|---|---|
Sequence[Node[T]]
|
List[Node]: List of all leaf nodes |
levels
property
¶
Get nodes by level in the tree.
Returns:
| Type | Description |
|---|---|
list[list[Node[T]]]
|
List[List[Node]]: List of lists of nodes at each level |
size
property
¶
Count the total number of nodes in the tree.
Returns:
| Name | Type | Description |
|---|---|---|
int |
int
|
Total number of nodes |
__init__(value, left=None, right=None)
¶
__iter__()
¶
Iterate through all nodes in the tree in level-order.
Returns:
| Type | Description |
|---|---|
Iterator[Node[T]]
|
Iterator[Node]: Iterator over all nodes |