种群优化算法:Nelder-Mead(NM),或单纯形搜索方法·进阶篇
📐

种群优化算法:Nelder-Mead(NM),或单纯形搜索方法·进阶篇

(2/3)·当梯度法失效,1965 年的单纯形思想如何用多智能体在 MT5 里跑出实盘优化

含代码示例 第 2/3 篇

很多交易者一遇到目标函数没解析导数就退回网格暴力扫参,既慢又容易卡在局部谷底。Nelder-Mead 用一组会变形的顶点在参数空间里自己找路,本篇把它扩成多智能体版本,继续深挖上篇未尽的工程细节。

「Nelder-Mead 代理类的成员骨架」

下面这段 C_AO_NMm 类的声明,把 Nelder-Mead 单纯形法的可调参数和内部状态一次性摊开了。coords 是坐标维度,popSize 是种群规模,simplexPoints 记录单纯形顶点数,这三个整型私有成员决定了优化空间的形状。 reflectionCoeff、expansionCoeff、contractionCoeff 三个双精度成员对应反射、扩展、收缩系数,Init 方法接收外部传入的 reflectionCoeffP / expansionCoeffP / contractionCoeffP 完成初始化。在 MT5 里把反射系数设为 1.0、扩展系数 2.0、收缩系数 0.5 是常见起点,但贵金属跨周期拟合时往往要下调扩展系数以避免过冲。 私有方法里 Sorting 负责按目标值对单纯形顶点排序,CalcCentroid 算除最差点为外的质心,Reflection / Expansion / Contraction / Flying 分别对应四种顶点更新操作。SeInDiSp、RNDfromCI、Scale 是辅助函数,处理区间随机与量纲缩放。外汇与贵金属市场高杠杆、高波动,直接套用该优化器做实盘参数搜索须先以历史数据回测验证稳定性。

MQL5 / C++
const class="type">class="kw">double reflectionCoeffP,  class=class="str">"cmt">//Reflection coefficient
const class="type">class="kw">double expansionCoeffP,     class=class="str">"cmt">//Expansion coefficient
const class="type">class="kw">double contractionCoeffP); class=class="str">"cmt">//Contraction coefficient
class="kw">public: class="type">void Moving();
class="kw">public: class="type">void Revision();
class=class="str">"cmt">//----------------------------------------------------------------------------
class="kw">private: class="type">int    coords;          class=class="str">"cmt">//coordinates number
class="kw">private: class="type">int    popSize;         class=class="str">"cmt">//population size
class="kw">private: class="type">int    simplexPoints; class=class="str">"cmt">//simplex points
class="kw">private: class="type">class="kw">double reflectionCoeff;  class=class="str">"cmt">//Reflection coefficient
class="kw">private: class="type">class="kw">double expansionCoeff;   class=class="str">"cmt">//Expansion coefficient
class="kw">private: class="type">class="kw">double contractionCoeff; class=class="str">"cmt">//Contraction coefficient
class="kw">private: class="type">bool   revision;
class="kw">private: S_Point pTemp [];
class="kw">private: class="type">int    ind  [];
class="kw">private: class="type">class="kw">double val  [];
class="kw">private: class="type">void   Sorting(S_Point &p []);
class="kw">private: class="type">void   CalcCentroid(S_Simplex &s, class="type">int indW);
class="kw">private: class="type">void   Reflection(S_Agent &agent, class="type">int indW);
class="kw">private: class="type">void   Expansion(S_Agent &agent);
class="kw">private: class="type">void   Contraction(S_Agent &agent, class="type">int indW);
class="kw">private: class="type">void   Flying(S_Agent &agent, class="type">int indW);
class="kw">private: 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="kw">private: class="type">class="kw">double RNDfromCI(class="type">class="kw">double min, class="type">class="kw">double max);
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=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_NMm::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">class="kw">double reflectionCoeffP,   class=class="str">"cmt">//reflection coefficient
                     const class="type">class="kw">double expansionCoeffP,    class=class="str">"cmt">//expansion coefficient

◍ Nelder-Mead 代理群的初始化与游走逻辑

这段 C_AO_NMm 类的构造函数把下山单纯形法塞进了多代理框架:simplexPoints 被定为 coords+1,也就是 N 维问题需要 N+1 个顶点。反射、扩张、收缩三个系数由外部传入,MathSrand 用微秒计数重置随机种子,避免每次回测跑出同一串伪随机序列。 Moving() 在非 revision 阶段给每个代理的单纯形顶点撒点:先用 RNDfromCI 在 [rangeMin, rangeMax] 里均匀抽,再用 SeInDiSp 按 rangeStep 吸附到离散网格。若 popSize=20、coords=5,单次 Moving 会生成 20×6×5=600 个坐标赋值,MT5 策略测试器里能直接数出这个循环次数。 Revision() 才是单纯形迭代的核心:先对每个代理的顶点按适应度排序,把倒数第二和最后一格标成 indG(好点)与 indW(坏点),CalcCentroid 除掉坏点算质心,然后统一挂 reflection 操作。注意它顺手把各代理最优顶点拎出来比 fB,若优于当前全局最优就拷进 cB——这意味着群体里任何一个单纯形冒出好解都会立刻被记录。 外汇与贵金属行情的高波动会让适应度面剧烈变形,这类随机优化在实盘大概率漂移,建议先在 MT5 用历史数据跑 popSize 从 10 到 50 的对照,观察 cB 收敛稳定性再谈参数。

