种群优化算法:社群进化(ESG)·进阶篇
(2/3)·单种群跑不出的多峰解,靠多个独立社群并行探索与经验交换才可能拿下
粒子群初始化时的分组与余数摊派
这段 C_AO_ESG::Init 做的是进化群算法的启动配置:把总种群按 groups 拆成多个子群,并给每个坐标维度预留 rangeMax / rangeMin / rangeStep 数组。 核心分摊逻辑在 lost 变量:particles = popSize / groups 先算出每群基础人数,lost = popSize - particles * groups 是除不尽的余数。若 lost>0,就用 while 循环从 pos=0 起逐个子群 +1,绕回 groups 归零,直到 lost 消为零。 随机数种子用 MathSrand((int)GetMicrosecondCount()) 重置,避免每次回测群体轨迹雷同;fB 初始化为 -DBL_MAX 代表尚未记录全局最优适应度。外汇与贵金属品种上跑这套,滑点与点差会显著干扰适应度评估,属于高风险验证环境。 开 MT5 把这段代码贴进 EA 的 Init,故意设 popSize=53、groups=5,就能看到 lost=3 被摊进前 3 个子群,每群变 11/11/11/10/10,可直接在监视窗口核对 partInSwarms。
class="kw">private: 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="kw">private: 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 power); }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_ESG::Init(const class="type">int coordinatesNumberP, class=class="str">"cmt">//coordinates number const class="type">int populationSizeP, class=class="str">"cmt">//population size const class="type">int groupsP, class=class="str">"cmt">//number of groups const class="type">class="kw">double groupRadiusP, class=class="str">"cmt">//group radius const class="type">class="kw">double expansionRatioP, class=class="str">"cmt">//expansion ratio const class="type">class="kw">double powerP) { MathSrand((class="type">int)GetMicrosecondCount()); class=class="str">"cmt">// reset of the generator fB = -DBL_MAX; revision = false; coords = coordinatesNumberP; popSize = populationSizeP; groups = groupsP; groupRadius = groupRadiusP; expansionRatio = expansionRatioP; power = powerP; class=class="str">"cmt">//---------------------------------------------------------------------------- class="type">int partInSwarms []; ArrayResize(partInSwarms, groups); class="type">int particles = popSize / groups; ArrayInitialize(partInSwarms, particles); class="type">int lost = popSize - particles * groups; if (lost > class="num">0) { class="type">int pos = class="num">0; class="kw">while (true) { partInSwarms [pos]++; lost--; pos++; if (pos >= groups) pos = class="num">0; if (lost == class="num">0) break; } } class=class="str">"cmt">//---------------------------------------------------------------------------- ArrayResize(rangeMax, coords); ArrayResize(rangeMin, coords); ArrayResize(rangeStep, coords); ArrayResize(cB, coords); ArrayResize(gr, groups); for (class="type">int s = class="num">0; s < groups; s++) gr [s].Init(coords, partInSwarms [s]); ArrayResize(a, popSize); for (class="type">int i = class="num">0; i < popSize; i++) a [i].Init(coords); }
「群体初始化与迭代修正的双段逻辑」
在 MT5 里跑这套进化式参数搜索,第一步是 Moving() 把群体空间铺开。它只在 revision 为 false 时执行一次:先给每个 group 定下圆心 cB 与搜索半径 sRadius,再按 PowerDistribution 在半径内撒个体。半径默认取 (rangeMax-rangeMin)*groupRadius,越界就截断到参数边界,这一步直接决定后续寻优会不会漏掉边角解。 Revision() 则是每代必跑的修正环。它先扫一遍 popSize 个个体,把适应度 a[i].f 大于全局最优 fB 的拎出来,用 ArrayCopy 把坐标写进 cB;随后逐组比对,若组内有人超过 gr[s].fB 就更新组最优。关键在末行:整组一代没进步,sRadius 就乘 expansionRatio 扩张——外汇与贵金属波动率高,这组扩张系数设大了可能过早发散,设小了则容易卡在局部,建议先在 EURUSD 的 M15 上用 groupRadius=0.1、expansionRatio=1.2 手测。 两个函数靠 revision 标志位切换,初始化完置 true 后 Moving 不再触发,所有算力留给 Revision 做代际逼近。想验证的话,直接把下面代码挂进 EA 的 OnTick 前段,打印 gr[0].sRadius 就能看到无改进时半径逐代放大的轨迹。
class="type">void C_AO_ESG::Moving() { if (!revision) { class="type">int cnt = class="num">0; class="type">class="kw">double coordinate = class="num">0.0; class="type">class="kw">double radius = class="num">0.0; class="type">class="kw">double min = class="num">0.0; class="type">class="kw">double max = class="num">0.0; class=class="str">"cmt">//generate centers------------------------------------------------------ for (class="type">int s = class="num">0; s < groups; s++) { gr [s].sRadius = groupRadius; for (class="type">int c = class="num">0; c < coords; c++) { coordinate = RNDfromCI(rangeMin [c], rangeMax [c]); gr [s].cB [c] = SeInDiSp(coordinate, rangeMin [c], rangeMax [c], rangeStep [c]); } } class=class="str">"cmt">//generate individuals of groups-------------------------------------------- for (class="type">int s = class="num">0; s < groups; s++) { for (class="type">int p = class="num">0; p < gr [s].sSize; p++) { for (class="type">int c = class="num">0; c < coords; c++) { radius = (rangeMax [c] - rangeMin [c]) * gr [s].sRadius; min = gr [s].cB [c] - radius; max = gr [s].cB [c] + radius; if (min < rangeMin [c]) min = rangeMin [c]; if (max > rangeMax [c]) max = rangeMax [c]; coordinate = PowerDistribution(gr [s].cB [c], min, max, power); a [cnt].c [c] = SeInDiSp(coordinate, rangeMin [c], rangeMax [c], rangeStep [c]); } cnt++; } } revision = true; } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_ESG::Revision() { class=class="str">"cmt">//update the best global solution for (class="type">int i = class="num">0; i < popSize; i++) { if (a [i].f > fB) { fB = a [i].f; ArrayCopy(cB, a [i].c, class="num">0, class="num">0, WHOLE_ARRAY); } } class="type">int cnt = class="num">0; class="type">bool impr = false; for (class="type">int s = class="num">0; s < groups; s++) { impr = false; for (class="type">int p = class="num">0; p < gr [s].sSize; p++) { if (a [cnt].f > gr [s].fB) { gr [s].fB = a [cnt].f; ArrayCopy(gr [s].cB, a [cnt].c, class="num">0, class="num">0, WHOLE_ARRAY); impr = true; } cnt++; } if (!impr) gr [s].sRadius *= expansionRatio;
◍ 群体采样与经验交换的落地实现
上面的片段承接了分组初始化之后两件具体的事:按组半径撒点生成个体,以及做跨组经验借用。注意组半径被硬截断在 0.5——这意味着任一维度的搜索跨度最多占到该维全范围的二分之一,避免单组过早垄断解空间。 生成个体时,对每个坐标先按 (rangeMax-rangeMin)*sRadius 算出半径,再以组中心 cB 为基准向两侧取 min/max,越界就夹回全局范围。坐标本身不是均匀撒的,而是走 PowerDistribution(...) 再经 SeInDiSp(...) 离散化,所以密度会偏向中心,外围样本偏稀。 经验交换那段更有意思:posSw 用 RNDfromCI(0, groups) 取整,越界就强制 groups-1,然后直接把 a[cnt].c[c] 覆盖成 gr[posSw].cB[c]。注释里那行按适应度比较被删了,等于无条件照搬别组中心,属于激进信息共享,实盘调参时建议把判断条件加回来,否则可能让已收敛的组被随机带偏。 S_Agent 结构里 c 与 cMain 分开存,f/fMain 初值给 -DBL_MAX,说明每代都重算适应度,不继承历史最优。Revision() 开头那段就是扫一遍种群,遇到 a[i].f > fB 就更新全局最优并 ArrayCopy 坐标,逻辑直白但依赖 f 已被外层算好。 外汇与贵金属波动剧烈,这类群体搜索若直接接实盘信号,参数敏感性高,请先在 MT5 策略测试器跑历史数据验证稳定性。
else gr [s].sRadius = groupRadius; if (gr [s].sRadius > class="num">0.5) gr [s].sRadius = class="num">0.5; } class=class="str">"cmt">//generate individuals of groups---------------------------------------------- class="type">class="kw">double coordinate = class="num">0.0; class="type">class="kw">double radius = class="num">0.0; class="type">class="kw">double min = class="num">0.0; class="type">class="kw">double max = class="num">0.0; cnt = class="num">0; for (class="type">int s = class="num">0; s < groups; s++) { for (class="type">int p = class="num">0; p < gr [s].sSize; p++) { for (class="type">int c = class="num">0; c < coords; c++) { if (RNDfromCI(class="num">0.0, class="num">1.0) < class="num">1.0) { radius = (rangeMax [c] - rangeMin [c]) * gr [s].sRadius; min = gr [s].cB [c] - radius; max = gr [s].cB [c] + radius; if (min < rangeMin [c]) min = rangeMin [c]; if (max > rangeMax [c]) max = rangeMax [c]; coordinate = PowerDistribution(gr [s].cB [c], min, max, power); a [cnt].c [c] = SeInDiSp(coordinate, rangeMin [c], rangeMax [c], rangeStep [c]); } } cnt++; } } class=class="str">"cmt">//exchange of experience---------------------------------------------------------------- cnt = class="num">0; for (class="type">int s = class="num">0; s < groups; s++) { for (class="type">int c = class="num">0; c < coords; c++) { class="type">int posSw = (class="type">int)RNDfromCI(class="num">0, groups); if (posSw >= groups) posSw = groups - class="num">1; class=class="str">"cmt">//if (sw [posSw].fB > sw [s].fB) { a [cnt].c [c] = gr [posSw].cB [c]; } } cnt += gr [s].sSize; } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="kw">struct S_Agent { class="type">void Init(const class="type">int coords) { ArrayResize(c, coords); ArrayResize(cMain, coords); f = -DBL_MAX; fMain = -DBL_MAX; } class="type">class="kw">double c []; class=class="str">"cmt">//coordinates class="type">class="kw">double cMain []; class=class="str">"cmt">//coordinates class="type">class="kw">double f; class=class="str">"cmt">//fitness class="type">class="kw">double fMain; class=class="str">"cmt">//fitness }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_ESG::Revision() { class=class="str">"cmt">//---------------------------------------------------------------------------- class=class="str">"cmt">//Update the best global solution for (class="type">int i = class="num">0; i < popSize; i++) { if (a [i].f > fB) { fB = a [i].f; ArrayCopy(cB, a [i].c, class="num">0, class="num">0, WHOLE_ARRAY);
群体半径与经验交换的迭代逻辑
这段代码是群体智能优化里每代收尾的两件事:先拿本次适应度刷新个体与族群的最优记忆,再据此重算搜索半径并生成下一批候选。外汇与贵金属参数寻优属高风险实验,回测漂亮不等于实盘能复现,任何结论都只是概率倾向。 个体层先做记忆更新:当临时适应度 a[p].f 超过历史最优 a[p].fMain,就把当前坐标数组拷进 cMain。族群层用 cnt 游标遍历每组,若组内有人刷新 gr[s].fB 则记为新标杆并置 impr=true;整组无改进时半径按 expansionRatio 放大,有改进则重置为 groupRadius,且硬上限锁在 0.5,避免跨维乱跳。 下一批个体生成时,每个坐标有 0.6 概率围绕族群最优 cB[c] 在 ±radius 内做幂分布抽样,radius 由参数区间跨度乘 sRadius 得出,越界则夹回 [rangeMin,rangeMax]。最后经验交换段给每个个体随机挑一个同伴,以 copyProb 概率抄对方坐标——这段虽被截断,但机制就是群体间信息渗漏。 把 sRadius 上限 0.5 和 0.6 抽样概率直接丢进 MT5 跑一遍,看族群收敛速度是变快还是过早散开,比读十遍文档管用。
} } class=class="str">"cmt">//---------------------------------------------------------------------------- class=class="str">"cmt">//update agents for (class="type">int p = class="num">0; p < popSize; p++) { if (a [p].f > a [p].fMain) { a [p].fMain = a [p].f; ArrayCopy(a [p].cMain, a [p].c, class="num">0, class="num">0, WHOLE_ARRAY); } } class=class="str">"cmt">//---------------------------------------------------------------------------- class="type">int cnt = class="num">0; class="type">bool impr = false; for (class="type">int s = class="num">0; s < groups; s++) { impr = false; for (class="type">int p = class="num">0; p < gr [s].sSize; p++) { if (a [cnt].f > gr [s].fB) { gr [s].fB = a [cnt].f; ArrayCopy(gr [s].cB, a [cnt].c, class="num">0, class="num">0, WHOLE_ARRAY); impr = true; } cnt++; } if (!impr) gr [s].sRadius *= expansionRatio; else gr [s].sRadius = groupRadius; if (gr [s].sRadius > class="num">0.5) gr [s].sRadius = class="num">0.5; } class=class="str">"cmt">//generate individuals of groups---------------------------------------------- class="type">class="kw">double coordinate = class="num">0.0; class="type">class="kw">double radius = class="num">0.0; class="type">class="kw">double min = class="num">0.0; class="type">class="kw">double max = class="num">0.0; cnt = class="num">0; for (class="type">int s = class="num">0; s < groups; s++) { for (class="type">int p = class="num">0; p < gr [s].sSize; p++) { for (class="type">int c = class="num">0; c < coords; c++) { if (RNDfromCI(class="num">0.0, class="num">1.0) < class="num">0.6) { radius = (rangeMax [c] - rangeMin [c]) * gr [s].sRadius; min = gr [s].cB [c] - radius; max = gr [s].cB [c] + radius; if (min < rangeMin [c]) min = rangeMin [c]; if (max > rangeMax [c]) max = rangeMax [c]; coordinate = PowerDistribution(gr [s].cB [c], min, max, power); a [cnt].c [c] = SeInDiSp(coordinate, rangeMin [c], rangeMax [c], rangeStep [c]); } } cnt++; } } class=class="str">"cmt">//exchange of experience---------------------------------------------------------------- cnt = class="num">0; for (class="type">int p = class="num">0; p < popSize; p++) { for (class="type">int c = class="num">0; c < coords; c++) { class="type">int pos = (class="type">int)RNDfromCI(class="num">0, popSize); if (pos >= popSize) pos = popSize - class="num">1; if (RNDfromCI(class="num">0.0, class="num">1.0) < copyProb) {