可控优化: 模拟退火·综合运用
🌡️

可控优化: 模拟退火·综合运用

(3/3)·从移动均线EA实测到退火三变体调参,把随机优化真正接进你的MT5测试器

新手友好 第 3/3 篇

很多交易者以为策略测试器只有完整搜索和遗传算法两条路,遇到多峰目标函数就卡在局部极值干瞪眼。模拟退火提供第三条随机寻优路径,但多数人没把它真正跑通在实盘EA上。本篇把算法实现、参数生成和对比测试一次讲透。

给均线EA注入模拟退火优化骨架

要把一个普通移动均线EA改造成能在策略测试器里跑模拟退火的版本,核心是把算法类挂进去但不动实盘逻辑。AnnealingMethod 和 FrameAnnealingMethod 两个类必须包含进工程,同时声明算法用的辅助变量;这些代码只在优化模式触发,实盘正常跑不受影响。 原EA输入参数需要重命名加 _Optim 后缀:MaximumRisk→MaximumRisk_Optim、DecreaseFactor→DecreaseFactor_Optim、MovingPeriod→MovingPeriod_Optim、MovingShift→MovingShift_Optim。算法配置变量用 sinput 声明锁死,包括迭代次数、方法类型(0玻尔兹曼/1柯西/2超快)、温度系数、终止温度系数、优化标准Crit(0净利润到13自定义)等,细节见表3定义。 OnInit 开头要加一段仅优化模式执行的初始化:从 data.bin 读 AnnealingMethod 跑出的数据,alg_err=0 且是首迭代时若 ManyPoint=true 就给每个代理生成不同初值,否则调 GetParams 填那四个 _Optim 参数。OnTesterInit 负责填参数名数组、跑 RunOptimization、建图形界面;之后控制权进 OnTester,里面用 GetFunction 算新目标函数、比 Fopt、判温度到否最小值后加帧写文件。 OnTesterPass 和 OnTesterDeInit 分别管帧显示与结果落盘,OnChartEvent 接图形回放按钮事件。改完这套,EA就具备退火寻优能力,接下来直接进测试环节看随机性下的真实表现。 测试用 TestAnnealing.mq5 从全搜索结果文本(5列:4变量+目标值)加载数据,在表里用退火法游走找优。单参数「均线周期」全搜索120次迭代(EURUSD,2017全年,1分钟OHLC,本金1万杠杆100,最大余额标准),退火只给30/60/90次迭代各跑1万次:30次迭代超快退火拿最佳值33%,60次柯西退火47%,90次柯西退火62%。外汇与贵金属优化属高风险,这些命中率只是历史样本概率,换品种或时段会漂移。 两参数「周期+偏移」全搜索要6600次迭代,退火试330到4950次。4950次配 p1=0.5,p2=3 的超快退火拿到最佳值65%,而随机搜索同条件下90次迭代仅52%——退火优势随迭代拉长在收缩,调 p1/p2 控温降速是实跑必做的旋钮。

◍ 超快退火与遗传算法的实盘参数较量

把超快退火和 MT5 策略测试器里自带的遗传算法(GA)摆到同一组 EURUSD 数据上比,结论很直接:迭代预算吃紧时,超快退火更划算。2017 全年 1 分钟 OHLC、无延迟、本金 10000、杠杆 1:100 的设定下,双变量(均线周期 1-120、偏移 6-60)优化跑 20 次 GA,平均迭代 175 次,目标函数均值 1529.771,仅是已知最佳值 2446.33 的 62.53%。 同样 175 次迭代,超快退火在 4 个测试代理上各跑 43-44 次,20 次结果均值 1653.735,触到最佳的 67.6%;若只塞进单代理跑满 175 次,均值还能抬到 1731.244(最佳的 70.8%)。差距不算爆炸,但方向明确:小迭代池里超快退火咬得更紧。 四变量扩展测试(加衰减因子 0.02-0.2、最大风控 3-30)时,GA 用 4870 次迭代交出最好 32782.91。超快退火初设 p1=0.75、p2=1 只跑出 26676.22,表现偏弱;把 p1 拉到 2 并令 CoeffOfTemp=4 缓住首步骤降(n=1 时 T 仅剩 T0*0.558),结果跳到 39145.25,反超 GA。温度公式 T0*exp(-p1*exp(-p2/4)*n^0.25) 里 p1、CoeffOfTemp 这两个旋钮,直接决定它会不会在前期把解空间探死。 外汇与贵金属优化本质是高风险的参数拟合游戏,过拟合随时可能发生,上述数字只是在该历史片段上的概率性优势,换品种或周期未必复现。开 MT5 把下面这段挂到测试器里,调 p1、p2 和 CoeffOfTemp 就能自己复跑。

