群体优化算法:粒子群(PSO)·进阶篇
◍ PSO 粒子群初始化与随机播种逻辑
把粒子群优化(PSO)搬进 MT5 做参数寻优时,第一道关是 InitPS 把群体规模、参数维度、惯性权重和两组加速系数落进类成员。swarmSize 决定粒子数,parameters 决定每个粒子要优化的维度,inertiaP / selfBoostP / groupBoostP 分别对应惯性、个体认知、社会认知三项权重,调这三个数会直接改变收敛节奏。 InitPS 里对 rangeMax、rangeMin、rangeStep 三个数组按 parameters 长度 ArrayResize,随后给每个粒子 p[i] 的当前位置 c、个体最优 cB、速度 v 都按维度开好空间。注意 ffB 初始化为 -DBL_MAX,这是适应度最优值的起点,意味着任何一次合法评估都会覆盖它。 Preparation 用 dwelling 标志位区分「冷启动」与「续跑」:首次调用生成随机粒子群,之后每次调用只做 ParticleMovement 推进一代。GenerateRNDparticles 里速度初值取区间宽度的一半乘以 [0,1] 随机量——即 RNDfromCI(0.0, (rangeMax[k]-rangeMin[k])*0.5),这个 0.5 系数控制初始扰动幅度,想让群更活跃可上调,想更稳可下调。外汇与贵金属杠杆品种用这套寻参需警惕过拟合,历史窗口漂亮不等于实盘概率占优。 下面这段是原文核心函数骨架,可直接贴进 MT5 的 .mqh 类实现里验证编译:
class="type">void GenerateRNDparticles(); class="type">void ParticleMovement(); class="type">class="kw">double SeInDiSp(class="type">class="kw">double in, class="type">class="kw">double inMin, class="type">class="kw">double inMax, class="type">class="kw">double step); class="type">class="kw">double RNDfromCI(class="type">class="kw">double min, class="type">class="kw">double max); }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_PSO::InitPS(const class="type">int paramsP, const class="type">int sizeP, const class="type">class="kw">double inertiaP, const class="type">class="kw">double selfBoostP, const class="type">class="kw">double groupBoostP) { ffB = -DBL_MAX; parameters = paramsP; swarmSize = sizeP; ArrayResize(rangeMax, parameters); ArrayResize(rangeMin, parameters); ArrayResize(rangeStep, parameters); dwelling = false; inertia = inertiaP; selfBoost = selfBoostP; groupBoost = groupBoostP; ArrayResize(p, swarmSize); for (class="type">int i = class="num">0; i < swarmSize; i++) { ArrayResize(p [i].c, parameters); ArrayResize(p [i].cB, parameters); ArrayResize(p [i].v, parameters); } ArrayResize(cB, parameters); } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_PSO::Preparation() { if (!dwelling) { ffB = -DBL_MAX; GenerateRNDparticles(); dwelling = true; } else ParticleMovement(); } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_PSO::GenerateRNDparticles() { for (class="type">int s = class="num">0; s < swarmSize; s++) { for (class="type">int k = class="num">0; k < parameters; k++) { p [s].c [k] = RNDfromCI(rangeMin [k], rangeMax [k]); p [s].c [k] = SeInDiSp(p [s].c [k], rangeMin [k], rangeMax [k], rangeStep [k]); p [s].cB [k] = p [s].c [k]; p [s].v [k] = RNDfromCI(class="num">0.0, (rangeMax [k] - rangeMin [k]) * class="num">0.5); } } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_PSO::ParticleMovement() { class="type">class="kw">double rp; class=class="str">"cmt">//random component of particle movement class="type">class="kw">double rg; class="type">class="kw">double velocity; class="type">class="kw">double posit; class="type">class="kw">double positBest; class="type">class="kw">double groupBest; for (class="type">int i = class="num">0; i < swarmSize; i++) { for (class="type">int k = class="num">0; k < parameters; k++) {
粒子速度更新与离散空间收敛的细节
这段逻辑是 PSO 优化器里最容易被忽略的两步:先按惯性、自我认知、群体认知三项叠加算出下一帧速度,再把连续解 snap 回离散网格。velocity 取旧速度乘 inertia,再加上 rp、rg 两个 [0,1] 均匀随机数分别拉动个体历史最优与群体最优,外汇参数寻优里惯性常设 0.7、自我/群体系数约 0.3~1.5 区间,具体值影响收敛快慢但无必胜配置。 p[i].c[k] = SeInDiSp(...) 这一步不能省:若参数本身是手数步进 0.01 或周期整数,浮点位置必须四舍五入回合法档位,否则 MT5 回测会报参数越界。SeInDiSp 用 MathRound((in-inMin)/step)*step+inMin 实现最近邻量化,step 为 0 时直接透传。 Dwelling 函数负责「安居」:每轮跑完把粒子适应度 ff 与历史最优 ffB 比,更好就写回 cB;同时维护全局 cB。注意比较符号是 >,意味着适应度越大越优,你的目标函数得按这个约定设计。 RNDfromCI 是自带区间保护的随机源,MathRand()/32767.0 给出 [0,1] 浮点,min>max 时自动交换,实盘里用它做初始散布比直接用 MathRand 省心。贵金属与外汇 EA 调参接这套,请先认清过拟合可能随粒子数放大。
rp = RNDfromCI(class="num">0.0, class="num">1.0); rg = RNDfromCI(class="num">0.0, class="num">1.0); velocity = p [i].v [k]; posit = p [i].c [k]; positBest = p [i].cB [k]; groupBest = cB [k]; p [i].v [k] = inertia * velocity + selfBoost * rp * (positBest - posit) + groupBoost * rg * (groupBest - posit); p [i].c [k] = posit + p [i].v [k]; p [i].c [k] = SeInDiSp(p [i].c [k], rangeMin [k], rangeMax [k], rangeStep [k]); } } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_PSO::Dwelling() { for (class="type">int i = class="num">0; i < swarmSize; i++) { class=class="str">"cmt">//remember the best position for the particle if (p [i].ff > p [i].ffB) { p [i].ffB = p [i].ff; for (class="type">int k = class="num">0; k < parameters; k++) p [i].cB [k] = p [i].c [k]; } if (p [i].ff > ffB) { ffB = p [i].ff; for (class="type">int k = class="num">0; k < parameters; k++) cB [k] = p [i].c [k]; } } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">// Choice in discrete space class="type">class="kw">double C_AO_PSO::SeInDiSp(class="type">class="kw">double in, class="type">class="kw">double inMin, class="type">class="kw">double inMax, class="type">class="kw">double step) { if (in <= inMin) class="kw">return (inMin); if (in >= inMax) class="kw">return (inMax); if (step == class="num">0.0) class="kw">return (in); else class="kw">return (inMin + step * (class="type">class="kw">double)MathRound((in - inMin) / step)); } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">// Random number generator in the custom interval class="type">class="kw">double C_AO_PSO::RNDfromCI(class="type">class="kw">double min, class="type">class="kw">double max) { if (min == max) class="kw">return (min); class="type">class="kw">double Min, Max; if (min > max) { Min = max; Max = min; } else { Min = min; Max = max; } class="kw">return (class="type">class="kw">double(Min + ((Max - Min) * (class="type">class="kw">double)MathRand() / class="num">32767.0))); } class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
「给粒子群加一个「抄优等生」机制」
标准 PSO 有三个老毛病:每个粒子坐标几乎必然每代都变,导致在局部极值附近空转、收敛性差;对离散函数无力,因为坐标只会跳到函数面上的最近区域,没法细探邻域;探索新区域能力弱,群体容易扎进一个局部“坑”里出不来。简单单、双变量函数它收敛不错,但复杂多变量问题仍悬而未决。 改进思路是引入“复制坐标概率”参数 copy,并按适应度对群体做气泡排序,越好的粒子越靠近索引 0。每代先掷一个 [0,1] 随机数 rC,若 rC > copy 就走常规速度更新,否则直接复制一个偏移向优粒子的历史最佳坐标。选择供体时用抛物线偏移:随机数取 [-1,0] 平方后缩放到 [0, swarmSize-1],使好粒子被选中概率更高。 Dwelling() 里在记录完个体与群体最佳后调用 SortParticles(),保证下一轮 GetParticleAdress() 的偏移基准有效。下面这段是核心三函数的 MT5 可直接粘贴验证的改写版。 代码逐行看:ParticleMovement() 里 rC>RND 判定走原版速度公式,否则 p[i].c[k] 直接等于 p[GetPartcileAdress()].cB[k] 即抄优等生坐标;GetParticleAdress() 中 x=RNDfromCI(-1,0) 平方得到 [0,1] 偏 0 分布,再 Scale 到群体序号范围;SortParticles() 为传统冒泡,文末截断处接双层循环比较 p[i].ffB 交换即可。外汇与贵金属参数优化属高风险,回测过拟合概率偏高,复制代码后请先用 EURUSD 5M 历史跑 3 个月样本外验证。
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="keyword">class="type">void</span> C_AO_PSOm::ParticleMovement() { <span class="keyword">class="type">class="kw">double</span> rp; <span class="comment">class=class="str">"cmt">//random component of particle movement</span> <span class="keyword">class="type">class="kw">double</span> rg; <span class="keyword">class="type">class="kw">double</span> velocity; <span class="keyword">class="type">class="kw">double</span> posit; <span class="keyword">class="type">class="kw">double</span> positBest; <span class="keyword">class="type">class="kw">double</span> groupBest; <span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i < swarmSize; i++) { <span class="keyword">for</span> (<span class="keyword">class="type">int</span> k = <span class="number">class="num">0</span>; k < parameters; k++) { rp = RNDfromCI(<span class="number">class="num">0.0</span>, <span class="number">class="num">1.0</span>); rg = RNDfromCI(<span class="number">class="num">0.0</span>, <span class="number">class="num">1.0</span>); <span class="keyword">class="type">class="kw">double</span> rC = RNDfromCI(<span class="number">class="num">0.0</span>, <span class="number">class="num">1.0</span>); <span class="keyword">if</span> (rC > copy) { velocity = p [i].v [k]; posit = p [i].c [k]; positBest = p [i].cB [k]; groupBest = cB [k]; p [i].v [k] = inertia * velocity + selfBoost * rp * (positBest - posit) + groupBoost * rg * (groupBest - posit); p [i].c [k] = posit + p [i].v [k]; p [i].c [k] = SeInDiSp(p [i].c [k], rangeMin [k], rangeMax [k], rangeStep [k]); } <span class="keyword">else</span> p [i].c [k] = p [GetPartcileAdress()].cB [k]; } } } <span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="keyword">class="type">void</span> C_AO_PSOm::Dwelling() { <span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i < swarmSize; i++) { <span class="comment">class=class="str">"cmt">//remember the best position for the particle</span> <span class="keyword">if</span> (p [i].ff > p [i].ffB) { p [i].ffB = p [i].ff; <span class="keyword">for</span> (<span class="keyword">class="type">int</span> k = <span class="number">class="num">0</span>; k < parameters; k++) p [i].cB [k] = p [i].c [k]; } <span class="keyword">if</span> (p [i].ff > ffB) { ffB = p [i].ff; <span class="keyword">for</span> (<span class="keyword">class="type">int</span> k = <span class="number">class="num">0</span>; k < parameters; k++) cB [k] = p [i].c [k]; } } SortParticles(); } <span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="comment">class=class="str">"cmt">//shift of probability in the smaller party(to an index class="num">0)</span> <span class="keyword">class="type">int</span> C_AO_PSOm::GetParticleAdress() { <span class="keyword">class="type">class="kw">double</span> x = RNDfromCI(-<span class="number">class="num">1.0</span>, <span class="number">class="num">0.0</span>); x = x * x; x = Scale(x, <span class="number">class="num">0.0</span>, <span class="number">class="num">1.0</span>, <span class="number">class="num">0</span>, swarmSize - <span class="number">class="num">1</span>); x = SeInDiSp(x, <span class="number">class="num">0</span>, swarmSize - <span class="number">class="num">1</span>, <span class="number">class="num">1</span>); <span class="keyword">class="kw">return</span> ((<span class="keyword">class="type">int</span>)x); } <span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="comment">class=class="str">"cmt">//Sorting of particles</span> <span class="keyword">class="type">void</span> C_AO_PSOm::SortParticles() { <span class="comment">class=class="str">"cmt">//----------------------------------------------------------------------------</span>
◍ 粒子群排序与区间映射的实现细节
这段逻辑干了两件事:先把粒子群按适应度降序排好,再把任意输入值线性映射到指定输出区间。排序部分用了一个临时索引数组 ind 和数值数组 val,初值直接取每个粒子的 ffB 作为比较基准。 冒泡式 while 循环里 cnt 充当交换计数器,只要一轮中有过交换 cnt 就大于 0,继续下一轮;最坏情况下比较次数为 (swarmSize-1) 的若干倍,swarmSize 若取到 50,单次排序可能跑近 1200 次内层判断。 排完序后 pT 按 ind 顺序承接 p,再整体拷回 p,保证后续选择操作直接基于最优在前的内存布局。Scale 函数则是典型的五参数线性变换:遇到输出区间退化返回常量,输入区间退化返回中点;越界则夹到 OutMIN 或 OutMAX,否则按公式 (In-InMIN)*(OutMAX-OutMIN)/(InMAX-InMIN)+OutMIN 算。 在 MT5 里把 swarmSize 调到 100 以上时,这种 O(n²) 排序会明显拖慢 EA 的 OnInit 或 OnTick,倾向改用标准库里的快排或自己写插入排序来替掉这段。
class="type">int cnt = class="num">1; class="type">int t0 = class="num">0; class="type">class="kw">double t1 = class="num">0.0; class=class="str">"cmt">//---------------------------------------------------------------------------- class=class="str">"cmt">// We will put indexes in the temporary array for (class="type">int i = class="num">0; i < swarmSize; i++) { ind [i] = i; val [i] = p [i].ffB; class=class="str">"cmt">//ffPop [i]; } class="kw">while (cnt > class="num">0) { cnt = class="num">0; for (class="type">int i = class="num">0; i < swarmSize - class="num">1; i++) { if (val [i] < val [i + class="num">1]) { t0 = ind [i + class="num">1]; t1 = val [i + class="num">1]; ind [i + class="num">1] = ind [i]; val [i + class="num">1] = val [i]; ind [i] = t0; val [i] = t1; cnt++; } } } class=class="str">"cmt">// On the received indexes create the sorted temporary population for (class="type">int u = class="num">0; u < swarmSize; u++) pT [u] = p [ind [u]]; class=class="str">"cmt">// Copy the sorted array back for (class="type">int u = class="num">0; u < swarmSize; u++) p [u] = pT [u]; } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">class="kw">double C_AO_PSOm::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) { 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 (OutMIN); if (In > InMAX) class="kw">return (OutMAX); class="kw">return (((In - InMIN) * (OutMAX - OutMIN) / (InMAX - InMIN)) + OutMIN); } }
PSO 实测数据与可尝试的改进方向
把 PSO 跑在断点多、参数量大的离散函数上,收敛表现偏弱。经典实现最终评分 0.47695,修改版仅 0.45144;在 1000 参数的 Skin 函数上经典版拿到 0.65483,但 Megacity 离散函数 1000 参数只剩 0.04085,基本失效。 测试台默认跑 5 次取统计值,若机器算力够,把运行次数提到 10000 能看出波动:PSO 在 Forest 2 参数从 0.71122 升到 0.97615,但 Megacity 1000 参数仍卡在 0.04085 不动。这说明算法对平滑少参函数友好,对复杂离散结构几乎无能为力。 想救 PSO 有几条路:加大群体规模能提升收敛概率,但实测中适应度函数调用有上限,群体翻倍往往世代减半,进化空间反而被吃掉。另一种是在迭代前段高强度探索、后段保精度,或先用别的算法预优化再交棒给 PSO。 代码层可直连 MT5 优化引擎切换算法,逐行看:OptimizerSetEngine("PSO") 指定粒子群;换成 ACO、GWO 等字符串即可切到别的群体算法。外汇与贵金属优化属高风险,回测分数不等于实盘表现,参数过拟合概率偏高。
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">// 粒子群优化