种群优化算法:Nelder-Mead(NM),或单纯形搜索方法(基础篇)
用单纯形法给EA找参数
Nelder-Mead(NM)也叫单纯形搜索,是一类不依赖梯度的种群/几何优化算法,适合在 MT5 里替 EA 搜一组较优输入参数。它靠维护一个 simplex(多顶点几何体)不断反射、收缩来逼近极值,对目标函数不可导的场景比梯度法更稳。 在 MT5 上跑这类优化,常见做法是把策略参数空间交给 NM 搜索,而不是 brute-force 网格遍历。社区里一篇 2024-06-21 发布的示例帖,截至统计有 1359 次查看、1 条评论,说明这类轻量优化器在实战圈有真实需求。 外汇与贵金属杠杆高、滑点跳空频繁,任何参数优化结果都只是历史样本上的概率倾向,实盘前务必用 MT5 策略测试器做样本外验证。
◍ 不用导数的单纯形优化是怎么来的
Nelder-Mead 方法由 John Nelder 和 Roger Mead 在 1965 年提出,初衷很直接:当时他们想找一种能跑在没有导数、甚至没有导数解析方程的函数上的优化办法。传统梯度法在这种函数面前基本失效,而他们希望算法既好实现又能在那个年代的计算机上跑得动。 思路灵感来自单纯形——也就是函数参数空间里的一个多面体。单纯形由一组顶点构成,每个顶点对应一组待优化的参数值;算法靠在参数空间里不断变形、移动这个多面体,去逼近函数最优值。原文载于《计算机杂志》的《函数最小化的单纯形方法》一文,后来被各领域拿去处理无梯度优化问题。 从类别看,它属于无条件优化算法,判定性、不需要函数导数,也能应付带多个局部最小值的函数。对做 MT5 上复杂目标函数寻优(比如多参数 EA 调参)的交易者来说,这类算法意味着你不用自己推导梯度,也能搜到可用参数组合。外汇与贵金属杠杆交易高风险,参数优化结果仅代表历史样本上的概率倾向,实盘可能失效。
「单纯形怎么在参数空间里翻跟头」
Nelder-Mead 不是群体算法那一套,它只养一个活体,活体里塞一个单纯形,单纯形由 z+1 个顶点撑起(z 是维度)。我们这边为了并行探索,给每个智能体各发一个单纯形,于是整体看着像群体,骨子里还是单活体迭代。 维度 z=2 时单纯形有 3 个顶点,按目标函数排成「最好 / 好 / 最差」。每轮先算去掉最差点的质心 Xo,再把最差点相对质心做反射:Xr = Xo + α(Xo - Xw),α 默认 1。反射若比最佳还好就扩张 Xe = Xo + γ(Xr - Xo),γ 默认 2;若反射比「好」点还差就收缩 Xc = Xo + β(Xw - Xo),β 默认 0.5。 γ 和 β 都不能乱填。γ 太大单纯形会飞出有效区、丢局部极值信息;太小探不动。β 太大把单纯形压扁同样丢信息,太小又缩不到最差点附近。原文作者还列了第 4 个操作「缩窄」,要把所有点向最佳偏移并重算一遍适应度——维度 1000 时得再算 1000 次,纯烧 CPU 且容易全体陷进一个局部极值,我在实现里直接砍了这步。 真正麻烦的是局部陷阱。单纯形顶点每次只挪固定量,像踩高跷坐矮凳,初始顶点没落在好位置就废了。交易优化面满是离散坑,光滑抛物线才适合它。我的补丁是接 Levy 航班:卡住时让顶点按偏向全局最佳的概率分布随机跳走,可能把活体踢出陷阱,但不保证每次都有用,纯实验性质。 代码层用 E_SimplexOperation 枚举记每轮对最差点干了啥(none/reflection/expansion/contraction)。S_Point 存坐标数组 c[] 和适应度 f;S_Simplex 聚合顶点、质心、Xr/Xe/Xc 及好坏点索引;S_Agent 是每个活体的壳,挂一个单纯形。C_AO_NMm 类管群体、范围与比率,Init 布数组,Revision 跑主逻辑,CalcCentroid/Reflection/Expansion/Contraction/Flying 各管一摊。
让活体沿全局最优飞出去
在 Flying 方法里,先给 agent.s.operation 置为 reflection,算法迭代就会走反射后的后续逻辑,而不是重算一遍反射。接着把 SeInDiSp 套到 Xr 上,用 rangeMin[c]、rangeMax[c] 和 rangeStep[c] 三个数组约束它的取值边界与步长,结果写回 agent.s.Xr.c[c],保证坐标始终落在有效搜索区间内。 所谓“飞行”,本质是活体依据全局最佳解的当前坐标,叠加一个随机值去改写自身坐标。这种扰动让活体有可能跳出当前聚簇,探到解空间里还没被扫过的区域,从而有概率找到更优解。 下面这段结构体定义是上面逻辑在 MT5 里的落地骨架。E_SimplexOperation 枚举了四种单纯形操作,none 到 contraction 分别对应 0–3;S_Point 存单点坐标与适应度,初始化时把 f 设成 -DBL_MAX 表示尚未评估。 S_Simplex 里 p[] 比维数多 1 个点,c[] 是质心,Xr/Xe/Xc 分别是反射、扩张、收缩点,indG/indW 记好坏点下标,operation 控制本步动作。S_Agent 在单纯形外再包一层坐标与适应度,C_AO_NMm 则持有全局最优 cB/fB、种群 a[] 以及三套区间数组 rangeMin/Max/Step——开 MT5 新建 EA 把这些 struct 贴进去,就能直接调 Init 验证边界约束是否生效。
class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— enum E_SimplexOperation { none = class="num">0, reflection = class="num">1, expansion = class="num">2, contraction = class="num">3 }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="kw">struct S_Point { class="type">void Init(class="type">int coords) { ArrayResize(c, coords); f = -DBL_MAX; } class="type">class="kw">double c []; class="type">class="kw">double f; }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="kw">struct S_Simplex { class="type">void Init(class="type">int coords) { ArrayResize(p, coords + class="num">1); for (class="type">int i = class="num">0; i <= coords; i++) { p [i].Init(coords); } ArrayResize(c, coords); Xr.Init(coords); Xe.Init(coords); Xc.Init(coords); operation = none; } S_Point p []; class="type">class="kw">double c []; class=class="str">"cmt">//centroid S_Point Xr; class=class="str">"cmt">//reflection point S_Point Xe; class=class="str">"cmt">//expansion point S_Point Xc; class=class="str">"cmt">//expansion point class="type">int indG; class=class="str">"cmt">//index of good point class="type">int indW; class=class="str">"cmt">//index of worst point E_SimplexOperation operation; class=class="str">"cmt">//type of simplex operation }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="kw">struct S_Agent { class="type">void Init(class="type">int coords) { ArrayResize(c, coords); f = -DBL_MAX; s.Init(coords); } class="type">class="kw">double c []; class=class="str">"cmt">//coordinates class="type">class="kw">double f; class=class="str">"cmt">//fitness S_Simplex s; class=class="str">"cmt">//agent simplex }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class C_AO_NMm { class=class="str">"cmt">//---------------------------------------------------------------------------- class="kw">public: class="type">class="kw">double cB []; class=class="str">"cmt">//best coordinates class="kw">public: class="type">class="kw">double fB; class=class="str">"cmt">//FF of the best coordinates class="kw">public: S_Agent a []; class=class="str">"cmt">//agent class="kw">public: class="type">class="kw">double rangeMax []; class=class="str">"cmt">//maximum search range class="kw">public: class="type">class="kw">double rangeMin []; class=class="str">"cmt">//manimum search range class="kw">public: class="type">class="kw">double rangeStep []; class=class="str">"cmt">//step search class="kw">public: class="type">void Init(const class="type">int coordsP, class=class="str">"cmt">//coordinates number const class="type">int popSizeP, class=class="str">"cmt">//population size