群体算法的基类作为高效优化的支柱·进阶篇
(2/3)·从遗传算法到蚁群策略,看基类如何把多种群体智能揉成一把快刀
不少开发者把群体算法当成孤立工具,写完一个遗传脚本就丢在一边,遇到收敛慢只会调参数。其实把不同算法的优势塞进同一个基类,往往比死磕单法更快摸到最优解。
「优化器基类的参数容器与初始化」
这段基类代码把遗传/进化类优化器的公共骨架定下来了:用 rangeMin、rangeMax、rangeStep 三个数组锁死每个待优参数的搜索边界与步长,coords 记录维度数量。StandardInit 里先拿 GetMicrosecondCount 做 MathSrand 种子重置,避免每次 MT5 重启后种群演化轨迹完全复刻。 初始化时会做一道维度校验——coords 必须等于三个范围数组的 size,否则直接 return false。实测若你只传了 2 个参数的 min 却给了 3 个 step,该函数返回假,后续种群 ArrayResize 不会执行,EA 加载可能静默失败。 尾部那串 include 和 E_AO 枚举是调度层:SelectAO 按枚举 new 出具体算法实例(BGA、P_O_ES、SDSm、ESG、DE)。想接自己的算子,照着 C_AO 派生后在这里加一个 case 即可,外汇与贵金属参数寻优属高风险实验,回测过拟合概率偏高,建议先单品种验证。
str += (class="type">class="kw">string)params [i].val + "|"; } class="kw">return str; } class="kw">protected: class=class="str">"cmt">//----------------------------------------------------------------- class="type">class="kw">string ao_name; class=class="str">"cmt">//ao name; class="type">class="kw">string ao_desc; class=class="str">"cmt">//ao description class="type">class="kw">double rangeMin []; class=class="str">"cmt">//minimum search range class="type">class="kw">double rangeMax []; class=class="str">"cmt">//maximum search range class="type">class="kw">double rangeStep []; class=class="str">"cmt">//step search class="type">int coords; class=class="str">"cmt">//coordinates number class="type">int popSize; class=class="str">"cmt">//population size class="type">bool revision; C_AO_Utilities u; class=class="str">"cmt">//auxiliary functions class="type">bool StandardInit(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 { MathSrand((class="type">int)GetMicrosecondCount()); class=class="str">"cmt">//reset of the generator fB = -DBL_MAX; revision = class="kw">false; coords = ArraySize(rangeMinP); if (coords == class="num">0 || coords != ArraySize(rangeMaxP) || coords != ArraySize(rangeStepP)) class="kw">return class="kw">false; ArrayResize(rangeMin, coords); ArrayResize(rangeMax, coords); ArrayResize(rangeStep, coords); ArrayResize(cB, coords); ArrayCopy(rangeMin, rangeMinP, class="num">0, class="num">0, WHOLE_ARRAY); ArrayCopy(rangeMax, rangeMaxP, class="num">0, class="num">0, WHOLE_ARRAY); ArrayCopy(rangeStep, rangeStepP, class="num">0, class="num">0, WHOLE_ARRAY); class="kw">return true; } }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="macro">#include "AO_BGA_Binary_Genetic_Algorithm.mqh" class="macro">#include "AO_(P_O)ES_Evolution_Strategies.mqh" class="macro">#include "AO_DE_Differential_Evolution.mqh" class="macro">#include "AO_SDSm_Stochastic_Diffusion_Search.mqh" class="macro">#include "AO_ESG_Evolution_of_Social_Groups.mqh"; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— enum E_AO { AO_BGA, AO_P_O_ES, AO_SDSm, AO_ESG, AO_DE, AO_NONE }; C_AO *SelectAO(E_AO a) { C_AO *ao; class="kw">switch (a) { case AO_BGA: ao = new C_AO_BGA(); class="kw">return (GetPointer(ao)); case AO_P_O_ES: ao = new C_AO_P_O_ES(); class="kw">return (GetPointer(ao)); case AO_SDSm: ao = new C_AO_SDSm(); class="kw">return (GetPointer(ao)); case AO_ESG:
◍ 用工厂方法隔离不同 AO 算法实例
这段片段属于一个 AO 指标对象的工厂构造逻辑,依据传入的枚举类型决定具体实例化哪一个派生类。 当分支为 AO_ESG 时,用 new 创建 C_AO_ESG 对象并通过 GetPointer 返回其指针;遇到 AO_DE 则改为构造 C_AO_DE,同样返回指针。 default 分支直接把 ao 置为 NULL 并返回 NULL,意味着调用方在拿到空指针时必须自行处理,否则后续访问会触发无效指针异常。 在 MT5 里跑这段,只需确认你的枚举里确实定义了 AO_ESG 与 AO_DE,否则编译器会报 case 标签不匹配。外汇与贵金属行情波动剧烈,此类对象创建失败不会提示盈亏,仅表现为指标不绘制,高风险环境下需自行加日志排查。
ao = new C_AO_ESG(); class="kw">return (GetPointer(ao)); case AO_DE: ao = new C_AO_DE(); class="kw">return (GetPointer(ao)); class="kw">default: ao = NULL; class="kw">return NULL; } }
从基类派生 SDSm 代理与算法类
随机扩散搜索(SDSm)在 MT5 里适合作为优化代理的继承范例:用 C_SDS_Agent 继承基础类 C_AO_Agent,代理里只补了餐厅地址、前次坐标与适应度暂存字段,而 c 坐标和 f 适应度直接沿用基类,不重复声明——所有优化算法代理都吃这两样,写一遍就够。 C_AO_SDSm 继承自 C_AO,构造时把外部参数塞进数组 params,默认 popSize=100、restNumb=100、probabRest=0.05。用数组而非散参,是为了不和测试台(如策略测试器参数接口)打架,用户之后随时可改。 Init 方法先调基类的 StandardInit 并传入 rangeMinP / rangeMaxP / rangeStepP;若返回 false 则整体初始化失败。随后 delete 旧代理(重用对象时防内存泄漏),再把 agent 数组与基类 a 的尺寸改到 popSize 并做类型转换,最后走 SDSm 原论文的扩散逻辑。 下面这段是类声明的骨架,注意 C_SDS_Agent::Init 里把 f 和 fPrev 都预置为 -DBL_MAX,意味着未评估前适应度视为负无穷,避免被误选为最优。
class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class C_SDS_Agent : class="kw">public C_AO_Agent { class="kw">public: class=class="str">"cmt">//-------------------------------------------------------------------- ~C_SDS_Agent() { } class="type">int raddr []; class=class="str">"cmt">//restaurant address class="type">int raddrPrev []; class=class="str">"cmt">//previous restaurant address class="type">class="kw">double cPrev []; class=class="str">"cmt">//previous coordinates(dishes) class="type">class="kw">double fPrev; class=class="str">"cmt">//previous fitness class="type">void Init(class="type">int coords) { ArrayResize(c, coords); ArrayResize(cPrev, coords); ArrayResize(raddr, coords); ArrayResize(raddrPrev, coords); f = -DBL_MAX; fPrev = -DBL_MAX; } }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class C_AO_SDSm : class="kw">public C_AO { class="kw">public: class=class="str">"cmt">//-------------------------------------------------------------------- ~C_AO_SDSm() { } C_AO_SDSm() { ao_name = "SDSm"; ao_desc = "Stochastic Diffusion Search"; popSize = class="num">100; class=class="str">"cmt">//population size restNumb = class="num">100; class=class="str">"cmt">//restaurants number probabRest = class="num">0.05; class=class="str">"cmt">//probability restaurant choosing ArrayResize(params, class="num">3); params [class="num">0].name = "popSize"; params [class="num">0].val = popSize; params [class="num">1].name = "restNumb"; params [class="num">1].val = restNumb; params [class="num">2].name = "probabRest"; params [class="num">2].val = probabRest; } class="type">void SetParams() { popSize = (class="type">int)params [class="num">0].val; restNumb = (class="type">int)params [class="num">1].val; probabRest = params [class="num">2].val; } 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="type">void Moving(); class="type">void Revision(); class=class="str">"cmt">//---------------------------------------------------------------------------- class="type">int restNumb; class=class="str">"cmt">//restaurants number
「SDS代理的初始化与河床结构铺设」
这段 MQL5 实现的是 SDS(随机扩散搜索)多代理模型的初始化环节,核心是把搜索空间切成若干‘餐厅’格子,并为每个坐标维度准备好‘河床’记录结构。 Init 函数先调用 StandardInit 做范围与步长校验,失败直接返回 false;随后按 popSize 重整 agent 与 a 两个数组,逐个 new 出 C_SDS_Agent 并绑定到 coords 维度上。 河床 rb 按坐标数 coords 展开,每个 rb[i].coordOnSector 再按 restNumb resize,并用 -DBL_MAX 初始化——这意味着该格点尚未被任何代理探过。restSpace[i] 则等于 (rangeMax[i]-rangeMin[i])/restNumb,即单维上每个餐厅的物理跨度。 别把河床当摆设 rb 里的 -DBL_MAX 是后续 Research 判断‘是否首次到达该格’的哨兵值,若你改了初始化填充值,探坑逻辑会直接漏掉冷门区间。外汇与贵金属参数寻优用这套结构时,网格过密会放大过拟合风险,建议先在 MT5 策略测试器里用小 restNumb 验证内存与耗时。
class="type">class="kw">double probabRest; class=class="str">"cmt">//probability restaurant choosing C_SDS_Agent *agent []; class=class="str">"cmt">//candidates class="kw">private: class=class="str">"cmt">//------------------------------------------------------------------- class="kw">struct S_Riverbed class=class="str">"cmt">//river bed { class="type">class="kw">double coordOnSector []; class=class="str">"cmt">//coordinate on the sector(number of cells: number of sectors on the coordinate, cell value: specific coordinate on the sector) }; class="type">class="kw">double restSpace []; class=class="str">"cmt">//restaurants space S_Riverbed rb []; class=class="str">"cmt">//riverbed class="type">void Research(const class="type">class="kw">double ko, const class="type">int raddr, const class="type">class="kw">double restSpace, const class="type">class="kw">double rangeMin, const class="type">class="kw">double rangeStep, const class="type">class="kw">double pitOld, class="type">class="kw">double &pitNew); }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">bool C_AO_SDSm::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 { if (!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) class="kw">return class="kw">false; class=class="str">"cmt">//---------------------------------------------------------------------------- for (class="type">int i = class="num">0; i < ArraySize(agent); i++) class="kw">delete agent [i]; ArrayResize(agent, popSize); ArrayResize(a, popSize); for (class="type">int i = class="num">0; i < popSize; i++) { a [i] = new C_SDS_Agent(); agent [i] = (C_SDS_Agent *)a [i]; agent [i].Init(coords); } ArrayResize(restSpace, coords); ArrayResize(rb, coords); for (class="type">int i = class="num">0; i < coords; i++) { ArrayResize(rb [i].coordOnSector, restNumb); ArrayInitialize(rb [i].coordOnSector, -DBL_MAX); } for (class="type">int i = class="num">0; i < coords; i++) { restSpace [i] = (rangeMax [i] - rangeMin [i]) / restNumb; } class="kw">return true; } class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
◍ 把测试台功能收进一个类里
现在不需要往测试台里加新东西,但把全部功能搬进 C_TestStand 类之后,调用和封装都省事很多。类内部逻辑和之前差别不大,重点在对外接口干净了:初始化时直接传画布宽高,不用再到处改硬编码。 标准输入参数里已经能选优化算法和测试函数,目前只支持平滑函数或离散函数两种基准,但你可以任意勾选组合来压测算法。比如同时挂两个离散函数,就能看出某算法在多点极小值附近的逃逸倾向。 下面这段是类的骨架代码,Init 里 W 默认可设 750、H 设 375,实际由外部传入;画布名固定为 AO_Test_Func_Canvas,创建失败会打印错误码。MaxMinDr 负责把全局最大最小值用黑色圆圈标出来,半径从 12 到 15 像素叠了四层,视觉上是个实心黑环。 代码逐行拆: #include <Canvas\Canvas.mqh> 引入画布库 #include <\Math\Functions.mqh> 引入数学函数库 class C_TestStand 定义测试台类 public: void Init(int width, int height) 公开初始化方法,接收宽高 W = width; 画布宽赋值(注释里原 750 为旧值) H = height; 画布高赋值(注释里原 375 为旧值) WscrFunc = H - 2; 函数区宽按高减边距 HscrFunc = H - 2; 函数区高减边距 string canvasName = "AO_Test_Func_Canvas"; 画布对象名 if (!Canvas.CreateBitmapLabel(canvasName, 5, 30, W, H, COLOR_FORMAT_ARGB_RAW)) 在坐标(5,30)建位图画布 Print("Error creating Canvas: ", GetLastError()); 失败则打印错误 return; 退出 Init ObjectSetInteger(0, canvasName, OBJPROP_HIDDEN, false); 画布不隐藏 ObjectSetInteger(0, canvasName, OBJPROP_SELECTABLE, true); 画布可选中 ArrayResize(FunctScrin, HscrFunc); 函数色板数组按高分配 for (int i=0; i<HscrFunc; i++) ArrayResize(FunctScrin[i].clr, HscrFunc); 每行颜色数组再按高分配 struct S_CLR { color clr []; }; 颜色结构体 public: void CanvasErase() 清屏方法 Canvas.Erase(XRGB(0,0,0)); 先擦成黑底 Canvas.FillRectangle(1,1,H-2,H-2, COLOR2RGB(clrWhite)); 左半填白 Canvas.FillRectangle(H+1,1,W-2,H-2, COLOR2RGB(clrWhite)); 右半填白 public: void MaxMinDr(C_Function & f) 画极值方法,引用函数对象 int x = (int)Scale(f.GetMaxFuncX(), f.GetMinRangeX(), f.GetMaxRangeX(), 1, W/2-1, false); 最大值X映射到左半区 int y = (int)Scale(f.GetMaxFuncY(), f.GetMinRangeY(), f.GetMaxRangeY(), 1, H-1, true); 最大值Y映射(Y反转) Canvas.Circle(x,y,12~15, COLOR2RGB(clrBlack)); 四层黑圈标最大值 x = (int)Scale(f.GetMinFuncX(), f.GetMinRangeX(), f.GetMaxRangeX(), 0, W/2-1, false); 最小值X映射 y = (int)Scale(f.GetMinFuncY(), f.GetMinRangeY(), f.GetMaxRangeY(), 0, H-1, true); 最小值Y映射 Canvas.Circle(x,y,12~13, COLOR2RGB(clrBlack)); 两层黑圈标最小值
class="macro">#include <Canvas\Canvas.mqh> class="macro">#include <\Math\Functions.mqh> class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class C_TestStand { class="kw">public: class="type">void Init(class="type">int width, class="type">int height) { W = width; class=class="str">"cmt">//class="num">750; H = height; class=class="str">"cmt">//class="num">375; WscrFunc = H - class="num">2; HscrFunc = H - class="num">2; class=class="str">"cmt">//creating a table --------------------------------------------------------- class="type">class="kw">string canvasName = "AO_Test_Func_Canvas"; if (!Canvas.CreateBitmapLabel(canvasName, class="num">5, class="num">30, W, H, COLOR_FORMAT_ARGB_RAW)) { Print("Error creating Canvas: ", GetLastError()); class="kw">return; } ObjectSetInteger(class="num">0, canvasName, OBJPROP_HIDDEN, class="kw">false); ObjectSetInteger(class="num">0, canvasName, OBJPROP_SELECTABLE, true); ArrayResize(FunctScrin, HscrFunc); for (class="type">int i = class="num">0; i < HscrFunc; i++) ArrayResize(FunctScrin [i].clr, HscrFunc); } class="kw">struct S_CLR { class="type">color clr []; }; class=class="str">"cmt">//---------------------------------------------------------------------------- class="kw">public: class="type">void CanvasErase() { Canvas.Erase(XRGB(class="num">0, class="num">0, class="num">0)); Canvas.FillRectangle(class="num">1, class="num">1, H - class="num">2, H - class="num">2, COLOR2RGB(clrWhite)); Canvas.FillRectangle(H + class="num">1, class="num">1, W - class="num">2, H - class="num">2, COLOR2RGB(clrWhite)); } class=class="str">"cmt">//---------------------------------------------------------------------------- class="kw">public: class="type">void MaxMinDr(C_Function & f) { class=class="str">"cmt">//draw Max global------------------------------------------------------------- class="type">int x = (class="type">int)Scale(f.GetMaxFuncX(), f.GetMinRangeX(), f.GetMaxRangeX(), class="num">1, W/class="num">2 - class="num">1, class="kw">false); class="type">int y = (class="type">int)Scale(f.GetMaxFuncY(), f.GetMinRangeY(), f.GetMaxRangeY(), class="num">1, H - class="num">1, true); Canvas.Circle(x, y, class="num">12, COLOR2RGB(clrBlack)); Canvas.Circle(x, y, class="num">13, COLOR2RGB(clrBlack)); Canvas.Circle(x, y, class="num">14, COLOR2RGB(clrBlack)); Canvas.Circle(x, y, class="num">15, COLOR2RGB(clrBlack)); class=class="str">"cmt">//draw Min global------------------------------------------------------------- x = (class="type">int)Scale(f.GetMinFuncX(), f.GetMinRangeX(), f.GetMaxRangeX(), class="num">0, W/class="num">2 - class="num">1, class="kw">false); y = (class="type">int)Scale(f.GetMinFuncY(), f.GetMinRangeY(), f.GetMaxRangeY(), class="num">0, H - class="num">1, true); Canvas.Circle(x, y, class="num">12, COLOR2RGB(clrBlack)); Canvas.Circle(x, y, class="num">13, COLOR2RGB(clrBlack)); } class=class="str">"cmt">//----------------------------------------------------------------------------
把函数点阵画到画布上的两套画法
PointDr 方法负责把一组 (x,y) 散点映射到画布坐标并上色。循环里每个点先累加求均值,再用 Scale 把数值域换算到 0~WscrFunc-1 和 0~HscrFunc-1 的像素区间,色相由 DoubleToColor 按序号从 0 到 270 度渐变,半径 1 像素实心圆。 均值点区分主从:非主序列画外径 3 黑、内径 2 白的双环,主序列则描 5 和 6 像素两道黑圈,视觉上能把基准轨迹从陪衬点里拎出来。外汇与贵金属图表叠加这类自绘层时,注意高价波动下坐标缩放可能挤压点距,属高风险观测手段。 SendGraphToCanvas 用双重循环把 FunctScrin[w].clr[h] 逐像素搬进 Canvas,偏移 +1 避免贴边;DrawFunctionGraph 则按 HscrFunc 分辨率反推每个像素对应的 x、y 值域再交回函数求值。开 MT5 把 WscrFunc、HscrFunc 调到 200 以上,能直接看清单点密度是否够支撑你的价格行为判断。
class="kw">public: class="type">void PointDr(class="type">class="kw">double &args [], C_Function & f, class="type">int shiftX, class="type">int shiftY, class="type">int count, class="type">bool main) { class="type">class="kw">double x = class="num">0.0; class="type">class="kw">double y = class="num">0.0; class="type">class="kw">double xAve = class="num">0.0; class="type">class="kw">double yAve = class="num">0.0; class="type">int width = class="num">0; class="type">int height = class="num">0; class="type">color clrF = clrNONE; for(class="type">int i = class="num">0; i < count; i++) { xAve += args [i * class="num">2]; yAve += args [i * class="num">2 + class="num">1]; x = args [i * class="num">2]; y = args [i * class="num">2 + class="num">1]; width = (class="type">int)Scale(x, f.GetMinRangeX(), f.GetMaxRangeX(), class="num">0, WscrFunc - class="num">1, class="kw">false); height = (class="type">int)Scale(y, f.GetMinRangeY(), f.GetMaxRangeY(), class="num">0, HscrFunc - class="num">1, true); clrF = DoubleToColor(i, class="num">0, count - class="num">1, class="num">0, class="num">270); Canvas.FillCircle(width + shiftX, height + shiftY, class="num">1, COLOR2RGB(clrF)); } xAve /=(class="type">class="kw">double)count; yAve /=(class="type">class="kw">double)count; width = (class="type">int)Scale(xAve, f.GetMinRangeX(), f.GetMaxRangeX(), class="num">0, WscrFunc - class="num">1, class="kw">false); height = (class="type">int)Scale(yAve, f.GetMinRangeY(), f.GetMaxRangeY(), class="num">0, HscrFunc - class="num">1, true); if(!main) { Canvas.FillCircle(width + shiftX, height + shiftY, class="num">3, COLOR2RGB(clrBlack)); Canvas.FillCircle(width + shiftX, height + shiftY, class="num">2, COLOR2RGB(clrWhite)); } else { Canvas.Circle(width + shiftX, height + shiftY, class="num">5, COLOR2RGB(clrBlack)); Canvas.Circle(width + shiftX, height + shiftY, class="num">6, COLOR2RGB(clrBlack)); } } class=class="str">"cmt">//-------------------------------------------------------------------------------- class="kw">public: class="type">void SendGraphToCanvas() { for (class="type">int w = class="num">0; w < HscrFunc; w++) { for (class="type">int h = class="num">0; h < HscrFunc; h++) { Canvas.PixelSet(w + class="num">1, h + class="num">1, COLOR2RGB(FunctScrin [w].clr [h])); } } } class=class="str">"cmt">//-------------------------------------------------------------------------------- class="kw">public: class="type">void DrawFunctionGraph(C_Function & f) { class="type">class="kw">double ar [class="num">2]; class="type">class="kw">double fV; for (class="type">int w = class="num">0; w < HscrFunc; w++) { ar [class="num">0] = Scale(w, class="num">0, H, f.GetMinRangeX(), f.GetMaxRangeX(), class="kw">false); for (class="type">int h = class="num">0; h < HscrFunc; h++) { ar [class="num">1] = Scale(h, class="num">0, H, f.GetMinRangeY(), f.GetMaxRangeY(), true);