Coverage for src/pyhrp/hrp.py: 100%

24 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-11-03 05:36 +0000

1"""Hierarchical Risk Parity (HRP) allocation entry points. 

2 

3This module owns the top-level allocation functions: 

4- hrp: Compute HRP portfolio weights from prices 

5- schur_hrp: Compute Schur Complementary Allocation weights from prices 

6 

7For backward compatibility the supporting building blocks stay importable from 

8``pyhrp.hrp``, but they are owned (and exported) by the modules that define them: 

9- build_tree, Dendrogram: see :mod:`pyhrp.dendrogram` 

10- compute_returns, compute_cov, compute_corr: see :mod:`pyhrp.covariance` 

11""" 

12 

13from __future__ import annotations 

14 

15from typing import Literal 

16 

17import polars as pl 

18 

19from .algos import risk_parity, schur_risk_parity 

20from .cluster import Cluster 

21from .covariance import check_finite_matrix, compute_returns 

22from .covariance import compute_corr as compute_corr 

23from .covariance import compute_cov as compute_cov 

24from .dendrogram import Dendrogram as Dendrogram 

25from .dendrogram import build_tree as build_tree 

26 

27__all__ = ["hrp", "schur_hrp"] 

28 

29Method = Literal["single", "complete", "average", "ward"] 

30 

31 

32def _prepare( 

33 prices: pl.DataFrame, node: Cluster | None, method: Method, bisection: bool 

34) -> tuple[pl.DataFrame, Cluster]: 

35 """Turn prices into the covariance matrix and the cluster tree the allocators need. 

36 

37 The correlation matrix is only computed when no ``node`` is supplied, since it 

38 is used for nothing but building the tree. 

39 

40 Args: 

41 prices (pl.DataFrame): Asset price time series (columns are assets, rows are dates) 

42 node (Cluster, optional): Root of a prebuilt cluster tree, used as-is if given 

43 method (Method): Linkage method passed to :func:`build_tree` 

44 bisection (bool): Whether to use bisection for tree construction 

45 

46 Returns: 

47 tuple[pl.DataFrame, Cluster]: The (finite) covariance matrix and the tree root 

48 """ 

49 returns = compute_returns(prices) 

50 cov = check_finite_matrix(compute_cov(returns), name="covariance matrix") 

51 if node is None: 

52 node = build_tree(compute_corr(returns), method=method, bisection=bisection).root 

53 return cov, node 

54 

55 

56def hrp( 

57 prices: pl.DataFrame, 

58 node: Cluster | None = None, 

59 method: Method = "ward", 

60 bisection: bool = False, 

61) -> Cluster: 

62 """Compute the hierarchical risk parity portfolio weights. 

63 

64 This is the main entry point for the HRP algorithm. It calculates returns from prices, 

65 builds a hierarchical clustering tree if not provided, and applies risk parity weights. 

66 

67 Note: 

68 A ``node`` passed in is weighted in place and returned; see the allocator 

69 contract in :mod:`pyhrp.algos`. Passing ``dendrogram.root`` therefore 

70 rewrites the portfolios inside that ``Dendrogram``, even though the 

71 ``Dendrogram`` itself is a frozen dataclass. 

72 

73 Args: 

74 prices (pl.DataFrame): Asset price time series (columns are assets, rows are dates) 

75 node (Cluster, optional): Root node of the hierarchical clustering tree. 

76 If None, a tree will be built from the correlation matrix. 

77 method (Literal["single", "complete", "average", "ward"]): Linkage method to use for distance calculation 

78 - "single": minimum distance between points (nearest neighbor) 

79 - "complete": maximum distance between points (furthest neighbor) 

80 - "average": average distance between all points 

81 - "ward": Ward variance minimization 

82 bisection (bool): Whether to use bisection method for tree construction 

83 

84 Returns: 

85 Cluster: The root cluster with portfolio weights assigned according to HRP 

86 """ 

87 cov, node = _prepare(prices, node, method, bisection) 

88 return risk_parity(root=node, cov=cov) 

89 

90 

91def schur_hrp( 

92 prices: pl.DataFrame, 

93 node: Cluster | None = None, 

94 method: Method = "ward", 

95 bisection: bool = False, 

96 gamma: float = 0.5, 

97) -> Cluster: 

98 """Compute Schur Complementary Allocation portfolio weights. 

99 

100 Extends HRP by augmenting each sub-covariance block with off-diagonal information 

101 via Schur complements before splitting risk between clusters. Introduced by Peter Cotton 

102 (arXiv:2411.05807). At gamma=0 this is identical to HRP; at gamma=1 it recovers the 

103 global minimum-variance portfolio through the same recursive hierarchy. 

104 

105 Note: 

106 A ``node`` passed in is weighted in place and returned; see the allocator 

107 contract in :mod:`pyhrp.algos`. Passing ``dendrogram.root`` therefore 

108 rewrites the portfolios inside that ``Dendrogram``, even though the 

109 ``Dendrogram`` itself is a frozen dataclass. 

110 

111 Args: 

112 prices (pl.DataFrame): Asset price time series (columns are assets, rows are dates) 

113 node (Cluster, optional): Root node of the hierarchical clustering tree. 

114 If None, a tree will be built from the correlation matrix. 

115 method (Literal["single", "complete", "average", "ward"]): Linkage method for clustering 

116 bisection (bool): Whether to use bisection method for tree construction 

117 gamma (float): Schur interpolation parameter in [0, 1]. 

118 0 recovers standard HRP; 1 recovers minimum-variance portfolio. 

119 

120 Returns: 

121 Cluster: The root cluster with portfolio weights assigned 

122 """ 

123 cov, node = _prepare(prices, node, method, bisection) 

124 return schur_risk_parity(root=node, cov=cov, gamma=gamma)