俗话说「别把鸡蛋放进同一个篮子」,很多同学听完就去做了一个「篮子扩建工程」:写三个策略,钱分三份,以为从此高枕无忧。可如果三个策略同涨同跌——比如三个都是追涨杀跌的动量策略——行情不好时它们会手拉手一起亏,你只是把鸡蛋放进了三个一模一样的篮子。
上一篇我们给单个策略装上了止盈止损和仓位规则(第 35 篇),解决的是「一个策略失控怎么办」;这一篇再往前走一步:多个策略放在一起,怎么放才能真的更稳?风控管的是单个策略的极端情况,组合管的是策略之间的「连坐」问题——这也是策略篇的收官一课。
本篇我们用最直观的方式把组合这件事做一遍:算相关性、合并净值、按两种方式分配权重、画一张相关性热力图。最后讲一个新手第一年几乎必犯的认知错误——策略组合不等于收益叠加。
本篇目标
- 能用相关系数判断两个策略是「真分散」还是「假分散」
- 能把多个策略的日收益按权重合并成一条组合净值曲线,并算出组合的年化收益、夏普与最大回撤
- 能实现等权与波动率倒数加权两种权重分配,并说清各自的适用场景
- 能用
corr()加热力图把策略间的相关性一图看清 - 能向别人解释:为什么「组合降低的是波动,而不是消除亏损」
核心概念
分散的数学直觉:相关性说了算
两个策略合在一起,波动会不会变小,由相关系数(Correlation Coefficient)决定。相关系数 ρ 取值 -1 到 1:1 表示完全同向,0 表示没关系,-1 表示完全反向。
两个策略等权组合的波动(用方差写):
σ_p² = w₁²σ₁² + w₂²σ₂² + 2·w₁·w₂·ρ·σ₁·σ₂
白话解释:前两项是两个策略各自的波动贡献,最后一项是「互动项」。只要 ρ < 1,互动项就比「完全同步」时小,组合波动就小于两策略波动的加权平均——这就是分散的全部来源。ρ 越接近 0 省得越多,ρ = -1 时甚至能互相抵消;反过来 ρ = 1 时公式退化成加权平均,组合等于「一个更大的单一策略」,白忙一场。
划重点:分散的前提是低相关,不是「数量多」。三个相关系数 0.9 的策略抱在一起,本质上还是一个策略,只是手续费翻了三倍。
合并的是日收益,不是净值曲线
把策略 A、B、C 组合起来,正确姿势只有一种:每个交易日的组合收益 = 各策略当日收益按权重加权求和:
r_组合,t = w₁·r_A,t + w₂·r_B,t + w₃·r_C,t
然后把这条日收益序列滚成净值。错误姿势是把三条净值曲线直接相加——那等于每个策略都投入了全仓资金,总仓位凭空变成 300%。本篇代码里会专门演示这个错误,让你亲眼看到两者的差别。
两种最常用的权重分配
等权(Equal Weight):每个策略 1/N。逻辑简单、不易出错,是新手默认选项。
波动率倒数加权(Inverse Volatility Weight):w_i = (1/σ_i) / Σ(1/σ_j),谁的波动小、谁的份额大。白话:让安静的策略多出力,让上蹿下跳的策略少掺和。它是「风险平价」(Risk Parity)思想的简化版——风险平价追求每个策略对组合风险的贡献相等,本篇不展开,知道有这回事就行。
相关性矩阵与热力图
DataFrame.corr() 一次算出所有策略两两之间的相关系数,对角线恒为 1。矩阵用肉眼扫很累,画成热力图(Heatmap)后,谁和谁「穿一条裤子」一眼就能看出来:偏红的格子是危险信号,偏绿的才是分散的来源。
教学提醒:组合不等于收益叠加
最后是本篇最重要的一段话,请反复读三遍:
- 组合优化的对象是波动与回撤,不是收益。组合的期望收益近似等于各策略期望收益的加权平均,一分钱都不会凭空多出来。
- 分散在最需要它的时候最容易失效:危机来临、流动性枯竭时,各类资产、各类策略的相关性会集体冲向 1,也就是「跌的时候一起跌」。
- 权重不是一劳永逸的。波动和相关都会漂移,需要定期重新估计、重新校准,这也是第 24 篇「定期再平衡」思想在策略层面的延续。
组合的意义是让净值曲线更平滑、让你在极端日子里拿得住、把策略执行下去——它解决的是「存活率」问题,不是印钞机问题。
完整代码
依赖安装:uv pip install numpy pandas matplotlib。本篇用造数据模拟三种典型的策略收益形态(真实用法:把你每个策略回测输出的日收益分别贴进 rets 即可,结构完全一样)。
# —— 第一块:造三个策略的日收益,算相关性,做两种加权 ——
import numpy as np
import pandas as pd
rng = np.random.default_rng(42) # 固定种子,结果可复现
dates = pd.bdate_range("2020-01-01", periods=1000) # 1000 个交易日
# 策略A:趋势型,日波动约 0.95%
ret_a = rng.normal(0.0004, 0.0095, size=1000)
# 策略B:低波动型,日波动约为 A 的一半
ret_b = rng.normal(0.00025, 0.005, size=1000)
# 策略C:大部分时间跟着 A 走(相关约 0.74),用来演示「假分散」
ret_c = 0.7 * ret_a + rng.normal(0.0002, 0.006, size=1000)
rets = pd.DataFrame({"策略A": ret_a, "策略B": ret_b, "策略C": ret_c}, index=dates)
corr = rets.corr() # 相关性矩阵:对角线恒为 1
print("策略间相关系数:")
print(corr.round(3))
# 等权:每个策略 1/3
eq_w = pd.Series(1 / 3, index=rets.columns)
# 波动率倒数:波动越小,权重越大
vol = rets.std()
inv_vol_w = (1 / vol) / (1 / vol).sum()
print("\n波动率倒数权重:")
print(inv_vol_w.round(3))
comb_eq = rets @ eq_w # @ 是矩阵乘法:相当于日收益按权重加权求和
comb_iv = rets @ inv_vol_w
# —— 第二块:错误示范与正确做法对比 ——
# 错误:把三条净值曲线直接相加(每个策略都投了全仓,总仓位 300%)
nav_wrong = rets.add(1).cumprod().sum(axis=1)
# 正确:日收益按权重合并,再滚成组合净值
nav_right = (1 + comb_eq).cumprod()
print(f"错误做法期末值(钱凭空变成三份):{nav_wrong.iloc[-1]:.2f}")
print(f"正确做法期末净值: {nav_right.iloc[-1]:.2f}")
# —— 第三块:绩效对比 + 相关性热力图 ——
import matplotlib.pyplot as plt
plt.rcParams["font.sans-serif"] = ["SimHei"] # 让图里能显示中文
plt.rcParams["axes.unicode_minus"] = False
def perf(ret: pd.Series) -> dict:
"""输入日收益序列,返回核心指标(无风险利率按系列统一口径:年化 2%)"""
nav = (1 + ret).cumprod() # 组合净值
ann_ret = nav.iloc[-1] ** (244 / len(ret)) - 1 # 年化收益(一年约 244 个交易日)
ann_vol = ret.std() * np.sqrt(244) # 年化波动
sharpe = (ann_ret - 0.02) / ann_vol # 夏普比率
mdd = (nav / nav.cummax() - 1).min() # 最大回撤
return {"年化收益": ann_ret, "年化波动": ann_vol, "夏普": sharpe, "最大回撤": mdd}
rows = {}
for name, r in [("策略A", rets["策略A"]), ("策略B", rets["策略B"]),
("策略C", rets["策略C"]), ("等权组合", comb_eq),
("倒波动组合", comb_iv)]:
rows[name] = perf(r)
print(pd.DataFrame(rows).T.round(3))
avg_member_vol = rets.std().mean() * np.sqrt(244)
print(f"\n成员平均年化波动:{avg_member_vol:.3f},等权组合年化波动:{perf(comb_eq)['年化波动']:.3f}")
# 热力图:颜色越红越相关
fig, ax = plt.subplots(figsize=(5, 4))
im = ax.imshow(corr, cmap="RdYlGn_r", vmin=-1, vmax=1)
ax.set_xticks(range(len(corr)))
ax.set_xticklabels(corr.columns)
ax.set_yticks(range(len(corr)))
ax.set_yticklabels(corr.columns)
for i in range(len(corr)): # 在每个格子里写上数值
for j in range(len(corr)):
ax.text(j, i, f"{corr.iloc[i, j]:.2f}", ha="center", va="center")
fig.colorbar(im)
ax.set_title("策略相关性热力图(示意数据)")
plt.tight_layout()
plt.show()
# —— 第四块:逐年度拆开看,检验「分散」是否年年成立 ——
yearly = (1 + pd.DataFrame({"策略A": rets["策略A"], "策略B": rets["策略B"],
"策略C": rets["策略C"], "等权组合": comb_eq})
).groupby(rets.index.year).prod() - 1 # 每年累计收益
print(yearly.round(3))
worst = yearly[["策略A", "策略B", "策略C"]].min(axis=1) # 每年表现最差的成员
print("\n每年最差成员 vs 等权组合(看组合是否常年「不垫底」):")
print(pd.DataFrame({"最差成员": worst, "等权组合": yearly["等权组合"]}).round(3))
代码逐段解释
- 第一块造数据与加权:
策略C = 0.7 × 策略A + 自身噪声,所以 C 与 A 的相关系数天然接近 0.74——这正是「看起来是三个策略、其实是两个半」的假分散样本。rets @ eq_w一行完成加权求和,等价于每天算1/3·r_A + 1/3·r_B + 1/3·r_C。波动率倒数权重里,策略 B 因为波动小,分到的份额明显更大。 示例输出(示意,种子固定但你的库版本可能带来微小出入,以实际运行为准):
策略间相关系数:
策略A 策略B 策略C
策略A 1.000 0.021 0.741
策略B 0.021 1.000 0.015
策略C 0.741 0.015 1.000
波动率倒数权重:
策略A 0.253
策略B 0.480
策略C 0.267
-
第二块错误示范:净值相加后期末值约 4.25,正确做法只有约 1.43——差出来的 3 倍不是赚的钱,是你凭空多投的两份本金。示例输出(示意):
错误做法期末值(钱凭空变成三份):4.25、正确做法期末净值:1.43。 -
第三块绩效表与热力图:重点看夏普一列——两个组合的夏普都高于三个成员中的任何一个。组合没有让收益变多(等权组合年化收益 0.092,低于策略 C 的 0.117),它做的是把波动从成员平均的 0.122 压到 0.093。热力图上 A-C 那对红色格子(0.74)就是本例里的「假分散」坐标。 示例输出(示意):
年化收益 年化波动 夏普 最大回撤
策略A 0.098 0.148 0.527 -0.163
策略B 0.061 0.078 0.526 -0.088
策略C 0.117 0.140 0.693 -0.148
等权组合 0.092 0.093 0.774 -0.098
倒波动组合 0.085 0.079 0.823 -0.084
划重点:读这张表的方法——先看波动列和回撤列,再看夏普列,最后才看收益列。组合的价值全部写在前两列里。
- 第四块逐年度检验:分散是否有效不能只看全样本。逐年拆开后你会发现,组合几乎从不「垫底」,但也没有哪一年「暴富」——它就是一条更平滑的路。示例输出(示意):每年最差成员常常是负数,而等权组合的年度结果大多在 0 附近或以上。
常见坑与小练习
常见坑
- 净值曲线直接相加。现象:组合期末值「翻了几倍」,兴奋地以为发现了印钞机。原因:净值相加等于每个策略各投一份全仓资金,总仓位 300%。避免:永远用日收益加权合并,本篇第二块就是现成的对照。
- 拿平静年份的相关性外推未来。现象:回测里相关性 0.2 的两个策略,实盘中同涨同跌。原因:危机时相关性会系统性上升。避免:除全样本相关外,专门算一遍「表现最差的 10% 交易日」里的相关性,看极端时期的抱团程度。
- 只看收益相关,不看回撤重叠。现象:两个策略日收益相关不高,但最大回撤发生在同一段行情里。原因:日频相关会漏掉低频的共同风险敞口(比如都押了同一个行业)。避免:把两条回撤曲线画在同一张图上对比时间是否重合。
- 权重算完就不管了。现象:半年后组合的波动悄悄回升。原因:各策略的波动与相关都在漂移,权重随之失效。避免:像第 24 篇那样设定固定的再校准频率(如每季度重算一次权重)。
小练习
- 把
ret_c里的系数 0.7 改成 0.95 再跑一遍,对比等权组合的夏普与最大回撤变化,体会「假分散」如何失效。提示:相关系数升到 0.95 后,A-C 几乎是同一个策略,组合波动会明显逼近成员平均水平。
- 实现「波动率平方倒数加权」:
w_i = (1/σ_i²) / Σ(1/σ_j²),与波动率倒数加权对比谁的低波动策略权重更大。提示:平方后进一步放大了低波动策略的份额,这是风险平价的更激进近似。
- 把你自己在前面篇目里写过的两个策略(比如第 18 篇的双均线与第 28 篇的均值回归)的日收益各存成一份 CSV,读入后替换本篇的造数据,跑一遍完整流程。
提示:两个 CSV 各需要一列日期、一列日收益,读入后按日期对齐(
join或concat(axis=1)),缺失值用 0 填充(当天没持仓就记 0 收益)。
风险提示:本文策略仅用于教学,不构成投资建议,实盘前请充分测试。