头脑风暴优化算法(第二部分): 多模态(基础篇)
◍ 多模态思路下的头脑风暴优化
头脑风暴优化(Brain Storm Optimization, BSO)在 MetaTrader 5 上的实现,第二篇重点转向多模态问题:传统 BSO 容易陷在单一全局峰,遇到多个等价优解的市场形态会过早收敛。 原文示例发布于 2024 年 10 月 31 日 09:06,截至统计有 565 次查看、4 条评论,说明这类算法改造在 MT5 圈子里有真实需求,不是纯论文玩具。 本小节只铺了三层骨架:1)问题背景,2)算法实现,3)测试结果;其中多模态改造的核心是在聚类后引入跨簇随机扰动,避免所有个体被拉向同一个质心。 想验证可行性,直接开 MT5 用 EA 向导挂一段聚类+扰动逻辑,先看回测曲线是否出现多个稳定 plateau,再谈参数调优。外汇与贵金属品种波动跳跃大,多模态优解不等于实盘稳健,请按高风险品种对待。
BSO 优化与聚类的配合逻辑
BSO(头脑风暴优化)把群体活动拆成观点生成与评估两个阶段,本质是在搜索空间里靠多智能体不断产出并筛选解。它和 K-Means、K-Means++ 这类聚类绑在一起用:聚类先把数据元素按相似度分组,让算法更快定位可能藏最优解的区域。 变异操作是另一块关键,它让 BSO 能跳过局部障碍、绕开搜索空间里的死胡同,从而走更短路径逼近最优。原文提到第一部分已拆解过这套逻辑结构,第二部分才进实盘代码与多模态测试。 对做 EA 参数寻优的交易者来说,这套组合的意义在于:单纯网格遍历容易卡在平坦区,而聚类+变异可能把回测耗时压下来。具体能省多少要看品种波动结构,外汇与贵金属属高风险品种,任何优化结果都只是历史样本上的概率倾向,不能直接当实盘保证。
「BSO智能体与种群类的骨架拆解」
BSO(头脑风暴优化)在MT5里落地,第一步是把每个候选解封装成结构体。下面这段S_BSO_Agent就是最小单元:c数组存坐标,f存适应度,label标聚类归属。 Init方法只接收一个coords参数,用ArrayResize把坐标数组拉到对应维度;f初始化成-DBL_MAX,也就是双精度负无穷,保证任何真实评价值都能覆盖它;label设-1表示还没进任何簇。创建新智能体时这三步必跑。 继承C_AO的C_AO_BSO类管整个种群。公开字段里popSize默认25、parentPopSize默认50,说明子代规模小于父代留存规模;clustersNumb、p_Replace、p_One、p_One_center、p_Two_center、k_Mutation、distribCoeff这组概率与比率,就是BSO区别于普通EA优化的核心旋钮。 Init里先调StandardInit做范围与步长校验,失败直接返false;随后agent数组Resize到popSize,clusters到clustersNumb,parents和parentsTemp都开到parentPopSize+popSize——多出来的空间就是给每轮并归父代与子代后排序留的。epochs按入参定,epochsNow从0起。 Moving方法每轮先epochsNow++,若revision为false就随机撒坐标然后退出;否则按u.RNDprobab()<p_Replace决定搬簇心,按<p_One决定从单簇还是双簇取观点,再做变异并用SeInDiSp夹紧到搜索范围。这套逻辑让智能体既跟簇心也跨簇探索。 Revision负责回写适应度、把新观点并回父代、按f排序只留优解、首次迭代用K-Means初始化聚类,并刷新各簇质心。原文作者实测BSO对明显局部极值探索不足,即便在FitnessDistance里加alpha权重调和距离与适应度,对K-Means质心更新改动后分配模式也没明显改善。 若K-Means系不给力,可换DBSCAN(密度聚类)、层次聚类、GMM或谱聚类;附带的类里直接替掉K-Means方法就能自测。外汇与贵金属参数优化属高风险实验,回测结论仅代表历史样本,实盘可能失效。
class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="kw">struct S_BSO_Agent { class="type">class="kw">double c []; class=class="str">"cmt">//coordinates class="type">class="kw">double f; class=class="str">"cmt">//fitness class="type">int label; class=class="str">"cmt">//cluster membership label class="type">void Init(class="type">int coords) { ArrayResize(c, coords); f = -DBL_MAX; label = -class="num">1; } }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class C_AO_BSO : class="kw">public C_AO { class="kw">public: class=class="str">"cmt">//-------------------------------------------------------------------- ~C_AO_BSO() { } C_AO_BSO() { ao_name = "BSO"; ao_desc = "Brain Storm Optimization"; ao_link = "[MQL5官方文档] popSize = class="num">25; class=class="str">"cmt">//population size parentPopSize = class="num">50; class=class="str">"cmt">//parent population size;
◍ 遗传参数落进结构体再回灌
这段把聚类遗传算法的可调项先写死成默认常量,再塞进一个长度为 9 的 params 数组,靠 name+val 配对,方便外部读取或 UI 暴露。默认 clustersNumb=5、p_Replace=0.1、p_One=0.5、p_One_center=0.3、p_Two_center=0.2、k_Mutation=20.0、distribCoeff=1.0,这些数直接决定种群交叉与变异的偏向。 SetParams() 做反向操作:把 params 里下标 0~8 的 val 按类型强转回整型或浮点,重新赋给 popSize、parentPopSize、clustersNumb 等运行变量。也就是说,优化器跑完一轮若改了 params,调用一次 SetParams 就能让下一轮初始化吃进新值,不用动原常量。 Init() 的入参是三组等长数组 rangeMinP、rangeMaxP、rangeStepP,分别对应每个待优化维度的下界、上界和搜索步长。外汇与贵金属参数寻优高风险,这类边界设窄了容易陷局部最优,设宽了 MT5 回测可能跑成小时级,建议先拿 5 维小样本试水温。
clustersNumb = class="num">5; class=class="str">"cmt">//number of clusters p_Replace = class="num">0.1; class=class="str">"cmt">//replace probability p_One = class="num">0.5; class=class="str">"cmt">//probability of choosing one p_One_center = class="num">0.3; class=class="str">"cmt">//probability of choosing one center p_Two_center = class="num">0.2; class=class="str">"cmt">//probability of choosing two centers k_Mutation = class="num">20.0; class=class="str">"cmt">//mutation coefficient distribCoeff = class="num">1.0; class=class="str">"cmt">//distribution coefficient ArrayResize(params, class="num">9); params [class="num">0].name = "popSize"; params [class="num">0].val = popSize; params [class="num">1].name = "parentPopSize"; params [class="num">1].val = parentPopSize; params [class="num">2].name = "clustersNumb"; params [class="num">2].val = clustersNumb; params [class="num">3].name = "p_Replace"; params [class="num">3].val = p_Replace; params [class="num">4].name = "p_One"; params [class="num">4].val = p_One; params [class="num">5].name = "p_One_center"; params [class="num">5].val = p_One_center; params [class="num">6].name = "p_Two_center"; params [class="num">6].val = p_Two_center; params [class="num">7].name = "k_Mutation"; params [class="num">7].val = k_Mutation; params [class="num">8].name = "distribCoeff"; params [class="num">8].val = distribCoeff; } class="type">void SetParams() { popSize = (class="type">int)params [class="num">0].val; parentPopSize = (class="type">int)params [class="num">1].val; clustersNumb = (class="type">int)params [class="num">2].val; p_Replace = params [class="num">3].val; p_One = params [class="num">4].val; p_One_center = params [class="num">5].val; p_Two_center = params [class="num">6].val; k_Mutation = params [class="num">7].val; distribCoeff = params [class="num">8].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 class="kw">const class="type">class="kw">double &rangeStepP []) class=class="str">"cmt">//step search