← К списку статей
May 25, 2026
5 мин. чтения

Внутри нашего «фирменного» алгоритма: HRP + long/short + CVaR по Халлу–Уайту

Внутри нашего «фирменного» алгоритма: HRP + long/short + CVaR по Халлу–Уайту
#оптимизация портфеля
#HRP
#иерархический паритет риска
#CVaR
#Халл-Уайт
#EWMA
#long/short
#управление риском
#Rust
#количественные финансы

В обзоре «12 алгоритмов оптимизации портфеля» мы прогнали дюжину методов аллокации бок о бок. Одиннадцать из них — классика из литературы. Двенадцатый, Pipeline, — наш собственный, и в обзоре ему досталась всего одна строчка. Эта статья — подробный разбор: что у него внутри, откуда берётся каждая формула и как спецификация превращается в код на Rust.

Pipeline не изобретает новый способ считать веса. Он берёт самый устойчивый из известных приёмов — Hierarchical Risk Parity (HRP) — и оборачивает его двумя слоями, которые нужны на реальном торговом счёте, но которых нет в чистом HRP: направленностью (long/short по сигналам стратегии) и жёстким бюджетом риска (CVaR с поправкой на текущий режим волатильности). Получается четыре стадии.

Четыре стадии

цены  I  лог-ретёрны  II  HRP-веса  III  long/short  IV  CVaR-бюджет\text{цены} \;\xrightarrow{\text{I}}\; \text{лог-ретёрны} \;\xrightarrow{\text{II}}\; \text{HRP-веса} \;\xrightarrow{\text{III}}\; \text{long/short} \;\xrightarrow{\text{IV}}\; \text{CVaR-бюджет}
  • I — лог-доходности всех активов.
  • II — базовые веса по HRP.
  • III — разбиение на long/short по сигналам агента, с долями риска по уверенности.
  • IV — коррекция через CVaR с волатильностью по Халлу–Уайту; излишек риска уходит в кэш.

Разберём по порядку.

Стадия I. Лог-доходности

Всё начинается с перехода от цен к логарифмическим доходностям:

ri,t=ln ⁣(Si,tSi,t1)r_{i,t} = \ln\!\left(\frac{S_{i,t}}{S_{i,t-1}}\right)

где ii — актив, tt — момент времени. Лог-ретёрны складываются во времени и симметричнее обычных процентных изменений — стандартный вход для любой ковариационной математики.

Стадия II. HRP как фундамент

HRP, предложенный Маркосом Лопесом де Прадо в 2016 году, обходит главную болезнь Mean-Variance Optimization — обращение плохо обусловленной ковариационной матрицы. Он вообще её не обращает. Вместо этого работает со структурой корреляций.

Ковариация и корреляция

Из доходностей строим ковариационную матрицу Σ\Sigma и нормируем её до корреляционной CC:

Σi,j=Cov(ri,rj),Ci,j=ρi,j=Cov(ri,rj)σiσj\Sigma_{i,j} = \mathrm{Cov}(r_i, r_j), \qquad C_{i,j} = \rho_{i,j} = \frac{\mathrm{Cov}(r_i, r_j)}{\sigma_i \sigma_j}

Матрица расстояний

Корреляцию превращаем в метрику расстояния — так, чтобы сильно скоррелированные активы оказались «близко»:

di,j=1ρi,j2d_{i,j} = \sqrt{\frac{1 - \rho_{i,j}}{2}}

Чем ближе ρi,j\rho_{i,j} к 1, тем ближе di,jd_{i,j} к 0 — и тем вероятнее активы попадут в один кластер.

Дендрограмма и порядок листьев

По матрице расстояний строим иерархию кластеров через average linkage и снимаем с дендрограммы порядок листьев π=(π1,,πN)\pi = (\pi_1, \ldots, \pi_N) — перестановку активов, в которой похожие стоят рядом.

Опциональный шаг: оптимальное число кластеров можно подбирать по коэффициенту силуэта si=biaimax(bi,ai)s_i = \dfrac{b_i - a_i}{\max(b_i, a_i)}, где aia_i — среднее расстояние внутри кластера, bib_i — до ближайшего соседнего. В базовом проходе он не нужен — рекурсивное деление само уважает иерархию.

Квазидиагонализация

Переставляем строки и столбцы Σ\Sigma в порядке π\pi, собирая большие значения вдоль диагонали:

