可控优化: 模拟退火(基础篇)
📘

可控优化: 模拟退火(基础篇)

第 1/3 篇

用模拟退火给 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 读单元格。

MQL5 / C++
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 对齐的离散点上,而非连续均匀分布。 外汇与贵金属品种波动受杠杆与跳空影响,用此类自动优化得到的参数在历史样本上表现偏好,实盘存在过拟合与滑点风险,仅可作参考。

MQL5 / C++
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 以引用传参回填,让退火对象能从中断点继续跑,而不是从头预热。外汇与贵金属参数寻优本身高风险,这种断点续跑只解决工程健壮性,不暗示任何胜率改善。

MQL5 / C++
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;

常见问题

可引入模拟退火思路,以一定概率接受较差解来扰动搜索路径,避免优化器过早收敛到局部极值。
至少先定初始温度、降温系数、迭代步长和停止温度,并把这些写进参数结构体方便后续续算。
小布可接管重复的参数扫描与落盘续算,你只需给初始区间和退火参数,它帮你跑并标出可疑过拟合区。
不会,只要钩子函数里做了落盘,重启后从最后存档的温度和参数继续算即可。
太大易早熟收敛到局部最优,太小则搜索过慢耗电耗时;建议先按 0.85~0.95 做粗调再细化。