Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat(hierarchical): add HERCOpt (HERC) and NCOpt (NCO) portfolio allocation#742

Open
hass-nation wants to merge 2 commits into
PyPortfolio:main from
hass-nation:feat/herc-nco-hierarchical
Open

feat(hierarchical): add HERCOpt (HERC) and NCOpt (NCO) portfolio allocation #742
hass-nation wants to merge 2 commits into
PyPortfolio:main from
hass-nation:feat/herc-nco-hierarchical

Conversation

@hass-nation

@hass-nation hass-nation commented Jun 27, 2026

Copy link
Copy Markdown

Summary

Extends pypfopt.hierarchical_portfolio with two new portfolio allocation methods that the module docstring already identifies as missing:

  • HERCOpt — Hierarchical Equal Risk Contribution (Raffinot 2018)
  • NCOpt — Nested Cluster Optimization (Lopez de Prado 2019)

Both classes share the same API as HRPOpt and are exported from pypfopt.__init__.


HERCOpt — Hierarchical Equal Risk Contribution

HERC extends HRP by replacing inverse-variance (IVP) cluster weights with Equal Risk Contribution (ERC) weights at every level of the hierarchy. The recursive bisection structure is identical to HRP, but the variance of each sub-cluster is computed as the variance of its ERC portfolio rather than its IVP portfolio.

Because ERC accounts for within-cluster correlations, HERC produces more balanced risk allocation when intra-cluster correlations are high — a common situation in equity portfolios where sector peers cluster together.

from pypfopt import HERCOpt
herc = HERCOpt(returns=returns_df) # or: cov_matrix=S
weights = herc.optimize(linkage_method="ward")
herc.portfolio_performance(verbose=True)

The ERC weights are solved via the multiplicative cyclical coordinate descent update of Roncalli (2013), which converges reliably and requires no SciPy optimization calls for individual clusters.


NCOpt — Nested Cluster Optimization

NCO partitions the asset universe into clusters, optimizes within each cluster, and then optimizes across the resulting cluster-portfolios in a two-level nested procedure.

from pypfopt import NCOpt
nco = NCOpt(returns=returns_df)
weights = nco.optimize(
 n_clusters=4,
 internal_opt="min_variance", # within-cluster: "min_variance", "erc", "equal"
 meta_opt="erc", # across-cluster: "min_variance", "erc", "equal"
 linkage_method="ward",
)
nco.portfolio_performance(verbose=True)
# Cluster assignment is available after optimize()
print(nco.clusters) # {0: ['AAPL', 'GOOG', ...], 1: [...], ...}

The meta-covariance across clusters is computed analytically from the full covariance matrix (w_i' Sigma w_j), so NCO works correctly even when only a covariance matrix (no return history) is supplied.

Supports all 9 combinations of (internal_opt x meta_opt).


Private helpers (also usable by downstream code)

Function Description
_erc_weights_ccd(cov) ERC via multiplicative CCD (Roncalli 2013)
_min_var_weights(cov) Long-only min-variance: analytic when feasible, SLSQP fallback

Files changed

File Change
pypfopt/hierarchical_portfolio.py +HERCOpt, +NCOpt, +2 helpers (~500 lines)
pypfopt/__init__.py Export HERCOpt, NCOpt
tests/test_herc_nco.py 53 tests, 9 test classes (new file)

Tests

53 tests, 0 failures (all existing HRP tests still pass)

Coverage includes: ERC weight correctness (equal RC property), min-var analytic solution, instantiation errors, weight validity (sum=1, non-negative), all 9 NCO method combinations, cov-only mode, comparison vs HRP (weights differ meaningfully), portfolio performance.

References

  1. Raffinot, T. (2018). Hierarchical clustering-based asset allocation. Journal of Portfolio Management, 44(2), 89-99.
  2. Lopez de Prado, M. (2019). A Robust Estimator of the Efficient Frontier. SSRN Working Paper. https://ssrn.com/abstract=3469961
  3. Roncalli, T. (2013). Introduction to Risk Parity and Budgeting. CRC Press.

Generated with Claude Code

hass-nation and others added 2 commits June 27, 2026 23:06
Extends the hierarchical portfolio module with two additional methods:
HERCOpt - Hierarchical Equal Risk Contribution (Raffinot 2018)
 Equal Risk Contribution (ERC) weights at every level of the hierarchy
 instead of the inverse-variance weights used by HRP. ERC accounts for
 within-cluster correlations, producing more balanced risk allocation
 when intra-cluster correlations are high. Implemented via cyclical
 coordinate descent (Roncalli 2013).
NCOpt - Nested Cluster Optimization (Lopez de Prado 2019)
 Two-level nested procedure: within-cluster optimization followed by
 meta-portfolio optimization across clusters. Supports three objectives
 at each level - min_variance, erc, and equal-weight - giving 9
 combinations. The meta-covariance is computed analytically from the
 full covariance matrix, so no return history is required.
Both classes:
 - Share the same API as HRPOpt (fit from returns or cov_matrix)
 - Export portfolio_performance() with expected return and Sharpe ratio
 - Are exported from pypfopt.__init__ and __all__
 - Accept all scipy linkage methods
Also adds two private helpers:
 - _erc_weights_ccd: ERC via multiplicative CCD (Roncalli 2013)
 - _min_var_weights: long-only min-variance with analytic + SLSQP fallback
53 tests across 9 test classes; all existing HRP tests still pass.
References
----------
Raffinot, T. (2018). Hierarchical clustering-based asset allocation.
Journal of Portfolio Management, 44(2), 89-99.
Lopez de Prado, M. (2019). A Robust Estimator of the Efficient Frontier.
SSRN Working Paper. https://ssrn.com/abstract=3469961
Roncalli, T. (2013). Introduction to Risk Parity and Budgeting.
CRC Press.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
... CCD
The previous implementation used Roncalli's multiplicative update, which
diverges when the covariance matrix has negative off-diagonal entries because
some risk contributions become negative during iterations and are incorrectly
clipped before taking the square root.
Replace with the Spinu (2013) cyclical coordinate descent, which minimises
the unconstrained convex objective
 f(w) = (1/2) w'Σw − (1/n) Σi log(wi)
by solving the exact one-dimensional sub-problem at each coordinate:
 Σii·wi2 + (Σw − Σii·wi)·wi − (1/n) = 0 → positive root
Crucially, the weights must NOT be normalised between coordinate updates;
normalisation happens once, at the end of each full pass over all assets.
This change fixes test_equal_risk_contributions (which uses a covariance
matrix with negative off-diagonal entries) and leaves all HERC / NCO tests
unchanged (53/53 pass), since the algorithm is equivalent for PD matrices
with the results scaled consistently.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

1 participant

AltStyle によって変換されたページ (->オリジナル) /