
我最近在帮一个分布式光伏项目做运营测算发现团队里争得最凶的不是组件效率不是储能寿命而是“电价到底定多少”。以前大家觉得分时电价表一贴就完事但光伏渗透率上来以后用户群根本不是一个静态负载他们会在群里对比峰谷价把洗衣机从晚上挪到中午电动车主把充电桩设到零点以后。你这边刚把电价压低那边负荷立刻涨上来光伏不够卖不得不高价买电你电价一调高负荷又掉下去光伏和储能白白弃掉。这个“你出价、用户响应、你再调整”的循环本质上就是一个典型的主从博弈Stackelberg game。在MATLAB里把这套博弈完整建出来并跑通是我这段时间主要折腾的事。这篇文章就把建模思路、代码实现以及中间踩过的坑完整拆开聊适合正在做电力市场、需求响应、新能源聚合运营相关仿真的朋友参考。1. 谁在博弈谁光伏场景下的领导者与跟随者1.1 聚合商是“领导者”但领导不是想怎么定就怎么定主从博弈的核心是两层决策结构上面一层先出牌下面一层看到牌之后再出牌。光伏场景里天然适合做上层“领导者”的是那个手里捏着屋顶光伏、储能系统和售电合同的聚合商。它的目标很直接在一天 24 个小时里最大化售电收入减去购电成本和运维成本。它的决策变量也很直接每个时段的售电价。但“领导者”并不意味着可以胡来。它把电价定高了用户就会少用光伏电力卖不出去储能也找不到套利空间它把电价定低了用户固然用得多但可能超出光伏出力聚合商就得去现货市场高价买电补缺口。也就是说聚合商的最优定价必须建立在对用户群“怎么响应价格”的预判上。这个预判不是猜而是把用户群的优化问题当作约束条件写进自己的优化模型里——这正是主从博弈和普通单层优化的最大区别。1.2 用户是“跟随者”但他们会理性地权衡舒适度与电费下层是用户群。我建模时不会去模拟每个用户的心理活动而是假设每个用户都会在自己的用电效用和电费支出之间做权衡。用户在电价高的时候少用可调负荷电价低的时候多用这个行为可以压缩成一个“给定电价优化用电量”的数学问题。为什么这里用主从博弈而不是用完全竞争模型因为定价权并不在用户手里用户数量再多每一个人的决策也只是对聚合商公布电价的被动响应而聚合商又必须照顾用户的响应规律。这就像房东定房租、租客决定住不住或住多大面积一样是一个“一个主导者 多个跟随者”的结构而不是所有参与者同时出价的纳什博弈。用主从博弈描述这段关系数学上更贴切求解出来的均衡价格也更有解释力。1.3 实验舞台一个有光伏、有储能、有可调负荷的 24 小时微电网我把仿真场景缩放到一个典型的社区微电网规模1 个光伏聚合商10 个可调节用户时段取 24 小时。光伏出力按典型晴天的钟形曲线生成储能系统带日循环约束用户侧有一部分基础负荷不可调另一部分是可调负荷比如热水器、洗衣机、空调负荷、电动汽车充电。每个用户面对电价时可以调整可调负荷的大小但不允许把可调负荷调到负值也不允许超过设备额定功率。这样设定出来的模型规模不大但结构完整有上层定价、下层响应、储能充放电、电网买卖电。既能跑通主从博弈的完整逻辑又不会让代码复杂到劝退。下面两节就把两层模型的数学形式逐项拆明白。2. 上下层模型怎么建效用函数、储能约束与平衡公式2.1 用户侧为什么选二次效用函数用户 j 在时段 t 的可调用电量为 x_{j,t}它带来的舒适感用效用函数 U_j(x) 描述。我用的二次效用函数是U_j(x) A_j * x - 0.5 * B_j * x^2其中 A_j 表示用户对电能的边际评价单位是元/kWh可以理解为“当用电量很少时用户愿意为 1 kWh 付出的价格”B_j 表示边际评价随用电量增加的下降速度B_j 越大用户对用电量越敏感。这个函数形式几乎是同类文献的标准选择原因有两个它是严格凹函数用户的用电效用符合“边际递减”的直觉——第一个小时的空调吹得最舒服第三个小时的舒适感提升就没那么明显它是二次的求导后是线性的这让后续博弈求解变得非常干净。用户的净收益是效用减去电费支出max_x [ U_j(x) - p * x ]对 x 求一阶导并令其等于零A_j - B_j * x - p 0所以用户的最优响应是x_j(p) (A_j - p) / B_j这就是用户的需求响应曲线。价格越低用电量越大价格越高用电量越小。注意这个式子成立的前提是 x 落在 [0, D_j] 的可行区间内其中 D_j 是可调负荷上限。如果算出来的值超过上限就取 D_j如果小于 0就取 0。区间边界处的处理很关键后面第 5 节我会专门讲它带来的坑。2.2 聚合商侧收入、储能和功率平衡聚合商的收入来自两处向用户售电的收入以及把多余光伏电力送到电网拿到的上网电价收入。支出则包括向电网买电的费用和光伏运维成本。因此聚合商的优化目标是max [ Σ_t ( p_t * X_total_t p_fit * Sell_t - c_spot_t * Buy_t - c_om * pv_t ) ]其中 X_total_t 是该时段所有用户的用电总量Sell_t 是上网电量Buy_t 是从电网购入电量c_spot_t 是现货购电价p_fit 是光伏上网电价c_om 是运维成本。聚合商还拥有一套储能。储能用荷电状态 S_t 描述充放电会改变下一时刻的荷电状态S_{t1} S_t η_ch * Pch_t - Pdis_t / η_dis其中 Pch_t 和 Pdis_t 分别是充电功率和放电功率η_ch 是充电效率η_dis 是放电效率。储能容量、充放电功率、SOC 上下限都必须限制在合理范围内另外我通常要求一天结束时的 SOC 回到初始值这样整个日循环是可复盘的储能电量不会凭空增加或减少。功率平衡是最容易写错也最容易忽略的一条约束。每个时段必须满足pv_t Pdis_t Buy_t L0_t X_total_t Pch_t Sell_t左边是电源侧出力右边是负荷侧消耗。注意这里的 L0_t 是不可调的基础负荷用户的可调负荷 X_total_t 会随着电价变化而变化——这正是聚合商调整电价后系统功率平衡被改写的核心机制。2.3 内部解假设把博弈变回一个凸二次规划的关键我在实际建模时做了一个“内部解”假设用户的最优响应始终落在 0 和 D_j 之间不会触界。在这个假设下用户的响应函数直接代入聚合商模型不需要再考虑取边界值的分片逻辑。把 x_j(p) (A_j - p) / B_j 代入聚合商目标后售电收入中会出现 p 的二次项p_t * X_total_t p_t * [ L0_t Σ_j (A_j - p_t) / B_j ] p_t * ( L0_t Σ_j A_j / B_j ) - p_t^2 * Σ_j 1 / B_j因为每个用户的 B_j 都为正所以 Σ_j 1/B_j 也是正数这意味着目标函数对 p_t 的二次项系数是负的——聚合商的目标关于电价是严格凹函数。凹函数最大化在数学上等价于凸函数最小化再加上功率平衡、储能 SOC、充放电功率这些约束全部是线性约束整个双层博弈的单层等价形式就变成了一个凸二次规划QP。这是整篇文章里最值得记住的一点主从博弈听起来很高端但在内部解假设下它落到 MATLAB 里就是一个 quadprog 能解的凸二次规划。这也是很多论文能够证明均衡存在且唯一的重要原因。3. 两条求解路径都意味着什么交替迭代更适合教学KKT 单层化更适合算均衡3.1 路线 A你来我往的交替迭代解决主从博弈最直观的方法就是照着博弈过程本身去仿真给一个初始电价 p0用户根据电价求解自己的用电量聚合商看到总用电量之后重算一个更有利的电价用户再根据新电价调整用电量重复直到电价和用电量都不再变化。这种方法的优点是逻辑直观特别适合在教学和验证性实验中展示“讨价还价”的过程。我在写第一版代码时用的就是这条路。但它有几个现实问题价格更新步长不好定定大了在均衡附近来回震荡定小了收敛极慢而且如果上下层各自只按当前值交替优化逻辑上会收敛到一个不动点但并不保证这个不动点就是主从博弈的 Stackelberg 均衡。只有在问题有很好的凸性时交替迭代才比较可靠。3.2 路线 B把下层问题“塞进”上层一次性求均衡更严谨的做法是借助 KKT 条件把下层用户问题转化为上层模型的约束。用户问题是凸优化KKT 条件既是必要条件也是充分条件所以可以用用户的 KKT 条件精确刻画“用户对任意电价的最优响应”。具体到我们的二次效用模型用户的 KKT 条件会退化成非常简洁的等式 A_j - B_j * x - p 0再加上互补松弛条件处理边界。在这个基础上聚合商模型和用户响应被合并成一个单层优化问题直接求解就能得到真正的 Stackelberg 均衡不需要多轮迭代。第 2.3 节已经说明了在内部解假设下这个单层问题是一个凸二次规划。如果用户真实地触到了边界问题就会变成带互补约束的数学规划MPEC这种问题理论上更难实践中通常用二进制变量和大 M 法把互补条件线性化再用混合整数规划求解器处理。这篇文章的主代码走内部解路线把最复杂的情况留给第 5 节讨论。3.3 我最终选哪条路我最终选择路线 B原因有三结论可靠KKT 单层化求解一次到位得到的均衡价格有理论保障求解速度快凸二次规划用 MATLAB 自带的 quadprog 就能秒解不需要商业求解器后续扩展方便增加储能约束、功率平衡约束都是往模型里加线性约束不会破坏结构。交替迭代并没有被完全抛弃它的角色变成了“验证工具”。我会在单层求解结束后把得到的均衡电价再放回用户模型检查用户用电量是否真的是最优响应以及聚合商在固定用户响应的前提下是否还能通过改价获得更高利润。能通过这两项检验均衡才算真正成立。4. 完整跑一遍24 小时光伏主从博弈的 MATLAB 代码4.1 参数设置先让用户响应区间和价格边界对上建模之前最重要的准备工作是让用户参数和电价边界匹配。如果 A_j 的平均值远高于电价上限用户在所有时段都会把可调负荷开到最大响应区间失去意义如果 A_j 远低于电价下限用户干脆不用可调负荷博弈就名存实亡。我使用的参数空间大致是用户保留价值 A_j 落在 0.65~1.05 元/kWh敏感系数 B_j 落在 0.10~0.25 元/kWh^2可调负荷上限 D_j 取 2.5 kW。加上电价区间限制在 0.30~1.00 元/kWh均衡电价大概率落在 0.4~0.8 元/kWh 之间这个区间既能保证用户有正常的响应幅度又不会因为触界把模型搞成分片函数。光伏出力用一个典型晴天曲线峰值 15 kW 出现在正午现货购电价在 9:00~20:00 取 0.65 元/kWh其余时段取 0.40 元/kWh光伏上网电价取 0.45 元/kWh。储能容量 20 kWh充电和放电效率都取 0.95最大充放电功率 6 kWSOC 允许范围 20%~90%初始和末尾 SOC 设为 10 kWh。4.2 核心代码YALMIP 建模quadprog 求解我用 YALMIP 做约束和目标建模底层求解器直接用 MATLAB 自带的 quadprog。如果你已经装了 Gurobi 或 CPLEX把 sdpsettings 里的 solver 改一下就行如果没有也不影响跑通这套代码。% 光伏聚合商 - 用户群 Stackelberg 博弈: 单层凸QP实现 % 依赖: YALMIP (https://yalmip.github.io) MATLAB 自带 quadprog T 24; % 时段数 J 10; % 可调用户数 % ---- 用户弹性参数 ---- rng(42); % 固定随机种子保证可复现 A_j 0.65 0.40 * rand(J, 1); % 用户保留价值单位 元/kWh B_j 0.10 0.15 * rand(J, 1); % 价格敏感系数单位 元/kWh^2 D_j 2.5 * ones(J, 1); % 单个用户可调负荷上限 kW % ---- 光伏出力曲线 / 基础负荷 / 购电成本 ---- pv zeros(T, 1); pv(7:18) [1.8, 4.2, 7.5, 10.5, 13.0, 15.0, 14.5, 12.0, 9.0, 6.0, 3.5, 1.5]; L0 8 2 * sin((0:T-1) / 24 * 2 * pi - pi/2); % 简化基础负荷 cspot 0.40 * ones(T, 1); cspot(9:20) 0.65; % 9:00-20:00 为购电高峰段 pfit 0.45; % 光伏上网电价 com 0.02; % 光伏运维成本 % ---- 储能参数 ---- Smax 20; % 储能容量 kWh eta_ch 0.95; % 充电效率 eta_dis 0.95; % 放电效率 PchMax 6; % 最大充电功率 kW PdisMax 6; % 最大放电功率 kW S0 10; % 初始/末尾 SOC kWh p_min 0.30; % 聚合商电价下限 p_max 1.00; % 聚合商电价上限 % ---- 决策变量 ---- p sdpvar(T, 1); % 聚合商售电价元/kWh Pch sdpvar(T, 1); % 充电功率 Pdis sdpvar(T, 1); % 放电功率 Buy sdpvar(T, 1); % 从电网购电功率 Sell sdpvar(T, 1); % 上网电功率 S sdpvar(T1, 1); % SOC 轨迹T1 个点 % ---- 约束组装 ---- con []; con [con, S(1) S0, S(T1) S0]; % 日循环约束 con [con, 0.2 * Smax S 0.9 * Smax]; % SOC 边界 con [con, 0 Pch PchMax]; % 充电功率边界 con [con, 0 Pdis PdisMax]; % 放电功率边界 con [con, 0 Buy, 0 Sell]; % 买卖电非负 con [con, p_min p p_max]; % 电价边界 for t 1:T % 储能状态递推 con [con, S(t1) S(t) eta_ch * Pch(t) - Pdis(t) / eta_dis]; % 用户群在当前价格下的总可调负荷响应 Xsum_t sum((A_j - p(t)) ./ B_j); % 限制 p(t) 使各用户响应都落在 [0, D_j] 区间内 for j 1:J con [con, A_j(j) - p(t) 0]; con [con, A_j(j) - p(t) B_j(j) * D_j(j)]; end % 功率平衡: 源侧 负荷侧 con [con, pv(t) Pdis(t) Buy(t) ... L0(t) Xsum_t Pch(t) Sell(t)]; end % ---- 目标函数: 最大化聚合商利润 ---- % 售电收入展开: p * (L0 sum(A_j/B_j)) - p^2 * sum(1/B_j) Revenue sum(p .* (L0 sum(A_j./B_j)) - (p.^2) .* sum(1./B_j)); Profit Revenue - sum(cspot .* Buy) pfit * sum(Sell) - com * sum(pv); % ---- 求解 ---- ops sdpsettings(solver, quadprog, verbose, 0); result optimize(con, -Profit, ops); if result.problem ~ 0 warning(求解出现问题: %s, result.info); else p_opt value(p); X_opt value(sum((A_j - p_opt) ./ B_j, 1)); % 各时段总可调负荷 SOC_opt value(S); end4.3 代码里的几个关键约束为什么要这么写先看用户响应区间的线性化约束。A_j(j) - p(t) 0保证可调负荷响应不小于 0A_j(j) - p(t) B_j(j) * D_j(j)保证响应不超过上限 D_j。这两个不等式展开后是当前价格与用户参数的线性关系会让电价在所有时段都被限制在一个有意义的价格走廊里。再看功率平衡。很多初学者会在代码里直接写成“光伏 放电 购电 固定负荷 可调负荷 充电 上网”但在这里Xsum_t不是一个外部输入而是由 p(t) 决定的内部变量。这意味着电价一旦变化系统功率平衡右侧会自动重构聚合商必须在“提高价格减少供给压力”和“降低价格增加光伏本地消纳”之间做权衡。这恰恰是主从博弈的微妙之处。最后是目标函数。Revenue里有一项(p.^2) .* sum(1./B_j)它让利润函数对电价为凹。这也是整个模型能交给 quadprog 求解的原因。如果去掉二次项目标退化成线性函数最优解一定会跑到电价边界那就不是博弈而变成了一个纯线性规划。4.4 从结果里能观察到什么把运行结果画出来后通常能看到几个典型现象电价曲线不会固定在上限或下限而是会呈现“跟光伏出力走”的形状。阳光强烈的午间聚合商有压低电价的激励用便宜电价吸引用户多用电避免光伏电量上网后只能拿到较低的上网电价傍晚光伏出力下降但基础负荷还高电价会回升引导用户削减可调负荷。储能的充放电规律会跟上电价变化配套。低价时段充电高价时段放电储能在中间起到削峰填谷和套利的作用。由于功率平衡约束把储能和电价耦合在了一起储能的存在会让电价曲线变得比无储能时更加平滑。用户总负荷不再是固定值。均衡电价下各个用户的可调负荷恰好达到“边际效用等于电价”的状态没有用户可以通过单方面调整用电量做得更好。我在实际调试时还会额外做一步均衡复核把 p_opt 放回用户模型重新验证每个用户的 KKT 条件再把用户最优响应放回聚合商目标看看是否还有改进空间。两层都能通过复核我才敢把这个结果称为均衡而不是某个任意可行解。5. 参数一调就翻车边界情形和三个可以继续深挖的方向5.1 最容易翻车的地方用户触界与参数走廊错配我在第 2.3 节强调“内部解假设”是因为它一旦不成立代码和结论都会出问题。假设你把 A_j 随机范围改成了 0.3~0.6而电价下限还是 0.3那么很多用户在高电价时段会直接算出 x 0可调负荷闲置反之如果把 D_j 调得太小低电价时段又会大量触到上限。这些触界情况会让目标函数的分段点落在可行域内部简单的凸二次规划就不再是精确的均衡模型。处理触界有一个标准做法给每个用户的每个时段引入互补条件例如“x 0 或 KKT 等式成立”这类逻辑再用二进制变量和大 M 法转化为混合整数线性约束。但这样求解规模会明显变大而且问题从凸优化变成非凸混合整数问题后收敛性和最优性保障都不如内部解模型干净。所以我的经验是先通过参数设计尽量让均衡落在内部解区间把触界当作特殊情况单独分析而不是一开始就把模型往最复杂的方向建。5.2 延伸方向一把 10 个用户扩展到 1000 个代码里的 J10 是为了教学演示。如果真的要接一个小区或园区的数据J 可能会上千。上千个用户并不意味着变量要跟着上天常用的做法是用聚类把用户分成 K 类比如按 A_j 和 B_j 聚成 5 类每一类用类内均值和容量聚合模型规模基本不变。聚合后的用户群响应仍然可以写成一个平滑的需求函数只要聚类后的参数区间保持一致内部解假设依然成立。5.3 延伸方向二给用户侧加储能和电动汽车调度这套模型里只有聚合商拥有储能。现实场景下用户侧可能有自己的光伏、户用储能或者电动汽车充电桩。这种情况下用户侧不再只是“一个弹性负荷”而是也有自己的跨时段决策。模型会演化成多领导者或者多跟随者带内部状态的结构比如一个上层聚合商、多个带储能的下层用户甚至多个聚合商之间还要竞争用户。求解思路还是一样的把下层用户的 KKT 条件提出来代数化合并成单层问题。只不过下层变量变多互补条件变多求解难度会明显上升。5.4 延伸方向三出力不确定性与用户隐私保护光伏出力永远不可能完全预测准。更强的做法是在主从博弈框架里加随机场景聚合商定价时面对多个可能的光伏出力场景每个场景下用户各有响应结果。这等价于一个两阶段随机规划第一阶段定电价第二阶段根据场景实现做储能和买卖电调度。目标改为期望利润后模型仍然保持凸二次结构场景数增加只会增加线性约束的条数不会破坏求解器的主框架。还有一个工程上很敏感的维度是隐私。用户并不想把自己完整的负荷曲线和参数告诉聚合商。主从博弈的交替迭代路线在这里反而有了优势每次迭代只需要聚合商公布一个新电价用户返回一个总用电量不需要上交内部偏好和逐时段的详细数据。第 3 节的单层 QP 尽管可以一次性求出均衡但它要求聚合商知道所有用户的 A_j 和 B_j这在真实商业环境里很难做到。实践方案经常是把两者结合理论分析用单层 QP 拿基准解落地系统用交替迭代做分散交互两边互相校验。写到这里我已经把内部解条件下的主从博弈建模、单层化思路和 MATLAB 实现全部捋了一遍。个人最深的体会是博弈模型看起来比普通优化难但一旦把下层 KKT 条件代数化回到 MATLAB 里往往就是一个可以秒解的凸规划。真正费时间的从来不是求解环节而是参数区间设计——只有让用户响应区间、电价边界、储能容量这些条件互相匹配才能避免模型在边界处反复波动。最后分享一个小技巧代码跑通以后先从单时段解析解去验证结果再扩展到 24 小时带储能模型一步一步来比直接堆一个大模型要省力得多。