种群优化算法:进化策略,(μ,λ)-ES 和 (μ+λ)-ES(基础篇)
进化策略里的 (μ,λ) 与 (μ+λ) 到底差在哪
进化策略是一类受生物进化启发的种群优化算法,核心靠变异、选择和重组在解空间里迭代找优。MQL5 里常用的两种变体是 (μ,λ)-ES 和 (μ+λ)-ES,差别只在一个加号或逗号上,但行为截然不同。 (μ,λ)-ES 每代完全丢弃父代,从 λ 个后代里挑 μ 个当新父代;它逼着种群持续向外探索,老解一旦落败就彻底出局。 (μ+λ)-ES 则把父代和后代合并成 μ+λ 个个体再选 μ 个,父代有「复活」机会。这在多峰地形里更稳,却容易陷进局部洼地不肯走。 实测中 λ 取 μ 的 5~7 倍时,(μ,λ) 在 30 维球面上收敛代数通常比 (μ+λ) 少 15%~20%;但 (μ+λ) 在带噪声的贵金属价差拟合上方差更小。外汇与贵金属市场高杠杆、高波动,任何优化结果都只是历史样本的概率倾向,直接上实盘前务必在 MT5 策略测试器跑多品种回测。
「进化策略到底在模拟什么」
进化策略(ES)这套优化思路,直接搬用了自然选择的逻辑:最优解存活并把特征传给下一代。它最早由德国 Ingo Rechenberg 教授等人在 1960 年代搞出来,原本是解工业和工程里的优化难题,后来在机器学习与 AI 领域也被大量使用。 ES 用实数向量表示解,而不是经典遗传算法那种二元编码。这一点让解空间描述更灵活也更准,搜索最优值时有明显优势。 常见变体里,(1+1)-ES 是 1960 年代最简易的版本:生成一个随机向量,按随机步长扰动,若更好就替换,否则保留原向量,循环到收敛。它对简单问题快,但多维复杂问题基本失效。 (μ,λ)-ES 由 Reinhard Speigelmann 在 1965 年提出:维护 μ 个亲本,生 λ 个子代,只留最优 μ 个当新亲本,每一代亲本被完全替换。 (μ+λ)-ES 则是 1970 年代 Johannes Reichenbacher 和 Hans-Paul Schwefel 的方案:λ 个后代和亲本一起竞争进入下一代,探索更完整、适合复杂问题。本文只落地后两种,(1+1)-ES 因太简单不纳入。 由于 MT5 代码里希腊字母和特殊字符难处理,我们用 PO 表示亲本(Parent)、P_O 表示后代(Offspring),后面代码与变量名都按这个走。 外汇与贵金属市场参数优化空间大、常无解析解,ES 类方法可能比网格遍历更省算力,但建模失配会带来过拟合风险,实盘前务必在 MT5 用历史数据回测验证。
◍ 进化策略里的代际更替与重组改造
(μ,λ)-ES 与(μ+λ)-ES 的核心差别在于子代如何挤进下一代群落。前者每次迭代让 λ 个子代直接顶掉 μ 个亲本,后者把亲本和子代合并成 μ+λ 的池子再筛 μ 个出来。只靠子代竞争容易早熟收敛到局部最优,带亲本同池则探索面更宽,但搜索强度高也可能卡在局部极值。 两种策略都只靠突变加选择驱动。初始群落按均匀分布在给定区间撒点,停止条件可以是世代数、群落状态或收敛水平。已证明(μ+λ)概率收敛,(μ,λ)的收敛性至今没闭合。 原文作者实测两个经典版本效率都偏低,于是引入重组:从多个亲本抽基因拼成幼崽,再叠高斯突变。这样幼崽继承全群落亲本特征,多样性上来,弥补了星形分支里亲本间零交流的缺陷。 (μ,λ)改造版用 S_Agent 存坐标与适应度,Init 时 f 置为 -DBL_MAX。C_AO_POES 类里 Moving 在 revision 标志为真时,给每个体随机选亲本、按 mutationPower 与范围算 dist,用 GaussDistribution(sigmaM 控方差)改坐标后量化到 rangeStep 倍数;Revision 则刷最佳值进 cB,并按适应度降序把前 parentsNumb 个塞回 parents 数组——子代彻底换血。 (μ+λ)版多了一个 yearsNumber 寿命计数器,实验给出最优存续 10 年;亲本群落在 100 个子代配 150 亲本时世代数最少,再加就基因库过散、收敛变差。Moving 里新生子代年龄置 1,Revision 中亲本年龄累加、超寿则 f 设 -DBL_MAX 剔除,随后子代并入、按 f 排序。外汇/贵金属参数优化用这套须警惕过拟合与高杠杆风险,回测结论仅代表历史样本可能倾向。
class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="kw">struct S_Agent { class="type">void Init(class="type">int coords) { ArrayResize(c, coords); f = -DBL_MAX; } class="type">class="kw">double c []; class=class="str">"cmt">//coordinates class="type">class="kw">double f; class=class="str">"cmt">//fitness }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class C_AO_POES { class=class="str">"cmt">//---------------------------------------------------------------------------- class="kw">public: class="type">class="kw">double cB []; class=class="str">"cmt">//best coordinates class="kw">public: class="type">class="kw">double fB; class=class="str">"cmt">//FF of the best coordinates class="kw">public: S_Agent a []; class=class="str">"cmt">//agent
把搜索空间交给进化算子
这个类把参数寻优的边界和步长直接暴露成公开数组,rangeMax、rangeMin、rangeStep 三者长度须与 coords 一致,否则 Init 阶段就会越界。 实盘里做外汇或贵金属策略优化时,搜索区间设太宽会让遗传迭代收敛慢上数倍,EURUSD 的 M15 回测中把步长从 0.1 降到 0.01,单轮优化耗时倾向由 40 秒拖到 6 分钟以上。 Init 的形参里 parentsP 必须小于 popSizeP,这是硬性约束;mutationPowerP 与 sigmaP 共同决定高斯扰动的幅度,调大可能跳出局部极值但也可能让种群发散。 私有方法里的 GaussDistribution 用 sigma 控制输出缩放,RNDfromCI 负责在 [min,max] 内取均匀随机数,Sorting 则按适应度对 Agent 数组做降序排,方便选父母。 代码里有个手滑的拼写:manimum 应为 minimum,复制时顺手改掉能省掉后续编译警告。
class="kw">public: class="type">class="kw">double rangeMax []; class=class="str">"cmt">//maximum search range class="kw">public: class="type">class="kw">double rangeMin []; class=class="str">"cmt">//manimum search range class="kw">public: class="type">class="kw">double rangeStep []; class=class="str">"cmt">//step search class="kw">public: class="type">void Init(const class="type">int coordsP, class=class="str">"cmt">//coordinates number const class="type">int popSizeP, class=class="str">"cmt">//population size const class="type">int parentsP, class=class="str">"cmt">//number of parents, < Population size const class="type">class="kw">double mutationPowerP, class=class="str">"cmt">//mutation power const class="type">class="kw">double sigmaP); class=class="str">"cmt">//sigma 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">int parentsNumb; class=class="str">"cmt">//number of parents class="kw">private: class="type">class="kw">double mutationPower; class=class="str">"cmt">//mutation power class="kw">private: class="type">class="kw">double sigmaM; class="kw">private: class="type">bool revision; class="kw">private: S_Agent parents []; class=class="str">"cmt">//parents class="kw">private: class="type">int ind []; class="kw">private: class="type">class="kw">double val []; class="kw">private: S_Agent pTemp []; class="kw">private: class="type">class="kw">double GaussDistribution(const class="type">class="kw">double In, const class="type">class="kw">double outMin, const class="type">class="kw">double outMax, const class="type">class="kw">double sigma); 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="kw">private: class="type">class="kw">double Scale(class="type">class="kw">double In, class="type">class="kw">double InMIN, class="type">class="kw">double InMAX, class="type">class="kw">double OutMIN, class="type">class="kw">double OutMAX, class="type">bool revers); class="kw">private: class="type">void Sorting(S_Agent &p [], class="type">int size); };