种群优化算法:二进制遗传算法(BGA)。第 II 部分·进阶篇
🧬

种群优化算法:二进制遗传算法(BGA)。第 II 部分·进阶篇

(2/3)· 搞懂 BGA 的二进制表达与算子机制,才不会把 EA 参数优化做成瞎蒙

偏理论 第 2/3 篇
很多交易者把 MT5 里的参数优化当成黑箱按钮,却不知道背后跑的是二进制遗传算法。理解 BGA 的染色体与突变逻辑,你才看得懂为什么某些组合总被筛掉。这篇接着上一部分的基础概念,把算法本身拆开看。

「遗传算法Agent与种群的结构骨架」

这段结构定义把遗传算法的核心数据容器一次性铺开:单个解向量由 S_Agent 承载,其内部用 chromosome 字符数组拼接各维基因的二进制位,coords 维参数各自带 min/max 边界与小数位精度。 Init 里先 ArrayResize(c, coords) 给坐标数组定容,再把 f 初始化为 -DBL_MAX,意味着适应度在首轮评估前处于“未命中”状态;染色体预留 1000 增量空间,循环里每维基因生成后通过 ArrayCopy 追加进 chromosome,pos 偏移由 gene.length 累加控制。 S_Roulette 只存 start/end 双精度,是典型的轮盘赌选择区间结构,不夹带多余字段。C_AO_BGA 作为主类暴露 cB/fB(最优坐标与适应度)、a[] 种群、以及 rangeMin/rangeMax/rangeStep 三组搜索边界,Init 入参已出现 coordsP、popSizeP、parentPopSizeP、crossoverProbabP——种群规模与交叉概率在构造时即固化。 在 MT5 里把这段 struct/class 原样贴入 EA 头文件,改 doubleDigitsInChromo 参数即可验证染色体长度对内存占用的线性影响;外汇与贵金属参数寻优属高风险实验,回测过拟合概率偏高,须用样本外数据复核。

MQL5 / C++
class="kw">struct S_Agent
{
  class="type">void Init(const class="type">int coords, const class="type">class="kw">double &min [], const class="type">class="kw">double &max [], class="type">int doubleDigitsInChromo)
  {
    ArrayResize(c, coords);
    f = -DBL_MAX;
    ArrayResize(genes, coords);
    ArrayResize(chromosome, class="num">0, class="num">1000);
    for(class="type">int i = class="num">0; i < coords; i++)
    {
      genes [i].Init(min [i], max [i], doubleDigitsInChromo);
      ArrayCopy(chromosome, genes [i].gene, ArraySize(chromosome), class="num">0, WHOLE_ARRAY);
    }
    calculated = class="kw">false;
  }
  class="type">void ExtractGenes()
  {
    class="type">uint pos = class="num">0;
    for (class="type">int i = class="num">0; i < ArraySize(genes); i++)
    {
      c [i] = genes [i].ToDouble(chromosome, pos);
      pos  += genes [i].length;
    }
  }
  class="type">class="kw">double c []; class=class="str">"cmt">//coordinates
  class="type">class="kw">double f;    class=class="str">"cmt">//fitness
  S_BinaryGene genes [];
  class="type">char chromosome    [];
  class="type">bool calculated;
};

class="kw">struct S_Roulette
{
  class="type">class="kw">double start;
  class="type">class="kw">double end;
};

