老鹰策略(ES)·进阶篇
🦅

老鹰策略(ES)·进阶篇

(2/3)· 当网格遍历卡在局部最优,鹰眼式全局-局部交替搜索可能帮你跳出陷阱

含代码示例 第 2/3 篇
很多交易者跑 EA 优化时习惯性把迭代次数拉满,以为穷举更可靠,结果 CPU 烧了一夜仍困在局部洼地。老鹰策略把'广域扫视'和'俯冲定点'拆成两套机制交替跑,思路值得借来改你的调参脚本。

◍ 萤火虫算法的全局与局部搜索切换逻辑

这段初始化与移动代码实现了一个混合进化策略:先用 Lévy 飞行做全局探索,一旦当前最优适应度 fB 超过上轮记录 prevBestFitness,就切进以最优个体为中心的局部开发阶段。 初始化里 gamma_es 固定为 1.0,这是萤火虫算法吸收亮度的固定参数;种群每个个体坐标先在 [rangeMin, rangeMax] 内随机生成,再用 SeInDiSp 对齐到离散步长网格,避免无效浮点解。 Moving() 每次 epochCurrent 自增后做相位判断:不在局部阶段时跑 GlobalExploration(),若连续 5 代没提升(stagnationCounter>5),lambda 从当前值减 0.1 但不低于 1.0,让 Lévy 步长更激进。进入局部阶段后,有 0.8 概率调用 LocalExploitation(),用萤火虫相互吸引机制精搜。 在 MT5 里把 stagnationCounter 的阈值 5 和局部阶段触发概率 0.8 改成变量,能直接观察外汇品种参数空间陷入早熟的概率变化,贵金属与外汇均属高风险,回测结论仅代表历史样本倾向。