Σi,jq=Σπi,πj\Sigma^{q}_{i,j} = \Sigma_{\pi_i, \pi_j}

Рекурсивное деление

Дальше — рекурсия сверху вниз. На каждом шаге кластер делится пополам на LL и RR, и капитал между половинами распределяется обратно пропорционально их дисперсиям:

wL=1/σL21σL2+1σR2,wR=1wLw_L = \frac{1/\sigma_L^2}{\dfrac{1}{\sigma_L^2} + \dfrac{1}{\sigma_R^2}}, \qquad w_R = 1 - w_L

Дисперсия кластера считается на его подматрице ковариаций как σC2=1m2i,jCΣi,jq\sigma_C^2 = \tfrac{1}{m^2}\sum_{i,j \in C}\Sigma^{q}_{i,j}. Спуск продолжается, пока в каждом узле не останется один актив. Веса только длинные, неотрицательные, в сумме 1.0.

В нашей реализации это функция hrp_from_cov(cov) -> Vec<f64>: корреляция → расстояние → average linkage → порядок листьев → квазидиагонализация → рекурсивное деление. Именно её Pipeline вызывает как базу — и она же является публичным optimize() для случая без сигналов.

Стадия III. Long/short-оверлей

Чистый HRP — портфель «только купить». Но стратегия часто говорит не только сколько, но и в какую сторону. Стадия III принимает от агента сигналы (Long/Short) по каждому активу и строит два подпортфеля.

  1. Активы делятся на long- и short-корзины по сигналам.
  2. Внутри каждой корзины веса считаются тем же HRP (на подматрице ковариаций соответствующих активов), сумма по корзине — 1.
  3. Если агент выдаёт ещё и уверенность pip_i, доли риска между сторонами задаются суммарной уверенностью:

ξL=iLpi,ξS=iSpi,λL=ξLξL+ξS,λS=ξSξL+ξS\xi_L = \sum_{i \in L} p_i, \quad \xi_S = \sum_{i \in S} p_i, \qquad \lambda_L = \frac{\xi_L}{\xi_L + \xi_S}, \quad \lambda_S = \frac{\xi_S}{\xi_L + \xi_S}

Без уверенности доли падают обратно на количество активов в каждой корзине. Итоговый знаковый вес: wi=λLwiHRPw_i = \lambda_L \cdot w_i^{\text{HRP}} для long и wi=λSwiHRPw_i = -\lambda_S \cdot w_i^{\text{HRP}} для short, после чего вся валовая экспозиция нормируется к 1.

Честная пометка о коде. В исходной спецификации фигурируют поправочные коэффициенты αL=λL/σL\alpha_L = \sqrt{\lambda_L}/\sigma_L, αS=λS/σS\alpha_S = \sqrt{\lambda_S}/\sigma_S — но там же стоит вопрос «нужен ли этот шаг?». В реализации мы их не применяем: стороны комбинируются напрямую через доли риска λ\lambda, что держит валовую экспозицию ровно на 1 и не создаёт скрытого плеча. Это сознательное упрощение спеки, а не недосмотр.

Стадия IV. CVaR с поправкой Халла–Уайта

HRP балансирует риск структурно, но ничего не знает про абсолютный уровень риска в деньгах. Последняя стадия ставит жёсткий потолок на хвостовой риск — и делает его чувствительным к смене рыночного режима.

Доходность портфеля и EWMA-волатильность

Сначала сворачиваем веса в доходность портфеля и оцениваем условную волатильность по EWMA:

rp,t=i=1nwiri,t,σp,t2=λσp,t12+(1λ)rp,t12r_{p,t} = \sum_{i=1}^{n} w_i\, r_{i,t}, \qquad \sigma_{p,t}^2 = \lambda\, \sigma_{p,t-1}^2 + (1 - \lambda)\, r_{p,t-1}^2

с λ=0.94\lambda = 0.94 (классическое значение RiskMetrics). EWMA даёт «сегодняшнюю» волатильность, а не усреднённую по всей истории.

Перешкалирование по Халлу–Уайту

Ключевая идея: прошлые доходности нельзя брать как есть — они получены при другой волатильности. Метод Халла–Уайта масштабирует каждую прошлую доходность к текущему уровню:

