黑洞算法(BHA)·进阶篇
🕳️

黑洞算法(BHA)·进阶篇

(2/3)· 为什么多数交易者卡在参数调优,而 BHA 靠零参数吞噬局部最优?

案例拆解 第 2/3 篇
把优化器当黑箱乱调迭代次数,结果陷入局部最优还以为是数据问题。BHA 连一个参数都不用调,却常被误用成普通群体算法。看懂事件视界怎么吞星辰,才不至于白白浪费它的结构优势。

「黑洞算法里视界半径怎么算」

黑洞算法(BHA)把最优解当成黑洞,其余候选解当作星辰,靠吸引力做群体寻优。它最省心的地方是除了群体大小外没有任何要调的参数,在常见优化算法里很少见。 原版里事件视界半径定义为 R = fitBH / Σfiti,也就是最佳适应度占全体适应度之和的比例。这个式子有个坑:群体一大,分母跟着暴涨,R 就会变得极小,而且数值直接绑死在适应度函数的绝对值上,换个问题尺度就失真。 跑 10000 次函数在 Hilly 测试上,群体 5 / 25 / 500 时的结果分别是 0.6834 / 0.4728 / 0.2783,总评分 3.41461(37.94%),低于平均水平。观察中发现它容易因群体多样性缺失陷进局部最优,而且每颗星辰算到黑洞的欧几里得距离是资源大户。 改进版 BHAm 把半径在 [0,1] 归一化,用群体最小和最大适应度之差做基准,不再依赖绝对值和群体规模。吸引机制也从「距离小于 R 就整颗随机替换」改成「对每个坐标按概率吸引」,群体变化更平滑,0.01 的替换率大幅降低了剧烈扰动。 别把 R 的公式当摆设 原版 R 在 500 颗星辰时已经缩到约 0.2783 量级的倒数附近,实际吸引几乎不发生。开 MT5 把 popSize 从 50 调到 500,看日志里 R 的输出,能直接验证分母拖垮半径的现象。