MQL5 / C++
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">//------------------------------------------------------------------
  class=class="str">"cmt">// Initialize arrays
  ArrayResize(levyStep, coords);
  class=class="str">"cmt">// Initialize phase tracking
  inLocalSearchPhase = false;
  localSearchCenter  = class="num">0;
  localSearchCounter = class="num">0;
  class=class="str">"cmt">// Initialize convergence tracking
  prevBestFitness   = -DBL_MAX;
  stagnationCounter = class="num">0;
  class=class="str">"cmt">// Initialize epoch tracking
  epochMax    = epochsP;
  epochCurrent = class="num">0;
  class=class="str">"cmt">// Fixed Firefly parameter
  gamma_es = class="num">1.0;
  class=class="str">"cmt">// Initialize the population randomly
  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]);
    }
    a [i].f  = -DBL_MAX;
    a [i].fB = -DBL_MAX;
  }
  class="kw">return true;
}
class="type">void C_AO_ES::Moving()
{
  epochCurrent++;
  class=class="str">"cmt">// PHASE DECISION: Alternating between global and local search
  if (!inLocalSearchPhase)
  {
    class=class="str">"cmt">// PHASE class="num">1: GLOBAL EXPLORATION using Levy flights
    GlobalExploration();
    class=class="str">"cmt">// Check whether it is necessary to class="kw">switch to local search
    class=class="str">"cmt">// Switch if we find a promising area(improving the best fitness)
    if (fB > prevBestFitness)
    {
      inLocalSearchPhase = true;
      localSearchCounter = class="num">0;
      prevBestFitness     = fB;
      stagnationCounter   = class="num">0;
      class=class="str">"cmt">// Find the best agent to center local search
      localSearchCenter = class="num">0;
      class="type">class="kw">double bestFit = -DBL_MAX;
      for (class="type">int i = class="num">0; i < popSize; i++)
      {
        if (a [i].f > bestFit)
        {
          bestFit = a [i].f;
          localSearchCenter = i;
        }
      }
    }
    else
    {
      stagnationCounter++;
      class=class="str">"cmt">// In case of stagnation, increase exploration
      if (stagnationCounter > class="num">5)
      {
        lambda = MathMax(class="num">1.0, lambda - class="num">0.1); class=class="str">"cmt">// Make Levy flights more aggressive
      }
    }
  }
  else
  {
    if (u.RNDprobab() < class="num">0.8)
    {
      class=class="str">"cmt">// PHASE class="num">2: LOCAL EXPLOITATION using the Firefly algorithm
      LocalExploitation();
      localSearchCounter++;

「局部收敛后如何切回全局搜索」

这段逻辑处理的是优化器在局部搜索迭代次数耗尽后的状态回退。当 localSearchCounter 达到 localIterations 时,把 inLocalSearchPhase 置为 false,并将 lambda 还原为 params[1].val 的初始值,避免局部参数污染下一轮全局探索。 Revision 函数负责更新个体历史最优与全局最优:若当前适应度 a[i].f 优于记录值 a[i].fB,则拷贝坐标数组;同样逻辑比对全局 fB,保证 cB 始终指向种群最佳位置。 全局探索阶段用 Levy 飞行更新坐标。stepScale 按进度自适应,初始为 0.01+0.2*(1-progress),epochCurrent 越接近 epochMax,步长越小,从大步长搜素收敛到精细搜素的概率倾向明显。边界用 SeInDiSp 做离散约束,防止越界。 局部开发阶段先筛出最佳解超球体内的代理,normalized_dist 累加各维度距离,为后续萤火虫算法靠拢做准备。外汇与贵金属参数优化属高风险操作,回测过拟合可能导致实盘失效。

MQL5 / C++
      class=class="str">"cmt">// Return to global search after local iterations are complete
      if (localSearchCounter >= localIterations)
      {
        inLocalSearchPhase = false;
        lambda = params [class="num">1].val; class=class="str">"cmt">// Reset lambda to its original value
      }
    }
    else
    {
      for (class="type">int i = class="num">0; i < popSize; i++)
      {
        for (class="type">int c = class="num">0; c < coords; c++)
        {
          if (u.RNDprobab() < class="num">0.5)
          {
            a [i].c [c] = cB [c];
          }
        }
      }
    }
  }
}
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————
class="type">void C_AO_ES::Revision()
{
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    class=class="str">"cmt">// Updating the personal best one
    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">// Update the global best one
    if (a [i].f > fB)
    {
      fB = a [i].f;
      ArrayCopy(cB, a [i].c, class="num">0, class="num">0, WHOLE_ARRAY);
    }
  }
}
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————
class=class="str">"cmt">// PHASE class="num">1: GLOBAL EXPLORATION using Levy flights
class="type">void C_AO_ES::GlobalExploration()
{
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    class=class="str">"cmt">// Generate Levy step
    GenerateLevyStep();
    class=class="str">"cmt">// Update position using Levy flight
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      class="type">class="kw">double range = rangeMax [c] - rangeMin [c];
      class=class="str">"cmt">// Adaptive step size depending on search progress
      class="type">class="kw">double progress = (epochMax > class="num">0) ? (class="type">class="kw">double)epochCurrent / (class="type">class="kw">double)epochMax : class="num">0.5;
      class="type">class="kw">double stepScale = class="num">0.01 + class="num">0.2 * (class="num">1.0 - progress); class=class="str">"cmt">// Start with big steps
      class=class="str">"cmt">// Apply the Levy step
      a [i].c [c] += levyStep [c] * range * stepScale;
      class=class="str">"cmt">// Boundary restrictions
      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=class="str">"cmt">// PHASE class="num">2: Local exploitation using the Firefly algorithm
class="type">void C_AO_ES::LocalExploitation()
{
  class=class="str">"cmt">// Identification of agents inside the hypersphere around the best solution
  class="type">class="kw">double agents_in_sphere [];
  ArrayResize(agents_in_sphere, class="num">0);
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    class="type">class="kw">double normalized_dist = class="num">0.0;
    for (class="type">int c = class="num">0; c < coords; c++)
    {

邻域过稀时的兜底取样

上面那段逻辑先用归一化欧氏距离圈出球形邻域里的个体,半径内或中心自身都会被收进 agents_in_sphere。若某次局部搜索中心周围太空,数组长度不足 5,就直接清空重来,改为对全部 popSize 个个体算距离再挑最近的。 兜底规则写得很直白:numAgents 取 5 与 popSize/3 的较大值,但绝不超过 popSize 本身。也就是说种群小于 15 时只补到 5 个,大于等于 15 时按约 33% 比例抽近邻,避免局部搜索退化成单点抖动。 选人过程是个朴素的最小距离扫描:每轮从还没入选的索引里挑 distances 最小的填进数组,时间复杂度约 O(numAgents * popSize),种群上万时在 MT5 里可能吃掉可观的 tick 时间。实盘前建议把 popSize/3 改成可配置参数,黄金盘面跳空多,邻域过稀会比欧美对更频繁触发这条分支。

MQL5 / C++
class="type">class="kw">double diff = (a [i].c [c] - a [localSearchCenter].c [c]) / (rangeMax [c] - rangeMin [c]);
normalized_dist += diff * diff;
}
normalized_dist = MathSqrt(normalized_dist);
class=class="str">"cmt">// Include agents inside the sphere or the center itself
if (normalized_dist <= sphereRadius || i == localSearchCenter)
{
  class="type">int size = ArraySize(agents_in_sphere);
  ArrayResize(agents_in_sphere, size + class="num">1);
  agents_in_sphere [size] = i;
}
}
class=class="str">"cmt">// If there are too few agents, expand to the nearest neighbors
if (ArraySize(agents_in_sphere) < class="num">5)
{
  ArrayResize(agents_in_sphere, class="num">0);
  class=class="str">"cmt">// Calculate distances for all agents
  class="type">class="kw">double distances [];
  ArrayResize(distances, popSize);
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if (i == localSearchCenter)
    {
      distances [i] = class="num">0.0;
    }
    else
    {
      class="type">class="kw">double dist = class="num">0.0;
      for (class="type">int c = class="num">0; c < coords; c++)
      {
        class="type">class="kw">double diff = (a [i].c [c] - a [localSearchCenter].c [c]) / (rangeMax [c] - rangeMin [c]);
        dist += diff * diff;
      }
      distances [i] = MathSqrt(dist);
    }
  }
  class=class="str">"cmt">// Take the closest class="num">5 agents or class="num">30% of the population
  class="type">int numAgents = MathMin(popSize, MathMax(class="num">5, popSize / class="num">3));
  ArrayResize(agents_in_sphere, numAgents);
  class=class="str">"cmt">// Simple selection of nearby agents
  for (class="type">int k = class="num">0; k < numAgents; k++)
  {
    class="type">class="kw">double minDist = DBL_MAX;
    class="type">int minIdx = -class="num">1;
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      class="type">bool already_selected = false;
      for (class="type">int j = class="num">0; j < k; j++)
      {
        if (agents_in_sphere [j] == i)
        {
          already_selected = true;
          break;
        }
      }
      if (!already_selected && distances [i] < minDist)
      {
        minDist = distances [i];
        minIdx = i;
      }
    }
    agents_in_sphere [k] = minIdx;
  }
}
class=class="str">"cmt">// Execute the Firefly algorithm on the selected agents
class="type">int numLocalAgents = ArraySize(agents_in_sphere);
for (class="type">int i = class="num">0; i < numLocalAgents; i++)
{
  class="type">int idx_i = (class="type">int)agents_in_sphere [i];

◍ 萤火虫吸引与莱维步长的代码落地

这段逻辑把「邻域里更亮的个体吸引当前个体」和「莱维飞行扰动」两块拆开了写,直接对应 EA 优化器的核心迭代。 先说吸引部分:外层遍历本地智能体,跳过自身后取对方索引 idx_j,仅当对方适应度 a[idx_j].f 大于自身时才发生位移。距离用各维归一化差的平方和 r_squared 表示,再套 beta = beta0 * exp(-gamma_es * r_squared) 算吸引力——离得越远吸引力衰减越快,这是典型萤火虫算法设定。 位移方程里 a[idx_i].c[c] += beta*(目标-自身) + alpha*随机.uniform(-0.5,0.5)*区间*0.1,后半段是随机扰动,最后用 SeInDiSp 把坐标夹回 [rangeMin, rangeMax] 并量化到 step。开 MT5 把 gamma_es 从 0.5 调到 2.0,能明显看到收敛半径变窄。 莱维步长走 Mantegna 近似:sigma = (Γ(1+λ)·sin(πλ/2) / (Γ((1+λ)/2)·λ·2^((λ-1)/2)))^(1/λ),再用两个高斯采样 u_val、v_val 算 levyStep[c]=u_val/v_val^(1/λ)。代码把步长硬截断在 ±10.0,避免极端跳跃把参数搜飞。 外汇与贵金属品种参数空间崎岖,这类算法可能陷入局部谷,实盘前请用历史数据回测验证稳定性。

MQL5 / C++
for (class="type">int j = class="num">0; j < numLocalAgents; j++)
{
  if (i == j) class="kw">continue;
  class="type">int idx_j = (class="type">int)agents_in_sphere [j];
  class=class="str">"cmt">// If agent j is better than agent i, move i to j
  if (a [idx_j].f > a [idx_i].f)
  {
    class=class="str">"cmt">// Calculate the distance
    class="type">class="kw">double r_squared = class="num">0.0;
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      class="type">class="kw">double diff = (a [idx_j].c [c] - a [idx_i].c [c]) / (rangeMax [c] - rangeMin [c]);
      r_squared += diff * diff;
    }
    class=class="str">"cmt">// Calculate attractiveness
    class="type">class="kw">double beta = beta0 * MathExp(-gamma_es * r_squared);
    class=class="str">"cmt">// Move agent i to agent j
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      class="type">class="kw">double range = rangeMax [c] - rangeMin [c];
      class=class="str">"cmt">// Firefly motion equation
      a [idx_i].c [c] += beta * (a [idx_j].c [c] - a [idx_i].c [c]) +
                         alpha * (u.RNDfromCI(-class="num">0.5, class="num">0.5)) * range * class="num">0.1;
      class=class="str">"cmt">// Apply borders
      a [idx_i].c [c] = u.SeInDiSp(a [idx_i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
    }
  }
}
}
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————
class=class="str">"cmt">// Generate Levy step using Mantegna algorithm
class="type">void C_AO_ES::GenerateLevyStep()
{
  class=class="str">"cmt">// Calculate sigma for Mantegna algorithm
  class="type">class="kw">double numerator   = Gamma(class="num">1.0 + lambda) * MathSin(M_PI * lambda / class="num">2.0);
  class="type">class="kw">double denominator = Gamma((class="num">1.0 + lambda) / class="num">2.0) * lambda * MathPow(class="num">2.0, (lambda - class="num">1.0) / class="num">2.0);
  class="type">class="kw">double sigma = MathPow(numerator / denominator, class="num">1.0 / lambda);
  for (class="type">int c = class="num">0; c < coords; c++)
  {
    class=class="str">"cmt">// Generate u and v from normal distributions
    class="type">class="kw">double u_val = GenerateGaussian() * sigma;
    class="type">class="kw">double v_val = MathAbs(GenerateGaussian());
    class=class="str">"cmt">// Calculate the Levy step
    if (v_val > class="num">1e-10)
    {
      levyStep [c] = u_val / MathPow(v_val, class="num">1.0 / lambda);
    }
    else
    {
      levyStep [c] = class="num">0.0;
    }
    class=class="str">"cmt">// Limit extreme values
    if (MathAbs(levyStep [c]) > class="num">10.0)
    {
      levyStep [c] = class="num">10.0 * (levyStep [c] > class="num">0 ? class="num">1.0 : -class="num">1.0);
    }
  }
}

