简介:均值方差资产配置模型是马科维茨现代投资组合理论的核心工具,用于在给定风险水平下求解最优资产权重,它聚焦股票、债券等大类资产的投资比例问题,适合金融工程、量化投资与组合管理方向的学习者和从业者掌握从收益率序列到资产权重的完整链路。压缩包内共2个文件,包体约36KB,一个M脚本负责读取历史收益率、构造协方差矩阵并通过优化算法求解权重,另一个Excel文件保存模拟数据、风险参数及优化后的配置结果,二者结合便于对照验证。已有325人学习下载,借助该资源可以快速复现均值-方差优化的关键步骤,并在此基础上调整资产范围、加入约束条件,进一步观察不同风险偏好下的权重变化。对于希望深入理解有效前沿、风险平价等后续主题的读者,也是一个很好的拓展起点。
1. 资产配置模型:样本内越精致,样本外越脆弱
量化团队里最常见的争执不是因子选得不对,而是策略研究员给出来的权重,组合经理根本不敢下单——过去三年收益曲线拟合得越漂亮的资产配置模型,未来一年跑输等权重的概率就越大。这不是代码写错了,而是资产配置模型的结构性缺陷:优化器会放大输入噪声,把样本内的"最优权重"变成样本外的"过度自信"。本文面向要亲手实现资产配置模型的工程师和策略人员,先把选型逻辑讲透,再给出一套能直接跑通的最小 Python 实现,最后落到参数估计、回测协议和上线前的验证手段。默认你已经会用 NumPy 和 pandas,但不需要系统学过金融学教材,所有公式都从代码反推理解。
2. 资产配置模型选型:均值方差、最小方差与风险平价怎么选
2.1 均值方差优化的问题不在目标函数,而在输入估计
Markowitz 的均值方差框架把资产配置问题变成一个二次规划:固定目标收益,最小化组合方差;或者固定风险预算,最大化期望收益。数学上两个方向都简单,cvxpy 几十行就能解。真正的问题在于目标函数需要两类估计:期望收益率向量 μ 和协方差矩阵 Σ。协方差至少还有成交数据支撑,期望收益率本质上是"猜"——μ 里任何微小扰动,都会被优化器放大成权重的剧烈翻转。
我拿到一个均值方差模型,先不做任何优化,而是检查协方差矩阵的条件数和权重的敏感度。条件数超过 1000 的协方差矩阵,反演结果基本不值得信任。敏感度测试的做法是:给 μ 加上 5% 的随机扰动,重新求解,如果某个资产权重从 30% 跳到 70%,这个资产配置模型的样本外表现基本可以预期为不稳定。等权重组合在这种测试里几乎不波动,这正是它难以被击败的原因。
2.2 最小方差组合:砍掉收益预测,代价是行业偏离
把期望收益率从模型里拿掉,只保留协方差矩阵,得到的就是最小方差组合。它的意义很直接:构建一个不需要预测涨跌的资产配置模型,在所有可行权重里挑波动率最小的那个。样本外表现通常比均值方差稳定,但有两个副作用:权重高度集中在低波动资产上,比如债券或公用事业,形成隐性的风格押注;协方差矩阵本身的估计误差依然存在,集中度越高,误差的杀伤力越大。
判断最小方差组合是否可用,看权重平方和就行。权重平方和超过 0.5 意味着前两大资产已经控制了组合,这时我会给模型加上单资产权重上限,比如 30%。加了约束后组合的风险不会显著上升,但极端集中度会明显缓解。
2.3 风险平价:把风险贡献从间接指标变成硬约束
风险平价走的是另一条路线:不让权重相等,而是让每个资产对组合总风险的边际贡献相等。数学表述是:对每个资产 i,风险贡献 RC_i = w_i × (Σw)_i / √(w'Σw) 都等于同一个常数。这里的 (Σw)_i 是协方差矩阵乘权重向量后第 i 个分量,代表资产 i 与整个组合的协方差,再乘上权重 w_i 才是它对总风险的实际贡献。
风险平价本质上做了一个假设:风险贡献均衡等于收益来源均衡。这个假设在股债多资产组合里效果显著,因为传统 60/40 组合中债券的风险贡献被严重低估,风险平价会自动提高债券权重。但要注意,它不是免费的:如果组合加了杠杆,一旦股债相关性在压力期上升,风险平价的分散效果会迅速衰减。近几年多次股债双杀的行情已经把这个问题暴露得很彻底。
2.4 常见资产配置模型的对比与选型表
| 模型 | 核心输入 | 优点 | 主要缺陷 | 适合场景 |
|---|---|---|---|---|
| 等权重 1/N | 无 | 无估计误差、不依赖模型 | 忽略资产间协方差结构 | 基准对照、样本不足时 |
| 均值方差 | μ、Σ | 风险和收益的权衡可由参数显式控制 | 对 μ 的估计误差极其敏感 | 有稳定收益预测信号的团队 |
| 最小方差 | Σ | 只需协方差、样本外相对稳定 | 权重集中、容易变成低波动风格押注 | 低风险偏好、债券为主的组合 |
| 风险平价 | Σ | 风险贡献可解释、分散度可控 | 可能隐含杠杆、相关性突变时失灵 | 多资产、股债混合组合 |
| Black-Litterman | 先验权重、观点 | 能融入人工观点、抑制极端权重 | 参数多、观点设定主观性强 | 机构组合、有明确宏观观点 |
风险贡献的计算是后面所有模型的基础,直接用 NumPy 就能复现:
import numpy as np def risk_contributions(weights, cov): w = np.asarray(weights) sigma = np.sqrt(w @ cov @ w) # 组合年化波动率 mrc = (cov @ w) / sigma # 边际风险贡献 rc = w * mrc # 风险贡献,加总等于 sigma return sigma, mrc, rc参数说明: - weights:形状为 (N,) 的权重向量,必须满足 sum(w)=1。 - cov:N×N 年化协方差矩阵,否则输出的波动率口径会乱。 - mrc 的第 i 个分量表示增加一单位第 i 个资产权重对组合风险的边际拉动。 - rc 加总恒等于 sigma,这个性质可以用来校验风险平价求解器是否收敛。逻辑说明:风险贡献只依赖权重和协方差矩阵,不依赖收益预测,所以它成了控制组合结构的直接抓手。后面风险平价的目标函数,就是把每个资产的 rc 往 sigma/n 上拉。
3. 用 Python 在本地跑通资产配置模型的最小实现
3.1 造一组能复现结果的资产收益率数据
先用合成数据把流程跑通,再接真实行情。合成数据的好处是协方差结构已知,求解器算得对不对一眼就能看出来。下面生成 4 个资产、600 个交易日的日收益率,前两个资产相关性高,后两个资产相关性低,形成可辨识的结构:
import numpy as np import pandas as pd n_assets, n_days = 4, 600 np.random.seed(42) annual_vol = np.array([0.15, 0.18, 0.10, 0.12]) corr = np.array([ [1.00, 0.70, 0.20, 0.10], [0.70, 1.00, 0.15, 0.05], [0.20, 0.15, 1.00, 0.30], [0.10, 0.05, 0.30, 1.00], ]) cov_d = np.diag(annual_vol) @ corr @ np.diag(annual_vol) / 252 rets = np.random.multivariate_normal( mean=np.full(n_assets, 0.0004), cov=cov_d, size=n_days ) returns = pd.DataFrame(rets, columns=["equity", "tech", "bond", "gold"])参数说明: - annual_vol:年化波动率,决定每个资产的绝对风险量级。 - corr:相关性矩阵,是合成数据里唯一需要人工指定的结构。 - cov_d:日频协方差,由年化波动率乘相关矩阵再除以 252 得到。 - mean=0.0004 对应日收益 4 个基点,年化约 10%,方便后面观察有效前沿。逻辑说明:先构造相关矩阵再换算协方差,比直接手填协方差矩阵直观,后续做压力测试时只需要改相关结构,波动率可以不动。
3.2 用 cvxpy 求解最小方差组合
import cvxpy as cp cov_ann = returns.cov() * 252 n = cov_ann.shape[0] w = cp.Variable(n) risk = cp.quad_form(w, cov_ann.values) constraints = [cp.sum(w) == 1, w >= 0] prob = cp.Problem(cp.Minimize(risk), constraints) prob.solve() weights_minvar = pd.Series(w.value, index=returns.columns)参数说明: - cp.quad_form(w, cov) 计算二次型 w'Σw,是凸优化里的标准风险表达式。 - cp.sum(w) == 1 是全投资约束;w >= 0 禁止做空。放开非负约束会出现极端权重,样本外通常更差。 - prob.solve() 默认走 ECOS 求解器,遇到数值警告时可以换 CLARABEL,cvxpy 会自动调用已安装的可用求解器。运行后你会看到最小方差把大部分权重压到 bond 上,这正是 2.2 节提到的集中度问题。要缓解,需要给单资产加权重上限,比如在 constraints 里追加w <= 0.3。
3.3 用 scipy 求解最大夏普与风险平价权重
夏普最大化不是凸问题,最省事的做法是 scipy.optimize.minimize 配 SLSQP。风险平价则把目标定义为"每个资产风险贡献与目标值之差的平方和最小化":
from scipy.optimize import minimize def max_sharpe_objective(w, mu, cov, rf=0.0): ret = w @ mu - rf vol = np.sqrt(w @ cov @ w) return -ret / vol mu_ann = returns.mean() * 252 cons = [{"type": "eq", "fun": lambda w: np.sum(w) - 1}, {"type": "ineq", "fun": lambda w: w}] res = minimize(max_sharpe_objective, x0=np.ones(n) / n, args=(mu_ann, cov_ann.values), constraints=cons, method="SLSQP", options={"maxiter": 200}) weights_sharpe = pd.Series(res.x, index=returns.columns) def risk_parity_objective(w, cov): sigma, _, rc = risk_contributions(w, cov) return np.sum((rc - sigma / n) ** 2) res_rp = minimize(risk_parity_objective, x0=np.ones(n) / n, args=(cov_ann.values,), method="SLSQP", constraints=cons, options={"maxiter": 500}) weights_rp = pd.Series(res_rp.x, index=returns.columns)参数说明: - max_sharpe_objective 返回负夏普,因为 minimize 默认做最小化。 - x0 是等权重起点,SLSQP 是局部求解器,初值不合适容易陷入不合理的局部解。 - risk_parity_objective 中的 sigma/n 是目标风险贡献,即让每个资产平分组合总风险。 - SLSQP 对约束函数的写法敏感,注意 ineq 约束返回的是 w 本身,表示 w >= 0。逻辑说明:两个求解器共享同一组约束,只有目标函数不同。这暴露了资产配置模型的一个通用模式——约束决定可行性,目标函数决定偏好。要换模型,只需要替换目标函数,数据管道完全不用动。
3.4 把固定权重落到净值曲线
权重求出来后,还要能快速评估它的历史表现。下面这段把日频收益率聚合到月度,然后和一个固定权重做矩阵乘法得到组合净值:
monthly = returns.resample("ME").mean().values def backtest_nav(monthly_rets, weights): nav = (1 + monthly_rets @ weights).cumprod() return nav / nav.iloc[0] nav_minvar = backtest_nav(monthly, weights_minvar)参数说明: - resample("ME") 把日频数据聚合到自然月,mean() 只是近似月收益,用于演示净值形态。 - 真实回测要在每个再平衡日重新求解权重,这里用固定权重只是检验求解器和估值逻辑。 - nav 除以首个净值做归一化,三条净值曲线画在一起才能比较。这个最小实现已经覆盖了资产配置模型的完整链路:数据 → 协方差 → 优化器 → 净值评估。从这一步往后,所有精度问题都集中在参数估计和回测协议上。
4. 资产配置模型参数估计与回测:协方差收缩、滚动窗口和换手约束
4.1 协方差矩阵先用 Ledoit-Wolf 收缩再进优化器
资产配置模型对协方差质量的敏感度远高于对收益预测的敏感度。当资产数 N 接近样本数 T 时,样本协方差的条件数会快速恶化,优化结果里的权重也会跟着剧烈抖动。解决办法之一是收缩估计:把样本协方差向一个结构目标压缩,收缩强度由数据自动决定。sklearn 的 LedoitWolf 直接实现了这个方法:
from sklearn.covariance import LedoitWolf lw = LedoitWolf().fit(returns) cov_shrunk = pd.DataFrame( lw.covariance_, index=returns.columns, columns=returns.columns ) print("收缩强度:", lw.shrinkage_)参数说明: - shrinkage_ 是自动算出的收缩系数,介于 0 和 1 之间,越大说明样本协方差越不可信。 - LedoitWolf 的收缩目标是常数相关矩阵,保留方差、压缩相关性,等价于把相关系数向均值回归。 - 用 cov_shrunk 替换第 3 章求解里的 cov_ann,权重通常会平滑很多。但要警惕,收缩解决的是估计误差,不是结构突变。发生股债相关性跃升、流动性骤降这类极端行情时,协方差矩阵本身已经失准,再收缩也只是把旧结构平滑化,不会产生新的信息。
4.2 滚动窗口估计与再平衡周期的四个默认值
回测里最容易跳过的步骤是窗口长度、再平衡频率、缓冲阈值和交易成本的设定。我常用的默认值是:估计窗口 252 个交易日、再平衡周期 21 个交易日、权重缓冲 2%~5%、双边交易成本千五。下面是非重叠滚动回测的骨架:
def rolling_allocate(returns, est_window=252, rebalance=21, model="minvar"): result = {} for i in range(est_window, len(returns), rebalance): win = returns.iloc[i - est_window:i] result[i] = solve_model(win, model) return pd.DataFrame(result, index=returns.columns).T def solve_model(win, model): cov = LedoitWolf().fit(win).covariance_ if model == "minvar": w = cp.Variable(win.shape[1]) cp.Problem( cp.Minimize(cp.quad_form(w, cov)), [cp.sum(w) == 1, w >= 0] ).solve() return w.value参数说明: - est_window=252 对应一年交易日;资产数 N 较大时建议窗口不小于 3N。 - rebalance=21 是月度再平衡,63 是季度。月频通常是换手成本和跟踪误差的折中点。 - solve_model 把模型选择抽象成参数,后面加风险平价只需扩展这个函数。 - cp 复用第 3 章的 cvxpy 命名空间,实际工程中建议封装成独立模块。回测循环里有一个隐性铁律:每个再平衡日只能用该日之前的数据求解。任何用到全样本信息的处理,包括全局标准化、用全样本求均值补缺失值,都会引入前视偏差,让收益曲线虚高。
4.3 用缓冲带和换手上限约束实际调仓
再平衡不是越勤越好。高频调仓会让资产配置模型在噪声上反复交易,手续费和冲击成本最终贡献给券商。常见做法是设置缓冲带:单资产权重偏离目标超过阈值才触发调仓,调仓时一次性调整到目标权重。
def rebalance_with_buffer(current, target, buffer=0.03, cost=0.001): target = np.asarray(target) current = np.asarray(current) if not np.any(np.abs(target - current) > buffer): return current, 0.0 turnover = 0.5 * np.abs(target - current).sum() return target, turnover * cost参数说明: - buffer=0.03 表示单资产偏离目标 3 个百分点以上才触发调仓。 - turnover 是换手率近似,含义是一半的曼哈顿距离,对应买卖总规模占总资产比例。 - cost 是单边费率,turnover * cost 就是本次调仓的成本,在回测净值里直接扣减。把这段逻辑接进 4.2 的滚动回测,每次求解出的新权重先与当前持仓比较,满足触发条件才落单。实测中模型的换手率通常能下降一半以上,净值几乎不受影响。
4.4 回测里最容易骗到自己的三个坑
第一个坑是用同一段样本既做模型选择又做业绩评估,选出来的必然是最过拟合的那个。至少把样本切成三段,最右段只做验证。第二个坑是忽略停牌、除权除息和涨跌停约束,回测里的成交价在实盘中根本买不到。第三个坑是只盯年化收益和最大回撤,不跟踪换手率和权重稳定性——而这两者才是资产配置模型落地的真实瓶颈。
提示:回测净值图漂亮不是终点。把每个再平衡日的权重序列导出来扫一遍,看有没有单期权重突跳。突跳期数占比超过 10% 时,先修权重稳定性,再谈收益。
5. 资产配置模型上线的三道校验:权重稳定性、压力风险贡献与实时归因
5.1 相邻再平衡周期的权重漂移检查
上线前我习惯把相邻两次再平衡的权重差整理成矩阵:行是资产,列是再平衡时点,颜色是权重变化幅度。某一行频繁出现大幅跳动,说明该资产的权重不受控。定量指标用平均换手率,公式就是 4.3 里的 turnover,加总后除以再平衡次数。经验阈值是 15%,超过这个数需要回头检查窗口长度是否太短、协方差是否做了收缩。
5.2 用压力协方差矩阵重算风险贡献
危机场景的特征是相关性上升。把历史相关矩阵乘以放大系数、波动率抬升一定幅度,就能构造一个"股债相关性走高"的压力协方差矩阵,重新计算每个资产的风险贡献:
def stress_cov(cov, corr_scale=1.3, vol_scale=1.2): vol = np.sqrt(np.diag(cov)) corr = cov / np.outer(vol, vol) corr_s = np.clip(corr * corr_scale, -1, 1) np.fill_diagonal(corr_s, 1.0) vol_s = vol * vol_scale return np.diag(vol_s) @ corr_s @ np.diag(vol_s)参数说明: - corr_scale=1.3 表示把相关系数放大 30%,clip 保证对角线为 1、非对角线不超过 1。 - vol_scale=1.2 表示波动率抬升 20%,模拟风险资产波动放大的压力期。 - 输出 cov_s 重新传给 risk_contributions,就能对比压力前后的风险贡献占比。如果压力下某类资产的风险贡献占比从 30% 变成 60%,名义上的分散在尾部场景里并不存在,需要重新审视模型约束。
5.3 把风险贡献归因做成每日监控
上线后的监控对象不是权重本身,而是实际持仓每个交易日的风险贡献占比。用最近 20 个交易日的数据估计协方差,计算当日各资产的 RC 占比,与目标 RC 对比,偏差超过 8 个百分点时触发预警。这个检查和模型是否优美无关,只回答一个问题:组合当下实际承担的风险来自哪里。资产配置模型的可信度,最终建立在可解释、可监控、可验证这套闭环上。
本文还有配套的精品资源,点击获取