Inside Our House Algorithm: HRP + Long/Short + CVaR with Hull-White
In our overview «12 Portfolio Optimization Algorithms, Compared» we raced a dozen allocation methods side by side. Eleven of them are textbook classics. The twelfth, Pipeline, is ours — and it got exactly one bullet point in that post. This article is the deep dive: what is inside it, where each formula comes from, and how the specification turns into Rust.
Pipeline does not invent a new way to compute weights. It takes the most robust known recipe — Hierarchical Risk Parity (HRP) — and wraps it in the two layers a live trading account actually needs but plain HRP lacks: direction (long/short from strategy signals) and a hard risk budget (CVaR adjusted for the current volatility regime). That gives four stages.
The four stages
- I — log returns of every asset.
- II — base weights from HRP.
- III — a long/short split from agent signals, with risk shares set by confidence.
- IV — a CVaR correction with Hull-White volatility; the surplus risk goes to cash.
Let's walk them in order.
Stage I. Log returns
Everything starts with the move from prices to log returns:
where is the asset and the time step. Log returns add up over time and are more symmetric than plain percentage changes — the standard input for any covariance math.
Stage II. HRP as the foundation
HRP, proposed by Marcos López de Prado in 2016, sidesteps Mean-Variance Optimization's central disease — inverting an ill-conditioned covariance matrix. It never inverts it at all. Instead it works with the structure of the correlations.
Covariance and correlation
From the returns we build the covariance matrix and normalize it into the correlation matrix :
Distance matrix
We turn correlation into a distance metric, so that strongly correlated assets land "close together":
The closer is to 1, the closer is to 0 — and the more likely the assets share a cluster.
Dendrogram and leaf order
From the distance matrix we build a cluster hierarchy via average linkage and read off the leaf order — a permutation of the assets in which similar ones sit adjacent.
Optional step: the optimal number of clusters can be chosen by the silhouette coefficient , where is the mean distance within a cluster and the mean distance to the nearest neighboring one. The base pass does not need it — recursive bisection already respects the hierarchy.
Quasi-diagonalization
We permute the rows and columns of by , gathering large values along the diagonal:
Recursive bisection
Then the recursion runs top-down. At each step a cluster is split in half into and , and capital is allocated between the halves inversely proportional to their variances:
A cluster's variance is computed on its covariance sub-block as . The descent continues until each node holds a single asset. Weights are long-only, non-negative, summing to 1.0.
In our implementation this is the function hrp_from_cov(cov) -> Vec<f64>: correlation → distance → average linkage → leaf order → quasi-diagonalization → recursive bisection. Pipeline calls it as its base — and it is also the public optimize() for the no-signals case.
Stage III. The long/short overlay
Plain HRP is a "buy-only" portfolio. But a strategy often says not just how much but which way. Stage III takes per-asset signals (Long/Short) from the agent and builds two sub-portfolios.
- Assets are split into long and short baskets by signal.
- Inside each basket, weights are computed with the same HRP (on the covariance sub-block of those assets), summing to 1 per basket.
- If the agent also emits a confidence , the risk shares between the sides are set by total confidence:
With no confidence, the shares fall back to the asset count in each basket. The final signed weight is for longs and for shorts, after which the whole gross exposure is normalized to 1.
An honest note about the code. The original spec carries correction factors , — but it also marks them with a "do we even need this step?". The implementation does not apply them: the two sides are combined directly by the risk shares , which keeps gross exposure exactly at 1 and creates no hidden leverage. That is a deliberate simplification of the spec, not an oversight.
Stage IV. CVaR with a Hull-White adjustment
HRP balances risk structurally, but knows nothing about the absolute level of risk in money terms. The final stage puts a hard ceiling on tail risk — and makes it sensitive to a change of market regime.
Portfolio return and EWMA volatility
First we collapse the weights into a portfolio return and estimate conditional volatility with EWMA:
with (the classic RiskMetrics value). EWMA gives "today's" volatility rather than one averaged over all history.
Hull-White rescaling
The key idea: past returns cannot be taken as-is — they happened under a different volatility. The Hull-White method rescales each past return to the current level:
A calm month is "stretched", a turbulent one "compressed", and the distribution is brought into the current regime.
VaR and CVaR
On the rescaled distribution we take the loss quantile and the mean loss in the tail:
CVaR (a.k.a. Expected Shortfall) answers not "how bad is a typical bad day" but "how bad is it on average across the worst percent" — so it sees the thickness of the tail, not just its edge.
Risk budget and cash
If CVaR exceeds the acceptable threshold, every risky position is shrunk by a single factor, and the freed capital moves to cash:
So the portfolio de-risks itself when tail risk grows and re-enters the market when it calms down.
From spec to code
The whole algorithm lives in a single Rust crate, portfolio-pipeline, and obeys the workspace's uniform contract:
pub fn optimize(prices: &[Vec<f64>]) -> Vec<f64>
This is the long-only projection (stages I, II, IV with no signals) — the exact same prices -> weights interface as the other eleven algorithms, so Pipeline is a drop-in replacement for any of them. The full version with every stage is a separate function:
pub fn run(
prices: &[Vec<f64>],
signals: Option<&[Side]>, // Long / Short per asset
confidence: Option<&[f64]>, // agent confidence → risk shares λ
cfg: &PipelineConfig, // CVaR / Hull-White parameters
) -> PipelineResult // signed weights + cash + cvar + σ
The overlay defaults: tail cvar_alpha = 0.05, budget cvar_max = 0.05, EWMA ewma_lambda = 0.94, Hull-White window hw_window = 0 (all history). The implementation has no external dependencies and is deliberately defensive: on short histories (fewer than 4 price points) it returns equal weights, and the CVaR overlay only kicks in at ≥8 return observations — otherwise there is nothing to estimate a tail from.
Why Rust: one deterministic codebase for both backtest and production, with no "Python in research, something else in prod" drift, and fast enough to run all twelve algorithms in a single request through the comparison backend.
What it costs in time
How fast is "fast enough"? We pulled the HRP core (log returns → covariance → average linkage → quasi-diagonalization → recursive weights) into a standalone benchmark and ran the exact same math in C, Rust and Node.js. Same conditions: Apple Silicon, single thread, 365 daily observations per asset, synthetic prices. The table shows the full-pass time (TOTAL) as a function of the asset count .
| assets | C (gcc -O3) | Rust (release) | Node.js |
|---|---|---|---|
| 10 | 33 µs | 51 µs | 1.6 ms |
| 50 | 462 µs | 520 µs | 2.4 ms |
| 100 | 1.7 ms | 2.1 ms | 4.3 ms |
| 200 | 10.0 ms | 15.0 ms | 14.3 ms |
| 500 | 58 ms | 82 ms | 108 ms |
| 1000 | 260 ms | 401 ms | 615 ms |
| 2000 | 1.53 s | 2.51 s | 2.97 s |
What this shows:
- On realistic portfolios it's free. A crypto basket is dozens of assets, rarely more than a hundred. At a full HRP pass is single-digit milliseconds even in Node and microseconds in Rust/C. Recomputing weights on every tick is a non-issue.
- Rust is within ~1.3–1.6× of C — the same order of magnitude, both compiled. C is a touch faster on raw arithmetic, but Rust gives the same predictability with no garbage collector and no UB.
- Node holds up surprisingly well. At small it is ~30–50× slower than C due to interpretation, but as grows the linkage dominates everything and the gap collapses: at Node (2.97 s) is only twice as slow as C (1.53 s).
- The bottleneck is average linkage, . That, not the language, governs scaling: at C takes ~20 s and Rust ~33 s (Node is out of its depth at that size). For real-world portfolio sizes it doesn't matter, but for thousands of assets the first thing to change is the clustering algorithm, not the language.
The pragmatic takeaway: at our portfolio sizes, choosing Rust is not about "beating C" (C is slightly faster here) — it's about one deterministic codebase for research and production, with no GC pauses and years of performance headroom. The benchmark itself (C / Rust / Node, plus a Zig port) is open in the project repository.
Where Pipeline sits among the twelve
In our comparison on a single (deliberately rigged) basket, Pipeline behaved like HRP — because through the long-only optimize() entry point it is HRP with a CVaR overlay. Its directional machinery only comes alive when you feed it strategy signals. That is the whole point: Pipeline is not "yet another optimizer for backtesting weights" but the execution layer between strategy signals and real orders — it takes your buy/sell calls, lays out capital by HRP within each side, balances the sides by confidence, and clips tail risk down to a set budget.
For the full context — which other eleven methods exist and how they differ — see the overview, «12 Portfolio Optimization Algorithms, Compared». And you can try all of it live at portfolio-optimizer.marketmaker.cc.
References
- López de Prado, M. (2016). Building Diversified Portfolios that Outperform Out of Sample. The Journal of Portfolio Management.
- López de Prado, M. (2018). Advances in Financial Machine Learning. Wiley.
- Hull, J., & White, A. (1998). Incorporating Volatility Updating into the Historical Simulation Method for Value at Risk. Journal of Risk.
- Rockafellar, R. T., & Uryasev, S. (2000). Optimization of Conditional Value-at-Risk. Journal of Risk.
- RiskMetrics Group (1996). RiskMetrics — Technical Document. J.P. Morgan.
- Marketmaker.cc: marketmaker.cc
Citation
@article{soloviov2026pipeline,
author = {Soloviov, Eugen and Zhuravleva, Marina and Kiselev, Kirill},
title = {Inside Our House Algorithm: HRP + Long/Short + CVaR with Hull-White Adjustment},
year = {2026},
url = {https://marketmaker.cc/en/blog/post/portfolio-pipeline-hrp-cvar},
description = {A deep dive into Pipeline, a composite portfolio allocation algorithm built on Hierarchical Risk Parity with a signal-driven long/short overlay and a Hull-White CVaR risk-budget correction, with the full specification and its Rust implementation.}
}
Pengarang
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.
Financial mathematics
Fifth-year student at Bauman Moscow State Technical University (Automatic Control Systems), specializing in financial mathematics. Background in calibrating stochastic-volatility (Heston) and local-volatility (Dupire) models, fair pricing of options including exotics via both Monte-Carlo and analytic formulas, hedging-error reduction, and exposure to LSV models.
Portfolio optimization
Fourth-year student at the Faculty of Mechanics and Mathematics, Novosibirsk State University (NSU); thesis on Heston-model calibration and delta-hedging within the same model. Works on portfolio optimization.