群体优化算法:智能水滴(IWD)算法·进阶篇
📘

群体优化算法:智能水滴(IWD)算法·进阶篇

第 2/3 篇

AO算法初始化与首两轮种群布置

这套基于水动力隐喻的优化器,在 Init 里先把随机种子按微秒级时钟重置,避免每次回测种群轨迹雷同。坐标数 coords、种群规模 popSize、扇区数 sectorsNumber 和水黏度 waterViscosity(须 ≥1)是四个外部注入参数,决定解空间切分粒度。 代码里把 riverbed 相关数组按坐标维度和扇区数二级展开,河床深度初始化为 0.0,扇区坐标初值置为 -DBL_MAX,相当于清空上一轮地形记忆。种群个体 p[i] 逐个 Init(coords),保证每个粒子拿到独立坐标容器。 迭代计数器为 0 时,sectorSpace[i] 直接由 (rangeMax[i]-rangeMin[i])/sectorsNumber 算得,把每维搜索区间均分。第 0 和第 1 次迭代走同一段布置逻辑:每个个体随机落进某个扇区,再在扇区子区间内用 SeInDiSp 做离散化采样,并把 fPrev 存为当前适应度。 想在 MT5 里验证,把下面代码段塞进你的 EA 或脚本,改 popSize=30、sectorsNumber=10,跑 EURUSD 的 H1 参数寻优,观察前两轮 p[i].c 的分布是否均匀落在 rangeMin~rangeMax 内。外汇与贵金属杠杆高,回测分布不代表实盘概率。

MQL5 / C++
class="type">void C_AO_IWDm::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     sectorsNumberP,    class=class="str">"cmt">//sectors number
                        const class="type">class="kw">double  waterViscosityP)   class=class="str">"cmt">//water viscosity(>= class="num">1)
{
  MathSrand((class="type">int)GetMicrosecondCount()); class=class="str">"cmt">// reset of the generator
  fB                = -DBL_MAX;
  iterationCounter = class="num">0;
  coords    = coordsP;
  popSize   = popSizeP;
  sectorsNumber  = sectorsNumberP;
  waterViscosity = waterViscosityP;
  ArrayResize(sectorSpace, coords);
  ArrayResize(p, popSize);
  for (class="type">int i = class="num">0; i < popSize; i++) p [i].Init(coords);
  ArrayResize(rangeMax,  coords);
  ArrayResize(rangeMin,  coords);
  ArrayResize(rangeStep, coords);
  ArrayResize(cB,        coords);
  ArrayResize(rb,        coords);
  for (class="type">int i = class="num">0; i < coords; i++)
  {
    ArrayResize(rb [i].riverbedDepth, sectorsNumber);
    ArrayResize(rb [i].coordOnSector, sectorsNumber);
    ArrayInitialize(rb [i].riverbedDepth, class="num">0.0);
    ArrayInitialize(rb [i].coordOnSector, -DBL_MAX);
  }
}

  if (iterationCounter == class="num">0)
  {
    for (class="type">int i = class="num">0; i < coords; i++) sectorSpace [i] = (rangeMax [i] - rangeMin [i]) / sectorsNumber;
  }
  class=class="str">"cmt">//class="num">1,class="num">4
  if (iterationCounter == class="num">0 || iterationCounter == class="num">1)
  {
    class="type">class="kw">double min = class="num">0.0;
    class="type">class="kw">double max = class="num">0.0;
    class="type">int    s   = class="num">0;
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      p [i].fPrev = p [i].f;
      for (class="type">int c = class="num">0; c < coords; c++)
      {
        s = (class="type">int)(RNDfromCI(class="num">0, sectorsNumber));
        if (s >= sectorsNumber) s = sectorsNumber - class="num">1;
        p [i].rSection [c] = s;
        min = rangeMin [c] + sectorSpace [c] * s;
        max = min + sectorSpace [c];
        p [i].c [c] =  SeInDiSp(RNDfromCI(min, max), rangeMin [c], rangeMax [c], rangeStep [c]);
      }
    }
    for (class="type">int i = class="num">0; i < popSize; i++) p [i].fPrev = p [i].f;
    class="kw">return;
  }

◍ 适应度增量归一化与河床选择逻辑

这段代码在做两件事:先把种群里每个个体的适应度变化量做归一化,再用所谓“河床深度”去引导下一轮坐标采样。外汇与贵金属市场波动剧烈,这类基于随机优化的搜索对参数敏感,实盘前务必在 MT5 策略测试器里跑历史回测确认稳定性。 先初始化最大最小变化量边界,遍历种群用 fabs 取本次与上次适应度的绝对差。若所有个体变化量相等,直接把 altChange 清零避免除零;否则按 (altChange - minAltChange)/(maxAltChange - minAltChange) 压缩到 0~1 区间,这一步让后续选择有统一尺度。 标记 8 的循环里,只要某个体适应度优于上一代,就按其归一化变化量累加到对应坐标维度的 riverbedDepth 数组,相当于“表现好”的扇区被冲刷得更深。标记 9 则是核心采样:随机挑一个个体 n,若它比当前个体好就直接复制其扇区并重新在扇区内均匀采样;否则用随机数 randSC 乘各扇区河床深度,挑出加权最深扇区 indSector 作为新坐标落点。 让小布替你跑这套 把上面循环粘进 EA 的优化例程,把 sectorsNumber 调到 20、popSize 设 50,用 EURUSD 的 M15 数据回测,观察 riverbedDepth 是否集中在少数扇区——若发散说明适应度函数可能没捕捉到价格行为的真实结构。

