群体优化算法:智能水滴(IWD)算法·进阶篇
AO算法初始化与首两轮种群布置
这套基于水动力隐喻的优化器,在 Init 里先把随机种子按微秒级时钟重置,避免每次回测种群轨迹雷同。坐标数 coords、种群规模 popSize、扇区数 sectorsNumber 和水黏度 waterViscosity(须 ≥1)是四个外部注入参数,决定解空间切分粒度。 代码里把 riverbed 相关数组按坐标维度和扇区数二级展开,河床深度初始化为 0.0,扇区坐标初值置为 -DBL_MAX,相当于清空上一轮地形记忆。种群个体 p[i] 逐个 Init(coords),保证每个粒子拿到独立坐标容器。 迭代计数器为 0 时,sectorSpace[i] 直接由 (rangeMax[i]-rangeMin[i])/sectorsNumber 算得,把每维搜索区间均分。第 0 和第 1 次迭代走同一段布置逻辑:每个个体随机落进某个扇区,再在扇区子区间内用 SeInDiSp 做离散化采样,并把 fPrev 存为当前适应度。 想在 MT5 里验证,把下面代码段塞进你的 EA 或脚本,改 popSize=30、sectorsNumber=10,跑 EURUSD 的 H1 参数寻优,观察前两轮 p[i].c 的分布是否均匀落在 rangeMin~rangeMax 内。外汇与贵金属杠杆高,回测分布不代表实盘概率。
class="type">void C_AO_IWDm::Init(const class="type">int coordsP, class=class="str">"cmt">//coordinates number const class="type">int popSizeP, class=class="str">"cmt">//population size const class="type">int sectorsNumberP, class=class="str">"cmt">//sectors number const class="type">class="kw">double waterViscosityP) class=class="str">"cmt">//water viscosity(>= class="num">1) { MathSrand((class="type">int)GetMicrosecondCount()); class=class="str">"cmt">// reset of the generator fB = -DBL_MAX; iterationCounter = class="num">0; coords = coordsP; popSize = popSizeP; sectorsNumber = sectorsNumberP; waterViscosity = waterViscosityP; ArrayResize(sectorSpace, coords); ArrayResize(p, popSize); for (class="type">int i = class="num">0; i < popSize; i++) p [i].Init(coords); ArrayResize(rangeMax, coords); ArrayResize(rangeMin, coords); ArrayResize(rangeStep, coords); ArrayResize(cB, coords); ArrayResize(rb, coords); for (class="type">int i = class="num">0; i < coords; i++) { ArrayResize(rb [i].riverbedDepth, sectorsNumber); ArrayResize(rb [i].coordOnSector, sectorsNumber); ArrayInitialize(rb [i].riverbedDepth, class="num">0.0); ArrayInitialize(rb [i].coordOnSector, -DBL_MAX); } } if (iterationCounter == class="num">0) { for (class="type">int i = class="num">0; i < coords; i++) sectorSpace [i] = (rangeMax [i] - rangeMin [i]) / sectorsNumber; } class=class="str">"cmt">//class="num">1,class="num">4 if (iterationCounter == class="num">0 || iterationCounter == class="num">1) { class="type">class="kw">double min = class="num">0.0; class="type">class="kw">double max = class="num">0.0; class="type">int s = class="num">0; for (class="type">int i = class="num">0; i < popSize; i++) { p [i].fPrev = p [i].f; for (class="type">int c = class="num">0; c < coords; c++) { s = (class="type">int)(RNDfromCI(class="num">0, sectorsNumber)); if (s >= sectorsNumber) s = sectorsNumber - class="num">1; p [i].rSection [c] = s; min = rangeMin [c] + sectorSpace [c] * s; max = min + sectorSpace [c]; p [i].c [c] = SeInDiSp(RNDfromCI(min, max), rangeMin [c], rangeMax [c], rangeStep [c]); } } for (class="type">int i = class="num">0; i < popSize; i++) p [i].fPrev = p [i].f; class="kw">return; }
◍ 适应度增量归一化与河床选择逻辑
这段代码在做两件事:先把种群里每个个体的适应度变化量做归一化,再用所谓“河床深度”去引导下一轮坐标采样。外汇与贵金属市场波动剧烈,这类基于随机优化的搜索对参数敏感,实盘前务必在 MT5 策略测试器里跑历史回测确认稳定性。 先初始化最大最小变化量边界,遍历种群用 fabs 取本次与上次适应度的绝对差。若所有个体变化量相等,直接把 altChange 清零避免除零;否则按 (altChange - minAltChange)/(maxAltChange - minAltChange) 压缩到 0~1 区间,这一步让后续选择有统一尺度。 标记 8 的循环里,只要某个体适应度优于上一代,就按其归一化变化量累加到对应坐标维度的 riverbedDepth 数组,相当于“表现好”的扇区被冲刷得更深。标记 9 则是核心采样:随机挑一个个体 n,若它比当前个体好就直接复制其扇区并重新在扇区内均匀采样;否则用随机数 randSC 乘各扇区河床深度,挑出加权最深扇区 indSector 作为新坐标落点。 让小布替你跑这套 把上面循环粘进 EA 的优化例程,把 sectorsNumber 调到 20、popSize 设 50,用 EURUSD 的 M15 数据回测,观察 riverbedDepth 是否集中在少数扇区——若发散说明适应度函数可能没捕捉到价格行为的真实结构。
class="type">class="kw">double maxAltChange = -DBL_MAX; class="type">class="kw">double minAltChange = DBL_MAX; class="type">class="kw">double altChange = class="num">0.0; class="type">class="kw">double randSC = class="num">0.0; class=class="str">"cmt">//random selection component class="type">class="kw">double maxRC = class="num">0.0; class="type">class="kw">double nowRC = class="num">0.0; class="type">int indSector = class="num">0; for (class="type">int i = class="num">0; i < popSize; i++) { altChange = fabs(p [i].f - p [i].fPrev); p [i].altChange = altChange; if (altChange < minAltChange) minAltChange = altChange; if (altChange > maxAltChange) maxAltChange = altChange; } if (minAltChange == maxAltChange) { for (class="type">int i = class="num">0; i < popSize; i++) { p [i].altChange = class="num">0.0; } } else { for (class="type">int i = class="num">0; i < popSize; i++) { altChange = p [i].altChange; p [i].altChange = (altChange - minAltChange) / (maxAltChange - minAltChange); } } class=class="str">"cmt">//class="num">8--------------------------------------------------------------------------- for (class="type">int i = class="num">0; i < popSize; i++) { if (p [i].f > p [i].fPrev) { for (class="type">int c = class="num">0; c < coords; c++) { rb [c].riverbedDepth [p [i].rSection [c]] += p [i].altChange; } } } class=class="str">"cmt">//class="num">9--------------------------------------------------------------------------- for (class="type">int i = class="num">0; i < popSize; i++) { for (class="type">int c = class="num">0; c < coords; c++) { n = (class="type">int)(RNDfromCI(class="num">0, popSize)); if (n >= popSize) n = popSize - class="num">1; if (p [n].f > p [i].f) { p [i].rSection [c] = p [n].rSection [c]; min = rangeMin [c] + sectorSpace [c] * p [i].rSection [c]; max = min + sectorSpace [c]; p [i].c [c] = SeInDiSp(RNDfromCI(min, max), rangeMin [c], rangeMax [c], rangeStep [c]); } else { randSC = RNDfromCI(class="num">0.0, class="num">1.0); nowRC = rb [c].riverbedDepth [class="num">0] * randSC; maxRC = nowRC; indSector = class="num">0; for (class="type">int r = class="num">1; r < sectorsNumber; r++) { nowRC = rb [c].riverbedDepth [r] * randSC; if (nowRC > maxRC) { maxRC = nowRC; indSector = r; } } if (rb [c].coordOnSector [indSector] == -DBL_MAX) { min = rangeMin [c] + sectorSpace [c] * indSector; max = min + sectorSpace [c]; p [i].c [c] = RNDfromCI(min, max); } else {
「适应度回写与河道坐标修订」
种群迭代收尾时,先跑一遍循环把当前适应度写进历史字段:若 p[i].f 大于 p[i].fPrev 就覆盖,保证后续比较用的是最新一代的成绩。这一步没有阈值判断,纯粹是状态同步,避免在下一轮变异时误用陈旧数据。 Revision() 负责把最优个体沉淀进全局河道。当某个体的适应度 p[i].f 超过全局最优 fB,就更新 fB,并用 ArrayCopy 把它的坐标数组 cB 整段拷入,同时把每个维度的落点写回 rb[c].coordOnSector 对应扇区。若没破纪录,则仅在该扇区坐标仍是 -DBL_MAX(未被占据)时补入当前值,等于给空扇区兜底。 迭代计数器 iterationCounter 在每次 Revision 末尾自增 1,这是外部调度器判断是否需要触发重新初始化或停机的唯一依据。在 MT5 里把 popSize 设到 200 以上时,你能明显看到 fB 在前 30 代内收敛速度最快,之后单代提升常小于 1e-4,此时可凭 iterationCounter 切到局部精细搜索。 别让空扇区一直饿着 如果某个扇区的 coordOnSector 长期是 -DBL_MAX,说明该维度采样盲区,Revision 里的 else 分支就是专门填坑的。调试时打印各扇区占位率,低于 60% 就要调大 sectorSpace 或降低 waterViscosity。
class="type">class="kw">double x = RNDfromCI(-class="num">1.0, class="num">1.0); class="type">class="kw">double y = x * x; class="type">class="kw">double pit = class="num">0.0; class="type">class="kw">double dif = Scale(y, class="num">0.0, class="num">1.0, class="num">0.0, sectorSpace [c] * waterViscosity, class="kw">false); pit = rb [c].coordOnSector [indSector]; pit += x > class="num">0.0 ? dif : -dif; p [i].c [c] = pit; } min = rangeMin [c] + sectorSpace [c] * indSector; max = min + sectorSpace [c]; p [i].c [c] = SeInDiSp(p [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); p [i].rSection [c] = indSector; } } } for (class="type">int i = class="num">0; i < popSize; i++) { if (p [i].f > p [i].fPrev) p [i].fPrev = p [i].f; } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_IWDm::Revision() { class=class="str">"cmt">//class="num">3,class="num">6,class="num">11---------------------------------------------------------------------- for (class="type">int i = class="num">0; i < popSize; i++) { if (p [i].f > fB) { fB = p [i].f; ArrayCopy(cB, p [i].c, class="num">0, class="num">0, WHOLE_ARRAY); for (class="type">int c = class="num">0; c < coords; c++) { rb [c].coordOnSector [p [i].rSection [c]] = p [i].c [c]; } } else { for (class="type">int c = class="num">0; c < coords; c++) { if (rb [c].coordOnSector [p [i].rSection [c]] == -DBL_MAX) { rb [c].coordOnSector [p [i].rSection [c]] = p [i].c [c]; } } } } iterationCounter++; } class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
SDSm 里那道 Research 改写逻辑
IWDm 跑出的结果让人想到顺手改一版 SDS——评级里原本最稳的那个。两者都围着「区域 / 餐馆」这类实体转,IWDm 用四元概率分布做位置细化,这套思路挪到 SDS 上,最终测试表现被明显拉动。改动后餐馆内新坐标不再均匀撒点,分布形态和测试结果都变了,于是标了 m。 SDSm 每次迭代尝试改三类坐标:当前最优候选的坐标、随机挑一家餐馆里存着的最优坐标(类似 IWDm 的河床,相当于城市里所有餐馆最佳菜肴的清单)、以及自己对应餐馆的原坐标。概率分布宽度比我没做成外部参数,按当前写死的值更顺手。 核心就是 Research 函数,旧版测试领跑者经它一改,指标提升超过 12%。它先吐一个 [-1.0, 1.0] 的随机数,按餐馆尺寸放大,叠到待改坐标上,再卡边界、按优化步长修剪成可接受值。 下面这段 Revision 是落点。若随机选出的 n 号候选历史适应度更好,就抄它的餐馆地址并用 0.25 宽度比做 Research;否则按 probabRest 概率跳去随机餐馆用 1.0 宽度比细化,不跳就守着自己上一轮地址用 0.25 细化。
class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_SDSm::Revision() { class=class="str">"cmt">/* here is the old code, no changes */ for (class="type">int i = class="num">0; i < populationSize; i++) { for (class="type">int c = class="num">0; c < coords; c++) { n = (class="type">int)(RNDfromCI(class="num">0, populationSize)); if (n >= populationSize) n = populationSize - class="num">1; if (cands [n].fPrev > cands [i].fPrev) { cands [i].raddr [c] = cands [n].raddrPrev [c]; Research(class="num">0.25, cands [i].raddr [c], restSpace [c], rangeMin [c], rangeStep [c], cands [n].cPrev [c], cands [i].c [c]); } else { rnd = RNDfromCI(class="num">0.0, class="num">1.0); if (rnd < probabRest) { n = (class="type">int)(RNDfromCI(class="num">0, restNumb)); if (n >= restNumb) n = restNumb - class="num">1; cands [i].raddr [c] = n; Research(class="num">1.0, cands [i].raddr [c], restSpace [c], rangeMin [c], rangeStep [c], rb [c].coordOnSector [n], cands [i].c [c]); } else { cands [i].raddr [c] = cands [i].raddrPrev [c]; Research(class="num">0.25, cands [i].raddr [c], restSpace [c], rangeMin [c], rangeStep [c], cands [i].cPrev [c], cands [i].c [c]);