「用差分进化给 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 里跑这套前先想清楚搜索步长会不会卡在局部。
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="keyword">class="kw">struct</span> S_Vector { <span class="keyword">class="type">void</span> Init(<span class="keyword">class="type">int</span> coords) { <span class="functions">ArrayResize</span> (c, coords); <span class="functions">ArrayResize</span> (cPrev, coords); f = -<span class="macro">DBL_MAX</span>; fPrev = -<span class="macro">DBL_MAX</span>; } <span class="keyword">class="type">class="kw">double</span> c []; <span class="comment">class=class="str">"cmt">//coordinates</span> <span class="keyword">class="type">class="kw">double</span> cPrev []; <span class="comment">class=class="str">"cmt">//previous coordinates</span> <span class="keyword">class="type">class="kw">double</span> f; <span class="comment">class=class="str">"cmt">//fitness</span> <span class="keyword">class="type">class="kw">double</span> fPrev; <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 { <span class="comment">class=class="str">"cmt">//----------------------------------------------------------------------------</span> <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> cB []; <span class="comment">class=class="str">"cmt">//best coordinates</span> <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> fB; <span class="comment">class=class="str">"cmt">//FF of the best coordinates</span> <span class="keyword">class="kw">public</span>: S_Vector v []; <span class="comment">class=class="str">"cmt">//vector</span> <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> rangeMax []; <span class="comment">class=class="str">"cmt">//maximum search range</span> <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> rangeMin []; <span class="comment">class=class="str">"cmt">//manimum search range</span> <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> <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> coordsP, <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,能直观看到首轮散布密度变化。
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 调小做敏感性验证。
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 跟粒子群、梯度法混着跑。算法只是模板,别神化单次回测——多跑几遍、换初始种群,才可能摸到真实最优区间。外汇和贵金属市场高杠杆高风险,任何优化结论都只是概率倾向,实盘前务必在策略测试器里自己验证。