MQL5 / C++
class="macro">#class="kw">property tester_file "data.bin"
class=class="str">"cmt">// 包含类
class="macro">#include <AnnealingMethod.mqh>
class="macro">#include <FrameAnnealingMethod.mqh>
AnnealingMethod Optim;
FrameAnnealingMethod Frame;
Input InputMass[];                 class=class="str">"cmt">// 输入参数数组
class="type">class="kw">string SParams[];                  class=class="str">"cmt">// 输入参数名称数组
class="type">class="kw">double Fopt=class="num">0;                     class=class="str">"cmt">// 函数的最佳值
class="type">int it_agent=class="num">0;                    class=class="str">"cmt">// 用于测试代理的算法迭代次数
class="type">uint alg_err=class="num">0;                    class=class="str">"cmt">// 出错次数
class="type">class="kw">double MaximumRisk_Optim=MaximumRisk;
class="type">class="kw">double DecreaseFactor_Optim=DecreaseFactor;
class="type">int MovingPeriod_Optim=MovingPeriod;
class="type">int MovingShift_Optim=MovingShift;
sinput class="type">int iteration=class="num">50;           class=class="str">"cmt">// 迭代次数
sinput class="type">int method=class="num">0;               class=class="str">"cmt">// class="num">0 - 玻尔兹曼退火, class="num">1 - 柯西退火, class="num">2 - 超快退火
sinput class="type">class="kw">double CoeffOfTemp=class="num">1;       class=class="str">"cmt">// 初始温度的比例系数
sinput class="type">class="kw">double CoeffOfMinTemp=class="num">0;    class=class="str">"cmt">// 最低温度系数
sinput class="type">class="kw">double Func0=-class="num">10000;        class=class="str">"cmt">// 目标函数的初始值
sinput class="type">class="kw">double P1=class="num">1;                class=class="str">"cmt">// 超快退火的附加参数, p1
sinput class="type">class="kw">double P2=class="num">1;                class=class="str">"cmt">// 超快退火的附加参数, p2
sinput class="type">int Crit=class="num">0;                 class=class="str">"cmt">// 目标函数计算方法
sinput class="type">int ModOfAlg=class="num">0;             class=class="str">"cmt">// 算法修改类型
sinput class="type">bool ManyPoint=class="kw">false;       class=class="str">"cmt">// 多点优化
class=class="str">"cmt">//+------------------------------------------------------------------+

「优化模式下退火参数的载入与分发」

在 MT5 策略测试器开启优化(MQL5_OPTIMIZATION 为真)时,EA 不会走常规初始化,而是先调用 Optim.ReadData 从 data.bin 读回 InputMass、Fopt 与 it_agent。若返回码 alg_err 不为 0,直接 Print("读文件错误") 并以 INIT_FAILED 退出,避免拿空参数跑优化。 当 Fopt 等于 Func0 且 ManyPoint 为真,说明是多点位首次搜索,代码对 InputMass 每个元素用 UniformValue 在 Start~Stop 按 Step 均匀撒随机初值,并把 BestValue 先等于 Value。若不是首次,则走 Optim.GetParams 按当前 method 与 CoeffOfMinTemp 生成新一轮退火参数。 随后一个 for+switch 把 InputMass 的数值塞回实盘变量:索引 0→MaximumRisk_Optim,1→DecreaseFactor_Optim,2→MovingPeriod_Optim(强转 int),3→MovingShift_Optim(强转 int)。你在 MT5 里改这四个输入项的 Start/Stop/Step,就能直接控制退火搜索空间宽度,外汇与贵金属品种参数敏感度高,误设步长可能让优化耗时翻倍且过拟合概率上升。

MQL5 / C++
if(MQL5InfoInteger(MQL5_OPTIMIZATION))
  {
   class=class="str">"cmt">// 打开文件并读取数据
   class=class="str">"cmt">//  if(FileGetInteger("data.bin",FILE_EXISTS,class="kw">false))
   class=class="str">"cmt">//  {
        alg_err=Optim.ReadData(InputMass,Fopt,it_agent);
        if(alg_err==class="num">0)
          {
           class=class="str">"cmt">// 若是第一次运行, 如果从不同点位执行搜索, 则随机生成参数
           if(Fopt==Func0)
             {
              if(ManyPoint)
                for(class="type">int i=class="num">0;i<ArraySize(InputMass);i++)
                  {
                   InputMass[i].Value=Optim.UniformValue(InputMass[i].Start,InputMass[i].Stop,InputMass[i].Step);
                   InputMass[i].BestValue=InputMass[i].Value;
                  }
             }
           else
             Optim.GetParams(method,InputMass,CoeffOfMinTemp);    class=class="str">"cmt">// generate new parameters
           class=class="str">"cmt">// 填充智能交易系统的参数
           for(class="type">int i=class="num">0;i<ArraySize(InputMass);i++)
             class="kw">switch(InputMass[i].num)
               {
                case (class="num">0): {MaximumRisk_Optim=InputMass[i].Value; class="kw">break;}
                case (class="num">1): {DecreaseFactor_Optim=InputMass[i].Value; class="kw">break;}
                case (class="num">2): {MovingPeriod_Optim=(class="type">int)InputMass[i].Value; class="kw">break;}
                case (class="num">3): {MovingShift_Optim=(class="type">int)InputMass[i].Value; class="kw">break;}
               }
          }
        else
          {
           Print("读文件错误");
           class="kw">return(INIT_FAILED);
          }
  }
