MATLAB均值方差投资组合模型实战:从数据到有效前沿
简介:这份资源面向金融工程、量化投资方向的学生与研究者,以及希望用MATLAB落地资产配置策略的从业者,围绕马科维茨均值方差模型展开,解决从历史行情数据到最优权重求解的完整实现问题。压缩包共12个文件,约108KB,包含8个m脚本、2个csv行情数据、1个mat数据文件与1个xlsx表格,脚本覆盖数据抓取、清洗、聚合与建模全流程,数据文件则提供标普500等标的的历史收益率样本。已有1917人学习下载,说明其在教学与实操场景中具备一定参考价值。读者可借助这些代码理解期望回报、方差与协方差矩阵的计算方式,掌握用quadprog求解二次规划、构建有效前沿并确定最优投资组合权重的思路,同时通过回测观察组合在历史数据上的表现,为后续策略调整与风险偏好匹配提供可复用的脚本框架与数据基础。
1. 均值方差投资组合模型:从一组收益率数据到有效前沿的最小闭环
很多人第一次接触量化投资,都是从马科维茨的均值方差模型开始的。它解决的问题很朴素:给定一堆资产的日收益率序列,怎么分配权重,才能在目标收益下把波动压到最低。标题里这个.rar包,本质就是一份「数据 + MATLAB 代码」的案例集,把教科书上的二次规划公式变成能跑出有效前沿的脚本。它适合两类人:一类是金融工程、数量经济方向的学生,需要交一份能复现的课程作业;另一类是刚转量化的工程师,想用 MATLAB 把组合优化的整条链路走通一遍。这篇笔记就顺着这个案例,把数据怎么读、协方差怎么估、权重怎么解、前沿怎么画,一步步拆开讲清楚,顺带把那些一跑就翻车的地方标出来。
2. 均值方差模型的数学骨架与 MATLAB 落地选型
2.1 目标函数与约束到底在优化什么
均值方差模型的核心是一个带约束的二次规划。设资产权重向量为 $w$,期望收益向量为 $\mu$,协方差矩阵为 $\Sigma$,模型写成:
$$\min_w \frac{1}{2} w^T \Sigma w \quad \text{s.t.} \quad \mu^T w = r,\ \sum_i w_i = 1,\ w_i \ge 0$$
第一行是目标:最小化组合方差,也就是 $w^T \Sigma w$。第二行是约束:组合期望收益等于目标收益 $r$,权重之和为 1,且不允许做空。这里有个容易被忽略的点——目标函数前面的 $\frac{1}{2}$ 只是为了求导后系数干净,不影响最优解位置。真正决定结果的是 $\mu$ 和 $\Sigma$ 的估计质量,而不是求解器本身。
MATLAB 里解这个问题的标准入口是quadprog,它属于 Optimization Toolbox。如果你的环境没装这个工具箱,quadprog会直接报未定义函数,这是新手最常见的第一个拦路虎。替代方案是用fmincon,但速度慢很多,或者自己写拉格朗日乘子法硬解,不推荐。
2.2 为什么用 quadprog 而不是 fmincon
quadprog专门针对二次型目标加线性约束,内部用的是内点法或信赖域反射法,收敛快且稳定。fmincon是通用非线性求解器,把二次问题丢给它属于杀鸡用牛刀,还容易因为初值选得不好掉进局部解。案例代码里如果用的是quadprog,说明作者走的是标准路线,值得信任。
调用格式大致是这样:
% H 是目标函数的二次项矩阵,f 是一次项,A/b 是不等式约束,Aeq/beq 是等式约束 % lb/ub 是权重上下界,options 控制求解器行为 [w_opt, fval, exitflag] = quadprog(H, f, A, b, Aeq, beq, lb, ub, [], options);对应到均值方差模型:H = Sigma(因为目标里是 $w^T \Sigma w$,二次项系数矩阵就是 $\Sigma$),f = zeros(n,1)(没有一次项),Aeq = [mu'; ones(1,n)],beq = [r; 1],lb = zeros(n,1),ub = []。exitflag必须检查,等于 1 才代表正常收敛,其他值都要回头看数据或约束。
2.3 数据读取与收益率矩阵的构造
案例包里的数据通常是.mat或.xlsx格式,存的是若干资产的收盘价或收益率。如果是价格数据,第一步要转成对数收益率或简单收益率。我一般用对数收益率,因为可加性更好,统计性质也更接近正态。
% 假设 data.mat 里有个变量 prices,是 T×N 的价格矩阵 load('data.mat'); % 载入数据 returns = diff(log(prices)); % 对数收益率,T-1 行 N 列 mu = mean(returns)'; % 期望收益,转成列向量 Sigma = cov(returns); % 协方差矩阵,N×Ndiff默认做一阶前向差分,log是自然对数。注意mean默认按列求均值,得到 1×N 行向量,转置成列向量才能和Sigma维度对齐。cov默认除以 T-1 做无偏估计,样本量小的时候这个选择会影响结果,但差异不大。
提示:如果数据里有 NaN,
mean和cov会直接返回 NaN,必须先做缺失值处理。常见做法是用前值填充或直接删掉含 NaN 的行。
3. 有效前沿的求解流程与参数调优
3.1 从单点最优到整条前沿的循环结构
有效前沿不是一条解析曲线,而是一系列目标收益 $r$ 对应的最优组合点连成的线。做法是:在最小期望收益和最大期望收益之间取一串 $r$,对每个 $r$ 解一次quadprog,把得到的组合波动和收益存下来。
n = size(Sigma, 1); r_min = min(mu); % 前沿左端点收益 r_max = max(mu); % 前沿右端点收益 N_points = 50; % 前沿采样点数 target_returns = linspace(r_min, r_max, N_points); frontier_risk = zeros(N_points, 1); frontier_weights = zeros(n, N_points); options = optimoptions('quadprog', 'Display', 'off', 'Algorithm', 'interior-point-convex'); for k = 1:N_points r = target_returns(k); Aeq = [mu'; ones(1, n)]; beq = [r; 1]; [w, fval, exitflag] = quadprog(Sigma, zeros(n,1), [], [], Aeq, beq, ... zeros(n,1), [], [], options); if exitflag == 1 frontier_risk(k) = sqrt(2 * fval); % 波动率 = 标准差 frontier_weights(:, k) = w; else frontier_risk(k) = NaN; % 标记失败点 end end这里fval返回的是 $\frac{1}{2} w^T \Sigma w$,所以组合方差是 $2 \times fval$,波动率再开根号。exitflag不等于 1 的点直接标 NaN,画图时会自动断开,不会污染曲线。Algorithm显式指定interior-point-convex,避免 MATLAB 在不同版本里默认算法变化导致结果漂移。
3.2 关键参数:采样点数、收益上下界与权重约束
N_points取 50 是经验值,太少前沿看起来是折线,太多计算时间线性增长。一般 30 到 100 之间都合理。r_min和r_max直接用min(mu)和max(mu)有个问题:当某个资产期望收益特别高但波动也极大时,前沿右端会拉得很长,图形比例失调。更稳的做法是取r_min = min(mu),r_max = max(mu) * 0.95或者用分位数截断。
权重约束lb = zeros(n,1)表示不允许做空。如果案例允许卖空,lb要设成-inf或具体负值。ub一般设[]表示无上界,但实务里常加单资产上限,比如ub = 0.3 * ones(n,1),防止组合过度集中。这个改动会显著改变前沿形状,值得单独跑一组对比。
| 参数 | 典型取值 | 影响 |
|---|---|---|
| N_points | 30~100 | 前沿平滑度与计算耗时 |
| lb | 0 或 -inf | 是否允许做空 |
| ub | [] 或 0.2~0.4 | 单资产集中度上限 |
| Algorithm | interior-point-convex | 求解稳定性 |
3.3 协方差估计的坑:样本协方差不是唯一选择
用cov(returns)得到的是样本协方差矩阵。当资产数量 N 接近甚至超过时间长度 T 时,这个矩阵会接近奇异,quadprog可能报「矩阵接近奇异」或者给出极端权重。这是均值方差模型最著名的血泪经验之一。
常见补救有三种。第一种是收缩估计,把样本协方差向对角矩阵或常数相关矩阵收缩,MATLAB 里可以手写 Ledoit-Wolf 收缩。第二种是用因子模型降维,先做主成分分析,保留前几个主成分再重构协方差。第三种最简单:加一点对角扰动,Sigma = Sigma + 1e-6 * eye(n),让矩阵正定。案例代码如果没做这一步,资产一多就容易翻车。
% 简单的对角加载,缓解协方差矩阵病态 Sigma = cov(returns); Sigma = Sigma + 1e-6 * eye(size(Sigma, 1));1e-6这个量级要视数据尺度而定。如果收益率是小数形式(比如 0.01 表示 1%),1e-6足够小不影响结果;如果收益率是百分数形式,可能要调到1e-4。判断标准是加载后cond(Sigma)明显下降且最优权重不再出现极端值。
4. 结果可视化与组合权重的解读
4.1 画出有效前沿和资本市场线
有效前沿画出来是波动率做横轴、期望收益做纵轴的曲线。再叠加一条从无风险利率出发的切线,就是资本市场线。MATLAB 里用plot和scatter组合即可。
figure; plot(frontier_risk, target_returns, 'b-', 'LineWidth', 1.5); hold on; scatter(sqrt(diag(Sigma)), mu, 60, 'r', 'filled'); % 单个资产 xlabel('组合波动率 \sigma'); ylabel('期望收益 \mu'); title('均值方差有效前沿'); grid on; legend('有效前沿', '单个资产', 'Location', 'best');sqrt(diag(Sigma))是每个资产自身的波动率,mu是各自期望收益,散点图能直观看出哪些资产在前沿内部、哪些靠近前沿。如果某个资产点落在前沿右侧很远,说明它被支配了,理性投资者不会单独持有它。
4.2 权重向量的可读性处理
frontier_weights是一个 N×N_points 的矩阵,直接看数字没意义。我一般挑几个关键点看:最小方差组合(前沿最左端)、最大夏普组合、以及目标收益等于某个基准的点。最大夏普组合需要额外算夏普比率再找最大值。
rf = 0.02; % 无风险利率,年化 sharpe = (target_returns - rf) ./ frontier_risk; [~, idx_max_sharpe] = max(sharpe); w_max_sharpe = frontier_weights(:, idx_max_sharpe); disp(array2table(w_max_sharpe, 'RowNames', asset_names, 'VariableNames', {'Weight'}));asset_names是资产名称的 cell 数组,从数据文件里读出来。用array2table输出比裸矩阵可读性好得多。注意sharpe计算里如果frontier_risk有 NaN,max会忽略 NaN,但最好先过滤掉失败点。
注意:无风险利率
rf要和收益率的时间频率匹配。如果收益率是日频,rf也要用日频,不能直接填年化的 2%。
5. 避坑与排查:均值方差模型最常见的五类翻车
5.1 quadprog 报「矩阵接近奇异或缩放不良」
现象:运行到某个目标收益时,MATLAB 警告Matrix is close to singular or badly scaled,exitflag返回负值。
原因:协方差矩阵条件数过大,通常因为资产数量多、样本时间短,或者某些资产高度相关。
解决:先做对角加载Sigma = Sigma + 1e-6 * eye(n),再检查cond(Sigma)。如果条件数仍大于 1e10,考虑减少资产数量或改用收缩估计。另外把options里的TolFun和TolX适当放宽也有帮助,但治标不治本。
5.2 有效前沿出现「折返」或负波动率
现象:画出来的前沿曲线在某些段往回折,或者frontier_risk出现负值。
原因:fval理论上非负,但如果求解失败返回了垃圾值,sqrt(2*fval)可能得到虚数或负数。另外exitflag没检查就全部存下来,失败点混进了曲线。
解决:循环里严格判断exitflag == 1才存结果,否则存 NaN。画图前用frontier_risk(frontier_risk < 0) = NaN兜底。负波动率在数学上不可能,出现就是求解器没收敛。
5.3 权重全是零或集中在一个资产上
现象:最优权重向量里大部分是 0,或者某个资产权重接近 1。
原因:约束设置有问题,比如Aeq维度不对导致等式约束没生效;或者某个资产期望收益远高于其他资产,在无上界约束下求解器把所有权重都给了它。
解决:先检查size(Aeq)是否为 2×N,beq是否为 2×1。然后加单资产上限ub = 0.4 * ones(n,1),强制分散。如果加了上限还是集中,说明数据本身有问题,回头看收益率序列是不是有异常值。
5.4 数据频率不一致导致结果失真
现象:组合波动率算出来特别小或特别大,和直觉不符。
原因:有的资产是日频数据,有的是周频或月频,混在一起算协方差,时间尺度对不上。
解决:统一到同一频率。日频转周频可以用resample或手动按周聚合。检查方法很简单:size(returns, 1)对所有资产应该一致,不一致就说明频率或缺失值处理有问题。
5.5 前沿曲线和单个资产散点对不上
现象:散点图里某个资产明明在有效前沿左侧,但前沿曲线却从它右边穿过。
原因:散点用的是单个资产的mu和sqrt(diag(Sigma)),而前沿是组合优化结果。单个资产本来就不一定在有效前沿上,落在右侧是正常的。如果落在左侧,说明前沿求解有问题,大概率是r_min设得比该资产收益还高,导致前沿左端被截断。
解决:把r_min设成min(mu)或更低,确保前沿覆盖所有单个资产收益范围。另外确认frontier_risk和target_returns长度一致,画图时没有错位。
6. 从案例代码到可复用函数:封装与敏感性验证
把上面这套流程封装成一个函数,是让案例真正变成自己工具的关键一步。我一般会写成mean_variance_frontier(returns, n_points, lb, ub, rf),输入收益率矩阵和参数,输出前沿风险、收益、权重和最大夏普组合。这样换一组数据只要改输入,不用动逻辑。
function [risk, ret, weights, w_sharpe] = mean_variance_frontier(returns, n_points, lb, ub, rf) % returns: T×N 收益率矩阵 % n_points: 前沿采样点数 % lb, ub: 权重下界和上界 % rf: 无风险利率(与收益率同频率) mu = mean(returns)'; Sigma = cov(returns); Sigma = Sigma + 1e-6 * eye(size(Sigma, 1)); % 对角加载 n = size(Sigma, 1); ret = linspace(min(mu), max(mu), n_points)'; risk = nan(n_points, 1); weights = nan(n, n_points); options = optimoptions('quadprog', 'Display', 'off', ... 'Algorithm', 'interior-point-convex'); for k = 1:n_points Aeq = [mu'; ones(1, n)]; beq = [ret(k); 1]; [w, fval, exitflag] = quadprog(Sigma, zeros(n,1), [], [], Aeq, beq, ... lb, ub, [], options); if exitflag == 1 risk(k) = sqrt(2 * fval); weights(:, k) = w; end end sharpe = (ret - rf) ./ risk; [~, idx] = max(sharpe); w_sharpe = weights(:, idx); end封装好之后,做敏感性验证就方便了。换不同的n_points看前沿是否稳定,换lb从 0 到 -0.2 看做空对前沿的改善,换ub从 1 到 0.3 看分散化约束的代价。这些对比跑一遍,比读十篇理论文章都管用。
我自己的习惯是每次拿到新数据,先跑三组:无约束、仅做多、做多加上限。三组前沿叠在一张图上,如果差异巨大,说明数据里存在极端资产,需要先做异常值处理再谈优化。均值方差模型对输入极其敏感,这不是它的缺陷,而是它的本性——它忠实地把你对收益和风险的估计翻译成权重,估计错了,结果一定错。希望帮到你。
本文还有配套的精品资源,点击获取