「高斯与伽马函数的底层采样实现」

在 MT5 里做统计型指标,最常被卡住的就是随机数与特殊函数的来源。下面这两段直接给了可编译的取法,复制进 EA 或脚本就能跑。 GenerateGaussian 用 Box-Muller 变换出标准正态随机数。它靠两个均匀随机数 u_val、v_val(都从 0~1 区间取),通过 mag = sqrt(-2*ln(u)) 和角度 2πv 映射成独立正态样本;用 static hasSpare 缓存另一个分量,下次调用直接返回,省掉一半计算。注意 u_val 加了 1e-10 避免 ln(0) 报错。 Gamma 函数走 Lanczos 近似,g 取 7.0、系数数组长度 9。z<0.5 时走反射公式递归,否则减 1 后累加系数,最后用 sqrt(2π)·t^(z+0.5)·e^(-t)·x 出值。这套系数在 z>0.5 时相对误差通常在 1e-10 量级,够价格分布拟合用。 外汇与贵金属波动聚集明显,用这类采样做蒙特卡洛压力测试时,样本量低于 1e4 可能给出偏薄的尾部分布,建议先在策略测试器里拉 5e4 次对照直方图再上实盘逻辑。

MQL5 / C++
class=class="str">"cmt">// Generate a Gaussian random number using the Box-Muller transform
class="type">class="kw">double C_AO_ES::GenerateGaussian()
{
  class="kw">static class="type">bool hasSpare = false;
  class="kw">static class="type">class="kw">double spare;
  if (hasSpare)
  {
    hasSpare = false;
    class="kw">return spare;
  }
  hasSpare = true;
  class="type">class="kw">double u_val = u.RNDfromCI(class="num">0.0, class="num">1.0);
  class="type">class="kw">double v_val = u.RNDfromCI(class="num">0.0, class="num">1.0);
  class="type">class="kw">double mag = MathSqrt(-class="num">2.0 * MathLog(u_val + class="num">1e-10));
  spare = mag * MathCos(class="num">2.0 * M_PI * v_val);
  class="kw">return mag * MathSin(class="num">2.0 * M_PI * v_val);
}
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————
class=class="str">"cmt">// Approximation of the gamma function using Lanczos approximation
class="type">class="kw">double C_AO_ES::Gamma(class="type">class="kw">double z)
{
  if (z < class="num">0.5)
  {
    class=class="str">"cmt">// Reflection formula for z < class="num">0.5
    class="kw">return M_PI / (MathSin(M_PI * z) * Gamma(class="num">1.0 - z));
  }
  class=class="str">"cmt">// Lanczos coefficients
  const class="type">class="kw">double g = class="num">7.0;
  class="type">class="kw">double coef [] =
  {
    class="num">0.99999999999980993,
    class="num">676.5203681218851,
    -class="num">1259.1392167224028,
    class="num">771.32342877765313,
    -class="num">176.61502916214059,
    class="num">12.507343278686905,
    -class="num">0.13857109526572012,
    class="num">9.9843695780195716e-6,
    class="num">1.5056327351493116e-7
  };
  z -= class="num">1.0;
  class="type">class="kw">double x = coef [class="num">0];
  for (class="type">int i = class="num">1; i < class="num">9; i++)
  {
    x += coef [i] / (z + i);
  }
  class="type">class="kw">double t = z + g + class="num">0.5;
  class="type">class="kw">double sqrt2pi = MathSqrt(class="num">2.0 * M_PI);
  class="kw">return sqrt2pi * MathPow(t, z + class="num">0.5) * MathExp(-t) * x;
}
class=class="str">"cmt">//————————————————————————————————————————————————————————————————————
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到参数优化卡点提示,你只需判断该换搜索模式还是加代理规模。

常见问题

典型随机游走步长均匀,容易在邻域打转;莱维飞行由大量小步加偶发超长跳组成,更易跳出局部最优进入未探索区,ES 用它做全局阶段。
小布盯盘目前提供优化瓶颈诊断与模式切换建议,不直接替你编译 ES 的 MQL5 代码,但可标记何时该从局部搜切回全局跳。
它做密集局部搜索,代理亮度对应解质量、吸引力随距离指数衰减,在利用已知优解和探索周边间形成平衡。
半径定义局部搜索范围,过大倾向变成又一轮广搜、丧失定点收敛效率,过小则易陷原洼地,需按问题维度试错。
遗传算法交叉变异易早熟,ES 靠周期切换探索/利用并配自适应 λ 与步长递减,在多名局部最优空间更可能触达全局解。