用LINGO和MATLAB搞定投资组合优化:从数学建模到代码实现
用LINGO和MATLAB搞定投资组合优化:从数学建模到代码实现
最近在辅导几个学生准备数学建模竞赛,发现很多同学在面对投资组合优化这类经典题目时,常常卡在从理论模型到代码实现的“最后一公里”。纸上推公式头头是道,一打开软件就手足无措。这让我想起自己早年参赛的经历,当时也是对着LINGO和MATLAB的界面发愁,不知道如何把那些优美的数学符号变成计算机能理解的指令。
投资组合优化,说白了就是在收益和风险之间找平衡的艺术。给你一笔钱,面前摆着股票、债券等一堆资产,每个都有预期的回报率和潜在的风险。你的任务就是设计一个购买方案,让赚的钱尽可能多,同时把亏钱的可能性压到最低。这听起来像是基金经理的日常工作,但它本质上是一个标准的数学优化问题,非常适合用建模竞赛来考察学生的综合能力。
这篇文章,我想从一个“过来人”和实际使用者的角度,聊聊怎么用LINGO和MATLAB这两款风格迥异的工具,把这道经典赛题从题目描述变成可运行的代码。我不会照搬任何现成的论文或教材答案,而是结合我这些年带比赛和做项目的实战经验,拆解其中的关键思路、易错点,以及两种工具各自的“脾气”。无论你是正在备赛的学生,还是对量化投资初步感兴趣的爱好者,希望这些接地气的分享能帮你少走些弯路。
1. 问题拆解:把一道应用题变成一个数学模型
拿到题目,第一步不是急着打开软件,而是拿起笔,把问题翻译成数学语言。很多初学者失败就失败在跳过了这一步,直接去网上找代码,结果发现根本对不上。
1998年这道A题,条件给得比较细,我们一条条来梳理。
核心要素:
- 资金总量:有一笔总额为
M的资金。在具体算例中,M=10000元。 - 资产选项:有
n种风险资产(S1, S2, ..., Sn)和1种无风险资产(银行存款)。每种风险资产Si有三个关键参数:ri:平均收益率(%)qi:风险损失率(%)pi:交易费率(%)ui:交易费计算阈值(元)。这是本题一个容易忽略的细节:当购买金额不超过ui时,交易费按购买ui计算;超过则按实际购买金额计算。不买则不收费。
- 无风险资产:银行存款,收益率
r0=5%,无风险 (q0=0),无交易费 (p0=0)。 - 决策变量:我们最终要决定的是对每种资产(包括银行存款)的投资金额
xi(i=0,1,...,n,其中i=0代表银行存款)。
目标与约束: 题目要求“净收益尽可能大,总体风险尽可能小”。这是一个典型的双目标优化问题。在数学上,处理双目标问题常用两种方法:
- 效用函数法(加权求和法):引入一个权重系数
w(0 ≤ w ≤ 1),将两个目标合并为一个。w代表对风险的厌恶程度,w越大,越厌恶风险。- 目标函数可以设为:
Minimize Z = w * 总风险 - (1-w) * 总净收益 - 这样,通过调整
w,我们就能得到一系列权衡了收益和风险的不同解,也就是所谓的“有效前沿”。
- 目标函数可以设为:
- 约束法:固定一个目标(如风险不超过某个值),优化另一个目标(最大化收益),或者反过来。
本题的官方参考解答和大多数教材都采用了第一种方法,因为它能系统性地描绘出整个权衡曲线。
接下来,我们需要用数学公式定义“总净收益”和“总风险”。
-
净收益计算:收益减去交易成本。
- 对于资产
i,其投资收益为ri * xi。 - 其交易成本
c(xi)是一个分段函数:如果 xi = 0: c(xi) = 0 如果 0 < xi ≤ ui: c(xi) = pi * ui 如果 xi > ui: c(xi) = pi * xi - 因此,总净收益
V = Σ (ri * xi - c(xi)),求和范围包括所有资产(含银行存款,其r0=5%,c(x0)=0)。
- 对于资产
-
总体风险定义:题目明确说“总体风险可用所投资的Si中最大的一个风险来度量”。注意,这里“风险”指的是
qi * xi,即该资产可能造成的最大损失金额。- 所以,总风险
Q = max{ qi * xi },其中i取所有风险资产(银行存款风险为0,不参与最大值的比较)。
- 所以,总风险
-
资金约束:总投资额(本金+交易费)不能超过总资金
M。Σ (xi + c(xi)) = M。注意这里是等号,意味着资金必须全部投入使用(可以存银行)。
注意:这个分段交易费函数
c(xi)是建模和编程的第一个难点。它既不是线性的,也不是连续的(在xi=0处)。直接处理比较麻烦,一个常用的技巧是利用“大M法”引入0-1变量将其线性化,或者利用优化软件的非线性求解能力。
把以上所有文字描述汇总,我们就得到了一个完整的数学规划模型。对于 n=4 的算例,我们可以把具体数字代入。这个模型就是我们后续用LINGO或MATLAB求解的蓝图。
2. LINGO实战:专为优化而生的“直译”工具
如果你追求的是快速、直观地把数学模型“告诉”计算机,并得到答案,LINGO几乎是首选。它的语法非常贴近数学表达,特别适合处理线性、非线性和整数规划问题。
2.1 LINGO建模思想:集合与变量声明
LINGO的核心思想是基于集合的建模。我们不需要为每个资产单独写一行代码,而是先定义“资产”这个集合,然后在这个集合上定义属性。
对于我们的问题,可以这样定义集合:
sets:
asset /1..5/: r, q, p, u, x, c;
! 定义了名为asset的集合,有5个成员(1:银行存款,2-5:四种风险资产)。
! 每个成员都有收益率r、风险率q、交易费率p、阈值u、投资额x、交易费c这些属性。
endsets
定义好集合和属性后,我们就可以用简洁的循环语句来描述目标函数和约束条件,这比在MATLAB里写循环矩阵优雅得多。
2.2 关键代码段解析与易错点
我们聚焦几个最关键的、容易出错的建模部分。
1. 分段交易费 c(i) 的实现:
这是模型非线性的来源。在LINGO中,我们可以利用 @if 函数和 @smax 函数来直接表达。
@for(asset(i):
c(i) = @if(x(i) #eq# 0, 0, @smax(p(i)*u(i), p(i)*x(i)));
);
这行代码的意思是:对于每种资产 i,如果投资额 x(i) 等于0,则交易费为0;否则,交易费取 p(i)*u(i) 和 p(i)*x(i) 中的较大值。这完美对应了题目中的分段规则。
2. 总体风险 Q 的定义:
风险是最大损失,直接用 @max 函数。
Q = @max(asset(i) | i #gt# 1: q(i) * x(i));
! i#gt#1 表示i>1,即排除银行存款(i=1),因为其q(1)=0。
3. 双目标加权处理:
我们引入权重 w,构建单目标函数。
min = w * Q - (1-w) * V;
! 目标是最小化这个综合指标。w越大,对风险Q的惩罚越大,优化结果会更倾向于低风险。
! V是总净收益,前面需要计算:V = @sum(asset(i): r(i)*x(i) - c(i));
4. 资金约束:
@sum(asset(i): x(i) + c(i)) = M;
! 总投资额(本金+交易费)等于总资金M。
把所有这些片段组合起来,就构成了一个完整的LINGO模型。为了画出有效前沿(即不同 w 值下的最优解集),我们通常会把 w 作为一个参数,写一个循环,让LINGO自动求解一系列 w 取值下的问题,并记录结果。
2.3 完整LINGO脚本结构与输出处理
一个结构清晰的LINGO脚本可能如下所示:
model:
sets:
asset /1..5/: r, q, p, u, x, c;
scenario /1..11/: w_val, risk, profit; ! 定义11个场景,对应w从0到1,步长0.1
endsets
data:
! 输入数据,注意百分比转换为小数
r = 0.05, 0.28, 0.21, 0.23, 0.25;
q = 0, 0.025, 0.015, 0.055, 0.026;
p = 0, 0.01, 0.02, 0.045, 0.065;
u = 0, 103, 198, 52, 40;
M = 10000;
enddata
calc:
@for(scenario(s):
w = (s-1)*0.1; ! 设置当前场景的权重w
@solve(); ! 求解当前优化问题
! 记录结果
w_val(s) = w;
risk(s) = Q;
profit(s) = V;
);
! 将结果输出到文件或窗口
@write("权重w, 风险, 净收益\n");
@for(scenario(s): @write(@format(w_val(s), "#8.3f"), " ", @format(risk(s), "#12.2f"), " ", @format(profit(s), "#12.2f"), "\n"));
endcalc
! 以下是模型部分(目标函数和约束)
[OBJ] min = w*Q - (1-w)*V;
@for(asset(i): c(i) = @if(x(i) #eq# 0, 0, @smax(p(i)*u(i), p(i)*x(i))));
Q = @max(asset(i) | i #gt# 1: q(i)*x(i));
V = @sum(asset(i): r(i)*x(i) - c(i));
@sum(asset(i): x(i) + c(i)) = M;
end
运行这个模型,LINGO会输出11行数据,每行对应一个 w 下的最小风险和最大收益的组合。把这些点 (风险, 收益) 画在图上,就是有效前沿曲线。LINGO本身绘图功能不强,通常我们会把数据导出到Excel,再用MATLAB或Python绘图。
提示:在调试复杂模型时,可以先用
@pause语句暂停,查看中间变量值;或者使用@divert将求解日志输出到文本文件,便于排查错误。
3. MATLAB实现:灵活强大的“编程式”求解
与LINGO的“声明式”风格不同,MATLAB要求我们以编程的方式定义目标函数和约束。这给了我们更大的灵活性,但也对编程能力提出了更高要求。MATLAB的优化工具箱 fmincon 是解决此类有约束非线性规划问题的利器。
3.1 问题转化:适应 fmincon 的格式
fmincon 求解的问题是:
min f(x)
s.t. A*x ≤ b, Aeq*x = beq, lb ≤ x ≤ ub, c(x) ≤ 0, ceq(x) = 0
我们需要把自己的模型“塞”进这个格式。
- 决策变量
x:一个列向量[x0; x1; x2; x3; x4],分别代表对银行存款和4种风险资产的投资额。 - 目标函数
f(x):就是w * Q - (1-w) * V。我们需要写一个函数myObjective(x)来计算它。 - 线性约束:本题只有资金总额一个等式约束,但它非线性(因为包含交易费
c(x)),所以不能放在Aeq, beq中。 - 非线性约束:我们需要写一个函数
myConstraint(x),返回资金约束Σ(xi + c(xi)) - M = 0作为等式约束ceq。 - 边界约束
lb:投资额不能为负,所以lb = zeros(5,1)。没有上限,ub设为空[]。
3.2 核心函数编写与坑点指南
在MATLAB中,我们通常需要创建三个独立的函数文件:主脚本、目标函数文件、非线性约束函数文件。交易费计算函数可以单独写,也可以嵌套在内。
主脚本 (main.m) 框架:
clear; clc;
global w asset M % 声明全局变量,方便在各个函数间传递
% 1. 输入数据
asset = [0.05, 0, 0, 0; % 银行存款
0.28, 0.025, 0.01, 103;
0.21, 0.015, 0.02, 198;
0.23, 0.055, 0.045,52;
0.25, 0.026, 0.065,40];
M = 10000;
num_assets = size(asset, 1);
% 2. 定义优化选项和问题结构
options = optimoptions('fmincon', 'Display', 'iter', 'Algorithm', 'sqp');
% 使用‘sqp’算法通常对这类问题效果较好
% 3. 循环求解不同w下的问题
results = zeros(11, 3); % 存储 [w, 风险, 收益]
x_optimal = zeros(num_assets, 11); % 存储每个w下的最优投资方案
for i = 1:11
w = (i-1) * 0.1;
% 定义优化问题
problem.objective = @myObjective; % 目标函数句柄
problem.x0 = ones(num_assets, 1) * (M / num_assets); % 初始解:资金平均分配
problem.Aineq = []; problem.bineq = [];
problem.Aeq = []; problem.beq = [];
problem.lb = zeros(num_assets, 1);
problem.ub = [];
problem.nonlcon = @myConstraint; % 非线性约束函数句柄
problem.solver = 'fmincon';
problem.options = options;
% 求解
[x, fval] = fmincon(problem);
% 计算当前解的实际风险和收益
[current_risk, current_profit] = calculateMetrics(x);
% 记录结果
results(i, :) = [w, current_risk, current_profit];
x_optimal(:, i) = x;
end
% 4. 输出结果并绘图
disp('权重w, 风险, 净收益');
disp(results);
plot(results(:,2), results(:,3), 'o-');
xlabel('总体风险 Q (元)'); ylabel('净收益 V (元)');
title('投资组合有效前沿');
grid on;
目标函数文件 (myObjective.m):
function f = myObjective(x)
global w asset
% 计算交易费
c = calcTransactionFee(x);
% 计算总风险(最大损失)
risk = max(asset(2:end, 2) .* x(2:end)); % 排除银行存款
% 计算总净收益
profit = sum(asset(:,1) .* x - c);
% 加权目标函数
f = w * risk - (1-w) * profit;
end
交易费计算函数 (calcTransactionFee.m): 这是最容易出错的地方。分段函数的处理需要非常小心。
function c = calcTransactionFee(x)
global asset
num = length(x);
c = zeros(num, 1);
for i = 1:num
if x(i) == 0
c(i) = 0;
elseif x(i) > 0 && x(i) <= asset(i, 4) % 0 < xi <= ui
c(i) = asset(i, 3) * asset(i, 4);
else % xi > ui
c(i) = asset(i, 3) * x(i);
end
end
end
注意:在浮点数计算中,严格判断
x(i) == 0可能不稳定。有时一个极小的数(如1e-10)在优化过程中会被视为0。一个更稳健的做法是设置一个容差tol = 1e-6,判断x(i) < tol。这也是很多参考代码中设置x(i) <= 0.06之类奇怪阈值的原因——那可能是在特定迭代下为了绕过数值问题而采取的临时措施,并非普适解法。更好的方法是重构问题,避免这种不连续的分段判断。
非线性约束函数 (myConstraint.m):
function [c, ceq] = myConstraint(x)
global M
% 计算交易费
fee = calcTransactionFee(x);
% 没有不等式约束
c = [];
% 等式约束:总投资额(本金+交易费)等于M
ceq = sum(x + fee) - M;
end
3.3 调试技巧与结果分析
运行上述MATLAB代码,你可能会遇到一些问题:
- 初始点敏感:
fmincon对初始猜测x0比较敏感。如果初始点设得不好,可能会收敛到局部最优解,甚至不收敛。可以尝试多个不同的初始点(如随机生成),或者使用MultiStart、GlobalSearch等全局优化算法来寻找更好的解。 - 数值不稳定:分段交易费函数在
x(i)=0和x(i)=ui处不可导,这会给基于梯度的优化器带来困难。‘sqp’或‘interior-point’算法通常比‘active-set’更能处理这类问题。 - 结果验证:将MATLAB得到的结果与LINGO的结果进行对比。如果两者在相同
w下得到的风险和收益值非常接近,那么可以相互印证。如果差异较大,需要仔细检查模型的一致性,特别是交易费的计算逻辑。
当程序成功运行后,你会得到一系列 (风险, 收益) 点。将它们绘制成散点图或连线图,就能清晰地看到那条经典的、向右上方倾斜的“有效前沿”曲线。曲线上每一点都代表在给定风险水平下能获得的最大收益,或者在给定收益目标下能承受的最小风险。
4. 工具对比与场景选择:LINGO vs MATLAB
通过上面的实践,你应该能感受到LINGO和MATLAB在解决同一问题时的不同哲学。下面这张表总结了它们的主要区别,帮你决定在什么情况下用哪个。
| 特性维度 | LINGO | MATLAB (with Optimization Toolbox) |
|---|---|---|
| 建模范式 | 声明式建模。直接描述问题是什么(目标、约束),接近数学语言。 | 命令式编程。需要编写函数来定义目标、约束,告诉计算机怎么做。 |
| 学习曲线 | 相对平缓。掌握基本集合、函数语法后,可以快速建立优化模型。 | 相对陡峭。需要熟悉MATLAB编程、函数句柄、优化工具箱API。 |
| 模型表达力 | 对线性、非线性、整数规划的原生支持很好,语法简洁。处理复杂分段函数、逻辑约束相对方便。 | 极其灵活。任何能用代码描述的函数和约束都能处理,适合高度定制化、算法嵌入式的复杂模型。 |
| 求解能力 | 内置强大的求解器,对常见优化问题开箱即用,用户无需关心算法细节。 | 提供多种算法选择(如 interior-point, sqp, active-set),需要用户根据问题特性进行选择和调参。 |
| 调试与可视化 | 调试功能相对简单,主要靠查看变量窗口和输出日志。可视化能力弱。 | 调试工具强大(断点、变量查看器等)。可视化能力极强,可轻松绘制有效前沿、收敛过程等图形。 |
| 结果分析与扩展 | 擅长快速求解并给出结果。但进行后续的灵敏度分析、批量处理、集成到更大系统时不如MATLAB方便。 | 易于将优化模块作为一部分,集成到数据分析、机器学习、仿真等更大的工作流中。结果处理和分析非常方便。 |
| 适用场景 | 数学建模竞赛、快速原型验证、教学演示。当你需要快速验证一个优化模型的想法,并得到可靠解时,LINGO是高效的选择。 | 学术研究、算法开发、复杂系统仿真、产品级应用。当你的问题需要定制化算法、与其他分析工具深度集成,或者需要复杂的后处理时,MATLAB更胜一筹。 |
我的个人经验是:
- 在数学建模竞赛的48小时内,如果问题核心是一个清晰的优化模型,我会首选LINGO。它能让我在几分钟内把思路变成可运行的代码,把宝贵的时间留给结果分析和论文写作。它的求解日志也便于我们理解模型的规模和解的状态。
- 如果竞赛问题需要我自己设计优化算法(比如遗传算法、模拟退火),或者优化只是整个模型的一小部分,前后有大量的数据预处理和结果可视化,那么MATLAB 的综合优势就体现出来了。
- 在日常研究和工作中,面对更复杂、更动态的优化问题(比如参数随时间变化、需要与仿真耦合),我几乎总是使用MATLAB 或 Python,因为它们的生态系统和灵活性无可替代。
最后,无论选择哪个工具,深刻理解问题本身的数学本质永远是第一位的。工具只是帮你计算的“笔”,而模型和思路才是你要表达的“思想”。在真正动手编程前,花时间在草稿纸上清晰地列出所有变量、目标、约束,定义好每一个函数,这个好习惯的价值远超任何编程技巧。当你对模型了如指掌时,用LINGO翻译或者用MATLAB实现,都只是水到渠成的事情。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)