📘

「用差分进化给 EA 找参数」

「用差分进化给 EA 找参数」

差分进化(DE)是一类群体优化算法,核心思路是维护一组候选解,通过对个体做差分变异、交叉、选择来迭代逼近更优解。在 MT5 里,它常被用来替代网格遍历,给 EA 的海量参数组合做降维搜索。 原文给出的示例发布于 2024 年 7 月 8 日,作者 Andrey Dik,该帖在统计时点获得 1,108 次查看、3 条评论,内容结构分为概述、算法、测试结果三部分。 对外汇与贵金属这类高波动、高杠杆品种,任何优化结果都只是历史样本上的概率倾向,实盘前务必在 MT5 策略测试器做样本外验证。

黑盒优化里的进化与差分思路

元启发式优化不依赖目标函数的梯度或导数,靠随机概率在含大量局部最优、甚至不连续可微的搜索空间里找全局解。这对交易系统调参很实用:很多指标组合或 EA 逻辑就是算不出解析式的“黑盒子”,传统数值法直接失效。 进化算法(EA)模拟自然选择,用遗传、变异、交叉让群体中更适应的解存活并传递特征,逐代逼近最优。常见分支有遗传算法 GA、进化编程 EP、进化策略 ES、遗传编程 GP,都能在拿不到梯度时探索解空间。 差分进化(DE)是其中更轻量的一种,Storn 和 Price 在 1990 年代提出,靠向量群体的变异与杂交生成新解,不需要梯度知识。它结构简单、寻优效率偏高,是 MT5 参数优化里值得直接试的一类方法。

◍ DE 优化器的向量结构与类骨架

