「台球优化算法在 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 测收敛速度,外汇与贵金属参数优化属高风险,回测优解实盘可能漂移。
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 排序,为下一轮选择压力做准备。外汇与贵金属市场的高波动可能让适应度曲面剧烈变形,这套机制在实盘前务必用历史数据回测验证收敛性。
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 样本看收敛曲线。
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 跑一遍,比看排名表直观。 它外部参数少、易抄写,适合做中等复杂度问题的探索开发平衡器,真要上高维得另加修正。外汇与贵金属市场高风险,任何算法信号都只是概率倾向,实盘前先走严格回测。