if(MQL5InfoInteger(MQL5_OPTIMIZATION))
alg_err=Optim.ReadData(InputMass,Fopt,it_agent);

模拟退火在EA参数优化里的落地骨架

这段逻辑把模拟退火算法接进了 MT5 的测试器生命周期,核心是两个回调:OnTesterInit 做初始化并拉起优化,OnTester 在每个回测步里更新温度与最优解。 OnTesterInit 里先给 SParams 数组定长 4,把 MaximumRisk、DecreaseFactor、MovingPeriod、MovingShift 四个外部参数名塞进去,再调用 RunOptimization 启动。也就是说,你想换优化目标,至少得先改这组参数名和数组长度。 OnTester 的收敛判断很直接:遍历 InputMass,统计温度已低于 CoeffOfMinTemp 乘距离的参数个数,全部达标就往图形帧丢一条 id=-1 的结束帧。否则算当前目标函数值 Fnew,按 Crit 编号决定最大化还是最小化——注意 Crit 为 5、6、11、12 时是最小化分支,其余走最大化。 温度更新受 ModOfAlg 控制:模式 0 下仅当 Fnew==Fopt 才降温,模式 1 每步都降。降温调用 GetT,传入的系数是 CoeffOfTemp 乘参数区间距离,再除以当前温度做归一。外汇与贵金属行情跳空频繁,这类全局优化结果在样本外可能明显衰减,实盘前务必用不同年份数据重验。

