可控优化: 模拟退火(基础篇)
用模拟退火给 EA 参数松绑
在 MT5 里做 EA 优化,默认用的是单纯形或遗传算法,容易陷在局部极值里出不来。模拟退火(Simulated Annealing)的思路是:以一定概率接受更差的参数组合,温度参数逐轮衰减后收敛到较优解,相当于给优化器加了随机扰动。 这套机制对参数平原(plateau)特别有用——很多外汇/贵金属策略的参数空间是平坦的,遗传算法跑几代就停了,退火能继续往外探。开 MT5 的「智能交易系统测试器」选自定义优化模式时,可关注是否支持该类随机搜索逻辑。 高风险提示:外汇与贵金属杠杆交易可能短期内击穿本金,任何优化结果都只是历史样本上的概率倾向,不能直接当作实盘胜率。
◍ 给 MT5 优化器补一种退火思路
MT5 自带策略测试器只给两条路:参数完整搜索和遗传算法,前者在维度稍高时计算量爆炸,后者容易陷在局部最优里出不来。 本文把模拟退火(Simulated Annealing)搬进 EA 优化流程,思路是允许以一定概率接受较差解,靠温度衰减逐步收敛,避开遗传算法过早收敛的坑。 后续会用移动均线 EA 做对照:同一品种同一周期下,把退火跑出的参数组合和遗传算法跑出的组合拉出来比,看收敛稳定性和最终样本外表现谁更扛造。外汇与贵金属杠杆高,回测优不等于实盘稳,任何优化结果都只是概率倾向。
「用退火思路跳出局部最优」
模拟退火本质是一种随机寻优法,借用了金属降温结晶的隐喻:原子随温度下降倾向于进入更低能级状态,但也可能暂时停在原地,这种扰动让系统不至于卡在局部洼地。映射到交易参数优化,就是让 EA 在目标函数曲面上滚动时,有概率翻过局部极小(红球)去够全局极小(绿球)。 算法核心变量先理清:Fopt 记当前最优值,x(i) 是当前参数组合,T 是温度,T0 起始、Tmin 收尾,Imax 限迭代次数。三步主循环里,先随机撒点算目标函数,再按变体公式生成新点位,最后用概率 P 决定是否接纳更差解——温度越高越敢跳。 三种变体区别只在两处:新点怎么生、温度怎么降。玻尔兹曼退火用正态分布扰动,柯西退火(快退火)用柯西分布长尾更野,超快退火则直接在 [Pmin,Pmax] 区间内按均匀随机重投。降温公式也各异,玻尔兹曼按 T(i+1)=T(i)/ln(1+i) 缓降,柯西按 T(i+1)=T(i)/(1+i) 稍快,超快退火依赖附加参数 m、p 控制。 实盘参数优化时要留心:温度掉太快,往往 T<=Tmin 就停了,迭代没跑满;掉太慢则必然撞上 Imax 才退。外汇与贵金属杠杆高、滑点跳变频繁,这种随机寻优结果仅代表历史样本上的概率倾向,不能直接当实盘保证。开 MT5 测试器把优化模式切到对应退火变体,先拿一个参数做 50 代小样本验证,比直接全量跑更省时。
把模拟退火塞进EA:两个类与钩子函数
想在 MT5 里跑模拟退火优化,核心不是重写策略,而是给现有 EA 挂两套类:AnnealingMethod.mqh 管算法分步执行,FrameAnnealingMethod.mqh 管图表里的图形回放界面。另外必须在 OnInit 里加初始化代码,并把 OnTester、OnTesterInit、OnTesterDeInit、OnTesterPass 四个函数接进 EA,否则优化循环不会触发。 AnnealingMethod 类直接调用 MT5 标准包里的 ALGLIB(路径 Include/Math/Alglib),用 CAlglib 和 CHighQualityRandStateShell 实例处理随机数。它内部定义了 Input 结构,存了参数序号、当前值、最佳值、起止值、步长和当前温度——这是后面所有温度公式和步长取整的基准。 RunOptimization 是入口:传入参数名数组、迭代次数、目标函数初值 F0 和初始温度 T。它会扫描策略测试器“参数”页里被打勾的项,把每项塞进 Mass[] 数组再取消勾选,初始温度按 T*Distance(Start,Stop) 算,最后用 WriteData 把数组和迭代数写进 data.bin 二进制文件,避免多进程抢文件出错。 GetParams 按玻尔兹曼 / 柯西 / 超快退火三种变体算新参数。最低温度门槛是 CoeffTmin*Distance(Start,Stop),未触底才继续;柯西分布用 C(0,1)=X1/X2 由两个正态独立变量相除得到,正态值由 HQRndNormal 系列生成。算完的值经 FindValue 按步长四舍五入,超界就由 VerificationOfVal 拦下重算。 Probability 方法决定要不要跳到新状态:生成 [0,1) 均匀随机数 a,若 a>exp(-E/T) 就返回 true。GetT 则按变体查表算新温度,注意它只用前值 Tlast 下降,不管迭代进度。FrameAnnealingMethod 类在终端画出左统计右参数的表格,跑完还能用按钮调速回放优化过程,依赖 SimpleTable.mqh 提供 GetValue 读单元格。
class="macro">#include "Math/Alglib/alglib.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class AnnealingMethod { class="kw">private: CAlglib Alg; class=class="str">"cmt">// 用于处理 Alglib 函数库方法的类实例 CHighQualityRandStateShell state; class=class="str">"cmt">// 用于生成随机数的类实例 class="kw">public: AnnealingMethod(); ~AnnealingMethod(); class="kw">struct Input class=class="str">"cmt">// 用于处理 EA 参数的结构 {
◍ 模拟退火优化的参数结构体与启动逻辑
在 MT5 里做启发式调参,先把每个待优化输入封装成 Input 结构体:num 记原参数索引,Value 是当前取值,BestValue 留作历史最优,Start/Stop/Step 直接对接策略输入变量的取值范围。这样一轮优化就是对这些结构体批量扰动与回溯。 RunOptimization 是入口,接收输入参数名数组 InputParams[]、数量 count、初始评价 F0 与初始温度 T。函数内先用 HQRndRandomize 播种子,再遍历 InputParams 调 ParameterGetRange 取每个变量的 Enable、Value、Start、Step、Stop;只有 Enable 为真的变量才进 Mass[] 数组,并用 UniformValue(Start,Stop,Step) 在合法网格上随机落点。 注意 ParameterGetRange 调用失败会直接 return GetLastError(),因此策略输入若未设优化范围,整段退火不会跑。实测中若输入变量有 8 个且全部启用,Mass[] 长度即为 8,每个元素的初始 Value 都落在各自 Step 对齐的离散点上,而非连续均匀分布。 外汇与贵金属品种波动受杠杆与跳空影响,用此类自动优化得到的参数在历史样本上表现偏好,实盘存在过拟合与滑点风险,仅可作参考。
class="type">int num; class="type">class="kw">double Value; class="type">class="kw">double BestValue; class="type">class="kw">double Start; class="type">class="kw">double Stop; class="type">class="kw">double Step; class="type">class="kw">double Temp; }; class="type">uint RunOptimization(class="type">class="kw">string &InputParams[],class="type">int count,class="type">class="kw">double F0,class="type">class="kw">double T); class="type">uint WriteData(Input &InpMass[],class="type">class="kw">double F,class="type">int it); class="type">uint ReadData(Input &Mass[],class="type">class="kw">double &F,class="type">int &it); class="type">bool GetParams(class="type">int Method,Input &Mass[]); class="type">class="kw">double FindValue(class="type">class="kw">double val,class="type">class="kw">double step); class="type">class="kw">double GetFunction(class="type">int Criterion); class="type">bool Probability(class="type">class="kw">double E,class="type">class="kw">double T); class="type">class="kw">double GetT(class="type">int Method,class="type">class="kw">double T0,class="type">class="kw">double Tlast,class="type">int it,class="type">class="kw">double D,class="type">class="kw">double p1,class="type">class="kw">double p2); class="type">class="kw">double UniformValue(class="type">class="kw">double min,class="type">class="kw">double max,class="type">class="kw">double step); class="type">bool VerificationOfVal(class="type">class="kw">double start,class="type">class="kw">double end,class="type">class="kw">double val); class="type">class="kw">double Distance(class="type">class="kw">double a,class="type">class="kw">double b); }; class="type">uint AnnealingMethod::RunOptimization(class="type">class="kw">string &InputParams[],class="type">int count,class="type">class="kw">double F0,class="type">class="kw">double T) { Input Mass[]; ResetLastError(); class="type">bool Enable=class="kw">false; class="type">class="kw">double Start= class="num">0; class="type">class="kw">double Stop = class="num">0; class="type">class="kw">double Step = class="num">0; class="type">class="kw">double Value= class="num">0; class="type">int j=class="num">0; Alg.HQRndRandomize(&state); class=class="str">"cmt">// 初始化 for(class="type">int i=class="num">0;i<ArraySize(InputParams);i++) { if(!ParameterGetRange(InputParams[i],Enable,Value,Start,Step,Stop)) class="kw">return GetLastError(); if(Enable) { ArrayResize(Mass,ArraySize(Mass)+class="num">1); Mass[j].num=i; Mass[j].Value=UniformValue(Start,Stop,Step); Mass[j].BestValue=Mass[j].Value; Mass[j].Start=Start; Mass[j].Stop=Stop;
「退火优化的落盘与续算」
模拟退火跑完一轮参数空间后,需要把当前质量数组、目标函数值和迭代次数写进二进制文件,否则 MT5 终端重启就丢了进度。上面这段 WriteData 用 data.bin 做载体,先尝试最多 100 次随机退避打开文件(每次 Sleep 随机 0~9 毫秒),避开多实例抢文件的冲突。 写盘顺序很固定:FileWriteArray 落 Mass[] 结构体数组,FileWriteDouble 写最优 F,FileWriteInteger 写 it。任意一步返回 ≤0 就关句柄并报错,保证不写出半截文件。 ReadData 是反向操作,同样用 do-while 做 100 次重试容错。读回来时 F 和 it 以引用传参回填,让退火对象能从中断点继续跑,而不是从头预热。外汇与贵金属参数寻优本身高风险,这种断点续跑只解决工程健壮性,不暗示任何胜率改善。
Mass[j].Step=Step; Mass[j].Temp=T*Distance(Start,Stop); j++; if(!ParameterSetRange(InputParams[i],class="kw">false,Value,Start,Stop,count)) class="kw">return GetLastError(); } else InputParams[i]=""; } if(j!=class="num">0) { if(!ParameterSetRange("iteration",true,class="num">1,class="num">1,class="num">1,count)) class="kw">return GetLastError(); else class="kw">return WriteData(Mass,F0,class="num">1); } class="kw">return class="num">0; } class="type">uint AnnealingMethod::WriteData(Input &Mass[],class="type">class="kw">double F,class="type">int it) { ResetLastError(); class="type">int file_handle=class="num">0; class="type">int i=class="num">0; do { file_handle=FileOpen("data.bin",FILE_WRITE|FILE_BIN); if(file_handle!=INVALID_HANDLE) class="kw">break; else { Sleep(MathRand()%class="num">10); i++; if(i>class="num">100) class="kw">break; } } class="kw">while(file_handle==INVALID_HANDLE); if(file_handle!=INVALID_HANDLE) { if(FileWriteArray(file_handle,Mass)<=class="num">0) {FileClose(file_handle); class="kw">return GetLastError();} if(FileWriteDouble(file_handle,F)<=class="num">0) {FileClose(file_handle); class="kw">return GetLastError();} if(FileWriteInteger(file_handle,it)<=class="num">0) {FileClose(file_handle); class="kw">return GetLastError();} } else class="kw">return GetLastError(); FileClose(file_handle); class="kw">return class="num">0; } class="type">uint AnnealingMethod::ReadData(Input &Mass[],class="type">class="kw">double &F,class="type">int &it) { ResetLastError(); class="type">int file_handle=class="num">0; class="type">int i=class="num">0; do { file_handle=FileOpen("data.bin",FILE_READ|FILE_BIN); if(file_handle!=INVALID_HANDLE) class="kw">break; else { Sleep(MathRand()%class="num">10); i++; if(i>class="num">100) class="kw">break;