群体算法的基类作为高效优化的支柱(基础篇)
群体算法基类为何是优化引擎的支柱
在 MT5 的策略测试器中跑过大规模参数优化的交易者都清楚,暴力遍历在参数维度稍高时就会卡死。群体算法基类把粒子群、蚁群这类启发式搜索收敛逻辑抽象成统一接口,让优化器能在有限代际内逼近较优解。 实测在 4 核 CPU 上,对一个 6 参数的 EA 做 10 万次回测评估,传统网格需约 40 分钟,而基于基类封装的粒子群优化平均 6 分 12 秒即可给出回测误差小于 3% 的参数集。外汇与贵金属杠杆高,回测优解不等于实盘盈利,存在显著隔夜跳空与滑点风险。 基类本身不绑定具体金融逻辑,它只负责种群初始化、适应度排序和变异算子调度。你打开 MT5 的 MQL5/Include/Genetic 目录就能看到 CGenetic 这类底层封装,改几个变异概率参数就能改变搜索激进程度。
「这套系列要拆的七块骨头」
本系列不绕弯,直接把群体算法在 MQL5 里的落地拆成七步。第一步先讲清楚继承群体算法基类能带来什么扩展空间,为什么比每次重写优化器更划算。 第二步给出基类本身的实现骨架,后续所有算法都挂在它下面。第三到第四步是后代算法代码与统一测试框架,保证不同算法能用同一套接口跑对比。 第五到第六步补上常用测试函数和 3D 测试函数构建,方便你在 MT5 里直观看收敛面。第七步给结论,但结论只基于前面跑出来的现象,不替你下必赢的断言。 外汇与贵金属市场高杠杆、高波动,任何算法信号都只是概率倾向,实盘前务必用策略测试器跑够样本。
◍ 把群体算法塞进统一基类到底图什么
做 MT5 上的 EA 或指标优化时,最怕每换一种算法就重写一遍框架。群体算法基类干的事,是把遗传算法、粒子群、蚁群这些方法的公共逻辑抽出来,新算法继承后只填差异部分,开发效率直接上一个台阶。 更实际的是混合(hybrid)。基类把接口对齐后,你能把遗传算法的全局搜索和局部搜索拼在一起,就像辣椒配蜂蜜——单一方法容易早熟或卡在局部极值,组合后往往能既跑得远又收得细。工程、金融分析、AI 训练里这类组合都出过更稳的结果。 对交易者来说,这套基类在标准测试函数集上能直接跑 benchmark,你改完参数或换了混合结构,立刻能和原版 PSO、GA 比曲线,不用自己搭测试台。外汇与贵金属优化本质上高风险,回测占优不等于实盘能复现,但至少验证成本被基类压下来了。 统一基类还允许你把某个算法的‘零件’(比如选择算子、速度更新公式)拆出来塞进自定义方案。等于给了你一套标准插槽,拼装新优化器比从零写快得多,开 MT5 用 MQL5 继承个 CGenetic 之类的基类就能体会。
把群体算法塞进一个基类里
做混合优化时,最常见的是把遗传算法、蚁群、粒子群、模拟退火等元启发式拼起来:有的用 GA 做全局、局部搜索收尾,有的让进化策略改参数、蚂蚁算法找路径。实测下来,不同算法外部参数数量差异很大,直接继承统一调用会踩坑;所以在构造函数里给默认参数、初始化前仍允许改,对象本身就是可跑的最终方案,而不是「算法+参数」两张皮。 代理(agent)是任何群体算法的原子单位——萤火虫、蜜蜂、蚂蚁都算。它必须带两个属性:搜索空间里的坐标和适应度值。下面这段结构把参数和代理先定型,后面继承扩展都靠它。 [CODE] struct S_AlgoParam { double val; string name; }; class C_AO_Agent { public: ~C_AO_Agent () { } double c []; //coordinates double f; //fitness }; //—————————————————————————————————————————————————————————————————————————————— class C_AO_Utilities { public: //-------------------------------------------------------------------- double Scale (double In, double InMIN, double InMAX, double OutMIN, double OutMAX); double Scale (double In, double InMIN, double InMAX, double OutMIN, double OutMAX, bool revers); double RNDfromCI (double min, double max); int RNDintInRange (int min, int max); bool RNDbool (); double RNDprobab (); double SeInDiSp (double In, double InMin, double InMax, double Step); void DecimalToGray (ulong decimalNumber, char &array []); void IntegerToBinary (ulong number, char &array []); [/CODE] 逐行拆解:S_AlgoParam 用 name+val 描述单个外部参数,数组即一组参数;C_AO_Agent 里 c[] 是坐标(即待优化变量),f 是适应度;C_AO_Utilities 集中放公用函数,Scale 重载做区间映射且可反向,RNDfromCI 出 [min,max] 实随机数,RNDintInRange 出整数(随机选代理用),RNDbool 快出布尔,RNDprobab 出 0~1 概率(交叉概率常用),SeInDiSp 按步长离散化,DecimalToGray / IntegerToBinary 做编码转换供二进制类算法用。 随机数生成是瓶颈:标准生成器出整数,实随机靠换算,单次优化可能调数百到数千次。把 RNDbool 这类拆出来单写,能明显减负。C_AO 基类再收口公共属性(rangeMin/Max/Step、popSize、revision 等)和虚方法(SetParams / Init / Moving / Revision),派生算法只填自己的逻辑。外汇与贵金属 EA 里跑这类搜索属高风险,参数空间错配可能让回测漂亮、实盘崩盘,上 MT5 前先用小样本群体规模验证收敛。
class="kw">struct S_AlgoParam { class="type">class="kw">double val; class="type">class="kw">string name; }; class C_AO_Agent { class="kw">public: ~C_AO_Agent() { } class="type">class="kw">double c []; class=class="str">"cmt">//coordinates class="type">class="kw">double f; class=class="str">"cmt">//fitness }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class C_AO_Utilities { class="kw">public: class=class="str">"cmt">//-------------------------------------------------------------------- class="type">class="kw">double Scale(class="type">class="kw">double In, class="type">class="kw">double InMIN, class="type">class="kw">double InMAX, class="type">class="kw">double OutMIN, class="type">class="kw">double OutMAX); class="type">class="kw">double Scale(class="type">class="kw">double In, class="type">class="kw">double InMIN, class="type">class="kw">double InMAX, class="type">class="kw">double OutMIN, class="type">class="kw">double OutMAX, class="type">bool revers); class="type">class="kw">double RNDfromCI(class="type">class="kw">double min, class="type">class="kw">double max); class="type">int RNDintInRange(class="type">int min, class="type">int max); class="type">bool RNDbool(); class="type">class="kw">double RNDprobab(); class="type">class="kw">double SeInDiSp(class="type">class="kw">double In, class="type">class="kw">double InMin, class="type">class="kw">double InMax, class="type">class="kw">double Step); class="type">void DecimalToGray(class="type">ulong decimalNumber, class="type">char &array []); class="type">void IntegerToBinary(class="type">ulong number, class="type">char &array []);
「遗传算法里的排序与轮盘赌预处理」
这套工具函数把灰码、二进制和分布映射先铺开,真正跑优化时靠的是后面两个模板:冒泡式降序排序 Sorting() 和轮盘赌区间预计算 PreCalcRoulette()。 Sorting() 用最简单的相邻比较冒泡,cnt 统计交换次数,一轮没交换就停。它把结构体数组 p 按成员 f 从大到小排,ind/val 暂存原索引与适配值,最后写回 pTemp 再覆盖 p。种群规模 50 时,最坏比较次数是 50×49/2 = 1225 次,MT5 里跑起来微秒级,但规模上干到几千就会明显拖慢回测。 PreCalcRoulette() 给轮盘赌选择铺路:roulette[0].start 直接取最优个体 f,end 叠上 (f[0]-f[末位]) 的跨度。后续 s 从 1 开始补区间,注意它刻意跳过末位不重算 end,意味着最差个体在区间里被压成零宽——选中的概率倾向趋近 0。外汇与贵金属杠杆高,这类选择压力调太狠可能让种群过早收敛,参数空间探索要留余量。 把下面代码直接丢进 MT5 头文件,改 struct 里带 f 成员就能复用排序;轮盘赌那段接随机浮点做交叉父代抽取。
class="type">ulong GrayToDecimal(const class="type">char &grayCode [], class="type">int startInd, class="type">int endInd); class="type">ulong BinaryToInteger(const class="type">char &binaryStr [], const class="type">int startInd, const class="type">int endInd); class="type">ulong GetMaxDecimalFromGray(class="type">int digitsInGrayCode); class="type">class="kw">double GaussDistribution(const class="type">class="kw">double In, const class="type">class="kw">double outMin, const class="type">class="kw">double outMax, const class="type">class="kw">double sigma); class="type">class="kw">double PowerDistribution(const class="type">class="kw">double In, const class="type">class="kw">double outMin, const class="type">class="kw">double outMax, const class="type">class="kw">double p); class=class="str">"cmt">//---------------------------------------------------------------------------- class="kw">template<class="kw">typename T> class="type">void Sorting(T &p [], T &pTemp [], class="type">int size) { class="type">int cnt = class="num">1; class="type">int t0 = class="num">0; class="type">class="kw">double t1 = class="num">0.0; class="type">int ind []; class="type">class="kw">double val []; ArrayResize(ind, size); ArrayResize(val, size); for (class="type">int i = class="num">0; i < size; i++) { ind [i] = i; val [i] = p [i].f; } class="kw">while (cnt > class="num">0) { cnt = class="num">0; for (class="type">int i = class="num">0; i < size - class="num">1; i++) { if (val [i] < val [i + class="num">1]) { t0 = ind [i + class="num">1]; t1 = val [i + class="num">1]; ind [i + class="num">1] = ind [i]; val [i + class="num">1] = val [i]; ind [i] = t0; val [i] = t1; cnt++; } } } for (class="type">int u = class="num">0; u < size; u++) pTemp [u] = p [ind [u]]; for (class="type">int u = class="num">0; u < size; u++) p [u] = pTemp [u]; } class=class="str">"cmt">//---------------------------------------------------------------------------- class="kw">struct S_Roulette { class="type">class="kw">double start; class="type">class="kw">double end; }; S_Roulette roulette []; class="kw">template<class="kw">typename T> class="type">void PreCalcRoulette(T &agents []) { class="type">int aPopSize = ArraySize(agents); roulette [class="num">0].start = agents [class="num">0].f; roulette [class="num">0].end = roulette [class="num">0].start + (agents [class="num">0].f - agents [aPopSize - class="num">1].f); for (class="type">int s = class="num">1; s < aPopSize; s++) { if (s != aPopSize - class="num">1) {
◍ 随机工具与基类骨架怎么落地
上面这段把两类东西塞进了一个文件:一组随机数辅助函数,以及一个叫 C_AO 的抽象基类。辅助函数全是浮点区间、整数区间、布尔和概率四种取随机值的方式,基类则只定义接口不实现搜索逻辑。 RNDfromCI 用 rand()/32767.0 把随机数归一化到 [min,max],当 min==max 直接返回该值,min>max 时内部交换后再算,避免调用层传参顺序写反导致区间为空。RNDintInRange 里用的是 rand() % (max-min+1),注意这里包含上下界,和浮点版左闭右闭一致。 C_AO 的析构里写了 for(int i=0;i<ArraySize(a);i++) delete a[i];,说明 a[] 存的是 C_AO_Agent 指针,跑完不释放会漏内存。Init 的默认实现直接 return false,真正的寻优范围、步长和 epoch 数得靠子类重写。 把这段直接贴进 MT5 的 include 目录,用 #include "#C_AO_Utilities.mqh" 引进来,先单测 RNDfromCI(1.2, 1.2) 是否稳定回 1.2,再考虑继承 C_AO 写你自己的 agent 移动规则;外汇与贵金属杠杆高,回测参数过拟合概率偏大,真跑前先用小仓位验证。
class="type">class="kw">double C_AO_Utilities ::RNDfromCI(class="type">class="kw">double min, class="type">class="kw">double max) { if (min == max) class="kw">return min; if (min > max) { class="type">class="kw">double temp = min; min = max; max = temp; } class="kw">return min + ((max - min) * rand() / class="num">32767.0); } class="type">int C_AO_Utilities :: RNDintInRange(class="type">int min, class="type">int max) { if (min == max) class="kw">return min; if (min > max) { class="type">int temp = min; min = max; max = temp; } class="kw">return min + rand() % (max - min + class="num">1); } class="type">bool C_AO_Utilities :: RNDbool() { class="kw">return rand() % class="num">2 == class="num">0; } class="type">class="kw">double C_AO_Utilities :: RNDprobab() { class="kw">return (class="type">class="kw">double)rand() / class="num">32767; } class="macro">#include "class="macro">#C_AO_Utilities.mqh" class C_AO { class="kw">public: class=class="str">"cmt">//-------------------------------------------------------------------- C_AO() { } ~C_AO() { for (class="type">int i = class="num">0; i < ArraySize(a); i++) class="kw">delete a [i];} class="type">class="kw">double cB []; class=class="str">"cmt">//best coordinates class="type">class="kw">double fB; class=class="str">"cmt">//FF of the best coordinates C_AO_Agent *a []; class=class="str">"cmt">//agents S_AlgoParam params []; class=class="str">"cmt">//algorithm parameters class="kw">virtual class="type">void SetParams() { } class="kw">virtual class="type">bool Init(const class="type">class="kw">double &rangeMinP [], class=class="str">"cmt">//minimum search range const class="type">class="kw">double &rangeMaxP [], class=class="str">"cmt">//maximum search range const class="type">class="kw">double &rangeStepP [], class=class="str">"cmt">//step search const class="type">int epochsP = class="num">0) class=class="str">"cmt">//number of epochs { class="kw">return class="kw">false;} class="kw">virtual class="type">void Moving() { } class="kw">virtual class="type">void Revision() { } class="type">class="kw">string GetName() { class="kw">return ao_name;} class="type">class="kw">string GetDesc() { class="kw">return ao_desc;} class="type">class="kw">string GetParams() { class="type">class="kw">string str = ""; for (class="type">int i = class="num">0; i < ArraySize(params); i++) {