黑洞算法(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 的输出,能直接验证分母拖垮半径的现象。
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 对象已存在再跑。
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,那时星体几乎只向黑洞收缩,容易卡在局部解。
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 参数收敛速度的影响;外汇与贵金属市场波动剧烈,此类优化仅降低过拟合概率,实盘仍属高风险。
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">//——————————————————————————————————————————————————————————————————————————————