简介:一套基于DDPG(深度确定性梯度策略)算法的售电公司竞价策略Python代码,面向电力市场研究者、量化策略开发人员及深度强化学习爱好者,用于模拟多个售电公司在连续状态与动作空间下的竞标和报价行为,从而在复杂市场中寻求最优收益策略。针对传统博弈论方法依赖信息完备、经典强化学习算法仅能处理低维离散空间的局限,代码以演员-评论家双网络架构为核心:策略网络负责生成报价策略,价值网络通过时序差分目标评估动作价值,两者交替更新,相较传统强化学习具有更高精度与稳定性,在不完全信息环境中也能收敛至接近完全信息纳什均衡的结果。同时,代码支持定量调整发电商的耐心参数,可直观反映不同程度的默契合谋,为分析市场博弈提供了有效手段。资源为单个PDF文档,内含完整源码与配套说明,对网络结构、训练流程与参数调整方式均有交代,压缩包大小约170KB,轻量易用;已有481人学习下载。结构清晰、扩展性强,便于在此框架中加入更多市场因素、环境模型或优化算法,适合作为进一步研究的工作基座。
1. 售电公司竞价策略:为什么DDPG成了被讨论最多的那个算法
做售电公司中长期合约与日前市场申报策略的人,应该都遇到过同一个瓶颈:电价曲线是非平稳的,用户负荷有季节性,竞争对手报价又是黑匣子,传统优化模型(比如线性规划或动态规划)算出的“最优报价”,放到真实市场里常常跑不过一套带记忆的启发式规则。原因很简单——你建模时根本拿不到对手完整的策略空间,而且市场出清价和你的报价之间存在非线性的、滞后的反馈。
深度确定性策略梯度(DDPG,Deep Deterministic Policy Gradient)这类深度强化学习算法之所以在售电竞价策略研究里被反复提及,是因为它天然适合“连续动作 + 高维状态 + 环境非平稳”的场景:你的报价不再是离散的几个档位,而是一个连续的价格区间;你的状态空间可以塞进历史负荷、日前电价、偏差考核、对手历史报价特征;你的奖励函数可以直接设计成扣除偏差考核后的净收益。这篇文章就沿着“算法原理 → 环境建模 → 代码实现 → 参数调优 → 踩坑排查”这条路径,把一套能跑通的DDPG售电公司竞价策略完整拆开讲。
2. DDPG算法核心:从确定性策略梯度到售电报价的映射逻辑
2.1 为什么不能用DQN:连续动作空间下的Q值最大化困境
如果你之前用过DQN做电网调度或机组组合,你会很快发现它在售电报价上有一个绕不过去的坎:DQN的输出是离散动作的Q值,你要么把报价离散成“+5元/MWh、+10元/MWh”这种档位,要么就得面对动作空间爆炸。更糟的是,DQN在训练时取的是argmax Q(s,a),这个操作要求对每个动作都评估一遍,动作一多,训练速度和稳定性都会崩。
DDPG走的是另一条路:Actor网络直接输出一个确定性的动作值,也就是连续报价系数;Critic网络负责评估“在这个状态下采取这个动作能拿多少长期收益”。两个网络交替训练,回避了“连续动作空间里求最大化”的计算难题。这一点恰好和售电公司报价匹配——你在现货市场申报的实际上是一个价格系数或者一段报价曲线,是连续值,不是档位选择。
另外一个现实原因:售电公司的竞价策略往往要同时考虑“中长期合约持仓”和“现货市场申报”两个决策层,DDPG的连续动作输出可以天然地拆成多维动作向量,比如第一维是合约电量比例,第二维是现货报价偏移量。这个灵活性是离散动作算法很难给的。
2.2 Actor-Critic架构与目标网络的软更新机制
DDPG一共维护四个网络:在线Actor(策略网络)、目标Actor、在线Critic(Q网络)、目标Critic。训练时,在线Actor根据当前状态s输出动作a,在线Critic根据(s,a)计算Q值;目标侧则负责计算TD目标:
y = r + γ * Q_target(s', μ_target(s'))
这里的soft update(软更新)是关键:目标网络的参数不是定期硬拷贝,而是每次训练步都做一次小幅移动:
θ_target = τ * θ_online + (1 - τ) * θ_target
τ一般取0.005到0.01。这个机制让目标网络的变化非常平缓,Q值的训练目标不会剧烈跳动,这是DDPG能稳定收敛的重要前提。我在做售电竞价仿真时,一开始把τ设成0.1,结果Q值loss直接发散,后面改成0.005才稳住。
2.3 把竞价策略问题改写成马尔可夫决策过程(MDP)
在用DDPG之前,必须先把售电公司竞价问题形式化为MDP,这一步做不好,后面代码写得再漂亮也白搭。我一般这样定义:
- 状态空间s:当前时段t的日前预测电价、历史3天的实际出清价、系统负荷预测、本公司的中长期合约持仓量、偏差考核累计值、对手近期报价均值(如果有数据)。
- 动作空间a:现货市场的报价系数,比如相对预测电价的偏移比例,范围约束在[-0.2, 0.2]之间;或者是分段报价曲线的各段价格。
- 奖励函数r:t时段售电公司的净利润,等于售电收入减去购电成本、偏差考核费用和违约惩罚。
2.4 为什么DDPG适合电力市场而非简单回归
常见的做法是用LSTM预测电价,然后基于预测结果做启发式报价。但这里有个根本性区别:预测只是一个中间步骤,竞价是一个序贯决策问题。你今天报高价可能明天被对手压价,你这周多签了中长期合约可能下周现货价格暴跌导致亏损,这类跨时间的决策耦合是回归模型没法处理的。
DDPG学到的是一个策略函数:给定当前市场状态,直接输出最优报价动作,它不需要显式建模对手策略,而是通过试错和奖励反馈隐式学习对手行为模式。这也是为什么在电力市场竞价策略研究中,DDPG比单纯的预测+优化方法更受关注。
提示:如果你的市场规则里报价是离散档位(比如只能报“±10元”三档),DDPG的优势会被削弱,这时可以考虑TD3或者离散化的SAC,但DDPG依然是动手做研究时最容易上手的基线。
3. 售电公司竞价环境建模:状态、动作、奖励与市场出清规则
3.1 环境类设计:交易周期与状态转移
写代码第一步是把市场环境封装成一个Python类,模仿Gym的接口。我习惯把交易周期设为一整天96个时段(15分钟一个点),每个时段做一次申报和出清。环境的step函数接收动作,返回下一个状态、奖励和是否结束。
import numpy as np class ElectricityMarketEnv: def __init__(self, price_data, load_data, penalty_coef=1.5): self.price_data = price_data # 历史日前出清价 self.load_data = load_data # 系统负荷 self.penalty_coef = penalty_coef # 偏差考核系数 self.current_step = 0 self.total_steps = len(price_data) self.position = 0.0 # 中长期合约持仓(MWh) self.cumulative_deviation = 0.0 def reset(self): self.current_step = 0 self.position = self.load_data[0] * 0.6 # 初始合约比例60% return self._get_state() def _get_state(self): # 状态向量:当前预测电价、负荷、持仓、偏差积累 state = np.array([ self.price_data[self.current_step], self.load_data[self.current_step], self.position, self.cumulative_deviation ]) return state def step(self, action): # action: 报价偏移系数,范围[-0.2, 0.2] bid_price = self.price_data[self.current_step] * (1 + action[0]) cleared_volume = self._market_clearing(bid_price) reward = self._calculate_reward(bid_price, cleared_volume) self.current_step += 1 done = self.current_step >= self.total_steps next_state = self._get_state() return next_state, reward, done, {}逻辑说明:这里把环境简化成“报价偏移系数→出清电量→奖励”的流水线,实际项目中你还要加入对手报价模拟器。状态向量只有4维,是为了先跑通训练流程;后面加特征时,再扩充到10维左右,包括历史电价差分、负荷变化率等。
参数说明:penalty_coef是偏差考核费用的放大系数,一般电力市场里偏差考核单价是电价的1.2到2倍,取1.5是比较常见的中间值。初始持仓比例60%是模拟一个偏保守的售电公司,你可以改成随机初始化来增强泛化性。
3.2 出清模型与奖励函数的设计细节
市场出清这里不追求复现PJM或者广东现货那种完整的节点电价计算,一个简化的统一出清模型足够用来验证DDPG策略的有效性。核心逻辑是:报价越低,出清电量越大,但边际利润可能被压缩;报价越高,单位利润高但可能不被出清。
def _market_clearing(self, bid_price): # 简化出清模型:报价低于市场均衡价则全部出清,否则按比例缩减 equilibrium_price = self.price_data[self.current_step] * 1.05 if bid_price <= equilibrium_price: return self.load_data[self.current_step] * 0.9 else: ratio = max(0.0, 1.0 - (bid_price - equilibrium_price) / equilibrium_price) return self.load_data[self.current_step] * ratio def _calculate_reward(self, bid_price, cleared_volume): # 收益 = 售电收入 - 购电成本 - 偏差考核 revenue = bid_price * cleared_volume purchase_cost = self.price_data[self.current_step] * cleared_volume * 0.85 deviation = abs(cleared_volume - self.position) penalty = deviation * self.price_data[self.current_step] * self.penalty_coef reward = revenue - purchase_cost - penalty # 更新持仓为本次出清量,模拟滚动合约 self.position = cleared_volume return reward逻辑说明:出清模型里,我设定了一个“市场均衡价”,它略高于历史出清价(乘1.05),模拟的是发电侧报价上浮。如果我们的申报价低于这个均衡价就能拿到90%的负荷量,否则出清比例线性下降。
奖励函数里最关键的是偏差考核项:售电公司实际用电量和申报量偏差过大会被罚款,所以策略不能只盯着售电收入,还要考虑申报量和实际负荷的匹配度。这里用cleared_volume和position的差值近似偏差,实际项目中应该用“实际用电量 - 中标电量”。
3.3 状态空间扩展:加入对手报价与天气预报特征
基础状态只有4维,训练出来的策略会比较“笨”——它只会根据当前电价和负荷做反应,学不到对手的报价规律。我建议至少再加下面几个特征:
- 过去24个时段的平均出清价和方差
- 最近3个时段对手的最高/最低报价(如果有历史数据)
- 温度预测值(夏天空调负荷对电价影响显著)
- 本公司当前累计收益(用于风险偏好调节)
加特征时要注意归一化:电价和负荷的数值范围差异很大,不归一化会导致Actor输出的动作被某些大数值特征主导。我一般用MinMaxScaler把每个特征压缩到[-1,1],或者用标准化。
from sklearn.preprocessing import StandardScaler # 在环境初始化时构建特征 def _build_full_state(self): recent_prices = self.price_data[max(0, self.current_step-24):self.current_step] price_mean = np.mean(recent_prices) price_std = np.std(recent_prices) # 拼接扩展状态 state = np.array([ self.price_data[self.current_step], self.load_data[self.current_step], self.position, self.cumulative_deviation, price_mean, price_std ]) return scaler.transform(state.reshape(1, -1)).flatten()逻辑说明:这里把最近24个时段的电价均值和标准差加进状态,让策略能感知价格趋势和波动性。scaler是在整个数据集上先fit好的,不能在训练过程中实时fit,否则会造成信息泄漏。
参数说明:历史窗口长度取24,对应一天的96个点里每4小时一个采样,这个粒度比较合适。如果你用的是小时级数据,窗口可以取72到168(3到7天)。
4. DDPG算法代码实现:从网络结构到训练循环的完整复现
4.1 Actor-Critic网络结构:PyTorch实现
DDPG的网络结构不需要太深,三层全连接足够处理大部分电力市场状态特征。Actor的输入是状态维度,输出是动作值(报价偏移系数),最后一层用tanh激活把动作限幅到[-1,1],再乘上动作范围系数。
import torch import torch.nn as nn import torch.optim as optim class Actor(nn.Module): def __init__(self, state_dim, action_dim, max_action=0.2): super(Actor, self).__init__() self.net = nn.Sequential( nn.Linear(state_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, action_dim), nn.Tanh() # 输出范围[-1,1] ) self.max_action = max_action def forward(self, state): return self.net(state) * self.max_action class Critic(nn.Module): def __init__(self, state_dim, action_dim): super(Critic, self).__init__() self.net = nn.Sequential( nn.Linear(state_dim + action_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, 1) ) def forward(self, state, action): return self.net(torch.cat([state, action], dim=1))逻辑说明:Critic的输入是状态和动作的拼接,这在DDPG中是标准做法,让Q函数能感知动作对状态价值的边际影响。Actor输出乘以max_action,把tanh的[-1,1]映射到实际的报价偏移范围[-0.2, 0.2]。
参数说明:中间层256是经验值——电力市场的状态维度一般不超过20,256到512之间的宽度就足够拟合,太宽反而容易过拟合训练噪声。如果状态维度很小(4维),128也够用。
4.2 经验回放缓冲区:解决时序样本相关性
强化学习训练最怕样本之间高度相关,电力市场数据的时间相关性尤其强——上午10点的电价和上午9点的电价几乎线性相关。如果不做经验回放,连续训练几十步,网络很容易被带偏。
from collections import deque import random class ReplayBuffer: def __init__(self, capacity=100000): self.buffer = deque(maxlen=capacity) def push(self, state, action, reward, next_state, done): self.buffer.append((state, action, reward, next_state, done)) def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) states, actions, rewards, next_states, dones = zip(*batch) return ( torch.FloatTensor(np.array(states)), torch.FloatTensor(np.array(actions)), torch.FloatTensor(np.array(rewards)).unsqueeze(1), torch.FloatTensor(np.array(next_states)), torch.FloatTensor(np.array(dones)).unsqueeze(1) ) def __len__(self): return len(self.buffer)逻辑说明:buffer容量设为10万条,如果按96个时段一天来算,能存约1000天的交易数据,这已经覆盖了3年的市场运行。采样时用random.sample,保证每次训练的batch里的样本来自不同的时间点,打破时间相关性。
参数说明:batch_size后面训练时取64或128。capicity太小(比如1万)会导致旧样本被过早覆盖,策略会出现“短期记忆”效应;太大则会让新策略的学习速度变慢,因为新样本占比太低。
4.3 训练循环:软更新、目标策略平滑与梯度更新
训练主循环是DDPG落地的核心,这里把Ornstein-Uhlenbeck噪声、软更新、梯度裁剪都写进去。注意:DDPG训练对超参数非常敏感,代码里的数值是我调过多个电力市场仿真场景后觉得比较稳的。
class DDPGAgent: def __init__(self, state_dim, action_dim, max_action=0.2): self.actor = Actor(state_dim, action_dim, max_action) self.critic = Critic(state_dim, action_dim) self.target_actor = Actor(state_dim, action_dim, max_action) self.target_critic = Critic(state_dim, action_dim) # 初始化目标网络参数 self.target_actor.load_state_dict(self.actor.state_dict()) self.target_critic.load_state_dict(self.critic.state_dict()) self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=1e-4) self.critic_optimizer = optim.Adam(self.critic.parameters(), lr=1e-3) self.replay_buffer = ReplayBuffer(100000) self.max_action = max_action self.tau = 0.005 self.gamma = 0.99 def select_action(self, state, noise_scale=0.1): state = torch.FloatTensor(state).unsqueeze(0) action = self.actor(state).cpu().data.numpy().flatten() # 训练时加探索噪声 noise = np.random.normal(0, noise_scale, size=action.shape) return np.clip(action + noise, -self.max_action, self.max_action) def update(self, batch_size=128): if len(self.replay_buffer) < batch_size: return states, actions, rewards, next_states, dones = self.replay_buffer.sample(batch_size) # 计算目标Q值 with torch.no_grad(): next_actions = self.target_actor(next_states) target_q = self.target_critic(next_states, next_actions) target_q = rewards + (1 - dones) * self.gamma * target_q # 更新Critic current_q = self.critic(states, actions) critic_loss = nn.MSELoss()(current_q, target_q) self.critic_optimizer.zero_grad() critic_loss.backward() nn.utils.clip_grad_norm_(self.critic.parameters(), 1.0) self.critic_optimizer.step() # 更新Actor:最大化Q值 actor_loss = -self.critic(states, self.actor(states)).mean() self.actor_optimizer.zero_grad() actor_loss.backward() nn.utils.clip_grad_norm_(self.actor.parameters(), 1.0) self.actor_optimizer.step() # 软更新目标网络 for target_param, param in zip(self.target_actor.parameters(), self.actor.parameters()): target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data) for target_param, param in zip(self.target_critic.parameters(), self.critic.parameters()): target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data) return critic_loss.item(), actor_loss.item()逻辑说明:select_action在训练时加了高斯噪声来做探索,这比OU噪声简单且在这个场景下效果接近。OU噪声的优势是时间相关性,适合需要持续探索的情况,但高斯噪声配合经验回放也能达到足够好的探索效果。
更新顺序是Critic在前、Actor在后,这是因为Actor的梯度依赖于Critic的Q值评估,Critic必须先更新一步,让Q值更准确。grad_norm裁剪到1.0,防止训练到后期出现梯度爆炸——电力市场数据里偶尔会出现极端电价,会导致单batch的loss突增。
参数说明:Actor学习率1e-4比Critic的1e-3低一个数量级,这是DDPG的经典配置。原因是Actor的更新质量依赖于Critic的评估精度,Actor学太快容易被Critic的错误Q值误导。gamma取0.99适合电价这种day-level的决策,不需要太长的视野。
4.4 完整的训练入口:数据加载与评估逻辑
把环境、智能体、训练循环串起来,才是可以直接跑的训练脚本。注意这里的数据我用的是合成数据,你换成真实市场数据时,需要先做缺失值填充和异常值剔除。
def train_ddpg(env, agent, episodes=500): batch_size = 128 episode_rewards = [] for episode in range(episodes): state = env.reset() episode_reward = 0 done = False while not done: action = agent.select_action(state, noise_scale=max(0.1, 0.5/(episode+1))) next_state, reward, done, _ = env.step(action) agent.replay_buffer.push(state, action, reward, next_state, done) agent.update(batch_size) state = next_state episode_reward += reward episode_rewards.append(episode_reward) if episode % 20 == 0: avg_reward = np.mean(episode_rewards[-20:]) print(f"Episode {episode}, Avg Reward: {avg_reward:.2f}") # 每20个episode做一次确定性策略评估 eval_reward = evaluate_policy(env, agent) print(f"Eval Reward: {eval_reward:.2f}") return episode_rewards逻辑说明:探索噪声随着训练进程衰减,从0.5逐渐降到0.1,这个设计让前期充分探索,后期趋向于利用已学到的策略。每20个episode输出一次平均reward,同时做一次无噪声评估——评估时不加噪声,才能反映策略的真实水平。
参数说明:episodes取500,一个episode是96个时段,总共48000步。在普通CPU上跑大约需要20到40分钟,如果加上真实市场数据预处理可能要更久。如果你的环境步数更多(比如月度仿真),可以适当减少episodes到200。
5. DDPG竞价策略的五大避坑点:从训练发散到策略退化
5.1 训练发散:Q值爆炸的根源在奖励函数尺度
现象:训练到100到200个episode时,critic loss突然飙升到1e6以上,后续奖励全部变成NaN或者极端负数。
原因:售电公司的单时段收益数值可能在几十万量级,如果直接用绝对金额做奖励,Q值的梯度会非常大,网络权重更新一步就可能把参数推出有效范围。叠加市场均衡价的突然跳变,Critic对“高奖励状态”的估计会指数级膨胀。
解决:对奖励做归一化或裁剪。最简单做法是除以一个基准值(比如当日预测电价乘以平均负荷),把奖励压缩到[-1,1]区间。另一个办法是clip reward到[-10,10],虽然粗糙但有效。我在项目中还试过用reward的标准差做动态归一化,效果更平滑。
5.2 策略退化:所有时段报价一样,失去了博弈能力
现象:训练收敛后,Actor对所有输入状态都输出几乎相同的动作——比如固定报价偏移0.05,不管电价是高是低。
原因:环境建模过于简单时,策略学到的“最优解”确实是一个常数。比如出清模型是线性的,奖励函数里没有对报价弹性的约束,那最优策略就是固定报价。另一个原因是探索噪声衰减太快,模型在训练后期没有机会尝试新动作。
解决:检查出清模型是否过于线性——加入“报价越高,出清概率非线性下降”的机制。同时把探索噪声的最小值从0.1提高到0.15,或者在训练后期穿插随机动作episode。如果你发现常数策略的reward已经很高,说明环境本身的博弈空间不足,需要引入对手报价的动态变化。
5.3 训练不稳定:目标网络与在线网络更新不同步导致Q值震荡
现象:训练曲线大幅震荡,critic loss忽高忽低,actor的奖励在相邻两个episode之间相差5倍以上。
原因:一个常见问题是τ(软更新系数)设得太大,比如0.1,导致目标网络跟踪在线网络的速度过快,TD目标本身不断变化,Q值学习变成追自己的尾巴。另一个问题是目标网络初始化不一致——如果目标Actor和目标Critic没有从在线网络复制初始参数,前几百步的TD目标就是乱的。
解决:把τ固定在0.005,且确认目标网络参数初始完全复制在线网络。我建议在前100个episode冻结Actor的更新,只训练Critic,让Q值先稳定下来。这个技巧在电力市场环境中帮助很大,因为环境反馈延迟较长,早期Q值误差太大。
5.4 数据泄漏:用未来数据做状态特征导致虚高表现
现象:评估时策略看起来收益惊人,比所有基线高30%以上,但换到新数据上就崩。
原因:最常见的做法是把全年的电价和负荷数据预先做了标准化,scaler在全体数据上fit,包括测试期。这样训练时“间接看到”了未来的电价分布,模型学会了利用分布信息做插值。另一个数据泄漏点是:状态里包含了“当前时段的真实出清价”,而动作是影响出清价的,这形成因果倒置。
解决:严格按照时间序列划分训练集和测试集,scaler只fit训练集。状态里不能用“当前时段的真实出清价”——应该用“上一时段的出清价”或“日前预测价”。我把这个叫做“决策因果检查”:状态里任何变量都不能被当前动作影响。
5.5 环境随机性不足:策略过拟合历史电价形态
现象:训练集上平均reward持续上升,但换到下一年的数据时,reward直接掉到负值。
原因:某些时段的电价有强季节性,如果训练数据只包含一个季度,模型会把“这个时间点电价高”当成规律,而实际可能是偶然。更隐蔽的是,环境里如果只有一条电价曲线,模型会记住曲线的每个拐点,而不是学习策略逻辑。
解决:训练时对电价做扰动——在每一步给状态里的电价乘一个(1 + ε),ε服从N(0, 0.03)。另外一个做法是训练集用3年数据,测试集用之后1年。我在项目中还测试过用bootstrap抽样生成多条相似但不完全相同的电价曲线,对提升泛化能力有显著帮助。
提示:DDPG的调试周期可能占整个项目70%的时间。不要指望第一次跑就收敛。我的习惯是先把环境固定、奖励函数简化到奖励值能直接看出逻辑的水平(比如“报价越低,电量越多,收益先升后降”),再把DDPG接上去,这样出了问题能快速定位是环境问题还是算法问题。
6. 从仿真到评估:验证DDPG竞价策略有效性的三个实用技巧
6.1 与启发式基线的对比验证
DDPG策略好不好,不能只看绝对收益,要和几个常见基线对比,这样才能说明“强化学习学到的策略确实优于常规方法”。我一般设置三个基线:
- 固定报价策略:始终按预测电价的1.05倍申报,不做调整。
- 趋势跟随策略:电价上涨时报价上调10%,下跌时下调10%。
- 滚动优化策略:每个时段用最近24小时数据做一个小型线性优化,求最优报价偏移量。
对比时用同一套测试数据,记录总收益、收益波动率、偏差考核费用三个指标。DDPG如果只在总收益上领先10%以内,但波动率远低于基线,这仍然是有价值的——因为售电公司追求的是长期稳定收益,风险调整后的收益才是关键。
def evaluate_policy(env, agent, episodes=10): total_rewards = [] for _ in range(episodes): state = env.reset() episode_reward = 0 done = False while not done: action = agent.select_action(state, noise_scale=0) # 无噪声 next_state, reward, done, _ = env.step(action) episode_reward += reward state = next_state total_rewards.append(episode_reward) return np.mean(total_rewards)逻辑说明:评估时noise_scale必须设为0,否则你评估的是一套带了随机扰动的策略,而不是DDPG学到的确定性策略。虽然DDPG本身是确定性策略,但加上噪声评估会让结果每次都不一致,难以复现和比较。
参数说明:评估10个episode已经足够稳定,如果方差太大可以增加到30个。每个episode用不同的初始持仓比例,这样能验证策略对不同初始条件的适应能力。
6.2 敏感性分析:调整风险偏好系数观察策略行为变化
DDPG策略的一个亮点是:通过修改奖励函数里的风险项,你可以让策略表现出不同的风险偏好。把偏差考核的惩罚系数从1.0调到3.0,观察策略输出动作的变化——惩罚系数越大,策略应该越倾向于保守报价(接近均衡价以获得稳定出清量,而不是报高价搏收益)。
我在实际研究中发现一个比较有价值的规律:当惩罚系数设成2.0时,DDPG学到的策略呈现出“电价高时保守、电价低时激进”的行为——这符合售电公司在高电价时减少持仓风险的直觉。这种行为和启发式策略相比,最大的优势是平滑——不会因为单时段电价的突然波动做出极端决策。
你还可以做一个更有洞察的测试:把中长期合约持仓比例从60%改成20%,看策略是否会自动调整报价偏移量来适应新的风险敞口。如果DDPG学到的策略没有随着持仓比例变化而改变报价行为,说明状态空间里持仓特征的权重不够,需要调整网络结构或特征缩放。
6.3 模型鲁棒性验证:对抗性扰动测试
除了正常测试集上的表现,我还建议做一组对抗性测试:在测试期间人为注入异常电价(比如尖峰电价突然翻倍、连续低价持续一周),观察策略的掉收益幅度。这个测试的价值在于,电力市场参与者最怕的不是正常波动,而是极端事件。
def stress_test(env, agent, stress_factor=2.0): # 修改环境的电价数据,模拟尖峰 env.price_data = env.price_data * stress_factor result = evaluate_policy(env, agent) # 恢复正常数据 env.price_data = env.price_data / stress_factor return result逻辑说明:这个简单的方法能把原始电价整体抬高,模拟一个极端高价时段。更精细的做法是在特定时段插入尖峰,但整体抬高的好处是能快速评估策略对价格水平偏移的敏感度。如果DDPG策略在stress_factor=2.0时还能保持正收益,说明它有一定的鲁棒性。
参数说明:stress_factor从1.5到3.0逐级测试,观察收益下降的斜率。如果斜率很陡,说明策略过度依赖电价绝对值,建议在状态里增加更长时间窗口的电价趋势特征。
我自己做范围搜索的时候,也会打开实验记录,把每次超参数调整的序列数据保存到 CSV,追踪到底哪组参数组合可用。因为这种测试内外的体验差异,才是最真实的。说实话,DDPG在售电竞价策略研究里谈不上完美,它更像是一个把“连续动作决策”这个关键问题解决到能落地的基线框架。如果你能从这套完整环境搭建开始,一一步步把市场规则细化,就会相信:比起盲目去改网络结构,先想清楚状态和奖励的物理含义,才是这事最值得投入的部分。希望帮到你。
本文还有配套的精品资源,点击获取