层次风险平价:当机器学习遇上投资组合构建
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 法则。"
为什么会这样:协方差矩阵求逆的诅咒
均值方差优化必须 对协方差矩阵求逆。问题就出在这里:
- 估计误差: 当资产数 N=10 时,你需要估计 55 个相关系数。用 5 年的月度数据(60 个观测值),平均每个参数只摊到 1 个数据点。噪声占了主导。
- 病态矩阵: 较小的特征值(对应几乎冗余的资产)在求逆时会被放大,导致配置走向极端。
- 过拟合: 优化器找到的组合能完美拟合历史噪声,却在样本外一败涂地。
机构投资者早就明白这一点。 所以 Bridgewater、AQR 和其他量化基金并不使用均值方差优化。他们用的是更好的方法,比如 HRP。
The 解决方案: 层次风险平价 (HRP)
核心洞见
与其把所有资产当成彼此独立(那就必须做矩阵求逆),不如换个思路: HRP 认识到资产是按层次聚成簇的:
- 美国大盘股和美国小盘股之间的相似度,高于它们与黄金的相似度
- 各类股票彼此之间的相似度,高于它们与债券的相似度
- 资产天然形成一个层次结构:股票 → 美国/国际 → 大盘/小盘/价值/成长
HRP 的做法: 用层次聚类把相似资产归为一组,然后在各个簇之间(而不是各个资产之间)平均分配风险。这样就完全绕开了矩阵求逆。
HRP 算法的三个步骤
第一步:构建距离矩阵
把相关性转换成距离。走势同步的资产彼此"近",走势不同步的彼此"远"。
公式: dij = √(½(1 - ρij))
直观理解: 若相关性为 1(完全正相关),距离为 0;若相关性为 -1,距离为 1。
第二步:层次聚类
按距离把资产组织成一棵树(树状图)。
方法: 单连接聚类
- 起点:每个资产各自成为一个簇
- 迭代:合并距离最近的两个簇
- 重复:直到所有资产归入同一棵树
结果: 得到一个层次结构,显示哪些资产最相似(最先合并)、哪些最不相似(最后合并)。
第三步:用递归二分求权重
通过递归拆分这棵树来分配权重,分配比例与波动率成反比。
算法:
- 先把 100% 的仓位分配给根簇
- 把该簇拆成两个子簇
- 在两个子簇之间按波动率的倒数分配(波动越低 → 权重越高)
- 对每个子簇递归执行同样的操作
- 一直分到单个资产为止
关键性质: 这套做法很稳定,是因为它不需要对协方差矩阵求逆,只用到两两之间的相关性和各簇的波动率。
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 年期以上美国国债 |
| VNQ | REITs | 美国房地产 |
| GLD | 黄金 | 实物黄金 |
| DBC | 大宗商品 | 广义大宗商品 |
| TIP | TIPS | 通胀保值债券 |
| 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%
退休组合的实战实现
再平衡 Protocol
频率: 每年再平衡一次,在维持目标配置和压低成本之间取得了最好的平衡。
HRP 年度再平衡流程
- 1 月 1 日: 下载全部资产近 3 年的日收益数据
- 计算新的 HRP 权重 使用上面的代码
- 与当前组合对比: 计算偏离的百分比
- 5% 阈值规则: 仅当某资产偏离目标超过 5% 时才交易
- 执行交易: 卖出超配的,买入低配的
- 税务优化: 先做亏损收割,再做再平衡
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 为应对各种经济环境设计的框架