HRP Verification Report: Six Allocators, Two Defects Fixed
The allocator produces weights that sum to one, contain no shorts, and drift far less out of sample than an unconstrained minimum-variance optimiser on the same panel (+1.3% against +13.6%). Two defects were found and fixed before it did, and one over-specified test was corrected rather than the code.
- Verification record
- v1.0 · first run 16 August 2026 · published as a report 4 September 2026
- Implementation under test
quantmedia-research/hierarchical-risk-parity/hrp.py, in the public repository- Environment
- Python 3.11.9; numpy 2.4.6; pandas 3.0.5; scipy 1.17.1 — all newer than the package pins, so this doubles as a forward-compatibility check
- Verifier
- Cemil Ertürk
- Status
- Independent. Not peer reviewed. Synthetic data only; no claim about any real security.
On this synthetic panel, HRP with single linkage has out-of-sample volatility of 0.1419, compared with 0.1489 for unconstrained minimum variance. Other baselines narrow that difference. The comparison is useful precisely because all six results are visible.
- Reproduction record
- Checked 4 September 2026. Seed:
20260808. - Source
- hierarchical-risk-parity at revision
70ae45c. - Environment used
- Python 3.11.9, NumPy 2.4.6, pandas 3.0.5, SciPy 1.17.1.
- Data
- Synthetic. These examples test the implementation and illustrate the method; they do not measure trading performance on real securities.
Set up the experiment
The panel contains 20 assets. The first 120 periods fit the weights; the next 400 evaluate them without refitting. The in-sample mean absolute correlation is 0.240 and the covariance condition number is about 43.
Compare the output
Volatility is annualized in the example using 252 periods per year. Drift is 100 × (out-of-sample volatility / in-sample volatility − 1), calculated before the displayed volatility figures are rounded.
| Allocator | Volatility in | Volatility out | Drift |
|---|---|---|---|
| HRP (single) | 0.1401 | 0.1419 | +1.3% |
| HRP (Ward) | 0.1419 | 0.1425 | +0.4% |
| Minimum variance | 0.1311 | 0.1489 | +13.6% |
| Minimum variance, clipped to long-only | 0.1325 | 0.1460 | +10.2% |
| Minimum variance, shrinkage 0.3 | 0.1330 | 0.1432 | +7.7% |
| Equal weight | 0.1474 | 0.1443 | −2.1% |
The long-only baseline clips negative weights and renormalizes the result. It is a simple comparison, not a constrained optimization solver. The full comparison CSV also includes concentration, short exposure, drawdown and Sharpe. Out-of-sample Sharpe is negative for every allocator in this example.
Read the result carefully
Minimum variance has the lowest fitted volatility and the largest increase outside the fitted sample. Shrinkage reduces that increase. Equal weight has a small decrease; a negative drift is possible and is not an implementation error.
A small drift means the two volatility estimates are similar. It does not by itself imply the highest return, the lowest risk or the best portfolio. HRP single and Ward illustrate that even the linkage choice changes the result.
Check the implementation
The 13 tests include weights summing to one, nonnegative HRP weights, distance-matrix properties, and a two-asset case with a known 80/20 allocation. The implementation copies the correlation array before modifying it and symmetrizes the distance matrix to handle floating-point noise.
Limits and a useful next experiment
This is one fixed-seed panel with no transaction costs and no weight refitting. It does not establish which allocator wins across assets or market regimes. That experiment has now been run, with the panel's true covariance as the yardstick: see HRP vs minimum variance: where the drift comes from. Its numbers are not folded into this report.
Defects found during verification
The distance matrix was built by calling np.fill_diagonal on
DataFrame.values. pandas 3 returns a read-only view, so the call
raised. Fix: copy first. This is the kind of defect a
“works on my machine” implementation carries for years.
Floating-point noise left the correlation-distance matrix very slightly
asymmetric, and scipy.spatial.distance.squareform rejects that.
Fix: symmetrise explicitly with 0.5·(D + Dᵀ).
A third item was a test, not the code: an assertion that linkage methods must differ was over-specified for one unlucky seed, on which single and Ward linkage happened to agree. The methods were confirmed to differ on other seeds, and the test was fixed rather than the implementation. Adjusting a bad test is legitimate; adjusting a test to hide a real gap is not.
What this verification does not establish
- One synthetic panel, one seed. It illustrates the estimation-error mechanism; it is not evidence that HRP beats mean-variance in general.
- No transaction costs anywhere in the comparison. Weights are held fixed, which flatters every allocator and the least stable one most.
- Out-of-sample Sharpe was negative for every allocator on this panel, which is why none is reported as a performance figure.
Reproduce
python quantmedia-research/verify_examples.py asserts
outputs/comparison.csv and outputs/weights.csv match
the committed files. Then python tests/test_hrp.py: expected 13
passed.