MQL5 / C++
const class="type">class="kw">double contractionCoeffP) class=class="str">"cmt">//contraction coefficient
{
  MathSrand((class="type">int)GetMicrosecondCount()); class=class="str">"cmt">// reset of the generator
  fB       = -DBL_MAX;
  revision = false;
  coords        = coordsP;
  popSize        = popSizeP;
  simplexPoints = coords + class="num">1;
  reflectionCoeff  = reflectionCoeffP;
  expansionCoeff   = expansionCoeffP;
  contractionCoeff = contractionCoeffP;
  ArrayResize(pTemp, simplexPoints);
  ArrayResize(ind,   simplexPoints);
  ArrayResize(val,   simplexPoints);
  ArrayResize(a, popSize);
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    a [i].Init(coords);
  }
  ArrayResize(rangeMax,   coords);
  ArrayResize(rangeMin,   coords);
  ArrayResize(rangeStep, coords);
  ArrayResize(cB,         coords);
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_NMm::Moving()
{
  if (!revision)
  {
    class="type">int cnt = class="num">0;
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      for (class="type">int p = class="num">0; p < simplexPoints; p++)
      {
        cnt++;
        for (class="type">int c = class="num">0; c < coords; c++)
        {
          a [i].s.p [p].c [c] = RNDfromCI(rangeMin [c], rangeMax [c]);
          a [i].s.p [p].c [c] = SeInDiSp(a [i].s.p [p].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
        }
      }
    }
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_NMm::Revision()
{
  class=class="str">"cmt">//----------------------------------------------------------------------------
  if (!revision)
  {
    class=class="str">"cmt">//sort agent simplex points by FF value and assign BGW
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      Sorting(a [i].s.p);
    }
    class=class="str">"cmt">//calculate the simplex centroid
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      a [i].s.indG = simplexPoints - class="num">2;
      a [i].s.indW = simplexPoints - class="num">1;
      CalcCentroid(a [i].s, a [i].s.indW);
    }
    class=class="str">"cmt">//assign the next type of operation - reflection
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      Reflection(a [i], a [i].s.indW);
      a [i].s.operation = reflection;
    }
    class=class="str">"cmt">//save the best point of the agents’ simplexes as a global solution 
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      if (a [i].s.p [class="num">0].f > fB)
      {
        fB = a [i].s.p [class="num">0].f;
        ArrayCopy(cB, a [i].s.p [class="num">0].c, class="num">0, class="num">0, WHOLE_ARRAY);
      }

反射之后的单纯形顶点替换逻辑

这段逻辑处理的是 Nelder-Mead 单纯形优化里「反射」动作结束后的分支判定。每一个个体 a[i] 若刚做过 reflection,就把反射点适应度记到 Xr.f,再跟当前最差点 Xw、最好点 Xb 比大小,决定是替换、扩张还是收缩。 当反射点比最差点好(Xr > Xw)时,直接用反射点顶掉最差点,并打上 needUpd 标记;若反射点还胜过了最好点(Xr > Xb),就立刻跑 Expansion() 做扩张并跳过后续。反过来,反射点不如次差点(Xr <= Xg)时走 Contraction() 收缩。 如果只替换了最差点、没碰最好点,就重排顶点、重算质心并再反射一次;否则调用 Flying() 让单纯形「飞」到新位置。外汇与贵金属参数优化属高风险场景,这套逻辑仅降低陷入局部最优的概率,不保证实盘收益。 下面这段是 revision 标记下的精英保留片段:若本轮迭代发现比历史最优 fB 更优的个体,就把它拷进全局最优数组 cB。

MQL5 / C++
   }
   revision = true;
   class="kw">return;
   }
   class=class="str">"cmt">//----------------------------------------------------------------------------
   if (revision)
   {
      class="type">int pos = -class="num">1;
      for (class="type">int i = class="num">0; i < popSize; i++)
      {
         if (a [i].f > fB) pos = i;
      }
      if (pos != -class="num">1)
      {
         fB = a [pos].f;
         ArrayCopy(cB, a [pos].c, class="num">0, class="num">0, WHOLE_ARRAY);
      }
   }

「单纯形收缩后的坐标重算逻辑」