class C_AO_BGA
{
  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
  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      parentPopSizeP,          class=class="str">"cmt">//parent population size
                                const class="type">class="kw">double crossoverProbabP,          class=class="str">"cmt">//crossover probability

◍ 遗传算法类的成员变量与接口拆解

在 MT5 里用遗传算法做参数寻优,第一步是把算法骨架落进一个 C++ 风格的类。下面这段声明定义了种群规模、交叉/变异/倒位概率等核心旋钮,以及染色体灰度码长度这类底层结构。 构造函数参数里 crossoverPointsP 控制单点还是多点交叉,mutationProbabP 与 inversionProbabP 是 [0,1] 区间的浮点,doubleDigitsInChromoP 决定基因十进制精度——比如设 4 就代表染色体解码后保留四位小数,直接影响手数或挂单价步长。 私有成员中 lengthChrome 按 Gray 码算字符穿长度,points 与 poRND 管理染色体断点索引,roulette 是实现轮盘赌选择的数组。开 MT5 新建 EA 时,把这些字段原样抄进头文件,就能在 Strategy Tester 里跑出可复现的种群进化;外汇与贵金属杠杆高,回测盈利不代表实盘胜率,参数过拟合概率偏大。 别把正态当圣经:mutationProbab 设 0.01 还是 0.1,种群多样性差异巨大,建议先用 0.05 跑 200 代看收敛曲线再调。

MQL5 / C++
const class="type">int      crossoverPointsP,      class=class="str">"cmt">//crossover points
const class="type">class="kw">double mutationProbabP,      class=class="str">"cmt">//mutation probability
const class="type">class="kw">double inversionProbabP,     class=class="str">"cmt">//inversion probability
const class="type">int      doubleDigitsInChromoP);class=class="str">"cmt">//number of decimal places in the gene
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     parentPopSize;      class=class="str">"cmt">//parent population size
class="kw">private: class="type">class="kw">double crossoverProbab;     class=class="str">"cmt">//crossover probability
class="kw">private: class="type">int     crossoverPoints;    class=class="str">"cmt">//crossover points
class="kw">private: class="type">class="kw">double mutationProbab;      class=class="str">"cmt">//mutation probability
class="kw">private: class="type">class="kw">double inversionProbab;     class=class="str">"cmt">//inversion probability
class="kw">private: class="type">int     doubleDigitsInChromo; class=class="str">"cmt">//number of decimal places in the gene
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=class="str">"cmt">//temporary array for sorting the population
class="kw">private: class="type">class="kw">double     val         [];  class=class="str">"cmt">//temporary array for sorting the population
class="kw">private: S_Agent    pTemp       [];  class=class="str">"cmt">//temporary array for sorting the population
class="kw">private: class="type">char       tempChrome [];   class=class="str">"cmt">//temporary chromosome for inversion surgery
class="kw">private: class="type">uint       lengthChrome;    class=class="str">"cmt">//length of the chromosome(the length of the class="type">class="kw">string of characters according to the Gray code)
class="kw">private: class="type">int        pCount;          class=class="str">"cmt">//indices of chromosome break points
class="kw">private: class="type">uint       poRND       [];  class=class="str">"cmt">//temporal indices of chromosome break points
class="kw">private: class="type">uint       points      [];  class=class="str">"cmt">//final indices of chromosome break points
class="kw">private: S_Roulette roulette    [];  class=class="str">"cmt">//roulette
class="kw">private: class="type">void   PreCalcRoulette();
class="kw">private: class="type">int    SpinRoulette();

遗传算法类的私有方法与初始化落点

C_AO_BGA 把几个底层操做封装成 private 方法:SeInDiSp 做区间离散化,RNDfromCI 在 [min,max] 内取均匀随机数,Sorting 对 Agent 指针数组按适应度排序,Scale 则是把输入区间线性映射到输出区间。这类接口不暴露给调用方,只服务于内部进化循环。 Init 是真正干活的地方。先用 MathSrand((int)GetMicrosecondCount()) 以微秒计数重置随机种子,避免每次回测初始种群雷同;随后把 fB 置为 -DBL_MAX、revision 置 false,并批量把入参写进成员变量。 几个防御式下限值得注意:crossoverPoints 和 pCount 若小于 1 会被强制拉回 1,否则单点交叉都跑不起来。数组尺寸按 parentPopSize+popSize 预留,roulette 只开 parentPopSize 长度,说明轮盘赌仅在父代规模上做选择。 直接把这段 Init 抄进 MT5 的 EA 头文件,改 coords 和 popSize 两个入参,就能在策略测试器里观察种群数组实际占用的内存峰值,外汇与贵金属品种下高 popSize 可能显著拖慢 tick 级回测。