r~p,s=σp,t+1σp,srp,s,s=tN+1,,t\widetilde{r}_{p,s} = \frac{\sigma_{p,t+1}}{\sigma_{p,s}}\, r_{p,s}, \qquad s = t - N + 1, \ldots, t

Спокойный месяц «растягивается», бурный — «сжимается», и распределение приводится к текущему режиму.

VaR и CVaR

По перешкалированному распределению берём квантиль убытков и средний убыток в хвосте:

VaRαHW=q1α(r~p)VaR_\alpha^{HW} = -q_{1-\alpha}(\widetilde{r}_p)

CVaRαHW=E ⁣[r~pr~pq1α(r~p)]CVaR_\alpha^{HW} = -\mathbb{E}\!\left[\widetilde{r}_p \mid \widetilde{r}_p \le q_{1-\alpha}(\widetilde{r}_p)\right]

CVaR (он же Expected Shortfall) отвечает не «насколько плохо в типичный плохой день», а «насколько плохо в среднем по худшим α\alpha процентам» — то есть видит толщину хвоста, а не только его край.

Бюджет риска и кэш

Если CVaR превышает допустимый порог, все рискованные позиции ужимаются одним коэффициентом, а высвободившийся капитал уходит в кэш:

winew=γwi,γ=CVaRmaxCVaRαHW,wcash=1i=1nwineww_i^{new} = \gamma\, w_i, \quad \gamma = \frac{CVaR_{\max}}{CVaR_\alpha^{HW}}, \qquad w_{cash} = 1 - \sum_{i=1}^{n} |w_i^{new}|

Так портфель сам себя «обналичивает» при росте хвостового риска и снова входит в рынок, когда тот успокаивается.

CVaR с поправкой Халла–Уайта: исторические доходности масштабируются к текущей волатильности, хвостовой риск обрезается до бюджета

От спецификации к коду

Весь алгоритм живёт в одном Rust-крейте portfolio-pipeline и подчиняется общему контракту воркспейса:

pub fn optimize(prices: &[Vec<f64>]) -> Vec<f64>

Это long-only-проекция (стадии I, II, IV без сигналов) — ровно тот же интерфейс цены -> веса, что и у остальных одиннадцати алгоритмов, поэтому Pipeline взаимозаменяем с ними в любом коде. Полная версия со всеми стадиями — отдельная функция:

pub fn run(
    prices: &[Vec<f64>],
    signals: Option<&[Side]>,      // Long / Short по каждому активу
    confidence: Option<&[f64]>,    // уверенность агента → доли риска λ
    cfg: &PipelineConfig,          // параметры CVaR/Hull-White
) -> PipelineResult                // знаковые веса + cash + cvar + σ

Параметры оверлея по умолчанию: хвост cvar_alpha = 0.05, бюджет cvar_max = 0.05, EWMA ewma_lambda = 0.94, окно Халла–Уайта hw_window = 0 (вся история). Реализация без внешних зависимостей и сознательно «защищённая»: на коротких историях (менее 4 точек цен) она отдаёт равные веса, а CVaR-оверлей включается только при ≥8 наблюдениях доходности — иначе хвост оценивать не на чем.

Почему Rust: единый детерминированный код для бэктеста и для продакшена, без расхождения «питон в ресёрче — что-то другое в бою», и достаточно быстрый, чтобы гонять все двенадцать алгоритмов на одном запросе через бэкенд сравнения.

Сколько это стоит по времени

«Достаточно быстрый» — это сколько? Мы вынесли ядро HRP (лог-ретёрны → ковариация → average linkage → квазидиагонализация → рекурсивные веса) в отдельный бенчмарк и прогнали одну и ту же математику на C, Rust и Node.js. Условия одинаковые: Apple Silicon, один поток, 365 дневных наблюдений на актив, синтетические цены. В таблице — полное время прохода (TOTAL) в зависимости от числа активов NN.

NN активов 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