当代理个体在上一步被判为收缩(contraction)时,代码先把它算出的收缩点目标值 Xc.f 赋给结构体,再和当前最差点的目标值比较。若 Xc.f 大于最差点,说明收缩没带来改善,就把 Xc 塞进最差位置、重新排序、重算形心并做一次反射;否则直接调用 Flying 让该点随机飞走,避免陷在局部。 CalcCentroid 的实作是按维度累加除最差点外所有顶点的坐标,再除以 coords(维度数)得到形心。注意这里分母是 coords 而非 simplexPoints-1,若你的顶点数定义和维数不一致,形心会偏,EA 优化可能倾向早熟收敛。 Reflection 与 Expansion 都用了固定系数:Xr = Xo + reflectionCoeff*(Xo-Xw),Xe = Xo + expansionCoeff*(Xr-Xo),算完用 SeInDiSp 把结果夹回参数上下界与步长网格。在 MT5 里把 reflectionCoeff 从 1.0 调到 1.3 左右,黄金 1 小时框的均线周期寻优可能更快跳出平坦区,但外汇与贵金属属高风险品种,参数激进取胜概率并不保证。

MQL5 / C++
else                 a [i].s.p [a [i].s.indW] = a [i].s.Xr;
    Sorting(a [i].s.p);
    a [i].s.indG = simplexPoints - class="num">2;
    a [i].s.indW = simplexPoints - class="num">1;
    CalcCentroid(a [i].s, a [i].s.indW);
    Reflection(a [i],   a [i].s.indW);
    class="kw">continue;
  }
class=class="str">"cmt">//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
class=class="str">"cmt">//if there was contraction +++++++++++++++++++++++++++++++++++++++++++
if (a [i].s.operation == contraction)
{
  a [i].s.Xc.f = a [i].f;
  if (a [i].s.Xc.f > a [i].s.p [a [i].s.indW].f)
  {
    a [i].s.p [a [i].s.indW] = a [i].s.Xc;
    Sorting(a [i].s.p);
    a [i].s.indG = simplexPoints - class="num">2;
    a [i].s.indW = simplexPoints - class="num">1;
    CalcCentroid(a [i].s, a [i].s.indW);
    Reflection(a [i],   a [i].s.indW);
  }
  else Flying(a [i], a [i].s.indW);
  class="kw">continue;
}
}
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_NMm::CalcCentroid(S_Simplex &s, class="type">int indW)
{
  class="type">class="kw">double summ = class="num">0.0;
  for (class="type">int c = class="num">0; c < coords; c++)
  {
    summ = class="num">0.0;
    for (class="type">int p = class="num">0; p < simplexPoints; p++)
    {
      if (p != indW) summ += s.p [p].c [c];
    }
    s.c [c] = summ / coords;
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_NMm::Reflection(S_Agent &agent, class="type">int indW)
{
  class="type">class="kw">double Xo;
  class="type">class="kw">double Xr;
  class="type">class="kw">double Xw;
  for (class="type">int c = class="num">0; c < coords; c++)
  {
    Xo = agent.s.c [c];
    Xw = agent.s.p [indW].c [c];
    class=class="str">"cmt">//Xr = Xo + RNDfromCI(class="num">0.0, reflectionCoeff) * (Xo - Xw);
    Xr = Xo + reflectionCoeff * (Xo - Xw);
    agent.s.Xr.c [c] = SeInDiSp(Xr, rangeMin [c], rangeMax [c], rangeStep [c]);
    agent.c        [c] = agent.s.Xr.c [c];
  }
  agent.s.operation = reflection;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_NMm::Expansion(S_Agent &agent)
{
  class="type">class="kw">double Xo;
  class="type">class="kw">double Xr;
  class="type">class="kw">double Xe;
  for (class="type">int c = class="num">0; c < coords; c++)
  {
    Xo = agent.s.c      [c];
    Xr = agent.s.Xr.c [c];
    class=class="str">"cmt">//Xe = Xo + RNDfromCI(class="num">0.0, expansionCoeff) * (Xr - Xo);
    Xe = Xo + expansionCoeff * (Xr - Xo);
    agent.s.Xe.c [c] = SeInDiSp(Xe, rangeMin [c], rangeMax [c], rangeStep [c]);
    agent.c        [c] = agent.s.Xe.c [c];
  }
  agent.s.operation = expansion;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_NMm::Contraction(S_Agent &agent, class="type">int indW)
{
  class="type">class="kw">double Xo;
  class="type">class="kw">double Xw;
  class="type">class="kw">double Xc;
把重复劳动交给小布
这些诊断与小布盯盘的 AIGC 已内置,打开对应品种页即可看到参数空间的单纯形收敛轨迹,你只需判断哪组权重值得上实盘。

常见问题

原文实现给每个智能体配独立单纯形,多个活体并行探索,因此可归类为群体化变体,而非经典单单纯形判定搜索。
小布盯盘内置的 AIGC 诊断可识别此类无导数优化的输出形态,但策略编译仍需在 MT5 环境完成,小布负责看盘口与收敛提示。
不会,无论维度多少,排序后'好'点始终是仅次于'最差'的第二点,质心始终排除最差顶点求平均。
外汇贵金属属高风险市场,NM 可能收敛到局部最优,建议多智能体种子分散并结合样本外验证再考虑上线。