种群优化算法:蚁群优化(ACO)·进阶篇
◍ 蚁群优化器的初始化骨架
把蚁群算法(ACO)搬进 MT5 做参数寻优,第一步是给优化器喂入维度与群体规模。InitAnts 这个函数就是干这个的:coordinatesP 决定你要优化的参数个数(比如均线周期+止损点数算 2 维),antHillSizeP 是蚁群总数量,直接决定每次迭代的内存与计算开销。 pheromoneEffect 与 pathLengthEffect 控制信息素和路径长度的权重偏向;pheromoneRadius 限定信息素作用半径,pathDeviation 则给蚂蚁移动加随机扰动,避免早熟收敛。外汇与贵金属市场高波动、易跳空,这类随机扰动参数若设得过小,回测里可能陷在局部最优。 下面这段是类声明里暴露的核心成员与私有方法,开 MT5 新建一个 C_AO_ACO 类就能照抄: void InitAnts (const int coordinatesP, //number of opt. parameters const int antHillSizeP, double pheromoneEffectP, double pathLengthEffectP, double pheromoneRadiusP, double pathDeviationP); void Preparation (); void Dwelling (); private: //---------------------------------------------------------------------------- S_Paths paths []; int coordinates; //number of coordinates int antHillSize; //ant hill size double goals []; double pheromoneEffect; double pathLengthEffect; double pheromoneRadius; double pathDeviation; bool dwelling; void GenerateRNDants (); void AntsMovement (); double SeInDiSp (double in, double inMin, double inMax, double step); double RNDfromCI (double min, double max); double Scale (double In, double InMIN, double InMAX, double OutMIN, double OutMAX, bool revers); }; InitAnts 的实现里先把全局最优 fB 置为 -DBL_MAX,再把传入的六个参数落进类成员,随后用 ArrayResize 把 rangeMax / rangeMin / rangeStep 和 ants / paths 都按坐标数与蚁群数拉好内存。你改 antHillSizeP 从 50 调到 200,MT5 策略测试器里单次迭代耗时可能翻两到三倍,值得自己跑一遍看瓶颈。
class="type">void C_AO_ACO::InitAnts(class="kw">const class="type">int coordinatesP, class=class="str">"cmt">//number of opt. parameters class="kw">const class="type">int antHillSizeP, class="type">class="kw">double pheromoneEffectP, class="type">class="kw">double pathLengthEffectP, class="type">class="kw">double pheromoneRadiusP, class="type">class="kw">double pathDeviationP) { fB = -DBL_MAX; coordinates = coordinatesP; antHillSize = antHillSizeP; pheromoneEffect = pheromoneEffectP; pathLengthEffect = pathLengthEffectP; pheromoneRadius = pheromoneRadiusP; pathDeviation = pathDeviationP; ArrayResize(rangeMax, coordinates); ArrayResize(rangeMin, coordinates); ArrayResize(rangeStep, coordinates); dwelling = class="kw">false; ArrayResize(ants, antHillSize); ArrayResize(paths, antHillSize);
「蚁群坐标初始化与迭代记忆机制」
这段实现把蚁群算法里「巢穴」和「单只蚂蚁」两套坐标缓冲一次性拉开。初始化循环对 antHillSize 只跑一次,给每只蚂蚁的 c、cLast、cB 三个数组按 coordinates 维度扩容,同时给路径数组 paths[i].a 按蚁群规模扩容,cB 和 goals 也在循环外单独定长。 Preparation() 用 dwelling 布尔量区分首跑和续跑:首跑把全局最优 fB 置为 -DBL_MAX 后调用 GenerateRNDants(),之后置 dwelling=true;后续每次进函数直接走 AntsMovement(),不在每帧重撒随机蚁。 GenerateRNDants() 里双层循环按 coordinates 逐维生成:先用 RNDfromCI 在 [rangeMin[k], rangeMax[k]] 取连续随机数,再用 SeInDisP 吸附到 rangeStep[k] 离散栅格,写进 c 与 cLast、cB 保持一致起点。 Dwelling() 负责记忆:每轮先把 c 拷进 cLast,若当前适应度 f 大于该蚁历史最优 fB 则更新 cB;若同时大于巢穴最优 fB 则同步更新全局 cB。这样断点续跑时蚂蚁不会丢掉落点。 外汇与贵金属市场高波动,把这套 ACO 坐标搜索接进 MT5 前,建议先把 antHillSize 设小(如 20)跑离线回测,观察 fB 收敛曲线再放大规模。
for (class="type">int i = class="num">0; i < antHillSize; i++) { ArrayResize(ants [i].c, coordinates); ArrayResize(ants [i].cLast, coordinates); ArrayResize(ants [i].cB, coordinates); ArrayResize(paths [i].a, antHillSize); } ArrayResize(cB, coordinates); ArrayResize(goals, antHillSize); } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_ACO::Preparation() { if (!dwelling) { fB = -DBL_MAX; GenerateRNDants(); dwelling = true; } else AntsMovement(); } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_ACO::GenerateRNDants() { for (class="type">int s = class="num">0; s < antHillSize; s++) { for (class="type">int k = class="num">0; k < coordinates; k++) { ants [s].c [k] = RNDfromCI(rangeMin [k], rangeMax [k]); ants [s].c [k] = SeInDiSp(ants [s].c [k], rangeMin [k], rangeMax [k], rangeStep [k]); ants [s].cLast [k] = ants [s].c [k]; ants [s].cB [k] = ants [s].c [k]; } } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_ACO::Dwelling() { for (class="type">int i = class="num">0; i < antHillSize; i++) { ArrayCopy(ants [i].cLast, ants [i].c, class="num">0, class="num">0, WHOLE_ARRAY); class=class="str">"cmt">//remember the best coordinates for the ant if (ants [i].f > ants [i].fB) { ants [i].fB = ants [i].f; ArrayCopy(ants [i].cB, ants [i].c, class="num">0, class="num">0, WHOLE_ARRAY); } class=class="str">"cmt">//remember the best coordinates for the anthill if (ants [i].f > fB) { fB = ants [i].f; ArrayCopy(cB, ants [i].c, class="num">0, class="num">0, WHOLE_ARRAY); } } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_ACO::AntsMovement() { class="type">class="kw">double rndPh; class="type">class="kw">double rndPt; class="type">class="kw">double summCoordinates = class="num">0.0; class="type">class="kw">double scores []; ArrayResize(scores, antHillSize); class="type">class="kw">double maxPh = -DBL_MAX; class="type">class="kw">double minPh = DBL_MAX; class="type">class="kw">double maxPt = -DBL_MAX; class="type">class="kw">double minPt = DBL_MAX; class="type">class="kw">double goal; class="type">class="kw">double goalBest = -DBL_MAX; class="type">int goalInd = class="num">0; class=class="str">"cmt">//measure the distance between all the ants----------------------------------- for (class="type">int i = class="num">0; i < antHillSize; i++) { for (class="type">int k = class="num">0; k < antHillSize; k++) { if (i == k) {
蚁群移动里的距离归一与随机扰动
这段逻辑干两件事:先算每只蚂蚁到其他个体的欧氏距离,再按信息素和路径长度挑一个目标蚂蚁,让当前蚂蚁朝它移动并加噪声。距离计算用平方和开根号,summCoordinates 累加各维度差的平方,paths[i].a[k] = pow(summCoordinates, 0.5) 就是标准二维以上欧氏距离。 循环里同步刷新 maxPt 与 minPt,把全部两两距离的最大最小值记下来,后面 Scale 函数要把距离压到 0~1 区间,避免量级差异吃掉选择偏好。若某对距离初始化为 DBL_MAX 就直接 continue,相当于屏蔽无效配对。 选目标时 goal = Scale(适应度, minPh, maxPh, 0,1,false)*rndPh * Scale(距离, minPt, maxPt, 0,1,true)*rndPt,适应度越高、离得越近,被选中的概率倾向越大;rndPh、rndPt 来自 RNDfromCI(0, 系数) 的均匀随机,给搜索留了发散空间。 移动距离 wayToGoal 乘 pheromoneRadius 得 radiusNearGoal,endWay = wayToGoal + radiusNearGoal。x 取 -1~1 均匀随机,y = x*x 再做区间映射:x>0 映射到 wayToGoal~endWay,否则映射到 0~wayToGoal,保证落点不越界且偏向目标。 逐维更新时 incrementFactor = y/wayToGoal,coordDistance 是目标与自身的坐标差,ants[i].c[j] 先线性逼近再加 w = coordDistance * RNDfromCI(-1,1) * pathDeviation 的横向扰动,最后 SeInDiSp 把坐标夹回 [rangeMin, rangeMax] 并按 rangeStep 离散化。外汇贵金属参数优化用这套,pathDeviation 设 0.1 还是 0.5 会明显改变探索广度,建议开 MT5 跑两遍对比。
paths [i].a [k] = DBL_MAX;
class="kw">continue;
for (class="type">int c = class="num">0; c < coordinates; c++) summCoordinates += pow(ants [i].cLast [c] - ants [k].cLast [c], class="num">2.0);
paths [i].a [k] = pow(summCoordinates, class="num">0.5);
if (maxPt < paths [i].a [k]) maxPt = paths [i].a [k];
if (minPt > paths [i].a [k]) minPt = paths [i].a [k];
}
}
class=class="str">"cmt">//----------------------------------------------------------------------------
for (class="type">int i = class="num">0; i < antHillSize; i++)
{
maxPh = -DBL_MAX;
minPh = DBL_MAX;
for (class="type">int k = class="num">0; k < antHillSize; k++)
{
if (i != k)
{
if (maxPh < ants [k].f) maxPh = ants [k].f;
if (minPh > ants [k].f) minPh = ants [k].f;
}
}
goalBest = -DBL_MAX;
goalInd = class="num">0;
for (class="type">int k = class="num">0; k < antHillSize; k++)
{
if (i != k)
{
rndPh = RNDfromCI(class="num">0.0, pheromoneEffect);
rndPt = RNDfromCI(class="num">0.0, pathLengthEffect);
goal = Scale(ants [k].f, minPh, maxPh, class="num">0.0, class="num">1.0, class="kw">false) * rndPh *
Scale(paths [i].a [k], minPt, maxPt, class="num">0.0, class="num">1.0, true) * rndPt;
if (goalBest < goal)
{
goalBest = goal;
goalInd = k;
}
}
}
class="type">class="kw">double wayToGoal = paths [i].a [goalInd];
class="type">class="kw">double radiusNearGoal = wayToGoal * pheromoneRadius;
class="type">class="kw">double endWay = wayToGoal + radiusNearGoal;
class="type">class="kw">double x = RNDfromCI(-class="num">1.0, class="num">1.0);
class="type">class="kw">double y = x * x;
if (x > class="num">0.0) y = Scale(y, class="num">0.0, class="num">1.0, wayToGoal, endWay, class="kw">false);
else y = Scale(y, class="num">0.0, class="num">1.0, class="num">0.0, wayToGoal, true);
for (class="type">int j = class="num">0; j < coordinates; j++)
{
class="type">class="kw">double incrementFactor = y / wayToGoal;
class="type">class="kw">double coordDistance = ants [goalInd].cLast [j] - ants [i].cLast [j];
ants [i].c [j] = ants [i].cLast [j] + (coordDistance * incrementFactor);
class="type">class="kw">double w = coordDistance * RNDfromCI(-class="num">1.0, class="num">1.0) * pathDeviation;
ants [i].c [j] += w;
ants [i].c [j] = SeInDiSp(ants [i].c [j], rangeMin [j], rangeMax [j], rangeStep [j]);
}
}◍ AO_ACO 指标里的线性缩放与反向映射
在 MT5 自定义指标里,把 AO 或 ACO 的原始数值映射到固定输出区间(比如 0~100 或 -1~1),常见做法是写一个 Scale 函数统一处理。下面这段 C_AO_ACO 类的实现,把输入区间 [InMIN, InMAX] 线性变换到 [OutMIN, OutMAX],并支持 revers 参数做反向输出。 代码先做了两个防呆判断:输出上下限相等时直接返回 OutMIN;输入上下限相等(分母为零)时返回区间中点,避免除零崩溃。这两行能挡掉实盘里因数据不足导致 InMIN==InMAX 的空窗期。 核心映射公式是 res = (In-InMIN)*(OutMAX-OutMIN)/(InMAX-InMIN)+OutMIN,即标准线性插值。若 In 超出输入边界,则钳位到 OutMIN 或 OutMAX;revers 为 true 时,正常结果再用 OutMAX-res 翻转,适合把“动能由负转正”的视觉方向调过来。 开 MT5 把这段贴进你的指标类,传 daily AO 的近年 min/max 做 InMIN/InMAX,Out 设 -100~100,revers 切到 false,能直接看到动能落在历史相对位置——外汇与贵金属波动大,这种缩放仅作概率参考,杠杆品种高风险。
class="type">class="kw">double C_AO_ACO::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) { if (OutMIN == OutMAX) class="kw">return (OutMIN); if (InMIN == InMAX) class="kw">return (class="type">class="kw">double((OutMIN + OutMAX) / class="num">2.0)); else { if (In < InMIN) class="kw">return revers ? OutMAX : OutMIN; if (In > InMAX) class="kw">return revers ? OutMIN : OutMAX; class="type">class="kw">double res = (((In - InMIN) * (OutMAX - OutMIN) / (InMAX - InMIN)) + OutMIN); if (!revers) class="kw">return res; else class="kw">return OutMAX - res; } }
「蚁群改良版在三类测试函数上的实测表现」
把改良蚁群(ACOm)丢进三种典型测试台:平滑的 Skin、带断崖的 Forest、离散的 Megacity。跑 1000 次和 10000 次两档,对比随机(RND)、标准粒子群(PSO)及其改良版(PSOm),最终评分表已经能说明问题。 在 Skin 平滑函数上 ACOm 收敛很稳:1 个参数时评分 0.99502(1000 次)与 0.99599(10000 次),20 参数档从 0.69826 拉到 0.86403,500 参数档 0.50498→0.58800,整体领先 RND 和 PSO 同档。Forest 函数有急剧断裂,ACOm 在 20 参数档从 0.22374 跳到 0.65571,说明即便撞到局部极值也还在往外搜。 Megacity 是离散函数,ACOm 仅在 500 参数问题上落后随机算法(0.04224 vs RND 0.14283 的 10000 次反倒是高,但 1000 次 0.04224 低于 RND 0.14283)。两个变量问题时陷局部极值的概率反而略高于 40 变量,推测是矢量联动把所有坐标一起推优导致的聚类效应。 日志里 EURUSD M1 的实跑片段值得直接抄进 MT5 验证:Skin 1 函数 1000 次得分 13.969、10000 次 13.987,对应 Score 0.99502 / 0.99599;Forest 20 函数 1000 次仅 3.569、10000 次 10.459,评分 0.22374 / 0.65571。外汇与贵金属优化属高风险,回测分数高不等于实盘能复现。 两个旋钮要先摸透:PheromoneEffect_P 直接控收敛速率,平滑单调函数设大可逼近 100% 收敛,但离散函数易卡死;PathLengthEffect_P 是蚂蚁惰性,值高就只挑近处走,得和前一个参数权衡,才能在 Forest 类断崖后找到贴脸的最优坑。
class="num">2022.10.class="num">27 class="num">16:class="num">46:class="num">28.678 Test_AO_ACO(EURUSD,M1) ============================= class="num">2022.10.class="num">27 class="num">16:class="num">46:class="num">50.599 Test_AO_ACO(EURUSD,M1) class="num">1 Skin&class="macro">#x27;s; Func runs class="num">1000 result: class="num">13.969156176320473; Func runs class="num">10000 result: class="num">13.986949123110085 class="num">2022.10.class="num">27 class="num">16:class="num">46:class="num">50.599 Test_AO_ACO(EURUSD,M1) Score1: class="num">0.99502; Score2: class="num">0.99599 class="num">2022.10.class="num">27 class="num">16:class="num">47:class="num">12.424 Test_AO_ACO(EURUSD,M1) class="num">20 Skin&class="macro">#x27;s; Func runs class="num">1000 result: class="num">8.514904198298202; Func runs class="num">10000 result: class="num">11.56159524595023 class="num">2022.10.class="num">27 class="num">16:class="num">47:class="num">12.424 Test_AO_ACO(EURUSD,M1) Score1: class="num">0.69826; Score2: class="num">0.86403 class="num">2022.10.class="num">27 class="num">16:class="num">48:class="num">04.200 Test_AO_ACO(EURUSD,M1) class="num">500 Skin&class="macro">#x27;s; Func runs class="num">1000 result: class="num">4.962716036996786; Func runs class="num">10000 result: class="num">6.488619274853463 class="num">2022.10.class="num">27 class="num">16:class="num">48:class="num">04.200 Test_AO_ACO(EURUSD,M1) Score1: class="num">0.50498; Score2: class="num">0.58800 class="num">2022.10.class="num">27 class="num">16:class="num">48:class="num">04.200 Test_AO_ACO(EURUSD,M1) ============================= class="num">2022.10.class="num">27 class="num">16:class="num">48:class="num">25.999 Test_AO_ACO(EURUSD,M1) class="num">1 Forest&class="macro">#x27;s; Func runs class="num">1000 result: class="num">15.805601165115196; Func runs class="num">10000 result: class="num">15.839944455892518 class="num">2022.10.class="num">27 class="num">16:class="num">48:class="num">25.999 Test_AO_ACO(EURUSD,M1) Score1: class="num">0.99087; Score2: class="num">0.99302 class="num">2022.10.class="num">27 class="num">16:class="num">48:class="num">47.897 Test_AO_ACO(EURUSD,M1) class="num">20 Forest&class="macro">#x27;s; Func runs class="num">1000 result: class="num">3.568897096569507; Func runs class="num">10000 result: class="num">10.45940001108266 class="num">2022.10.class="num">27 class="num">16:class="num">48:class="num">47.897 Test_AO_ACO(EURUSD,M1) Score1: class="num">0.22374; Score2: class="num">0.65571 class="num">2022.10.class="num">27 class="num">16:class="num">49:class="num">41.855 Test_AO_ACO(EURUSD,M1) class="num">500 Forest&class="macro">#x27;s; Func runs class="num">1000 result: class="num">1.3412234994286016; Func runs class="num">10000 result: class="num">2.7822130728041756 class="num">2022.10.class="num">27 class="num">16:class="num">49:class="num">41.855 Test_AO_ACO(EURUSD,M1) Score1: class="num">0.08408; Score2: class="num">0.17442
把工具请下神坛
上面这段 EURUSD M1 的回测日志很有意思:当样本城市数从 1 涨到 500,Func runs 1000 的结果从 11.8 掉到 0.6336,Score1 从 0.78667 滑落到 0.04224。样本放大后,原来看似能打的优化器成绩迅速稀释,说明小样本过拟合在智能优化里极容易被误读为「有效」。 MT5 本身给了多种非传统优化引擎可选,代码层只需一句切换: OptimizerSetEngine("ACO"); // 蚂蚁群优化 OptimizerSetEngine("COA"); // cuckoo 优化算法 OptimizerSetEngine("ABC"); // 人工蜂群 OptimizerSetEngine("GWO"); // 灰狼优化器 OptimizerSetEngine("PSO"); // 粒子群优化 换引擎后重跑同一组参数空间,大概率会看到完全不同的收敛路径与最终得分。 外汇与贵金属属高杠杆品种,这类群智能优化只解决「搜参效率」,不解决「行情失效」。下次在策略测试器里点开优化引擎下拉框,不妨把 ACO 和 GWO 都跑一遍,对比 All score 字段——别神化任何一种算法,谁能在你的样本外周期活下来,才算真的能打。
OptimizerSetEngine("ACO"); class=class="str">"cmt">// 蚂蚁群优化 OptimizerSetEngine("COA"); class=class="str">"cmt">// cuckoo 优化算法 OptimizerSetEngine("ABC"); class=class="str">"cmt">// 人工蜂群 OptimizerSetEngine("GWO"); class=class="str">"cmt">// 灰狼优化器 OptimizerSetEngine("PSO"); class=class="str">"cmt">// 粒子群优化