简介:本资源是一套基于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 / estimateFrontier | setScenarios / 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优化最终是一个线性规划问题,变量数和约束数会随情景数量线性增长。不同规模的体验差异很大:
| 情景数量 | 资产数量 | 求解时间(我的机器) | 体验 |
|---|---|---|---|
| 500 | 10 | < 1秒 | 飞快 |
| 2000 | 20 | 2-3秒 | 顺畅 |
| 10000 | 30 | 20-30秒 | 可接受 |
| 50000 | 50 | 数分钟 | 等得难受 |
如果你做的是滚动回测,要跑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 = windowSize+1 : 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,建议先把你手上的资产数据跑一遍最小实例,然后把置信水平、约束、情景数量这些参数逐个改一遍,看看权重和前沿曲线的响应——这个过程比读十篇文档都管用。
本文还有配套的精品资源,点击获取