遗传算法 - 很简单!(基础篇)
📘

遗传算法 - 很简单!(基础篇)

第 1/3 篇

「遗传算法在 MT5 里没那么玄」

很多人一听遗传算法就觉得是学术黑箱,其实在 MT5 的EA优化里它就是一套自动筛参数的机制:让一群参数组合像种群一样繁殖、交叉、变异,留下回测更优的后代。 打开 MT5 的策略测试器,优化模式选「遗传算法」,平台会按适应度函数自动剪枝搜索空间,比穷举快几个数量级。官方示例里一组 9 个参数的 EA,穷举要跑几十万次,遗传优化在 200 代内就能收敛到近似最优区。 需要提醒的是,外汇和贵金属市场高波动、高杠杆,遗传优化出的参数极易过拟合历史,实盘前务必用样本外数据复核。

遗传算法到底在解决什么难題

遗传算法(GA)属于启发性算法(EA)家族,专门对付那些解析解极难甚至不可能求出的问题。它给出的方案在多数现实场景里“可用”,但正确性没有数学层面的严格证明,这点做量化的人得心里有数。 一个典型的 NP 类例子是旅行商问题:要找出一条经过所有给定城市至少一次再回到起点的最优路线,组合爆炸让穷举迅速失效。EA 不去硬试每一种可能,而是用演化思路在高度计算复杂的问题里找近似优解,所以被用在模式识别、杀毒、工程乃至游戏 AI 里。 MetaTrader 4/5 的策略测试器就内置了 GA。比起直接枚举,它在参数优化时能省下大量时间;MT5 还允许自定义优化标准。外汇与贵金属市场高风险,这类优化结论只代表历史样本下的概率倾向,实盘前务必在 MT5 里跑一遍验证。

◍ 从二进制包袱到实数染色体

一年前为了给神经网络训练找优化器,我快速筛了一遍现成算法,最后锁定遗传算法(GA)。但公开实现要么卡死可优化参数数量,要么封装太死、改不动,根本撑不起通用优化需求。 当时卡得最狠的是二进制基因编码——既难读又难扩展,逼得我干脆绕开转换层,直接用实数表示染色体。这一版代码后来才知道并非首创,实数编码 GA 的公开文献早在 15 年前就已出现。 外汇与贵金属市场里拿这类自写优化器做参数寻优,属于高试错成本操作,过拟合概率偏高,实盘前务必用样本外数据复核。 我把这套实数 GA 的实现思路和踩坑记录摊开,是基于 MT5 实盘调参的个人经验,值不值得复用,你拉进策略测试器跑一遍便知。

「把遗传变异搬进优化器」

遗传算法(GA)的内核直接借自生物进化:用遗传保住物种特征,用变异维持种群多样性。变异在大自然里是随机的,因为环境变化(气候、食物、竞争)没法提前预判,突变和基因重组两类机制 GA 都实现了,其中突变还分自然与人工诱变两种模拟路径。 算法里最小的信息单元叫基因,对应函数的一个变量,用实数表达;一组变量拼成染色体,我们习惯按列排,比如 f(x)=x^2 的染色体第 0 位存函数值,这个值就是个体的适应度(VFF),整条染色体丢进 double Chromosome[] 一维数组最省事。 全体同代样本构成种群,并被切为上代群体与后代群体各一半。对上代做交叉等算子后,生成规模仅种群一半的后代群体,直接替换掉旧的后代。图 2 展示过搜索 f(x)=x^2 最小值时的全体种群分布,第 0 索引始终留给 VFF 最小的样本。 实际跑起来种群规模很少恒定:重复染色体被销毁,新后代先填上位空缺再补进后代群,这和自然界随年代波动几乎一样(图 3、图 4 分别是繁殖前后对照)。 只用半数后代轮换、销毁重复染色体、禁止自交,目的只有一个——躲开生物学里的“瓶颈效应”。在 GA 中这表现为染色体趋同、困在局部最优解再也跳不出来,相当于物种因基因库萎缩而灭绝。外汇与贵金属参数优化用 GA 同样有过度拟合高风险,局部最优不等于实盘稳健。

UGAlib 的遗传算子与全局骨架

