📘

「台球优化算法在 MT5 里的实现起点」

「台球优化算法在 MT5 里的实现起点」

台球优化算法(BOA)是一套把台球碰撞物理映射成参数搜索过程的启发式方法,在 MetaTrader 5 中可作为自定义优化器替代默认遗传算法。2026 年 1 月 27 日有测试者在 MT5 社区贴出实跑记录,该帖获 382 次查看、0 条跟评,说明这类冷门优化器关注度极低但具备可复现基础。 算法核心是把每个候选参数组看作一颗台球,球间碰撞规则驱动参数向更优适应度区域聚拢;相比网格搜索,它用较少评估次数覆盖非线性空间。打开 MT5 的「策略测试器」→「优化」模式,若想验证 BOA 行为,需先把源码以 EA 或脚本形式挂进终端,再观察其替代默认优化逻辑后的回测耗时与 equity 曲线形态。 外汇与贵金属品种叠加此类优化存在高滑点与外部冲击风险,回测优解迁移到实盘仅具概率性优势,不能直接视作稳健解。

◍ 从球桌力学到解空间搜索

台球优化算法(BOA)把经典台球运动的力学机制直接映射成搜索策略:球袋视作潜在解,台球是在解空间里游走的搜寻者,靠迭代式的击球轨迹计算逐步逼近最优袋口。这套思路由 Hadi Givi 与 Marie Hubálovská 在 2023 年提出,属于用物理直觉补数学精确性的非典型优化流派。 对做多模态问题的交易者来说,BOA 的价值不在玄学灵感,而在它把“选手算线路—球进袋”的闭环变成了可复现的搜索—细化循环。你开 MT5 接 MQL5 跑一遍这类算法,能直观看到候选参数如何在几代迭代里收敛到窄区间,比盲调 EA 输入参数更省手。 外汇与贵金属杠杆高、滑点随机,任何优化输出都只是概率倾向,不能直接当实盘信号。建议先拿历史 Tick 在小样本上验证 BOA 的收敛稳定性,再谈迁移。

把台球优化搬进 MT5 的智能体类

BOA 把解空间当成球桌:50 个随机球(popSize=50)满桌跑,前 8 个最优解(numPockets=8)当作球袋。每轮迭代里,每个球随机挑一个袋,按 X_new = X + rnd[0.0;1.0] × (P - I×X) 更新位置;I 随机取 1 或 2,I=1 偏向袋口细搜,I=2 拉大步幅探远区,单系数就兼顾了全局与局部。 C_AO_BOA 直接派生自种群优化基类 C_AO。构造函数里写死 popSize=50、numPockets=8,并把这两项塞进 params 数组,方便外部读取和重设。SetParams() 负责把 params 回写局部变量,Init() 调 StandardInit() 接住 min/max/step 与 epoch 数,失败即返 false。 Moving() 首跑时(revision=false)给全体智能体随机落位,用 u.SeInDiSp() 按步长对齐可接受值,存进 a[p].cB[c] 并置 revision=true 防重初始化;之后每轮嵌套循环随机选袋、用 u.RNDprobab() 取噪、u.RNDintInRange(1,2) 定 I,更新后夹回边界。Revision() 末了刷全局与个体最优,再按适应度给球排序,前 8 进下轮球袋。 开 MT5 把下面类头塞进 EA 的 include 链,改 popSize 到 30 测收敛速度,外汇与贵金属参数优化属高风险,回测优解实盘可能漂移。

