Coverage for src/pyhrp/hrp.py: 100%
20 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-30 04:15 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-30 04:15 +0000
1"""Hierarchical Risk Parity (HRP) allocation entry points.
3This module exposes the top-level allocation functions and re-exports the
4supporting building blocks so the public ``pyhrp.hrp`` API is unchanged:
5- hrp: Compute HRP portfolio weights from prices
6- schur_hrp: Compute Schur Complementary Allocation weights from prices
7- build_tree: Build a hierarchical cluster tree (see :mod:`pyhrp.dendrogram`)
8- compute_cov / compute_corr: Second-moment estimators (see :mod:`pyhrp.covariance`)
9- Dendrogram: Clustering result container (see :mod:`pyhrp.dendrogram`)
10"""
12from __future__ import annotations
14from typing import Literal
16import polars as pl
18from .algos import risk_parity, schur_risk_parity
19from .cluster import Cluster
20from .covariance import _returns, compute_corr, compute_cov
21from .dendrogram import Dendrogram, build_tree
23__all__ = ["Dendrogram", "build_tree", "compute_corr", "compute_cov", "hrp", "schur_hrp"]
26def hrp(
27 prices: pl.DataFrame,
28 node: Cluster | None = None,
29 method: Literal["single", "complete", "average", "ward"] = "ward",
30 bisection: bool = False,
31) -> Cluster:
32 """Compute the hierarchical risk parity portfolio weights.
34 This is the main entry point for the HRP algorithm. It calculates returns from prices,
35 builds a hierarchical clustering tree if not provided, and applies risk parity weights.
37 Args:
38 prices (pl.DataFrame): Asset price time series (columns are assets, rows are dates)
39 node (Cluster, optional): Root node of the hierarchical clustering tree.
40 If None, a tree will be built from the correlation matrix.
41 method (Literal["single", "complete", "average", "ward"]): Linkage method to use for distance calculation
42 - "single": minimum distance between points (nearest neighbor)
43 - "complete": maximum distance between points (furthest neighbor)
44 - "average": average distance between all points
45 - "ward": Ward variance minimization
46 bisection (bool): Whether to use bisection method for tree construction
48 Returns:
49 Cluster: The root cluster with portfolio weights assigned according to HRP
51 Examples:
52 >>> import polars as pl
53 >>> from pyhrp.hrp import hrp
54 >>> prices = pl.DataFrame({"A": [100.0, 101.0, 99.0, 102.0], "B": [50.0, 51.0, 49.0, 52.0]})
55 >>> root = hrp(prices, method="ward")
56 >>> round(sum(root.portfolio.weights.values()), 6)
57 1.0
58 """
59 returns = _returns(prices)
60 cov = compute_cov(returns)
61 cor = compute_corr(returns)
62 node = node or build_tree(cor, method=method, bisection=bisection).root
64 return risk_parity(root=node, cov=cov)
67def schur_hrp(
68 prices: pl.DataFrame,
69 node: Cluster | None = None,
70 method: Literal["single", "complete", "average", "ward"] = "ward",
71 bisection: bool = False,
72 gamma: float = 0.5,
73) -> Cluster:
74 """Compute Schur Complementary Allocation portfolio weights.
76 Extends HRP by augmenting each sub-covariance block with off-diagonal information
77 via Schur complements before splitting risk between clusters. Introduced by Peter Cotton
78 (arXiv:2411.05807). At gamma=0 this is identical to HRP; at gamma=1 it recovers the
79 global minimum-variance portfolio through the same recursive hierarchy.
81 Args:
82 prices (pl.DataFrame): Asset price time series (columns are assets, rows are dates)
83 node (Cluster, optional): Root node of the hierarchical clustering tree.
84 If None, a tree will be built from the correlation matrix.
85 method (Literal["single", "complete", "average", "ward"]): Linkage method for clustering
86 bisection (bool): Whether to use bisection method for tree construction
87 gamma (float): Schur interpolation parameter in [0, 1].
88 0 recovers standard HRP; 1 recovers minimum-variance portfolio.
90 Returns:
91 Cluster: The root cluster with portfolio weights assigned
93 Examples:
94 >>> import polars as pl
95 >>> from pyhrp.hrp import schur_hrp
96 >>> prices = pl.DataFrame({"A": [100.0, 101.0, 99.0, 102.0], "B": [50.0, 51.0, 49.0, 52.0]})
97 >>> root = schur_hrp(prices, method="ward", gamma=0.5)
98 >>> round(sum(root.portfolio.weights.values()), 6)
99 1.0
100 """
101 returns = _returns(prices)
102 cov = compute_cov(returns)
103 cor = compute_corr(returns)
104 node = node or build_tree(cor, method=method, bisection=bisection).root
106 return schur_risk_parity(root=node, cov=cov, gamma=gamma)