ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

资产配置模型对比与Python实践:从均值方差到风险平价

资产配置模型对比与Python实践:从均值方差到风险平价 简介均值方差资产配置模型是马科维茨现代投资组合理论的核心工具用于在给定风险水平下求解最优资产权重它聚焦股票、债券等大类资产的投资比例问题适合金融工程、量化投资与组合管理方向的学习者和从业者掌握从收益率序列到资产权重的完整链路。压缩包内共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。 - covN×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( meannp.full(n_assets, 0.0004), covcov_d, sizen_days ) returns pd.DataFrame(rets, columns[equity, tech, bond, gold])参数说明 - annual_vol年化波动率决定每个资产的绝对风险量级。 - corr相关性矩阵是合成数据里唯一需要人工指定的结构。 - cov_d日频协方差由年化波动率乘相关矩阵再除以 252 得到。 - mean0.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, indexreturns.columns)参数说明 - cp.quad_form(w, cov) 计算二次型 wΣw是凸优化里的标准风险表达式。 - cp.sum(w) 1 是全投资约束w 0 禁止做空。放开非负约束会出现极端权重样本外通常更差。 - prob.solve() 默认走 ECOS 求解器遇到数值警告时可以换 CLARABELcvxpy 会自动调用已安装的可用求解器。运行后你会看到最小方差把大部分权重压到 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, rf0.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, x0np.ones(n) / n, args(mu_ann, cov_ann.values), constraintscons, methodSLSQP, options{maxiter: 200}) weights_sharpe pd.Series(res.x, indexreturns.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, x0np.ones(n) / n, args(cov_ann.values,), methodSLSQP, constraintscons, options{maxiter: 500}) weights_rp pd.Series(res_rp.x, indexreturns.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_, indexreturns.columns, columnsreturns.columns ) print(收缩强度:, lw.shrinkage_)参数说明 - shrinkage_ 是自动算出的收缩系数介于 0 和 1 之间越大说明样本协方差越不可信。 - LedoitWolf 的收缩目标是常数相关矩阵保留方差、压缩相关性等价于把相关系数向均值回归。 - 用 cov_shrunk 替换第 3 章求解里的 cov_ann权重通常会平滑很多。但要警惕收缩解决的是估计误差不是结构突变。发生股债相关性跃升、流动性骤降这类极端行情时协方差矩阵本身已经失准再收缩也只是把旧结构平滑化不会产生新的信息。4.2 滚动窗口估计与再平衡周期的四个默认值回测里最容易跳过的步骤是窗口长度、再平衡频率、缓冲阈值和交易成本的设定。我常用的默认值是估计窗口 252 个交易日、再平衡周期 21 个交易日、权重缓冲 2%5%、双边交易成本千五。下面是非重叠滚动回测的骨架def rolling_allocate(returns, est_window252, rebalance21, modelminvar): 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, indexreturns.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_window252 对应一年交易日资产数 N 较大时建议窗口不小于 3N。 - rebalance21 是月度再平衡63 是季度。月频通常是换手成本和跟踪误差的折中点。 - solve_model 把模型选择抽象成参数后面加风险平价只需扩展这个函数。 - cp 复用第 3 章的 cvxpy 命名空间实际工程中建议封装成独立模块。回测循环里有一个隐性铁律每个再平衡日只能用该日之前的数据求解。任何用到全样本信息的处理包括全局标准化、用全样本求均值补缺失值都会引入前视偏差让收益曲线虚高。4.3 用缓冲带和换手上限约束实际调仓再平衡不是越勤越好。高频调仓会让资产配置模型在噪声上反复交易手续费和冲击成本最终贡献给券商。常见做法是设置缓冲带单资产权重偏离目标超过阈值才触发调仓调仓时一次性调整到目标权重。def rebalance_with_buffer(current, target, buffer0.03, cost0.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参数说明 - buffer0.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_scale1.3, vol_scale1.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_scale1.3 表示把相关系数放大 30%clip 保证对角线为 1、非对角线不超过 1。 - vol_scale1.2 表示波动率抬升 20%模拟风险资产波动放大的压力期。 - 输出 cov_s 重新传给 risk_contributions就能对比压力前后的风险贡献占比。如果压力下某类资产的风险贡献占比从 30% 变成 60%名义上的分散在尾部场景里并不存在需要重新审视模型约束。5.3 把风险贡献归因做成每日监控上线后的监控对象不是权重本身而是实际持仓每个交易日的风险贡献占比。用最近 20 个交易日的数据估计协方差计算当日各资产的 RC 占比与目标 RC 对比偏差超过 8 个百分点时触发预警。这个检查和模型是否优美无关只回答一个问题组合当下实际承担的风险来自哪里。资产配置模型的可信度最终建立在可解释、可监控、可验证这套闭环上。本文还有配套的精品资源点击获取
返回列表