层次风险平价:当机器学习遇上投资组合构建

Read in English

传统的投资组合优化有一个不愿明说的秘密:它并不管用。输入只要有微小变化,得出的配置就天差地别。层次风险平价(HRP)由 Marcos López de Prado 在 AQR Capital Management 开发,用机器学习构建更稳定、分散得更好、样本外表现更优的投资组合。这是管理着数十亿美元的机构投资者所用的方法,这里把它改造成适合退休组合的版本。

🎯 你将学到什么

  • 传统优化为什么会失败 (以及你的投资组合可能哪里出了问题)
  • HRP 的工作原理 运用层次聚类与图论
  • Python 实现 附完整代码与回测
  • 实战应用 用于退休组合(10-15 只 ETF)
  • 业绩对比 HRP、均值方差与等权重的对比(2015-2024)

读完之后: 你会理解机构级的投资组合构建方法,并拿到可直接运行的实现代码。

问题所在:传统优化为什么会失败

均值方差优化的美好承诺

1952 年,Harry Markowitz 提出现代投资组合理论,并给出一个漂亮的承诺:只要给定预期收益、波动率和相关性,我们就能用数学方法找到"最优"投资组合,在给定风险水平下把收益最大化。

理论很优雅:

  • 最小化目标:组合方差 = w'Σw(其中 w 为权重,Σ 为协方差矩阵)
  • 约束条件:预期收益约束
  • 结果:得到由最优组合构成的"有效前沿"

现实很残酷: 它在实践中并不管用。

均值方差优化的三个致命缺陷

缺陷一:极度不稳定

现象: 把预期收益改动 0.5%,最优配置就会变动 30-50%。

示例: 把美股的预期收益从 8.0% 提高到 8.5%:

  • 调整前:60% 美股、20% 国际股票、20% 债券
  • 调整后:85% 美股、5% 国际股票、10% 债券

为什么这是灾难性的: 没有人知道真实的预期收益。如果 0.5% 的估计误差就能带来 25% 的配置摆动,这套优化就毫无价值。

缺陷二:极度集中

现象: 优化器给出的组合高度集中,80% 以上的仓位压在少数几个资产上。

真实案例(2010-2015): 对 10 类资产做均值方差优化:

  • 美国大盘股:62%
  • 长期国债:28%
  • 其余 8 类资产合计:10%

问题在于: 这完全背离了分散投资的初衷。你实质上是在赌两个资产。

缺陷三:样本外表现糟糕

现象: 在样本内看起来"最优"的组合,到了样本外还不如等权重。

DeMiguel、Garlappi、Uppal(2009)的研究: 在 7 组数据集上检验均值方差优化:

  • 均值方差优化:年化收益 8.2%,夏普比率 0.42
  • 等权重(1/N):年化收益 9.1%,夏普比率 0.51

结论: "我们发现,所检验的 14 个优化模型没有一个能跑赢 1/N 法则。"

为什么会这样:协方差矩阵求逆的诅咒

均值方差优化必须 对协方差矩阵求逆。问题就出在这里:

  1. 估计误差: 当资产数 N=10 时,你需要估计 55 个相关系数。用 5 年的月度数据(60 个观测值),平均每个参数只摊到 1 个数据点。噪声占了主导。
  2. 病态矩阵: 较小的特征值(对应几乎冗余的资产)在求逆时会被放大,导致配置走向极端。
  3. 过拟合: 优化器找到的组合能完美拟合历史噪声,却在样本外一败涂地。

机构投资者早就明白这一点。 所以 Bridgewater、AQR 和其他量化基金并不使用均值方差优化。他们用的是更好的方法,比如 HRP。

The 解决方案: 层次风险平价 (HRP)

核心洞见

与其把所有资产当成彼此独立(那就必须做矩阵求逆),不如换个思路: HRP 认识到资产是按层次聚成簇的:

  • 美国大盘股和美国小盘股之间的相似度,高于它们与黄金的相似度
  • 各类股票彼此之间的相似度,高于它们与债券的相似度
  • 资产天然形成一个层次结构:股票 → 美国/国际 → 大盘/小盘/价值/成长

HRP 的做法: 用层次聚类把相似资产归为一组,然后在各个簇之间(而不是各个资产之间)平均分配风险。这样就完全绕开了矩阵求逆。

HRP 算法的三个步骤

第一步:构建距离矩阵

把相关性转换成距离。走势同步的资产彼此"近",走势不同步的彼此"远"。

公式: dij = √(½(1 - ρij))

直观理解: 若相关性为 1(完全正相关),距离为 0;若相关性为 -1,距离为 1。

第二步:层次聚类

按距离把资产组织成一棵树(树状图)。

方法: 单连接聚类

  • 起点:每个资产各自成为一个簇
  • 迭代:合并距离最近的两个簇
  • 重复:直到所有资产归入同一棵树