MQL5 / C++
class="type">class="kw">double maxAltChange = -DBL_MAX;
class="type">class="kw">double minAltChange =  DBL_MAX;
class="type">class="kw">double altChange     = class="num">0.0;
class="type">class="kw">double randSC        = class="num">0.0; class=class="str">"cmt">//random selection component
class="type">class="kw">double maxRC         = class="num">0.0;
class="type">class="kw">double nowRC         = class="num">0.0;
class="type">int    indSector     = class="num">0;
for (class="type">int i = class="num">0; i < popSize; i++)
{
  altChange = fabs(p [i].f - p [i].fPrev);
  p [i].altChange = altChange;
  if (altChange < minAltChange) minAltChange = altChange;
  if (altChange > maxAltChange) maxAltChange = altChange;
}
if (minAltChange == maxAltChange)
{
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    p [i].altChange = class="num">0.0;
  }
}
else
{
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    altChange = p [i].altChange;
    p [i].altChange = (altChange - minAltChange) / (maxAltChange - minAltChange);
  }
}
class=class="str">"cmt">//class="num">8---------------------------------------------------------------------------
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if (p [i].f > p [i].fPrev)
    {
      for (class="type">int c = class="num">0; c < coords; c++)
      {
        rb [c].riverbedDepth [p [i].rSection [c]] += p [i].altChange;
      }
    }
  }
class=class="str">"cmt">//class="num">9---------------------------------------------------------------------------
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      n = (class="type">int)(RNDfromCI(class="num">0, popSize));
      if (n >= popSize) n = popSize - class="num">1;
      if (p [n].f > p [i].f)
      {
        p [i].rSection [c] = p [n].rSection [c];
        min = rangeMin [c] + sectorSpace [c] * p [i].rSection [c];
        max = min + sectorSpace [c];
        p [i].c [c] = SeInDiSp(RNDfromCI(min, max), rangeMin [c], rangeMax [c], rangeStep [c]);
      }
      else
      {
        randSC = RNDfromCI(class="num">0.0, class="num">1.0);
        nowRC = rb [c].riverbedDepth [class="num">0] * randSC;
        maxRC = nowRC;
        indSector = class="num">0;
        for (class="type">int r = class="num">1; r < sectorsNumber; r++)
        {
          nowRC = rb [c].riverbedDepth [r] * randSC;
          if (nowRC > maxRC)
          {
            maxRC     = nowRC;
            indSector = r;
          }
        }
        if (rb [c].coordOnSector [indSector] == -DBL_MAX)
        {
          min = rangeMin [c] + sectorSpace [c] * indSector;
          max = min + sectorSpace [c];
          p [i].c [c] = RNDfromCI(min, max);
        }
        else
        {

「适应度回写与河道坐标修订」

种群迭代收尾时,先跑一遍循环把当前适应度写进历史字段:若 p[i].f 大于 p[i].fPrev 就覆盖,保证后续比较用的是最新一代的成绩。这一步没有阈值判断,纯粹是状态同步,避免在下一轮变异时误用陈旧数据。 Revision() 负责把最优个体沉淀进全局河道。当某个体的适应度 p[i].f 超过全局最优 fB,就更新 fB,并用 ArrayCopy 把它的坐标数组 cB 整段拷入,同时把每个维度的落点写回 rb[c].coordOnSector 对应扇区。若没破纪录,则仅在该扇区坐标仍是 -DBL_MAX(未被占据)时补入当前值,等于给空扇区兜底。 迭代计数器 iterationCounter 在每次 Revision 末尾自增 1,这是外部调度器判断是否需要触发重新初始化或停机的唯一依据。在 MT5 里把 popSize 设到 200 以上时,你能明显看到 fB 在前 30 代内收敛速度最快,之后单代提升常小于 1e-4,此时可凭 iterationCounter 切到局部精细搜索。 别让空扇区一直饿着 如果某个扇区的 coordOnSector 长期是 -DBL_MAX,说明该维度采样盲区,Revision 里的 else 分支就是专门填坑的。调试时打印各扇区占位率,低于 60% 就要调大 sectorSpace 或降低 waterViscosity。

MQL5 / C++
class="type">class="kw">double x = RNDfromCI(-class="num">1.0, class="num">1.0);
class="type">class="kw">double y = x * x;
class="type">class="kw">double pit = class="num">0.0;
class="type">class="kw">double dif = Scale(y, class="num">0.0, class="num">1.0, class="num">0.0, sectorSpace [c] * waterViscosity, class="kw">false);
pit = rb [c].coordOnSector [indSector];
pit += x > class="num">0.0 ? dif : -dif;
p [i].c [c] = pit;
}
min = rangeMin [c] + sectorSpace [c] * indSector;
max = min + sectorSpace [c];
p [i].c [c] = SeInDiSp(p [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
p [i].rSection [c] = indSector;
}
}
}
for (class="type">int i = class="num">0; i < popSize; i++)
  {
  if (p [i].f > p [i].fPrev)
  p [i].fPrev = p [i].f;
  }
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_IWDm::Revision()
{
  class=class="str">"cmt">//class="num">3,class="num">6,class="num">11----------------------------------------------------------------------
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
  if (p [i].f > fB)
  {
  fB = p [i].f;
  ArrayCopy(cB, p [i].c, class="num">0, class="num">0, WHOLE_ARRAY);
  for (class="type">int c = class="num">0; c < coords; c++)
  {
  rb [c].coordOnSector [p [i].rSection [c]] = p [i].c [c];
  }
  }
  else
  {
  for (class="type">int c = class="num">0; c < coords; c++)
  {
  if (rb [c].coordOnSector [p [i].rSection [c]] == -DBL_MAX)
  {
  rb [c].coordOnSector [p [i].rSection [c]] = p [i].c [c];
  }
  }
  }
  }
  iterationCounter++;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————

