非洲水牛优化(ABO)·进阶篇
📘

非洲水牛优化(ABO)·进阶篇

第 2/2 篇

◍ ABO 算法的初始化与位置更新内核

这段 C_AO_ABO 类的实现把基于蜜蜂觅食隐喻的群体优化(ABO)落到了 MT5 可调用的具体函数里。初始化阶段先由 StandardInit 校验参数区间,再按 popSize 把种群个体 b 的权重数组 w 扩到 coords 维并清零,这一步决定了后续搜索的维度规模。 Moving 函数分两支:首次调用(revision 为 false)时,对每个个体的每个坐标用 RNDfromCI 在 [rangeMin, rangeMax] 内随机撒点,再用 SeInDiSp 按 rangeStep 对齐到离散网格,之后置 revision=true 直接返回。 非首次进入时,Moving 才执行真正的群体移动。对每个个体的每个坐标,取 r1、r2 两个 [0, lp1]、[0, lp2] 间的随机系数,结合全局最优 cB 与个体历史最优 a[i].cB,按 b[i].w[c] = w + r1*(bg-m) + r2*(bp-m) 更新速度项,再以 m = lambda*(m + b[i].w[c]) 收缩步长并更新坐标。lp1、lp2、lambda 三个参数直接控制收敛激进程度,在外汇与贵金属这种高波动、高杠杆品种上,lambda 设大可能让参数过早锁死在局部最优。 Revision 函数负责优胜记录:遍历种群,若某个体适应度 a[i].f 超过全局最优 fB 则刷新 cB;同时每个个体若超越自身历史最好则更新 a[i].cB。注意 ArrayCopy 用 WHOLE_ARRAY 做整维拷贝,coords 维度不一致会静默出错,开 MT5 跑前务必核对 popSize 与 coords 的传入。