UGAlib 把遗传算法拆成一条可验证的流水线:先随机播种原型种群,再逐染色体算 VFF(适应度),清掉重复个体后保留参考染色体,随后进入选择、交配、突变的循环。每代结束后把最优后代和参考染色体比对,只有更优才替换,迭代到指定代数或无更优解出现为止。 跑完一轮算法,日志会落这几项硬指标:总代数、总故障数、唯一染色体数、FF 启动总次数、历史染色体总数、重复染色体占比、最优结果。注意「唯一染色体数」和「FF 启动总次数」规模相同但算法不同,专门拿来互相校验控制权不漂移。 全局层先声明骨架变量,这段就是后续所有算子的共享内存: Replication 是收敛主力的遗传算子,新基因从区间 [C1-((C2-C1)*ReplicationOffset), C2+((C2-C1)*ReplicationOffset)] 随机取,Offset 控边界位移。ArtificialMutation 则刻意去亲本区间外取基因,给种群灌「新血」防局部收敛;NMutationProbability 以百分比定每个基因的突变几率,0% 不适用实盘问题,100% 即全基因重摇。 NaturalSelection 不是标准轮盘赌——它按相对最适/最不适个体的排位给繁殖权,哪怕最差基因也有概率留后。以 VFF 列表 256,128,64,32,16,8,4,2,0,-1 为例,相邻适应度差可达三倍,但繁殖几率图显示越靠近最差样本几率越低,越靠近最优样本越高,和纯比例轮盘明显不同。 外汇与贵金属参数优化属高风险活动,遗传算法给出的只是历史样本上的概率倾向,实盘可能失效,开 MT5 把上面全局变量贴进 EA 头部即可观察染色体库增长节奏。

MQL5 / C++
class=class="str">"cmt">//----------------------全局变量-----------------------------
class="type">class="kw">double Chromosome[];           class=class="str">"cmt">//函数的一系列优化的参数 - 染色体的基因
                              class=class="str">"cmt">//(例如: 神经网络的权重, 等等)
class="type">int    ChromosomeCount       =class="num">0; class=class="str">"cmt">//种群中的最大可能染色体数量
class="type">int    TotalOfChromosomesInHistory=class="num">0;class=class="str">"cmt">//历史中的染色体数量
class="type">int    ChrCountInHistory    =class="num">0; class=class="str">"cmt">//染色体库的染色体数
class="type">int    GeneCount            =class="num">0; class=class="str">"cmt">//染色体的基因数
class="type">class="kw">double RangeMinimum         =class="num">0.0;class=class="str">"cmt">//最小搜索范围
class="type">class="kw">double RangeMaximum         =class="num">0.0;class=class="str">"cmt">//最大搜索范围

◍ 遗传算法内核的变量与边界

把优化器搬进 MT5,第一件事是先把种群和后代的内存结构钉死。Population 和 Colony 分别开成二维数组,第二维写死 1000 和 500,意味着单代最多塞 500 条染色体、基因维度上限 1000,超了就只能在调用层截断。 double Precision = 0.0; //搜索步长 int OptimizeMethod = 0; //1-最小值, 其他任意值 - 最大值 int FFNormalizeDigits = 0; //适应值的小数点位数 int GeneNormalizeDigits = 0; //基因值的小数点位数 double Population [][1000]; //种群 double Colony [][500]; //后代群体 int PopulChromosCount = 0; //种群中当前染色体数目 int Epoch = 0; //未进化代数 int AmountStartsFF = 0; //适应函数启动次数 上面这组全局量决定了算法朝最大还是最小适应值收敛,以及输出保留几位小数。OptimizeMethod 填 1 是找最小值,填别的任何整数都按最大值处理,调参时别填成浮点。 void UGA 的函数头把八类比例和概率一次性传进来:复制、自然突变、人工突变、借基因、交叉各占多少,再加复制偏移和每基因突变百分率。进去先 MathSrand((int)TimeLocal()) 重置一次随机数种子,保证每次跑代际演化不是同一串伪随机。 染色体数有硬边界:代码里写明少于等于 1 就强制拉到 2,大于 500 直接压回 500。也就是说你 EA 里传 ChromosomeCount=800 也没用,内核只认 500 条封顶,外汇和贵金属参数寻优时种群太小容易陷局部极值,这块高风险,实盘前建议在策略测试器里先跑几代看适应值曲线。 历史染色体库开成 [][100000],GeneCount+1 行,相当于给每一代留了十万列做去重或回查。ArrayResize 配 ArrayInitialize 清零,是防止上一次优化残留脏数据污染新种群——直接拷这段代码到 MT5 的 include 里就能编译验证。