结果: 得到一个层次结构,显示哪些资产最相似(最先合并)、哪些最不相似(最后合并)。

第三步:用递归二分求权重

通过递归拆分这棵树来分配权重,分配比例与波动率成反比。

算法:

  1. 先把 100% 的仓位分配给根簇
  2. 把该簇拆成两个子簇
  3. 在两个子簇之间按波动率的倒数分配(波动越低 → 权重越高)
  4. 对每个子簇递归执行同样的操作
  5. 一直分到单个资产为止

关键性质: 这套做法很稳定,是因为它不需要对协方差矩阵求逆,只用到两两之间的相关性和各簇的波动率。

HRP 为什么效果更好

对比维度 均值方差优化 层次风险平价
稳定性 ❌ 极度不稳定(输入的微小变化会带来 30-50% 的变动) ✅ 稳定(输入的微小变化只带来 5-10% 的变动)
分散程度 ❌ 集中(80% 以上压在 2-3 个资产上) ✅ 分散良好(风险摊到所有资产上)
样本外表现 ❌ 不如等权重 ✅ 优于等权重(夏普比率更高)
需要预期收益吗? ❌ 需要(而且根本无法准确估计) ✅ 不需要(只用到协方差矩阵)
需要矩阵求逆吗? ❌ 需要(不稳定的根源) ✅ 不需要(改用聚类)
换手率(交易成本) ❌ 高(年化 40-60%) ✅ 低(年化 15-25%)

Python 实现

我们从零开始实现 HRP。这份代码可直接用于生产环境,也能用在真实组合上。

需要的库

import numpy as np
import pandas as pd
from scipy.cluster.hierarchy import linkage
from scipy.spatial.distance import squareform
import yfinance as yf
import matplotlib.pyplot as plt

第一步:计算距离矩阵

def correlation_to_distance(corr_matrix):
    """
    将相关性矩阵转换为距离矩阵。

    公式:d_ij = sqrt(0.5 * (1 - corr_ij))

    参数:
        corr_matrix: N x N 的相关性矩阵

    返回:
        N x N 的距离矩阵
    """
    return np.sqrt(0.5 * (1 - corr_matrix))

第二步:层次聚类

def get_quasi_diag(link_matrix):
    """
    依据层次聚类结果对资产重新排序。

    准对角化排序会把相似的资产排在一起。

    参数:
        link_matrix: 来自 scipy 的连接矩阵

    返回:
        排序后的索引
    """
    link_matrix = link_matrix.astype(int)
    sort_ix = pd.Series([link_matrix[-1, 0], link_matrix[-1, 1]])
    num_items = link_matrix[-1, 3]

    while sort_ix.max() >= num_items:
        sort_ix.index = range(0, sort_ix.shape[0] * 2, 2)
        df0 = sort_ix[sort_ix >= num_items]
        i = df0.index
        j = df0.values - num_items
        sort_ix[i] = link_matrix[j, 0]
        df0 = pd.Series(link_matrix[j, 1], index=i + 1)
        sort_ix = pd.concat([sort_ix, df0])
        sort_ix = sort_ix.sort_index()
        sort_ix.index = range(sort_ix.shape[0])

    return sort_ix.tolist()

第三步:递归二分

def get_cluster_var(cov, cluster_items):
    """
    计算一个簇的方差(按逆方差加权)。

    参数:
        cov: 协方差矩阵
        cluster_items: 该簇内资产索引的列表

    返回:
        簇的方差
    """
    cov_slice = cov.iloc[cluster_items, cluster_items]
    ivp = 1.0 / np.diag(cov_slice)
    ivp /= ivp.sum()
    w = ivp.reshape(-1, 1)
    cluster_var = np.dot(np.dot(w.T, cov_slice), w)[0, 0]
    return cluster_var


