遗传算法 - 很简单!(基础篇)
「遗传算法在 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 头部即可观察染色体库增长节奏。
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 里就能编译验证。
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">//----------------------------------------------------------------------