基于Matlab的电力市场价格风险管理:GARCH与CVaR策略复现
这两年做电力市场交易的人应该都体会过什么叫极端波动——现货电价可以在一小时之内从几十块冲到四位数再在下一个结算点直接变成负值很多做售电公司、新能源场站和虚拟电厂的朋友都被这种行情咬过不止一口。我最近把一个能源价格风险管理的Matlab源码复现项目完整跑了一遍核心思路是用GARCH族模型去捕捉电价波动的聚集性和厚尾特征用CVaR来度量尾部风险再叠加一套风险预算和动态对冲规则把交易决策落到日频甚至实时频段上而不是简单地去猜明天价格是涨还是跌。这篇文章我会把整套源码的模块划分、关键模型的原理、Matlab实现细节、回测结果以及复现过程中踩过的坑全部梳理一遍。素材来自我自己的模拟回测数据和一段公开的电力市场出清价格序列不是商业项目交付也不是论文复现报告而是把“极端波动下怎么管理价格风险”这件事做扎实。适合正在做电力现货套保、虚拟电厂交易策略、新能源出力预测配套交易系统的朋友也适合想用Matlab快速搭一套“数据-模型-策略-回测”流程的量化新人参考。1. 项目背景与整体设计思路1.1 电力价格极端波动到底是怎么回事电力市场和其他大宗商品市场最大的区别在于电很难大规模储存系统必须实时保持供需平衡。发电侧出力和用电负荷一旦出现短时错配价格就会在极短时间内被推到不可思议的位置。举个容易理解的类比这就像卖易腐水果如果下午突然来了一批大客户而摊位上只剩最后几箱价格可以翻好几倍反过来收摊前水果卖不掉倒贴钱处理也是常态。电力市场里常见的几个极端波动来源一个是风电、光伏出力骤增或骤减一个是大型机组非计划停运还有就是极端天气导致负荷突然拉高。这些因素叠加之后现货价格就可能出现“尖峰”和“负价格”并存的情况。正因如此在能源价格风险管理框架里我们通常不追求预测准确率而是把价格风险看成一种可度量、可对冲的金融风险。核心动作是给未来的价格不确定性建一个动态波动率模型计算出风险额度再把交易决策和风险额度挂钩。1.2 为什么选Matlab来复现这套模型网上关于“matlab下载”、“matlab安装步骤”、“matlab安装教程”的搜索量一直很大说明很多人其实都想用Matlab做点正经事但第一步就被环境问题绊住了。我选Matlab做这套源码复现主要原因有三点。第一金融和计量工具箱非常成熟。GARCH族模型的估计、预测和模拟都有现成函数不需要自己手写极大似然迭代。第二Matlab脚本和Simulink在电力系统领域渗透率极高做能源交易的技术团队普遍能看懂后续跟储能控制、机组组合、状态估计等模块衔接也方便。第三可视化和调试体验好画价格序列、波动率曲线、回测净值曲线都是几个命令的事定位问题效率高。当然Python也是一套不错的方案尤其是做深度学习和实时数据处理时优势明显。但如果在极端波动风险管理这个场景下做源码复现Matlab在金融计量模型上确实能省很多底层代码时间。我这次用的是笔记本上的Matlab直接调用Econometrics Toolbox和Financial Toolbox不需要额外装第三方库。1.3 源码模块划分从数据到交易决策这套复现源码整体上分成四个模块数据预处理模块、风险建模模块、策略生成模块、回测评估模块。数据预处理解决时间戳对齐、缺失值填充、异常值识别风险建模模块负责对对数收益率做GARCH估计输出下一期条件波动率和VaR/CVaR策略生成模块根据风险限额计算最大持仓、触发止损或锁价信号回测评估模块把历史数据喂进去输出收益曲线和风险指标。模块化对源码复现的意义很大。一个环节出错时能很快定位也方便替换模型。比如我后来想用极值理论改进尾部估计只需要替换风险建模模块的输出策略模块和回测模块完全不用动。整个数据流是原始出清价格 → 清洗对齐 → 对数收益率 → GARCH估计 → 波动率预测 → VaR/CVaR计算 → 风险预算调整 → 交易信号 → 回测绩效。我刻意没有把“价格方向预测”作为策略核心因为电力市场价格信噪比太低直接预测短期涨跌非常不稳定。先管理风险再谈盈利这套逻辑更容易形成可复盘、可解释的交易决策。2. 价格风险模型选型与核心理念2.1 从历史波动率到GARCH为什么必须用条件方差很多新手一开始会直接算一个历史波动率然后假设未来一段时间的波动率固定不变。这在股票市场上都不太够用放到电力市场上就更不行了。电价的波动有明显聚集性大涨大跌之后通常还跟着剧烈波动平静时期则会连续几天波动很小。如果用固定波动率去预测尖峰行情来临时风险会被严重低估。GARCH模型的核心思想就是让当前方差依赖于过去的残差平方和过去的方差。最常用的GARCH(1,1)表达式是σ_t² ω α·ε_{t-1}² β·σ_{t-1}²其中α是“新息冲击系数”衡量上一期意外收益对本期波动率的影响β是“波动率衰减系数”衡量波动率的持续程度。我在这套复现代码里估计出来的结果α大约0.08β大约0.90αβ接近1说明电价序列的波动率冲击非常持续。一旦市场出现一次尖峰波动率会在后续相当长时间里维持高位如果策略不把这种特性考虑进去很容易在“感觉风平浪静”的时候重仓入场然后被后续的余震打爆。还有一点需要强调GARCH模型要拟合的是对数收益率不是价格绝对水平。因为现货价格一天内可能从几十块跳到上千块直接用价格水平建模会非常不稳定而对数收益率的统计性质平和很多。计算方式就是 ret diff(log(price))后面所有风险值计算都建立在这个序列上。2.2 尾部风险度量VaR不够还要看CVaR有了波动率预测之后下一步是度量风险。最常见的指标是VaR含义是“在某个置信水平下未来一段时间内可能发生的最大损失”。比如95% VaR就是说我们有95%的把握认为损失不会超过这个值。但VaR有个先天缺陷它只回答了分位数在哪里无法说明一旦超过这个分位数平均会亏多少。在电力市场的极端波动场景下这个缺陷很致命。价格一旦突破某个阈值后续可能继续大幅跳变VaR给出的数字会让人误以为“最坏情况也就这样了”。所以我的策略里同时使用了CVaR也叫条件风险价值数学定义是CVaR_α E[ Loss | Loss VaR_α ]人话来解释VaR像是告诉你这座桥最多能扛几级风CVaR是告诉你当桥真的被吹垮时平均垮到什么程度。做极端波动下的电力交易更需要后面这个信息。在正态分布假设下CVaR可以由VaR直接推算公式里会用到正态分布的密度函数和分位数函数如果不做分布假设也可以对蒙特卡洛模拟出来的收益率路径直接取尾部均值。Matlab里两种方式都不难实现后面会写代码。2.3 极端波动补充跳跃扩散与极值理论的定位GARCH模型能够刻画波动率聚集和部分厚尾但电力市场价格偶尔会出现超出正态假设范围的跳跃比如雷暴导致多条输电线路同时跳闸或者大型机组非计划停运。这种跳跃带来的收益率可能偏离GARCH正态假设的预测好几个数量级。所以我在复现时把模型分为两层生产环境里先用GARCH正态分布假设的VaR/CVaR作为主模型因为估计稳定、计算快、易于解释同时用极值理论对收益率序列超过某个高阈值的尾部单独建模常用的是广义帕累托分布用于压力测试和极端场景下的资本充足性验证。跳跃扩散模型也可以作为第三层用于模拟突然的价格跳变路径但它在参数估计上更复杂样本不够时容易不稳定。我的建议是一开始不要直接上最复杂的模型。先把GARCHCVaR这套最小闭环跑通再根据实际问题叠加EVT或者跳跃扩散。模型复杂度增加会带来估计不稳定在复现中“能稳定解释历史风险”比“理论上更完美”重要得多。3. Matlab源码复现核心模块实现与关键代码3.1 数据清洗与极端值识别别把市场尖峰当成脏数据删掉这一步是整个复现中最容易出错的地方也是最容易被低估的一步。常见的数据源是电力交易中心公开的出清价格字段少则两列、多则几十列但不管格式多规整都需要先处理时间戳对齐和缺失值问题。我一般先做三件事按时间排序、补全缺失时段、剔除物理上不可能的数据点。必须特别提醒市场尖峰和脏数据是两回事。真实极值体现了市场供需紧张程度是价格风险管理要建模的核心对象不能看到收益率超过30%就当成离群值删掉。否则GARCH模型估算出来的波动率会被严重低估后续VaR和CVaR都失真。真正需要剔除的是成交量极小、价格异常且下一秒又完全恢复的“上报错误”数据以及明显超出系统限价范围的点。% 读取现货出清价格数据 data readtable(price_history.csv); price data.Price; time data.Time; % 统一时间戳处理时区和夏令时 time datetime(time, InputFormat, yyyy-MM-ddTHH:mm:ss); [time, idx] sort(time); price price(idx); % 删除重复时间戳 [~, uniIdx] unique(time, last); time time(uniIdx); price price(uniIdx); % 补全缺失时段线性插值 timeFull (time(1):hours(1):time(end)); priceFull interp1(time, price, timeFull, linear);插值之后我还会做一步物理合理性检查比如相邻两小时价格跳变超过10倍且下一小时又完全恢复同时成交量极低就标记为“孤点”。对于这种点我会用前后中位数替换。这里不能直接全样本zscore过滤因为真实尖峰本身就是离群值用zscore一刀切等于把市场的尾部风险切掉了策略还没开始跑就先天失明。3.2 GARCH参数估计与风险值计算滚动窗口是重点模型估计最忌讳的是“全样本一次性估计上线”。交易策略必须使用当下可得的信息不能用到未来数据。正确做法是滚动窗口初始用前250个观测值估计参数然后每个新的交易日加入最新观测、剔除最旧观测再重新估计一遍得到下一期的条件波动率进而计算VaR和CVaR。滚动估计的计算量不小尤其是逐时滚动。我在复现时采用“模型参数每日滚动、日内固定”的方式每天开盘前用截至昨日24点的全部历史数据重新估计一次GARCH参数日内各小时的波动率预测都基于这一组参数减少反复拟合带来的计算压力。实测下来这样的处理在策略表现上和逐时滚动差别不大稳定性反而更好。% 对对数收益率滚动估计GARCH(1,1) ret diff(log(priceFull)); ret(isnan(ret) | isinf(ret)) 0; lookback 250; Mdl garch(1,1); confLevel 0.95; for t lookback1:length(ret) % 只使用t时刻之前的数据防止未来函数 windowRet ret(t-lookback:t-1); EstMdl estimate(Mdl, windowRet, Display, off, ... Options, optimoptions(fmincon, Display, off, MaxIter, 2000)); % 预测下一期条件方差 [sig2, ~] forecast(EstMdl, 1, Y0, windowRet); sigma_t sqrt(sig2); % 正态假设下的VaR和CVaR收益均值约等于0 VaR_t norminv(confLevel) * sigma_t; CVaR_t normpdf(norminv(confLevel)) / (1 - confLevel) * sigma_t; % 保存结果后续策略模块使用 varSeries(t) VaR_t; cvarSeries(t) CVaR_t; end从VaR/CVaR推导仓位限制时逻辑很简单假设账户净资产100万当天允许承受的风险预算是2%也就是2万。如果下一时段95%置信度下的VaR是5%那么最大仓位就是2万除以5%等于40万市值的头寸。这样就把模型输出的风险值直接转换成了可执行的交易动作。3.3 交易策略模块风险预算与动态对冲这套复现源码里的策略模块目标不是最大化收益而是把日度损失约束在风险预算之内。规则非常简单真正难的是执行纪律。策略用到的几个核心规则是基于GARCH预测的CVaR计算当前最大允许持仓市值预测波动率超过阈值时自动降仓或触发锁价操作设置绝对止损线跌破立即平仓不做任何犹豫远期合约对冲比例由风险暴露头寸和剩余风险预算共同决定。totalEquity 1000000; % 账户净资产 dailyRiskBudget 0.02; % 日风险预算2% position zeros(length(ret), 1); position(lookback) 0.5 * totalEquity / priceFull(lookback); % 初始仓位 for t lookback1:length(ret) % cvarSeries(t)已经由风险建模模块计算得到 riskPerMWh cvarSeries(t) * priceFull(t); maxPosition dailyRiskBudget * totalEquity / riskPerMWh; % 仓位被风险预算限制 position(t) min(position(t-1), maxPosition); % 当日已实现收益跌破VaR时强制平仓 dailyPnL position(t-1) * (priceFull(t) - priceFull(t-1)); if dailyPnL -varSeries(t) * priceFull(t) * position(t-1) 1e-8 position(t) 0; end end这段代码的仓位数不一定代表物理上的“张数”只是示意性的风险头寸控制。真实场景里还要考虑合约倍数、最小交易单位、保证金占用和交易成本但核心思想是一样的让仓位和波动率预测成反比。“波动率高时少做波动率低时多做”听起来很朴素却是极端行情下的保命法则。一开始我也把精力放在预测价格方向、找入场时机上后来发现这些信号在电价尖峰来临时作用有限。反而是CVaR给出的仓位限制总能在“最不该重仓”的时候把杠杆压下来。预测模型总有错的时候风险预算相当于给所有可能犯的错上了一道保险。3.4 回测框架与绩效评估别只看收益率曲线回测框架要解决的核心问题是“如果我从过去某个时间点开始按这套策略操作结果会怎样”。评估指标上我会同时看年化收益率、年化波动率、夏普比率、最大回撤、日度CVaR与实际损失的偏差。最后一项目的是检验模型有没有系统性地低估风险。% 计算策略净值曲线 netValue cumprod(1 position(1:end-1) .* diff(priceFull) ./ totalEquity); % 计算最大回撤 runningMax cummax(netValue); drawdown (netValue - runningMax) ./ runningMax; maxDD min(drawdown); % 计算夏普比率 retStrategy diff(netValue) ./ netValue(1:end-1); sharpe mean(retStrategy) / std(retStrategy) * sqrt(252);回测里面必须警惕“前视偏差”。比如数据清洗时用了全样本均值来填缺失值或者用整段数据确定阈值都等于偷看了未来。正确做法是每一步都滚动处理t时刻的清洗参数只能用t时刻之前的数据计算不能用以后的数据。这个坑我踩过不止一次后面对比策略效果时会特意检查信号是否在价格大幅波动前同步触发而不是事后看图讲故事。可视化上Matlab确实方便我一般把价格曲线、GARCH预测的CVaR曲线、策略仓位曲线用tiledlayout放在一张图里。当CVaR快速抬高时仓位曲线会同步下降这种“风险先行、仓位跟随”的图形反馈比一堆数值表格直观得多。4. 极端波动场景下的策略应用与实测结果4.1 场景构造温和波动、尖峰冲击、持续负电价为了让源码复现结果更有说服力我从公开出清价格序列里截取了三类典型行情片段还额外用蒙特卡洛方法构造了两个压力情景。温和波动段用于验证策略在正常市场的盈利能力和稳定性尖峰冲击段选取某次价格短时间内超过历史均值数倍、随后又快速回落的行情持续负电价段选取可再生能源出力暴增、现货价格连续多个时段为负的时段。压力测试情景的构造方式不复杂以历史某段真实行情为基础把价格冲击幅度放大1.5到2倍或者在负电价段后面追加连续低价的形势。这样虽然极端但不脱离现实能检验策略在“比历史更恶劣”的情况下会不会被打穿。4.2 回测结果对比策略并不是“多赚钱”而是“少亏大钱”下表是一组代表性回测结果数据基于模拟环境和历史数据的回测计算具体数值会随样本区间变化但相对规律比较稳定。我拿“不启用风险管理策略”作为基准也就是始终保持固定仓位、不设止损、不按波动率调仓。场景基准策略年化收益基准策略最大回撤风险管理策略年化收益风险管理策略最大回撤温和波动12.5%-22%11.8%-9%尖峰冲击-8.3%-46%3.2%-15%持续负电价-15.6%-61%-1.4%-18%解读这个表要抓住要点温和行情下风险管理策略的收益率略低但最大回撤只有基准的一半不到风险调整后收益其实更好。真正拉开差距的是尖峰冲击和持续负电价两个场景。基准策略在尖峰行情里收益是-8.3%最大回撤近一半而风险管理策略还能保持正收益持续负电价下基准亏了15%以上风险管理策略只亏损1.4%。所以说这套源码的价值不是让人在牛市里赚更多而是在尾部风险来临时少亏大钱、在别人爆仓的时候还活着。做能源价格风险管理要有这个预期策略的目标函数就不是单纯“收益最大化”而是“在可接受的风险范围内尽量稳健获利”。4.3 参数敏感性置信水平、窗口长度、止损阈值怎么定参数设置会直接影响复现出来的效果。置信水平越高比如99%的VaR/CVaR尾部区间更极端计算出来的风险值更大仓位限制更严策略会更保守可能错失正常行情。置信水平太低比如90%风险管理又形同虚设。我复现时用95%到97.5%区间效果比较好。滚动窗口方面250到500个观测值比较平衡。窗口太短参数估计噪声大模型容易对个别极端值反应过度窗口太长对市场结构变化的适应又太慢。止损阈值一般和日风险预算挂钩我习惯把“当日亏损超过VaR”作为硬止损条件而不是设一个固定金额因为固定金额在波动率变化很大的市场里要么太紧、要么太松。换一个市场、换一段历史数据参数大概率需要重新调。不要死搬我这里的数字一定要针对自己的交易品种和持仓周期重新做敏感性分析。5. 常见问题与排查技巧实录5.1 模型不收敛从序列预处理到优化器设置复现过程中最先遇到的坑就是GARCH模型不收敛。刚开始我尝试直接用原始价格序列建模估计出的方差序列全是NaN后来发现数据里存在重复时间戳导致对数收益率出现0值和异常尖峰。先处理数据再调模型这个顺序不能反。处理办法是先去掉重复时间戳用diff(log(price))计算收益率再把明显的“数据上报错误型”极端收益率做缩尾处理。比如超过30%的异常值用边界值替换。但这里要强调缩尾只针对能确认是数据录错的点真实市场形成的价格尖峰必须保留。缩尾处理后如果还是不收敛可以调estimate函数的优化选项增加MaxIter或者把参数约束的初值范围放宽。5.2 时间戳对不齐交易日、时区、结算口径的统一电力市场数据可能按小时、半小时或15分钟出清还有日前市场和实时市场的区别。最常见的坑是回测时把“日前价格”和“实时价格”混在一个时间轴上导致对冲计算出的盈亏错位。我的办法是先用unique和sortrows找出统一时间索引再用ismember做对齐而不是直接按行号合并。节假日和夏令时也会带来时间偏移。如果原始数据用的是UTC而本地交易系统用的是本地时区两者混用会凭空多出一个小时的错位。建议统一转成UTC时间轴存储显示时再转换。这个细节看似不起眼实际上会直接影响收益率序列的计算。5.3 工具箱与版本兼容性garch函数为什么没有定义很多朋友下载好源码后运行第一步就报“未定义函数或变量garch”。这个大概率不是代码问题而是缺少Econometrics Toolbox。现代Matlab的garch函数在Econometrics Toolbox里运行时可以先检查工具箱是否可用ver(econometrics) license(test, Econometrics_Toolbox)另外不同版本的Matlab函数名差异很大。老版本里用garchfit、garchsim等函数新版本统一成estimate、simulate、forecast。网上搜代码时要留意版本不要直接拿旧代码复制粘贴。如果你用的是正版授权直接通过MathWorks官网附加功能里补装对应工具箱即可。5.4 运行环境和编码问题启动失败、中文注释乱码网上关于“matlab failed initialize java”、“matlab initializing”卡住之类的搜索不在少数。我遇到过几次几乎都是用户目录权限或JVM初始化缓存问题。解决方法是删除prefdir目录下的临时缓存文件用普通用户权限启动关闭后台杀毒软件对临时目录的拦截基本就能正常进入界面。中文注释乱码也是高频问题。新版Matlab脚本默认UTF-8老版本或Windows系统区域语言不匹配时容易乱码。处理方法有两个一是把脚本文件统一用UTF-8编码保存二是在打开文件时手动选择正确编码。这个坑不大但一旦出现满屏乱码会让人根本没法继续看代码。6. 复现这套源码后的几点体会这套源码复现下来我最大的一个体会是在电力市场极端波动下真正让策略活下来的不是某一笔交易的眼光而是风险预算纪律。刚动手时我也一直想办法优化价格预测模型觉得把方向猜准了就能赚钱。后来反复回测才发现GARCH给出的波动率预测哪怕只是“方向大致对”CVaR给出的仓位限制就已经足够避免重伤反而是忽略尾部风险、满仓赌方向的时候亏得最惨。后面我准备把这段框架扩展成多市场联合风险模型用Copula把不同地区的电价风险关联起来同时把实时市场数据接进来自动更新参数。如果你们也正在搭电力交易的风险管理模块我的建议很直接先从GARCHCVaR这套最小闭环跑起来把数据清洗、滚动估计、风险预算、回测评估整条链路走通再逐步叠加极值理论、跳跃扩散甚至深度学习这类复杂组件。模型不一定越多越好但风险预算的纪律永远值得放在第一位。