简介本资源是一套基于Matlab金融工具箱的CVaR投资组合优化实战代码面向计算机、电子信息工程、数学等专业的本科生及研究生用于课程设计、期末大作业与毕业设计中的金融建模实践。代码依托PortfolioCVaR对象实现条件风险价值CVaR优化建模聚焦尾部风险管理支持Matlab 2014/2019a/2024a多版本附带可直接运行的案例数据与完整参数化框架便于调整预期收益、协方差矩阵及风险约束。压缩包共7个文件主体为6个XML配置与元数据文件含文档结构、核心属性及关系定义配合1个rels关系描述文件总大小274KB结构规范、模块清晰利于理解Matlab金融工具箱底层对象序列化机制。已有492人学习下载读者可即开即用获得可复现的CVaR优化全流程从资产收益率模拟、CVaR模型构建、约束设置到最优权重求解与结果可视化注释详尽、逻辑分层明确显著降低金融工程编程入门门槛。 做组合优化的这些年我最大的感受是很多人把“风险”两个字理解得太扁平了。以前我也一样打开Matlab用Portfolio对象算一个均值-方差最优权重觉得把标准差压住了就是控制风险了。直到有一次回测里组合的波动率并不高但某个极端交易日直接把半年的收益全部吐了回去我才开始认真研究条件风险价值CVaR——也正是在那时候我把PortfolioCVaR对象当成了主力工具。这篇文章是我把整套用Matlab做CVaR投资组合优化的代码、参数调节过程、以及运行时踩到的坑重新整理后的完整记录。目标很直接给那些已经知道CVaR概念、但还没找到一条顺手上手路径的人一份可以照着跑的方案。你不需要是数学专家只要有基本的Matlab使用经验能读懂收益率的矩阵结构就能把下面的代码跑通然后根据自己手上的资产数据去做优化。1. 为什么我放弃了标准差开始盯尾部损失1.1 标准差骗了我波动率低的组合照样巨亏均值-方差模型的核心逻辑是用标准差衡量风险这有一个隐含假设收益率近似服从正态分布。可真实市场里的资产收益尤其是股票、加密货币、商品期货普遍存在厚尾特征。所谓厚尾就是极端涨跌出现的频率比正态分布预测的要高得多。我做过一次对比实验用同一批资产数据分别用标准差和CVaR作为风险指标去优化然后把两个组合放进一段包含急跌行情的样本外区间里回测。标准差最小的那个组合波动率数据确实好看但最大回撤达到了18%而CVaR优化出来的组合最大回撤控制在11%左右。问题就出在标准差把上涨和下跌一视同仁而CVaR只关心“最坏的那一部分损失”。这解释了一个反直觉的现象当你把波动率压得极低时组合往往大量持有低波动资产但低波动资产在危机时未必跌得少甚至可能因为流动性差而跌得更猛。波动率是一个“对称”的风险度量而投资者真正害怕的是“不对称”的尾部损失。1.2 VaR和CVaR的直观区别一个看水位一个看淹没深度要理解PortfolioCVaR在做什么得先分清VaR和CVaR。VaR风险价值说的是在给定置信水平下比如95%组合在未来一段时间内的最大可能损失是多少。用大白话说100天里最多有5天亏损会超过这个数。它回答的是“最坏情况是哪一档”。CVaR条件风险价值则更进一步当亏损真的超过VaR那条线之后这些“最坏的日子”平均会亏多少。它回答的是“最坏情况到底有多坏”。我常用一个防洪的例子来类比VaR告诉你大坝的高度是多少CVaR告诉你如果洪水没过坝顶整个城市平均被淹到几楼。两者都重要但如果只能选一个指标来指导设计CVaR显然更接近“灾难损失”的本质。还有一个数学模型层面的关键差异VaR不满足次可加性也就是说分散投资后组合的VaR可能反而比单个资产的加权VaR更大这就和“鸡蛋不要放在一个篮子里”的基本直觉冲突了。而CVaR是满足次可加性的分散化永远不增加CVaR。这意味着CVaR在数学上更适合作优化目标优化结果也更稳健。1.3 PortfolioCVaR对象能做什么Matlab金融工具箱里的PortfolioCVaR对象就是围绕CVaR优化封装好的一套API。它能做几件事接收历史收益数据或模拟生成的情景数据作为优化的输入设置置信水平ProbabilityLevel、权重边界、预算约束、交易成本、换手率约束等用线性规划求解器找到最小化CVaR的最优权重绘制CVaR-收益有效前沿方便你观察不同收益目标下的尾部风险水平。相比自己写CVaR优化的线性规划模型这个对象帮你把建模细节遮住了但前提是你得理解它内部的输入输出逻辑否则很容易出现“代码跑通了结果明显不对”的情况。这一篇的后面部分我会把每个关键环节展开讲。2. PortfolioCVaR的优化逻辑情景是第一输入2.1 为什么输入不是协方差矩阵而是情景矩阵用过Portfolio对象做均值-方差优化的人一定熟悉estimateAssetMoments和estimateAssetCovariances这两个方法。它们把历史收益压缩成均值和协方差两个矩阵后续优化只需要这两个统计量即可。但CVaR不是这样工作的。CVaR是对“情景”求尾部均值所以PortfolioCVaR对象的核心输入是情景矩阵。每一行代表一个市场情景下所有资产的收益率每一列代表一只资产在不同情景下的表现。你可以把情景矩阵理解成一组“平行世界”假如未来只有这500个交易日的历史情景每个情景等概率发生那么在给定的权重组合下每个情景会有一个组合收益率。把所有情景的组合收益率从小到大排序CVaR就是最左侧那5%假设置信水平0.95情景的平均损失。这种设计有几个直接后果。第一它不依赖正态性假设任何分布的尾部特征都会被情景数据如实反映。第二情景数据的质量直接决定优化结果的质量如果你的情景没有覆盖到极端行情CVaR就会被严重低估。第三协方差矩阵里的相关性在情景矩阵里实际上是“隐含”的不需要单独估计。2.2 情景来源历史、模拟、还是手动压力测试Matlab的PortfolioCVaR提供了几种给情景赋值的方式我按实际优先级说明第一种直接用历史收益率作为场景。代码是p p.setScenarios(returns)其中returns是一个T行N列的矩阵。这种情况下每个历史交易日被视为一个等权情景优点是完全真实、没有分布假设缺点是尾部样本量有限。如果你只有250个交易日约一年的数据95%置信水平下尾部只有12个情景估算误差会非常大。第二种用蒙特卡洛模拟生成情景。代码是p p.simulateNormalScenariosByData(returns, numScenarios)它会基于历史数据的均值、协方差生成指定数量的多元正态随机情景。优点是情景数量可以很多比如5000甚至20000个缺点是引入了正态假设而真实收益率往往是厚尾的所以模拟出的尾部会比实际情况温和。第三种手动添加压力情景。用p p.addScenarios(scenarios, prob)可以把极端但可能发生的情景直接塞进去比如某天全市场下跌10%、某个行业指数下跌25%并给这些情景一个主观概率。这是把“观点”注入模型的常用方式。我的实践建议是历史情景为主干用模拟情景扩充尾部样本再手动加入两三个压力情景做兜底。别只依赖单一来源尤其是只拿几百行历史数据硬算结果很容易被某几个极端交易日牵着走。2.3 ProbabilityLevel到底该怎么设置信水平是PortfolioCVaR最敏感的参数之一。它控制的是“你关心的尾部有多深”。代码里设置方式是p p.setProbabilityLevel(0.95);0.95和0.99看起来只差0.04但实际效果差异巨大。以下几点是我实测下来的体会0.90尾部样本多估计稳定但风险度量偏温和可能压不住真正的极端行情。适合资产数量多、数据量少的场景。0.95工程上最常用的折中点尾部风险覆盖和估计稳定性相对平衡。0.99对极端风险最敏感但尾部样本极少。如果只有1000个情景尾部只有10个点这10个点里只要有一个异常值优化结果就可能剧烈变动权重会变得非常不稳定。所以我通常会先用0.95做基准回测等整体流程稳定了再把0.99作为压力测试环境来对比。不要在回测阶段就直接上0.99否则你很难判断组合表现变差究竟是因为策略逻辑有问题还是因为尾部估计噪声太大。2.4 和Portfolio对象的方法对照一张表看明白我整理了一张对照表方便从均值-方差模型切换过来的人快速定位知识迁移点维度Portfolio均值-方差PortfolioCVaR风险度量标准差CVaR尾部期望损失核心输入预期收益、协方差矩阵情景矩阵优化问题类型二次规划QP线性规划LP对厚尾分布的敏感度低高置信水平参数无ProbabilityLevel有效前沿坐标横轴标准差纵轴收益横轴CVaR纵轴收益典型方法estimateAssetMoments / estimateFrontiersetScenarios / estimateFrontier这套对应关系记住后基本上可以无缝切换。PortfolioCVaR的很多约束方法其实和Portfolio对象是共享的比如setBounds、setBudget、setGroupBounds迁移成本没那么高。3. 第一次跑通从价格数据到有效前沿3.1 环境与数据准备我用的环境是Matlab R2021b需要金融工具箱Financial Toolbox。如果你电脑里没有这个工具箱代码会直接报错因为PortfolioCVaR类就定义在这个工具箱里。检查方法是命令窗口输入ver看列表里有没有Financial Toolbox。数据方面我准备了一个模拟示例6只资产的月度价格数据时间跨度从2018年到2023年约72个月。这里用月频数据是为了让组合优化结果更接近长期资产配置的场景如果你做的是中高频调仓改用日频数据也是完全一样的逻辑。首先把价格数据转换成收益率矩阵。这里要注意Matlab的tick2ret函数默认算的是简单收益率还是对数收益率取决于你传不传方法参数。我习惯用简单收益率% prices: T x N 矩阵每列是一类资产的历史收盘价 returns tick2ret(prices);得到的returns行数是T-1行因为每期收益率是相邻两期价格的变化率。记得检查一下returns里有没有NaN或者Inf尤其是数据源有空缺日期时这一步偷懒后面会非常痛苦。3.2 完整代码创建PortfolioCVaR对象并优化下面这段代码是我建议的最小可用版本每行都有注释说明% --- 基本设置 --- assetNames {沪深300, 中证500, 国债ETF, 黄金ETF, 标普500, 纳指}; numAssets length(assetNames); % --- 创建对象 --- p PortfolioCVaR; p p.setAssetList(assetNames); % --- 设置情景直接把历史收益率当作情景 --- p p.setScenarios(returns); % --- 置信水平0.95表示关心最差5%情景的平均损失 --- p p.setProbabilityLevel(0.95); % --- 约束设置 --- % 权重非负且加和为1做多组合 p p.setDefaultConstraints; % 单只资产权重上限设为30%避免过度集中 p p.setBounds(0, 0.3); % --- 估计有效前沿返回20个组合点 --- wFrontier p.estimateFrontier(20); % --- 画图观察前沿 --- figure; p.plotFrontier(20); title(CVaR-Return Frontier); % --- 指定目标月收益为0.8%求解对应最优权重 --- targetReturn 0.008; wTarget p.estimateFrontierByReturn(targetReturn); % --- 输出该组合的风险与收益 --- [riskCVaR, retPortfolio] p.estimatePortMoments(wTarget); disp([目标收益下CVaR: , num2str(riskCVaR * 100), %]); disp([组合期望收益: , num2str(retPortfolio * 100), %]); % --- 查看权重 --- for i 1:numAssets fprintf(%s: %.4f\n, assetNames{i}, wTarget(i)); endestimateFrontier(20)返回的是一个N行20列的矩阵每一列是一组权重向量对应有效前沿上的一个组合。从最保守到最激进20个点逐渐增加组合预期收益同时CVaR风险也在上升。estimateFrontierByReturn则是反向操作你给我一个目标收益我还你一组在这个收益下CVaR最小的权重。这里有个隐含要求目标收益必须在有效前沿的收益范围内如果设得太高Matlab会报错或返回一个边界解。3.3 结果解读先看形状再看权重跑完上面的代码你会得到一条从左上到右下倾斜的前沿曲线。横轴是CVaR纵轴是收益。曲线的形状和斜率很有信息量左端低CVaR端通常是大量配置债券和黄金的组合收益低但尾部风险很小右端是高收益资产为主的组合CVaR迅速攀升。如果中间某段曲线明显变陡说明在这个区间内每增加一点收益需要承受的尾部风险增量是递增的。输出目标权重之后第一步不是看业绩而是看权重是否合理。我见过有人跑出一个组合权重全压在一只资产上回查发现是约束没设对或者情景数据里其他资产的尾部风险被严重高估了。权重过度集中本身就是风险哪怕CVaR很低也说明模型没有真正起到分散化的作用。4. 实战中的坑分位数、估计误差与权重集中4.1 尾部样本不足置信水平越高噪声越大这是PortfolioCVaR最容易被低估的一个坑。我在跑一批300个交易日的数据时把置信水平设成了0.99优化出的权重几乎全部押在单只资产上换一个数据窗口结果又完全不同。后来一分析问题出在尾部样本太少。300个情景0.99置信水平意味着只有3个情景属于“尾部”。这3个情景全部来自具体的历史极端日。如果其中一天正好某只资产没跌那只资产就会被模型当成“安全资产”从而获得极高权重。这是典型的过拟合尾部噪声。解决思路有几条用模拟情景扩充数量。把300个历史情景通过simulateNormalScenariosByData扩成5000个尾部样本会平滑很多。降低置信水平到0.95。工程上这是更稳的做法。手动增加压力情景让尾部包含多种极端情况而不是依赖某几天历史。我后来把0.99留作风控复核指标优化阶段统一跑0.95问题基本消失了。4.2 权重集中CVaR优化的“天然倾向”CVaR优化的本质是“找一组权重让最差情景的平均损失最小”。如果有一只资产在大部分尾部情景里表现都不错模型就会倾向于把大量权重给它。这不像均值-方差模型里协方差矩阵会天然惩罚高相关性资产CVaR优化更容易做出“孤注一掷”的决策。我的处理方式分三层第一层设置单资产权重上限。比如前面代码里的setBounds(0, 0.3)每只资产最多只能占30%从根上杜绝过度集中。第二层用行业或风格分组约束。比如要求权益类资产总权重在40%到60%之间债券类资产总权重不低于20%用setGroupBounds实现。第三层加入换手率或交易成本约束让权重变化不过于频繁这也是对“模型跳跃”的惩罚。判断模型有没有“过度自信”的一个好办法重采样测试。把样本数据按时间顺序随机抽200天反复计算最优权重观察权重的分布。如果某个资产的权重在多次抽样中从0跳到0.6说明结果非常不稳定需要加约束或者调整情景。4.3 求解性能情景数量与计算时间的平衡PortfolioCVaR优化最终是一个线性规划问题变量数和约束数会随情景数量线性增长。不同规模的体验差异很大情景数量资产数量求解时间我的机器体验50010 1秒飞快2000202-3秒顺畅100003020-30秒可接受5000050数分钟等得难受如果你做的是滚动回测要跑100个窗口每个窗口都做一次优化那情景数量就得严格控制。我在滚动回测里通常把情景数压到2000左右既保证了尾部样本有40个点左右又不会让整体回测时间失控。另外说一句在虚拟机里跑Matlab的优化任务确实会明显变慢尤其是内存占用大的线性规划。如果你卡在这一步优先检查是不是虚拟机资源分配不够而不是急着优化代码。5. 从单次优化到滚动回测的完整流程5.1 一个可以复用的回测骨架单次优化只能证明“这个模型在某段历史上表现不错”在实盘决策前你得做滚动回测每次用过去一段窗口的数据优化权重在下一个时段按这个权重持仓等下一个时点到来时重新优化。下面是一个简化的滚动回测骨架我用的是月度再平衡% 假设 returns 是全部时间段内的资产收益率时间粒度是月度 T size(returns, 1); windowSize 36; % 训练窗口36个月 step 1; % 每次往前滚动1个月 rebalanceMonths 1; % 再平衡频率1个月 % 存储每次的最优权重与实际组合收益 weightsHistory []; portfolioReturns []; for t windowSize1 : T % 训练数据最近windowSize个月 trainData returns(t-windowSize : t-1, :); % 创建CVaR组合对象 p PortfolioCVaR; p p.setAssetList(assetNames); p p.setScenarios(trainData); p p.setProbabilityLevel(0.95); p p.setDefaultConstraints; p p.setBounds(0, 0.3); % 目标收益可以动态调整这里简单用历史训练期平均收益 targetRet mean(mean(trainData, 1)); w p.estimateFrontierByReturn(targetRet); % 记录权重 weightsHistory [weightsHistory, w]; % 用下一期的实际收益计算组合收益 portfolioReturns [portfolioReturns; w * returns(t, :)]; end回测里有三个容易踩的坑前视偏差trainData的最后一个时间点是t-1评估用的是t时刻的实际收益二者严格错开不能混用。如果数据重叠优化结果会虚高。目标收益设定直接取历史均值有时候会导致优化器在有效前沿上找不到对应点稳妥做法是设为目标收益落在历史收益的20%到80%分位数之间或者用estimateFrontierByRisk代替。权重变化幅度如果两个相邻月份的权重变化很大真实交易中会产生大量手续费。回测里我会加一个简单的交易成本惩罚实际收益减去换手率乘以双边成本。5.2 我现在的实用工作流踩了几年坑之后我现在跑CVaR组合优化的流程基本固定成五步第一步数据清洗。补齐缺失值、剔除停牌期异常数据、检查是否有重复行。这一步很无聊但能避免80%的后续问题。第二步情景生成。历史情景为底用蒙特卡洛模拟扩充到2000-5000个情景再手动加入2-3个压力情景。第三步约束设计。单资产上下限、大类资产权重区间、必要时加换手率约束。约束是和人沟通的桥梁约束越贴实际投资逻辑结果越可落地。第四步优化与重采样验证。先跑一次基准优化再做30次有放回的重采样看权重分布是否稳定。不稳定就回到第三步加约束。第五步事后归因。每期回测结束后把组合实际收益里的尾部贡献拆出来看是不是和建模时的CVaR假设一致。如果实际尾部损失远大于模型预测说明情景里的极端情况还不够极端。5.3 扩展方向从CVaR到谱风险度量用熟了PortfolioCVaR之后你可能会遇到一个进阶问题单一置信水平的CVaR其实只关注了尾部的一个切面。0.95和0.99给出的结果可能差别很大到底该信哪个理论上更好的做法是谱风险度量Spectral Risk Measure它给不同损失分位数赋予不同的风险权重相当于把整个尾部都纳入考量而不是只盯一个阈值。Matlab本身的PortfolioCVaR不直接支持自定义谱权重但你可以通过多次调用PortfolioCVaR分别计算不同置信水平下的CVaR然后按权重组合成一个综合风险指标。虽然这不是严格意义上的谱风险度量求解但在工程实现上已经能覆盖大部分需求。另一个值得尝试的方向是结合Copula模型来生成情景。多元正态模拟生成的情景无法刻画资产之间的非线性相关比如股市大跌时所有资产的相关性急剧上升这是正态分布模拟不出来。用Copula拟合历史收益的相依结构再抽样生成情景效果会比simulateNormalScenariosByData好不少。不过这个方向对数学功底和代码能力要求都比较高建议先把基础流程跑通再考虑。我自己现在的做法比较朴素回测阶段用0.95的CVaR做优化同时每期输出0.99的CVaR做风险报警线。优化和监控用两套置信水平既能保证权重稳定又能时刻盯住极端尾部变化。如果你也正在用Matlab调PortfolioCVaR建议先把你手上的资产数据跑一遍最小实例然后把置信水平、约束、情景数量这些参数逐个改一遍看看权重和前沿曲线的响应——这个过程比读十篇文档都管用。本文还有配套的精品资源点击获取