SDSm 里那道 Research 改写逻辑

IWDm 跑出的结果让人想到顺手改一版 SDS——评级里原本最稳的那个。两者都围着「区域 / 餐馆」这类实体转,IWDm 用四元概率分布做位置细化,这套思路挪到 SDS 上,最终测试表现被明显拉动。改动后餐馆内新坐标不再均匀撒点,分布形态和测试结果都变了,于是标了 m。 SDSm 每次迭代尝试改三类坐标:当前最优候选的坐标、随机挑一家餐馆里存着的最优坐标(类似 IWDm 的河床,相当于城市里所有餐馆最佳菜肴的清单)、以及自己对应餐馆的原坐标。概率分布宽度比我没做成外部参数,按当前写死的值更顺手。 核心就是 Research 函数,旧版测试领跑者经它一改,指标提升超过 12%。它先吐一个 [-1.0, 1.0] 的随机数,按餐馆尺寸放大,叠到待改坐标上,再卡边界、按优化步长修剪成可接受值。 下面这段 Revision 是落点。若随机选出的 n 号候选历史适应度更好,就抄它的餐馆地址并用 0.25 宽度比做 Research;否则按 probabRest 概率跳去随机餐馆用 1.0 宽度比细化,不跳就守着自己上一轮地址用 0.25 细化。

MQL5 / C++
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_SDSm::Revision()
{
  class=class="str">"cmt">/*
here is the old code, no changes
  */
  for (class="type">int i = class="num">0; i < populationSize; i++)
  {
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      n = (class="type">int)(RNDfromCI(class="num">0, populationSize));
      if (n >= populationSize) n = populationSize - class="num">1;
      if (cands [n].fPrev > cands [i].fPrev)
      {
        cands [i].raddr [c] = cands [n].raddrPrev [c];
        Research(class="num">0.25,
                cands      [i].raddr [c],
                restSpace [c],
                rangeMin  [c],
                rangeStep [c],
                cands      [n].cPrev [c],
                cands      [i].c      [c]);
      }
      else
      {
        rnd = RNDfromCI(class="num">0.0, class="num">1.0);
        if (rnd < probabRest)
        {
          n = (class="type">int)(RNDfromCI(class="num">0, restNumb));
          if (n >= restNumb) n = restNumb - class="num">1;
          cands [i].raddr [c] = n;
          Research(class="num">1.0,
                  cands      [i].raddr          [c],
                  restSpace [c],
                  rangeMin  [c],
                  rangeStep [c],
                  rb        [c].coordOnSector [n],
                  cands      [i].c                [c]);
        }
        else
        {
          cands [i].raddr [c] = cands [i].raddrPrev [c];
          Research(class="num">0.25,
                  cands      [i].raddr [c],
                  restSpace [c],
                  rangeMin  [c],
                  rangeStep [c],
                  cands      [i].cPrev [c],
                  cands      [i].c      [c]);

常见问题

首两轮建议用低差异序列(如 Sobol)撒点,避免随机聚堆;前两代只做探索不回写河床,降低劣质解锁定河道的概率。
检查增量分母是否用了全局最大差而非滑动窗口差,改成近 N 代窗口可恢复分支多样性,防止单河道被反复选中。
可以。小布能读取你的参数文件,把每轮坐标偏移画成热力图,并标出回写异常的迭代点,省去你手算核对。
多数情况是阈值比较符号反了:应保留增量高于均值的个体,若写成低于均值就会清掉好解,调回大于号即可。
大概率有关。首两轮若步长设得过大,河床坐标会被推太远,后续要花几十代拉回,建议首代步长减半观察。