群体优化算法·进阶篇
(2/3)·跳过历史铺垫,直接钻进测试基台与简单 OA 的工程细节,看清收敛率背后的真实代价
「搭一个能横向比算法的评估台」
比较不同优化算法(OA)不能拍脑袋,因为各自擅长的问题类别不同:有的收敛快但变量一多就拉胯,有的能扛上千维却不稳定。要横向比,得先把收敛、收敛率、稳定性、伸缩性拆成统一口径。 收敛性用三个测试函数,把结果线性映射到 0.0(最差)到 1.0(最佳),这样不同函数之间能直接比「保底能力」。收敛率看第 1000 次和第 10,000 次运行的最佳值,曲线越早贴顶越快。稳定性是每个函数跑 5 次取均值,波动大的算法在这一项会现原形。 伸缩性差异最直观:有些 OA 只能处理 ≤2 个变量,有些能跑 1000 个变量,后者才可能拿去当神经网络的优化器。MT5 优化器默认大约只给 10,000 步找最大值,而两个变量的 Skin 函数若用 0.00001 步长枚举,X、Y 各百万步,总计算量是 10^12 次——OA 的搜索难度可想而知。 下面这段父类把参数边界、函数极值、名称都封装好,子类只重写 Core() 即可;枚举器 SelectFunction() 用来按需切换测试函数对象。本文及后续测试统一用步长 0.0(或算法最小可能值),别手滑设成枚举步长。
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="keyword">class</span> C_Function { <span class="keyword">class="kw">public</span>: <span class="comment">class=class="str">"cmt">//====================================================================</span> <span class="keyword">class="type">class="kw">double</span> CalcFunc(<span class="keyword">class="type">class="kw">double</span> &args [], <span class="comment">class=class="str">"cmt">//function arguments</span> <span class="keyword">class="type">int</span> amount) <span class="comment">class=class="str">"cmt">//amount of runs functions</span> { <span class="keyword">class="type">class="kw">double</span> x, y; <span class="keyword">class="type">class="kw">double</span> sum = <span class="number">class="num">0.0</span>; <span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i < amount; i++) { x = args [i * <span class="number">class="num">2</span>]; y = args [i * <span class="number">class="num">2</span> + <span class="number">class="num">1</span>]; sum += Core(x, y); } sum /= amount; <span class="keyword">class="kw">return</span> sum; } <span class="keyword">class="type">class="kw">double</span> GetMinArg() { <span class="keyword">class="kw">return</span> minArg;} <span class="keyword">class="type">class="kw">double</span> GetMaxArg() { <span class="keyword">class="kw">return</span> maxArg;} <span class="keyword">class="type">class="kw">double</span> GetMinFun() { <span class="keyword">class="kw">return</span> minFun;} <span class="keyword">class="type">class="kw">double</span> GetMaxFun() { <span class="keyword">class="kw">return</span> maxFun;} <span class="keyword">class="type">class="kw">string</span> GetNamFun() { <span class="keyword">class="kw">return</span> fuName;} <span class="keyword">class="kw">protected</span>: <span class="comment">class=class="str">"cmt">//==================================================================</span> <span class="keyword">class="type">void</span> SetMinArg(<span class="keyword">class="type">class="kw">double</span> min) { minArg = min;} <span class="keyword">class="type">void</span> SetMaxArg(<span class="keyword">class="type">class="kw">double</span> max) { maxArg = max;} <span class="keyword">class="type">void</span> SetMinFun(<span class="keyword">class="type">class="kw">double</span> min) { minFun = min;} <span class="keyword">class="type">void</span> SetMaxFun(<span class="keyword">class="type">class="kw">double</span> max) { maxFun = max;} <span class="keyword">class="type">void</span> SetNamFun(<span class="keyword">class="type">class="kw">string</span> nam) { fuName = nam;} <span class="keyword">class="kw">private</span>: <span class="comment">class=class="str">"cmt">//====================================================================</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">class="kw">double</span> Core(<span class="keyword">class="type">class="kw">double</span> x, <span class="keyword">class="type">class="kw">double</span> y) { <span class="keyword">class="kw">return</span> <span class="number">class="num">0.0</span>;} <span class="keyword">class="type">class="kw">double</span> minArg; <span class="keyword">class="type">class="kw">double</span> maxArg; <span class="keyword">class="type">class="kw">double</span> minFun; <span class="keyword">class="type">class="kw">double</span> maxFun; <span class="keyword">class="type">class="kw">string</span> fuName; }; <span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="keyword">enum</span> EFunc { Skin, Forest, Megacity, }; C_Function *SelectFunction(EFunc f) { C_Function *func; <span class="keyword">class="kw">switch</span> (f) { <span class="keyword">case</span> Skin: func = <span class="keyword">new</span> C_Skin(); <span class="keyword">class="kw">return</span> (<span class="functions">GetPointer</span> (func)); <span class="keyword">case</span> Forest: func = <span class="keyword">new</span> C_Forest(); <span class="keyword">class="kw">return</span> (<span class="functions">GetPointer</span> (func)); <span class="keyword">case</span> Megacity: func = <span class="keyword">new</span> C_Megacity(); <span class="keyword">class="kw">return</span> (<span class="functions">GetPointer</span> (func)); <span class="keyword">class="kw">default</span>:
◍ 把测试函数塞进类里跑二维曲面
上面那段工厂方法只是入口,真正定义优化地貌的是派生自 C_Function 的几个子类。每个子类在构造函数里写死名称、自变量区间和已知极值坐标,Core() 才是被采样器反复调用的目标函数。 以 C_Skin 为例:自变量限在 [-5.0, 5.0],最小函数值 -4.3182 出现在 (x=3.07021, y=3.315935),最大 14.0606 在 (x=-3.315699, y=-3.072485)。它的 Core 用 2*x*x、2*y*y 做平方放大,再套 cos/sin 偏移平方求和,曲面带多个浅坑,适合测算法是否陷局部极小。 C_Forest 的区间很怪:MinArg=-50、MaxArg=-18,也就是 x、y 都被压在负轴一段。最大值 15.95123239744 落在 (-25.1327, -32.5575),Core 用 sqrt(abs(...)) 包 sin/cos 再四次方,负值直接截断为 0,形成大片零值平原加尖峰,对全局搜索是硬骨头。 C_Megacity 把区间放宽到 [-15, 15],最小函数值 0 且注明 many points——意味着零值等高位可能连成片,容易让优化器在平原上乱走。外汇与贵金属实盘用这类测试函数验证 EA 寻优逻辑时,须注意历史拟合不代表未来,杠杆品种风险极高。
func = new C_Skin(); class="kw">return (GetPointer(func)); } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class C_Skin : class="kw">public C_Function { class="kw">public: class=class="str">"cmt">//=================================================================== C_Skin() { SetNamFun("Skin"); SetMinArg(-class="num">5.0); SetMaxArg(class="num">5.0); SetMinFun(-class="num">4.3182); class=class="str">"cmt">//[x=class="num">3.07021;y=class="num">3.315935] class="num">1 point SetMaxFun(class="num">14.0606); class=class="str">"cmt">//[x=-class="num">3.315699;y=-class="num">3.072485] class="num">1 point } class="kw">private: class=class="str">"cmt">//=================================================================== class="type">class="kw">double Core(class="type">class="kw">double x, class="type">class="kw">double y) { class="type">class="kw">double a1=class="num">2*x*x; class="type">class="kw">double a2=class="num">2*y*y; class="type">class="kw">double b1=MathCos(a1)-class="num">1.1; b1=b1*b1; class="type">class="kw">double c1=MathSin(class="num">0.5*x)-class="num">1.2; c1=c1*c1; class="type">class="kw">double d1=MathCos(a2)-class="num">1.1; d1=d1*d1; class="type">class="kw">double e1=MathSin(class="num">0.5*y)-class="num">1.2; e1=e1*e1; class="type">class="kw">double res=b1+c1-d1+e1; class="kw">return(res); } }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class C_Forest : class="kw">public C_Function { class="kw">public: class=class="str">"cmt">//=================================================================== C_Forest() { SetNamFun("Forest"); SetMinArg(-class="num">50.0); SetMaxArg(-class="num">18.0); SetMinFun(class="num">0.0); class=class="str">"cmt">//many points SetMaxFun(class="num">15.95123239744); class=class="str">"cmt">//[x=-class="num">25.132741228718345;y=-class="num">32.55751918948773] class="num">1 point } class="kw">private: class=class="str">"cmt">//=================================================================== class="type">class="kw">double Core(class="type">class="kw">double x, class="type">class="kw">double y) { class="type">class="kw">double a = MathSin(MathSqrt(MathAbs(x - class="num">1.13) + MathAbs(y - class="num">2.0))); class="type">class="kw">double b = MathCos(MathSqrt(MathAbs(MathSin(x))) + MathSqrt(MathAbs(MathSin(y - class="num">2.0)))); class="type">class="kw">double f = a + b; class="type">class="kw">double res = MathPow(f, class="num">4); if (res < class="num">0.0) res = class="num">0.0; class="kw">return (res); } }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class C_Megacity : class="kw">public C_Function { class="kw">public: class=class="str">"cmt">//=================================================================== C_Megacity() { SetNamFun("Megacity"); SetMinArg(-class="num">15.0); SetMaxArg(class="num">15.0); SetMinFun(class="num">0.0); class=class="str">"cmt">//many points
网格扫描找极值的一段实测代码
下面这段 MQL5 把自定义函数放到一个二维网格里暴力扫一遍,目的是抓出最大值点和最小值点。输入参数里 Step 默认 0.01,意味着在 argMin 到 argMax 区间内,x、y 两个维度各要跑成百上千步,cnt 计数器会如实打印总调用次数。 核心循环先用双层 while 遍历 (x,y),每一步调 TestFunc.CalcFunc 算函数值,再和当前 maxFuncValue / minFuncValue 比大小并记录坐标。注意 cnt==1 时强行把首尾值赋给最大最小,避免初始 0 误判——这个细节直接决定打印出来的极值坐标是否可信。 外汇与贵金属品种用这类扫描做参数敏感性测试时,网格过密会让 OnStart 跑满几十秒,且 MT5 策略测试器里的高频调用有滑点失真风险,结果只能当概率参考。
SetMaxFun(class="num">15.0); class=class="str">"cmt">//[x=`class="num">3.16;y=class="num">1.990] class="num">1 point } class="kw">private: class=class="str">"cmt">//=================================================================== class="type">class="kw">double Core(class="type">class="kw">double x, class="type">class="kw">double y) { class="type">class="kw">double a = MathSin(MathSqrt(MathAbs(x - class="num">1.13) + MathAbs(y - class="num">2.0))); class="type">class="kw">double b = MathCos(MathSqrt(MathAbs(MathSin(x))) + MathSqrt(MathAbs(MathSin(y - class="num">2.0)))); class="type">class="kw">double f = a + b; class="type">class="kw">double res = floor(MathPow(f, class="num">4)); class="kw">return (res); } }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— input EFunc Function = Skin; input class="type">class="kw">double Step = class="num">0.01; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void OnStart() { C_Function *TestFunc = SelectFunction(Function); class="type">class="kw">double argMin = TestFunc.GetMinArg(); class="type">class="kw">double argMax = TestFunc.GetMaxArg(); class="type">class="kw">double maxFuncValue = class="num">0; class="type">class="kw">double xMaxFunc = class="num">0.0; class="type">class="kw">double yMaxFunc = class="num">0.0; class="type">class="kw">double minFuncValue = class="num">0; class="type">class="kw">double xMinFunc = class="num">0.0; class="type">class="kw">double yMinFunc = class="num">0.0; class="type">class="kw">double fValue = class="num">0.0; class="type">class="kw">double arg [class="num">2]; arg [class="num">0] = argMin; arg [class="num">1] = argMin; class="type">long cnt = class="num">0; class="kw">while (arg [class="num">1] <= argMax && !IsStopped()) { arg [class="num">0] = argMin; class="kw">while (arg [class="num">0] <= argMax && !IsStopped()) { cnt++; fValue = TestFunc.CalcFunc(arg, class="num">1); if (fValue > maxFuncValue) { maxFuncValue = fValue; xMaxFunc = arg [class="num">0]; yMaxFunc = arg [class="num">1]; } if (fValue < minFuncValue) { minFuncValue = fValue; xMinFunc = arg [class="num">0]; yMinFunc = arg [class="num">1]; } arg [class="num">0] += Step; if (cnt == class="num">1) { maxFuncValue = fValue; minFuncValue = fValue; } } arg [class="num">1] += Step; } Print("======", TestFunc.GetNamFun(), ", launch counter: ", cnt);
「把优化结果画到画布上看个明白」
上面那段 Print 把极值的坐标和函数值用 16 位精度打出来,maxFuncValue / minFuncValue 配合 xMaxFunc、yMinFunc 等变量,能直接看到算法在这一轮搜到的峰谷位置。 真正落地要看下面这套输入参数和画布初始化。Population_P 默认 50,Test3FuncRuns_P 设到 500 次,意味着第三组测试函数会被反复跑 500 遍来统计收敛表现;NumberRepetTest_P=5 说明每组实验重复 5 次取平均,避免单次随机性误导判断。 画布部分用 CCanvas 开了 750×375 的位图标签,测试函数显示区砍掉 2 像素边距变成 373×373。FunctScrin 是个二维 color 数组,用来把每次评估的函数值映射成像素颜色。开 MT5 把这段 include 和 input 原样贴进 EA,改 Test3FuncRuns_P 到 1000,看右下角终端里 MinFuncValue 是否继续往下走——外汇与贵金属复盘用这类算法搜参数极易过拟合,高风险,仅作离线研究。
Print("MaxFuncValue: ", DoubleToString(maxFuncValue, class="num">16), " X: ", DoubleToString(xMaxFunc, class="num">16), " Y: ", DoubleToString(yMaxFunc, class="num">16)); Print("MinFuncValue: ", DoubleToString(minFuncValue, class="num">16), " X: ", DoubleToString(xMinFunc, class="num">16), " Y: ", DoubleToString(yMinFunc, class="num">16)); class="kw">delete TestFunc; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="macro">#include <Canvas\Canvas.mqh> class="macro">#include <\Math\Functions.mqh> class="macro">#include "AO_RND.mqh" class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— input class="type">int Population_P = class="num">50; input class="type">class="kw">double ArgumentStep_P = class="num">0.0; input class="type">int Test1FuncRuns_P = class="num">1; input class="type">int Test2FuncRuns_P = class="num">20; input class="type">int Test3FuncRuns_P = class="num">500; input class="type">int Measur1FuncValue_P = class="num">1000; input class="type">int Measur2FuncValue_P = class="num">10000; input class="type">int NumberRepetTest_P = class="num">5; input class="type">int RenderSleepMsc_P = class="num">0; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">int WidthMonitor = class="num">750; class=class="str">"cmt">//monitor screen width class="type">int HeighMonitor = class="num">375; class=class="str">"cmt">//monitor screen height class="type">int WidthScrFunc = class="num">375 - class="num">2; class=class="str">"cmt">//test function screen width class="type">int HeighScrFunc = class="num">375 - class="num">2; class=class="str">"cmt">//test function screen height CCanvas Canvas; class=class="str">"cmt">//drawing table C_AO_RND AO; class=class="str">"cmt">//AO object C_Skin SkiF; C_Forest ForF; C_Megacity ChiF; class="kw">struct S_CLR { class="type">class="kw">color clr []; }; S_CLR FunctScrin []; class=class="str">"cmt">//two-dimensional matrix of colors class="type">class="kw">double ScoreAll = class="num">0.0; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void OnStart() { 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, WidthMonitor, HeighMonitor, 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, HeighScrFunc); for (class="type">int i = class="num">0; i < HeighScrFunc; i++) { ArrayResize(FunctScrin [i].clr, HeighScrFunc); } class=class="str">"cmt">//============================================================================ class=class="str">"cmt">//Test Skin###################################################################
◍ 三组函数族的画布比对跑法
这段调用把三个函数对象(SkiF、ForF、ChiF)各自喂给 FuncTests 做可视化验证,每组都跑 Test1~3 三套函数,并用不同颜色区分收敛轨迹:Lime 对应测试一,Aqua 对应测试二,OrangeRed 对应测试三。 SkiF 的已知最优区被锁在 x∈[-3.315699, -3.072485],ForF 落在 [-25.132741228718345, -32.55751918948773],ChiF 则是正向区间 [3.16, 1.990]——这些边界直接写死在参数里,改一处就要同步三行,容易漏。 CanvasErase 负责清黑底并填两块白矩形:左侧 1×1 到 HeighMonitor-2 的竖条,和右侧 HeighMonitor+1 起始的主图区,都是给后续 DrawFunctionGraph 留干净背景。 FuncTests 内部先用 DrawFunctionGraph 画曲线,SendGraphToCanvas 搬上画布,再把理论最优点 (xBest,yBest) 经 Scale 映射到像素坐标,画两个半径 10/11 的黑圈标记,Update 后 Sleep(1000) 停一秒让人眼能追上。 最后一行 Print 输出 ScoreAll/18.0,说明总评分被 18 次运行平摊(3 函数 × 3 测试 × 2 对象族?实际是 3×3×2=18 的计数逻辑),开 MT5 把这段塞进 OnStart 尾段就能直接看三族收敛图差异。外汇与贵金属相关回测高风险,图形结论仅作概率参考。
Print("============================="); CanvasErase(); FuncTests(SkiF, Test1FuncRuns_P, SkiF.GetMinFun(), SkiF.GetMaxFun(), -class="num">3.315699, -class="num">3.072485, clrLime); FuncTests(SkiF, Test2FuncRuns_P, SkiF.GetMinFun(), SkiF.GetMaxFun(), -class="num">3.315699, -class="num">3.072485, clrAqua); FuncTests(SkiF, Test3FuncRuns_P, SkiF.GetMinFun(), SkiF.GetMaxFun(), -class="num">3.315699, -class="num">3.072485, clrOrangeRed); class=class="str">"cmt">//Test Forest################################################################# Print("============================="); CanvasErase(); FuncTests(ForF, Test1FuncRuns_P, ForF.GetMinFun(), ForF.GetMaxFun(), -class="num">25.132741228718345, -class="num">32.55751918948773, clrLime); FuncTests(ForF, Test2FuncRuns_P, ForF.GetMinFun(), ForF.GetMaxFun(), -class="num">25.132741228718345, -class="num">32.55751918948773, clrAqua); FuncTests(ForF, Test3FuncRuns_P, ForF.GetMinFun(), ForF.GetMaxFun(), -class="num">25.132741228718345, -class="num">32.55751918948773, clrOrangeRed); class=class="str">"cmt">//Test Megacity############################################################# Print("============================="); CanvasErase(); FuncTests(ChiF, Test1FuncRuns_P, ChiF.GetMinFun(), ChiF.GetMaxFun(), class="num">3.16, class="num">1.990, clrLime); FuncTests(ChiF, Test2FuncRuns_P, ChiF.GetMinFun(), ChiF.GetMaxFun(), class="num">3.16, class="num">1.990, clrAqua); FuncTests(ChiF, Test3FuncRuns_P, ChiF.GetMinFun(), ChiF.GetMaxFun(), class="num">3.16, class="num">1.990, clrOrangeRed); Print("All score for C_AO_RND: ", ScoreAll / class="num">18.0); } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void CanvasErase() { Canvas.Erase(XRGB(class="num">0, class="num">0, class="num">0)); Canvas.FillRectangle(class="num">1, class="num">1, HeighMonitor - class="num">2, HeighMonitor - class="num">2, COLOR2RGB(clrWhite)); Canvas.FillRectangle(HeighMonitor + class="num">1, class="num">1, WidthMonitor - class="num">2, HeighMonitor - class="num">2, COLOR2RGB(clrWhite)); } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void FuncTests(C_Function &f, class="type">int funcCount, class="type">class="kw">double minFuncVal, class="type">class="kw">double maxFuncVal, class="type">class="kw">double xBest, class="type">class="kw">double yBest, class="type">class="kw">color clrConv) { DrawFunctionGraph(f.GetMinArg(), f.GetMaxArg(), minFuncVal, maxFuncVal, f); SendGraphToCanvas(class="num">1, class="num">1); class="type">int x = (class="type">int)Scale(xBest, f.GetMinArg(), f.GetMaxArg(), class="num">0, WidthScrFunc - class="num">1, class="kw">false); class="type">int y = (class="type">int)Scale(yBest, f.GetMinArg(), f.GetMaxArg(), class="num">0, HeighScrFunc - class="num">1, class="kw">false); Canvas.Circle(x + class="num">1, y + class="num">1, class="num">10, COLOR2RGB(clrBlack)); Canvas.Circle(x + class="num">1, y + class="num">1, class="num">11, COLOR2RGB(clrBlack)); Canvas.Update(); Sleep(class="num">1000); class="type">int xConv = class="num">0.0;
正文
<span class="keyword">int</span> yConv = <span class="number">0.0</span>; <span class="keyword">int</span> EpochCmidl = <span class="number">0</span>; <span class="keyword">int</span> EpochCount = <span class="number">0</span>; <span class="keyword">double</span> aveMid = <span class="number">0.0</span>; <span class="keyword">double</span> aveEnd = <span class="number">0.0</span>; <span class="comment">//----------------------------------------------------------------------------</span> <span class="keyword">for</span> (<span class="keyword">int</span> test = <span class="number">0</span>; test < NumberRepetTest_P; test++) { InitAO (funcCount * <span class="number">2</span>, f.GetMaxArg (), f.GetMinArg (), ArgumentStep_P); EpochCmidl = Measur1FuncValue_P / (<span class="functions">ArraySize</span> (AO.S_Colony)); EpochCount = Measur2FuncValue_P / (<span class="functions">ArraySize</span> (AO.S_Colony)); <span class="comment">// Optimization-------------------------------------------------------------</span> AO.F_EpochReset (); <span class="keyword">for</span> (<span class="keyword">int</span> epochCNT = <span class="number">1</span>; epochCNT <= EpochCount && !<span class="functions">IsStopped</span> (); epochCNT++) &nbs