大逃杀优化器(BRO)·进阶篇
📘

大逃杀优化器(BRO)·进阶篇

第 2/3 篇

「BRO算法的种群对抗与空间收缩机制」

大逃杀优化(BRO)把种群当成竞技场:每个解在指定区间内随机生成坐标,迭代中若碰到更优解就刷新全局最优。解之间两两找最近邻「对战」,胜者伤害计数器清零,败者伤害加1并向全局最优移动;当某个解伤害累计到 maxDamage 阈值,直接淘汰并用随机解替换。这种机制让劣势解持续被挤压出局,种群整体向高适应度区迁移。 搜索空间不是一成不变的。原文用 δ = ⌊总迭代 / log₁₀(总迭代)⌋ 控制收缩节奏,每过 δ 轮就按种群各维标准差重定边界:新下限 = 最优解 − 标准差,新上限 = 最优解 + 标准差。曲线显示 δ 随迭代增大但远慢于线性,前期收缩频、后期收缩疏,兼顾探索与精细化。我在实验里改用两次对数 δ = ⌊epochs / log(log(epochs))⌋,实测聚焦更快。 代码层面对应一个继承 C_AO 的 C_AO_BRO 类。popSize 默认100、maxDamage 默认3,SetParams() 支持运行时改这两个值;Init() 调 StandardInit 并初始化 damages 数组,delta 算错时兜底为1。下面这段是 δ 初始化与类骨架,注意 ao_link 只是内部引用,实盘用不到。 [CODE] // Calculate the initial delta to narrow the search space delta = (int)MathFloor(epochs / MathLog(MathLog(epochs))); //—————————————————————————————————————————————————————————————————————————————— class C_AO_BRO : public C_AO { public: //-------------------------------------------------------------------- ~C_AO_BRO () { } C_AO_BRO () { ao_name = "BRO"; ao_desc = "Battle Royale Optimizer"; ao_link = "[MQL5官方文档] popSize = 100; // population size maxDamage = 3; // maximum damage threshold ArrayResize (params, 2); params [0].name = "popSize"; params [0].val = popSize; params [1].name = "maxDamage"; params [1].val = maxDamage; } void SetParams () { popSize = (int)params [0].val; maxDamage = (int)params [1].val; } 逐行拆解:第1行注释说明下方算搜索空间收缩间隔 δ;第2行用 MathFloor 对 epochs/log(log(epochs)) 向下取整赋给 delta,双对数让收缩更缓。class 行定义公开继承 C_AO 的 BRO 类;析构为空。构造函数里先写算法名与描述,ao_link 保留出处链接;popSize 设100、maxDamage 设3;ArrayResize 把参数数组扩到2,分别绑 popSize 与 maxDamage 初值。SetParams 从 params 读值回写成员变量,使 EA 运行中可动态调参。 外汇与贵金属市场高杠杆、高波动,这类元启发式优化仅用于离线调参或信号研究,实盘前务必在 MT5 策略测试器用历史数据验证,任何参数组合都不保证盈利。