MQL5 / C++
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">void   Sorting(S_Agent &p [], class="type">int size);
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">void C_AO_BGA::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      parentPopSizeP,     class=class="str">"cmt">//parent population size
                     const class="type">class="kw">double   crossoverProbabP,   class=class="str">"cmt">//crossover probability
                     const class="type">int      crossoverPointsP,   class=class="str">"cmt">//crossover points
                     const class="type">class="kw">double   mutationProbabP,    class=class="str">"cmt">//mutation probability
                     const class="type">class="kw">double   inversionProbabP,   class=class="str">"cmt">//inversion probability
                     const class="type">int      doubleDigitsInChromoP) class=class="str">"cmt">//number of decimal places in the gene
{
  MathSrand((class="type">int)GetMicrosecondCount()); class=class="str">"cmt">// reset of the generator
  fB        = -DBL_MAX;
  revision = class="kw">false;
  coords                = coordsP;
  popSize               = popSizeP;
  parentPopSize         = parentPopSizeP;
  crossoverProbab       = crossoverProbabP;
  crossoverPoints       = crossoverPointsP;
  pCount                = crossoverPointsP;
  mutationProbab        = mutationProbabP;
  inversionProbab       = inversionProbabP;
  doubleDigitsInChromo = doubleDigitsInChromoP;
  if (crossoverPoints < class="num">1) crossoverPoints = class="num">1;
  if (pCount < class="num">1) pCount = class="num">1;
  ArrayResize(poRND,   pCount);
  ArrayResize(points, pCount + class="num">2);
  ArrayResize(ind,   parentPopSize + popSize);
  ArrayResize(val,   parentPopSize + popSize);
  ArrayResize(pTemp, parentPopSize + popSize);
  ArrayResize(a,     popSize);
  ArrayResize(parents, parentPopSize + popSize);
  ArrayResize(roulette, parentPopSize);
  ArrayResize(rangeMax,  coords);
  ArrayResize(rangeMin,  coords);

「遗传算法的种群初始化与繁殖循环」

这段 C_AO_BGA::Moving 方法实现了基于遗传算法的参数寻优核心逻辑,第一次调用时 revision 为 false,会先铺满初始种群。 初始化阶段对每个个体调用 Init 并按 MathRand() 返回的 [0,32767] 区间随机数填染色体:大于 16384 置 1,否则置 0,相当于用 50% 概率做二进制基因播种。 随后 ExtractGenes 解码,再用 SeInDiSp 把基因映射到各维度的 rangeMin~rangeMax 实值空间,个体适应度 f 统一设为 -DBL_MAX 等待回测打分。 非首次进入时走繁殖分支:PreCalcRoulette 建轮盘、SpinRoulette 按适应度概率选父代,ArrayCopy 把父代染色体原样拷给子代,再掷 RNDfromCI(0,1) 决定是否以 crossoverProbab 概率交叉。 交叉时随机取 pCount 个断点(poRND),越界则钳到 lengthChrome-1,ArraySort 排序后做多点重组——在 MT5 里把 pCount 和 crossoverProbab 调大,种群可能更快跳出局部最优,但外汇与贵金属杠杆品种的高波动会让过拟合风险明显上升。

MQL5 / C++
  ArrayResize(rangeStep, coords);
  ArrayResize(cB,           coords);
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_BGA::Moving()
{
  class=class="str">"cmt">//----------------------------------------------------------------------------
  if (!revision)
  {
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      a [i].Init(coords, rangeMin, rangeMax, doubleDigitsInChromo);
      class="type">int r = class="num">0;
      for (class="type">int len = class="num">0; len < ArraySize(a [i].chromosome); len++)
      {
        r  = MathRand(); class=class="str">"cmt">//[class="num">0,class="num">32767]
        if (r > class="num">16384) a [i].chromosome [len] = class="num">1;
        else           a [i].chromosome [len] = class="num">0;
      }
      a [i].ExtractGenes();
      for (class="type">int c = class="num">0; c < coords; c++) a [i].c [c] = SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
      a [i].f = -DBL_MAX;
      a [i].calculated = true;
    }
    lengthChrome = ArraySize(a [class="num">0].chromosome);
    ArrayResize(tempChrome, lengthChrome);
    for (class="type">int i = class="num">0; i < parentPopSize + popSize; i++)
    {
      parents [i].Init(coords, rangeMin, rangeMax, doubleDigitsInChromo);
      parents [i].f = -DBL_MAX;
    }
    revision = true;
    class="kw">return;
  }
  class=class="str">"cmt">//----------------------------------------------------------------------------
  class="type">int    pos       = class="num">0;
  class="type">class="kw">double r         = class="num">0;
  class="type">uint   p1        = class="num">0;
  class="type">uint   p2        = class="num">0;
  class="type">uint   p3        = class="num">0;
  class="type">uint   temp      = class="num">0;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    PreCalcRoulette();
    class=class="str">"cmt">//selection, select and copy the parent to the child------------------------
    pos = SpinRoulette();
    ArrayCopy(a [i].chromosome, parents [pos].chromosome, class="num">0, class="num">0, WHOLE_ARRAY);
    class=class="str">"cmt">//crossover-----------------------------------------------------------------
    r = RNDfromCI(class="num">0.0, class="num">1.0);
    if (r < crossoverProbab)
    {
      class=class="str">"cmt">//choose a second parent to breed with------------------------------------
      pos = SpinRoulette();
      class=class="str">"cmt">//determination of chromosome break points--------------------------------
      for (class="type">int p = class="num">0; p < pCount; p++)
      {
        poRND [p] = (class="type">int)RNDfromCI(class="num">0.0, lengthChrome);
        if (poRND [p] >= lengthChrome) poRND [p] = lengthChrome - class="num">1;
      }
      ArraySort(poRND);
把算子跑批交给小布盯盘
小布盯盘的 AIGC 模块已内置种群寻优的预处理视图,打开对应品种页就能直接看参数收敛轨迹,不必自己手算每一代适应度。

常见问题

每个被优化参数会映射成定长比特段,组合后构成一条染色体;交叠与突变只在比特层操作,再解码回参数空间。
目前小布提供收敛诊断与历史代际对比,完整 BGA 寻优仍在 MT5 策略测试器内执行,两者可配合看板使用。
贵金属与外汇价差序列非凸且噪声高,BGA 以概率搜索避开维度灾难,更可能在有限代内逼近较优区,但仍是概率性结果。
过高突变会破坏优良基因,种群可能在可行域乱跳;实盘前建议用历史段做多轮测试观察稳定度。