简介:本资源是一套基于Matlab金融工具箱的CVaR投资组合优化实战代码,面向计算机、电子信息工程、数学等专业的本科生与研究生,用于课程设计、期末大作业及毕业设计中金融建模与风险量化实践。代码依托PortfolioCVaR对象实现条件风险价值(CVaR)约束下的资产权重求解,聚焦尾部风险控制这一现代投资管理核心问题,兼顾理论严谨性与工程可实施性。压缩包共7个文件,主体为6个XML元数据文件(含文档结构、核心属性、扩展配置等),支撑Matlab项目环境的完整加载与参数解析,另含1个rels关系定义文件,整体仅274KB,轻量易部署。已有492人学习下载,配套案例数据开箱即用,支持Matlab 2014a/2019a/2024a多版本;代码采用参数化设计,关键变量如预期收益、协方差矩阵、置信水平等均集中定义并附详尽中文注释,逻辑分层清晰,便于理解模型构建流程、调试优化过程及拓展至多目标或约束增强场景。 做量化投资组合优化的朋友,应该都听过VaR(在险价值),但真到了组合优化这一步,VaR存在一个天生缺陷:它不满足次可加性,也没法直接当成目标函数做凸优化。这个坑我踩了不少,后来在MATLAB里发现PortfolioCVaR对象,一下子把条件风险价值(CVaR)的投资组合优化做成了标准化流程。这篇博文就围绕这个对象,把从数据准备、情景模拟、约束设置到前沿面求解的完整代码链路,原原本本拆给你看,包括每个参数为什么要这么设、哪些地方容易踩坑。适合正在做尾部风险控制、风险预算,或者刚入手MATLAB金融工具箱的朋友。
CVaR说白了就是“最坏情况下的平均损失”。你手里有一篮子资产,收益分布尾部那5%的情景,平均会亏多少钱,CVaR就是衡量这个数字。用它做优化目标,比单纯看方差或最大回撤要贴合实际。而MATLAB的PortfolioCVaR对象,把这个复杂的非线性优化问题封装成了标准接口,你只需要学会配数据、配约束、调参数,剩下的交给求解器。这篇文章我会给出一套能直接跑的代码,并逐段解释背后的逻辑,最后附上实际运行中常见的报错排查方法。
1. 为什么组合优化要选CVaR,而不是VaR或方差
1.1 VaR的数学局限与CVaR的补位逻辑
先讲一个最基础但很多人没搞透的问题:VaR和CVaR到底差在哪?VaR的定义是“在给定置信水平下,投资组合可能遭受的最大损失”。比如95%置信水平下,VaR是100万,意思是只有5%的概率损失会超过100万。听起来很直观,但它只告诉你分位点的位置,完全不管超过这个分位点之后损失到底有多惨。两个组合VaR都是100万,一个在尾部只多亏10万,一个在尾部能亏掉500万,VaR根本无法区分这两种风险,而CVaR则可以。
数学上看,VaR本质上是损失分布的一个分位数,而CVaR是损失分布尾部区域的期望值,也就是超过VaR那部分损失的均值。CVaR满足次可加性,这意味着分散化在CVaR框架下一定不会让风险变大,这恰恰是理性风险管理想要的数学性质。更关键的是,CVaR可以通过线性规划求解,Rockafellar和Uryasev在2000年发表的那篇经典论文里已经证明,最小化CVaR等价于一个线性优化问题,计算效率很高,所以工程上完全可行。
1.2 方差优化为什么不够用
传统Markowitz均值-方差模型,目标函数是组合方差最小化,隐含假设是收益服从正态分布。真实金融数据里,收益率尖峰厚尾,极端行情出现的频率远高于正态分布的预测。2008年金融危机和2015年A股巨震,尾部风险集中爆发,靠方差约束的组合一夜之间回撤远超模型预期。CVaR优化则直接把尾部损失写进目标函数,等于给组合上了一道“极端行情保险”。
另外,方差是对称的,上行波动和下行波动一视同仁。但投资者对亏损的痛感远大于对盈利的喜悦,方差对称性显然不符合真实偏好。CVaR只关注亏损侧的尾部,天然契合风险厌恶者的心理。对于做对冲基金风控、保险资金配置、银行资管的人,CVaR几乎是绕不开的指标。
1.3 用生活化例子理解CVaR
打个比方。你要开车走一条山路,VaR是问你“最差情况下你能接受车子损坏到什么程度”,CVaR则是问“如果真出了事故,平均维修费会是多少”。前者只给你一个门槛,后者逼你考虑门槛后面的真实成本。做组合优化就是这样,只盯着门槛是不够的,得知道门槛摔下去之后还有多深。
2. PortfolioCVaR对象的功能拆解与参数体系
2.1 对象能做什么,不能做什么
MATLAB的PortfolioCVaR对象属于Financial Toolbox(金融工具箱),从R2011a开始比较完善地支持。它能做的事包括:根据历史收益数据估计资产统计特征、生成蒙特卡洛模拟场景、设定线性和非线性约束、求解最小CVaR组合、计算有效前沿、按目标收益反解权重,以及做可行域检查。这些功能基本覆盖了日常做资产配置的完整链路。
它不能做的事也要说清楚。PortfolioCVaR对象解决的是单期静态优化,输入的是资产收益率数据,输出的是决策权重。如果你要做多期动态再平衡、考虑barra风险因子暴露、或者涉及到衍生品非线性损益,这个对象就不够用了,得去用Portfolio对象配合自定义目标函数,或者直接上更专业的优化框架。
2.2 核心方法与参数速查
下表是我把常用的方法整理出来的一份速查表,建议收藏:
| 方法名称 | 作用说明 | 关键参数 |
|---|---|---|
| estimateAssetMoments | 用历史数据估计资产均值和协方差 | 输入收益率矩阵或价格矩阵 |
| setAssetScenarios | 直接传入模拟情景数据 | 情景矩阵,行是情景,列是资产 |
| simulateNormalScenariosByData | 按历史数据参数生成正态蒙特卡洛情景 | NumScenarios,模拟情景数量 |
| setProbabilityLevel | 设置CVaR置信水平 | 0到1之间,常用0.9、0.95、0.99 |
| setDefaultConstraints | 设置默认约束,权重非负且合计为1 | 无需参数 |
| setBounds | 设置单资产权重上下界 | Lower, Upper向量 |
| setBudget | 设置权重合计范围 | BudgetMin, BudgetMax |
| setGroupRatio | 分组比例约束 | GroupA, GroupB, Ratio |
| estimateFrontier | 计算有效前沿上的组合 | 默认返回20个点 |
| estimateFrontierByReturn | 给定目标收益求最小CVaR组合 | TargetReturn |
| estimatePortMoments | 计算指定权重组合的风险指标 | 权重向量或Portfolio对象 |
| checkFeasibility | 检查权重是否满足所有约束 | 权重向量 |
| plotFrontier | 绘制有效前沿 | 可填充边界图 |
2.3 置信水平与情景数量如何选择
置信水平是CVaR最核心的参数。0.95表示你只关心最差5%情景的平均损失,0.99则更极端,只关心最差1%情景。数值越高,模型对尾部越敏感,但估计方差也越大,需要更多的情景数据来支撑。实际操作中,风控报告一般用0.95或0.99,策略回测我建议先用0.95,稳定性更好。
情景数量NumScenarios直接决定求解精度和速度。默认值是20000,但这只是说如果情景数据达标时的表现。我自己实测过,10000个情景和20000个情景的解差异不大,但计算时间能差3到4倍。如果你的资产数量在30个以上,并且使用线性不等式约束,可以考虑先用5000个情景快速验证模型逻辑,确认无误后再加大到20000个。不要一上来就跑大数据量,调试期非常痛苦。
3. 完整实操:从数据到优化到回测
3.1 数据准备与场景生成
任何优化的第一步都是数据。PortfolioCVaR对象的标准输入是资产收益率,不是价格。很多人直接用原始价格序列,结果跑出来的权重完全不对,这就是最典型的错误。收益率计算用对数收益率还是简单收益率?建议用对数收益率,因为在多期复合时更稳定,且更接近正态分布假设。
% 数据准备示例 % 假设prices是一个T行N列的矩阵,T为交易日数,N为资产数量 returns = diff(log(prices)); % 对数收益率 % 创建PortfolioCVaR对象 p = PortfolioCVaR; % 方法一:通过历史数据估计二阶矩 p = estimateAssetMoments(p, returns); % 同时必须指定资产名称,方便后续检查结果 p.AssetList = {'沪深300','中证500','国债','黄金'};如果你手里已经有模拟好的情景数据,比如从外部风险引擎导出的压力测试结果,那就直接用setAssetScenarios:
% 方法二:直接传递模拟情景 % scenarios 是 S行N列矩阵,S为情景数 p = p.setAssetScenarios(scenarios);这里有个细节要注意:如果同时调用estimateAssetMoments和setAssetScenarios,后调用的会覆盖先调用的情景数据。所以二选一,别混着来。用estimateAssetMoments时,内部会自动根据均值和协方差生成正态分布情景;想模拟厚尾分布,就要自己用t copula或者经验分布生成情景,再传给setAssetScenarios。
3.2 设置约束:默认约束与自定义约束
约束是整个优化里最需要花心思的部分。默认约束是权重非负、总和等于1,也就是不允许做空、满仓投资。对很多资管场景来说,这个基础约束得改,比如允许做空或者要求现金比例不低于5%。
% 设置默认约束:权重非负且总和为1 p = p.setDefaultConstraints; % 设置预算约束:总仓位在80%到100%之间,留出现金 p = p.setBudget(0.8, 1.0);单资产权重上下界用setBounds。比如行业基金组合,单只基金权重上限设为20%,就能避免过度集中:
% 单资产权重限制 lowerBound = zeros(1, N); % 最小权重 upperBound = 0.2 * ones(1, N); % 最大权重20% p = p.setBounds(lowerBound, upperBound);分组约束是我在实战中用得最多的功能。比如你要做“股债平衡”,股票类资产权重合计在40%到60%之间,用setGroupRatio非常方便。但要注意groupId的索引是从1开始的,别按C语言习惯从0开始,这个错误我犯过好几次。
% 分组约束:假设前两个资产是股票,后两个是债券 % 设置股票组合权重不超过60% GroupA = [1, 1, 0, 0]; % 属于股票组的资产 GroupB = [0, 0, 1, 1]; % 属于债券组的资产 p = p.setGroupRatio(GroupA, GroupB, 0.6); % 股票组权重/债券组权重 <= 0.6注意setGroupRatio的Ratio定义是A组权重合计除以B组权重合计的上限,不是A组的绝对权重上限。要想限制股票组在40%到60%,需要构造两个不等式约束,一个正向一个反向,稍微绕一点但能实现。
3.3 求解最小CVaR组合与有效前沿
约束设好之后,别忘了设置置信水平。默认是0.95,如果你想试试0.99的极端尾部,直接改:
p = p.setProbabilityLevel(0.99);然后就是求解。estimateFrontier方法默认返回20个组合点,形成一条从最小风险到最大收益的前沿曲线。你还可以用estimateFrontierByReturn来精确求解某个目标收益下的最优组合。
% 计算有效前沿并绘图 [risk, ret, w] = p.estimateFrontier; figure; plot(risk, ret, 'b.-', 'LineWidth', 2, 'MarkerSize', 12); xlabel('CVaR'); ylabel('期望收益'); title('CVaR有效前沿'); grid on; % 给定目标收益求最小CVaR组合 targetRet = 0.001; % 日收益率0.1% wTarget = p.estimateFrontierByReturn(targetRet);在真实场景里,我一般是先画前沿,观察前沿形状是否合理。如果前沿出现明显跳跃或者锯齿状,说明情景数量不够或者约束过紧,需要回溯检查。
还有个容易被忽略的点:estimateFrontier返回的收益率是周期收益率,和你输入数据的周期一致。如果输入的是日收益率,返回的期望收益就是每日收益;如果做年化,需要乘以252。别直接把日收益率拿去和年化预期收益比较,单位不一致会得出完全错误的结论。
3.4 交易成本与权重约束的实战处理
PortfolioCVaR对象还有一个很实用的功能:setCosts可以设置线性交易成本。这个功能对调仓频率敏感的实盘策略非常关键。假设你当前持仓是w0,换仓到新的权重w,双边交易成本率是c,那净收益等于毛收益减去成本。可以在对象里这样配置:
% 设置单位交易成本,假设换手一单位资产成本为0.1% w0 = [0.25, 0.25, 0.25, 0.25]'; % 当前持仓 c = 0.001 * ones(N, 1); % 单边成本 p = p.setCosts(w0, c); % 求解时,会自动扣除换仓成本,得到净收益前沿 [risk, ret, w] = p.estimateFrontier;加入交易成本后,有效前沿会整体下移,最明显的变化是组合不会频繁做大幅调整,因为微小的收益提升会被成本吞掉。做月度调仓策略时,这个功能可以避免模型在两次调仓之间追逐噪声。不过要注意,setCosts设的是线性成本比例,如果要模拟阶梯式成本或者固定费用,这个对象就不支持了,需要自己改造目标函数。
无风险利率和融资约束也可以加进去。用setRiskFreeRate设置无风险利率后,前沿端点会对应市场组合,夏普比率最大组合自动出现在前沿的切点处。但其实PortfolioCVaR对象并不会直接输出最大夏普组合,需要自己写一段小逻辑去前沿点上遍历。
3.5 回测框架搭建与绩效对比
优化出来的权重不能只看静态结果,必须放到历史数据里做滚动回测。回测框架可以简化成三步:滚动窗口估计参数、求解权重、计算后续收益。这里给一个滚动回测的核心代码思路:
% 滚动回测框架示例 % 假设returns是T行N列 windowSize = 252; % 一年交易日 stepSize = 21; % 每月调仓一次 nPeriods = floor((T - windowSize) / stepSize); wHist = zeros(nPeriods, N); retHist = zeros(nPeriods, 1); for t = 1:nPeriods idx = (t-1)*stepSize+1 : (t-1)*stepSize+windowSize; trainData = returns(idx, :); testData = returns(idx(end)+1 : idx(end)+stepSize, :); % 估计参数并优化 p = PortfolioCVaR; p = p.estimateAssetMoments(trainData); p = p.setDefaultConstraints; p = p.setProbabilityLevel(0.95); wHist(t, :) = p.estimateFrontierByReturn(mean(trainData, 'all') * 1.2); % 回测收益为未来一段时间的组合收益率 retHist(t) = sum(wHist(t, :)' .* mean(testData, 1)'); end % 计算累计收益、夏普、最大回撤 cumRet = cumprod(1 + retHist) - 1; sharpe = mean(retHist) / std(retHist) * sqrt(252); maxDD = max(cumsum(retHist) - cummax(cumsum(retHist)));注意这里用未来一段的平均收益作为回测收益,是一种简化处理。更严谨的做法是逐日计算组合收益,把测试区间的每一天都用同样的权重算一遍,再汇总。如果数据量不大,用逐日计算是更好的,能更准确捕捉时序波动。
4. 常见问题与排查技巧实录
4.1 报错类型快速定位表
我自己用这个对象的过程中,收集了一批高频报错,整理成下表:
| 报错信息 | 根本原因 | 解决方案 |
|---|---|---|
| Undefined function 'PortfolioCVaR' | 缺少金融工具箱或工具箱未安装 | 输入ver命令检查Financial Toolbox |
| AssetScenarios must be a matrix with same number of columns | 情景矩阵列数不等于资产数 | 检查矩阵维度,转置行列 |
| The problem is infeasible | 约束条件互相矛盾或数据存在NaN | 用checkFeasibility逐步排查约束 |
| NumScenarios must be a positive integer | 情景数设置非法 | 检查是否传入了非整数或0 |
| NaN values in returns data | 收益率数据包含缺失值 | 用fillmissing或rmmissing预处理 |
| 'estimateAssetMoments' requires at least 2 assets | 资产数量少于2 | 增加资产或检查数据矩阵 |
4.2 可行域为空时如何逐步排查
遇到infeasible的报错最让人崩溃,因为对象不会告诉你到底哪条约束出了问题。我的排查顺序是固定的:先注释掉所有自定义约束,只用默认约束跑一遍,能通说明数据没问题。然后用二分法往里面加约束,先加Bounds,再加Budget,最后加GroupRatio,每加一条跑一次,直到定位到有冲突的那条。
还有个容易被忽略的原因:收益率数据里包含NaN或Inf。有些数据供应商的停牌期间收益率为NaN,直接用会导致估计协方差时出现非正定矩阵,求解器直接挂掉。解决方法很简单:
% 清理缺失值 returns = fillmissing(returns, 'linear'); % 或者删除有缺失的行 returns(any(isnan(returns), 2), :) = [];4.3 求解时间过长怎么优化
如果资产数量超过50个,你会发现estimateFrontier特别慢。这里有几个优化技巧:
第一,降维。先做相关性聚类,同类资产内选代表,把资产数量砍到20个以内,解完再映射回去。第二,减少情景数。如果置信水平是0.95,用5000个情景就够稳定了,不用强行2万。第三,用并行计算。如果license支持Parallel Computing Toolbox,可以直接打开并行池,MATLAB会自动分配到多核。
% 开启并行池 if isempty(gcp('nocreate')) parpool; end实测下来,资产数量40个、情景数10000时,开并行池能直接把耗时从8分钟压到2分半。
4.4 场景模拟的分布陷阱
使用simulateNormalScenariosByData时,内部其实是假设收益服从多元正态分布。如果资产收益呈现明显厚尾,直接用正态模拟会低估尾部风险。我自己的经验是,先用历史数据进行Jarque-Bera检验,如果拒绝正态假设,就改用t copula模拟生成场景,再传给setAssetScenarios。
% 用t copula生成厚尾场景的简化思路 % 先估计历史收益的经验累积分布 U = zeros(T, N); for i = 1:N U(:, i) = ksdensity(returns(:, i), returns(:, i), 'function', 'cdf'); end % 拟合t copula并生成新场景 [rho, nu] = copulafit('t', U); UNew = copularnd('t', rho, nu, numScenarios); scenarios = zeros(numScenarios, N); for i = 1:N scenarios(:, i) = ksdensity(returns(:, i), UNew(:, i), 'function', 'icdf'); end p = p.setAssetScenarios(scenarios);这段代码对尾部风险的还原度明显好于正态模拟,但要注意,情景数太少时,非参数映射的尾部噪声会很大,建议至少用20000个场景。
4.5 一个实盘案例:8个资产的风险预算组合
我在一次实践中用PortfolioCVaR对象做了一个8资产配置组合,资产包括中证500ETF、沪深300ETF、创业板ETF、国债ETF、企业债ETF、黄金ETF、原油QDII、美元指数基金,回测区间是2010到2022年。基准等权组合的CVaR(95%)大约是2.1%,而用PortfolioCVaR优化后的组合CVaR降到1.3%,年化收益反而提升了1.8个百分点。这说明尾部风险的降低并不是简单靠牺牲收益换来的,去掉高相关性的重复风险暴露,反而能在不损失收益的情况下大幅压缩尾部风险。
这个过程中我最大的体会是:CVaR优化对相关性输入极其敏感,资产间相关系数估计轻微变化都会引起权重大幅波动。所以在用历史数据估计协方差时,我建议对2到3年的窗口用指数加权平均,让近期的相关结构有更大权重,能显著提高权重稳定性。
5. PortfolioCVaR对象使用中的经验细节
5.1 权重稳定性问题与收缩估计
用历史数据估计协方差矩阵,在资产数量超过样本数量时会出现严重估计误差。有一种简单好用的方案是Ledoit-Wolf收缩估计,把样本协方差向单位矩阵收缩,能大幅改善权重稳定性。MATLAB里用covarianceShrinkage函数,这一段逻辑值得加入自己的工作流。
% 收缩估计协方差 Sigma = covarianceShrinkage(returns);然后把收缩后的协方差和样本均值一起传给PortfolioCVaR对象,注意不要再用estimateAssetMoments,会覆盖掉自定义协方差矩阵。
5.2 极端约束下的可行前沿
当资产间相关性极高时,比如同类商品或者同板块股票,CVaR前沿会出现高度不稳定区域。这种情况下我会检查前沿曲线上是否出现明显的反向凸起,如果出现,说明求解器在这个区域对约束敏感,组合权重可能是病态解。解决方法一般是收紧Bounds,或者合并相关性高于0.95的资产。
5.3 代码发布前的路径问题
标题里是.rar压缩包,很多朋友下载之后直接解压,发现代码跑不通,最常见的原因是原始代码用了绝对路径。我在分享代码的时候都会刻意用相对路径,并且把数据读取部分单独封装。如果你拿到别人的代码,先检查是不是用了类似D:\Users\admin\data.csv这样的绝对路径,改成相对路径即可。另外,MATLAB版本差异也会导致某些方法不可用,比如旧版本没有setCosts方法,建议用ver('finance')查看金融工具箱版本。
5.4 与Portfolio对象的联动
很多人不知道,PortfolioCVaR对象和Portfolio对象之间不能直接转换,它们的目标函数逻辑是不同的。Portfolio对象最小化的是方差,PortfolioCVaR对象最小化的是CVaR。如果你的研究流程需要同时输出均值-方差前沿和均值-CVaR前沿,就需要跑两套优化。不过结果可以放在同一张图上对比,一般都会看到CVaR前沿在边界部分更保守,这正是尾部风险约束的效果体现。
6. 从代码到决策的最后一公里
写完优化代码只是第一步,真正的难点在于怎么解释结果、怎么让决策层接受。CVaR给出的是一个组合,但没有告诉你“为什么是这个组合”。我在实际工作中会把权重向量转换成可解释的归因结果:检查每个资产的边际CVaR贡献,看哪个资产是风险的主要来源。MATLAB里虽然没有直接给出边际CVaR的内置函数,但可以通过数值导数计算:
% 计算边际CVaR的简化思路 w0 = wTarget; dcVaR = zeros(1, N); epsilon = 0.001; for i = 1:N w_plus = w0; w_plus(i) = w0(i) + epsilon; w_plus = w_plus / sum(w_plus); % 重新归一化 pTemp = p; % 注意要重新估计情景对权重的映射 dcVaR(i) = (computeCVaR(pTemp, w_plus) - computeCVaR(pTemp, w0)) / epsilon; end这里的computeCVaR函数需要根据情景矩阵自己写一套计算逻辑:组合收益等于情景矩阵乘以权重向量,然后取尾部均值。这样就能定位到“组合中哪个资产是尾部风险的最大贡献者”,这是写风控报告时最核心的数据。
我在实际使用中发现,PortfolioCVaR对象虽然好用,但别太迷信默认参数。每一个参数的设定背后都对应着投资逻辑的判断,比如置信水平反映的是你能承受多大的尾部风险,Bounds反映的是你对单资产风险的容忍度,这些都是投资决策的一部分,不是求解器能替你决定的。最后再分享一个小技巧:在跑任何优化之前,先运行一次p.checkFeasibility(weights),看看初始权重是否满足所有约束,这个小动作能在调试时帮你省下好几个小时的时间。
本文还有配套的精品资源,点击获取