群体优化算法:粒子群(PSO)·进阶篇
📘

群体优化算法:粒子群(PSO)·进阶篇

第 2/2 篇

◍ 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 类实现里验证编译:

MQL5 / C++
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 调参接这套,请先认清过拟合可能随粒子数放大。

MQL5 / C++
  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 个月样本外验证。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span>
<span class="keyword">class="type">void</span> C_AO_PSOm::ParticleMovement()
{
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> rp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//random component of particle movement</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> rg;
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> velocity;
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> posit;
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> positBest;
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> groupBest;
&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i &lt; swarmSize; i++)
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">int</span> k = <span class="number">class="num">0</span>; k &lt; parameters; k++)
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;rp = RNDfromCI(<span class="number">class="num">0.0</span>, <span class="number">class="num">1.0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;rg = RNDfromCI(<span class="number">class="num">0.0</span>, <span class="number">class="num">1.0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (rC &gt; copy)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;velocity&nbsp;&nbsp;= p [i].v&nbsp;&nbsp;[k];
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;posit&nbsp;&nbsp;&nbsp;&nbsp; = p [i].c&nbsp;&nbsp;[k];
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;positBest = p [i].cB [k];
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;groupBest = cB [k];
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;p [i].v [k] = inertia * velocity + selfBoost * rp * (positBest - posit) + groupBoost * rg * (groupBest - posit);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;p [i].c [k] = posit + p [i].v [k];
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;p [i].c [k] = SeInDiSp(p [i].c [k], rangeMin [k], rangeMax [k], rangeStep [k]);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">else</span> p [i].c [k] = p [GetPartcileAdress()].cB [k];
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;}
}
<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()
{
&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i &lt; swarmSize; i++)
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//remember the best position for the particle</span>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (p [i].ff &gt; p [i].ffB)
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;p [i].ffB = p [i].ff;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">int</span> k = <span class="number">class="num">0</span>; k &lt; parameters; k++) p [i].cB [k] = p [i].c [k];
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (p [i].ff &gt; ffB)
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ffB = p [i].ff;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">int</span> k = <span class="number">class="num">0</span>; k &lt; parameters; k++) cB [k] = p [i].c [k];
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;}
&nbsp;&nbsp;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()
{
&nbsp;&nbsp;<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>);
&nbsp;&nbsp;x = x * x;
&nbsp;&nbsp;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>);
&nbsp;&nbsp;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>);
&nbsp;&nbsp;<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()
{
&nbsp;&nbsp;<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,倾向改用标准库里的快排或自己写插入排序来替掉这段。

MQL5 / C++
  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 等字符串即可切到别的群体算法。外汇与贵金属优化属高风险,回测分数不等于实盘表现,参数过拟合概率偏高。

MQL5 / C++
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">// 粒子群优化

常见问题

把随机种子绑定到时间戳或行情 tick 计数,避免每轮用相同伪随机序列;播种范围按品种波动率缩放更稳。
速度步长未做取整或区间截断,导致粒子在边界震荡;给速度乘衰减系数并做离散映射即可缓解。
小布可替你批量调度粒子群初始化与收敛诊断,把重复劳动交给小布,你专注决策参数区间。
可能是抄袭比例过高导致多样性丧失;建议抄袭权重控制在 0.1~0.3 并保留部分随机扰动。
用线性区间映射:将排名归一化到 [0,1] 再乘以参数上下界差值,加下限即得实际参数值。