可控优化: 模拟退火·进阶篇
🌡️

可控优化: 模拟退火·进阶篇

(2/3)·遗传算法总卡在局部极值?用温度衰减逼出全局最优的参数组合

含代码示例实战向 第 2/3 篇
多数交易者把策略测试器的遗传优化当默认终点,却没意识到它早停在局部洼地。模拟退火靠温度概率跳出陷阱,只是集成门槛劝退了一批人。本篇把算法拆成可抄的 EA 模块,让你直接替换原生的优化器。

退火采样里的温度阈值与越界重试

这段代码实现的是模拟退火算法里针对单组参数的取值逻辑,核心在 GetParams 方法。Method=0 时,只要当前温度 Temp 不低于 CoeffTmin 乘以起止区间距离,就持续用正态扰动生成候选值。 扰动幅度由 Mass[i].Temp * HQRndNormal 决定,每轮 count 累加,一旦 count 达到 100 仍没采到合法值,就直接回退到 Mass[i].Value 并清零计数器跳出。这个 100 次的上限是防止死循环的真实护栏。 候选值必须过 VerificationOfVal 的起止区间校验,通不过就重采;旧代码里注释掉的 while 写法是直接卡边界,现在换成封装函数更灵活。外汇与贵金属参数优化属高风险操作,这类采样结果仅代表历史空间内的可能分布,不等于未来稳健。

