遗传算法 - 很简单!·综合运用
🧬

遗传算法 - 很简单!·综合运用

(3/3)·从基因到种群,手把手把 UGA 嵌进策略测试,避开二进制转换的坑

实战向进阶 第 3/3 篇
直接枚举在参数稍多时就让优化变成时间黑洞,很多人却还在硬跑。实数编码 GA 跳过二进制转换,改以浮点基因表达染色体,改起来更顺手。本篇收尾把前面两篇的原理落到可复用的工程实现上。

人工突变里的边界与越界处理

这段人工突变函数(ArtificialMutation)做的是:先随机挑两个父代,再对每个基因位在其取值区间内做偏移扩域后抽样。ReplicationOffset 控制子代基因相对父母区间向外扩张的倍数,代码里用 Minimum=C1-((C2-C1)*ReplicationOffset) 与 Maximum=C2+((C2-C1)*ReplicationOffset) 动态算出搜索边界。 无论父母基因还是扩域后的边界,都强制夹在 RangeMinimum 与 RangeMaximum 之间——任何超出都直接拉回区间端点,避免进化搜索跑出参数合法域。外汇与贵金属 EA 参数优化中,这种硬约束能降低过拟合到非法参数的风险,但模型本身仍可能因样本窗口偏差失效,属高风险操作。 随机数判定用了 MathRand()<16383.5,而 MathRand() 返回 0~32767,意味着约 50% 概率从父母区间左外侧抽样、另 50% 从右外侧抽样,子代有概率探索父母未覆盖的邻域。想验证的话,把 ReplicationOffset 从 0.5 调到 2.0,在 MT5 策略测试器里观察参数分布发散程度的变化。

MQL5 / C++
class="type">void ArtificialMutation(
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,p=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;
  p=MathRand();
  if(p<class="num">16383.5)
  {
   temp=RNDfromCI(RangeMinimum,Minimum);
   child[i]=
    NormalizeDouble(SelectInDiscreteSpace(temp,RangeMinimum,RangeMaximum,Precision,class="num">3),GeneNormalizeDigits);
  }
  else
  {
   temp=RNDfromCI(Maximum,RangeMaximum);
   child[i]=
    NormalizeDouble(SelectInDiscreteSpace(temp,RangeMinimum,RangeMaximum,Precision,class="num">3),GeneNormalizeDigits);
  }
 }
}

◍ 遗传算子的交叉与亲本抽取

在 MT5 的 EA 遗传优化框架里,单亲与双亲生成是两套独立路径。单亲复制走 SelectOneParent,直接把轮盘赌选出的个体地址塞进 child 数组;双亲交叉则先选一父一母,再随机切一刀换基因。 交叉点用 MathFloor((GeneCount-1)*(MathRand()/32767.5)) 算,MathRand 上限 32767,所以 32767.5 这个分母把随机值压到 [0,1) 区间。切点在第 address_of_gene+1 个基因前抄母亲,之后抄父亲,染色体长度由 GeneCount 决定。 SelectTwoParents 最多重试 10 次,直到 address_mama != address_papa 才 break。这意味着若种群多样性极差,可能退回两个相同个体自交,读者在 MT5 策略测试器里跑多代后会观察到子代方差收敛变慢。 NaturalSelection 内部先 ArrayResize(fit,PopulChromosCount) 再 ArrayInitialize(fit,0.0),适配种群规模动态变化;外汇与贵金属遗传优化属高风险实验,回测拟合不代表实盘概率占优。

MQL5 / C++
for(class="type">int i=class="num">1;i<=GeneCount;i++)
{
  class=class="str">"cmt">//-----------------选择父母------------------------
  SelectOneParent(address);
  class=class="str">"cmt">//--------------------------------------------------------
  child[i]=Population[i][address];
}
}
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//交叉.
class="type">void CrossingOver(
 class="type">class="kw">double &child[]
)
{
  class=class="str">"cmt">//-----------------------变量-------------------------------------
  class="type">int address_mama=class="num">0,address_papa=class="num">0;
  class=class="str">"cmt">//----------------------------------------------------------------------
  class=class="str">"cmt">//-----------------选择父母------------------------
  SelectTwoParents(address_mama,address_papa);
  class=class="str">"cmt">//--------------------------------------------------------
  class=class="str">"cmt">//决定突破点
  class="type">int address_of_gene=(class="type">int)MathFloor((GeneCount-class="num">1)*(MathRand()/class="num">32767.5));
  for(class="type">int i=class="num">1;i<=GeneCount;i++)
  {
    class=class="str">"cmt">//----复制母亲基因--------
    if(i<=address_of_gene+class="num">1)
      child[i]=Population[i][address_mama];
    class=class="str">"cmt">//----复制父亲基因--------
    else
      child[i]=Population[i][address_papa];
  }
}
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//选择两个亲本.
class="type">void SelectTwoParents(
 class="type">int &address_mama,
 class="type">int &address_papa
)
{
  class=class="str">"cmt">//-----------------------变量-------------------------------------
  class="type">int cnt=class="num">1;
  address_mama=class="num">0;class=class="str">"cmt">//种群中母亲个体的位置
  address_papa=class="num">0;class=class="str">"cmt">//种群中父亲个体的位置
  class=class="str">"cmt">//----------------------------------------------------------------------
  class=class="str">"cmt">//----------------------------选择亲本--------------------------
  class=class="str">"cmt">//尝试10次选择不同的亲本.
  class="kw">while(cnt<=class="num">10)
  {
    class=class="str">"cmt">//对于母亲个体
    address_mama=NaturalSelection();
    class=class="str">"cmt">//对于父亲个体
    address_papa=NaturalSelection();
    if(address_mama!=address_papa)
      break;
  }
  class=class="str">"cmt">//---------------------------------------------------------------------
}
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//选择一个亲本
class="type">void SelectOneParent(
 class="type">int &addressclass=class="str">"cmt">//亲本个体在种群中的地址
)
{
  class=class="str">"cmt">//-----------------------变量-------------------------------------
  address=class="num">0;
  class=class="str">"cmt">//----------------------------------------------------------------------
  class=class="str">"cmt">//----------------------------选择一个亲本--------------------------
  address=NaturalSelection();
  class=class="str">"cmt">//---------------------------------------------------------------------
}
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//自然选择.
class="type">int NaturalSelection()
{
  class=class="str">"cmt">//-----------------------变量-------------------------------------
  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,PopulChromosCount);
  ArrayInitialize(fit,class="num">0.0);

「轮盘赌选种与去重实现」

遗传算法里选染色体不能纯随机,得按适应度区间做加权抽取。下面这段先算 delta 做区间偏移,再给每个染色体分配一段 [fit[i][0], fit[i][1]) 的累加区间,最后用 RNDfromCI 落点反查命中的染色体索引 u。 double delta=(Population[0][0]-Population[0][PopulChromosCount-1])*0.01-Population[0][PopulChromosCount-1]; //---------------------------------------------------------------------- for(i=0;i<PopulChromosCount;i++) { fit[i][0]=start; fit[i][1]=start+MathAbs(Population[0][i]+delta); start=fit[i][1]; } p=RNDfromCI(fit[0][0],fit[PopulChromosCount-1][1]); for(u=0;u<PopulChromosCount;u++)