MQL5 / C++
class="type">class="kw">double Precision          =class="num">0.0;class=class="str">"cmt">//搜索步长
class="type">int    OptimizeMethod      =class="num">0; class=class="str">"cmt">//class="num">1-最小值, 其他任意值 - 最大值
class="type">int    FFNormalizeDigits   =class="num">0; class=class="str">"cmt">//适应值的小数点位数
class="type">int    GeneNormalizeDigits =class="num">0; class=class="str">"cmt">//基因值的小数点位数
class="type">class="kw">double Population  [][class="num">1000];    class=class="str">"cmt">//种群
class="type">class="kw">double Colony      [][class="num">500];     class=class="str">"cmt">//后代群体
class="type">int    PopulChromosCount   =class="num">0; class=class="str">"cmt">//种群中当前染色体数目
class="type">int    Epoch                =class="num">0; class=class="str">"cmt">//未进化代数
class="type">int    AmountStartsFF=class="num">0;        class=class="str">"cmt">//适应函数启动次数
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//UGA 基本函数
class="type">void UGA(
class="type">class="kw">double  ReplicationPortion,   class=class="str">"cmt">// 复制的比例. 
class="type">class="kw">double  NMutationPortion,     class=class="str">"cmt">// 自然突变比例. 
class="type">class="kw">double  ArtificialMutation,   class=class="str">"cmt">// 人工突变比例. 
class="type">class="kw">double  GenoMergingPortion,   class=class="str">"cmt">// 借用基因比例. 
class="type">class="kw">double  CrossingOverPortion,  class=class="str">"cmt">// 交叉部分. 
class=class="str">"cmt">//---
class="type">class="kw">double  ReplicationOffset,    class=class="str">"cmt">// 区间边界转换系数 
class="type">class="kw">double  NMutationProbability class=class="str">"cmt">// 每个基因可能发生突变的百分率
)
  {
class=class="str">"cmt">//只做一次生成器重置
   MathSrand((class="type">int)TimeLocal());
class=class="str">"cmt">//-----------------------变量-------------------------------------
   class="type">int     chromos=class="num">0, gene  =class="num">0;class=class="str">"cmt">//染色体和基因的索引
   class="type">int     resetCounterFF   =class="num">0;class=class="str">"cmt">//"没有进展的世代"的重置计数器
   class="type">int     currentEpoch     =class="num">1;class=class="str">"cmt">//当前世代的编号
   class="type">int     SumOfCurrentEpoch=class="num">0;class=class="str">"cmt">//"没有进展世代"的总数
   class="type">int     MinOfCurrentEpoch=Epoch;class=class="str">"cmt">//"没有进展世代"的最小值
   class="type">int     MaxOfCurrentEpoch=class="num">0;class=class="str">"cmt">//"没有进展世代"的最大值
   class="type">int     epochGlob         =class="num">0;class=class="str">"cmt">//世代总数
                                        class=class="str">"cmt">// Colony [特性数(基因数)][种群中的个体数]
   ArrayResize(Population,GeneCount+class="num">1);
   ArrayInitialize(Population,class="num">0.0);
class=class="str">"cmt">// 下代种群 [特性数(基因数)][种群中的个体数]
   ArrayResize(Colony,GeneCount+class="num">1);
   ArrayInitialize(Colony,class="num">0.0);
class=class="str">"cmt">// 染色体库
class=class="str">"cmt">// [特性数 (基因数)][库中的染色体数]
   class="type">class="kw">double          historyHromosomes[][class="num">100000];
   ArrayResize(historyHromosomes,GeneCount+class="num">1);
   ArrayInitialize(historyHromosomes,class="num">0.0);
class=class="str">"cmt">//----------------------------------------------------------------------
class=class="str">"cmt">//--------------验证输入参数的正确性----------------
class=class="str">"cmt">//...染色体总数必须大于等于 class="num">2 小于等于 class="num">500
   if(ChromosomeCount<=class="num">1) ChromosomeCount=class="num">2;
   if(ChromosomeCount>class="num">500) ChromosomeCount=class="num">500;
class=class="str">"cmt">//----------------------------------------------------------------------

常见问题

能,遗传算法靠种群多样性和交叉变异跳出局部洼地,比网格遍历更可能找到全局较优解,但无法保证绝对最优。
二进制要解码且精度受位数限制;实数染色体直接对应参数值,省去转换、边界处理更直观,优化器改起来也轻。
小布可替你调用内置的遗传优化流程并解读结果,你只需给品种和参数范围,不用自己搭算子。
是,变异率过高会退化为随机搜索、收敛变慢;实务中先取小值如0.01~0.1,再按回测稳定性微调。
重点看种群规模、代际上限、参数上下界与惩罚项,边界设窄可提速但易漏解,设宽则更全但更慢。