MQL5 / C++
Optim.GetParams(method,InputMass,CoeffOfMinTemp;class=class="str">"cmt">// 生成新参数
class="type">void OnTesterInit()
  {
  class=class="str">"cmt">// 填充所有 EA 参数的名称数组
   ArrayResize(SParams,class="num">4);
   SParams[class="num">0]="MaximumRisk";
   SParams[class="num">1]="DecreaseFactor";
   SParams[class="num">2]="MovingPeriod";
   SParams[class="num">3]="MovingShift";
   class=class="str">"cmt">// 开始优化
   Optim.RunOptimization(SParams,iteration,Func0,CoeffOfTemp);
   class=class="str">"cmt">// 创建图形界面
   Frame.FrameInit(SParams);
  }
class="type">class="kw">double OnTester()
  {
   class="type">int i=class="num">0;                                                       class=class="str">"cmt">// 循环计数器
   class="type">int count=class="num">0;                                                   class=class="str">"cmt">// 辅助变量
  class=class="str">"cmt">// 当达到最低温度时检查算法是否完成
   for(i=class="num">0;i<ArraySize(InputMass);i++)
      if(InputMass[i].Temp<CoeffOfMinTemp*Optim.Distance(InputMass[i].Start,InputMass[i].Stop))
         count++;
   if(count==ArraySize(InputMass))
      Frame.FrameTester(class="num">0,class="num">0,InputMass,-class="num">1,it_agent);               class=class="str">"cmt">// 添加一个零参数的帧, 且 id=-class="num">1
   else
     {
      class="type">class="kw">double Fnew=Optim.GetFunction(Crit);                        class=class="str">"cmt">// 计算函数的当前值
      if((Crit!=class="num">5) && (Crit!=class="num">6) && (Crit!=class="num">11) && (Crit!=class="num">12))      class=class="str">"cmt">// 如果有必要最大化目标函数
        {
         if(Fnew>Fopt)
            Fopt=Fnew;
         else
           {
            if(Optim.Probability(Fopt-Fnew,CoeffOfTemp*InputMass[class="num">0].Temp/Optim.Distance(InputMass[class="num">0].Start,InputMass[class="num">0].Stop)))
               Fopt=Fnew;
           }
        }
      else                                                        class=class="str">"cmt">// 如果有必要最小化目标函数
        {
         if(Fnew<Fopt)
            Fopt=Fnew;
         else
           {
            if(Optim.Probability(Fnew-Fopt,CoeffOfTemp*InputMass[class="num">0].Temp/Optim.Distance(InputMass[class="num">0].Start,InputMass[class="num">0].Stop)))
               Fopt=Fnew;
           }
        }
      class=class="str">"cmt">// 覆盖最佳参数值
      if(Fopt==Fnew)
         for(i=class="num">0;i<ArraySize(InputMass);i++)
            InputMass[i].BestValue=InputMass[i].Value;
      class=class="str">"cmt">// 降低温度
      if(((ModOfAlg==class="num">0) && (Fnew==Fopt)) || (ModOfAlg==class="num">1))
        {
         for(i=class="num">0;i<ArraySize(InputMass);i++)
            InputMass[i].Temp=Optim.GetT(method,CoeffOfTemp*Optim.Distance(InputMass[i].Start,InputMass[i].Stop),InputMass[i].Temp,it_agent,ArraySize(InputMass),P1,P2);
        }
      Frame.FrameTester(Fnew,Fopt,InputMass,iteration,it_agent);          class=class="str">"cmt">// 添加新帧

◍ 把优化结果落盘并接管测试器事件

这段片段承接了前文遗传迭代的主循环,核心是把每一代算出的输入参数和适应度写回文件,并在测试器生命周期里接管图形帧的刷新与收尾。 迭代计数器 it_agent 自增后,调用 Optim.WriteData(InputMass,Fopt,it_agent) 把当前输入质量矩阵、目标函数值和代次编号一并写入数据文件;若返回码非 0 立即 return 把错误码抛出,避免脏数据继续参与后续代际。 主循环外先把 InputMass 每个元素的 BestValue 赋为初始 Value,相当于给最优解缓存做冷启动。OnTesterPass 里用 Frame.FrameTesterPass(Crit) 把每帧结果推到图形界面;OnTesterDeinit 中 Frame.FrameToFile(4) 把第 4 类帧导出为文件再释放资源;OnChartEvent 则把图表交互事件透传给 Frame.FrameOnChartEvent 处理。 直接在 MT5 策略测试器里挂这段回调,跑完优化后打开生成的帧文件,就能看到每一代参数分布和收敛轨迹,比只看最终报告更有用。外汇与贵金属品种点差跳空频繁,这类回测结论只代表历史样本,实盘存在较高风险。

MQL5 / C++
  it_agent++;
                                                         class=class="str">"cmt">// 增加迭代计数器
   alg_err=Optim.WriteData(InputMass,Fopt,it_agent);
                   class=class="str">"cmt">// 将新数值写入文件
   if(alg_err!=class="num">0)
     class="kw">return alg_err;
   }
  class="kw">return Fopt;
 }
for(i=class="num">0;i<ArraySize(InputMass);i++)
       InputMass[i].BestValue = InputMass[i].Value;
class="type">void OnTesterPass()
  {
    Frame.FrameTesterPass(Crit);class=class="str">"cmt">// 方法显示图形界面中的帧
  }
class="type">void OnTesterDeinit()
  {
   Frame.FrameToFile(class="num">4);
   Frame.FrameDeinit();
  }
class="type">void OnChartEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam)
  {
   Frame.FrameOnChartEvent(id,lparam,dparam,sparam,Crit); class=class="str">"cmt">// 处理图形界面的方法
  }

「把这条线请下神坛」

超快退火在三类实现里跑出了最佳结果,但模拟退火终究不是云端可测、即插即用的银弹。它不能在 MT5 云测试器里跑,和智能系统的集成也得手动挑参数,这两个坑靠通用模块才有可能填平。 作者留下的附件里,AnnealingMethod.mqh 是 31.58 KB 的核心类,FrameAnnealingMethod.mqh 管终端图形界面,test.zip 里 TestAnnealing.mq5 能直接加载数据回放算法运算。想验证超快退火是否真比遗传算法省迭代,把这些文件丢进 MQL5/Include 和 Images 目录,开本地策略测试器跑一遍就知道了。 外汇与贵金属优化测试的高风险不变:历史回测占优不代表实盘概率倾斜,参数空间每扩一层,过拟合可能就多一分。

让小布替你跑这套退火扫描
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到不同优化路径下的参数稳定性分布,把重复劳动交给小布,你专注决策。

常见问题

核心区别是新点位生成方式和温度下降规则;柯西退火步长更重尾、易跳出局部,超快退火降温更陡,新手可先从柯西入手看收敛表现。
小布盯盘内置了多路径参数诊断视图,可对比退火类与遗传类优化的分布差异,省去手动反复跑测试器的繁琐,但交易决策仍由你判断,外汇贵金属属高风险。
其目标函数形态相对清晰、参数少,能直观暴露全局与局部极值陷阱,便于对照遗传算法结果衡量退火搜索效率。
可能概率性跳转不足,算法过早收敛到局部极值;倾向用预扫描估算目标函数跨度再定T0,而非拍脑袋给常数。
无绝对优劣;退火在避开局部极值上概率更稳,遗传在维数高时可能更快,具体看EA目标地形,建议交叉验证。