if((fit[u][0]<=p && p<fit[u][1])p==fit[u][1])

break; return(u); 抽完种还得清重复,否则种群多样性塌得快。RemovalDuplicates 用 chromosomeUnique[1000] 标重,初始全置 1 假定无重复,双层循环比对 GeneCount 个基因,只要有一个基因不等就 break,全相等则把后者的标记清 0。 void RemovalDuplicates() { //-----------------------变量------------------------------------- int chromosomeUnique[1000];//保存每个染色体唯一特性的数组: // 0-重复, 1-唯一 ArrayInitialize(chromosomeUnique,1); //假定没有重复 double PopulationTemp[][1000]; ArrayResize (PopulationTemp,GeneCount+1); ArrayInitialize(PopulationTemp,0.0); int Ge =0; //基因索引 int Ch =0; //染色体索引 int Ch2=0; //第二个染色体的索引 int cnt=0; //计数器 //---------------------------------------------------------------------- //----------------------删除重复染色体---------------------------1 //从一对中选择第一个做比较... for(Ch=0;Ch<PopulChromosCount;Ch++) { //如果不是重复的... if(chromosomeUnique[Ch]!=0) { //从一对中选择第二个... for(Ch2=0;Ch2<PopulChromosCount;Ch2++) { if(Ch!=Ch2 && chromosomeUnique[Ch2]!=0) { //相同基因计数器清零 cnt=0; //比较基因. 有相同基因出现 for(Ge=1;Ge<=GeneCount;Ge++) { if(Population[Ge][Ch]!=Population[Ge][Ch2]) break; 在 MT5 里把 GeneCount 设成你 EA 的参数维度(比如 5 个均线周期),PopulChromosCount 给 50~200,跑几百代后观察 chromosomeUnique 里被置 0 的比例;若比例超过 60% 说明交叉变异太弱,外汇与贵金属策略迭代易过早收敛,属高风险调参环节。

MQL5 / C++
class="type">class="kw">double delta=(Population[class="num">0][class="num">0]-Population[class="num">0][PopulChromosCount-class="num">1])*class="num">0.01-Population[class="num">0][PopulChromosCount-class="num">1];
class=class="str">"cmt">//----------------------------------------------------------------------
for(i=class="num">0;i<PopulChromosCount;i++)
{
  fit[i][class="num">0]=start;
  fit[i][class="num">1]=start+MathAbs(Population[class="num">0][i]+delta);
  start=fit[i][class="num">1];
}
p=RNDfromCI(fit[class="num">0][class="num">0],fit[PopulChromosCount-class="num">1][class="num">1]);
for(u=class="num">0;u<PopulChromosCount;u++)
  if((fit[u][class="num">0]<=p && p<fit[u][class="num">1]) || p==fit[u][class="num">1])
    break;
class="kw">return(u);
}
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//根据 VFF中的排序删除重复染色体
class="type">void RemovalDuplicates()
{
class=class="str">"cmt">//-----------------------变量-------------------------------------
  class="type">int          chromosomeUnique[class="num">1000];class=class="str">"cmt">//保存每个染色体唯一特性的数组:
class=class="str">"cmt">// class="num">0-重复, class="num">1-唯一
  ArrayInitialize(chromosomeUnique,class="num">1);   class=class="str">"cmt">//假定没有重复
  class="type">class="kw">double       PopulationTemp[][class="num">1000];
  ArrayResize(PopulationTemp,GeneCount+class="num">1);
  ArrayInitialize(PopulationTemp,class="num">0.0);
  class="type">int Ge =class="num">0;                            class=class="str">"cmt">//基因索引
  class="type">int Ch =class="num">0;                            class=class="str">"cmt">//染色体索引
  class="type">int Ch2=class="num">0;                            class=class="str">"cmt">//第二个染色体的索引
  class="type">int cnt=class="num">0;                            class=class="str">"cmt">//计数器
class=class="str">"cmt">//----------------------------------------------------------------------
class=class="str">"cmt">//----------------------删除重复染色体---------------------------class="num">1
class=class="str">"cmt">//从一对中选择第一个做比较...
  for(Ch=class="num">0;Ch<PopulChromosCount;Ch++)
  {
class=class="str">"cmt">//如果不是重复的...
    if(chromosomeUnique[Ch]!=class="num">0)
    {
class=class="str">"cmt">//从一对中选择第二个...
      for(Ch2=class="num">0;Ch2<PopulChromosCount;Ch2++)
      {
        if(Ch!=Ch2 && chromosomeUnique[Ch2]!=class="num">0)
        {
class=class="str">"cmt">//相同基因计数器清零
          cnt=class="num">0;
class=class="str">"cmt">//比较基因. 有相同基因出现
          for(Ge=class="num">1;Ge<=GeneCount;Ge++)
          {
            if(Population[Ge][Ch]!=Population[Ge][Ch2])
              break;

去重后怎么给染色体排座次

遗传算法跑完交叉变异,先得把种群里基因完全一致的染色体剔掉。上面那段逻辑就是双层循环比对:当某条染色体 Ch2 与参照染色体 Ch 的所有基因位都吻合,计数器 cnt 累加到等于 GeneCount,就把 chromosomeUnique[Ch2] 置 0,标记为重复。 去重后用一个 cnt 计数器把唯一染色体搬进 PopulationTemp,再把 PopulChromosCount 改写成实际唯一数,最后回填进主种群数组。这一步直接决定后续排序的样本规模——若初始种群 200 条、重复 40 条,排序只处理 160 条。 排序函数 PopulationRanking 里先给每条染色体建索引并取第 0 行(通常存适应度 VFF)填入 ValueOnIndexes。OptimizeMethod==1 时走冒泡式 while 循环,cnt 初始为 1、每轮无交换则 cnt 保持 0 退出,属于最朴素但零依赖的升序实现。 临时数组 PopulationTemp 被硬性开成 [][1000],GeneCount+1 行通过 ArrayResize 限定。如果你在 MT5 里跑多参数优化,GeneCount 超过 999 会直接数组越界报错,调参前先确认基因维度。

MQL5 / C++
else
   cnt++;
   class=class="str">"cmt">//如果相同基因数和总基因数相等
   class=class="str">"cmt">//..染色体会被认为是重复的
   if(cnt==GeneCount)
      chromosomeUnique[Ch2]=class="num">0;
   }
   }
   }
}
class=class="str">"cmt">//计算唯一染色体数量的计数器
cnt=class="num">0;
class=class="str">"cmt">//把唯一的染色体复制到临时数组中
for(Ch=class="num">0;Ch<PopulChromosCount;Ch++)
{
class=class="str">"cmt">//如果染色体是唯一的就复制, 否则到下一个
if(chromosomeUnique[Ch]==class="num">1)
{
   for(Ge=class="num">0;Ge<=GeneCount;Ge++)
      PopulationTemp[Ge][cnt]=Population[Ge][Ch];
   cnt++;
}
}
class=class="str">"cmt">//给 "全部染色体"变量赋值为唯一染色体计数器的值
PopulChromosCount=cnt;
class=class="str">"cmt">//把唯一染色体返回到
class=class="str">"cmt">//综合种群的临时存储数组
for(Ch=class="num">0;Ch<PopulChromosCount;Ch++)
   for(Ge=class="num">0;Ge<=GeneCount;Ge++)
      Population[Ge][Ch]=PopulationTemp[Ge][Ch];
class=class="str">"cmt">//=================================================================class="num">1
class=class="str">"cmt">//----------------种群排序---------------------------class="num">2
PopulationRanking();
class=class="str">"cmt">//=================================================================class="num">2
}
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//种群排序.
class="type">void PopulationRanking()
{
class=class="str">"cmt">//-----------------------变量-------------------------------------
class="type">int cnt=class="num">1, i = class="num">0, u = class="num">0;
class="type">class="kw">double          PopulationTemp[][class="num">1000];           class=class="str">"cmt">//临时种群
ArrayResize(PopulationTemp,GeneCount+class="num">1);
ArrayInitialize(PopulationTemp,class="num">0.0);
class="type">int            Indexes[];                         class=class="str">"cmt">//染色体索引
ArrayResize(Indexes,PopulChromosCount);
ArrayInitialize(Indexes,class="num">0);
class="type">int   t0=class="num">0;
class="type">class="kw">double          ValueOnIndexes[];                 class=class="str">"cmt">//对应染色体索引的VFF
ArrayResize(ValueOnIndexes,PopulChromosCount);
ArrayInitialize(ValueOnIndexes,class="num">0.0); class="type">class="kw">double t1=class="num">0.0;
class=class="str">"cmt">//----------------------------------------------------------------------
class=class="str">"cmt">//填充临时数组 temp2 的索引并且
class=class="str">"cmt">//从排好顺序的数组中复制第一行
for(i=class="num">0;i<PopulChromosCount;i++)
{
   Indexes[i] = i;
   ValueOnIndexes[i] = Population[class="num">0][i];
}
if(OptimizeMethod==class="num">1)
{
   class="kw">while(cnt>class="num">0)
   {
      cnt=class="num">0;
      for(i=class="num">0;i<PopulChromosCount-class="num">1;i++)
      {

◍ 染色体排序与离散空间取值的实现细节

遗传算法里每一代跑完,都要按适应度把种群排个序,这段代码用冒泡思路在索引数组上做交换,而不是直接搬动二维种群矩阵,能少写不少内存拷贝。 排序方向由外部标志决定:若要求从优到劣,就比 ValueOnIndexes[i] 是否大于后一位,是则交换索引与对应值;反向时逻辑镜像,用小于号判断。每次发生交换 cnt 自增,外层 while(cnt>0) 保证未完全有序就再来一轮,最坏情况比较次数为 (PopulChromosCount-1) 乘上可能的轮数。 排完序后,用 Indexes[u] 作为列号,把 Population 按行映射到 PopulationTemp,再复制回去——这样原种群按适应度重排,后续选育直接从头截取即可。外汇与贵金属参数优化属高风险,回测顺手不代表实盘概率占优。 随机数发生器 RNDfromCI 把 MathRand() 除以 32767.5 映射到 [0,1),再线性拉伸到给定区间;注意除数是 32767.5 而非 32768,使上限端点理论可达概率略高。 SelectInDiscreteSpace 处理参数必须落在离散网格的场景:先防住 InMax<InMin 的边界颠倒,越界直接夹回边界;step 为 0 原样返回。最后用 MathCeil((InMax-InMin)/step) 反算实际步长,避免整数格点除不尽导致漏掉末端值。

MQL5 / C++
if(ValueOnIndexes[i]>ValueOnIndexes[i+class="num">1])
{
  class=class="str">"cmt">//-----------------------
  t0 = Indexes[i+class="num">1];
  t1 = ValueOnIndexes[i+class="num">1];
  Indexes    [i+class="num">1] = Indexes[i];
  ValueOnIndexes    [i+class="num">1] = ValueOnIndexes[i];
  Indexes    [i] = t0;
  ValueOnIndexes    [i] = t1;
  class=class="str">"cmt">//-----------------------
  cnt++;
}
      }
    }
  }
  else
  {
    class="kw">while(cnt>class="num">0)
    {
      cnt=class="num">0;
      for(i=class="num">0;i<PopulChromosCount-class="num">1;i++)
      {
        if(ValueOnIndexes[i]<ValueOnIndexes[i+class="num">1])
        {
          class=class="str">"cmt">//-----------------------
          t0 = Indexes[i+class="num">1];
          t1 = ValueOnIndexes[i+class="num">1];
          Indexes    [i+class="num">1] = Indexes[i];
          ValueOnIndexes    [i+class="num">1] = ValueOnIndexes[i];
          Indexes    [i] = t0;
          ValueOnIndexes    [i] = t1;
          class=class="str">"cmt">//-----------------------
          cnt++;
        }
      }
    }
  }
  class=class="str">"cmt">//根据获得的索引建立排好序的数组
  for(i=class="num">0;i<GeneCount+class="num">1;i++)
    for(u=class="num">0;u<PopulChromosCount;u++)
      PopulationTemp[i][u]=Population[i][Indexes[u]];
  class=class="str">"cmt">//把排好序的数组复制回来
  for(i=class="num">0;i<GeneCount+class="num">1;i++)
    for(u=class="num">0;u<PopulChromosCount;u++)
      Population[i][u]=PopulationTemp[i][u];
}
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//在给定区间内的随机数发生器
class="type">class="kw">double RNDfromCI(class="type">class="kw">double RangeMinimum,class="type">class="kw">double RangeMaximum)
{ class="kw">return(RangeMinimum+((RangeMaximum-RangeMinimum)*MathRand()/class="num">32767.5));}
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//在离散空间内选择.
class=class="str">"cmt">//模式:
class=class="str">"cmt">//class="num">1-下方最接近
class=class="str">"cmt">//class="num">2-上方最接近
class=class="str">"cmt">//任何最接近
class="type">class="kw">double SelectInDiscreteSpace(
 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="type">int     RoundMode
)
{
  if(step==class="num">0.0)
    class="kw">return(In);
  class=class="str">"cmt">// 保证边界正确性
  if( InMax < InMin )
  {
    class="type">class="kw">double temp = InMax; InMax = InMin; InMin = temp;
  }
  class=class="str">"cmt">// 在突破中 - 返回突破的边界
  if( In < InMin ) class="kw">return( InMin );
  if( In > InMax ) class="kw">return( InMax );
  if( InMax == InMin || step <= class="num">0.0 ) class="kw">return( InMin );
  class=class="str">"cmt">// 带到指定的尺度中
  step = (InMax - InMin) / MathCeil( (InMax - InMin) / step );

「三种取整模式的分档落点」

这段分支直接决定了外部输入值 In 被吸附到网格的哪一条刻度上,区别只在舍入方向。RoundMode=1 向下靠档,用 MathFloor 截去小数;RoundMode=2 向上靠档,用 MathCeil 进一位;其余情况走 MathRound 就近取整。 以 InMin=1.1000、step=0.0025、In=1.1018 为例:模式1得到 1.1000,模式2得到 1.1025,默认模式得到 1.1025(因 0.72 个步长更靠近上档)。在 MT5 里改 RoundMode 这个值,同一笔报价会映射出不同挂单位置,外汇与贵金属杠杆品种下误用可能放大滑点风险。 实盘前建议把三种模式各跑一遍历史 tick,确认你想要的「保守接单」还是「激进接单」倾向,再固化参数。

MQL5 / C++
  class="kw">switch( RoundMode )
  {
  case class="num">1:  class="kw">return( InMin + step * MathFloor( ( In - InMin ) / step ) );
  case class="num">2:  class="kw">return( InMin + step * MathCeil( ( In - InMin ) / step ) );
  class="kw">default: class="kw">return( InMin + step * MathRound( ( In - InMin ) / step ) );
  }

用 UGA 在两个例子里验证进化搜索

把优化问题按染色体语义分成两类:基因型与表型一致时,基因值就是目标函数自变量,例如求 Skin 函数极值;不一致时,基因需要先解释成具体含义再算适应度,例如后面用 ZigZag 替代顶点的问题。 先拿已知答案的 Skin 函数压测算法:区间 [-5,5] 上理论最小值为 -4.3182(位置约 3.07~3.32),最大值为 14.0606(位置约 -3.32~-3.07)。跑完脚本后 Comment() 输出的数值与理论值吻合,说明引擎本身没掉链子。 第二个例子更贴近实战。用 M1 周期 GBPJPY 最近 100 根柱、点差按 80 点(五位报价)计,先枚举 ExtDepth 找 ZZ 最佳参数:ExtDepth=3 时盈利 4077 点,19 个指标顶点;深度加大后顶点数和盈利都降。 真正麻烦的是:100 根柱每根有「最高/最低/无顶点」三种状态,直接枚举是 3^100 ≈ 5.15378e47 种组合。按每秒 1e8 次算,要 1.6e32 年——宇宙年龄都远远不够。这就是典型基因型≠表型:把基因搜值区间定为 [0,5],约定 [0,1] 表最高顶点、[4,5] 表最低顶点、[1,4] 表无顶点。 初代随机种群基本都是负几百点的垃圾,但给「无顶点」个体强行标 -10000000.0 适应度压到最底层,逼进化往有交易的方向走。最终 UGA 只用了 17929 次计算、21.7 秒,找出总盈利 4939 点的替代顶点排列。结论倾向:ZZ 之外确实存在其他入口点,但 GBPJPY 等高杠杆品种波动剧烈,这类历史拟合结果不代表未来可稳定复用,实盘前务必自行回测。

MQL5 / C++
class="macro">#class="kw">property script_show_inputs
class=class="str">"cmt">//+----------------------------------------------------------------------+
class="macro">#include "UGAlib.mqh"
class="macro">#include "Skin.mqh" class=class="str">"cmt">//测试函数
class=class="str">"cmt">//+----------------------------------------------------------------------+
class=class="str">"cmt">//----------------------------------------------------------------------
class=class="str">"cmt">//----------------------外来参数--------------------------------
class="kw">input class="type">class="kw">string GenofundParam        = "----基因池参数----";
class="kw">input class="type">int    ChromosomeCount_P    = class="num">50;    class=class="str">"cmt">//群体中的染色体数
class="kw">input class="type">int    GeneCount_P          = class="num">2;     class=class="str">"cmt">//基因数
class="kw">input class="type">int    FFNormalizeDigits_P  = class="num">4;     class=class="str">"cmt">//适应度符号数
class="kw">input class="type">int    GeneNormalizeDigits_P= class="num">4;     class=class="str">"cmt">//基因数
class="kw">input class="type">int    Epoch_P              = class="num">50;    class=class="str">"cmt">//无进展世代数
class=class="str">"cmt">//---
class="kw">input class="type">class="kw">string GA_OperatorParam     = "----算子参数----";
class="kw">input class="type">class="kw">double ReplicationPortion_P = class="num">100.0; class=class="str">"cmt">//复制比例.
class="kw">input class="type">class="kw">double NMutationPortion_P   = class="num">10.0;  class=class="str">"cmt">//自然突变比例.
class="kw">input class="type">class="kw">double ArtificialMutation_P= class="num">10.0;  class=class="str">"cmt">//人工突变比例.
class="kw">input class="type">class="kw">double GenoMergingPortion_P= class="num">20.0;  class=class="str">"cmt">//借用基因比例.
class="kw">input class="type">class="kw">double CrossingOverPortion_P= class="num">20.0;  class=class="str">"cmt">//交叉比例.
class=class="str">"cmt">//---
class="kw">input class="type">class="kw">double ReplicationOffset_P  = class="num">0.5;   class=class="str">"cmt">//区间边界转换系数
class="kw">input class="type">class="kw">double NMutationProbability_P= class="num">5.0;  class=class="str">"cmt">//每个基因突变可能性百分率
class=class="str">"cmt">//---
class="kw">input class="type">class="kw">string OptimisationParam    = "----优化参数----";
class="kw">input class="type">class="kw">double RangeMinimum_P       = -class="num">5.0;  class=class="str">"cmt">//最小搜索范围

◍ 把遗传算法参数灌进 UGA 主循环

这段脚本把外部输入和全局变量对接到通用遗传算法(UGA)里,跑完会打印耗时毫秒数。注意 RangeMaximum_P 默认 5.0、Precision_P 默认 0.0001,意味着搜索空间上限 5、最小步长万分之一,优化方法选 1 代表求最小误差,选其他值则求最大。 OnStart 里先给 Chromosome 数组按 GeneCount+1 扩容并清零,再把复制比例、自然突变比例、人工突变比例、借用基因比例、交叉比例以及区间边界转换系数、单基因突变概率一股脑传给 UGA。跑前用 GetTickCount 记起点,跑完再取终点,Print 出两者差就是本次实现时间(mc)。 脚本尾部还有一组独立 input:History=100 根 K 线、Spred=80.0 点差、Depth=5 一次性深度、loop=true 是否枚举。外汇与贵金属杠杆高、点差跳动快,这组默认值在实盘前建议在 MT5 策略测试器里按品种重设,否则回测误差可能偏大。 别把默认点差当真实成本 Spred=80.0 只是脚本占位,黄金实际点差常在 20~50 点间跳,欧美也可能跌破 10 点;直接拿 80 去跑 UGA,筛选出的参数集在真账户上失效概率偏高。开 MT5 把 Spred 改成经纪商实时值再跑一遍更稳。

MQL5 / C++
class="kw">input class="type">class="kw">double RangeMaximum_P          = class="num">5.0;    class=class="str">"cmt">//最大搜索范围
class="kw">input class="type">class="kw">double Precision_P              = class="num">0.0001;class=class="str">"cmt">//所需的精确度
class="kw">input class="type">int    OptimizeMethod_P         = class="num">1;      class=class="str">"cmt">//优化方法:class="num">1-最小,其他-最大
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//----------------------全局变量-----------------------------
class="type">class="kw">double ERROR=class="num">0.0;class=class="str">"cmt">//基因中的平均错误
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//--------------------------程序主体--------------------------------
class="type">int OnStart()
{
  class=class="str">"cmt">//-----------------------变量-------------------------------------
  class=class="str">"cmt">//准备 UGA 的全局变量
  ChromosomeCount=ChromosomeCount_P; class=class="str">"cmt">//群体中的染色体数
  GeneCount      =GeneCount_P;       class=class="str">"cmt">//基因数
  RangeMinimum   =RangeMinimum_P;    class=class="str">"cmt">//最小搜索范围
  RangeMaximum   =RangeMaximum_P;    class=class="str">"cmt">//最大搜索范围
  Precision      =Precision_P;       class=class="str">"cmt">//搜索步长
  OptimizeMethod =OptimizeMethod_P;  class=class="str">"cmt">//class="num">1-最小, 其他 - 最大
  FFNormalizeDigits   = FFNormalizeDigits_P;   class=class="str">"cmt">//适应度小数点位数
  GeneNormalizeDigits = GeneNormalizeDigits_P;class=class="str">"cmt">//基因小数点位数
  ArrayResize(Chromosome,GeneCount+class="num">1);
  ArrayInitialize(Chromosome,class="num">0);
  Epoch=Epoch_P;                      class=class="str">"cmt">//无进展世代数
  class=class="str">"cmt">//----------------------------------------------------------------------
  class=class="str">"cmt">//局部变量
  class="type">int time_start=GetTickCount(),time_end=class="num">0;
  class=class="str">"cmt">//----------------------------------------------------------------------
  class=class="str">"cmt">//运行 UGA 主函数
  UGA(
   ReplicationPortion_P, class=class="str">"cmt">//复制比例.
   NMutationPortion_P,   class=class="str">"cmt">//自然突变比例.
   ArtificialMutation_P, class=class="str">"cmt">//人工突变比例.
   GenoMergingPortion_P, class=class="str">"cmt">//借用基因比例.
   CrossingOverPortion_P,class=class="str">"cmt">//交叉比例.
   class=class="str">"cmt">//---
   ReplicationOffset_P,  class=class="str">"cmt">//区间边界转换系数
   NMutationProbability_Pclass=class="str">"cmt">//每个基因突变可能性百分率
   );
  class=class="str">"cmt">//----------------------------------
  time_end=GetTickCount();
  class=class="str">"cmt">//----------------------------------
  Print(time_end-time_start," mc - 实现时间");
  class=class="str">"cmt">//----------------------------------
  class="kw">return(class="num">0);
}
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class="macro">#class="kw">property script_show_inputs
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//----------------------外来参数--------------------------------
class="kw">input  class="type">int    History=class="num">100;
class="kw">input  class="type">class="kw">double Spred  =class="num">80.0;
class="kw">input  class="type">int    Depth  =class="num">5;   class=class="str">"cmt">//为“一次性”使用
class="kw">input  class="type">bool   loop   =true;class=class="str">"cmt">//是否使用枚举
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————

「用循环扫不同深度下的之字线极值」

这段程序主体干的事,是把 ZigZag 指标在不同 depth 参数下的峰值序列抓出来,算一波总波动点数。外汇和贵金属波动受杠杆与跳空影响,这类统计只反映历史形态,实盘复制有高风险,结论都带概率性。 核心思路是从 depth=3 起逐步加到 200,每档都用 iCustom 挂系统自带的 ZigZag,复制 History 根 K 线的缓冲区。若 100 秒内 BarsCalculated 仍不大于 0 就跳过等待,复制失败直接 Print 错误并 return。 提取峰值时只保留 NormalizeDouble 后大于 0 的缓冲区值,塞进 PeaksOfZigzag 数组;随后把相邻峰值差换算成点数除以 Point 再减 Spred(点差),累加得 PipsSum。当 PeaksCount 小于等于 2 时 flag 置 false 退出循环,否则打印 depth、峰值数和 PipsSum 并 depth+1 继续。 你在 MT5 里把 History 和 Spred 先按品种设好,跑一遍就能看到不同 ZigZag 灵敏度对应的历史波动总量,用来判断哪一档深度更贴合你盯的波段节奏。

MQL5 / C++
class="type">void OnStart()
{
   class="type">class="kw">double ZigzagBuffer [];class=class="str">"cmt">//保存之字线指标缓冲区
   class="type">class="kw">double PeaksOfZigzag[];class=class="str">"cmt">//保存之字线极值数值
   class="type">int    Zigzag_handle;  class=class="str">"cmt">//指标句柄
   ArraySetAsSeries(ZigzagBuffer,true);
   ArrayResize(PeaksOfZigzag,History);
   class="type">int    depth=class="num">3;
   class="type">class="kw">double PipsSum=class="num">0.0;
   class="type">int    PeaksCount=class="num">0;
   class="type">bool   flag=true;
   if(loop==true)
   {
      class="kw">while(depth<class="num">200 && flag==true)
      {
         Zigzag_handle=iCustom(NULL,class="num">0,"ZigZag",depth);
         ResetLastError();
         for(class="type">int i=class="num">0;i<class="num">100;i++)
         {
            if(BarsCalculated(Zigzag_handle)>class="num">0)
               break;
            Sleep(class="num">1000);
         }
         class="type">int copied=CopyBuffer(Zigzag_handle,class="num">0,class="num">0,History,ZigzagBuffer);
         if(copied<=class="num">0)
         {
            Print("无法复制指标缓冲区. 错误 =",GetLastError(),"  复制数=",copied);
            class="kw">return;
         }
         PipsSum=class="num">0.0;
         PeaksCount=class="num">0;
         for(class="type">int u=class="num">0;u<History;u++)
         {
            if(NormalizeDouble(ZigzagBuffer[u],Digits())>class="num">0.0)
            {
               PeaksOfZigzag[PeaksCount]=NormalizeDouble(ZigzagBuffer[u],Digits());
               PeaksCount++;
            }
         }
         for(class="type">int V=class="num">0;V<PeaksCount-class="num">1;V++)
            PipsSum+=NormalizeDouble((MathAbs(PeaksOfZigzag[V]-PeaksOfZigzag[V+class="num">1]))/Point(),Digits())-Spred;
         if(PeaksCount<=class="num">2)
            flag=false;
         else
         {
            Print(depth," ",PeaksCount," ",PipsSum);
            depth+=class="num">1;
         }
      }
   }
   else
   {

抓取 ZigZag 拐点并累加波动点数

这段逻辑用来在脚本里加载 ZigZag 指标,并把历史中的有效拐点抽出来统计总波动。注意它用 iCustom 挂的是名为 "ZigZag" 的自定义指标,Depth 作为参数传入,意味着不同拐点深度会生成不同样本。 [CODE] Zigzag_handle=iCustom(NULL,0,"ZigZag",Depth); ResetLastError(); for(int i=0;i<History;i++) { if(BarsCalculated(Zigzag_handle)>0) break; Sleep(1000); } int copied=CopyBuffer(Zigzag_handle,0,0,History,ZigzagBuffer); if(copied<=0) { Print("无法复制指标缓冲区. 错误 =",GetLastError()," 复制数=",copied); return; } [/CODE] 先建句柄再 ResetLastError 清错误码;循环里最多等 History 秒(每次 Sleep(1000))等 BarsCalculated 大于 0,才跳出。CopyBuffer 复制 0 号缓冲区到 ZigzagBuffer,复制数 <=0 就打印错误并退出——这是 MT5 里读自定义指标最常见的防卡死写法。 [CODE] for(int u=0;u<History;u++) { if(NormalizeDouble(ZigzagBuffer[u],Digits())>0.0) { PeaksOfZigzag[PeaksCount]=NormalizeDouble(ZigzagBuffer[u],Digits()); PeaksCount++; } } [/CODE] ZigZag 在非拐点处缓冲值为 0,所以只挑大于 0.0 的存进 PeaksOfZigzag 数组,PeaksCount 同步自增。这样我们就拿到了按时间排列的拐点价格序列。 [CODE] for(int V=0;V<PeaksCount-1;V++) { PipsSum+=NormalizeDouble((MathAbs(PeaksOfZigzag[V]-PeaksOfZigzag[V+1]))/Point(),Digits())-Spred; } Print(Depth," ",PeaksCount," ",PipsSum); [/CODE] 相邻拐点价差除以 Point() 转成点数,减去 Spred(点差)后累加进 PipsSum。最后打印 Depth、拐点数量、总净点数三列——跑一遍就能看某 Depth 下历史波动的粗略容量。外汇和贵金属杠杆高,点差侵蚀真实利润,这组数据只反映历史样本,不预示后续走势。 后面紧接的 input 块是基因算法池的参数:染色体数 100、基因数 100、无进展世代数 50,适配度与基因都保留 0 位小数。改 ChromosomeCount_P 或 Epoch_P 会直接动摇寻优耗时与过拟合概率,建议先在策略测试器里用小样本跑通再放大。

MQL5 / C++
Zigzag_handle=iCustom(NULL,class="num">0,"ZigZag",Depth);
ResetLastError();
for(class="type">int i=class="num">0;i<History;i++)
{
  if(BarsCalculated(Zigzag_handle)>class="num">0)
    break;
  Sleep(class="num">1000);
}
class="type">int copied=CopyBuffer(Zigzag_handle,class="num">0,class="num">0,History,ZigzagBuffer);
if(copied<=class="num">0)
{
  Print("无法复制指标缓冲区. 错误 =",GetLastError(),"  复制数=",copied);
  class="kw">return;
}
for(class="type">int u=class="num">0;u<History;u++)
{
  if(NormalizeDouble(ZigzagBuffer[u],Digits())>class="num">0.0)
  {
    PeaksOfZigzag[PeaksCount]=NormalizeDouble(ZigzagBuffer[u],Digits());
    PeaksCount++;
  }
}
for(class="type">int V=class="num">0;V<PeaksCount-class="num">1;V++)
{
  PipsSum+=NormalizeDouble((MathAbs(PeaksOfZigzag[V]-PeaksOfZigzag[V+class="num">1]))/Point(),Digits())-Spred;
}
Print(Depth," ",PeaksCount," ",PipsSum);

class="macro">#class="kw">property script_show_inputs
class="macro">#include "UGAlib.mqh"
class="kw">input class="type">class="kw">string GenofundParam       = "----基因池参数----";
class="kw">input class="type">int    ChromosomeCount_P   = class="num">100;    class=class="str">"cmt">//群体中的染色体数
class="kw">input class="type">int    GeneCount_P         = class="num">100;    class=class="str">"cmt">//基因数
class="kw">input class="type">int    FFNormalizeDigits_P = class="num">0;      class=class="str">"cmt">//适应度小数点位数
class="kw">input class="type">int    GeneNormalizeDigits_P= class="num">0;     class=class="str">"cmt">//基因小数点位数
class="kw">input class="type">int    Epoch_P             = class="num">50;     class=class="str">"cmt">//无进展世代数

◍ 把遗传算子参数搬进EA全局变量

这段输入块把遗传算法的算子权重和优化边界全部暴露成 EA 外部参数,方便在 MT5 策略测试器里直接拖滑杆调。复制比例默认 100.0、自然突变与人工突变各 10.0、借用基因与交叉各 20.0,意味着每一代子群构成里复制母体占绝对大头,突变合计才两成,收敛倾向偏稳但容易陷局部极值。 搜索区间写死在 RangeMinimum_P=0.0 到 RangeMaximum_P=5.0,步长 Precision_P=1.0,OptimizeMethod_P=2 代表走最大化适应度(填 1 才是最小化)。点差参数 Spred=80.0 是按老式点数计的常量,跑外汇或贵金属前务必按自己经纪商小数位重算,否则回测盈利可能是虚的。 下面 OnStart 开头这一段干的事很单纯:把 input 的值逐一对拷到全局变量,并给染色体数组按基因数+1维resize。注意 GeneCount 来自另一段输入(本小节未列),若它和 RangeMaximum 不匹配,ArrayResize 出来的矩阵会直接让后续适应度计算越界。

MQL5 / C++
class="kw">input class="type">class="kw">string GA_OperatorParam      =      "----算子参数----";
class="kw">input class="type">class="kw">double ReplicationPortion_P  = class="num">100.0; class=class="str">"cmt">//复制比例.
class="kw">input class="type">class="kw">double NMutationPortion_P    = class="num">10.0;  class=class="str">"cmt">//自然突变比例.
class="kw">input class="type">class="kw">double ArtificialMutation_P  = class="num">10.0;  class=class="str">"cmt">//人工突变比例.
class="kw">input class="type">class="kw">double GenoMergingPortion_P  = class="num">20.0;  class=class="str">"cmt">//借用基因比例.
class="kw">input class="type">class="kw">double CrossingOverPortion_P = class="num">20.0;  class=class="str">"cmt">//交叉比例.
class="kw">input class="type">class="kw">double ReplicationOffset_P   = class="num">0.5;   class=class="str">"cmt">//区间转换系数
class="kw">input class="type">class="kw">double NMutationProbability_P= class="num">5.0;   class=class="str">"cmt">//每个基因突变可能性百分率
class=class="str">"cmt">//---
class="kw">input class="type">class="kw">string OptimisationParam     =      "----优化参数----";
class="kw">input class="type">class="kw">double RangeMinimum_P        = class="num">0.0;   class=class="str">"cmt">//最小搜索范围
class="kw">input class="type">class="kw">double RangeMaximum_P        = class="num">5.0;   class=class="str">"cmt">//最大搜索范围
class="kw">input class="type">class="kw">double Precision_P           = class="num">1.0;   class=class="str">"cmt">//请求的精确度
class="kw">input class="type">int   OptimizeMethod_P       = class="num">2;     class=class="str">"cmt">//优化方法:class="num">1-最小,其他 -最大
class="kw">input class="type">class="kw">string Other                 =      "----其他----";
class="kw">input class="type">class="kw">double Spred                 = class="num">80.0;
class="kw">input class="type">bool  Show                   = true;
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//----------------------全局变量-----------------------------
class="type">class="kw">double   Hight  [];
class="type">class="kw">double   Low    [];
class="type">class="kw">datetime Time   [];
class="type">class="kw">datetime Ti     [];
class="type">class="kw">double   Peaks  [];
class="type">bool     show;
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//--------------------------程序主体--------------------------------
class="type">int OnStart()
{
  class=class="str">"cmt">//-----------------------变量-------------------------------------
  class=class="str">"cmt">//为UGA准备全局变量
  ChromosomeCount=ChromosomeCount_P; class=class="str">"cmt">//群体中的染色体数
  GeneCount       =GeneCount_P;      class=class="str">"cmt">//基因数
  RangeMinimum   =RangeMinimum_P;    class=class="str">"cmt">//最小搜索范围
  RangeMaximum   =RangeMaximum_P;    class=class="str">"cmt">//最大搜索范围
  Precision       =Precision_P;      class=class="str">"cmt">//搜索步长
  OptimizeMethod =OptimizeMethod_P;  class=class="str">"cmt">//class="num">1-最小, 其他 - 最大
  FFNormalizeDigits   = FFNormalizeDigits_P;   class=class="str">"cmt">//适应度小数点位数
  GeneNormalizeDigits = GeneNormalizeDigits_P;class=class="str">"cmt">//基因小数点位数
  ArrayResize(Chromosome,GeneCount+class="num">1);
  ArrayInitialize(Chromosome,class="num">0);
  Epoch=Epoch_P;                     class=class="str">"cmt">//无进展世代数
  class=class="str">"cmt">//----------------------------------------------------------------------
  class=class="str">"cmt">//准备全局变量

「把高低价与时间序列喂给遗传算法」

这段逻辑出现在指标或 EA 的 OnInit/OnCalculate 收尾处,先把最近 GeneCount+1 根 K 线的高、低、时间拷进数组,并统一改成时间序列索引(下标 0 是最新一根)。注意 CopyHigh/CopyLow/CopyTime 的第三参数都是 0,代表从当前品种当前图表抓起,换周期得自己改第二参数。 ArraySetAsSeries(Hight,true); CopyHigh(NULL,0,0,GeneCount+1,Hight); ArraySetAsSeries(Low,true); CopyLow(NULL,0,0,GeneCount+1,Low); ArraySetAsSeries(Time,true); CopyTime(NULL,0,0,GeneCount+1,Time); 上面三行就是典型的逐列取数。若 GeneCount 设为 500,则实际取 501 个样本点,数组边界要留 +1 余量,否则最后一波迭代会越界。 清场再跑是必须的 ObjectsDeleteAll(0,-1,-1); ChartRedraw(0); 先删光旧对象重绘,避免上一次 UGA 画的峰谷线堆叠。之后调用 UGA(...) 主函数,传入复制比例、自然/人工突变比例、借用基因比例、交叉比例,以及区间边界转换系数和每基因突概率。这些参数直接决定寻优倾向,外汇与贵金属波动大、滑点高,参数过激可能让信号在实盘频繁反转。 计时只为一事:看卡不卡 time_start=GetTickCount(); 跑前记 tick,跑完 time_end-time_start 用 Print 打出毫秒。若返回值经常超过 200 мс,说明 GeneCount 或种群规模在该品种上偏重,可下调 GeneCount 到 300 左右验证帧率。 ServiceFunction 是被 UGA 回调的展示层。show 为 true 时才进 if 块累加 PipsSum 等统计;不需要可视化就把函数体留空,能省掉每代重绘开销。建议你在 MT5 里把 show 初值设为 false 跑一遍,对比 Print 出的执行时间差。

MQL5 / C++
ArraySetAsSeries(Hight,true);  CopyHigh(NULL,class="num">0,class="num">0,GeneCount+class="num">1,Hight);
ArraySetAsSeries(Low,true);    CopyLow(NULL,class="num">0,class="num">0,GeneCount+class="num">1,Low);
ArraySetAsSeries(Time,true);   CopyTime(NULL,class="num">0,class="num">0,GeneCount+class="num">1,Time);
ArrayResize(Ti,GeneCount+class="num">1);ArrayInitialize(Ti,class="num">0);
ArrayResize(Peaks,GeneCount+class="num">1);ArrayInitialize(Peaks,class="num">0.0);
show=Show;
class=class="str">"cmt">//----------------------------------------------------------------------
class=class="str">"cmt">//局部变量
class="type">int time_start=GetTickCount(),time_end=class="num">0;
class=class="str">"cmt">//----------------------------------------------------------------------
class=class="str">"cmt">//删除对象并重画图表
ObjectsDeleteAll(class="num">0,-class="num">1,-class="num">1);
ChartRedraw(class="num">0);
class=class="str">"cmt">//运行UGA主函数
UGA(
  ReplicationPortion_P, class=class="str">"cmt">//复制比例.
  NMutationPortion_P,   class=class="str">"cmt">//自然突变复制比例.
  ArtificialMutation_P, class=class="str">"cmt">//人工突变比例.
  GenoMergingPortion_P, class=class="str">"cmt">//借用基因比例.
  CrossingOverPortion_P,class=class="str">"cmt">//交叉比例.
  class=class="str">"cmt">//---
  ReplicationOffset_P,  class=class="str">"cmt">//区间边界转换系数
  NMutationProbability_Pclass=class="str">"cmt">//每个基因突变可能性百分率
  );
class=class="str">"cmt">//----------------------------------
class=class="str">"cmt">//在屏幕上显示最后结果
show=true;
ServiceFunction();
class=class="str">"cmt">//----------------------------------
time_end=GetTickCount();
class=class="str">"cmt">//----------------------------------
Print(time_end-time_start," мс - 执行时间");
class=class="str">"cmt">//----------------------------------
class="kw">return(class="num">0);
}
class="type">void ServiceFunction()
{
 if(show==true)
 {
 class=class="str">"cmt">//-----------------------变量-----------------------------------
 class="type">class="kw">double PipsSum=class="num">0.0;

适应度函数怎么把波峰波谷折算成点值

遗传算法里每个染色体代表一套买卖规则,适应度函数负责把这套规则在历史 K 线上的表现量化成可比较的数字。上面这段代码用 temp<=1.0 判定为高点、temp>=4.0 判定为低点,把候选拐点塞进 Peaks[] 数组并累计 PeaksCount。 第一段逻辑出现在回测绘制分支:除了记录价格,还顺手用 OBJ_TREND 把相邻峰谷连成线段,并扣除 Spred(点差)后得到 PipsSum。注意 PipsSum 用 (MathAbs(峰差)/Point()) 再减点差,意味着每跨一个峰谷就被折算成净点数——外汇和贵金属杠杆高,点差吞噬在分钟级回测里可能让盈利轮廓直接反转。 第二段 FitnessFunction() 是 UGA 调用的精简版:它不画对象,只循环 Colony[u][chromos] 收集峰谷,若 PeaksCount>1 才进入累加。两种写法共用同一套阈值(1.0 / 4.0),你可以直接把 GeneCount 调到 500 跑一轮,观察 PeaksCount 分布是否集中在 20~80 之间,进而判断阈值要不要随品种重标定。 别把 1.0 和 4.0 当通用解:黄金 M15 的波动幅度常把 temp 推到 6 以上,这时原代码会漏掉一批低点,适应度被系统性低估。

MQL5 / C++
class="type">int    PeaksCount=class="num">0;
class="type">class="kw">double temp=class="num">0.0;
class=class="str">"cmt">//--------------------------------------------------------------------
for(class="type">int u=class="num">1;u<=GeneCount;u++)
{
  temp=Chromosome[u];
  if(temp<=class="num">1.0 )
  {
    Peaks[PeaksCount]=NormalizeDouble(Hight[u],Digits());
    Ti   [PeaksCount]=Time[u];
    PeaksCount++;
  }
  if(temp>=class="num">4.0)
  {
    Peaks[PeaksCount]=NormalizeDouble(Low[u],Digits());
    Ti   [PeaksCount]=Time[u];
    PeaksCount++;
  }
}
ObjectsDeleteAll(class="num">0,-class="num">1,-class="num">1);
for(class="type">int V=class="num">0;V<PeaksCount-class="num">1;V++)
{
  PipsSum+=NormalizeDouble((MathAbs(Peaks[V]-Peaks[V+class="num">1]))/Point(),FFNormalizeDigits)-Spred;
  ObjectCreate(class="num">0,"BoxBackName"+(class="type">class="kw">string)V,OBJ_TREND,class="num">0,Ti[V],Peaks[V],Ti[V+class="num">1],Peaks[V+class="num">1]);
  ObjectSetInteger(class="num">0,"BoxBackName"+(class="type">class="kw">string)V,OBJPROP_COLOR,Black);
  ObjectSetInteger(class="num">0,"BoxBackName"+(class="type">class="kw">string)V,OBJPROP_SELECTABLE,true);
}
ChartRedraw(class="num">0);
Comment(PipsSum);
}
class=class="str">"cmt">//----------------------------------------------------------------------
else
  class="kw">return;
}
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//-----------------------------------------------------------------------+
class=class="str">"cmt">// 确定个体适应度的函数从 UGA 中调用.                                                              |
class=class="str">"cmt">//-----------------------------------------------------------------------+
class="type">void FitnessFunction(class="type">int chromos)
{
class=class="str">"cmt">//-----------------------变量-------------------------------------
  class="type">class="kw">double PipsSum=class="num">0.0;
  class="type">int    PeaksCount=class="num">0;
  class="type">class="kw">double temp=class="num">0.0;
class=class="str">"cmt">//----------------------------------------------------------------------
  for(class="type">int u=class="num">1;u<=GeneCount;u++)
  {
    temp=Colony[u][chromos];
    if(temp<=class="num">1.0)
    {
      Peaks[PeaksCount]=NormalizeDouble(Hight[u],Digits());
      PeaksCount++;
    }
    if(temp>=class="num">4.0)
    {
      Peaks[PeaksCount]=NormalizeDouble(Low[u],Digits());
      PeaksCount++;
    }
  }
  if(PeaksCount>class="num">1)
  {
    for(class="type">int V=class="num">0;V<PeaksCount-class="num">1;V++)

◍ 峰谷距转点值并扣点差

这段代码把相邻峰位之间的绝对距离换算成点数,再减去点差,累加到 PipsSum 上,作为单条染色体的适应度原始分。 NormalizeDouble 用 FFNormalizeDigits 控制精度,(MathAbs(Peaks[V]-Peaks[V+1]))/Point() 把价格差除以点值得到裸点数,避免不同品种点值差异导致的偏差。 若路径不合法则 Colony[0][chromos] 直接置为 -10000000.0,相当于在遗传算法里判死刑,不参与后续选择;AmountStartsFF 每算完一条染色体自增,用于统计已评估样本数。 在 MT5 里把 FFNormalizeDigits 设为 1、点差取 EURUSD 常规 10 点(0.00010),一段 50 点峰谷差算出来 PipsSum 约为 40,可直接观察适应度分布是否过偏。

MQL5 / C++
  PipsSum+=NormalizeDouble((MathAbs(Peaks[V]-Peaks[V+class="num">1]))/Point(),FFNormalizeDigits)-Spred;
  Colony[class="num">0][chromos]=PipsSum;
 }
 else
   Colony[class="num">0][chromos]=-class="num">10000000.0;
 AmountStartsFF++;
}

「UGA 调参的几个实操注意点」

在 FF 里把预期条件设对,算法才更可能吐出能用的结果。回看例 2 就能明白,为什么这算是一条头等建议:条件错了,后面跑得再花哨也是白搭。 Precision 别给太小。算法虽然支持以步进 0 运行,但你应当要求解具备合理精度,这个参数本意就是压缩问题规模,设太低只会让搜索空间虚胖。 种群规模和代数阈值要动起来试。经验上,好解对应的 Epoch 参数应比 MaxOfCurrentEpoch 显示值大两倍左右;但别猛加过大数值,那并不会加速收敛,只是烧 CPU。 遗传算子的参数没有万能档位,得按具体任务条件去分配。外汇与贵金属市场高杠杆、高波动,这类优化结果仅代表历史样本上的概率倾向,实盘前务必在 MT5 策略测试器里复核。

记住这一条就够了

UGA 这套基于人群优化的遗传框架在 MT5 策略测试器里跑得通,核心在于它把 ZigZag 拐点当基因、把历史 Bar 数量直接映射成基因长度——有读者在 2024 年 10 月确认:条形图数量即基因数量,这个对应关系没歧义。 代码层面有个坑得提一句:老版本里变量和函数同名 ArtificialMutation 会导致 UGAlib 编译报错,改成 ArtificialMutationP 后就能直接编译运行,社区里已有人验证过。 它用 BSD 授权放出全部源码,你想加新算子或换运算块都行,扩展性摆在那。外汇和贵金属市场高杠杆、滑点诡异,拿这套做优化别当圣杯,回测漂亮不等于实盘能活。 真要下手,先把附带的 UGA_script 解压、改掉那个函数名、挂上 ZigZag 跑一轮参数进化,比看十篇软文都实在。

把重复劳动交给小布
这些多代种群的适应度诊断与参数收敛曲线,小布盯盘的 AIGC 已内置,打开对应品种页即可看到,你只需判断哪条路线值得跟。

常见问题

省掉了基因型到表现型的二进制互转,染色体直接用浮点数组表示,交叉变异都在实数域操作,代码可读性和改动成本更低。
通过 OnTester 返回用户适应度值,让测试器以该值为导向筛选后代,避免只盯净利润这类单一目标。
每代评估个体数线性增加,若遗传代数也高,总回测次数可能逼近枚举量级,倾向先用小种群探收敛再扩容。
小布负责把已跑完的优化结果做收敛诊断与异常代标记,实盘参数决策仍由你基于高风险品种特性判断。
都属于组合爆炸的 NP 类问题,无法穷举证明最优,只能用启发式在可接受时间内拿到较优解。