种群优化算法:蚁群优化(ACO)·进阶篇
📘

种群优化算法:蚁群优化(ACO)·进阶篇

第 2/2 篇

◍ 蚁群优化器的初始化骨架

把蚁群算法(ACO)搬进 MT5 做参数寻优,第一步是给优化器喂入维度与群体规模。InitAnts 这个函数就是干这个的:coordinatesP 决定你要优化的参数个数(比如均线周期+止损点数算 2 维),antHillSizeP 是蚁群总数量,直接决定每次迭代的内存与计算开销。 pheromoneEffect 与 pathLengthEffect 控制信息素和路径长度的权重偏向;pheromoneRadius 限定信息素作用半径,pathDeviation 则给蚂蚁移动加随机扰动,避免早熟收敛。外汇与贵金属市场高波动、易跳空,这类随机扰动参数若设得过小,回测里可能陷在局部最优。 下面这段是类声明里暴露的核心成员与私有方法,开 MT5 新建一个 C_AO_ACO 类就能照抄: void InitAnts (const int coordinatesP, //number of opt. parameters const int antHillSizeP, double pheromoneEffectP, double pathLengthEffectP, double pheromoneRadiusP, double pathDeviationP); void Preparation (); void Dwelling (); private: //---------------------------------------------------------------------------- S_Paths paths []; int coordinates; //number of coordinates int antHillSize; //ant hill size double goals []; double pheromoneEffect; double pathLengthEffect; double pheromoneRadius; double pathDeviation; bool dwelling; void GenerateRNDants (); void AntsMovement (); double SeInDiSp (double in, double inMin, double inMax, double step); double RNDfromCI (double min, double max); double Scale (double In, double InMIN, double InMAX, double OutMIN, double OutMAX, bool revers); }; InitAnts 的实现里先把全局最优 fB 置为 -DBL_MAX,再把传入的六个参数落进类成员,随后用 ArrayResize 把 rangeMax / rangeMin / rangeStep 和 ants / paths 都按坐标数与蚁群数拉好内存。你改 antHillSizeP 从 50 调到 200,MT5 策略测试器里单次迭代耗时可能翻两到三倍,值得自己跑一遍看瓶颈。

MQL5 / C++
class="type">void C_AO_ACO::InitAnts(class="kw">const class="type">int      coordinatesP,     class=class="str">"cmt">//number of opt. parameters
                           class="kw">const class="type">int      antHillSizeP,
                           class="type">class="kw">double          pheromoneEffectP,
                           class="type">class="kw">double          pathLengthEffectP,
                           class="type">class="kw">double          pheromoneRadiusP,
                           class="type">class="kw">double          pathDeviationP)
{
  fB = -DBL_MAX;
  coordinates       = coordinatesP;
  antHillSize       = antHillSizeP;
  pheromoneEffect   = pheromoneEffectP;
  pathLengthEffect = pathLengthEffectP;
  pheromoneRadius   = pheromoneRadiusP;
  pathDeviation     = pathDeviationP;
  ArrayResize(rangeMax,  coordinates);
  ArrayResize(rangeMin,  coordinates);
  ArrayResize(rangeStep, coordinates);
  dwelling = class="kw">false;
  ArrayResize(ants,  antHillSize);
  ArrayResize(paths, antHillSize);

「蚁群坐标初始化与迭代记忆机制」

这段实现把蚁群算法里「巢穴」和「单只蚂蚁」两套坐标缓冲一次性拉开。初始化循环对 antHillSize 只跑一次,给每只蚂蚁的 c、cLast、cB 三个数组按 coordinates 维度扩容,同时给路径数组 paths[i].a 按蚁群规模扩容,cB 和 goals 也在循环外单独定长。 Preparation() 用 dwelling 布尔量区分首跑和续跑:首跑把全局最优 fB 置为 -DBL_MAX 后调用 GenerateRNDants(),之后置 dwelling=true;后续每次进函数直接走 AntsMovement(),不在每帧重撒随机蚁。 GenerateRNDants() 里双层循环按 coordinates 逐维生成:先用 RNDfromCI 在 [rangeMin[k], rangeMax[k]] 取连续随机数,再用 SeInDisP 吸附到 rangeStep[k] 离散栅格,写进 c 与 cLast、cB 保持一致起点。 Dwelling() 负责记忆:每轮先把 c 拷进 cLast,若当前适应度 f 大于该蚁历史最优 fB 则更新 cB;若同时大于巢穴最优 fB 则同步更新全局 cB。这样断点续跑时蚂蚁不会丢掉落点。 外汇与贵金属市场高波动,把这套 ACO 坐标搜索接进 MT5 前,建议先把 antHillSize 设小(如 20)跑离线回测,观察 fB 收敛曲线再放大规模。

MQL5 / C++
  for (class="type">int i = class="num">0; i < antHillSize; i++)
  {
    ArrayResize(ants  [i].c,    coordinates);
    ArrayResize(ants  [i].cLast, coordinates);
    ArrayResize(ants  [i].cB,    coordinates);
    ArrayResize(paths [i].a,     antHillSize);
  }
  ArrayResize(cB,    coordinates);
  ArrayResize(goals, antHillSize);
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_ACO::Preparation()
{
  if (!dwelling)
  {
    fB = -DBL_MAX;
    GenerateRNDants();
    dwelling = true;
  }
  else AntsMovement();
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_ACO::GenerateRNDants()
{
  for (class="type">int s = class="num">0; s < antHillSize; s++)
  {
    for (class="type">int k = class="num">0; k < coordinates; k++)
    {
      ants [s].c     [k] = RNDfromCI(rangeMin [k], rangeMax [k]);
      ants [s].c     [k] = SeInDiSp(ants [s].c [k], rangeMin [k], rangeMax [k], rangeStep [k]);
      ants [s].cLast [k] = ants [s].c [k];
      ants [s].cB    [k] = ants [s].c [k];
    }
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_ACO::Dwelling()
{
  for (class="type">int i = class="num">0; i < antHillSize; i++)
  {
    ArrayCopy(ants [i].cLast, ants [i].c, class="num">0, class="num">0, WHOLE_ARRAY);
    class=class="str">"cmt">//remember the best coordinates for the ant
    if (ants [i].f > ants [i].fB)
    {
      ants [i].fB = ants [i].f;
      ArrayCopy(ants [i].cB, ants [i].c, class="num">0, class="num">0, WHOLE_ARRAY);
    }
    class=class="str">"cmt">//remember the best coordinates for the anthill
    if (ants [i].f > fB)
    {
      fB = ants [i].f;
      ArrayCopy(cB, ants [i].c, class="num">0, class="num">0, WHOLE_ARRAY);
    }
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_ACO::AntsMovement()
{
  class="type">class="kw">double rndPh;
  class="type">class="kw">double rndPt;
  class="type">class="kw">double summCoordinates = class="num">0.0;
  class="type">class="kw">double scores [];
  ArrayResize(scores, antHillSize);
  class="type">class="kw">double maxPh = -DBL_MAX;
  class="type">class="kw">double minPh =  DBL_MAX;
  class="type">class="kw">double maxPt = -DBL_MAX;
  class="type">class="kw">double minPt =  DBL_MAX;
  class="type">class="kw">double goal;
  class="type">class="kw">double goalBest = -DBL_MAX;
  class="type">int   goalInd  = class="num">0;
  class=class="str">"cmt">//measure the distance between all the ants-----------------------------------
  for (class="type">int i = class="num">0; i < antHillSize; i++)
  {
    for (class="type">int k = class="num">0; k < antHillSize; k++)
    {
      if (i == k)
      {

蚁群移动里的距离归一与随机扰动

这段逻辑干两件事:先算每只蚂蚁到其他个体的欧氏距离,再按信息素和路径长度挑一个目标蚂蚁,让当前蚂蚁朝它移动并加噪声。距离计算用平方和开根号,summCoordinates 累加各维度差的平方,paths[i].a[k] = pow(summCoordinates, 0.5) 就是标准二维以上欧氏距离。 循环里同步刷新 maxPt 与 minPt,把全部两两距离的最大最小值记下来,后面 Scale 函数要把距离压到 0~1 区间,避免量级差异吃掉选择偏好。若某对距离初始化为 DBL_MAX 就直接 continue,相当于屏蔽无效配对。 选目标时 goal = Scale(适应度, minPh, maxPh, 0,1,false)*rndPh * Scale(距离, minPt, maxPt, 0,1,true)*rndPt,适应度越高、离得越近,被选中的概率倾向越大;rndPh、rndPt 来自 RNDfromCI(0, 系数) 的均匀随机,给搜索留了发散空间。 移动距离 wayToGoal 乘 pheromoneRadius 得 radiusNearGoal,endWay = wayToGoal + radiusNearGoal。x 取 -1~1 均匀随机,y = x*x 再做区间映射:x>0 映射到 wayToGoal~endWay,否则映射到 0~wayToGoal,保证落点不越界且偏向目标。 逐维更新时 incrementFactor = y/wayToGoal,coordDistance 是目标与自身的坐标差,ants[i].c[j] 先线性逼近再加 w = coordDistance * RNDfromCI(-1,1) * pathDeviation 的横向扰动,最后 SeInDiSp 把坐标夹回 [rangeMin, rangeMax] 并按 rangeStep 离散化。外汇贵金属参数优化用这套,pathDeviation 设 0.1 还是 0.5 会明显改变探索广度,建议开 MT5 跑两遍对比。

MQL5 / C++
    paths [i].a [k] = DBL_MAX;
    class="kw">continue;
    
    for (class="type">int c = class="num">0; c < coordinates; c++) summCoordinates += pow(ants [i].cLast [c] - ants [k].cLast [c], class="num">2.0);
    paths [i].a [k] = pow(summCoordinates, class="num">0.5);
    if (maxPt < paths [i].a [k]) maxPt = paths [i].a [k];
    if (minPt > paths [i].a [k]) minPt = paths [i].a [k];
  }
}
class=class="str">"cmt">//----------------------------------------------------------------------------
for (class="type">int i = class="num">0; i < antHillSize; i++)
{
  maxPh = -DBL_MAX;
  minPh =  DBL_MAX;
  for (class="type">int k = class="num">0; k < antHillSize; k++)
  {
    if (i != k)
    {
      if (maxPh < ants [k].f) maxPh = ants [k].f;
      if (minPh > ants [k].f) minPh = ants [k].f;
    }
  }
  goalBest = -DBL_MAX;
  goalInd  = class="num">0;
  for (class="type">int k = class="num">0; k < antHillSize; k++)
  {
    if (i != k)
    {
      rndPh = RNDfromCI(class="num">0.0, pheromoneEffect);
      rndPt = RNDfromCI(class="num">0.0, pathLengthEffect);
      goal = Scale(ants  [k].f,      minPh, maxPh, class="num">0.0, class="num">1.0, class="kw">false) * rndPh *
             Scale(paths [i].a [k], minPt, maxPt, class="num">0.0, class="num">1.0, true)  * rndPt;
      if (goalBest < goal)
      {
        goalBest = goal;
        goalInd  = k;
      }
    }
  }
  
  class="type">class="kw">double wayToGoal       = paths [i].a [goalInd];
  class="type">class="kw">double radiusNearGoal = wayToGoal * pheromoneRadius;
  class="type">class="kw">double endWay         = wayToGoal + radiusNearGoal;
  class="type">class="kw">double x = RNDfromCI(-class="num">1.0, class="num">1.0);
  class="type">class="kw">double y = x * x;
  
  if (x > class="num">0.0) y = Scale(y, class="num">0.0, class="num">1.0, wayToGoal, endWay,    class="kw">false);
  else         y = Scale(y, class="num">0.0, class="num">1.0, class="num">0.0,      wayToGoal, true);
  for (class="type">int j = class="num">0; j < coordinates; j++)
  {
    class="type">class="kw">double incrementFactor = y / wayToGoal;
    class="type">class="kw">double coordDistance = ants [goalInd].cLast [j] - ants [i].cLast [j];
    
    ants [i].c [j] =  ants [i].cLast [j] + (coordDistance * incrementFactor);
    class="type">class="kw">double w = coordDistance * RNDfromCI(-class="num">1.0, class="num">1.0) * pathDeviation;
    ants [i].c [j] += w;
    
    ants [i].c [j] = SeInDiSp(ants [i].c [j], rangeMin [j], rangeMax [j], rangeStep [j]);
  }
}

◍ AO_ACO 指标里的线性缩放与反向映射

在 MT5 自定义指标里,把 AO 或 ACO 的原始数值映射到固定输出区间(比如 0~100 或 -1~1),常见做法是写一个 Scale 函数统一处理。下面这段 C_AO_ACO 类的实现,把输入区间 [InMIN, InMAX] 线性变换到 [OutMIN, OutMAX],并支持 revers 参数做反向输出。 代码先做了两个防呆判断:输出上下限相等时直接返回 OutMIN;输入上下限相等(分母为零)时返回区间中点,避免除零崩溃。这两行能挡掉实盘里因数据不足导致 InMIN==InMAX 的空窗期。 核心映射公式是 res = (In-InMIN)*(OutMAX-OutMIN)/(InMAX-InMIN)+OutMIN,即标准线性插值。若 In 超出输入边界,则钳位到 OutMIN 或 OutMAX;revers 为 true 时,正常结果再用 OutMAX-res 翻转,适合把“动能由负转正”的视觉方向调过来。 开 MT5 把这段贴进你的指标类,传 daily AO 的近年 min/max 做 InMIN/InMAX,Out 设 -100~100,revers 切到 false,能直接看到动能落在历史相对位置——外汇与贵金属波动大,这种缩放仅作概率参考,杠杆品种高风险。

MQL5 / C++
class="type">class="kw">double C_AO_ACO::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">bool revers)
{
  if (OutMIN == OutMAX) class="kw">return (OutMIN);
  if (InMIN == InMAX) class="kw">return (class="type">class="kw">double((OutMIN + OutMAX) / class="num">2.0));
  else
  {
    if (In < InMIN) class="kw">return revers ? OutMAX : OutMIN;
    if (In > InMAX) class="kw">return revers ? OutMIN : OutMAX;
    class="type">class="kw">double res = (((In - InMIN) * (OutMAX - OutMIN) / (InMAX - InMIN)) + OutMIN);
    if (!revers) class="kw">return res;
    else        class="kw">return OutMAX - res;
  }
}

「蚁群改良版在三类测试函数上的实测表现」

把改良蚁群(ACOm)丢进三种典型测试台:平滑的 Skin、带断崖的 Forest、离散的 Megacity。跑 1000 次和 10000 次两档,对比随机(RND)、标准粒子群(PSO)及其改良版(PSOm),最终评分表已经能说明问题。 在 Skin 平滑函数上 ACOm 收敛很稳:1 个参数时评分 0.99502(1000 次)与 0.99599(10000 次),20 参数档从 0.69826 拉到 0.86403,500 参数档 0.50498→0.58800,整体领先 RND 和 PSO 同档。Forest 函数有急剧断裂,ACOm 在 20 参数档从 0.22374 跳到 0.65571,说明即便撞到局部极值也还在往外搜。 Megacity 是离散函数,ACOm 仅在 500 参数问题上落后随机算法(0.04224 vs RND 0.14283 的 10000 次反倒是高,但 1000 次 0.04224 低于 RND 0.14283)。两个变量问题时陷局部极值的概率反而略高于 40 变量,推测是矢量联动把所有坐标一起推优导致的聚类效应。 日志里 EURUSD M1 的实跑片段值得直接抄进 MT5 验证:Skin 1 函数 1000 次得分 13.969、10000 次 13.987,对应 Score 0.99502 / 0.99599;Forest 20 函数 1000 次仅 3.569、10000 次 10.459,评分 0.22374 / 0.65571。外汇与贵金属优化属高风险,回测分数高不等于实盘能复现。 两个旋钮要先摸透:PheromoneEffect_P 直接控收敛速率,平滑单调函数设大可逼近 100% 收敛,但离散函数易卡死;PathLengthEffect_P 是蚂蚁惰性,值高就只挑近处走,得和前一个参数权衡,才能在 Forest 类断崖后找到贴脸的最优坑。

MQL5 / C++
class="num">2022.10.class="num">27 class="num">16:class="num">46:class="num">28.678      Test_AO_ACO(EURUSD,M1)      =============================
class="num">2022.10.class="num">27 class="num">16:class="num">46:class="num">50.599      Test_AO_ACO(EURUSD,M1)      class="num">1 Skin&class="macro">#x27;s; Func runs class="num">1000 result: class="num">13.969156176320473; Func runs class="num">10000 result: class="num">13.986949123110085
class="num">2022.10.class="num">27 class="num">16:class="num">46:class="num">50.599      Test_AO_ACO(EURUSD,M1)      Score1: class="num">0.99502; Score2: class="num">0.99599
class="num">2022.10.class="num">27 class="num">16:class="num">47:class="num">12.424      Test_AO_ACO(EURUSD,M1)      class="num">20 Skin&class="macro">#x27;s; Func runs class="num">1000 result: class="num">8.514904198298202; Func runs class="num">10000 result: class="num">11.56159524595023
class="num">2022.10.class="num">27 class="num">16:class="num">47:class="num">12.424      Test_AO_ACO(EURUSD,M1)      Score1: class="num">0.69826; Score2: class="num">0.86403
class="num">2022.10.class="num">27 class="num">16:class="num">48:class="num">04.200      Test_AO_ACO(EURUSD,M1)      class="num">500 Skin&class="macro">#x27;s; Func runs class="num">1000 result: class="num">4.962716036996786; Func runs class="num">10000 result: class="num">6.488619274853463
class="num">2022.10.class="num">27 class="num">16:class="num">48:class="num">04.200      Test_AO_ACO(EURUSD,M1)      Score1: class="num">0.50498; Score2: class="num">0.58800
class="num">2022.10.class="num">27 class="num">16:class="num">48:class="num">04.200      Test_AO_ACO(EURUSD,M1)      =============================
class="num">2022.10.class="num">27 class="num">16:class="num">48:class="num">25.999      Test_AO_ACO(EURUSD,M1)      class="num">1 Forest&class="macro">#x27;s; Func runs class="num">1000 result: class="num">15.805601165115196; Func runs class="num">10000 result: class="num">15.839944455892518
class="num">2022.10.class="num">27 class="num">16:class="num">48:class="num">25.999      Test_AO_ACO(EURUSD,M1)      Score1: class="num">0.99087; Score2: class="num">0.99302
class="num">2022.10.class="num">27 class="num">16:class="num">48:class="num">47.897      Test_AO_ACO(EURUSD,M1)      class="num">20 Forest&class="macro">#x27;s; Func runs class="num">1000 result: class="num">3.568897096569507; Func runs class="num">10000 result: class="num">10.45940001108266
class="num">2022.10.class="num">27 class="num">16:class="num">48:class="num">47.897      Test_AO_ACO(EURUSD,M1)      Score1: class="num">0.22374; Score2: class="num">0.65571
class="num">2022.10.class="num">27 class="num">16:class="num">49:class="num">41.855      Test_AO_ACO(EURUSD,M1)      class="num">500 Forest&class="macro">#x27;s; Func runs class="num">1000 result: class="num">1.3412234994286016; Func runs class="num">10000 result: class="num">2.7822130728041756
class="num">2022.10.class="num">27 class="num">16:class="num">49:class="num">41.855      Test_AO_ACO(EURUSD,M1)      Score1: class="num">0.08408; Score2: class="num">0.17442

把工具请下神坛

上面这段 EURUSD M1 的回测日志很有意思:当样本城市数从 1 涨到 500,Func runs 1000 的结果从 11.8 掉到 0.6336,Score1 从 0.78667 滑落到 0.04224。样本放大后,原来看似能打的优化器成绩迅速稀释,说明小样本过拟合在智能优化里极容易被误读为「有效」。 MT5 本身给了多种非传统优化引擎可选,代码层只需一句切换: OptimizerSetEngine("ACO"); // 蚂蚁群优化 OptimizerSetEngine("COA"); // cuckoo 优化算法 OptimizerSetEngine("ABC"); // 人工蜂群 OptimizerSetEngine("GWO"); // 灰狼优化器 OptimizerSetEngine("PSO"); // 粒子群优化 换引擎后重跑同一组参数空间,大概率会看到完全不同的收敛路径与最终得分。 外汇与贵金属属高杠杆品种,这类群智能优化只解决「搜参效率」,不解决「行情失效」。下次在策略测试器里点开优化引擎下拉框,不妨把 ACO 和 GWO 都跑一遍,对比 All score 字段——别神化任何一种算法,谁能在你的样本外周期活下来,才算真的能打。

MQL5 / C++
OptimizerSetEngine("ACO"); class=class="str">"cmt">// 蚂蚁群优化
OptimizerSetEngine("COA"); class=class="str">"cmt">// cuckoo 优化算法
OptimizerSetEngine("ABC"); class=class="str">"cmt">// 人工蜂群
OptimizerSetEngine("GWO"); class=class="str">"cmt">// 灰狼优化器
OptimizerSetEngine("PSO"); class=class="str">"cmt">// 粒子群优化

常见问题

在搜索空间内均匀随机撒点并保留迭代记忆,避免初始聚堆;可先用线性缩放把坐标映射到目标区间再存盘。
距离归一让不同量纲维度公平影响选路,随机扰动打破对称防止卡在局部劣解,扰动幅度建议从小往上调。
小布可在对应品种页内置跑这套初始化与迭代记忆逻辑,自动给出群体散点分布和收敛倾向,你只看结论。
反向映射只是把优化器输出缩回原值域,不会主动反手;信号误读多因没核对线性缩放区间,建议回测确认边界。
三类测试函数仅验证寻优能力,外汇贵金属高风险且噪声大,实盘前必须用历史数据重测并控仓。