做投资的朋友应该都有过这样的体验:手里拿了三五只股票,自以为已经分散了风险,结果大盘一调整,持仓几乎齐刷刷地往下掉。这时候才意识到,所谓的分散,远不只是“多买几只”那么简单。如果选到的资产彼此高度相关,那不管买多少只,风险其实都还抱成一团。
这个问题的答案,其实就是“投资组合优化”存在的原因。我入行做资产配置这几年,从最开始凭感觉搭组合,到后来老老实实用现代投资理论里的均值-方差框架去跑模型,中间踩了不少坑,也实打实感受到数据模型对投资的帮助。这篇文章就从我自己的实战视角出发,把投资组合优化的原理、建模方式、实操步骤和常见坑位完整梳理一遍。内容会比较适合两类朋友:一个是刚接触量化配置的个人投资者,另一个是想把分散投资从“口号”真正落到“计算”上的从业者。
先给你吃个定心丸:这篇文章不会堆一堆看不懂的数学,核心是讲清楚“为什么这样做”和“怎么一步步落地”。我在讲原理的时候会尽量拿生活化的例子类比,实操部分会附上可以直接跑的Python代码,你把这套流程跑通之后,就能把它用到自己的资产组合、基金组合甚至任何类型的多资产配置场景里。
1. 先从根上理解:投资组合优化到底在优化什么
很多人第一次听到“投资组合优化”这个词,下意识会以为这是个选股模型,用来找“哪只股票能涨”。这个理解其实偏差挺大的。投资组合优化做的事情并不是预测未来,而是在你已有的资产池里,找到一组权重分配,让整个组合在你愿意承受的风险范围内,收益尽量高;或者说,在你期望的收益目标下,风险尽量低。选的还是那些资产,但买多少、配多少,这就是模型要回答的问题。
现代投资组合理论的起点,是马科维茨在1952年提出的均值-方差模型,也叫现代组合理论(Modern Portfolio Theory,MPT)。这个理论拿过诺贝尔经济学奖,核心贡献就是把投资组合的“收益”和“风险”用两个数学指标量化了:收益用期望收益率来衡量,风险用收益率的标准差(波动率)来衡量。然后在此基础上,通过数学最优化方法,找到一套最佳的资金分配比例。
听起来好像不太难,但这里有一个非常关键、也非常反直觉的洞察:组合的风险并不等于各资产风险的简单加权平均。因为资产和资产之间是有相关性的,两个资产单独看波动都很大,但如果它们的走势刚好互补,一组亏损另一组盈利,那组合起来的整体波动反而会被熨平。马科维茨真正的贡献,就是第一次用数学语言把这个“分散化”效应给形式化了。
所以投资组合优化解决的核心问题,概括起来就是三件事:
- 如何量化单个资产的收益和风险;
- 如何量化多个资产之间的相关关系;
- 如何在“收益最大化”和“风险最小化”这两个互相冲突的目标之间,找到最合适的平衡点。
这里那组最好的平衡点连成一条线,就是传说中的“有效前沿”(Efficient Frontier)。在这条线上的任何一个组合,都是在同风险下收益最高的,或者在同收益下风险最低的。而你要做的,就是在有效前沿上挑一个最适合自己的位置。这个位置怎么挑?取决于你个人的风险承受能力——这也就是为什么同样的模型、同样的数据,不同的人会得到不同的“最优配置”。
提示:投资组合优化不是用来预测哪只股票涨的,它是在给定数据和风险偏好下,回答“各资产配多少”的数学工具。理解这一点,后面所有操作都不会跑偏。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心概念拆解:收益、风险与相关性
要动手建模之前,先把几个最基础的概念嚼透了。这几个概念在教科书里可能只是一行公式,但在实操中,每个都藏着自己的坑。
2.1 期望收益率:历史数据的平均还是未来预期的估计?
期望收益率,简单说就是“买入这个资产后,平均每期大概能赚多少”。在模型里,最常用的估算办法是取历史收益率的算术平均值。比如某只基金过去12个月的月度收益率分别是1%、2%、-1%、0.5%……把这些数加起来除以12,就当成了未来每个月的期望收益率。
听起来合理,但你要知道这里面的假设很强的:历史会重复。实际市场里,过去的平均收益并不等于未来的预期收益,尤其是市场风格切换的年份,用长期历史均值去预测短期未来,偏差会非常大。这一点在我后面“实操中的坑”部分会专门展开,现在先埋个伏笔。
实操里还有一个常见的做法:给不同的历史时点加权,近期的数据给更高的权重,远期的给低权重,也就是所谓的“指数加权平均”。这个办法能在一定程度上让模型更“贴近当下”,但也会带来一个问题——对短期波动过度敏感。我个人建议,起步阶段先用最简单的算术平均,把流程跑通,再去折腾加权方案。
2.2 风险为什么要用标准差来衡量
风险用标准差(或者说方差)来度量,这一点在扣细节的时候要自己想清楚。标准差描述的是“收益率偏离平均值的程度”,偏离越大,说明收益越不稳定,风险越高。
这有个天然的问题:在投资者心里,“亏钱”和“赚钱”带来的风险感受是完全不一样的。标准差分不清向上波动和向下波动,它把这两者一视同仁,都算作风险。这在学术界叫“方差作为风险度量”的缺陷。实际应用中,有人会改用下行标准差或者“在险价值”(Value at Risk,VaR)来衡量风险,这属于风险度量的进阶玩法,但对新手来说,标准差的优点是简单、稳定、好算,而且它已经足够支撑起组合优化的核心逻辑了。
注意:标准差度量的是“波动”,不是纯粹的“亏损风险”。如果你日后接触到更多风控工具,可以考虑用下行标准差、最大回撤等指标做补充,但作为第一版模型,标准差是性价比最高的选择。
2.3 相关性:组合风险的核心变量
这里要花点篇幅讲清楚,因为这是理解整个组合优化的钥匙。
先看一个简单例子。假设你手里有两个资产,各自年化标准差都是20%,如果它们之间的相关系数是0,那么各配50%的组合标准差是多少?很多人会直觉认为是20%,其实算下来约是14.14%,比单个资产下降了近三成。这就是分散化的数学本质:组合的波动不是各资产波动的加权,而是要看它们之间的相关性。
组合方差的计算公式是:
- 当只有两个资产时:σp² = w₁²σ₁² + w₂²σ₂² + 2w₁w₂σ₁σ₂ρ₁₂
其中 w 是权重,σ 是标准差,ρ 是两个资产的相关系数。这里最关键的就是最后那一项 ρ₁₂。如果 ρ₁₂ = 1,组合的标准差就是两个资产标准差的加权平均,分散化一点效果都没有;如果 ρ₁₂ = -1,理论上组合甚至可以做到零风险;如果 ρ₁₂ 在0附近,那就处在两者之间。
放到实际场景里来理解:你配置了10只科技股,表面上“很分散”,但它们同涨同跌,相关性接近1,那整个组合的波动其实还是相当大的。反过来,你拿3只科技股加上2只公用事业股,虽然数量少,但因为两个行业的相关性低,组合整体波动反而更平滑。这是很多投资者最容易忽略的一点:分散化的关键是低相关,不是数量多。
所以在建模的时候,协方差矩阵就是整个模型的灵魂。它记录了每个资产自身的方差,也记录了任意两个资产之间的协方差(协方差的标准化形式就是相关系数)。后面你会发现,很多组合优化结果“不合理”,问题往往不是出在收益率的估计上,而是出在协方差矩阵的估计上。
2.4 有效前沿:所有“好组合”的集合
把上面的收益和风险放到一张图上,横轴是标准差,纵轴是期望收益,每一个权重分配方案都是图上的一点。把所有“在同风险下收益最高”的点连起来,就能画出一条曲线,这就是有效前沿。曲线上方的区域在数学上不可达,曲线下方的区域都是“次优选择”。
有效前沿曲线的形状,本质上是资产间相关性的体现。相关性越低,曲线越向左上方弯曲,代表分散化的效果越好;相关性越高,曲线越接近一条直线,分散化的意义就越小。
有了有效前沿之后,投资组合优化就变成了一个非常清晰的问题:在曲线上找一个点,这个点代表的组合让你的“效用”最大。教科书里的做法是引入一个无风险收益率(比如国债收益率),然后找到无风险资产和有效前沿的切点,这个切点就是“最优风险组合”。在实际的个人投资操作中,通常更简单的做法是直接设定一个目标收益率,然后让模型去最小化风险,或者设定一个最大风险容忍度,让模型去最大化收益。
3. 动手实现:用Python一步步构建组合优化模型
理论讲完了,是该上手的时候了。这套流程我给很多朋友推荐过,只要你本地有Python环境,哪怕没装金融数据库,用免费的接口也能把整个流程跑起来。我这里的示例会尽量完整,你可以直接复制下来改一改,用在自己的资产池上。
3.1 第一步:准备数据
构建模型的第一步是搞定数据。我们需要的是每只资产的历史价格序列。时间段上,我个人习惯用日线数据,拉两年左右(大约500个交易日),这样样本量足够,统计上比较稳定。
数据源方面,目前国内能用的免费接口里,比较推荐akshare和tushare。如果你是做美股或者全球资产,yfinance也可以。这里我以A股ETF为例,用akshare来拉数据。
python复制import akshare as ak
import pandas as pd
import numpy as np
# 定义一个资产池:5只不同行业的ETF(仅作示例,不构成投资建议)
tickers = {
"510300": "沪深300ETF",
"510500": "中证500ETF",
"159915": "创业板ETF",
"512800": "银行ETF",
"518880": "黄金ETF",
}
# 拉取最近500个交易日的收盘价
price_data = {}
for code in tickers.keys():
df = ak.fund_etf_hist_em(
symbol=code,
period="daily",
start_date="20220101",
end_date="20241231",
adjust="qfq"
)
df = df.set_index("日期")
df.index = pd.to_datetime(df.index)
price_data[tickers[code]] = df["收盘"]
prices = pd.DataFrame(price_data).dropna()
print(prices.tail())
这边有两个细节要注意。第一是复权问题,ETF有分红和拆分,如果不做复权处理,价格序列会出现人为的跳空,直接影响收益率计算,建议一律用“前复权”数据。第二是日期对齐,不同资产的交易日期可能不完全一致,比如有的基金在某些交易日没有成交,直接合并DataFrame后要先dropna把缺失行删掉,避免后面计算协方差矩阵时出现NaN。
3.2 第二步:计算收益率与协方差矩阵
数据到手之后,下一步就把收盘价转换成收益率序列。一般用对数收益率,因为它具备时间可加性,更符合正态分布假设,计算上也更友好。不过如果只是做组合优化,用简单收益率(今天比昨天涨了百分之几)结果差异不大,我自己常用的是简单百分比收益率。
python复制# 计算日度收益率
returns = prices.pct_change().dropna()
# 年化期望收益率(日收益率均值 * 252)
mu = returns.mean() * 252
# 年化协方差矩阵(日协方差 * 252)
cov = returns.cov() * 252
print("年化期望收益率:")
print(mu.round(4))
print("年化协方差矩阵:")
print(cov.round(6))
这里乘以252是因为A股一年大约有252个交易日。如果你拉的是月度数据,就要乘以12。这一步很多人会漏掉,导致后续算出来的风险和收益量纲不统一,最优解也会跟着跑偏。
3.3 第三步:先跑一遍蒙特卡洛模拟,找找感觉
在敲最优化代码之前,我建议先做一次蒙特卡洛模拟。简单说就是随机生成大量权重组合,比如生成10万组,每一组都算一遍收益和风险,然后把这些点全部画在一张散点图上。这样你就能直观地看到“资产有哪些组合方式”以及“有效前沿大概长什么样”。这一步虽然不会直接给你最优解,但能让你对可行域的形状有个感性认识。
python复制# 蒙特卡洛模拟
num_portfolios = 100000
results = np.zeros((3, num_portfolios))
np.random.seed(42)
for i in range(num_portfolios):
weights = np.random.random(len(tickers))
weights /= np.sum(weights)
port_return = np.dot(weights, mu)
port_vol = np.sqrt(weights.T @ cov @ weights)
results[0, i] = port_return
results[1, i] = port_vol
# 夏普比率(假设无风险利率为2%)
results[2, i] = (port_return - 0.02) / port_vol
# 转成DataFrame方便画图
sim_df = pd.DataFrame(results.T, columns=["return", "volatility", "sharpe"])
print(sim_df.sample(5))
跑完模拟之后,你可以找一下这10万组里夏普比率最高的那一组,把它打印出来。这个结果虽然不保证是最优的,但它往往能给你一个很好的起点,也能让你快速发现一些特征,比如某个资产的权重被分到了很极端的数值。
sharpe最高的组合,往往就是“又准又稳”的参考系。我早期做模型的时候,经常先跑一遍蒙特卡洛,再拿结果去跟后面的数学最优化对照,如果两者差异很大,那说明代码或数据哪个地方出了问题。
3.4 第四步:用scipy做最优化求解
蒙特卡洛是在“大海捞针”,真正要拿到精确的最优解,还是得靠最优化求解器。Python里最常用的是scipy.optimize。
先演示一个最常见的场景:在给定目标收益率的情况下,最小化组合风险。这是一个典型的有约束最优化问题。
python复制from scipy.optimize import minimize
def portfolio_volatility(weights, cov):
return np.sqrt(weights.T @ cov @ weights)
# 约束条件:权重和等于1
def check_sum(weights):
return np.sum(weights) - 1
# 目标收益率设为15%
target_return = 0.15
def min_vol_for_target(weights, mu, cov, target):
port_return = np.dot(weights, mu)
# 加入惩罚项,让收益尽量贴近目标
return portfolio_volatility(weights, cov) + 1e3 * (port_return - target) ** 2
# 初始权重:等权
init_weights = np.ones(len(tickers)) / len(tickers)
constraints = {"type": "eq", "fun": check_sum}
bounds = tuple((0, 1) for _ in range(len(tickers))) # 限制最低0、最高1,不允许做空
result = minimize(
min_vol_for_target,
init_weights,
args=(mu, cov, target_return),
method="SLSQP",
bounds=bounds,
constraints=constraints,
options={"maxiter": 1000}
)
opt_weights = result.x
opt_return = np.dot(opt_weights, mu)
opt_vol = portfolio_volatility(opt_weights, cov)
print("最优权重:")
for name, weight in zip(tickers.values(), opt_weights):
print(f"{name}: {weight:.4f}")
print(f"组合年化收益: {opt_return:.4f}, 年化波动: {opt_vol:.4f}")
这里有几个写法上的细节值得说明。
第一个是把目标收益率的约束写成了损失函数里的惩罚项。严格来说,这不算真正的等式约束,而是一种“软约束”。好处是求解过程更稳定,不容易因为约束条件太硬而找不到可行解;坏处是结果可能跟目标收益率有一定偏差。如果你希望严格约束,可以改用下面的方式,把约束条件明确传给minimize函数:
python复制from scipy.optimize import LinearConstraint
constraints = [
{"type": "eq", "fun": check_sum},
{"type": "eq", "fun": lambda weights: np.dot(weights, mu) - target_return}
]
这两个方法我都用过,实际体验是:如果资产池比较大、相关性比较高,硬约束经常会导致求解器报“Inequality constraints incompatible”之类的错误。碰到这种情况,用惩罚项的写法往往更省心。我自己的习惯是先用惩罚项快速过一遍,再根据结果决定要不要换回硬约束。
第二个是bounds参数,也就是权重上下界。我在这里限制每个资产权重0到1之间,意思是只做多,不做空,也没有加杠杆。对大多数个人投资者来说,这个约束是合理的,毕竟做空和加杠杆的门槛和风险都比较高。但要注意,一旦加了这种约束,数学上的有效前沿会变成“受限前沿”,形状和教科书里的可能不太一样,这是正常的。
3.5 第五步:计算有效前沿曲线
把上面求解目标收益率的过程,从0%到25%取几十个点,每个点都跑一遍最优化,就可以描出完整的有效前沿曲线。这条曲线可以直接画图观察,也可以用来做后续的“选点”决策。
python复制targets = np.linspace(0.05, 0.25, 30)
frontier_vols = []
for target in targets:
res = minimize(
min_vol_for_target,
init_weights,
args=(mu, cov, target),
method="SLSQP",
bounds=bounds,
constraints={"type": "eq", "fun": check_sum},
options={"maxiter": 1000}
)
frontier_vols.append(portfolio_volatility(res.x, cov))
# 把结果画出来
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6))
plt.scatter(sim_df["volatility"], sim_df["return"], c=sim_df["sharpe"], cmap="viridis", s=5, alpha=0.5)
plt.plot(frontier_vols, targets, "r-", linewidth=3, label="Efficient Frontier")
plt.colorbar(label="Sharpe Ratio")
plt.xlabel("年化波动率")
plt.ylabel("年化收益率")
plt.title("投资组合有效前沿")
plt.legend()
plt.show()
跑完这一步,你应该能看到一条清晰的红色曲线,把所有模拟的散点包在上沿。这条曲线的左端,就是全局最小方差组合(Global Minimum Variance Portfolio),简称GMV。右端的点对应着高收益高波动的配置。你个人到底选哪个点,取决于你的风险偏好。如果倾向稳健,就选靠近左端的组合;如果愿意承担更多波动换收益,就往右选。
4. 实操中一定会遇到的几个坑
理论模型跑通之后,真正的挑战才刚刚开始。下面这几个坑是我自己踩过、也在跟朋友交流时反复被提到的,每一个都能直接让你的模型“失效”或者给出反直觉的结果。
4.1 输入参数太敏感,最优权重会“乱跳”
组合优化模型有一个令人头疼的特性:输入参数的微小变化,会导致输出权重发生剧烈变化。你今天用过去252天的数据算出银行ETF应该配35%,明天多增加一天的行情,结果可能就变成配5%。
这不是bug,而是模型对协方差矩阵和期望收益率“太敏感”了。尤其是期望收益率,本来就是用历史数据估算的,噪声非常大。一个最简单的解决办法是限制权重的上下界。比如把单个资产的权重上限定在40%、下限定在5%,这样哪怕模型很想激进地押注某个资产,也会被约束住。我在实盘里用的就是这个思路,本质上就是把数学上“最优”的解,往“稳健”的方向拉一拉。
提示:当你发现模型给出的权重很极端,比如某个资产权重超过50%,先别急着执行。加一道上下界约束,通常会得到更合理、更可落地的配置。
4.2 协方差矩阵的估计误差
协方差矩阵变量特别多。如果你有10个资产,那么协方差矩阵里就有10个方差项和45个协方差项。要在有限的历史数据里把这55个参数估计准确,数据量需求非常大。这就是所谓的“维度灾难”。
实操里,我建议资产数量控制在5到15个之间。太少了达不到分散效果,太多了估计误差会淹没真实信号。另外,如果两个资产高度相关(比如两只同行业ETF),可以考虑只留一只,否则协方差矩阵会出现多重共线性问题,最优化结果不稳定。
如果想更进阶一点,可以考虑给协方差矩阵做收缩估计(Shrinkage Estimation),把样本协方差矩阵向一个更结构化的目标矩阵(比如对角矩阵)收缩。Ledoit-Wolf收缩法是金融行业里最常见的处理方法,scikit-learn里有现成的实现:
python复制from sklearn.covariance import LedoitWolf
# 用收缩协方差矩阵替代样本协方差
lw = LedoitWolf().fit(returns.values)
cov_shrunk = lw.covariance_ * 252
这个操作看似简单,但对稳定性的提升非常明显。我第一次用收缩协方差矩阵替换掉原始样本协方差之后,发现最优权重不再那么“神经质”了。
4.3 用历史收益率做期望收益,效果差强人意
这是整个模型里最“看天吃饭”的环节。历史均值作为未来收益的估计值,回测里通常表现不好。原因很简单:过去涨得好的资产,未来不一定继续涨;过去跌得惨的,未来可能反而有修复行情。均值回归是市场里常见的现象。
我的处理办法有两类。一类是“去均值化”,不直接使用历史均值作为期望收益率,而是假设所有资产的期望收益率相同,只优化风险,得到全局最小方差组合。这个组合虽然不追求最高收益,但回撤控制得很出色。另一类是引入主观判断,调整期望收益率的输入值,比如你觉得新能源行业未来一年有明确利好,就手动把它的期望收益率调高一些。这个操作其实就是在模型里融入你的观点,本质上跟Black-Litterman模型的思路是一致的。
4.4 只看年度数据,忽略了再平衡成本
模型给出来的权重不是一个一劳永逸的答案。市场在变,资产价格在变,没过多久实际权重就会偏离目标权重。所以组合优化必须配合“再平衡”策略一起使用,比如每个季度重新计算一次权重,然后把仓位调回去。
但再平衡是有成本的,包括交易佣金、冲击成本和税费。所以频率不是越高越好。我个人的经验是:先设定一个再平衡阈值,比如当某个资产的权重偏离目标超过5个百分点时才触发调整,这样能大大减少不必要的交易。
5. 组合优化模型能用在哪些场景
这套模型的应用范围其实比想象中广。只要涉及“多个资产怎么分配资金”的问题,都可以套用。
5.1 大类资产配置
最常见的用途是在大类资产层面做配置,比如股票、债券、黄金、商品、房地产REITs等。5到6个大类资产之间的相关性相对较低,模型输出的结果比较稳定,是组合优化最容易见效的场景。
我自己给自己做的长期配置,就是在股票指数、债券、黄金之间做优化配置。这几个资产的相关性在大部分时间里都不算高,所以模型的解相对稳定,不需要频繁再平衡。拉长了看,组合收益不一定是最高的,但持有体验确实比单押某个资产好得多。
5.2 行业ETF轮动组合
如果你偏好股票市场,可以用行业ETF作为资产池,比如消费、医药、科技、银行、新能源、有色等。行业ETF之间的相关性整体偏高(毕竟都在A股市场里),所以有效前沿会比较“扁”,分散化效果不如大类资产组合。
但这个组合有个好处:如果某个行业有明显的景气度差异,模型会把更大的权重配给期望收益率更高的行业。此时你要多留个心眼——行业轮动的速度很快,历史收益的延续性并不强,好策略是配合趋势信号来做行业选择,而不是直接用长期历史均值当期望收益。
5.3 基金组合(FOF)的底层判断
在挑选基金构建组合的时候,同样的方法完全可以复用。把基金的历史净值当成价格序列,算出收益率和协方差,然后做组合优化,得到的权重就可以作为你买入不同基金的资金比例参考。
这里要注意的是,基金本身已经是分散化的组合,所以基金与基金之间的相关性往往偏高。尤其是指数基金,同类风格的基金相关系数经常在0.9以上。所以用基金做标的时,不要光看持仓是否相似,要去算净值相关性。我见过不少FOF产品,看起来买了好几只基金,结果回撤起来跟买一只基金没有区别,问题就出在这里。
6. 最后分享几个经验
组合优化模型我已经用了一段时间,积累了一些书本上不太会讲的感受。挑三个最想说的分享给你。
第一,模型是辅助,不是圣杯。无论参数算得多漂亮,模型始终建立在历史数据的假设之上,市场一旦发生结构性变化,模型输出就会失真。所以我自己会保留一部分主观判断空间,模型结果作为底色参考,最终决策还是投资人自己的事情。
第二,永远在组合里保留一些“低相关资产”。很多人建立组合时,关注的全是“哪些涨得好”,我反而建议先看“哪些跟我的重仓资产相关性低”。真正能在危机中对冲风险的,往往是那些平时不起眼、但跟股票负相关的资产,比如长期的国债、黄金等。它们平时可能拖累一点收益,但遇到极端行情时,是真正能让你睡得着觉的东西。
第三,想到投资组合优化的时候,别只盯着那组最优权重。整个流程里,计算协方差、分析相关性、设定约束条件的过程,其实比最终那组权重更有价值。你花时间梳理自己手里有哪些资产、它们之间有什么关系、你愿意承受多大风险,这些思考本身就能帮你避免大量不理性的投资决策。哪怕最后不套用公式,只做完了“算协方差”这一步,你的组合质量都会有一个明显的提升。
