种群优化算法:类电磁算法(EM - ElectroMagnetism)(基础篇)
「用类电磁机制给参数找优解」
类电磁算法(EM)把每个候选参数集当成带电粒子,用库仑式的引斥力在解空间里扰动搜索,属于种群优化的一类。它不靠梯度,适合 MT5 里那种目标函数坑洼、不可导的EA参数寻优。 在 2023-08-29 发布的示例里,作者用该算法跑过一轮测试,样本计数显示 1 331 次观测,说明解空间被扫得不算浅。外汇与贵金属杠杆高,回测漂亮不等于实盘能复现,任何优化结果都只是提高概率。 真要验证,开 MT5 把下面这段 EM 的电荷力计算抄进脚本,改两个参数就能看粒子怎么动。
◍ 类电磁算法从物理机制到优化工具
类电磁(EM)算法是 2003 年由 I. Birbil 与 S.C. Fang 提出的元启发式搜索方法,模拟物理空间中带电粒子的吸引与排斥行为,属于带随机噪声的群体进化算法。它面向连续域无约束非线性优化,靠电荷间相互作用在解空间里探索,已被用于不少复杂全局优化场景。 其物理底层只有正、负两类电荷,电荷量可正可负;电磁场能以无线电波传信息,地球磁场则帮我们挡掉太阳风与宇宙射线。自然界四种基本力里,电磁力与引力、弱力、强力并列,这也是该算法命名与机制来源的硬背景。 对交易者而言,EM 的价值不在物理现象本身,而在它提供了一种不依赖梯度、靠种群随机扰动找最优参数的思路。MT5 里若要做参数寻优,可借这类机制替代穷举,但外汇/贵金属市场高风险,任何优化结果都只是概率倾向,需实盘前用历史数据严格验证。
把解当电荷来搜:EM 优化器的粒子与力学
电磁优化(EM)把每个候选解看成搜索空间里的带电粒子,坐标向量决定位置,目标函数输出映射成电荷。目标值越好的粒子电荷越强,对邻域施加吸引力;目标值差的粒子产生排斥力,把弱解推离劣质区。算法从一组随机电荷起步,每次迭代按库仑式相互作用更新坐标,若干轮后解群会向全局较优区收敛。 逻辑单元是 S_Particle 结构:c[] 存坐标,C 是电荷,f 是适应度,R[] 是到其它粒子的距离,F[] 是合力矢量。C_AO_EM 类持有粒子数组 p[]、各维 rangeMax/rangeMin/rangeStep、最佳坐标 cB 与最佳值 fB;Init() 负责按坐标数、粒子数、环境常数和步长开数组并置初值,Moving(int iter) 管每轮位移,Revision() 管越界修订与最优跟踪。 Moving() 先判初始化:未初始化就给每个粒子随机坐标、清零电荷,并算各维极差向量 vect[]。已初始化则按「该粒子距全局最大偏差 / 所有粒子距全局最大偏差之和」归一化出电荷(偏差和为零则电荷为零)。接着清空距离与力数组,两两算欧氏距离,用库仑定律叠加受力——力与电荷乘积成正比、与距离成反比。作者原建议对合力归一化,但我实测不归一化结果更好,下面代码即未归一化版本。 粒子无质量,所以没有加速度阶段,直接按力矢量平移到新坐标,位移受 range 与 step 限制。Revision() 在每轮末尾扫一遍粒子,若某粒子 f 大于当前 fB 就刷新 fB 并把坐标拷进 cB,相当于在线记账最优解。SeInDiSp() 则是把越界变量夹回 [InMin,InMax] 并按步长对齐网格,避免无效探索。 外汇与贵金属参数寻优用这套有高波动风险,回测过拟合概率不低,建议拿 MT5 策略测试器先跑 3~5 个品种验证。
class="kw">struct S_Particle { class="type">class="kw">double c []; class=class="str">"cmt">//coordinates class="type">class="kw">double C; class=class="str">"cmt">//charge class="type">class="kw">double f; class=class="str">"cmt">//fitness class="type">class="kw">double R []; class=class="str">"cmt">//euclidean distance to other particles class="type">class="kw">double F []; class=class="str">"cmt">//force vector }; class C_AO_EM { class="kw">public: S_Particle p []; class=class="str">"cmt">//particles 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">//minimum search range class="kw">public: class="type">class="kw">double rangeStep []; class=class="str">"cmt">//step search 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: class="type">void Init(class="kw">const class="type">int coordinatesNumberP, class=class="str">"cmt">//coordinates number class="kw">const class="type">int particlesNumberP, class=class="str">"cmt">//particles number
「极值优化器的初始化落地」
上面这段是 C_AO_EM 类的构造尾部与 Init 方法,把蚁群式极值搜索器的参数和内存一次性钉死。coordinatesNumber 决定搜索空间维度,particlesNumber 是粒子数,envConstant、movConstant、exponent 三者共同控制游走步长与环境牵引强度,外汇与贵金属行情里这类参数对噪声极敏感,调错容易过拟合,属高风险操作。 Init 里第一行 MathSrand((int)GetMicrosecondCount()) 用微秒级种子重置随机发生器,避免每次回测得到同一串伪随机轨迹。随后 fB 置为 -DBL_MAX 作为历史最优值的初始负无穷,revision 置 false 表示尚未进入修正态。 五个 ArrayResize 分别给 rangeMax、rangeMin、rangeStep、vect 按坐标数开内存,给 p 按粒子数开内存;紧接着的 for 循环再给每个粒子的 c 数组按坐标数扩容。你在 MT5 里新建 EA 时,若粒子数设 50、坐标数设 4,这里就会实打实分配 50×4 的双精度槽位,开终端调试能直接看到数组大小变化。
class="kw">const class="type">class="kw">double envConstantP, class=class="str">"cmt">//environmental constant class="kw">const class="type">class="kw">double movConstantP, class=class="str">"cmt">//movement step class="kw">const class="type">class="kw">double exponentP); class=class="str">"cmt">//exponent class="kw">public: class="type">void Moving(); class="kw">public: class="type">void Revision(); class=class="str">"cmt">//---------------------------------------------------------------------------- class="kw">private: class="type">int coordinatesNumber; class=class="str">"cmt">//coordinates number class="kw">private: class="type">int particlesNumber; class=class="str">"cmt">//particles number class="kw">private: class="type">class="kw">double envConstant; class=class="str">"cmt">//environmental constant class="kw">private: class="type">class="kw">double movConstant; class=class="str">"cmt">//movement step class="kw">private: class="type">class="kw">double exponent; class=class="str">"cmt">//exponent class="kw">private: class="type">class="kw">double vect []; class=class="str">"cmt">//vector class="kw">private: class="type">bool revision; 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=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_EM::Init(class="kw">const class="type">int coordinatesNumberP, class=class="str">"cmt">//coordinates number class="kw">const class="type">int particlesNumberP, class=class="str">"cmt">//particles number class="kw">const class="type">class="kw">double envConstantP, class=class="str">"cmt">//environmental constant class="kw">const class="type">class="kw">double movConstantP, class=class="str">"cmt">//movement step class="kw">const class="type">class="kw">double exponentP) class=class="str">"cmt">//exponent { MathSrand((class="type">int)GetMicrosecondCount()); class=class="str">"cmt">// reset of the generator fB = -DBL_MAX; revision = class="kw">false; coordinatesNumber = coordinatesNumberP; particlesNumber = particlesNumberP; envConstant = envConstantP; movConstant = movConstantP; exponent = exponentP; ArrayResize(rangeMax, coordinatesNumber); ArrayResize(rangeMin, coordinatesNumber); ArrayResize(rangeStep, coordinatesNumber); ArrayResize(vect, coordinatesNumber); ArrayResize(p, particlesNumber); for (class="type">int i = class="num">0; i < particlesNumber; i++) { ArrayResize(p [i].c, coordinatesNumber);