遗传算法 - 很简单!·进阶篇
(2/3)· 从自然隐喻到实数染色体,把 MT5 策略优化的黑盒拆成可改的代码
◍ 遗传算法优化器的启动骨架
把 UGA(通用遗传算法)接进 MT5 策略优化,第一步不是调参数,而是把种群初始化和评估链路跑通。下面这段骨架代码展示了从建种群、算适应度到进入主循环的标准顺序,可直接贴进 EA 的 OnTester 或独立优化模块里改。 原始种群由 ProtopopulationBuilding() 生成,随后对两个子群体分别做适应度评估:第一个群体索引 0 到 ChromosomeCount-1,第二个群体索引 ChromosomeCount 到 2*ChromosomeCount-1,各自把基因拷进 Colony 缓冲再调 GetFitness(historyHromosomes)。注意 Population[0][chromos] 存的是该染色体的适应度值,gene 从 1 开始才装实际基因位。 去重与参照染色体展开不能省。RemovalDuplicates() 清掉重复个体避免早熟;之后把 Population[gene][0] 赋给 Chromosome[gene] 作为第 4 步的参照基准。主循环 while(currentEpoch<=Epoch) 里只干两件事:第 5 步 CycleOfOperators 跑复制/突变/交叉等算子,第 6 步在最小化模式(OptimizeMethod==1)下若种群最优 Population[0][0] 小于参照 Chromosome[0] 则替换参照。外汇与贵金属品种波动无序,遗传优化仅给出参数组合的概率优势,实盘前务必用历史数据多轮回测验证。 算子比例参数是后续调优重点:ReplicationPortion 控制复制规模,NMutationProbability 决定每个基因位突变百分比,CrossingOverPortion 管交叉比例。先在 EURUSD 的 M15 上用默认比例跑 50 代,观察 Population[0][0] 收敛曲线再动手改。
class=class="str">"cmt">//====================================================================== class=class="str">"cmt">// class="num">1) 创建原始种群 ProtopopulationBuilding(); class=class="str">"cmt">//====================================================================== class=class="str">"cmt">// class="num">2) 确定每个个体的适应性 class=class="str">"cmt">//对于第一个群体 for(chromos=class="num">0;chromos<ChromosomeCount;chromos++) for(gene=class="num">1;gene<=GeneCount;gene++) Colony[gene][chromos]=Population[gene][chromos]; GetFitness(historyHromosomes); for(chromos=class="num">0;chromos<ChromosomeCount;chromos++) Population[class="num">0][chromos]=Colony[class="num">0][chromos]; class=class="str">"cmt">//对于第二个群体 for(chromos=ChromosomeCount;chromos<ChromosomeCount*class="num">2;chromos++) for(gene=class="num">1;gene<=GeneCount;gene++) Colony[gene][chromos-ChromosomeCount]=Population[gene][chromos]; GetFitness(historyHromosomes); for(chromos=ChromosomeCount;chromos<ChromosomeCount*class="num">2;chromos++) Population[class="num">0][chromos]=Colony[class="num">0][chromos-ChromosomeCount]; class=class="str">"cmt">//====================================================================== class=class="str">"cmt">// class="num">3) 准备种群进行繁殖 RemovalDuplicates(); class=class="str">"cmt">//====================================================================== class=class="str">"cmt">// class="num">4) 展开染色体参考 for(gene=class="num">0;gene<=GeneCount;gene++) Chromosome[gene]=Population[gene][class="num">0]; class=class="str">"cmt">//====================================================================== ServiceFunction(); class=class="str">"cmt">//基因算法的主循环 从第 class="num">5 步到第 class="num">6 步 class="kw">while(currentEpoch<=Epoch) { class=class="str">"cmt">//==================================================================== class=class="str">"cmt">// class="num">5) UGA 算子 CycleOfOperators( historyHromosomes, class=class="str">"cmt">//--- ReplicationPortion, class=class="str">"cmt">//复制的比例. NMutationPortion, class=class="str">"cmt">//自然突变的比例. ArtificialMutation, class=class="str">"cmt">//人工诱变的比例. GenoMergingPortion, class=class="str">"cmt">//外来的基因比例. CrossingOverPortion,class=class="str">"cmt">//交叉比例. class=class="str">"cmt">//--- ReplicationOffset, class=class="str">"cmt">//区间边界转换系数 NMutationProbabilityclass=class="str">"cmt">//每个基因突变可能性百分比 ); class=class="str">"cmt">//==================================================================== class=class="str">"cmt">// class="num">6) 比较最佳后代的基因和参照染色体的基因. class=class="str">"cmt">// 如果最佳后代的染色体比参照染色体好, class=class="str">"cmt">// 替换掉参照染色体. class=class="str">"cmt">//如果优化模式为 - 最小化 if(OptimizeMethod==class="num">1) { class=class="str">"cmt">//如果种群的最佳染色体比参照染色体好 if(Population[class="num">0][class="num">0]<Chromosome[class="num">0]) {
「参照染色体替换与停滞代统计」
遗传算法跑起来后,每一代都要拿种群里的最优染色体跟参照染色体比。优化目标分两类:最大化时看 Population[0][0] 是否小于 Chromosome[0],最小化时反过来比是否大于,满足条件就把参照染色体整个换掉。 换参照的同时调用 ServiceFunction() 做适应度记录,并重置无进展世代计数器 currentEpoch 为 1,同时把本次停滞长度累进 SumOfCurrentEpoch,并更新最小/最大停滞代 MinOfCurrentEpoch、MaxOfCurrentEpoch,resetCounterFF 加一。若没更好,currentEpoch 自增,表示又白跑了一代。 全局世代 epochGlob 每次循环末尾无条件加一。跑完主循环后,Print 会吐出关键数据:重置次数 resetCounterFF、平均停滞代(SumOfCurrentEpoch/resetCounterFF 保留两位小数)、唯一染色体数 ChrCountInHistory、适应度函数总调用数 AmountStartsFF,以及重复率 = 100 - 唯一数/历史总数*100。 这些数字直接反映算法是不是在空转。比如重复率长期高于 70% 可能说明种群多样性崩了,外汇与贵金属参数优化本身高风险,这种结果只能当概率参考,别当成可稳定复用的结论。
for(gene=class="num">0;gene<=GeneCount;gene++) Chromosome[gene]=Population[gene][class="num">0]; ServiceFunction(); if(currentEpoch<MinOfCurrentEpoch) MinOfCurrentEpoch=currentEpoch; if(currentEpoch>MaxOfCurrentEpoch) MaxOfCurrentEpoch=currentEpoch; SumOfCurrentEpoch+=currentEpoch; currentEpoch=class="num">1; resetCounterFF++; class=class="str">"cmt">// 最小化分支中: if(Population[class="num">0][class="num">0]>Chromosome[class="num">0]) { for(gene=class="num">0;gene<=GeneCount;gene++) Chromosome[gene]=Population[gene][class="num">0]; ServiceFunction(); if(currentEpoch<MinOfCurrentEpoch) MinOfCurrentEpoch=currentEpoch; if(currentEpoch>MaxOfCurrentEpoch) MaxOfCurrentEpoch=currentEpoch; SumOfCurrentEpoch+=currentEpoch; currentEpoch=class="num">1; resetCounterFF++; } else currentEpoch++; epochGlob++; Print("到来世代=",epochGlob," 重置次数=",resetCounterFF); Print("MinOfCurrentEpoch",MinOfCurrentEpoch," AverageOfCurrentEpoch",NormalizeDouble((class="type">class="kw">double)SumOfCurrentEpoch/(class="type">class="kw">double)resetCounterFF,class="num">2)," MaxOfCurrentEpoch",MaxOfCurrentEpoch); Print(ChrCountInHistory," - 唯一染色体"); Print(AmountStartsFF," - 适应性函数运行总数"); Print(TotalOfChromosomesInHistory," - 历史中的染色体总数"); Print(NormalizeDouble(class="num">100.0-((class="type">class="kw">double)ChrCountInHistory*class="num">100.0/(class="type">class="kw">double)TotalOfChromosomesInHistory),class="num">2),"% 重复"); Print(Chromosome[class="num">0]," - 最佳结果");
染色体去重与适应度缓存的实现细节
遗传算法跑批量参数寻优时,最烧 CPU 的不是算适应度,而是反复评估同一组基因。这段代码用了一个上限 100000 的二维历史库 historyHromosomes,把算过的染色体连同适应度(索引 0 位)一并存住,下次碰到相同基因直接复用。 CheckHistoryChromosomes 的判定逻辑很直白:从基因 1 到 GeneCount 逐位比,只要有一个不等就 break;若 cnt 等于 GeneCount 说明全同,直接把库里现成的适应度填回 Colony[0][chromos]。这样在种群规模大、迭代轮次多时,能砍掉相当一部分冗余计算。 库空或查无此染色体时才调 FitnessFunction,算完若 ChrCountInHistory 还小于 100000 就写回历史库。注意硬编码上限 100000 意味着历史染色体最多存这么多,超了就不再缓存,回测长周期时可能掉回全量重算。 别把正态当圣经 初始化基因用 NormalizedDouble + SelectInDiscreteSpace 在 RangeMinimum~RangeMaximum 间离散取样,看似均匀实则依赖 RNDfromCI 的随机源质量;MT5 里换不同随机种子,种群收敛路径可能有明显差异,建议开策略测试器多跑几遍比对。
for(class="type">int chromos=class="num">0;chromos<PopulChromosCount;chromos++) { class=class="str">"cmt">//从索引 class="num">1 开始 (索引 class="num">0 为 VFF 保留) for(class="type">int gene=class="num">1;gene<=GeneCount;gene++) Population[gene][chromos]= NormalizeDouble(SelectInDiscreteSpace(RNDfromCI(RangeMinimum,RangeMaximum),RangeMinimum,RangeMaximum,Precision,class="num">3),GeneNormalizeDigits); TotalOfChromosomesInHistory++; } class="type">void GetFitness(class="type">class="kw">double &historyHromosomes[][class="num">100000]) { for(class="type">int chromos=class="num">0;chromos<ChromosomeCount;chromos++) CheckHistoryChromosomes(chromos,historyHromosomes); } class="type">void CheckHistoryChromosomes(class="type">int chromos, class="type">class="kw">double &historyHromosomes[][class="num">100000]) { class="type">int Ch1=class="num">0; class="type">int Ge =class="num">0; class="type">int cnt=class="num">0; if(ChrCountInHistory>class="num">0) { for(Ch1=class="num">0;Ch1<ChrCountInHistory && cnt<GeneCount;Ch1++) { cnt=class="num">0; for(Ge=class="num">1;Ge<=GeneCount;Ge++) { if(Colony[Ge][chromos]!=historyHromosomes[Ge][Ch1]) break; cnt++; } } if(cnt==GeneCount) Colony[class="num">0][chromos]=historyHromosomes[class="num">0][Ch1-class="num">1]; else { FitnessFunction(chromos); if(ChrCountInHistory<class="num">100000) { for(Ge=class="num">0;Ge<=GeneCount;Ge++) historyHromosomes[Ge][ChrCountInHistory]=Colony[Ge][chromos]; ChrCountInHistory++; } } } else { FitnessFunction(chromos); for(Ge=class="num">0;Ge<=GeneCount;Ge++) historyHromosomes[Ge][ChrCountInHistory]=Colony[Ge][chromos]; ChrCountInHistory++; } } class="type">void CycleOfOperators(class="type">class="kw">double &historyHromosomes[][class="num">100000], class="type">class="kw">double ReplicationPortion)
◍ UGA 算子比例怎么塞进染色体循环
这段 MT5 遗传算法核心里,先把六类算子(复制、自然突变、人工突变、借基因、交叉、保留位)的比例写进 portion[6] 数组,portion[5] 恒为 0.0 只作区间计算锚点。用累计区间 fit[i][0]~fit[i][1] 把比例映射成一条 0 到总和的线段,再让 RNDfromCI 随机落点,决定本轮 T 位置孵化哪种算子。 实际跑起来,若 ReplicationPortion 设 0.4、NMutationPortion 设 0.2,那前 40% 的随机落点必走复制分支,后段才轮到突变类。外汇或贵金属策略用这套分布时,参数组合的高波动可能让某类算子过度占用种群,需盯紧 Colony 填充是否偏态。 下面拆一下关键片段:portion 赋值后,while(T<ChromosomeCount) 主循环靠 fit 区间选 u,switch(u) 里 case 0 即复制——Replication(child,ReplicationOffset) 生成个体,再按 gene 从 1 到 GeneCount 写回 Colony[gene][T],T 与历史染色体数同步 +1。 别把 portion[5]=0.0 当摆设 它是区间累计的起点基准,若误改成非零值,fit 全体平移会让 p 的随机命中率整体偏移,交叉算子可能概率性失踪。
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 NMutationProbabilityclass=class="str">"cmt">//每个基因突变可能性百分率 ) { class=class="str">"cmt">//-----------------------变量------------------------------------- class="type">class="kw">double child[]; ArrayResize(child,GeneCount+class="num">1); ArrayInitialize(child,class="num">0.0); class="type">int gene=class="num">0,chromos=class="num">0, border=class="num">0; class="type">int i=class="num">0,u=class="num">0; class="type">class="kw">double p=class="num">0.0,start=class="num">0.0; class="type">class="kw">double fit[][class="num">2]; ArrayResize(fit,class="num">6); ArrayInitialize(fit,class="num">0.0); class=class="str">"cmt">//新种群植入点计数器. class="type">int T=class="num">0; class=class="str">"cmt">//---------------------------------------------------------------------- class=class="str">"cmt">//设置 UGA 算子的比例 class="type">class="kw">double portion[class="num">6]; portion[class="num">0]=ReplicationPortion; class=class="str">"cmt">//复制比例. portion[class="num">1]=NMutationPortion; class=class="str">"cmt">//自然突变比例. portion[class="num">2]=ArtificialMutation; class=class="str">"cmt">//人工突变比例. portion[class="num">3]=GenoMergingPortion; class=class="str">"cmt">//借用基因比例. portion[class="num">4]=CrossingOverPortion;class=class="str">"cmt">//交叉比例. portion[class="num">5]=class="num">0.0; class=class="str">"cmt">//------------------------ UGA 算子循环 --------- class=class="str">"cmt">//把后代群体填入种群 class="kw">while(T<ChromosomeCount) { class=class="str">"cmt">//============================ for(i=class="num">0;i<class="num">6;i++) { fit[i][class="num">0]=start; fit[i][class="num">1]=start+MathAbs(portion[i]-portion[class="num">5]); start=fit[i][class="num">1]; } p=RNDfromCI(fit[class="num">0][class="num">0],fit[class="num">4][class="num">1]); for(u=class="num">0;u<class="num">5;u++) { if((fit[u][class="num">0]<=p && p<fit[u][class="num">1]) || p==fit[u][class="num">1]) break; } class=class="str">"cmt">//============================ class="kw">switch(u) { class=class="str">"cmt">//--------------------- case class="num">0: class=class="str">"cmt">//------------------------复制-------------------------------- class=class="str">"cmt">//如果新群体有空间, 创建一个新个体 if(T<ChromosomeCount) { Replication(child,ReplicationOffset); class=class="str">"cmt">//把新个体加入新群体中 for(gene=class="num">1;gene<=GeneCount;gene++) Colony[gene][T]=child[gene]; class=class="str">"cmt">//占用一个位置, 增加计数器 T++; TotalOfChromosomesInHistory++; } class=class="str">"cmt">//--------------------------------------------------------------- break;
「遗传算子的四种落地分支」
这段逻辑是 UGA(通用遗传算法)算子循环的核心分派:用 switch 按算子编号决定新个体怎么生成,再统一塞进新群体矩阵。case 1 走自然突变,case 2 走人工诱变,case 3 用借用基因合并,其余情况全部归到 default 做交叉。 每个分支都先判 T<ChromosomeCount,也就是新群体还没满才生个体;生完用 for(gene=1;gene<=GeneCount;gene++) 把 child 的各基因写进 Colony[gene][T],随后 T++ 和 TotalOfChromosomesInHistory++ 各加一。GeneCount 通常是你策略里参数维度的总数,ChromosomeCount 决定单代容量,这两个值直接卡住内存与计算量。 循环退出后调 GetFitness(historyHromosomes) 算适应性,再把后代并回主群:若 PopulChromosCount 已不低于 ChromosomeCount,border 锁为 ChromosomeCount 且 PopulChromosCount 翻倍;否则 border 取当前数、容量加一个 ChromosomeCount。外汇与贵金属市场高波动,这类进化搜索只给出参数空间的概率优选,不保证样本外稳健。 在 MT5 里把这段嵌进 EA 的 OnTester 或自定义优化器,把 ChromosomeCount 设小(如 30)先跑几代看 TotalOfChromosomesInHistory 增速,能快速判断算子概率配得合不合理。
case class="num">1: class=class="str">"cmt">//---------------------自然突变------------------------- class=class="str">"cmt">//如果新群体有空间, 创建一个新个体 if(T<ChromosomeCount) { NaturalMutation(child,NMutationProbability); class=class="str">"cmt">//把新个体加入新群体中 for(gene=class="num">1;gene<=GeneCount;gene++) Colony[gene][T]=child[gene]; class=class="str">"cmt">//占用一个位置, 增加计数器 T++; TotalOfChromosomesInHistory++; } class=class="str">"cmt">//--------------------------------------------------------------- break; class=class="str">"cmt">//--------------------- case class="num">2: class=class="str">"cmt">//----------------------人工诱变----------------------- class=class="str">"cmt">//如果新群体有空间, 创建一个新个体 if(T<ChromosomeCount) { ArtificialMutation(child,ReplicationOffset); class=class="str">"cmt">//把新个体加入新群体中 for(gene=class="num">1;gene<=GeneCount;gene++) Colony[gene][T]=child[gene]; class=class="str">"cmt">//占用一个位置,增加计数器 T++; TotalOfChromosomesInHistory++; } class=class="str">"cmt">//--------------------------------------------------------------- break; class=class="str">"cmt">//--------------------- case class="num">3: class=class="str">"cmt">//-------------使用借用基因创建个体----------- class=class="str">"cmt">//如果新群体有空间, 创建一个新个体 if(T<ChromosomeCount) { GenoMerging(child); class=class="str">"cmt">//把新个体加入新群体中 for(gene=class="num">1;gene<=GeneCount;gene++) Colony[gene][T]=child[gene]; class=class="str">"cmt">//占用一个位置,增加计数器 T++; TotalOfChromosomesInHistory++; } class=class="str">"cmt">//--------------------------------------------------------------- break; class=class="str">"cmt">//--------------------- class="kw">default: class=class="str">"cmt">//---------------------------交叉--------------------------- class=class="str">"cmt">//如果新群体有空间, 创建一个新个体 if(T<ChromosomeCount) { CrossingOver(child); class=class="str">"cmt">//把新个体加入新群体中 for(gene=class="num">1;gene<=GeneCount;gene++) Colony[gene][T]=child[gene]; class=class="str">"cmt">//占用一个位置, 增加计数器 T++; TotalOfChromosomesInHistory++; } class=class="str">"cmt">//--------------------------------------------------------------- break; class=class="str">"cmt">//---------------------} class=class="str">"cmt">//UGA 算子循环结束-- class=class="str">"cmt">//确定下代群体中每个个体的适应性 GetFitness(historyHromosomes); class=class="str">"cmt">//把后代安排到主群中 if(PopulChromosCount>=ChromosomeCount) { border=ChromosomeCount; PopulChromosCount=ChromosomeCount*class="num">2; } else { border=PopulChromosCount; PopulChromosCount+=ChromosomeCount; }
繁殖函数里的基因交叉与突变边界
遗传算法跑完一代后,先把子代写回种群矩阵,再调用去重函数为下一轮繁殖清场。下面这段双层循环把 Colony 里的染色体按偏移量搬进 Population,边界用 border 控制,避免覆盖上一代精英。 for(chromos=0;chromos<ChromosomeCount;chromos++) for(gene=0;gene<=GeneCount;gene++) Population[gene][chromos+border]=Colony[gene][chromos]; //为下一次繁殖准备种群 RemovalDuplicates(); } //函数结束 复制阶段由 Replication() 完成:先选两条父代染色体,逐基因在两者数值之间外扩 ReplicationOffset 比例作为子代取值区间。若父代值越界则夹回 RangeMinimum/RangeMaximum,C1>C2 时交换保证 Minimum<Maximum。 void Replication(double &child[], double ReplicationOffset) { double C1=0.0,C2=0.0,temp=0.0,Maximum=0.0,Minimum=0.0; int address_mama=0,address_papa=0; SelectTwoParents(address_mama,address_papa); for(int i=1;i<=GeneCount;i++) { C1 = Population[i][address_mama]; C2 = Population[i][address_papa]; if(C1 < RangeMinimum) C1 = RangeMinimum; if(C1 > RangeMaximum) C1 = RangeMaximum; if(C2 < RangeMinimum) C2 = RangeMinimum; if(C2 > RangeMaximum) C2 = RangeMaximum; if(C1>C2){ temp=C1; C1=C2; C2=temp; } Minimum = C1-((C2-C1)*ReplicationOffset); Maximum = C2+((C2-C1)*ReplicationOffset); if(Minimum < RangeMinimum) Minimum = RangeMinimum; if(Maximum > RangeMaximum) Maximum = RangeMaximum; temp=RNDfromCI(Minimum,Maximum); child[i]= NormalizeDouble(SelectInDiscreteSpace(temp,RangeMinimum,RangeMaximum,Precision,3),GeneNormalizeDigits); } } 自然突变由 NaturalMutation() 处理,传入的 NMutationProbability 先被夹到 0~100 区间,再选一条父代做单点变异。外汇与贵金属参数优化用这套逻辑时,过拟合概率偏高,实盘前务必用 MT5 帧测不同 ReplicationOffset 对收敛速度的影响。
for(chromos=class="num">0;chromos<ChromosomeCount;chromos++) for(gene=class="num">0;gene<=GeneCount;gene++) Population[gene][chromos+border]=Colony[gene][chromos]; class=class="str">"cmt">//为下一次繁殖准备种群 RemovalDuplicates(); } class=class="str">"cmt">//函数结束 class="type">void Replication(class="type">class="kw">double &child[], class="type">class="kw">double ReplicationOffset) { class="type">class="kw">double C1=class="num">0.0,C2=class="num">0.0,temp=class="num">0.0,Maximum=class="num">0.0,Minimum=class="num">0.0; class="type">int address_mama=class="num">0,address_papa=class="num">0; SelectTwoParents(address_mama,address_papa); for(class="type">int i=class="num">1;i<=GeneCount;i++) { C1 = Population[i][address_mama]; C2 = Population[i][address_papa]; if(C1 < RangeMinimum) C1 = RangeMinimum; if(C1 > RangeMaximum) C1 = RangeMaximum; if(C2 < RangeMinimum) C2 = RangeMinimum; if(C2 > RangeMaximum) C2 = RangeMaximum; if(C1>C2){ temp=C1; C1=C2; C2=temp; } Minimum = C1-((C2-C1)*ReplicationOffset); Maximum = C2+((C2-C1)*ReplicationOffset); if(Minimum < RangeMinimum) Minimum = RangeMinimum; if(Maximum > RangeMaximum) Maximum = RangeMaximum; temp=RNDfromCI(Minimum,Maximum); child[i]= NormalizeDouble(SelectInDiscreteSpace(temp,RangeMinimum,RangeMaximum,Precision,class="num">3),GeneNormalizeDigits); } } class="type">void NaturalMutation(class="type">class="kw">double &child[], class="type">class="kw">double NMutationProbability) { class="type">int address=class="num">0; class="type">class="kw">double prob=class="num">0.0; if(NMutationProbability<class="num">0.0) prob=class="num">0.0; if(NMutationProbability>class="num">100.0) prob=class="num">100.0; SelectOneParent(address); for(class="type">int i=class="num">1;i<=GeneCount;i++)