动物迁徙优化(AMO)算法·进阶篇
🦌

动物迁徙优化(AMO)算法·进阶篇

(2/3)· 当驯鹿群避开碰撞的规则被写成坐标更新公式,优化问题的探索与利用如何被重新定义

含代码示例偏理论 第 2/3 篇
把群体智能算法直接套进 EA 却忽略邻域拓扑的静态约束,回测曲线往往会假稳定。AMO 里个体只跟固定索引的邻居交互,乱改邻域长度等于重写迁徙规则。外汇与贵金属杠杆高、滑点突发行情多,这类隐蔽偏差可能让实盘表现和实验室完全脱节。

「AMO 种群初始化与邻域扰动的内部逻辑」

这段 C_AO_AMO 类的实现,把自适应元启发式优化(AMO)在 MT5 下的种群运转拆成了三步:初始化、移动、修订。看懂这三段,你就能在 EA 里直接改 popSize 与 rangeStep 观察参数空间搜索行为。 构造函数里先调 StandardInit 做范围校验,随后用 ArrayResize 把 population 与 pTemp 都拉到 popSize 长度,再让每个个体 Init(coords) 占位。注意 epochsP 默认传 0,意味着若外部不显式给训练轮数,类内部不会自行限制迭代。 Moving() 第一次跑时 revision 为 false,会先用均匀随机(RNDfromCI)在 [rangeMin, rangeMax] 内撒点,再用 SeInDiSp 按 rangeStep 对齐到离散网格,之后置 revision=true 直接返回。后续调用才进入邻域高斯扰动:对每个个体 i 取邻居索引 ind1,算坐标差绝对值 dist,以自身坐标 x 为中心、[x-dist, x+dist] 为界(夹到全局范围),用 GaussDistribution 采样新值再离散化。 Revision() 负责记录历史最优:遍历种群,若临时个体 a[i].f 大于当前最优 fB 就更新 fB 与索引 ind,最后 ArrayCopy 把最优坐标 cB 拷出。外汇与贵金属市场高波动,这类优化器用于参数寻优时过拟合概率偏高,实盘前务必多周期验证。

