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 exposes the top-level allocation functions and re-exports the
supporting building blocks so the public pyhrp.hrp API is unchanged:
- hrp: Compute HRP portfolio weights from prices
- schur_hrp: Compute Schur Complementary Allocation weights from prices
- build_tree: Build a hierarchical cluster tree (see :mod:pyhrp.dendrogram)
- compute_returns: Simple returns from prices (see :mod:pyhrp.covariance)
- compute_cov / compute_corr: Second-moment estimators (see :mod:pyhrp.covariance)
- Dendrogram: Clustering result container (see :mod:pyhrp.dendrogram)
Dendrogram
dataclass
¶
Container for hierarchical clustering dendrogram data and visualization.
This class stores the results of hierarchical clustering and provides methods for accessing and visualizing the dendrogram structure.
Attributes:
| Name | Type | Description |
|---|---|---|
root |
Cluster
|
The root node of the hierarchical clustering tree |
assets |
list[str]
|
Names of assets included in the clustering |
linkage |
ndarray | None
|
Linkage matrix in scipy format for plotting |
distance |
DataFrame | None
|
Distance matrix used for clustering |
method |
str | None
|
Linkage method used for clustering |
Source code in src/pyhrp/dendrogram.py
ids
property
¶
Node values in the order left -> right as they appear in the dendrogram.
names
property
¶
The asset names as induced by the order of ids.
__post_init__()
¶
Validate dataclass fields after initialization.
Ensures that the optional distance matrix, when provided, is a polars DataFrame with columns aligned to the asset list, and verifies that the number of leaves in the cluster tree matches the number of assets.
Source code in src/pyhrp/dendrogram.py
one_over_n()
¶
Yield the hierarchical 1/N portfolios level by level for this tree.
Container-level convenience wrapper around :func:pyhrp.algos.one_over_n.
Yields:
| Type | Description |
|---|---|
Generator[tuple[int, Portfolio]]
|
tuple[int, Portfolio]: The level number and the equal-weight portfolio |
Generator[tuple[int, Portfolio]]
|
at that level. |
Source code in src/pyhrp/dendrogram.py
plot(**kwargs)
¶
Build and return a plotly dendrogram figure.
Delegates to :func:pyhrp.plot.plot_dendrogram; the plotly dependency
is imported lazily so importing the allocation core stays plotly-free.
Source code in src/pyhrp/dendrogram.py
build_tree(cor, method='ward', bisection=False)
¶
Build hierarchical cluster tree from correlation matrix.
This function converts a correlation matrix to a distance matrix, performs hierarchical clustering, and returns a Dendrogram object containing the resulting tree structure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cor
|
DataFrame
|
Correlation matrix of asset returns (columns are assets) |
required |
method
|
Literal['single', 'complete', 'average', 'ward']
|
Linkage method for hierarchical clustering - "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 |
|---|---|---|
Dendrogram |
Dendrogram
|
Object containing the hierarchical clustering tree, with: - root: Root cluster node - linkage: Linkage matrix for plotting - assets: List of assets - method: Clustering method used - distance: Distance matrix |
Examples:
>>> import polars as pl
>>> from pyhrp.dendrogram import build_tree
>>> cor = pl.DataFrame({"A": [1.0, 0.5], "B": [0.5, 1.0]})
>>> dg = build_tree(cor, method="ward")
>>> dg.root.leaf_count
2
Source code in src/pyhrp/dendrogram.py
check_finite_matrix(matrix, name='matrix')
¶
Raise if a matrix contains NaN or null entries.
A covariance matrix with non-finite entries makes every downstream weight meaningless; this guard fails loudly instead of propagating garbage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matrix
|
DataFrame
|
Square matrix (columns are assets) |
required |
name
|
str
|
Human-readable name used in the error message |
'matrix'
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
pl.DataFrame: The input matrix, unchanged |
Raises:
| Type | Description |
|---|---|
ValueError
|
If any entry of |
Source code in src/pyhrp/covariance.py
compute_corr(df)
¶
Compute correlation matrix from a DataFrame of returns.
compute_cov(df)
¶
Compute covariance matrix from a DataFrame of returns.
compute_returns(prices)
¶
Compute simple returns from prices.
Drops leading all-null rows produced by pct_change and fills remaining nulls/NaNs (e.g. from missing prices) with zero returns.
Warning
Filling with zero is a dirty helper, not a data-cleaning strategy: a
suspended asset or an asset listed mid-sample contributes fabricated
zero returns that bias variance down and correlations toward zero.
A :class:UserWarning is emitted whenever this path fires. Callers
working with messy universes should clean the data first and compose
the pipeline manually::
from pyhrp import build_tree, risk_parity
from pyhrp import compute_corr, compute_cov
returns = prices.select(pl.all().pct_change()).drop_nulls()
root = risk_parity(root=build_tree(compute_corr(returns)).root,
cov=compute_cov(returns))
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prices
|
DataFrame
|
Asset price time series (columns are assets, rows are dates) |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
pl.DataFrame: Simple returns, one row shorter than |
Examples:
>>> import polars as pl
>>> from pyhrp.covariance import compute_returns
>>> prices = pl.DataFrame({"A": [100.0, 110.0, 99.0]})
>>> compute_returns(prices)['A'].to_list()
[0.1, -0.1]
Source code in src/pyhrp/covariance.py
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)
¶
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.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
Cluster
|
The root node of the cluster tree |
required |
cov
|
DataFrame
|
Covariance matrix of asset returns |
required |
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)
¶
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.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
Cluster
|
The root node of the cluster tree |
required |
cov
|
DataFrame
|
Covariance matrix of asset returns |
required |
gamma
|
float
|
Interpolation parameter in [0, 1]. 0 = HRP, 1 = minimum variance. |
0.5
|
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 |
Source code in src/pyhrp/cluster.py
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 |
__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
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 |