你手里有一座 10 MW / 40 MWh 的电化学储能电站,如果只在现货电能量市场里“低充高放”,一年下来收益能有多少?如果同时把一部分容量留给调频辅助服务市场,情况又会变成什么样?这两个问题,背后其实是一个典型的储能双层交易决策问题。我最近用 Matlab 把这套“上层定策略、下层出价格”的模型完整跑通了一遍,顺手把建模思路、代码结构和踩过的坑都整理了出来。
这篇文章我按“背景逻辑 -> 双层模型设计 -> Matlab 实现 -> 算例结果 -> 调试经验”的顺序来讲。适合两类人看:一是刚接触电力市场优化方向的研究生,想快速理解双层优化到底在干什么;二是已经开始做储能经济性分析、想通过代码把多市场联合决策真正落地的工程师。读完你至少能明白一件事:储能参与市场,不是拍脑袋决定“多充一点还是少放一点”,而是要在系统出清规则的约束下,把能量市场和调频市场的收益一次性算清楚。
1. 储能参与电力市场的核心逻辑
1.1 现货电能量市场与调频辅助服务市场分别解决什么问题
现货电能量市场,通俗点说就是电力系统里的“大宗批发市场”。发电厂报量报价,用户报需求,系统在一个统一规则下算出每一时段的出清电价,这个价格通常每 15 分钟或者每小时更新一次。储能在这类市场里的玩法大家应该很熟悉:电价低的时候充电,电价高的时候放电,赚的就是峰谷价差,业内管这个叫能量套利。
调频辅助服务市场解决的是另一个问题。电力系统要求频率稳定在 50 Hz 附近,一旦功率不平衡,频率就会漂移,这时候需要一些具备快速调节能力的资源在几秒到几十秒内顶上容量或增加出力。调频市场就是为这种“分钟级甚至秒级的快速响应”付费。储能机组天然适合这个赛道——动作快、精度高、可以双向调节,很多地方做调频的收益甚至比纯套利还要高。
这里顺手把概念理顺:现货电能量市场买的是“量”,按 kWh 结算;调频市场付的是“容量可用性”和“实际调节里程”,按 MW 和调节幅度结算。两者的物理基础是同一套储能设备,这就给决策带来了耦合。
1.2 为什么只参与现货电能量市场远远不够
储能的前期投资非常大,一座 10 MW / 4 h 的磷酸铁锂电站,系统成本通常在 3000 万到 5000 万元。如果只靠峰谷套利,一天充放两次,假设峰谷价差在 0.6~0.8 元/kWh,一天的现货收益也就在几万元上下,回本周期非常长。
调频市场给了一个新的空间。储能的优势是响应速度快,可以在充电和放电之间来回切换,这跟常规火电比是碾压性的。很多实际的调频项目中,储能资源拿到的容量价格和里程价格都比火电高。但问题也随之而来:储能的总功率和总容量是固定的,如果一部分功率拿去调频,就意味着同一时间不能再用这部分功率去现货市场充放电。
这就是我说的“容量冲突”。所以储能参与市场的优化不能简单当成“现货收益 + 调频收益”两个独立项相加,必须放到一个联合决策框架里,搞清楚每一时刻每一兆瓦功率和每一兆瓦时容量到底应该分给谁。这个角度恰恰是很多人一开始容易忽略的。
1.3 双层优化到底在描述什么博弈关系
常规做储能收益评估,很多人的做法是直接给一组预测电价,然后做单层优化,储能只是一个价格接受者——市场价格是多少就按多少结算,储能决策不影响价格。这在储能规模很小的时候基本成立,比如一台 1 MW 的分布式储能,对整个省级市场来说无足轻重。
但如果储能规模比较大,或者一个聚合商手上掌握了几十上百兆瓦的资源呢?这时候储能申报的充放电曲线会实实在在改变系统出力,进而改变节点边际电价和调频出清价格。储能和市场价格是互相影响的关系,再用“固定电价”做单层优化就会产生偏差。
双层优化的思路正好用来描述这种博弈关系。上层是储能运营商的收益最优化,决定每个时段的充放电功率、调频容量报多少、SOC 怎么滚动;下层是市场运营机构的出清模型,在给定储能申报和系统约束的前提下,以系统总购电成本最小为目标,算出市场出清价格。储能的目标函数里用到的电价,就是下层出清产生的对偶变量。这样“上层决策影响下层出清,下层出清反馈给上层收益”的闭环,就是双层问题的核心结构。
2. 双层交易决策模型的建模过程
2.1 上层模型:储能运营商的收益最大化
上层模型相对好理解,目标函数是储能参与两类市场的综合收益最大化。我建模时用的是典型的一天 24 个时段,也可以扩展成 96 个时段,只是变量和计算量要成倍增加。
目标函数包含这么几项:现货电能量市场套利收益、调频容量收益、调频里程收益,再减去储能充放电循环带来的衰减成本。现货收益本质上是放电收入减去充电成本;调频容量收益是按照申报的上调频容量和下调频容量的容量价格结算;里程收益则和实际调频动作幅度有关,通常用一个里程系数乘以容量收益。
储能运行的物理约束必须写完整,包括:SOC 的离散递推方程,也就是上一时段剩余电量加上本时段充入电量、减去放出的电量再扣掉损耗;充放电功率上限制约;同一个时刻充电功率、放电功率、上调频容量、下调频容量加在一起不能超过储能额定功率;SOC 上下限一般在 10% 到 90% 之间,不能满充满放。
这里有一个比较容易漏的地方:调频容量和充放电功率不是完全独立的关系。如果你的储能正在满功率放电,那它基本没有能力再向上调频,因为已经放到了上限。所以我们把“同一时刻所有功率需求之和不超过额定功率”写在约束里。这个约束做不对,后面很多结果都是畸形的。
2.2 下层模型:电能量-调频联合出清
下层模型模拟市场运营机构做的事情:在收到所有市场主体的申报曲线后,以系统总成本最小为目标,优化各机组出力和系统需要的调频容量额度。
下层模型我做了必要简化,但保持了市场出清的核心机制。目标函数是最小化所有机组在 24 个时段的发电成本之和;约束条件包括系统功率平衡约束、常规机组出力上下限、机组爬坡约束,以及系统调频容量需求约束。下层优化完成后,会得到一系列对偶变量,其中最重要的两个就是现货电能量市场的节点电价 λ_t 和调频容量出清价格 π_t。
在双层模型里,来自下层的 λ_t 和 π_t 会直接出现在上层目标函数中。如果直接写成双层,YALMIP 这类工具是没法直接求解的,必须想办法把它改写成单层优化问题。换句话说,下层模型不是独立求解一次就完事,而是要把它的最优性条件嵌入到上层模型里,让上层在决策时就“预测”到市场出清的结果。
2.3 从双层到单层:KKT 条件、强对偶与大 M 法
这是整个建模过程中最核心、也最容易做错的一步。怎么把双层问题变成一个 Matlab 能吃进去的单层优化?
标准做法是:因为下层是线性规划,它的 KKT 条件既是必要条件也是充分条件,所以可以用下层模型的 KKT 条件替换下层模型。KKT 条件包括四部分:一是拉格朗日函数对下层变量的偏导等于零,也就是“平稳性条件”;二是下层所有原始约束,保证解是可行解;三是所有对偶变量的非负限制;四是互补松弛条件,即“若约束不被激活,则对应对偶变量为零”。
光有前三个条件还不够。互补松弛条件本质上是一个“两个非负变量相乘等于零”的非线性约束,YALMIP 和 Gurobi 不能直接处理。我常用的办法是大 M 法,引入一组 0-1 整数变量,把互补松弛条件改写成带 0-1 变量和大 M 常数的线性不等式。
比如一个简单的互补条件 μ·(G_max - G) = 0,可以等价写成 0 ≤ μ ≤ M·z,0 ≤ G_max - G ≤ M·(1 - z)。这里的 z 就是一个二进制变量。引入 0-1 变量后,整个模型就从线性规划变成了混合整数线性规划,只要大 M 取值合理,用 Gurobi 或 CPLEX 都能求解。
这里要特别提醒:不同的下层模型结构,大 M 的取值范围必须单独检查。取值太大,数值稳定性会很差,求解器对偶变量报告各种奇怪结果;取值太小,有可能把真正的最优解卡在可行域外面。后面我会专门讲怎么调这个大 M。
3. Matlab 代码实现与关键模块解析
3.1 运行环境与工具箱准备
我在本地用的是 Matlab R2022b,求解器配的 Gurobi,建模层用的是 YALMIP。YALMIP 最大的好处是可以把优化变量、约束条件和目标函数写成非常接近数学论文的形式,不用自己手动去拼大规模矩阵,这点在做双层优化研究时特别省心。如果没有 Gurobi,也可以尝试用 Cplex 或者开源的 Cbc,但大 M 加整数变量的混合整数规划问题,Gurobi 的求解效率和鲁棒性明显好一些。
装配过程不复杂,但有两件事必须确认。第一,YALMIP 的路径要在 Matlab 里 addpath 成功;第二,Gurobi 能正常被 Matlab 调用,最稳妥的验证方法是在命令行里输入yalmiptest,如果看到 Gurobi 那一栏显示 OK,就说明环境没问题。
3.2 数据准备与参数设置
建模之前先把基础参数孤立出来,放在脚本最前面,方便统一调整。电池参数、系统参数、市场参数各一组。电池这块包括额定功率、额定容量、初始 SOC、充放电效率、SOC 上下限;系统参数包括机组个数、各机组边际成本、出力上下限;市场参数包括调频容量需求、里程系数、现货市场预测电价曲线。
我习惯把每个时段的现货电价先单独画一遍,确认峰谷分布是不是合理。调频容量价格在有些算例里也是外生的,可以直接当成常数矩阵放进下层模型。下面这段代码是基础参数段的典型写法:
%% 基础参数 T = 24; % 时段数 P_max = 10; % 储能额定功率 MW E_max = 40; % 储能额定容量 MWh soc_min = 0.1 * E_max; soc_max = 0.9 * E_max; soc0 = 0.2 * E_max; % 初始SOC eta = 0.95; % 充放电效率 G_max = 60; % 火电机组出力上限 MW G_min = 10; % 火电机组出力下限 MW c_g = 300; % 机组边际成本 元/MWh3.3 核心代码结构与双层转化写法
真正建模时,我的代码分三大块:上层决策变量定义、下层出清约束和 KKT 互补条件、目标函数与求解调用。为了可读性,我通常会写成多个脚本文件,而不是一个超长脚本。主脚本main.m负责调用数据文件和建模求解,子函数build_model.m负责返回约束和目标。
上层变量声明部分长这样:
%% 上层变量 p_ch = sdpvar(T,1); % 每时段充电功率 p_dis = sdpvar(T,1); % 每时段放电功率 r_up = sdpvar(T,1); % 上调频容量 r_dn = sdpvar(T,1); % 下调频容量 soc = sdpvar(T+1,1); % 各时段SOC下层出清价格通过 KKT 条件被当成变量引进来,λ_t 就是现货能量市场出清价格,π_t 是调频容量价格。在模型里它们不是固定参数,而是在 KKT 条件约束下求解出来的影子价格,这是双层优化区别于“先用单个市场出清算价格、再固定价格做优化”的关键点。
核心约束会包含下面这些逻辑:功率平衡约束、机组上下限约束、调频容量需求约束、储能功率互斥约束、SOC 递推约束,以及 KKT 转换后引入的互补松弛条件。完整的代码结构大体会像这样:
%% 约束集合 constraints = []; % 系统功率平衡 constraints = [constraints, G1 + G2 + p_dis - p_ch == D]; % 储能功率约束 constraints = [constraints, 0 <= p_ch <= P_max]; constraints = [constraints, 0 <= p_dis <= P_max]; constraints = [constraints, p_ch + p_dis + r_up + r_dn <= P_max]; % SOC递推 constraints = [constraints, soc(2:T+1) == soc(1:T) ... + p_ch * eta - p_dis / eta]; constraints = [constraints, soc_min <= soc <= soc_max];目标函数部分则把现货收益、调频收益和衰减成本组合在一起:
%% 目标函数 revenue_energy = sum(lambda .* (p_dis - p_ch)); revenue_reg = sum(pi_cap .* (r_up + r_dn)); cost_degrad = sum(deg_coef .* (p_ch + p_dis)); objective = revenue_energy + revenue_reg - cost_degrad;求解调用用 YALMIP 的 optimize 函数:
%% 求解 ops = sdpsettings('solver', 'gurobi', 'verbose', 1, ... 'gurobi.TimeLimit', 300); result = optimize(constraints, -objective, ops);这里用-objective是因为 YALMIP optimize 统一是求最小化,而我们要的是收益最大化。求解完第一件事不是去看结果,而是检查result.problem是否等于 0。如果不为 0,别急着画图,先把问题定位清楚。
3.4 结果提取与可视化
求解完成后,用value()函数把变量提取出来。我习惯一次性输出一个结构体,把充放电功率、调频容量、SOC、市场出清价格全部装进去,方便后面做算例统计分析。
画图方面,我一般会画三张图:第一张是现货电价和储能充放电功率的组合图,可以直观看出储能是不是做到了“低价充电、高价放电”;第二张是 SOC 时序变化曲线,确认没有越界;第三张是调频容量和充放电功率的堆叠图,看同一时刻容量分配是否合理。每次改完参数,这三张图是我必看的。
4. 算例分析:不同策略下的收益对比
4.1 算例配置与对比方案
拿一个典型算例来说明。系统里有两台常规火电机组,一台边际成本 280 元/MWh,最大出力 60 MW;另一台边际成本 420 元/MWh,最大出力 40 MW。储能额定功率 10 MW,容量 40 MWh,初始 SOC 20%,SOC 运行范围 10%~90%。系统峰值负荷设在 80 MW,调频容量需求设为 8 MW。现货电能量市场采用节点边际电价,调频容量价格取一组随时间变化的曲线,白天系统负荷高、频率偏差风险大的时段调频价格更高。
我设置了三种对比方案。方案 A 储能只参与现货电能量市场进行套利,不提供调频;方案 B 储能只参与调频辅助服务市场,不参与能量套利;方案 C 就是本文讨论的双层联合优化,储能可以同时申报现货能量曲线和调频容量,由模型给出最优分配。
4.2 基本结果与收益组成
在设定参数下,三种方案的一天收益结果大致如下表。注意这只是演示算例,不同市场价格参数下结果会有明显变化,不要直接当成实际投资决策依据。
| 方案 | 日综合收益(元) | 现货套利收益(元) | 调频容量收益(元) | 调频里程收益(元) |
|---|---|---|---|---|
| A:仅现货套利 | 12600 | 12600 | 0 | 0 |
| B:仅调频 | 18200 | 0 | 13400 | 4800 |
| C:双层联合优化 | 23900 | 8600 | 11300 | 4100 |
方案 C 的综合收益明显超过前两者,但值得注意的不是“联合优化最高”这个结论,而是容量分配的变化。方案 C 里,储能在现货市场不再激进地满充满放,而是把一部分功率和容量留给了调频。白天某个高电价时段,现货放电功率比方案 A 少了约 2 MW,同时把那 2 MW 申报成上调频容量,这部分容量收益加上里程收益反而超过了多放 2 MW 电的边际收入。
这说明一个道理:储能在市场里的优化不是“越多越好”,而是“边际收益相等”。当调频容量价格处于高位时,储能应该主动减少现货套利功率,把资源挪给调频;反过来,当电价尖峰特别高、调频价格一般时,储能就应该狠一点,多放电少报调频。这种动态切换能力,只有联合优化模型才能给你。
4.3 灵敏度分析:储能容量与调频价格水平的影响
我额外跑了三组灵敏度分析。第一组修改储能容量倍率,从 1 小时逐步增加到 4 小时;第二组把调频容量价格整体乘以 0.5、1.0、2.0;第三组调整现货市场峰谷价差。结果呈现出几个典型特征。
储能容量倍率从 1 小时增加到 4 小时时,综合收益从 19200 元/日逐步上升到 23900 元/日,但收益增幅明显递减。原因在于,调频容量受功率限制更多,增加容量倍率对调频收益贡献有限;而现货套利在 2 小时倍率附近已经基本把峰谷时段用满了,继续增加容量只能多充一两次谷电,边际贡献很小。
调频容量价格翻倍时,方案 C 的收益提升幅度比方案 A 大很多,原因是模型自动把更多功率分配给调频,鼓励储能牺牲一部分现货电量来换调频收入。这组结果表明,双层模型对市场价格信号的响应非常敏感,储能申报曲线的斜率直接受价格比影响。做实际决策的时候,对调频价格预测曲线的准确性要求很高,模型再漂亮,输入价格预测错了,结果也没意义。
5. 常见问题与调试经验
5.1 大 M 参数怎么取才能不改坏模型
这是双层次优化转单层时最典型的坑。大 M 本质是给互补松弛条件“足够大”的上界,让二进制变量能区分约束是否激活。取太大,求解器会在对偶变量和互补条件的数值判断上出错,有时候明明是最优解,求解器报出来的却是“不可行”;取太小,又可能把不可行的情况误判成可行,解出来的东西压根不是原问题的最优解。
我的经验是,大 M 不能随便固定成 1e6,最好根据具体约束维度来定。比如出力约束的互补条件,大 M 取最大出力值的 5~10 倍就差不多了;价格相关约束的大 M,可以用绝对电价上限乘上一个安全系数,通常取 1e4 到 1e5。每次改完模型,重新审视一遍大 M 的量级,不要图省事套用上一版。
5.2 KKT 转化后求解失败,我从哪里开始排查
KKT 转化后模型出问题,头号嫌疑是互补松弛条件写错了。一个常见的低级错误是把互补条件的方向写反,导致求解器出来的对偶变量符号不对,目标函数变成负收益。排查时我第一步会先把二进制变量去掉,光看原始约束和对偶可行性,单独交给求解器检查约束是否一致;第二步再看平稳性条件,这个最容易因为下标写错出问题;第三步才检查互补条件和大 M 取值。
另外一个容易忽略的点是下层的凸性。下层如果不是线性规划,KKT 条件只是必要条件不是充分条件,转化后得到的解可能不是全局最优。这也是我强调下层出清模型要尽量保持线性的原因,加入太多非线性约束会直接影响最终结果的可信度。
5.3 调频里程收益和机会成本耦合容易漏
很多早期版本的模型只计调频容量收益,不计调频里程收益,这会严重低估联合优化的价值。调频里程收入虽然不像容量收入那样稳定可预期,但在实际市场中占比并不小。代价是调频动作会消耗储能电量和增加循环次数,这部分损耗如果不在目标函数里体现,模型就可能过度调频。
把这部分耦合写进模型后,你的目标函数会增加一项里程系数乘以调频容量,减一个储能循环衰减成本系数乘以充放电总量。这两个系数的取值直接决定储能是偏保守还是偏激进。我在算例里把里程系数设为容量价格的 0.3,衰减成本系数按 0.05 元/kWh 计入,跑出来的结果整体是稳健的。你也可以根据自己的电池循环寿命数据和度电衰减成本去修正。
这套流程跑完之后,我个人最大的体会是:储能参与市场的决策优化,本质上是在不同收益来源之间做边际权衡。单市场的分析永远只能讲清一半的故事,只有把现货电能量和调频辅助服务放进同一个双层框架里,市场价格、储能申报、系统出清三者之间的互动关系才能真正体现出来。模型的套路并不复杂,真正费时间的是调参数和排查那些藏在 KKT 转化细节里的坑。如果你也在做类似的储能市场决策问题,不妨先按我这个框架搭一个最小版本,跑通以后再逐步加复杂度,这样后续扩展会顺很多。