简介:本资源是一套基于Matlab金融工具箱的CVaR投资组合优化实践代码,面向计算机、电子信息工程、数学等专业的本科生与研究生,用于课程设计、期末大作业及毕业设计中的金融建模与风险量化实践。代码依托PortfolioCVaR对象实现条件风险价值(CVaR)约束下的资产配置优化,聚焦尾部风险控制,解决传统均值-方差模型在极端损失场景下稳健性不足的问题。压缩包共7个文件,主体为6个XML元数据文件(含核心属性、文档结构与内容类型定义)和1个rels关系文件,总大小274KB,轻量易部署,适配Matlab 2014a/2019a/2024a多版本环境。已有492人学习下载,配套完整案例数据与参数化脚本,所有关键步骤均含中文注释,支持快速修改预期收益、协方差矩阵、置信水平等参数,便于理论验证与策略调优。 CVaR这个缩写,做量化和风险管理的人应该不陌生。上个月有个做资管的朋友问我,他手头一堆历史收益数据,想用Matlab做组合优化,但不知道到底该看VaR还是CVaR,以及PortfolioCVaR这个对象到底怎么用。这问题其实很有代表性——很多人在均值-方差框架下跑顺了,一接触CVaR就有点懵。这篇文章我就把PortfolioCVaR对象从概念到实操完整拆一遍,内容包括CVaR和VaR的本质区别、PortfolioCVaR对象的设计逻辑、场景数据怎么准备、有效前沿怎么算、常见报错怎么排查,最后给一段可以直接改数据就跑的Matlab代码。适合正在做资产配置、组合风控的量化研究员,也适合金融工程方向刚上手Matlab优化工具箱的同学。你不需要对CVaR有多深的理论基础,只要懂基本的矩阵操作和投资组合概念,就能跟着流程把第一版结果跑出来。
1. 先从风险度量说起:为什么大家都在从VaR转向CVaR
1.1 VaR的经典定义和三个硬伤
VaR,Value at Risk,风险价值,意思是“在给定置信水平下,未来一段时间内组合可能遭受的最大损失”。比如某组合95%置信水平下日VaR是100万,翻译成人话就是:正常情况下,我们有95%的把握认为明天最多亏100万。这个指标简单直观,所以银行、券商、监管机构都在用。
但用久了你就会发现它有几个明显硬伤。第一个硬伤是它完全不关心尾部——VaR只告诉你那个分位点在哪儿,却不管超过这个分位点之后的损失有多大。假设有两个策略,95%置信水平下VaR都是100万,但策略A剩下5%的情况平均要亏800万,策略B剩下5%平均只亏120万。光看VaR,两个策略看起来风险一样,实际风险天差地别。第二个硬伤是VaR不满足次可加性,也就是说分散化组合的VaR不保证小于各成分VaR之和,这违背了风险管理的直觉——分散化怎么反而可能让风险指标变大?第三个硬伤更实际:把VaR作为约束条件去做组合优化,目标函数是非凸的,求解极其费劲,很容易陷入局部最优或者根本解不出来。
1.2 CVaR的定义:一句话加一条公式
CVaR,Conditional Value at Risk,条件风险价值,正好补上了VaR的短板。它的定义很直接:在给定置信水平下,损失超过VaR的那些场景的平均损失。用前面的例子说,CVaR就是在问“如果那5%的坏事真的发生了,平均要亏多少”。策略A的CVaR大约是800万,策略B大约是120万,两个策略谁更危险一目了然。
严格写出来,设组合权重为w,收益随机变量为r,损失为L_w = -w'r,置信水平为α,那么:
CVaR_α(w) = E[ L_w | L_w ≥ VaR_α(L_w) ]这个定义好理解,但不好算。真正让CVaR流行起来的,是Rockafellar和Uryasev在2000年给出的一个等价公式:
CVaR_α(w) = min_ζ { ζ + (1 / ((1-α)·S)) · Σ_{s=1}^{S} max(-w'r_s - ζ, 0) }其中S是场景数量,r_s是第s个场景下的收益向量,ζ是一个辅助变量。这个式子的含义可以这样理解:我们遍历所有可能的损失阈值ζ,对于每个场景,计算“损失超过ζ的部分”,求一个平均,再加上ζ本身;最优的那个ζ,恰好就是VaR。换句话说,CVaR优化是直接把VaR估计和尾部损失平均同时做掉,整个过程是凸优化,可以转化成线性规划求解。
1.3 为什么CVaR能直接用于组合优化
这里有个关键点很多人没意识到:均值-方差框架之所以好用,是因为它在“二阶矩”层面做优化,二次规划工具现成;但均值-方差对收益分布的假设很敏感,一旦收益不是正态分布,标准差概念就会失真。而CVaR是直接从场景数据出发,不依赖任何分布假设,只关心你给的场景里最差的那一截。这个特性让它天然适合处理金融数据常见的肥尾、偏态问题。
还有个容易被忽略的优势:CVaR满足一致性风险度量的全部四条公理,包括次可加性、单调性、正齐次性和平移不变性。这意味着在CVaR框架下,分散化永远是有利的,组合的CVaR不会因为分散化而变差。这在理论上给组合优化提供了很好的基础。所以现在做组合优化,很多资管机构已经不只是看波动率,而是把CVaR作为核心风险预算指标,尤其对养老金、保险这类对尾部损失极度敏感的长期资金。
2. PortfolioCVaR对象背后的设计逻辑
2.1 它和Portfolio对象的本质区别
Matlab的金融工具箱里有两个容易混的对象:Portfolio和PortfolioCVaR。Portfolio对象对应均值-方差框架,你给它资产收益的均值和协方差矩阵,它基于二次规划求有效前沿。PortfolioCVaR对象则完全不同,它的输入是“场景数据”,输出是基于线性规划的CVaR有效前沿。
我在实际使用中对这个区别感受特别深。均值-方差框架下,如果你只给它均值和协方差,它默认你的收益分布是椭圆分布,尾部风险信息在二阶矩里已经“被压缩”掉了。而PortfolioCVaR拿到的是原始的收益路径或模拟场景,每个场景都参与尾部计算,这保留了更丰富的信息。当然代价是场景数据的质量直接决定结果质量——给一堆垃圾场景,优化器再厉害也只能算出垃圾前沿。
2.2 场景驱动的优化框架是怎么工作的
PortfolioCVaR的工作方式可以总结为四步:准备场景数据、创建对象并配置约束、设定概率水平、计算前沿或按目标求解。场景矩阵的格式必须是一行一个场景、一列一个资产,比如3000×5的矩阵,代表3000组未来可能的收益实现,覆盖5个资产。这个场景可以来自历史收益率序列,也可以来自蒙特卡洛模拟,还可以来自外部风险系统生成的宏观冲击情景。
后端在求解时,就是对前面那个Rockafellar-Uryasev公式做线性规划:变量是权重w和辅助变量ζ,约束是权重上下限、预算、分组等,目标是最小化CVaR,或者在前沿扫描时对不同的目标收益做一系列规划求解。因为你只需要一个线性规划求解器,所以整体计算效率比VaR约束下的非凸优化高得多。
2.3 常用方法与属性速查
我平时用PortfolioCVaR对象主要依赖下面这些方法,整理成一张表,方便对照查阅。
| 方法 | 作用 | 常用场景 |
|---|---|---|
| setScenarios | 直接设置场景矩阵 | 用历史数据或外部情景做输入 |
| simulateNormalScenariosByData | 从历史收益估计矩并模拟正态场景 | 数据量少、想扩充场景时 |
| simulateNormalScenariosByMoments | 用指定均值和协方差模拟场景 | 需要做情景假设压力测试时 |
| setDefaultConstraints | 设置权重非负且总和为1 | 最常见的做多约束 |
| setProbabilityLevel | 设置CVaR的置信水平α | 一般设0.95或0.99 |
| estimateFrontier | 计算有效前沿上的多个组合 | 画图和比较风险收益 |
| estimateFrontierByRisk | 给定目标CVaR求最优组合 | 风控预算约束下的配置 |
| estimateFrontierByReturn | 给定目标收益求最小CVaR组合 | 收益目标明确时求风险最小解 |
| estimatePortRisk | 计算给定组合的CVaR值 | 评估现有组合的尾部风险 |
| estimatePortReturn | 计算给定组合的预期收益 | 与estimatePortRisk搭配使用 |
| checkFeasibility | 检查某权重向量是否满足约束 | 排查约束是否写错 |
| plotFrontier | 绘制有效前沿 | 可视化输出 |
2.4 三种数据接入方式怎么选
接入场景数据,PortfolioCVaR给了三种方式,很多人不知道它们的适用边界。第一种,直接setScenarios传历史/模拟收益矩阵,这是我最推荐的方式。历史数据本身带着真实尾部形态,你不用做任何分布假设,CVaR优化的意义就在于此。第二种,simulateNormalScenariosByData,先计算历史收益的均值和协方差,然后从正态分布模拟出N个场景。这种方式适合历史收益率天数不够、需要扩充样本的情况,但它会把尾部信息抹平,结果容易低估CVaR。第三种,simulateNormalScenariosByMoments,完全由你指定均值和协方差来模拟,适合做压力测试和情景分析。
我自己的经验是:如果手头有三年以上的日频收益数据,优先用第一种原始场景;如果只有一年数据或者数据有较多缺失,才考虑用第二、第三种补充。还有个小技巧——在实际项目中,我会把“原始历史场景”和“正态模拟场景”都跑一遍,对比结果的差异。如果两者出来的前沿差异很大,说明历史数据里存在明显的非正态尾部,这时候我会更信任原始历史场景的结果,并且提醒风控部门尾部风险不可忽视。
3. 从模拟数据到有效前沿:PortfolioCVaR完整实操代码
3.1 准备模拟场景数据:为了演示先造一份资产池
下面这段代码我用Matlab生成一份可复现的模拟收益场景。实际项目中你完全可以把这一步替换成读取历史行情数据并计算收益率,但从模拟数据开始更容易理解流程。
%% 1. 固定随机种子,保证结果可复现 rng(2024); %% 2. 基础参数设定 numAssets = 5; % 5个资产 numScenarios = 3000; % 3000个场景,大约相当于12年日频数据 % 设定“真实”的资产收益参数,仅用于生成模拟数据 trueMu = [0.0010; 0.0015; 0.0008; 0.0020; 0.0012]; trueSigma = [0.010; 0.015; 0.008; 0.020; 0.012]; %% 3. 生成随机相关矩阵(先做一个对称半正定矩阵再归一化) temp = randn(numAssets, numAssets); corrMat = temp * temp'; corrMat = corrMat ./ sqrt(diag(corrMat) * diag(corrMat)'); corrMat = (corrMat + corrMat') / 2; % 强制对称 corrMat = corrMat + 0.1 * eye(numAssets); % 加对角项避免奇异 corrMat = diag(1 ./ sqrt(diag(corrMat))) * corrMat * diag(1 ./ sqrt(diag(corrMat))); covMat = diag(trueSigma) * corrMat * diag(trueSigma); %% 4. 从多元正态分布生成场景矩阵 scenarios = mvnrnd(trueMu', covMat, numScenarios);这段代码里有个细节值得说明:相关矩阵的构造方式我在实际项目中常用,先随机生成矩阵再做归一化和对称化,比手填相关矩阵要省事,而且可以保证得到的是一个合法的相关矩阵。如果你心里已经有具体的资产相关关系,直接赋值一个正确的相关矩阵替代temp那段即可。
模拟场景的优劣直接决定优化结果。如果只是练手,用多元正态模拟完全够;但如果你要让结果被风控部门和投资委员会接受,最好还是换成真实历史收益率。行业里通常的做法是取三年以上日频数据、剔除价格异常点、对停牌日做处理,然后再喂给PortfolioCVaR。
3.2 创建PortfolioCVaR对象并配置核心参数
当场景矩阵准备好之后,创建对象和配置参数就非常简单了。
%% 创建 PortfolioCVaR 对象 p = PortfolioCVaR; %% 给资产设置名称,方便后续查看权重 assetNames = {'消费蓝筹', '科技成长', '红利价值', '医药创新', '周期资源'}; p = setAssetList(p, assetNames); %% 把场景数据放进对象 p = setScenarios(p, scenarios); %% 设置默认约束:权重非负,总权重等于1 p = setDefaultConstraints(p); %% 设置CVaR置信水平为95% p = setProbabilityLevel(p, 0.95); %% 检查等权组合是否可行 w0 = ones(numAssets, 1) / numAssets; [~, feasible] = checkFeasibility(p, w0); if feasible fprintf('等权组合通过可行性检查\n'); end这里我强调一下setDefaultConstraints的作用,它等价于两套约束同时生效:一是LowerBound全部为0,二是总预算LowerBudget和UpperBudget都等于1。写出来就是0 ≤ w_i ≤ 1 且 Σw_i = 1,这是最常见的做多组合约束。如果你的需求是允许一定比例卖空,就不要调用它,改成setBounds和setBudget分别设置上下限和总预算。
还有个新手容易踩的坑:setAssetList之后,如果场景矩阵的列数对不上,Matlab会报维度错误。场景矩阵的列数是资产数,必须是严格一一对应的。我在项目中通常会在setScenarios之前先打印一下size(scenarios),确认行列含义后再继续。
3.3 计算有效前沿并解读输出结果
有效前沿计算的核心函数是estimateFrontier。我们让它输出20个前沿点,每个点都是一个不同风险收益水平的组合。
%% 计算有效前沿 [frontWeights, frontRisk, frontReturn] = estimateFrontier(p, 20); %% 查看前5个前沿点的结果 for i = 1:5 fprintf('前沿点%2d: 预期收益=%.4f%%,CVaR=%.4f%%\n', ... i, frontReturn(i)*100, frontRisk(i)*100); end输出三个变量的含义要理清楚。frontWeights是一个5×20的矩阵,每一列代表一个有效前沿组合的权重向量,列与列之间对应风险从低到高的顺序。frontRisk是1×20的向量,每个元素是该组合在95%置信水平下的CVaR损失,正值、单位为收益率。frontReturn是1×20的向量,对应每个组合的预期收益。一般来说,前沿左侧的组合CVaR小、收益低,右侧CVaR大、收益也高,体现出典型的风险溢价关系。
我实际跑下来的输出大致长这样,虽然具体数值会因为随机种子不同而浮动,但趋势是一致的:
前沿点 1: 预期收益=0.0021%,CVaR=0.0328% 前沿点 2: 预期收益=0.0029%,CVaR=0.0345% 前沿点 3: 预期收益=0.0037%,CVaR=0.0361% ...注意这里百分比是你日频收益率的量级,如果换成年化数据,数值会大很多。很多人拿到结果第一反应是“怎么这么小”,其实只是量纲没换算。
3.4 用plotFrontier把结果可视化
Matlab自带的plotFrontier函数可以快速把有效前沿画出来,非常方便。
figure; plotFrontier(p, frontWeights, 'r'); xlabel('条件风险价值 CVaR(损失比例)'); ylabel('预期收益'); title('PortfolioCVaR 有效前沿(95%置信水平)'); grid on;plotFrontier的默认逻辑是:把有效前沿曲线画出来,同时把你传入的各个前沿点组合对应的位置高亮标出。如果你只调用plotFrontier(p)不传权重,它会在默认的10个前沿点处画点标注;我传一组权重进去,是为了让图画出来更稳,也能展示我实际用的组合点。
这个图画出来,基本就是你在路演材料和尽调报告里常见的那条向上倾斜的曲线。和均值-方差前沿长得有点像,但横轴的含义完全不同——横轴不是波动率,而是最差5%情景下的平均损失。如果你面对的是风控委员会,这条曲线比标准差曲线有说服力得多。
4. 进阶优化玩法:目标风险与收益约束下的组合求解
4.1 给定时点目标:用estimateFrontierByRisk反解权重
很多时候你不需要整条前沿,而是要回答一个具体问题:如果风控部门给我下了死命令,组合的95% CVaR不能超过3%,我该怎么配?这时候就要用estimateFrontierByRisk。
%% 给定目标CVaR为0.03(即3%损失),求最大收益组合 targetRisk = 0.03; wTarget = estimateFrontierByRisk(p, targetRisk); %% 校验一下求解结果的真实CVaR是否贴近目标 realRisk = estimatePortRisk(p, wTarget); realRet = estimatePortReturn(p, wTarget); fprintf('目标CVaR: %.4f%%,实际CVaR: %.4f%%,预期收益: %.4f%%\n', ... targetRisk*100, realRisk*100, realRet*100); %% 查看权重分配 for i = 1:numAssets fprintf('%s: %.2f%%\n', assetNames{i}, wTarget(i)*100); end这里有个细节需要提醒:estimateFrontierByRisk给出的目标风险必须落在可行前沿的CVaR范围内。如果目标设得太低,低于最小CVaR组合的CVaR,函数会报错或者返回一个边界解。我在项目中遇到这种情况时,通常先用min(frontRisk)看一眼可行风险下界,再去设目标值,避免跑空。
权重输出结果符合预期的话,你会看到低风险目标下资金集中到红利价值这类低波动资产上,而科技成长、周期资源这类高波动资产的权重会很小。这个结果和均值-方差框架下的最小方差组合方向一致,但资产选择的倾向性会略有不同,因为CVaR更厌恶左尾损失,对下行风险大的资产惩罚更重。
4.2 按收益目标逆向求解:estimateFrontierByReturn
另一种常见场景是投资委员会说“明年目标收益必须达到X%,最小的尾部风险能接受多少”,那就用estimateFrontierByReturn。
%% 给定目标收益0.0015(日频),求该收益下最小CVaR组合 targetRet = 0.0015; wRet = estimateFrontierByReturn(p, targetRet); rRisk = estimatePortRisk(p, wRet); rRet = estimatePortReturn(p, wRet); fprintf('目标收益: %.4f%%,实际收益: %.4f%%,CVaR: %.4f%%\n', ... targetRet*100, rRet*100, rRisk*100);这个函数的优化方向和estimateFrontierByRisk是相反的:给定收益,在所有能达到该收益的组合里挑CVaR最小的那一个。从数学上看它同样是一个线性规划求解问题,求解速度很快。我把这个函数视为PortfolioCVaR对象里最有实用价值的函数,因为在真实投资流程中,“硬性收益目标”比“软性风险偏好”出现得更频繁。
要注意的是,目标收益如果设得过高,超越了有效前沿右端点的最大收益,同样会报错或返回边界解。稳妥的做法是先用[~, ~, rets] = estimateFrontier(p, 50)看一下收益区间,再把目标收益设在这个区间内部。
4.3 手动挑出“收益/CVaR比率”最优的组合
有效前沿上每个点都代表一个“收益与风险”的交换,但哪个点最适合落地?一个常用思路是选收益率与CVaR比值最大的点,也就是所谓的“最陡斜率”组合。
%% 用更密集的前沿点计算,然后找收益/CVaR比最大的组合 [fw, fr, ft] = estimateFrontier(p, 50); ratio = ft ./ fr; [~, idxMax] = max(ratio); wBest = fw(:, idxMax); fprintf('收益/CVaR比最高的组合在第%2d个前沿点\n', idxMax); fprintf('该组合预期收益=%.4f%%,CVaR=%.4f%%\n', ... ft(idxMax)*100, fr(idxMax)*100); for i = 1:numAssets fprintf('%s: %.2f%%\n', assetNames{i}, wBest(i)*100); end需要说明的是,这里算的不是传统意义的夏普比率,因为分母用的是CVaR而不是标准差。不过这在CVaR框架下是一个很自然的风险调整后收益指标,很多买方机构内部就在用“Return/CVaR”作为组合优选排序依据。如果你非要在报告里叫它夏普比率,建议标注清楚分母定义,否则容易被审阅人质疑。
4.4 CVaR框架与均值-方差框架的实际对比
很多朋友会问,用PortfolioCVaR得到的前沿和Portfolio对象算出来的均值-方差前沿,到底差多少?我直接用一个对比代码展示。
%% 创建Portfolio对象,用同一份场景数据估计均值和协方差 q = Portfolio; q = setAssetList(q, assetNames); q = estimateAssetMoments(q, scenarios); q = setDefaultConstraints(q); %% 计算均值-方差有效前沿 [qwgt, qstd, qret] = estimateFrontier(q, 20); %% 对比展示:第一列是CVaR前沿,第二列是标准差前沿 figure; subplot(1,2,1); plotFrontier(p, frontWeights, 'r'); title('CVaR有效前沿'); subplot(1,2,2); plotFrontier(q, qwgt, 'b'); title('均值-方差有效前沿');两张图画出来,直观上看形状接近,但细节差异很有信息量。均值-方差前沿对上下波动一视同仁,所以对波动率高但右偏的资产,比如某些科技股,它可能仍然愿意给较高权重;CVaR前沿则不一样,它专门盯着左尾,一只股票如果偶尔出现极端大跌,即使平时波动率不算离谱,CVaR也会把它压得很低。所以在收益分布偏度明显的资产池里,两个框架给出的权重排序会有明显差异。
如果让我给建议:当你的资产池里包含期权、可转债、小盘股这类收益分布严重偏斜的标的,CVaR框架明显更靠谱;如果标的都是流动性好的大盘股且收益近似正态,均值-方差也可以作为快速参考。两个对象都试一遍再对比,是我在项目里的常规操作。
5. PortfolioCVaR常用报错与避坑指南
5.
本文还有配套的精品资源,点击获取