MQL5 / C++
class=class="str">"cmt">// Calculate the initial delta to narrow the search space
  delta = (class="type">int)MathFloor(epochs / MathLog(MathLog(epochs)));
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class C_AO_BRO : class="kw">public C_AO
{
  class="kw">public: class=class="str">"cmt">//--------------------------------------------------------------------
  ~C_AO_BRO() { }
  C_AO_BRO()
  {
    ao_name = "BRO";
    ao_desc = "Battle Royale Optimizer";
    ao_link = "[MQL5官方文档]
    popSize   = class="num">100;      class=class="str">"cmt">// population size
    maxDamage = class="num">3;        class=class="str">"cmt">// maximum damage threshold
    ArrayResize(params, class="num">2);
    params [class="num">0].name = "popSize";   params [class="num">0].val = popSize;
    params [class="num">1].name = "maxDamage"; params [class="num">1].val = maxDamage;
  }
  class="type">void SetParams()
  {
    popSize   = (class="type">int)params [class="num">0].val;
    maxDamage = (class="type">int)params [class="num">1].val;
  }

群体优化算法的初始化与游走逻辑

下面这段类声明和 Init 实现,来自一套基于群体搜索的布林带类指标优化框架(C_AO_BRO)。它的核心不是直接算指标,而是先圈定参数搜索空间,再用「损伤计数 + 空间收缩」去逼近较优解。外汇与贵金属波动率高,这类自动寻参仅降低人工试错成本,不承诺任何胜率。 Init 方法里有个关键量 delta:用 epochs 除以 log10(epochs) 向下取整得到收缩间隔,若结果 ≤0 则强制为 1。例如 epochs=1000 时,delta = floor(1000/3) ≈ 333,意味着每过约 333 代才缩一次搜索范围,避免过早陷入局部最优。 Moving 方法负责种群游走:当 revision 标志为假时,用双重循环给 popSize 个个体、coords 个坐标做随机初始化。这一步就是「盲搜」起点,后续 Revision 才根据 damages 数组淘汰差解。 开 MT5 把下面代码贴进自定义类的 .mqh,改 popSize 和 epochs 跑一遍,能直观看到 delta 随 epochs 变化的非线性节奏。

MQL5 / C++
class="type">bool Init(class="kw">const class="type">class="kw">double &rangeMinP [],class=class="str">"cmt">// minimum search range
              class="kw">const class="type">class="kw">double &rangeMaxP [],  class=class="str">"cmt">// maximum search range
              class="kw">const class="type">class="kw">double &rangeStepP [], class=class="str">"cmt">// search step
              class="kw">const class="type">int     epochsP = class="num">0);  class=class="str">"cmt">// number of epochs
class="type">void Moving();
class="type">void Revision();
class=class="str">"cmt">//----------------------------------------------------------------------------
class="type">int maxDamage;    class=class="str">"cmt">// maximum damage threshold
class="kw">private: class=class="str">"cmt">//-------------------------------------------------------------------
class="type">int    delta;       class=class="str">"cmt">// interval for shrinking the search space
class="type">int    damages []; class=class="str">"cmt">// amount of damage for each solution
class="type">int    epoch;       class=class="str">"cmt">// current epoch
class="type">int    epochs;      class=class="str">"cmt">// maximum number of epochs
class=class="str">"cmt">// Auxiliary methods
class="type">int    FindNearestNeighbor(class="type">int index);
class="type">class="kw">double CalculateDistance(class="type">int idx1, class="type">int idx2);
class="type">void   CalculateStandardDeviations(class="type">class="kw">double &sdValues []);
class="type">void   ShrinkSearchSpace();
};
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">bool C_AO_BRO::Init(class="kw">const class="type">class="kw">double &rangeMinP  [],  class=class="str">"cmt">// minimum search range
                     class="kw">const class="type">class="kw">double &rangeMaxP  [],  class=class="str">"cmt">// maximum search range
                     class="kw">const class="type">class="kw">double &rangeStepP [],  class=class="str">"cmt">// search step
                     class="kw">const class="type">int     epochsP = class="num">0)    class=class="str">"cmt">// number of epochs
{
  if (!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) class="kw">return false;
  class=class="str">"cmt">//----------------------------------------------------------------------------
  class=class="str">"cmt">// Initialize damage counters for each solution
  ArrayResize(damages, popSize);
  ArrayInitialize(damages, class="num">0);
  class=class="str">"cmt">// Set epochs
  epochs = epochsP;
  epoch  = class="num">0;
  class=class="str">"cmt">// Calculate the initial &class="macro">#x27;delta&class="macro">#x27; to narrow the search space
  delta = (class="type">int)MathFloor(epochs / MathLog10(epochs));
  if (delta <= class="num">0) delta = class="num">1;
  class="kw">return true;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
/——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_BRO::Moving()
{
  if (!revision)
  {
    class=class="str">"cmt">// Initialize the population with random decisions
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      for (class="type">int c = class="num">0; c < coords; c++)
      {

◍ 败者向最优解靠拢的迭代机制

Revision() 是群体优化里每轮必跑的更新函数,先 epoch++ 再刷新全局最优:遍历 popSize 个解,只要某个体适应度 a[i].f 大于已知最优 fB,就把它的坐标数组拷进 cB,fB 同步抬高。这一步保证历史最好解不丢失,是后续所有移动的锚点。 紧接着做近邻对抗:FindNearestNeighbor(i) 找出空间上最近的对手,若 a[i].f >= a[neighbor].f,i 胜出、自身 damages 清零而对手 damages++,败者按 r=u.RNDfromCI(0,1) 的比例朝 cB 插值移动,再经 SeInDiSp 钳回参数边界与步长。反向情况对称处理,i 败则自己朝最优解挪。 当某个解的 damages 累计到 maxDamage,直接整组坐标重掷为 [rangeMin,rangeMax] 内的随机点并重置计数器,避免种群早收敛在局部洼地。若设了 epochs>0 且 epoch % delta == 0,则调用 ShrinkSearchSpace() 周期性收窄搜索域,搜索粒度随时间收紧,后期更倾向精细 exploitation 而非盲目 exploration。 在 MT5 里把 maxDamage 设小(如 3~5)、delta 设 20~50,能明显加快收敛但过拟合风险升高;外汇与贵金属波动剧烈,这类参数寻优结果仅代表历史样本倾向,实盘仍需严控仓位。

MQL5 / C++
class="type">void C_AO_BRO::Revision()
{
  epoch++;
  class=class="str">"cmt">// Update the global best solution
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    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">// Compare each solution with its nearest neighbor and update damage counters
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    class="type">int neighbor = FindNearestNeighbor(i);
    if (neighbor != -class="num">1)
    {
      if (a [i].f >= a [neighbor].f)
      {
        class=class="str">"cmt">// Solution i wins
        damages [i] = class="num">0;
        damages [neighbor]++;
        class=class="str">"cmt">// The loser(neighbor) moves toward the best solution
        for (class="type">int c = class="num">0; c < coords; c++)
        {
          class="type">class="kw">double r = u.RNDfromCI(class="num">0, class="num">1);
          a [neighbor].c [c] = a [neighbor].c [c] + r * (cB [c] - a [neighbor].c [c]);
          a [neighbor].c [c] = u.SeInDiSp(a [neighbor].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
        }
      }
      else
      {
        class=class="str">"cmt">// Solution i loses
        damages [i]++;
        damages [neighbor] = class="num">0;
        class=class="str">"cmt">// The loser(i) moves to the best solution
        for (class="type">int c = class="num">0; c < coords; c++)
        {
          class="type">class="kw">double r = u.RNDfromCI(class="num">0, class="num">1);
          a [i].c [c] = a [i].c [c] + r * (cB [c] - a [i].c [c]);
          a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
        }
      }
    }
  }
  class=class="str">"cmt">// Check if any solution has reached maximum damage and replace it
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if (damages [i] >= maxDamage)
    {
      class=class="str">"cmt">// Reset the damage counter
      damages [i] = class="num">0;
      class=class="str">"cmt">// Generate a new random solution
      for (class="type">int c = class="num">0; c < coords; c++)
      {
        class="type">class="kw">double coordinate = u.RNDfromCI(rangeMin [c], rangeMax [c]);
        a [i].c [c] = u.SeInDiSp(coordinate, rangeMin [c], rangeMax [c], rangeStep [c]);
      }
    }
  }
  class=class="str">"cmt">// Periodic narrowing of the search space
  if (epochs > class="num">0 && epoch % delta == class="num">0)
  {
    ShrinkSearchSpace();
    class=class="str">"cmt">// Update delta

「邻域检索与搜索空间收缩的实现细节」

黑猩猩优化器里「找最近邻」不是靠暴力比对全部个体,而是用欧氏距离在多维坐标上筛。FindNearestNeighbor 从 0 扫到 popSize,跳过自身后取最小 distance 对应的下标,返回 nearestIndex;这套逻辑在群体规模 50 时大约要算 49×coords 次差值,MT5 单 tick 内跑完无压力。 CalculateDistance 只做一件事:把两个个体在第 c 维的坐标差平方累加,最后开根号。注意它没除以维度做归一,所以当 coords 很大时距离数值会随维度线性膨胀,调参时得留意边界爆 double。 ShrinkSearchSpace 是每代收束的核心。先算各维标准差 sdValues,再以当前最优 cB[c] 为中心、正负一个标准差重设 rangeMin/rangeMax,越界就夹回初始约束。外汇与贵金属波动率高,这种收缩若迭代过快可能过早陷在局部优解,实盘前建议在策略测试器里把 popSize 拉到 80 以上观察收敛曲线。

MQL5 / C++
  delta = delta + (class="type">int)MathRound(delta / class="num">2);
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">int C_AO_BRO::FindNearestNeighbor(class="type">int index)
{
  class="type">class="kw">double minDistance = DBL_MAX;
  class="type">int nearestIndex = -class="num">1;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if (i == index) class="kw">continue;
    class="type">class="kw">double distance = CalculateDistance(index, i);
    if (distance < minDistance)
    {
      minDistance = distance;
      nearestIndex = i;
    }
  }
  class="kw">return nearestIndex;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">class="kw">double C_AO_BRO::CalculateDistance(class="type">int idx1, class="type">int idx2)
{
  class="type">class="kw">double distanceSum = class="num">0.0;
  for (class="type">int c = class="num">0; c < coords; c++)
  {
    class="type">class="kw">double diff = a [idx1].c [c] - a [idx2].c [c];
    distanceSum += diff * diff;
  }
  class="kw">return MathSqrt(distanceSum);
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">class="kw">double C_AO_BRO::CalculateStandardDeviations(class="type">class="kw">double &sdValues [])
{
  ArrayResize(sdValues, coords);
  for (class="type">int c = class="num">0; c < coords; c++)
  {
    class="type">class="kw">double sum = class="num">0.0;
    class="type">class="kw">double mean = class="num">0.0;
    class=class="str">"cmt">// Calculate the average
    for (class="type">int i = class="num">0; i < popSize; i++) mean += a [i].c [c];
    mean /= popSize;
    class=class="str">"cmt">// Calculate the sum of squared deviations
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      class="type">class="kw">double diff = a [i].c [c] - mean;
      sum += diff * diff;
    }
    sdValues [c] = MathSqrt(sum / popSize);
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_BRO::ShrinkSearchSpace()
{
  class="type">class="kw">double sdValues [];
  CalculateStandardDeviations(sdValues);
  for (class="type">int c = class="num">0; c < coords; c++)
  {
    class=class="str">"cmt">// The new boundaries are centered around the best solution with a standard deviation width
    class="type">class="kw">double newMin = cB [c] - sdValues [c];
    class="type">class="kw">double newMax = cB [c] + sdValues [c];
    class=class="str">"cmt">// Make sure the new bounds are within the original constraints
    if (newMin < rangeMin [c]) newMin = rangeMin [c];
    if (newMax > rangeMax [c]) newMax = rangeMax [c];
    class=class="str">"cmt">// Update the boundaries
    rangeMin [c] = newMin;
    rangeMax [c] = newMax;
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————

常见问题

每轮迭代中劣势个体被淘汰,幸存者向当前最优区域收缩,形成对抗式筛选,直接在种群层面压缩搜索范围。
步长过大会跳过优质邻域导致收敛慢,建议先用小规模回测标定步长区间再上实盘参数。
可以,小布内置了群体优化算法的迭代诊断,把品种和周期交给它就能自动给出败者靠拢与空间收缩的可视化结果。
有可能,靠邻域检索扰动打破僵局,实盘中建议保留5%~10%的随机逃逸比例降低卡死概率。
当连续N代最优适应度变化低于阈值(如1e-4)即可停,继续缩只会浪费计算且易过拟合。