MQL5 / C++
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class C_AO_BHA : class="kw">public C_AO
{
  class="kw">public: class=class="str">"cmt">//--------------------------------------------------------------------
  ~C_AO_BHA() { }
  C_AO_BHA()
  {
    ao_name = "BHA";
    ao_desc = "Black Hole Algorithm";
    ao_link = "[MQL5官方文档]
    popSize = class="num">50;  class=class="str">"cmt">// Population size
    ArrayResize(params, class="num">1);
    class=class="str">"cmt">// Initialize parameters
    params [class="num">0].name = "popSize"; params [class="num">0].val = popSize;
  }
  class="type">void SetParams() class=class="str">"cmt">// Method for setting parameters
  {
    popSize = (class="type">int)params [class="num">0].val;
  }
  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

◍ 黑洞算法里星星怎么被吸进去

这段 C_AO_BHA 类的实现把黑洞算法(Black Hole Algorithm)的初始化和移动逻辑落到了 MT5 的 EA 框架里。Init 只做一件事:调 StandardInit 把参数区间灌进去,再把 blackHoleIndex 置 0,代表当前最优解下标从 0 开始。 Moving 方法第一次跑时 revision 为 false,会按 rangeMin/rangeMax/rangeStep 给每个个体(popSize 个)的每个维度(coords 个)做随机定位,并用 SeInDiSp 把连续值离散化到步长网格上,之后 revision 置 true 直接 return,不做引力移动。 后续每次调用 Moving,先累加所有个体适应度 sumFitness,再用 R = fitBH / Σfiti 算事件视界半径。对每个非黑洞个体算到黑洞的欧氏距离,一旦 distance < eventHorizonRadius,这颗星就被吞掉、重生成随机星。外汇与贵金属市场高杠杆、滑点大,这类优化器用于参数寻优时回测过拟合概率偏高,实盘前务必多周期验证。 代码里 u.RNDfromCI 和 u.SeInDiSp 是自定义工具类方法,若你本地没有这两个函数,编译会直接报未定义;开 MT5 把这段贴进类实现,先确认 StandardInit 与 u 对象已存在再跑。

MQL5 / C++
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=class="str">"cmt">// Moving method
class="type">void Revision();     class=class="str">"cmt">// Revision method
class="kw">private: class=class="str">"cmt">//-------------------------------------------------------------------
class="type">int blackHoleIndex;    class=class="str">"cmt">// Black hole index(best solution)
};
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">bool C_AO_BHA::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;
  blackHoleIndex = class="num">0; class=class="str">"cmt">// Initialize black hole index
  class="kw">return true;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_BHA::Moving()
{
  class=class="str">"cmt">// Initial random positioning on first run
  if (!revision)
  {
    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">// Generate a random position within the allowed range
        a [i].c [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);
        class=class="str">"cmt">// Convert to discrete values according to step
        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">// Calculate the sum of fitness values for the radius of the event horizon
  class="type">class="kw">double sumFitness = class="num">0.0;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    sumFitness += a [i].f;
  }
  class=class="str">"cmt">// Calculate the radius of the event horizon
  class=class="str">"cmt">// R = fitBH / Σfiti
  class="type">class="kw">double eventHorizonRadius = a [blackHoleIndex].f / sumFitness;
  class=class="str">"cmt">// Update star positions
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    class=class="str">"cmt">// Skip the black hole
    if (i == blackHoleIndex) class="kw">continue;
    class=class="str">"cmt">// Calculate the distance to the black hole
    class="type">class="kw">double distance = class="num">0.0;
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      class="type">class="kw">double diff = a [blackHoleIndex].c [c] - a [i].c [c];
      distance += diff * diff;
    }
    distance = sqrt(distance);
    class=class="str">"cmt">// Check for event horizon crossing
    if (distance < eventHorizonRadius)
    {
      class=class="str">"cmt">// Star is consumed - create a new random star

黑洞算法里星体怎么挪位与越界修正

这段 C_AO_BHA 类的 Moving 逻辑,把黑洞算法(Black Hole Algorithm)在 MT5 里的迭代过程拆得很直白。首次运行走 revision 分支,对每个种群个体 i 在 coords 维上调用 RNDfromCI 取 [rangeMin, rangeMax] 内均匀随机数,再用 SeInDiSp 按 rangeStep 吸附到离散网格——相当于把候选参数撒进合法搜索空间。 非首次运行时,引擎先算 aveFit(种群适应度均值)、maxFit(当前黑洞 fB)与 minFit,事件视界半径用 (aveFit - minFit) / (maxFit - minFit) 归一化。若某维随机数小于 eventHorizonRadius * 0.01,该星体坐标被重新随机化,概率上限约 1%,这是防止早熟收敛的逃逸机制。 普通更新走 Xi(t+1) = Xi(t) + rand×(XBH - Xi(t)),rand 来自 RNDfromCI(0.0, 1.0),新位置同样过 SeInDiSp 夹回边界。Revision 函数则线性扫 popSize 个个体,遇 a[i].f > fB 就刷新黑洞索引并 ArrayCopy 最优坐标到 cB——注意它用大于号,说明适应度越大越优,跟你写 EA 时若用回撤最小化要反转比较方向。 开 MT5 把这段塞进自己的 CAO_BHA 类,先单步看 eventHorizonRadius 在种群分化小时是否趋近 0,那时星体几乎只向黑洞收缩,容易卡在局部解。

MQL5 / C++
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]);
}
}
else
{
  class=class="str">"cmt">// Update the star position class="kw">using the equation:
  class=class="str">"cmt">// Xi(t+class="num">1) = Xi(t) + rand × (XBH - Xi(t))
  for (class="type">int c = class="num">0; c < coords; c++)
  {
    class="type">class="kw">double rnd = u.RNDfromCI(class="num">0.0, class="num">1.0);
    class="type">class="kw">double newPosition = a [i].c [c] + rnd * (a [blackHoleIndex].c [c] - a [i].c [c]);
    class=class="str">"cmt">// Check and correct the boundaries
    newPosition = u.SeInDiSp(newPosition, rangeMin [c], rangeMax [c], rangeStep [c]);
    a [i].c [c] = newPosition;
  }
}
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_BHA::Revision()
{
  class=class="str">"cmt">// Find the best solution(black hole)
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if (a [i].f > fB)
    {
      fB = a [i].f;
      blackHoleIndex = i;
      ArrayCopy(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_BHAm::Moving()
{
  class=class="str">"cmt">// Initial random positioning on first run
  if (!revision)
  {
    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">// Generate a random position within the allowed range
        a [i].c [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);
        class=class="str">"cmt">// Convert to discrete values according to step
        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=class="str">"cmt">// Calculate the average fitness values for the event horizon radius
  class="type">class="kw">double aveFit = class="num">0.0;
  class="type">class="kw">double maxFit = fB;
  class="type">class="kw">double minFit = a [class="num">0].f;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    aveFit += a [i].f;
    if (a [i].f < minFit) minFit = a [i].f;
  }
  aveFit /= popSize;
  class=class="str">"cmt">// Calculate the radius of the event horizon
  class="type">class="kw">double eventHorizonRadius = (aveFit - minFit) / (maxFit - minFit);
  class=class="str">"cmt">// Update star positions
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    class=class="str">"cmt">// Skip the black hole
    if (i == blackHoleIndex) class="kw">continue;
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      if (u.RNDprobab() < eventHorizonRadius * class="num">0.01)
      {
        a [i].c [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);

「黑洞引力下的参数漂移写法」

这段逻辑处理的是群体优化里第 i 个个体的第 c 维参数更新:若触发条件不成立,就走均匀映射把越界值拉回 [rangeMin, rangeMax] 并按 rangeStep 对齐;否则引入 0.0~1.0 的均匀分布随机数 rnd,让当前位置朝 blackHoleIndex 所指向的“黑洞”位置做线性插值。 newPosition = a[i].c[c] + rnd * (a[blackHoleIndex].c[c] - a[i].c[c]) 意味着随机步长最大不超过两者差值,rnd 取 1.0 时直接跳到黑洞位,取 0.0 时原地不动。 随后仍用 SeInDiSp 做边界校正,保证每一维都落在离散可行域内。开 MT5 把 blackHoleIndex 换成一个历史最优索引,就能直接验证这种漂移对 EA 参数收敛速度的影响;外汇与贵金属市场波动剧烈,此类优化仅降低过拟合概率,实盘仍属高风险。

MQL5 / C++
      a[i].c[c] = u.SeInDiSp(a[i].c[c], rangeMin[c], rangeMax[c], rangeStep[c]);
      }
      else
      {
        class="type">class="kw">double rnd = u.RNDfromCI(class="num">0.0, class="num">1.0);
        class="type">class="kw">double newPosition = a[i].c[c] + rnd * (a[blackHoleIndex].c[c] - a[i].c[c]);
        class=class="str">"cmt">// Check and correct the boundaries
        newPosition = u.SeInDiSp(newPosition, rangeMin[c], rangeMax[c], rangeStep[c]);
        a[i].c[c] = newPosition;
      }
    }
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 BHA 搜索轨迹与视界吞噬节点,把重复劳动交给小布,你专注决策。

常见问题

R = fitBH / Σfiti,即黑洞适应度除以所有星辰适应度之和,穿过该半径的星辰被吞噬并由随机新星辰替代,维持群体多样性。
群体大小 N 是初始化时的星辰数量,属于问题规模设定而非算法调谐参数,除 N 外 BHA 确实无需其他超参。
高杠杆外汇贵金属本身高风险,BHA 降低局部陷阱概率,但历史窗口拟合仍可能失真,回测结论仅代表历史可能。
可以,小布盯盘对应品种页内置了 BHA 搜索策略演示,绿蓝星辰趋近、红色传送新星的节点都能实时看到。
实际实现会对适应度做平移或归一,避免分母趋零;原文伪代码未展开,但工程上必须处理该边界。