MQL5 / C++
 class="kw">const class="type">int    epochsP = class="num">0)
{
  if (!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) class="kw">return false;
  class=class="str">"cmt">//----------------------------------------------------------------------------
  ArrayResize(population, popSize);
  ArrayResize(pTemp,       popSize);
  for (class="type">int i = class="num">0; i < popSize; i++) population [i].Init(coords);
  class="kw">return true;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_AMO::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">int     ind1    = class="num">0;
  class="type">class="kw">double  dist    = class="num">0.0;
  class="type">class="kw">double  x       = class="num">0.0;
  class="type">class="kw">double  min     = class="num">0.0;
  class="type">class="kw">double  max     = 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">//------------------------------------------------------------------------
      ind1 = GetNeighborsIndex(i);
      dist = fabs(population [ind1].c [c] - a [i].c [c]);
      x    = a [i].c [c];
      min  = x - dist;
      max  = x + dist;
      if (min < rangeMin [c]) min = rangeMin [c];
      if (max > rangeMax [c]) max = rangeMax [c];
      a [i].c [c] = u.GaussDistribution(x, min, max, deviation);
      a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
    }
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_AMO::Revision()
{
  class=class="str">"cmt">//----------------------------------------------------------------------------
  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="type">int     ind1 = class="num">0;
  class="type">int     ind2 = class="num">0;
  class="type">class="kw">double  dist = class="num">0.0;
  class="type">class="kw">double  x    = class="num">0.0;
  class="type">class="kw">double  min  = class="num">0.0;

交叉算子与邻域采样的实现细节

这段实现把种群规模悄悄扩了一倍:Init 里 ArrayResize(population, popSize * 2)pTemp 同样翻倍,并给 2*popSize 个个体做 Init(coords)。多出来的半数容量,是给下面的交叉阶段当临时缓冲 a[] 用的,避免原地覆盖父代。 交叉循环里概率按排名走:第 i 个个体的 prob = 1 - 1/(i+1),也就是说排名越靠后(i 越大)被重组的概率越接近 1。对每个坐标 c,若随机数小于 prob,就随机抽两个不同父代取中点,再用 SeInDiSp 把值钳回 [rangeMin, rangeMax] 并按 rangeStep 离散化。 邻域函数 GetNeighborsIndex 用 MathRand() 做边界感知采样:头尾各 Ncount 个元素只在局部 N=2*Ncount+1 窗口内取随机下标,中间元素则在 i-Ncount 起跳的窗口里取。这套写法在 MT5 里直接贴进 EA 的类的 cpp 就能跑,建议把 neighborsNumberOnSide 先设 2,看排序收敛是否变慢。 外汇与贵金属市场波动剧烈、杠杆风险高,这类群体算法参数敏感,实盘前务必在策略测试器用历史数据验证,结果仅代表历史样本表现,未来可能失效。

MQL5 / C++
class="type">class="kw">double max  = class="num">0.0;
class="type">class="kw">double prob = class="num">0.0;

for (class="type">int i = class="num">0; i < popSize; i++)
{
  prob = class="num">1.0 - (class="num">1.0 / (i + class="num">1));
  
  for (class="type">int c = class="num">0; c < coords; c++)
  {  
    if (u.RNDprobab() < prob)
    {
      ind1 = u.RNDminusOne(popSize);
      ind2 = u.RNDminusOne(popSize);
      if (ind1 == ind2)
      {
        ind2++;
        if (ind2 > popSize - class="num">1) ind2 = class="num">0;
      }
      a [i].c [c] = (population [ind1].c [c] + population [ind2].c [c]) * class="num">0.5;
      a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
    }
  }
}

class=class="str">"cmt">//----------------------------------------------------------------------------
for (class="type">int i = class="num">0; i < popSize; i++) population [i] = a [i];
u.Sorting(population, pTemp, popSize);
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">int C_AO_AMO::GetNeighborsIndex(class="type">int i)
{
  class="type">int Ncount = neighborsNumberOnSide;
  class="type">int N = Ncount * class="num">2 + class="num">1;
  class="type">int neighborIndex;
  class=class="str">"cmt">// Select a random neighbor given the array boundaries
  if (i < Ncount)
  {
    class=class="str">"cmt">// For the first Ncount elements
    neighborIndex = MathRand() % N;
  }
  else
  {
    if (i >= popSize - Ncount)
    {
      class=class="str">"cmt">// For the last Ncount elements
      neighborIndex = (popSize - N) + MathRand() % N;
    }
    else
    {
      class=class="str">"cmt">// For all other elements
      neighborIndex = i - Ncount + MathRand() % N;
    }
  }
  class="kw">return neighborIndex;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">bool C_AO_AMOm::Init(class="kw">const class="type">class="kw">double &rangeMinP  [],
                      class="kw">const class="type">class="kw">double &rangeMaxP  [],
                      class="kw">const class="type">class="kw">double &rangeStepP [],
                      class="kw">const class="type">int     epochsP = class="num">0)
{
  if (!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) class="kw">return false;
  class=class="str">"cmt">//----------------------------------------------------------------------------
  ArrayResize(population, popSize * class="num">2);
  ArrayResize(pTemp,       popSize * class="num">2);
  for (class="type">int i = class="num">0; i < popSize * class="num">2; i++) population [i].Init(coords);
  class="kw">return true;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————

◍ 变异阶段怎么用邻域距离约束搜索范围

在差分进化类 EA 里,子代变异不是纯随机撒点,而是先拿当前个体 i 在种群里的邻居 ind1,算两者在第 c 维上的绝对距离 dist,再把搜索区间锁在 [x-dist, x+dist] 之内,超出预设的 rangeMin/rangeMax 就截断。 代码里 prob = 1.0 - (1.0/(i+1)) 意味着排序越靠后的个体 i 越大(i 从 0 起,i=0 时 prob=0,i=9 时 prob≈0.9),交叉算子的触发概率随排名陡升,这是典型的排名偏好设计。 如果随机数小于 prob,就从种群里抽两个不同下标 ind1、ind2 做平均赋值:a[i].c[c] = (population[ind1].c[c] + population[ind2].c[c]) * 0.5,否则走高斯扰动 u.GaussDistribution(x, min, max, deviation)。最后统一用 SeInDiSp 把值吸附到离散网格步长上,避免浮点越界。 开 MT5 把 popSize 设成 30 跑一遍,观察 i 接近 popSize-1 时交叉平均占比是否明显拉高,能直接验证这套排名概率是否按预期工作;外汇与贵金属杠杆高,参数扰动可能引发净值剧烈回撤,需先在策略测试器用历史数据确认。

MQL5 / C++
for (class="type">int i = class="num">0; i < popSize; i++) population [i + popSize] = a [i];
u.Sorting(population, pTemp, popSize * class="num">2);
class="type">int    ind1 = class="num">0;
class="type">int    ind2 = class="num">0;
class="type">class="kw">double dist = class="num">0.0;
class="type">class="kw">double x    = class="num">0.0;
class="type">class="kw">double min  = class="num">0.0;
class="type">class="kw">double max  = class="num">0.0;
class="type">class="kw">double prob = class="num">0.0;
for (class="type">int i = class="num">0; i < popSize; i++)
{
prob = class="num">1.0 - (class="num">1.0 / (i + class="num">1));
  for (class="type">int c = class="num">0; c < coords; c++)
{
class=class="str">"cmt">//------------------------------------------------------------------------
ind1 = GetNeighborsIndex(i);
dist = fabs(population [ind1].c [c] - a [i].c [c]);
x    = a [i].c [c];
min  = x - dist;
max  = x + dist;
if (min < rangeMin [c]) min = rangeMin [c];
if (max > rangeMax [c]) max = rangeMax [c];
a [i].c [c] = u.GaussDistribution(x, min, max, deviation);
class=class="str">"cmt">//----------------------------------------------------------------------------
if (u.RNDprobab() < prob)
{
ind1 = u.RNDminusOne(popSize);
ind2 = u.RNDminusOne(popSize);
if (ind1 == ind2)
{
ind2++;
if (ind2 > popSize - class="num">1) ind2 = class="num">0;
}
a [i].c [c] = (population [ind1].c [c] + population [ind2].c [c]) * class="num">0.5;
}

a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
}
}
class="type">int    ind1     = class="num">0;
class="type">int    ind2     = class="num">0;
class="type">class="kw">double dist     = class="num">0.0;
class="type">class="kw">double x        = class="num">0.0;
class="type">class="kw">double min      = class="num">0.0;
class="type">class="kw">double max      = class="num">0.0;
class="type">class="kw">double prob     = class="num">0.0;
for (class="type">int i = class="num">0; i < popSize; i++)
{
prob = class="num">1.0 - (class="num">1.0 / (i + class="num">1));

for (class="type">int c = class="num">0; c < coords; c++)
{
class=class="str">"cmt">//------------------------------------------------------------------------
ind1 = GetNeighborsIndex(i);
dist = fabs(population [ind1].c [c] - a [i].c [c]);
x    = population [ind1].c [c];

「变异与精英留存的实现细节」

这段逻辑处在个体生成循环内部,先以当前解 x 为中心、dist 为半径划定搜索边界,再用 rangeMin/rangeMax 截断越界部分,保证新基因落在参数定义域内。 min = x - dist; max = x + dist; if (min < rangeMin[c]) min = rangeMin[c]; if (max > rangeMax[c]) max = rangeMax[c]; a[i].c[c] = u.GaussDistribution(x, min, max, deviation); a[i].c[c] = u.SeInDiSp(a[i].c[c], rangeMin[c], rangeMax[c], rangeStep[c]); 上方先用高斯分布做局部扰动,再用离散化函数把连续值吸附到合法步长上。 概率 prob 控制是否触发交叉:当随机数小于 prob 且再掷一次小于 0.01 时,取两个随机旧个体做平均交叉,这层 1% 的小概率让种群偶尔大幅跳变,可能逃离局部极值。 if (u.RNDprobab() < prob) { if (u.RNDprobab() <= 0.01) { ind1 = u.RNDminusOne(popSize); ind2 = u.RNDminusOne(popSize); a[i].c[c] = (population[ind1].c[c] + population[ind2].c[c]) * 0.5; a[i].c[c] = u.SeInDiSp(a[i].c[c], rangeMin[c], rangeMax[c], rangeStep[c]); } } 注释掉的 ind1==ind2 修正分支说明作者曾考虑去重,但最终留了简单版。 Revision 函数在每代末尾跑:先扫一遍找最优适应度 fB 并记录索引,若找到就用 ArrayCopy 把该染色体存进全局最优 cB。 int ind = -1; for (int i = 0; i < popSize; i++) { if (a[i].f > fB) { fB = a[i].f; ind = i; } } if (ind != -1) ArrayCopy(cB, a[ind].c, 0, 0, WHOLE_ARRAY); 随后把当代种群追加进 population 后半段,调用 Sorting 对两倍规模做排序,隐式完成精英保留——前 popSize 个会进入下一代。 for (int i = 0; i < popSize; i++) population[popSize + i] = a[i]; u.Sorting(population, pTemp, popSize * 2); 在 MT5 里把 popSize 设到 50、prob 设 0.8 跑黄金小时图,能直接观察 fB 是否单调改善来判断截断逻辑有无写错。

MQL5 / C++
min = x - dist;
max = x + dist;
if (min < rangeMin [c]) min = rangeMin [c];
if (max > rangeMax [c]) max = rangeMax [c];
a [i].c [c] = u.GaussDistribution(x, min, max, deviation);
a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
class=class="str">"cmt">//------------------------------------------------------------------------
if (u.RNDprobab() < prob)
{
  if (u.RNDprobab() <= class="num">0.01)
  {
    ind1 = u.RNDminusOne(popSize);
    ind2 = u.RNDminusOne(popSize);
    class=class="str">"cmt">//if (ind1 == ind2)
    {
      class=class="str">"cmt">//ind2++;
      class=class="str">"cmt">//if (ind2 > popSize - class="num">1) ind2 = class="num">0;
      a [i].c [c] = (population [ind1].c [c] + population [ind2].c [c]) * class="num">0.5;
      a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
    }
  }
  class=class="str">"cmt">//ind1 = u.RNDminusOne(popSize);
  class=class="str">"cmt">//a [i].c [c] = population [ind1].c [c];
}
}
}
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_AMO::Revision()
{
  class=class="str">"cmt">//----------------------------------------------------------------------------
  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++) population [popSize + i] = a [i];
  u.Sorting(population, pTemp, popSize * class="num">2);
}
让小布替你跑这套邻域扫描
小布盯盘已内置多群体智能算法的参数敏感性视图,打开对应品种页即可看到 AMO 邻域长度与种群更新率的实时诊断,你只管判断当前行情是否适配这种集体行为模型。

常见问题

通常在每个个体周围以同心圆划分半径,半径内为排斥、适中环为对齐、外环为弱吸引;MQL5 中用向量距离判断并分别乘不同系数更新坐标即可。
可以,小布盯盘的 AIGC 层会解析 EA 的迭代日志,把迁徙与种群更新两个阶段在品种页以独立轨道呈现,省去你手动打点。
原设计用环上固定索引保证每次迭代计算量恒定且行为可复现,动态近邻虽更灵活但会引入随机邻域导致收敛路径难以分析。
倾向用排序后的线性映射而非指数惩罚,保留中后段个体以维持多样性,具体系数建议在样本外数据上做敏感度扫描。