循环孤雌生殖算法(CPA)·进阶篇
📘

循环孤雌生殖算法(CPA)·进阶篇

第 2/2 篇

CPA 算法的种群初始化与首轮位置撒点

在 MT5 里实现殖民竞争算法(CPA)做参数寻优时,Init 函数先调用 StandardInit 校验区间与步长数组,再把总种群 popSize 按殖民地数 Nc 切成 Nm=popSize/Nc 个基础群。性别比例由 Fr 控制:fNumber=int(Nm*Fr),若算出来小于 1 则强制置 1,剩余 mNumber=Nm-fNumber 为雄性数量。 首轮 Moving 靠 revision 标志位区分:未修订时对每个个体 i 的 coords 个维度,先用 u.RNDfromCI 在 [rangeMin,rangeMax] 内取随机值,再用 u.SeInDiSp 对齐到离散步长网格,保证初始解落在可回测的参数点上。这一步直接决定后续寻优是否踩中 MT5 策略测试器的合法输入。 进入迭代后,搜索强度按 k=(epochs-epochNow)/epochs 线性衰减,epochs 为 0 时 k 会除零得到 NaN,所以 Init 传参务必给正整数轮次。雌性个体走孤雌更新:a[ind].c[c] = a[ind].cP[c] + alpha1*k*u.GaussDistribution(0.0,-1.0,1.0,8)*(rangeMax[c]-rangeMin[c]),其中高斯采样截断在 ±8σ,避免极端跳变。外汇与贵金属 EA 调参属高风险,回测优解实盘可能失效。 让小布替你跑这套 把 Init 里 epochsP 设成 50、Fr 设 0.3,在 EURUSD 的 M15 上跑一次,观察 fNumber 是否如预期为 Nm*0.3 并向下取整,能快速验证你抄的 CPA 类有没有把性别切分写反。