MQL5 / C++
if (!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) class="kw">return false;
class=class="str">"cmt">//----------------------------------------------------------------------------
ArrayResize(b, popSize);
for (class="type">int i = class="num">0; i < popSize; i++)
{
  ArrayResize(b [i].w, coords);
  ArrayInitialize(b [i].w, class="num">0.0);
}
class="kw">return true;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_ABO::Moving()
{
  class=class="str">"cmt">//----------------------------------------------------------------------------
  if (!revision)
  {
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      for (class="type">int c = class="num">0; c < coords; c++)
      {
        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="type">class="kw">double w  = class="num">0.0;
  class="type">class="kw">double m  = class="num">0.0;
  class="type">class="kw">double r1 = class="num">0.0;
  class="type">class="kw">double r2 = class="num">0.0;
  class="type">class="kw">double bg = class="num">0.0;
  class="type">class="kw">double bp = class="num">0.0;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      r1 = u.RNDfromCI(class="num">0, lp1);
      r2 = u.RNDfromCI(class="num">0, lp2);
      bg = cB [c];
      bp = a [i].cB [c];
      m = a [i].c [c];
      w = b [i].w [c];
      b [i].w [c] = w + r1 * (bg - m) + r2 * (bp - m);
      m = lambda * (m + b [i].w [c]);
      a [i].c [c] = u.SeInDiSp(m, rangeMin [c], rangeMax [c], rangeStep [c]);
    }
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_ABO::Revision()
{
  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">//----------------------------------------------------------------------------
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if (a [i].f > a [i].fB)
    {
      a [i].fB = a [i].f;
      ArrayCopy(a [i].cB, a [i].c, class="num">0, class="num">0, WHOLE_ARRAY);
    }
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_ABO::Moving()
{
  class=class="str">"cmt">//----------------------------------------------------------------------------
  if (!revision)
  {
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      for (class="type">int c = class="num">0; c < coords; c++)
      {

粒子位置更新的对称扰动写法

这段 C_AO_ABO::Moving 的逻辑,核心在 revision 标志位:首次进入若 revision 为 false,就对整个种群 popSize × coords 做均匀随机初始化,并立刻置 revision = true 后 return,避免每帧重撒种子。 真正的位置演化在后面双层循环里。被注释掉的旧版用 r1、r2 取 [0, lp1]、[0, lp2] 的正向区间,还保留了权重数组 b[i].w[c] 与 lambda 缩放;现行激活代码把随机区间改成对称形式 RNDfromCI(-lp1, lp1) 与 RNDfromCI(-lp2, lp2),让个体向全局最优 cB[c] 与自身历史最优 a[i].cB[c] 的偏移可正可负。 m = m + r1 * (bg - m) + r2 * (bp - m) 这一步直接改写坐标,不再经过 lambda 与 w 的二阶累积,计算量更轻。最后用 SeInDiSp 把 m 夹回 [rangeMin[c], rangeMax[c]] 并按 rangeStep[c] 离散化——外汇与贵金属参数空间里这一步决定遗传出的手数或周期是否落在合规网格,属高风险调参区,建议开 MT5 把 lp1、lp2 设成 0.5 附近先看散布。 注意第二份 Moving 副本里 bp = a[i].cB[c] 被注释掉了,意味着该维度暂不引用个体历史最优,仅靠全局最优拉动;若你复现时发现收敛偏慢,优先取消这行注释再比对回测曲线。

MQL5 / C++
if (!revision)
{
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      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="type">class="kw">double w  = class="num">0.0;
class="type">class="kw">double m  = class="num">0.0;
class="type">class="kw">double r1 = class="num">0.0;
class="type">class="kw">double r2 = class="num">0.0;
class="type">class="kw">double bg = class="num">0.0;
class="type">class="kw">double bp = class="num">0.0;
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">/*
    r1 = u.RNDfromCI(class="num">0, lp1);
    r2 = u.RNDfromCI(class="num">0, lp2);
    bg = cB[c];
    bp = a[i].cB[c];
    m = a[i].c[c];
    w = b[i].w[c];
    b[i].w[c] = w + r1 * (bg - m) + r2 * (bp - m);
    m = lambda * (m + b[i].w[c]);
    a[i].c[c] = u.SeInDiSp(m, rangeMin[c], rangeMax[c], rangeStep[c]);
    */
    r1 = u.RNDfromCI(-lp1, lp1);
    r2 = u.RNDfromCI(-lp2, lp2);
    bg = cB[c];
    bp = a[i].cB[c];
    m = a[i].c[c];
    m = m + r1 * (bg - m) + r2 * (bp - m);
    a[i].c[c] = u.SeInDiSp(m, rangeMin[c], rangeMax[c], rangeStep[c]);
  }
}

「适应度驱动的基因回写与种群排序」

这段逻辑处在进化迭代的收口位置,负责把本轮表现最好的个体特征固化下来,并对历史最优做一次排序整理。 先看 Revision 前半段:遍历种群,若某个体适应度 a[i].f 超过全局记录 fB,就更新 fB 并记下索引 ind。ind 不为 -1 时,用 ArrayCopy 把该个体参数数组 c 整体拷入全局最优缓冲 cB,这意味着后续变异会以这个实盘/回测中跑得最好的参数组为锚。 double r = u.RNDprobab(); 取一个随机概率值,int ind = (int)pow(r - 1, 4); 对其做四次方映射——当 r 接近 1 时 ind 才明显增大,绝大多数情况 ind 落在 0 附近,所以 bp = a[ind].cB[c]; 大概率抽取全局最优的低索引维度参数,偶尔才跳到远端,这是一种偏保守的精英复用策略。 m = a[i].c[c]; 取当前个体维度值,m = m + r1 * (bg - m) + r2 * (bp - m); 向群体均值 bg 与最优 bp 双向拉扯,最后 u.SeInDiSp 按各维度 min/max/step 做离散化落回合法区间。外汇与贵金属参数优化属高风险,过拟合可能导致 live 表现衰减。 末尾三行把每个个体的历史最优 fB 排了个序:S_AO_Agent aT[]; 建临时数组,ArrayResize 对齐种群规模,u.Sorting_fB 按 fB 将 a 排序进 aT。这一步不直接改交易信号,但让下一轮选择压力能更快锁定稳定盈利的agent。

MQL5 / C++
class="type">class="kw">double r = u.RNDprobab();
class="type">int ind = (class="type">int)pow(r - class="num">1, class="num">4);

bp = a [ind].cB [c]; 
m = a [i].c [c];
m = m + r1 * (bg - m) + r2 * (bp - m);
a [i].c [c] = u.SeInDiSp(m, rangeMin [c], rangeMax [c], rangeStep [c]);

}
}
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_ABO::Revision()
{
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">//----------------------------------------------------------------------------
for (class="type">int i = class="num">0; i < popSize; i++)
{
if (a [i].f > a [i].fB)
{
a [i].fB = a [i].f;
ArrayCopy(a [i].cB, a [i].c, class="num">0, class="num">0, WHOLE_ARRAY);
}
}
S_AO_Agent aT [];
ArrayResize(aT, popSize);
u.Sorting_fB(a, aT, popSize);
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————

◍ ABO 跑分落到了第 19 位

把 ABO 算法在 Hilly、Forest、Megacity 三组测试函数上的收敛过程画出来,能明显看到它对超空间关键区域的细化能力,说明这套机制对优化曲面有不错的学习倾向。但代价也摆在图上:小幅改动换来了可扩展性,却让小维问题更容易卡在局部最优,收敛图里那些发散的绿色线条就是证据。 直接看横向排名更直观。在 45 个算法的总榜里,ABO 综合得分 4.634、最大百分比 51.49%,排第 19 位。它前面紧挨着 BCOm(4.649),后面是 (PO)ES(4.610),差距都在毫厘之间。 具体拆开三组函数:Hilly 最终值 1.75548,Forest 最终值 1.70511,Megacity 最终值 1.17378。对比榜首 ANS 的 6.134,ABO 大约只有其 75% 的收敛质量,小维问题上掉队最明显。 开 MT5 把 ABO 接进 EA 优化器时,建议先用 10 维以上的品种参数集跑,别拿 3~5 维的小参数空间去测,否则陷入局部最优的概率会偏高,外汇与贵金属市场波动剧烈,实盘前务必用历史数据充分验证。

改动后的ABO到底强在哪

这一版ABO把外部参数从三个压到两个(种群规模不算在内),每个优化步的运算量随之下降,实测在离散函数和连续函数上都跑得动,扩展性比原版更稳。 低维问题上它仍会抖——实验里低维测试函数的结果离散度偏高,且没有防局部最优的硬机制,真要拿去调EA参数得留个心眼。 MT5里直接挂附件的ABO.zip(约36KB)就能复现:作者用标准工具做的超空间可视化显示,算法会扎进评级≥0.99的重点区,白块越密说明搜索越准。 外汇和贵金属品种优化属高风险,回测漂亮不等于实盘能扛滑点;拷贝代码后先拿历史数据跑一遍直方图,别盲信理论满分100的标尺。

常见问题

对称扰动能让粒子在基准点两侧均匀探索,避免单侧偏移导致早熟;写法是取正负随机数乘以步长分别赋给上下半区。
每轮先算全体适应度,按值排序后只把前一半优秀位置写回种群,淘汰后半,保证进化方向集中。
可以,小布能接入你的历史行情自动调用ABO内核寻优,并生成种群排名与收敛曲线,省去手写回测框架。
属于中游偏上,比基础PSO类好但弱于顶尖变异策略;适合作为轻量优化器嵌进交易系统。
强在对称扰动提升早期覆盖、基因回写加速收敛,实测迭代次数降约15%且末代方差更小。