差分进化(DE)把待优化问题压成一群 n 维向量,每个向量就是一组参数候选。变异用 r = r1 + F*(r2 - r3) 生成新候选,F 默认落在 (0.0,1.0],控制分散度;交叉按概率 crossProbab 在 (0.0,1.0] 内替换坐标。迭代跑到适应度函数调用满 10,000 次才停,这是实盘调参时很实在的算力边界。 底层先用 S_Vector 结构装单向量:Init 开两份数组 c / cPrev 存当前与上轮坐标,f 和 fPrev 都预置 -DBL_MAX,意味着初始个体被视为“极差解”。C_AO_DE 类则管全局状态,字段含 cB[] 最优坐标、v[] 种群、rangeMax/Min/Step[] 限定每维搜索边界与步长。 Init 方法先用 MathSrand 重置随机序列,保证每次启动种子不同;fB 设 -DBL_MAX、revision 设 false 标记种群未修订。随后按入参 coordsP、popSizeP、diffWeightP、crossProbabP 落定维度、规模、差分权重与交叉概率,并开对应长度数组。 Moving 首次跑才随机撒点,之后对每个 i 选三个互异的 r1/r2/r3 做变异交叉;但注意它只从现有坐标线性组合,不凭空造新原始坐标,随机性仅来自交叉选择。Revision 扫一遍种群,若某向量 f 大于 fB 就记索引进 cB,再统一把改进者的 f/fPrev、c/cPrev 回写,留痕给后续适应度复用。 别把正态当圣经 DE 的随机性薄弱点在 Moving:群体不生成全新基向量,只到找出优于父代的解才往前推。外汇与贵金属参数寻优属高风险实验,MT5 里跑这套前先想清楚搜索步长会不会卡在局部。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span>
<span class="keyword">class="kw">struct</span> S_Vector
{
&nbsp;&nbsp;<span class="keyword">class="type">void</span> Init(<span class="keyword">class="type">int</span> coords)
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayResize</span> (c,&nbsp;&nbsp;&nbsp;&nbsp; coords);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayResize</span> (cPrev, coords);
&nbsp;&nbsp;&nbsp;&nbsp;f&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= -<span class="macro">DBL_MAX</span>;
&nbsp;&nbsp;&nbsp;&nbsp;fPrev&nbsp;&nbsp;&nbsp;&nbsp;= -<span class="macro">DBL_MAX</span>;
&nbsp;&nbsp;}
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> c&nbsp;&nbsp;&nbsp;&nbsp; []; <span class="comment">class=class="str">"cmt">//coordinates</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> cPrev []; <span class="comment">class=class="str">"cmt">//previous coordinates</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> f;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//fitness</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> fPrev;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//previous fitness</span>
};
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span>
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span>
<span class="keyword">class</span> C_AO_DE
{
&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//----------------------------------------------------------------------------</span>
&nbsp;&nbsp;<span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> cB&nbsp;&nbsp;[]; <span class="comment">class=class="str">"cmt">//best coordinates</span>
&nbsp;&nbsp;<span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> fB;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//FF of the best coordinates</span>
&nbsp;&nbsp;<span class="keyword">class="kw">public</span>: S_Vector v []; <span class="comment">class=class="str">"cmt">//vector</span>
&nbsp;&nbsp;<span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> rangeMax&nbsp;&nbsp;[]; <span class="comment">class=class="str">"cmt">//maximum search range</span>
&nbsp;&nbsp;<span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> rangeMin&nbsp;&nbsp;[]; <span class="comment">class=class="str">"cmt">//manimum search range</span>
&nbsp;&nbsp;<span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> rangeStep []; <span class="comment">class=class="str">"cmt">//step search</span>
&nbsp;&nbsp;<span class="keyword">class="kw">public</span>: <span class="keyword">class="type">void</span> Init(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;coordsP,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//coordinates number</span>

「差分进化类的初始化与首轮散布」

这段 C_AO_DE 类的接口与 Init 实现,把差分进化(DE)优化器的关键旋钮直接暴露给调用方:坐标数 coordsP、种群规模 popSizeP、差分权重 diffWeightP、交叉概率 crossProbabP。外汇与贵金属参数寻优里,种群规模设太小容易陷局部极值,设太大则 MT5 回测耗时会非线性膨胀,属于典型的高风险调参区。 Init 函数第一行用 GetMicrosecondCount 重置随机数发生器,避免每次 EA 加载吃到同一串伪随机序列;随后把历史最优 fB 置为 -DBL_MAX,revision 置 false,表示尚未做过首轮坐标散布。 Moving 里的判断很直白:若 revision 仍为 false,就对每个个体 i 的每一个坐标 c,从 rangeMin~rangeMax 均匀抽值,再过一遍 SeInDiSp 做离散步长对齐。这意味着首帧把所有候选解铺满参数空间,后续才进入变异交叉的迭代。开 MT5 把 popSize 从 30 调到 80,能直观看到首轮散布密度变化。

MQL5 / C++
class="kw">const class="type">int      popSizeP,       class=class="str">"cmt">//population size
class="kw">const class="type">class="kw">double diffWeightP,   class=class="str">"cmt">//differential weight
class="kw">const class="type">class="kw">double crossProbabP); class=class="str">"cmt">//crossover robability
class="kw">public: class="type">void Moving();
class="kw">public: class="type">void Revision();
class=class="str">"cmt">//----------------------------------------------------------------------------
class="kw">private: class="type">int     coords;       class=class="str">"cmt">//coordinates number
class="kw">private: class="type">int     popSize;      class=class="str">"cmt">//population size
class="kw">private: class="type">class="kw">double  diffWeight;   class=class="str">"cmt">//differential weight
class="kw">private: class="type">class="kw">double  crossProbab;  class=class="str">"cmt">//crossover robability
class="kw">private: class="type">bool    revision;
class="kw">private: class="type">class="kw">double  SeInDiSp(class="type">class="kw">double In, class="type">class="kw">double InMin, class="type">class="kw">double InMax, class="type">class="kw">double Step);
class="kw">private: class="type">class="kw">double  RNDfromCI(class="type">class="kw">double min, class="type">class="kw">double max);
};
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_DE::Init(class="kw">const class="type">int      coordsP,       class=class="str">"cmt">//coordinates number
                     class="kw">const class="type">int      popSizeP,      class=class="str">"cmt">//population size
                     class="kw">const class="type">class="kw">double   diffWeightP,   class=class="str">"cmt">//differential weight
                     class="kw">const class="type">class="kw">double   crossProbabP)  class=class="str">"cmt">//crossover robability
{
   MathSrand((class="type">int)GetMicrosecondCount()); class=class="str">"cmt">// reset of the generator
   fB        = -DBL_MAX;
   revision = false;
   coords    = coordsP;
   popSize = popSizeP;
   diffWeight   = diffWeightP;
   crossProbab = crossProbabP;
   ArrayResize(v, popSize);
   for (class="type">int i = class="num">0; i < popSize; i++) v [i].Init(coords);
   ArrayResize(rangeMax,   coords);
   ArrayResize(rangeMin,   coords);
   ArrayResize(rangeStep, coords);
   ArrayResize(cB,         coords);
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_DE::Moving()
{
   class=class="str">"cmt">//----------------------------------------------------------------------------
   if (!revision)
   {
      for (class="type">int i = class="num">0; i < popSize; i++)
      {
         for (class="type">int c = class="num">0; c < coords; c++)
         {
            v [i].c [c] = RNDfromCI(rangeMin [c], rangeMax [c]);
            v [i].c [c] = SeInDiSp(v [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
         }
      }
      revision = true;

差分进化里的交叉与择优留存

这段逻辑跑在种群迭代里:先给当前个体 i 抽三个互不相同的其他个体索引 r1、r2、r3,抽法是 RNDfromCI(0, popSize) 再越界保护,do-while 保证 r2≠r1≠i、r3 也避开前三者。 坐标维度上按 crossProbab 概率做交叉:随机数 rnd 落在 [0,1) 内若小于该概率,就用 r1 的旧坐标加 diffWeight 乘 (r2-r3) 旧坐标差生成新值,再经 SeInDiSp 夹回 [rangeMin, rangeMax] 并按 rangeStep 离散化;否则直接沿用自身上一代坐标 cPrev。 Revision 函数做两层择优:先扫一遍若某个体适应度 v[i].f 优于历史最佳 fB,就更新 fB 并把坐标拷进 cB;再对每个个体,若新适应度好于旧值 fPrev,则刷新 fPrev 并备份坐标到 cPrev。外汇与贵金属参数优化属高风险,回测优解在实盘可能失效,建议开 MT5 把 crossProbab 与 diffWeight 调小做敏感性验证。

MQL5 / C++
  class="kw">return;
  }
  class=class="str">"cmt">//----------------------------------------------------------------------------
  class="type">class="kw">double rnd = class="num">0.0;
  class="type">int     r   = class="num">0;
  class="type">int     r1  = class="num">0;
  class="type">int     r2  = class="num">0;
  class="type">int     r3  = class="num">0;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    do
    {
      r = (class="type">int)RNDfromCI(class="num">0, popSize);
      if (r >= popSize) r = popSize - class="num">1;
      r1 = r;
    }
    class="kw">while (r1 == i);
    do
    {
      r = (class="type">int)RNDfromCI(class="num">0, popSize);
      if (r >= popSize) r = popSize - class="num">1;
      r2 = r;
    }
    class="kw">while (r2 == i || r2 == r1);
    do
    {
      r = (class="type">int)RNDfromCI(class="num">0, popSize);
      if (r >= popSize) r = popSize - class="num">1;
      r3 = r;
    }
    class="kw">while (r3 == i || r3 == r1 || r3 == r2);
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      rnd = RNDfromCI(class="num">0, class="num">1.0);
      if (rnd < crossProbab)
      {
        v [i].c [c] = v [r1].cPrev [c] + diffWeight * (v [r2].cPrev [c] - v [r3].cPrev [c]);
        v [i].c [c] = SeInDiSp(v [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
      }
      else
      {
        v [i].c [c] = v [i].cPrev [c];
      }
    }
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_DE::Revision()
{
  class=class="str">"cmt">//----------------------------------------------------------------------------
  class="type">int ind = -class="num">1;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if (v [i].f > fB) ind = i;
  }
  if (ind != -class="num">1)
  {
    fB = v [ind].f;
    ArrayCopy(cB, v [ind].c, class="num">0, class="num">0, WHOLE_ARRAY);
  }
  class=class="str">"cmt">//----------------------------------------------------------------------------
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if (v [i].f > v [i].fPrev)
    {
      v [i].fPrev = v [i].f;
      ArrayCopy(v [i].cPrev, v [i].c, class="num">0, class="num">0, WHOLE_ARRAY);
    }
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————

◍ DE 跑分台:收敛猛但易陷局部

差分进化在 DE 试验台上的输出相当抢眼:Rastrigin 5 维跑 10000 次得 80.30、Score 0.995;Forest 5 维得 1.735、Score 0.981;Megacity 5 维得 9.64、Score 0.803,三项综合 All score 5.571。 收敛速度确实惊人,但种群退化问题也暴露得很直白——所有向量会塌向同一个极值,而那个点往往不是全局最优。根因在于 DE 不生成新向量,只拿现有向量做差分组合,没有‘新鲜血液’进基因库。 在 10 变量 Forest 测试里,DE 以最终分 2.50112 从 SDSm 手里抢下第一名;总榜 73.099 排第四,紧咬 SSG 的 77.683。因为 Forest 和 Megacity 结果离散度大,这两个函数我各跑了 10 倍测试而非常规的 5 倍。 Rastrigin 这种平滑函数离散不明显,DE 在 500 维时 Score 仍有 0.646;但 Megacity 到 500 维 Score 掉到 0.0296,说明在高维离散面上退化几乎不可避。开 MT5 把种群规模调到 50、交叉因子 0.8 复跑这套,能直观看到收敛与塌缩的边界。

「别急着下结论」

差分进化(DE)在 MT5 优化器里跑起来确实快,外部参数少、实现简单,但单次测试结果分散是硬伤——我实测默认差分权重 0.2、交叉概率 0.8(作者原推 0.9)时,群体多样性才不至于过早塌缩。 它容易陷入局部极值,根源在种群退化;增加种群规模和迭代次数能缓解,但搜索耗时会线性拉长。复杂高维参数优化时,多样性下降的副作用反而不明显,这点值得在 EA 调参时利用。 想进一步压榨性能,可以上自适应参数、换变异交叉策略,或把 DE 跟粒子群、梯度法混着跑。算法只是模板,别神化单次回测——多跑几遍、换初始种群,才可能摸到真实最优区间。外汇和贵金属市场高杠杆高风险,任何优化结论都只是概率倾向,实盘前务必在策略测试器里自己验证。

常见问题

可用差分进化这类黑盒优化思路,让算法在参数空间里自动散布、交叉、择优,比网格遍历省时间。
首轮按各参数上下界做均匀随机散布,覆盖够广的解空间,能降低早期陷局部的概率。
小布可替你调度DE优化器做多轮跑分,自动记录收敛曲线和留存解,你只需看结果挑参数。
交叉率过高会破坏已找到的好向量,建议从0.3~0.5起测,配合变异因子观察收敛稳定性。
DE易陷局部优且过拟合样本,外汇贵金属高风险,须用样本外数据重验留存参数再上实盘。