MQL5 / C++
  S_AO_Agent aT [];         class=class="str">"cmt">// temporary colony for sorting
  class="type">void SortFromTo(S_AO_Agent &p [], S_AO_Agent &pTemp [], class="type">int from, class="type">int count); class=class="str">"cmt">// agent sorting function
};
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">// Initialization of the algorithm with the given search parameters
class="type">bool C_AO_CPA::Init(const class="type">class="kw">double &rangeMinP  [],
                     const class="type">class="kw">double &rangeMaxP  [],
                     const class="type">class="kw">double &rangeStepP [],
                     const class="type">int     epochsP = class="num">0)
{
  if (!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) class="kw">return false;
  class=class="str">"cmt">//----------------------------------------------------------------------------
  epochs   = epochsP;
  epochNow = class="num">0;
  class=class="str">"cmt">// Calculating the colony size and the number of individuals of each gender
  Nm       = popSize / Nc;
  fNumber  = class="type">int(Nm * Fr); if (fNumber < class="num">1) fNumber = class="num">1;
  mNumber  = Nm - fNumber;
  ArrayResize(aT, Nm);
  class="kw">return true;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">// The main function for moving individuals in the search space
class="type">void C_AO_CPA::Moving()
{
  epochNow++;
  class=class="str">"cmt">//----------------------------------------------------------------------------
  class=class="str">"cmt">// Initial random initialization of positions if this is the first iteration
  if (!revision)
  {
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      for (class="type">int c = class="num">0; c < coords; c++)
      {
        class=class="str">"cmt">// Generate a random position in a given range
        a [i].c [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);
        a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
      }
    }
    revision = true;
    class="kw">return;
  }
  class=class="str">"cmt">//----------------------------------------------------------------------------
  class=class="str">"cmt">// Calculate the search power decay rate over time
  class="type">class="kw">double k    = (epochs - epochNow)/(class="type">class="kw">double)epochs;
  class="type">int    ind  = class="num">0;
  class="type">int    indF = class="num">0;
  class=class="str">"cmt">// Handling each colony
  for (class="type">int col = class="num">0; col < Nc; col++)
  {
    class=class="str">"cmt">// Updating the positions of female individuals(parthenogenesis)
    for (class="type">int f = class="num">0; f < fNumber; f++)
    {
      ind = col * Nm + f;
      for (class="type">int c = class="num">0; c < coords; c++)
      {
        class=class="str">"cmt">// Parthenogenetic position update using normal distribution
        a [ind].c [c] = a [ind].cP [c] + alpha1 * k * u.GaussDistribution(class="num">0.0, -class="num">1.0, class="num">1.0, class="num">8) * (rangeMax [c] - rangeMin [c]);
      }
    }

◍ 殖民竞争算法里的位置修订与跨群迁移

在殖民竞争优化(CPA)的每轮迭代末尾,Revision() 负责把当前种群里的最优解挑出来并更新全局最优。代码用一次线性扫描比对 a[i].f 与 fB,一旦发现更优个体就记下索引并拷贝坐标数组到 cB,这一步直接决定了下一轮搜索的引力中心。 个体当前位置会被整体存进 cP 备用,随后每个殖民按目标函数值排序。SortFromTo() 接收起始偏移 from 与数量 count,把待排区段抽进临时数组再回写,保证每群里优等个体永远排在头部。 跨群飞行(迁移)受概率 Pf 控制:当 u.RNDprobab() 小于 Pf 时才触发。随机抽两个不同殖民,若前者头部不如后者则交换编号,再把优群的最优坐标覆盖到劣群的最差位置,并重排劣群。这种机制让落后群体可能以较低代价跳出现有局部洼地。 下面这段是 Revision() 与排序辅助的核心节选,注意 ArrayCopy 的 WHOLE_ARRAY 用法以及 do-while 保证两群互异:

MQL5 / C++
class=class="str">"cmt">// Function for revising positions and exchanging information between colonies
class="type">void C_AO_CPA::Revision()
{
  class=class="str">"cmt">// Find and update the best solution
  class="type">int ind = -class="num">1;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if (a [i].f > fB)
    {
      fB = a [i].f;
      ind = i;
    }
  }
  if (ind != -class="num">1) ArrayCopy(cB, a [ind].c, class="num">0, class="num">0, WHOLE_ARRAY);
  class=class="str">"cmt">//----------------------------------------------------------------------------
  class=class="str">"cmt">// Save the current positions
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    ArrayCopy(a [i].cP, a [i].c, class="num">0, class="num">0, WHOLE_ARRAY);
  }
  class=class="str">"cmt">// Sort individuals in each colony by the target function value
  for (class="type">int col = class="num">0; col < Nc; col++)
  {
    ind = col * Nm;
    SortFromTo(a, aT, ind, Nm);
  }
  class=class="str">"cmt">// Mechanism of flight(migration) between colonies
  if (u.RNDprobab() < Pf)
  {
    class="type">int indCol_1 = class="num">0;
    class="type">int indCol_2 = class="num">0;
    class=class="str">"cmt">// Select two random different colonies
    indCol_1 = u.RNDminusOne(Nc);
    do indCol_2 = u.RNDminusOne(Nc);
    while (indCol_1 == indCol_2);
    class=class="str">"cmt">// Ensure that the best solution is in the first colony 
    if (a [indCol_1 * Nm].f < a [indCol_2 * Nm].f)
    {
      class="type">int temp = indCol_1;
      indCol_1 = indCol_2;
      indCol_2 = temp;
    }
    class=class="str">"cmt">// Copy the best solution to the worst colony
    ArrayCopy(a [indCol_2 * Nm + Nm - class="num">1].cP, a [indCol_1 * Nm].cP, class="num">0, class="num">0, WHOLE_ARRAY);
    class=class="str">"cmt">// Re-sort the colony after migration
    SortFromTo(a, aT, indCol_2 * Nm, Nm);
  }
}

class=class="str">"cmt">// Auxiliary function for sorting agents by the value of the objective function
class="type">void C_AO_CPA::SortFromTo(S_AO_Agent &p [], S_AO_Agent &pTemp [], class="type">int from, class="type">int count)
{
  class="type">int    cnt = class="num">1;
  class="type">int    t0  = class="num">0;
  class="type">class="kw">double t1  = class="num">0.0;
  class="type">int    ind [];
  class="type">class="kw">double val [];
  ArrayResize(ind, count);
  ArrayResize(val, count);
  class=class="str">"cmt">// Copy values for sorting
  for (class="type">int i = class="num">0; i < count; i++)

「用冒泡法给价格片段排个序」

这段逻辑干的事很直接:先把索引和对应数值按偏移量 from 装进两个数组,随后跑一轮冒泡排序,目标是把 val 按从大到小排好。 外层 while 用 cnt 当交换计数器,只要这一轮发生过交换就再来一遍,直到 cnt 归零,说明序列已经稳定。内层 for 从 0 比到 count-2,只要 val[i] 小于后一个,就同时交换 ind 和 val 两个数组里的位置,保证索引和数值始终绑在一起。 排序完之后,用 ind 里的顺序把原结构 p 抽到 pTemp,再按 from 偏移写回 p。你在 MT5 里把 count 设成 50、from 设成 0,跑一遍就能看到最近 50 根里谁是最大值排在最前。外汇和贵金属波动快,这种重排只是预处理,信号真假还得结合上下文判断。

MQL5 / C++
  {
    ind [i] = i + from;
    val [i] = p [i + from].f;
  }
  class=class="str">"cmt">// Bubble sort in descending order
  while (cnt > class="num">0)
  {
    cnt = class="num">0;
    for (class="type">int i = class="num">0; i < count - class="num">1; i++)
    {
      if (val [i] < val [i + class="num">1])
      {
        class=class="str">"cmt">// Exchange of indices
        t0 = ind [i + class="num">1];
        ind [i + class="num">1] = ind [i];
        ind [i] = t0;
        class=class="str">"cmt">// Exchange values
        t1 = val [i + class="num">1];
        val [i + class="num">1] = val [i];
        val [i] = t1;
        cnt++;
      }
    }
  }
  class=class="str">"cmt">// Apply the sorting results
  for (class="type">int i = class="num">0;    i < count; i++)       pTemp [i] = p [ind [i]];
  for (class="type">int i = from; i < from + count; i++) p  [i] = pTemp [i - from];
}