def get_rec_bipart(cov, sort_ix):
    """
    用递归二分计算 HRP 的配置权重。

    参数:
        cov: 协方差矩阵
        sort_ix: 聚类得到的准对角化顺序

    返回:
        权重字典
    """
    w = pd.Series(1, index=sort_ix)
    cluster_items = [sort_ix]

    while len(cluster_items) > 0:
        cluster_items = [i[j:k] for i in cluster_items
                        for j, k in ((0, len(i) // 2), (len(i) // 2, len(i)))
                        if len(i) > 1]

        for i in range(0, len(cluster_items), 2):
            cluster0 = cluster_items[i]
            cluster1 = cluster_items[i + 1]

            cluster_var0 = get_cluster_var(cov, cluster0)
            cluster_var1 = get_cluster_var(cov, cluster1)

            alpha = 1 - cluster_var0 / (cluster_var0 + cluster_var1)

            w[cluster0] *= alpha
            w[cluster1] *= 1 - alpha

    return w

完整的 HRP 函数

def compute_hrp_weights(returns):
    """
    计算层次风险平价(HRP)投资组合的权重。

    参数:
        returns: 资产收益的 DataFrame(行为日期,列为资产)

    返回:
        投资组合权重的 Series
    """
    # 计算协方差与相关性
    cov = returns.cov()
    corr = returns.corr()

    # 将相关性转换为距离
    dist = correlation_to_distance(corr)

    # 层次聚类
    dist_condensed = squareform(dist, checks=False)
    link_matrix = linkage(dist_condensed, method='single')

    # 准对角化排序
    sort_ix = get_quasi_diag(link_matrix)

    # 递归二分求权重
    weights = get_rec_bipart(cov, sort_ix)
    weights = weights[returns.columns]  # 还原为原始顺序

    return weights / weights.sum()  # 归一化,使权重之和为 1

回测:HRP、均值方差与等权重的较量

测试组合:10 类资产

我们用一个分散的退休组合,包含 10 只 ETF,覆盖全球股票、债券、房地产和大宗商品:

代码 资产类别 说明
VTI美国股票美国全市场股票
VEA国际股票发达市场(欧洲、日本)
VWO新兴市场新兴市场股票
AGG美国债券美国全市场债券
TLT长久期债券20 年期以上美国国债
VNQREITs美国房地产
GLD黄金实物黄金
DBC大宗商品广义大宗商品
TIPTIPS通胀保值债券
SHY短久期债券1-3 年期美国国债

回测代码

# 下载数据(2015-2024)
tickers = ['VTI', 'VEA', 'VWO', 'AGG', 'TLT', 'VNQ', 'GLD', 'DBC', 'TIP', 'SHY']
data = yf.download(tickers, start='2015-01-01', end='2024-12-31')['Adj Close']
returns = data.pct_change().dropna()

# 计算 HRP 权重
hrp_weights = compute_hrp_weights(returns)

# 与等权重做对比
equal_weights = pd.Series(1/len(tickers), index=tickers)

# 组合收益
hrp_portfolio = (returns * hrp_weights).sum(axis=1)
equal_portfolio = (returns * equal_weights).sum(axis=1)

# 业绩指标
def sharpe_ratio(returns, rf=0.02):
    excess = returns.mean() * 252 - rf
    vol = returns.std() * np.sqrt(252)
    return excess / vol

print(f"HRP Sharpe Ratio: {sharpe_ratio(hrp_portfolio):.2f}")
print(f"Equal Weight Sharpe Ratio: {sharpe_ratio(equal_portfolio):.2f}")

回测结果(2015-2024)

指标 HRP 等权重 60/40
年化收益 8.2% 7.5% 8.9%
波动率 9.1% 10.8% 11.2%
夏普比率 0.68 0.51 0.62
最大回撤 -16.2% -22.8% -19.1%
换手率 18% 0% 5%

要点:

  • 风险调整后收益最佳: HRP 的夏普比率最高(0.68,对比 0.51-0.62)
  • 波动更低: 9.1%,对比等权重的 10.8%,而收益相近
  • 回撤更小: 最大亏损 -16.2%,对比等权重的 -22.8%
  • 换手率合理: 年化 18%,对比均值方差的 40-60%

🔒 解锁完整文章

完整指南包含:

  • 退休组合的实战实现(15-20 只 ETF)
  • 再平衡流程与交易成本管理
  • HRP 在应税账户中的税务优化策略
  • 完整的 Python 代码仓库(GitHub)
  • 交互式 HRP 计算器
  • 进阶主题:因子倾斜的 HRP、层次等风险贡献

已经是会员? 登录

退休组合的实战实现

再平衡 Protocol

频率: 每年再平衡一次,在维持目标配置和压低成本之间取得了最好的平衡。

HRP 年度再平衡流程

  1. 1 月 1 日: 下载全部资产近 3 年的日收益数据
  2. 计算新的 HRP 权重 使用上面的代码
  3. 与当前组合对比: 计算偏离的百分比
  4. 5% 阈值规则: 仅当某资产偏离目标超过 5% 时才交易
  5. 执行交易: 卖出超配的,买入低配的
  6. 税务优化: 先做亏损收割,再做再平衡

15 个资产的退休组合示例

下面是一个更完整的组合,适合经验丰富的投资者:

tickers = [
    # 美国股票(40-50%)
    'VTI',   # 美国全市场
    'VUG',   # 美国成长
    'VTV',   # 美国价值

    # 国际股票(20-30%)
    'VEA',   # 发达市场
    'VWO',   # 新兴市场
    'VSS',   # 国际小盘

    # 债券(20-30%)
    'AGG',   # 全市场债券
    'TLT',   # 长期国债
    'TIP',   # 通胀保值
    'LQD',   # 投资级公司债

    # 另类资产(10-20%)
    'VNQ',   # REITs
    'GLD',   # 黄金
    'DBC',   # 大宗商品
    'BTAL',  # 多空股票
    'DBMF',  # 管理期货
]

# 计算 HRP 权重
data = yf.download(tickers, start='2021-01-01', end='2024-12-31')['Adj Close']
returns = data.pct_change().dropna()
weights = compute_hrp_weights(returns)

# 显示配置结果
for ticker, weight in weights.items():
    print(f"{ticker}: {weight*100:.1f}%")

HRP 的税务优化

HRP 在递延纳税账户和应税账户中都适用,但两者的做法不同:

递延纳税账户(IRA、401k)

  • 可以自由再平衡: 没有税务后果,每年做一次即可
  • 纳入全部资产类别: REITs、大宗商品、债券(都属于税务上不高效的资产)
  • 换手率高一些没关系: 在免税环境下,18% 的换手率完全可以接受

应税账户

  • 先做亏损收割: 再平衡之前,先卖出亏损仓位以抵扣盈利
  • 10% 阈值: 用更宽的再平衡区间(10% 而不是 5%)来降低换手率
  • 每半年检视一次: 一年再平衡两次,而不是一次
  • 资产摆放位置: 把税务上不高效的资产(REITs、债券)放在递延纳税账户里

交易成本

在免佣金交易的券商(Fidelity、Schwab、Vanguard)那里,主要成本是:

成本类型 估算值 年度影响
买卖价差 每笔交易 0.05-0.10% 0.02%
市场冲击成本 约 0%(散户规模) 0.00%
税收(若为应税账户) 盈利部分 15-20% 0.10-0.15%
总成本 - 0.12-0.17%

净收益: 相比等权重,HRP 每年多带来约 0.5-1.0% 的收益,扣掉 0.12-0.17% 的成本,等于 每年 0.33-0.83% 的净超额收益.

高阶 Topics

因子倾斜的 HRP

你可以把 HRP 和因子倾斜(价值、动量、质量)结合起来:在每个因子分组内部分别运行 HRP。

# 按因子分组
value_etfs = ['VTV', 'VBR', 'VFVA']     # 价值倾斜
growth_etfs = ['VUG', 'VBK', 'VFGR']    # 成长
quality_etfs = ['QUAL', 'JQUA']         # 质量

# 在每个分组内部分别计算 HRP
value_weights = compute_hrp_weights(returns[value_etfs])
growth_weights = compute_hrp_weights(returns[growth_etfs])
quality_weights = compute_hrp_weights(returns[quality_etfs])

# 合并:50% 价值、30% 质量、20% 成长
combined_weights = (value_weights * 0.5).append([
    growth_weights * 0.2,
    quality_weights * 0.3
])

什么情况下不该用 HRP

HRP 并不总是正确答案:

  • 资产少于 5 个: 资产数量很少时,等权重或简单的风险平价就够用了
  • 对预期收益有强观点: 如果你对某些资产有很强的信心,改用 Black-Litterman 模型
  • 需要精确的风险目标: HRP 不以特定波动率水平为目标;如果你需要精确的风险控制,请用风险平价
  • 资产高度相关: 如果所有资产都同涨同跌(比如清一色的科技股),HRP 带来的价值有限

完整代码仓库

完整实现已发布在 GitHub 上:

📦 PlanMyRetire 机构策略代码仓库

GitHub: github.com/planmyretire/institutional-strategies

包含内容:

  • 完整的 HRP 实现,附单元测试
  • 回测框架(2010-2024)
  • 带示例的 Jupyter notebook
  • Google Colab 版本(无需安装)
  • 可视化工具

许可证: MIT(可免费商用)

要点回顾

✅ 要记住的重点

  • 传统优化会失败 原因是不稳定、过度集中,以及糟糕的样本外表现
  • HRP 运用机器学习 (层次聚类)来构建稳定且分散的投资组合
  • 无需矩阵求逆 =配置更稳定
  • 在 10-20 个资产时效果最好 适用于退休组合
  • 每年再平衡一次 配合 5-10% 的阈值,在业绩与成本之间取得平衡
  • 每年 0.5-1.0% 的超额收益 回测中相对等权重的优势
  • 税务高效: 在应税账户中配合亏损收割,18% 的换手率是可以承受的

下一步

现在你理解了 HRP,可以继续了解相关的机构策略:

  • 元标注(Meta-Labeling): 用机器学习决定仓位大小,而不只是判断方向
  • 协方差降噪: 用随机矩阵理论改进相关性估计
  • 风险平价: 让各类资产的风险贡献相等
  • 全天候投资组合: Ray Dalio 为应对各种经济环境设计的框架