Что отсюда видно:

  • На реалистичных портфелях это бесплатно. Криптокорзина — это десятки активов, редко больше сотни. На N100N \le 100 полный проход HRP занимает единицы миллисекунд даже в Node и микросекунды в Rust/C. Пересчитывать веса хоть на каждом тике — не проблема.
  • Rust в пределах ~1.3–1.6× от C — тот же порядок, оба компилируемые. C немного быстрее на «голой» арифметике, но Rust даёт ту же предсказуемость без сборщика мусора и без UB.
  • Node удивительно держится. На малых NN он в ~30–50× медленнее C из-за интерпретации, но с ростом NN всё съедает O(N3)O(N^3)-линкедж, и разрыв схлопывается: на N=2000N = 2000 Node (2.97 с) лишь вдвое медленнее C (1.53 с).
  • Узкое место — average linkage, O(N3)O(N^3). Именно он, а не выбор языка, определяет масштабирование: на N=5000N = 5000 C считает ~20 с, Rust ~33 с (Node на таких размерах уже неуместен). Для портфелей реального размера это неважно, но для тысяч активов первым делом меняют не язык, а алгоритм кластеризации.

Вывод прагматичный: при наших размерах портфеля выбор Rust — это не про «обогнать C» (C тут чуть быстрее), а про один детерминированный код для ресёрча и продакшена, без GC-пауз и с запасом по скорости на годы вперёд. Сам бенчмарк (C / Rust / Node, и порт на Zig) лежит открыто в репозитории проекта.

Где Pipeline в общем зачёте

В нашем сравнении на одной (намеренно «подстроенной») корзине Pipeline вёл себя как HRP — потому что через long-only-вход optimize() он и есть HRP с CVaR-надстройкой. Его направленная машинерия оживает только тогда, когда его кормят сигналами стратегии. Это и есть главная мысль: Pipeline — не «ещё один оптимизатор для бэктеста весов», а исполнительный слой между сигналами стратегии и реальными ордерами: он берёт ваши «купить/продать», раскладывает капитал по HRP внутри каждой стороны, балансирует стороны по уверенности и обрезает хвостовой риск до заданного бюджета.

Полный контекст — какие ещё одиннадцать методов существуют и чем они отличаются — в обзорной статье «12 алгоритмов оптимизации портфеля». А пощупать всё вживую можно на portfolio-optimizer.marketmaker.cc.

Источники

  1. López de Prado, M. (2016). Building Diversified Portfolios that Outperform Out of Sample. The Journal of Portfolio Management.
  2. López de Prado, M. (2018). Advances in Financial Machine Learning. Wiley.
  3. Hull, J., & White, A. (1998). Incorporating Volatility Updating into the Historical Simulation Method for Value at Risk. Journal of Risk.
  4. Rockafellar, R. T., & Uryasev, S. (2000). Optimization of Conditional Value-at-Risk. Journal of Risk.
  5. RiskMetrics Group (1996). RiskMetrics — Technical Document. J.P. Morgan.
  6. 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/ru/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.}
}
Дисклеймер: Информация в этой статье предоставлена исключительно в образовательных и ознакомительных целях и не является финансовым, инвестиционным или торговым советом. Торговля криптовалютами сопряжена с высоким риском убытков.

Авторы

Евгений Соловьёв
Евгений Соловьёв

Инженер торговых систем

Разработка торговых ботов с 2017 года: межбиржевой арбитраж (подключал до 30 бирж), парный арбитраж на коинтеграции между спотом и фьючерсами, скальпинг, фронтраннинг, торговля по новостям, сентиментный анализ, трендовые алгоритмы, а также алгоритмы управления и балансировки портфелей. Делает выставление ордеров до 1 мс, warehouse для big data, бэктестинг-движки, AI-агентов и интерфейсы для ботов (в т.ч. open-source profitmaker.cc). Стек: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, архитектура.

Марина Журавлёва
Марина Журавлёва

Финансовая математика

Студентка 5 курса МГТУ им. Баумана (направление «системы автоматического управления»), развивает финансовую математику. Занималась калибровкой моделей стохастической волатильности (Хестон) и локальной волатильности (Дюпир), справедливой оценкой опционов (в том числе экзотических) как методами Монте-Карло, так и аналитическими формулами, устранением ошибки хеджирования, соприкасалась с LSV.

Кирилл Киселёв
Кирилл Киселёв

Портфельная оптимизация

Студент 4 курса мехмата НГУ; диплом по калибровке модели Хестона и дельта-хеджированию в этой же модели. Занимался портфельной оптимизацией.

Newsletter

Будьте в курсе событий

Подпишитесь на нашу рассылку, чтобы получать эксклюзивную аналитику по AI-трейдингу и обновления платформы.

Мы уважаем вашу конфиденциальность. Отписаться можно в любой момент.