CPA 实测跑分与排名落点

把 CPA(周期性单性生殖算法)接到标准测试函数上跑完一轮,结果谈不上亮眼。它在 5/25/500 三个维度档位的 Hilly、Forest、Megacity 函数上各跑 10000 次,综合总分 3.12805,折算下来只摸到理论满分的 34.76%,最终在 45 种群体优化算法里排第 44。 具体看单组数值:Hilly 上 5/25/500 维结果分别是 0.7166、0.4001、0.2550;Forest 对应为 0.6218、0.3365、0.1926;Megacity 离散场景则是 0.3431、0.1677、0.0946。维度一拉高,收敛质量掉得很快,500 维基本腰斩再腰斩。 有意思的是肉眼可见的搜索轨迹——虚拟蚜虫在高维空间里分成好几个小群,群内移动和群体漂移都能直接看图分辨,这说明它的种群分化机制确实在起作用,只是单群精度撑不起高分。 横向比一下头部:排第一的 ANS 跨邻域搜索总分 6.134(68.15%),第十的 ACS 人工协同搜索也有 5.226(58.06%)。CPA 和榜首差了一倍多,做 MT5 自定义优化器时拿它当主力大概率不够用,更合适的位置是混合架构里的多样性扰动子模块。

◍ 优化器横向跑分:COAm 仍领先随机游走

上面这张表把多种元启发式优化器在 MT5 参数寻优任务里的表现摊开了看。COAm(布谷鸟优化算法改进版)综合得分 3.349,耗时 37.21 单位,随机游走(RW)只有 2.348 和 26.09,差距接近 43% 的综合分,说明盲目扰动在多维参数面上效率偏低。 带 M 标记的算法(COAm、SDOm、NMm、FAm)都做了某种变异策略改进,前三者综合分都在 3.2 以上,而 BBBC、CPA 这类无 M 标记的常规版掉到 3.1 附近。萤火虫算法 FAm 虽然综合 3.048,但第三项指标 0.32276 是全表最高,意味着它在局部精细搜索上有特长。 做 EA 调参时,若你的参数维度超过 8 个,直接套 RW 当优化器基本是在浪费回测时间;从这张表看,COAm 和改进型 Nelder-Mead 是更稳的起步选择,但实盘前必须重跑一遍,外汇与贵金属杠杆品种滑点会改写这些数值。

「画得少,看得清」

CPA 这套蚜虫隐喻在 MT5 回测台跑出来的评分区间被标成 0–100,100 只是理论天花板;附带的 CalculationTestResults.mqh 能直接算出排名表,有兴趣自己拉 CPA.zip 跑 Test_AO_CPA.mq5 验证。 它真正留下的不是收敛精度,而是把「有性 / 无性繁殖」「群体迁移」翻译成参数时的别扭:Nm、Fr、Pf、alpha1、alpha2 任一个偏移,解的质量就塌方,说明结构本身扛不住高维搜索。 所以别盯着它能不能实盘,把它当反面教材更划算——群体划分若没有强效迁移,大概率早收敛;生物行为巧妙不等于数学可优化。打开 github 里 Population-optimization-algorithms-MQL5 对照其他算法,你能少走很多弯路。

常见问题

用拉丁超立方或均匀随机映射把解空间切分后逐格撒点,能让首轮分布更散,降低早熟收敛概率。
按适应度阈值做位置修订,弱群个体向强群质心迁移一部分,保留少量异质个体维持多样性。
可以,小布能接入你的策略参数自动执行CPA撒点与跨群迁移,并生成横向跑分排名图,你只看落点结论。
片段长度控制在50根以内时冒泡开销可忽略,若超200根建议换快排,否则可能延迟触发。
COAm在殖民竞争阶段做了定向质心牵引,收敛稳定度更高,随机游走易在平原区空转。