MQL5 / C++
class="type">bool AnnealingMethod::GetParams(class="type">int Method,Input &Mass[],class="type">class="kw">double CoeffTmin)
  {
   class="type">class="kw">double delta=class="num">0;
   class="type">class="kw">double x1=class="num">0,x2=class="num">0;
   class="type">class="kw">double count=class="num">0;
   Alg.HQRndRandomize(&state);         class=class="str">"cmt">// 初始化
   class="kw">switch(Method)
     {
      case(class="num">0):
        {
         for(class="type">int i=class="num">0;i<ArraySize(Mass);i++)
           {
            if(Mass[i].Temp>=CoeffTmin*Distance(Mass[i].Start,Mass[i].Stop))
              {
               do
                 {
                  if(count==class="num">100)
                    {
                     delta=Mass[i].Value;
                     count= class="num">0;
                     class="kw">break;
                    }
                  count++;
                  delta=Mass[i].Temp*Alg.HQRndNormal(&state);
                  delta=FindValue(Mass[i].BestValue+delta,Mass[i].Step);
                 }
               class=class="str">"cmt">//  class="kw">while((delta<Mass[i].Start) || (delta>Mass[i].Stop));
               class="kw">while(!VerificationOfVal(Mass[i].Start,Mass[i].Stop,delta));
               Mass[i].Value=delta;
              }
           }
         class="kw">break;
        }
      case(class="num">1):
        {
         for(class="type">int i=class="num">0;i<ArraySize(Mass);i++)
           {
            if(Mass[i].Temp>=CoeffTmin*Distance(Mass[i].Start,Mass[i].Stop))
              {
               do
                 {
                  if(count==class="num">100)

「模拟退火里的两种扰动采样」

在优化器的 case(1) 与 case(2) 分支里,核心差异在于扰动 delta 的随机采样方式。case(1) 用正态组合 HQRndNormal2 生成 x1、x2,再让 delta = Temp*x1/x2,属于对称厚尾扰动,适合在温度尚高时大范围跳出局部极值。

case(2) 改用均匀采样 HQRndUniformR,并以 (1+1/Temp)^2*x1-1- 1 为幅度系数乘以区间距离 Distance(Start,Stop)。当 x1 大于 0.5 时 delta 为正、小于 0.5 为负,等于 0.5 则为零位移,这种单边指数型伸缩比正态更省计算。

两个分支都内置了 count==100 的兜底:连续 100 次未通过 VerificationOfVal 边界校验时,直接沿用上轮 Mass[i].Value 并重置计数器后 break,避免无限循环。外汇与贵金属品种点差跳动大,跑这套前建议把 CoeffTmin 调小一档,否则 Temp 过早低于阈值会令搜索过早冻结,属高风险调参。

MQL5 / C++
            {
               delta=Mass[i].Value;
               count=class="num">0;
               class="kw">break;
              }
             count++;
             Alg.HQRndNormal2(&state,x1,x2);
             delta=Mass[i].Temp*x1/x2;
             delta=FindValue(Mass[i].BestValue+delta,Mass[i].Step);
            }
          class="kw">while(!VerificationOfVal(Mass[i].Start,Mass[i].Stop,delta));
          Mass[i].Value=delta;
         }
       }
      class="kw">break;
     }
    case(class="num">2):
     {
      for(class="type">int i=class="num">0;i<ArraySize(Mass);i++)
       {
        if(Mass[i].Temp>=CoeffTmin*Distance(Mass[i].Start,Mass[i].Stop))
         {
          do
           {
            if(count==class="num">100)
             {
              delta=Mass[i].Value;
              count=class="num">0;
              class="kw">break;
             }
            count++;
            x1=Alg.HQRndUniformR(&state);
            if(x1-class="num">0.5>class="num">0)
              delta=Mass[i].Temp*(MathPow(class="num">1+class="num">1/Mass[i].Temp,MathAbs(class="num">2*x1-class="num">1))-class="num">1)*Distance(Mass[i].Start,Mass[i].Stop);
            else
             {
              if(x1==class="num">0.5)
               delta=class="num">0;
              else
               delta=-Mass[i].Temp*(MathPow(class="num">1+class="num">1/Mass[i].Temp,MathAbs(class="num">2*x1-class="num">1))-class="num">1)*Distance(Mass[i].Start,Mass[i].Stop);
             }

◍ 退火寻优里的取值吸附与回测指标钩子

上面这段是模拟退火参数搜索里两个容易被忽略的底层函数。FindValue 干的事是把连续扰动后的值『吸附』到步长网格上:当 step=1 时直接 round 取整,否则按离 val 最近的 step 整数倍返回,且四舍五入边界取 step/2。 [CODE] double AnnealingMethod::FindValue(double val,double step) { double buf=0; if(val==step) return val; if(step==1) return round(val); else { buf=(MathAbs(val)-MathMod(MathAbs(val),MathAbs(step)))/MathAbs(step); if(MathAbs(val)-buf*MathAbs(step)>=MathAbs(step)/2) { if(val<0) return -(buf + 1)*MathAbs(step); else return (buf + 1)*MathAbs(step); } else { if(val<0) return -buf*MathAbs(step); else return buf*MathAbs(step); } } } [/CODE] 逐行拆:buf 先算出 val 包含多少个完整 step;若余数 ≥ step 的一半就向上进一档,负数则对称取负。这样能保证每次退火迭代后的候选参数不漂在网格外,MT5 优化器跑自定义品种时不会因浮点抖动重复评估近似解。 GetFunction 则是把优化目标直接绑到 MT5 回测统计上:case 0 取 STAT_PROFIT(净利),1 取 STAT_PROFIT_FACTOR(盈利因子),2 取 STAT_RECOVERY_FACTOR(恢复因子),3 取 STAT_SHARPE_RATIO(夏普)。改这几个 case 的返回值,就能让同一套退火框架在『追收益』和『追稳健』之间切换。外汇与贵金属杠杆高,回测夏普好看不代表实盘能复现,参数过拟合概率偏大。 [CODE] double AnnealingMethod::GetFunction(int Criterion) { double Fc=0; switch(Criterion) { case(0): return TesterStatistics(STAT_PROFIT); case(1): return TesterStatistics(STAT_PROFIT_FACTOR); case(2): return TesterStatistics(STAT_RECOVERY_FACTOR); case(3): return TesterStatistics(STAT_SHARPE_RATIO); [/CODE] 开 MT5 把 Criterion 设成 3 跑一遍 EURUSD 的 H1 历史,看夏普是不是比默认净利目标收敛得更慢但曲线回撤更浅,这能帮你判断该用哪条标准喂给退火。

MQL5 / C++
class="type">class="kw">double AnnealingMethod::FindValue(class="type">class="kw">double val,class="type">class="kw">double step)
  {
  class="type">class="kw">double buf=class="num">0;
  if(val==step)
    class="kw">return val;
  if(step==class="num">1)
    class="kw">return round(val);
  else
    {
    buf=(MathAbs(val)-MathMod(MathAbs(val),MathAbs(step)))/MathAbs(step);
    if(MathAbs(val)-buf*MathAbs(step)>=MathAbs(step)/class="num">2)
      {
      if(val<class="num">0)
        class="kw">return -(buf + class="num">1)*MathAbs(step);
      else
        class="kw">return (buf + class="num">1)*MathAbs(step);
      }
    else
      {
      if(val<class="num">0)
        class="kw">return -buf*MathAbs(step);
      else
        class="kw">return buf*MathAbs(step);
      }
    }
  }
class="type">class="kw">double AnnealingMethod::GetFunction(class="type">int Criterion)
  {
  class="type">class="kw">double Fc=class="num">0;
  class="kw">switch(Criterion)
    {
    case(class="num">0):
      class="kw">return TesterStatistics(STAT_PROFIT);
    case(class="num">1):
      class="kw">return TesterStatistics(STAT_PROFIT_FACTOR);
    case(class="num">2):
      class="kw">return TesterStatistics(STAT_RECOVERY_FACTOR);
    case(class="num">3):
      class="kw">return TesterStatistics(STAT_SHARPE_RATIO);

把回测统计量拼成优化判分函数

在 MT5 策略测试器的自定义优化里,可以给不同 case 编号返回不同的 TesterStatistics 组合,让遗传或退火算法按你定义的分数挑参数。下面这段 switch 把 4~13 号映射成了期望盈利、权益/余额回撤、以及利润乘以各类风险调整系数的复合指标。 case(4) 直接返回 STAT_EXPECTED_PAYOFF(每笔期望收益);case(5)(6) 返回 STAT_EQUITY_DD 与 STAT_BALANCE_DD 的最小值类回撤;case(7)~(10) 则是用 STAT_PROFIT 分别乘 PROFIT_FACTOR、RECOVERY_FACTOR、SHARPE_RATIO、EXPECTED_PAYOFF,把绝对利润和风险效率揉成一个数。 case(11)(12) 做了除零保护:当余额或权益回撤大于 0 时才用利润除以回撤,否则原样返回利润;case(13) 示范了自定义标准——成交笔数乘利润。default 返回 -10000 代表无效解。 退火接受概率用 HQRndUniformR 取均匀随机数 a,再比 exp(-E/T) 决定是否接受较差解;T 越高越容易跳出局部最优。外汇与贵金属杠杆高,回测甜美的判分函数在实盘仍可能快速失效,调参前先在历史分段里跑一遍。

MQL5 / C++
   case(class="num">4):
      class="kw">return TesterStatistics(STAT_EXPECTED_PAYOFF);
   case(class="num">5):
      class="kw">return TesterStatistics(STAT_EQUITY_DD);class=class="str">"cmt">//min
   case(class="num">6):
      class="kw">return TesterStatistics(STAT_BALANCE_DD);class=class="str">"cmt">//min
   case(class="num">7):
      class="kw">return TesterStatistics(STAT_PROFIT)*TesterStatistics(STAT_PROFIT_FACTOR);
   case(class="num">8):
      class="kw">return TesterStatistics(STAT_PROFIT)*TesterStatistics(STAT_RECOVERY_FACTOR);
   case(class="num">9):
      class="kw">return TesterStatistics(STAT_PROFIT)*TesterStatistics(STAT_SHARPE_RATIO);
   case(class="num">10):
      class="kw">return TesterStatistics(STAT_PROFIT)*TesterStatistics(STAT_EXPECTED_PAYOFF);
   case(class="num">11):
      {
      if(TesterStatistics(STAT_BALANCE_DD)>class="num">0)
         class="kw">return TesterStatistics(STAT_PROFIT)/TesterStatistics(STAT_BALANCE_DD);
      else
         class="kw">return TesterStatistics(STAT_PROFIT);
      }
   case(class="num">12):
      {
      if(TesterStatistics(STAT_EQUITY_DD)>class="num">0)
         class="kw">return TesterStatistics(STAT_PROFIT)/TesterStatistics(STAT_EQUITY_DD);
      else
         class="kw">return TesterStatistics(STAT_PROFIT);
      }
   case(class="num">13):
      {
      class=class="str">"cmt">// 例如, 指定自定义标准
      class="kw">return TesterStatistics(STAT_TRADES)*TesterStatistics(STAT_PROFIT);
      }
   class="kw">default: class="kw">return -class="num">10000;
   }
}
class="type">bool AnnealingMethod::Probability(class="type">class="kw">double E,class="type">class="kw">double T)
  {
  class="type">class="kw">double a=Alg.HQRndUniformR(&state);
  class="type">class="kw">double res=exp(-E/T);
  if(res<=a)
      class="kw">return class="kw">false;
  else
      class="kw">return true;
  }
a=Alg.HQRndUniformR(&state);

「退火温度与均匀采样的代码骨架」

模拟退火里温度调度直接决定参数寻优能否跳出局部极值。下面这段 GetT 用三种降温法(Method 0/1/2)把当前迭代映射成温度 T,T0 是初始高温、Tlast 是上一步温度,D、p1、p2 控制衰减曲率。 Method 0 走对数降温:当 Tlast 不等于 T0 时,先由 exp(T0/Tlast)-1 反推已迭代次数 Iteration,再令 T = T0 / log(Iteration+1);若两者相等则 Iteration 强制为 1,避免除零。Method 1 是幂律降温,Iteration 由 (T0/Tlast)^D 取整得来,T = T0 / Iteration^(1/D),D 越大降温越慢。 Method 2 套了指数-指数复合衰减:Iteration 依赖 log(Tlast/T0) 除以 (-p1*exp(-p2/D)) 后再取 D 次幂,T 回算用 T0*exp(-p1*exp(-p2/D)*Iteration^(1/D))。三个分支都做了 Iteration<=0 的兜底,直接回退 T0。 UniformValue 负责在 [min,max] 内按步长 step 均匀取数:每次先 HQRndRandomize 重播种,再用 HQRndUniformR 生成 0~1 随机数线性映射到区间,最后 FindValue 对齐步长网格。VerificationOfVal 则是边界校验,start<end 时判断 val 是否落在区间内。 外汇与贵金属杠杆高、滑点随机,这类寻优逻辑回测和实盘偏差可能很大,上 MT5 跑之前先把 D 和 p1/p2 用手工网格搜一遍再接退火。

MQL5 / C++
class="type">class="kw">double AnnealingMethod::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">int Iteration=class="num">0;
   class="type">class="kw">double T=class="num">0;
   class="kw">switch(Method)
     {
      case(class="num">0):
        {
         if(Tlast!=T0)
            Iteration=(class="type">int)MathRound(exp(T0/Tlast)-class="num">1)+class="num">1;
         else
            Iteration=class="num">1;
         if(Iteration>class="num">0)
            T=T0/log(Iteration+class="num">1);
         else
            T=T0;
         class="kw">break;
        }
      case(class="num">1):
        {
         if(it!=class="num">1)
            Iteration=(class="type">int)MathRound(pow(T0/Tlast,D))+class="num">1;
         else
            Iteration=class="num">1;
         if(Iteration>class="num">0)
            T=T0/pow(Iteration,class="num">1/D);
         else
            T=T0;
         class="kw">break;
        }
      case(class="num">2):
        {
         if((T0!=Tlast) && (-p1*exp(-p2/D)!=class="num">0))
            Iteration=(class="type">int)MathRound(pow(log(Tlast/T0)/(-p1*exp(-p2/D)),D))+class="num">1;
         else
            Iteration=class="num">1;
         if(Iteration>class="num">0)
            T=T0*exp(-p1*exp(-p2/D)*pow(Iteration,class="num">1/D));
         else
            T=T0;
         class="kw">break;
        }
     }
   class="kw">return T;
  }
class="type">class="kw">double AnnealingMethod::UniformValue(class="type">class="kw">double min,class="type">class="kw">double max,class="type">class="kw">double step)
  {
   Alg.HQRndRandomize(&state);       class=class="str">"cmt">//初始化
   if(max>min)
      class="kw">return FindValue(Alg.HQRndUniformR(&state)*(max-min)+min,step);
   else
      class="kw">return FindValue(Alg.HQRndUniformR(&state)*(min-max)+max,step);
  }
class="type">bool AnnealingMethod::VerificationOfVal(class="type">class="kw">double start,class="type">class="kw">double end,class="type">class="kw">double val)
  {
   if(start<end)
     {
      if((val>=start) && (val<=end))
         class="kw">return true;
      else

◍ 把退火优化过程搬上图表控件

这段收尾代码把模拟退火的计算逻辑接到了 MT5 的 GUI 控件上,核心是一个 FrameAnnealingMethod 类,专门负责把每一代优化结果画到测试器图表。它内部挂了三张 CSimpleTable(t_value / t_inputs / t_stat)分别存目标值、输入向量和统计信息,还塞了四个 CBmpButton 做播放、后退、前进、停止的帧控制。 Distance 方法只是个绝对值封装:a<b 时返回 MathAbs(b-a),否则返回 MathAbs(a-b),用来算参数空间里的欧氏距离分量。注意前面未贴全的 CheckRange 逻辑里,val 落在 [start,end] 或反向区间会返回 true,否则 false——这是退火里约束边界的硬判断。 类声明里 frame_counter 用 long 计数,FrameTester 每次传入当前 F、历史最优 Fbest、输入质量数组 Mass[] 和迭代号 it。你在 MT5 里把 AnnealingMethod.mqh 和 SimpleTable.mqh 一并 include 后,直接实例化这个类就能在策略测试器逐帧回看参数游走轨迹,贵金属与外汇品种回测请留意点差滑点带来的高波动风险。

MQL5 / C++
   class="kw">return class="kw">false;
   }
  else
   {
     if((val>=end) && (val<=start))
       class="kw">return true;
     else
       class="kw">return class="kw">false;
   }
  }
class="type">class="kw">double AnnealingMethod::Distance(class="type">class="kw">double a,class="type">class="kw">double b)
  {
   if(a<b)
     class="kw">return MathAbs(b-a);
   else
     class="kw">return MathAbs(a-b);
  }
class="macro">#include <SimpleTable.mqh>
class="macro">#include <Controls\BmpButton.mqh>
class="macro">#include <Controls\Label.mqh>
class="macro">#include <Controls\Edit.mqh>
class="macro">#include <AnnealingMethod.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 类用于优化结果的输出                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class FrameAnnealingMethod
  {
class="kw">private:
  CSimpleTable       t_value;
  CSimpleTable       t_inputs;
  CSimpleTable       t_stat;
  CBmpButton         b_playbutton;
  CBmpButton         b_backbutton;
  CBmpButton         b_forwardbutton;
  CBmpButton         b_stopbutton;
  CLabel             l_speed;
  CLabel             l_stat;
  CLabel             l_value;
  CLabel             l_opt_value;
  CLabel             l_temp;
  CLabel             l_text;
  CLabel             n_frame;
  CEdit              e_speed;
  class="type">long               frame_counter;
class="kw">public:
  class=class="str">"cmt">//--- 构造函数/析构函数
                     FrameAnnealingMethod();
                    ~FrameAnnealingMethod();
  class=class="str">"cmt">//--- 策略测试器的事件
  class="type">void               FrameTester(class="type">class="kw">double F,class="type">class="kw">double Fbest,Input &Mass[],class="type">int num,class="type">int it);
  class="type">void               FrameInit(class="type">class="kw">string &SMass[]);
  class="type">void               FrameTesterPass(class="type">int cr);
  class="type">void               FrameDeinit(class="type">void);
  class="type">void               FrameOnChartEvent(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,class="type">int cr);
  class="type">uint               FrameToFile(class="type">int count);
  };
让小布替你跑这套温度曲线
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到退火收敛与遗传对比的实时面板,把重复劳动交给小布,你专注决策。

常见问题

没有通用值,取决于目标函数尺度;通常取让首轮接受概率在 0.8 以上的量级,再用小样本回测收缩。
差异主要在扰动分布与降温速率,柯西尾部更厚、易逃局部,代码上只改随机生成与 T 递减两处。
可以,小布的品种页支持导入外部参数集并标注来源算法,方便你横向比对遗传与退火的实际表现。
温度逼近 Tmin 时接受劣解概率趋零,算法在收敛;若过早锁死,多半是降温过快或 Imax 不够。
历史拟合优不等于实盘稳健,贵金属跳空与外盘流动性突变可能让最优参数失效,需做样本外压力测试。