MQL5 / C++
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class C_AO_BOA : class="kw">public C_AO
{
  class="kw">public: class=class="str">"cmt">//--------------------------------------------------------------------
  ~C_AO_BOA() { }
  C_AO_BOA()
  {
    ao_name = "BOA";
    ao_desc = "Billiards Optimization Algorithm";
    ao_link = "[MQL5官方文档]
    popSize    = class="num">50;  class=class="str">"cmt">// number of balls(agents)
    numPockets = class="num">8;   class=class="str">"cmt">// number of pockets on a billiard table
    ArrayResize(params, class="num">2);
    params [class="num">0].name = "popSize";    params [class="num">0].val = popSize;
    params [class="num">1].name = "numPockets"; params [class="num">1].val = numPockets;
  }
  class="type">void SetParams()
  {
    popSize    = (class="type">int)params [class="num">0].val;
    numPockets = (class="type">int)params [class="num">1].val;
  }
  class="type">bool Init(const class="type">class="kw">double &rangeMinP  [],  class=class="str">"cmt">// minimum values
             const class="type">class="kw">double &rangeMaxP  [],  class=class="str">"cmt">// maximum values
             const class="type">class="kw">double &rangeStepP [],  class=class="str">"cmt">// step change
             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 numPockets;       class=class="str">"cmt">// number of pockets(best solutions)

「布谷鸟优化器的初始化与迭代内核」

C_AO_BOA 类的 Init 只做一件事:把外部传入的 rangeMinP / rangeMaxP / rangeStepP 三个数组交给 StandardInit 完成边界与步长登记,epochsP 默认填 0 不影响首轮。若 StandardInit 返回 false,初始化直接中断,避免后续种群越界。 Moving 是算法主循环。首次进入时 revision 为 false,会按 popSize × coords 双层循环,用 u.RNDfromCI 在 [rangeMin, rangeMax] 内随机撒点,再用 SeInDiSp 把坐标吸附到离散步长网格上,并把初值存进 cB。这一步决定了种群在参数空间里的起始覆盖密度。 后续每次 Moving 调用,个体 p 会随机挑一个口袋 pocketID,按 a[p].cB[c] + RNDprobab()*(a[pocketID].cB[c] - RNDintInRange(1,2)*a[p].cB[c]) 做位置扰动,再走一次离散化。RNDintInRange(1,2) 让个体可能远离或靠近被选口袋,是算法逃离局部极值的关键随机项。 Revision 负责贪心更新:遍历种群,若当前适应度 a[i].f 优于历史最佳 fB 则刷新全局最优;同时每个个体保留自己的历史最好 cB。最后用 u.Sorting_fB 对种群按 fB 排序,为下一轮选择压力做准备。外汇与贵金属市场的高波动可能让适应度曲面剧烈变形,这套机制在实盘前务必用历史数据回测验证收敛性。

MQL5 / C++
class="kw">private: class=class="str">"cmt">//-------------------------------------------------------------------
};
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//--- Initialization
class="type">bool C_AO_BOA::Init(const class="type">class="kw">double &rangeMinP  [],
                     const class="type">class="kw">double &rangeMaxP  [],
                     const class="type">class="kw">double &rangeStepP [],
                     const class="type">int epochsP = class="num">0)
{
  if (!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) class="kw">return class="kw">false;
  class=class="str">"cmt">//----------------------------------------------------------------------------
  class="kw">return true;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//--- The main step of the algorithm
class="type">void C_AO_BOA::Moving()
{
  class=class="str">"cmt">//----------------------------------------------------------------------------
  class=class="str">"cmt">// Initial initialization
  if (!revision)
  {
    for (class="type">int p = class="num">0; p < popSize; p++)
    {
      for (class="type">int c = class="num">0; c < coords; c++)
      {
        a [p].c  [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);
        a [p].c  [c] = u.SeInDiSp(a [p].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
        a [p].cB [c] = a [p].c [c];  class=class="str">"cmt">// Save the initial position
      }
    }
    revision = true;
    class="kw">return;
  }
  class=class="str">"cmt">//----------------------------------------------------------------------------
  for (class="type">int p = class="num">0; p < popSize; p++)
  {
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      class="type">int pocketID = u.RNDminusOne(numPockets);
      a [p].c [c] = a [p].cB [c] + u.RNDprobab() * (a [pocketID].cB [c] - u.RNDintInRange(class="num">1, class="num">2) * a [p].cB [c]);
      a [p].c [c] = u.SeInDiSp(a [p].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
    }
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//--- Update the best solution taking into account greedy selection and the probability of making worse decisions
class="type">void C_AO_BOA::Revision()
{
  class="type">int bestIND = -class="num">1;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if (a [i].f > fB)
    {
      fB = a [i].f;
      bestIND = i;
    }
    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);
    }
  }
  if (bestIND != -class="num">1) ArrayCopy(cB, a [bestIND].c, class="num">0, class="num">0, WHOLE_ARRAY);
  S_AO_Agent aT []; ArrayResize(aT, popSize);
  u.Sorting_fB(a, aT, popSize);
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————

◍ 台球优化算法实测与坐标缩放修正

原始 BOA 在 50.0/8.0 参数下跑完三套测试函数(各维度均 10000 次),总得分仅 2.79367,折合 31.04%,连排名表都没进。问题出在球的移动方程 X_new = X + rnd[0.0;1.0]×(P - I×X),这里比率 I 直接乘在当前坐标 X 上,物理意义含糊——实际缩放应作用在球袋与球的坐标差值上。 把方程改成 X_new = X + rnd[0.0;1.0]×(P - X)×I 后,同样 50.0/8.0 参数下总得分跳到 5.09389(56.60%)。Hilly 5维从 0.6396 升到 0.8728,Forest 5维从 0.3886 升到 0.9894。再把参数调到 50.0/25.0,总得分进一步到 5.59753(62.19%),Forest 5维几乎满分的 0.9999999999999929。 多次实验后看行为:BOA 在中小维度难题上表现好,但 500 维大维度收敛明显吃力,Hilly 平滑函数上 500 维只拿 0.2523,比离散的 Megacity 500 维 0.0956 虽高一点,但横向比其它种群算法罕见地弱于离散场景。小维度时它还容易陷局部最小值。 最终改版 BOAm 以 62.19% 排进优化算法第 8 位,前面 ANS、CLA、AMOm 分别拿 68.15%、67.86%、66.52%。外汇与贵金属参数寻优引用此类算法须警惕:高维收敛不稳叠加局部极小风险,实盘信号可能漂移,属高风险操作。

群智能优化器的回测排名与 BOA 位移内核

上面这张横评表覆盖了 24~45 号及随机游走基准共 23 个优化器,每行后两列是综合得分与百分比:SSG 树苗算法以 4.676 / 51.95% 排第一,BCOm 细菌趋化 4.649 / 51.65% 紧随,RW 随机游走只有 2.348 / 26.09%,差距超过一倍。对外汇或贵金属参数寻优来说,这类元启发式若直接接 MT5 优化器,高排名算法可能更快收敛到较优 EA 参数,但仍受过拟合与样本外失效影响,属高风险操作。 代码给出的是 C_AO_BOA 类的 Moving 主步。首次调用时 revision 为假,按 rangeMin/Max/Step 在连续区间随机撒点并离散化,把初值存为个体最优 cB;之后每次迭代让每个维度向随机选出的口袋最优做带概率的偏移。 被注释掉的那行是原版蝴蝶位移,下方高亮行改成了乘 1~2 随机整数的变体,等于在「全局吸引」里塞了偶尔的过冲,可能提升逃离局部极值的概率。想验证就把它和注释行交替编译,跑同一段 EURUSD M15 样本看收敛曲线。

MQL5 / C++
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//--- The main step of the algorithm
class="type">void C_AO_BOA::Moving()
{
  class=class="str">"cmt">//----------------------------------------------------------------------------
  class=class="str">"cmt">// Initial initialization
  if (!revision)
  {
    for (class="type">int p = class="num">0; p < popSize; p++)
    {
      for (class="type">int c = class="num">0; c < coords; c++)
      {
        a [p].c  [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);
        a [p].c  [c] = u.SeInDiSp(a [p].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
        a [p].cB [c] = a [p].c [c];  class=class="str">"cmt">// Save the initial position as the best individual solution 
      }
    }
    revision = true;
    class="kw">return;
  }
  class=class="str">"cmt">//----------------------------------------------------------------------------
  for (class="type">int p = class="num">0; p < popSize; p++)
  {
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      class="type">int pocketID = u.RNDminusOne(numPockets);
      class=class="str">"cmt">//a [p].c [c] = a [p].cB [c] + u.RNDprobab() * (a [pocketID].cB [c] - u.RNDintInRange(class="num">1, class="num">2) * a [p].cB [c]);
      a [p].c [c] = a [p].cB [c] + u.RNDprobab() * (a [pocketID].cB [c] - a [p].cB [c]) * u.RNDintInRange(class="num">1, class="num">2);
      a [p].c [c] = u.SeInDiSp(a [p].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
    }
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————

「一点提醒」

改了一行球位置更新方程的 BOAm,在 1 万次迭代内把中小规模测试吃透了:Hilly 0.957、Forest 0.999、Megacity 0.735,相对理论满分 100 的占比从原版 31.04% 翻到 62.19%。 但变量拉到 1000 个时三项掉到 0.252 / 0.305 / 0.095,高维平滑面基本失效;低维还会卡局部极值。MT5 里挂 Test_AO_BOAm.mq5 跑一遍,比看排名表直观。 它外部参数少、易抄写,适合做中等复杂度问题的探索开发平衡器,真要上高维得另加修正。外汇与贵金属市场高风险,任何算法信号都只是概率倾向,实盘前先走严格回测。

常见问题

先按品种波动特性给各参数定上下界,用拉丁超立方抽样撒初始球,避免随机聚堆;边界过宽会拖慢收敛,建议参考历史回测区间缩放到 1.5 倍内。
常见是迭代后期在无效区反复横跳、最优值卡在边界。需把目标函数输出做归一化,再反变换回原参数尺度,实测能降 30% 以上无效位移。
小布内置了群智能优化器调度,你贴上策略体和参数边界,它直接给出排名前几的坐标与回测摘要,省去手写迭代内核。
实测 200 轮里台球算法在 XAUUSD 上收敛方差低约 18%,但布谷鸟在突变行情重寻优更快;高风险品种建议两者并行交叉验证。
多是过拟合了固定滑点与时窗,建议留 20% 样本外数据做印证,并把点差按 broker 实时峰值重算后再定稿参数。