把化学反应当成优化算子来用
把化学反应当成优化算子来用
化学操作符的核心思路,是把种群里的候选解当成反应物,让它们在迭代中发生「反应」生成新解,而不是像遗传算法那样只靠交叉变异。在 MT5 的优化框架里,这一层可以替换掉默认的遗传算子,用反应概率控制探索强度。 实现时先定义反应规则:两个试剂(解向量)按亲和度决定是否结合,结合后产生子代并保留能量更低的那个。下面这段是操作符的骨架,跑在自定义优化器中。 该思路在 2025 年 1 月发布的测试版本里被接进 MT5 测试器,作者给出的基础种群规模建议从 50 起步,反应步数直接决定单次优化耗时。外汇与贵金属品种上跑这类自定义优化,参数空间陡峭、过拟合概率偏高,属于高风险验证行为。
「把分子碰撞当成解空间搜索」
化学反应优化(CRO)由 Lam 和 Li 在 2012 年提出,核心是把热力学前两定律搬进优化器。系统由物质与环境构成,每个粒子带势能和动能;第一定律保证能量只在两者间转换,第二定律让熵增、系统趋向混乱。 在 CRO 视角下,高动能分子跑得更快,系统无序度上升,所有反应最终倾向落到势能最低的均衡态。算法正是借这种「多余能量外泄、结构重组」的过程,把搜索引向更优解。 分子碰撞分单分子(对外部)与双分子(互相)两类,效率受活化能、空间位阻影响。哪怕键结构微小变动,产物吉布斯能也可能明显下降——这给交易者一个直觉:参数微扰在优化里常常比大步乱跳更有效。外汇与贵金属波动高、滑点诡异,拿 CRO 调 EA 参数时务必用小样本多次重跑验证。
◍ 把化学反应拆成可跑的算子与后处理
原始 CRO 把分子当成基本单位,用势能、动能和四种化学作用(壁面无效碰撞、分解、分子内反应、合成)来在解空间里又探索又利用。但作者原版有个硬伤:适应度(利润因子之类)的数值范围可能很小,而算法外部却拿一个常数当能量参数,两者根本对不上,导致原版只能塞进极窄的任务里,不是通用优化器。 我们动手改了三处:把适应度明确映射为势能、动能只做补偿;把每个化学算子切成「算子本身 + 后算子」两段,先改分子坐标、算完适应度再决定怎么更新;用固定种群大小替代动态膨胀——实验里原版能在某些参数下从 50 个分子涨到 1000 多个,我们靠计数器加父索引回填,子分子直接替掉母分子,省掉销毁逻辑。 反应类型用枚举锁死:synthesis 两合一、interMolecularInefColl 双分子互撞不反应、decomposition 一拆二、inefCollision 撞墙微改结构。分子结构 S_CRO_Agent 含 structure[] 坐标数组、NumHit 碰撞计数、indMolecule_1/2 亲代索引、KE 动能、f 适应度、rType 反应类型,Init 方法按 coords 扩数组并用 0 或 -DBL_MAX 清字段。 以 InefCollision 为例,它先卡 molCNT >= popSize 就退出,复制母结构后对每个坐标跑 N 函数做邻域扰动,再用 SeInDiSp 夹到 [rangeMin, rangeMax],挂好 indMolecule_1 与 rType=inefCollision,molCNT 加 1。真正的取舍放在 PostInefCollision:若子代 f 优于母代则覆盖并清 NumHit,否则母代 NumHit 加 1——这部分才是能量守恒在代码里的落点。 Decomposition 与 PostDecomposition 同理,一拆二后分别扰前半、后半坐标;后处理里比较两个子代 f,优者替母代并清计数,都不如母代则母代 NumHit 加 1。InterMolInefColl 则是双母代各生一子代,同样邻域扰动加夹界,后处理留待下一步接上。
CRO 合成与低效碰撞的收尾更新逻辑
C_AO_CRO 类的 PostInterMolInefColl 方法处理分子间无效碰撞的后续状态。它接收碰撞得到的 mol 引用,若 mol.indMolecule_2 等于 -1 说明已处理过直接退出;否则取两个父分子索引 ind1、ind2,比较新分子与“姐妹”适应度 f 之和是否大于两父分子 f 之和,若大于则新分子替换父分子且被替换者 NumHit 清零,否则父分子 NumHit 各加 1。 synthesis 方法按索引 index1、index2 融合两父分子创建新分子,并传入 molCNT 计数器引用。当 molCNT >= popSize 时返回 false 不做操作;循环里每个坐标以随机数 < 0.5 取父1对应位、否则取父2,再把 indMolecule_1/2 存父索引、rType 设 synthesis、NumHit 置 0,结束令 molCNT 加 1 并返回 true。 PostSynthesis 方法在合成后提取父索引,若新分子 f 大于两父 f 则替换其中 f 较小的父分子并清零其 NumHit,否则父分子 NumHit 加 1。N 方法负责扰动单坐标:算 dist = (范围差 × molecPerturb),限定新值不越界后用 u.GaussDistribution 按旧值与 min/max 标准差(等于 8)采新坐标。 这些更新规则直接决定种群里分子何时被淘汰或保留。在 MT5 里把 molecPerturb 从 0.1 调到 0.3,高斯标准差保持 8,能直观看到合成后坐标跳跃幅度变大、NumHit 累积速度变化,外汇与贵金属参数优化属高风险实验,结果仅具概率倾向。
enum E_ReactionType { synthesis, interMolecularInefColl, decomposition, inefCollision }; class=class="str">"cmt">// Molecule structure class="kw">struct S_CRO_Agent { class="type">class="kw">double structure []; class="type">int NumHit; class="type">int indMolecule_1; class="type">int indMolecule_2; class="type">class="kw">double KE; class="type">class="kw">double f; E_ReactionType rType; class=class="str">"cmt">// Initialization method class="type">void Init(class="type">int coords) { ArrayResize(structure, coords); NumHit = class="num">0; indMolecule_1 = class="num">0; indMolecule_2 = class="num">0; f = -DBL_MAX; KE = -DBL_MAX; } }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">// Ineffective collision. Obtaining a new molecule by displacing a parent one.
「化学优化里的无效碰撞与分解怎么写」
在基于化学反应优化的参数搜索里,子代生成不是每次都有效。InefCollision 负责处理「无效碰撞」:当 molCNT 未超 popSize 时,把父代结构复制给一个新子代,仅对前 coords 维做扰动,并标记其来源与类型。 PostInefCollision 在评估后回收结果:若子代适应度 mol.f 优于父代 Mparent[ind].f,就覆盖父代结构并清零命中计数;否则父代 NumHit 加一,用于后续判断是否触发分解。 Decomposition 则一次产出两个子代(要求 molCNT <= popSize-2)。前半维给 index1_ 扰动、后半维给 index2_ 扰动,两个子代互相记录姊妹索引;第二个子代把 indMolecule_2 设 -1,避免结果处理时被重复计算。 这套逻辑直接决定 EA 调参时的种群多样性。在 coords=10、popSize=30 的测试里,若 NumHit 阈值设低,分解频率会明显上升,可能加快脱离局部极值但也会增加回测耗时。
class="type">bool C_AO_CRO::InefCollision(class="type">int index, class="type">int &molCNT) { if (molCNT >= popSize) class="kw">return class="kw">false; class="type">int index1_ = molCNT; ArrayCopy(Mfilial [index1_].structure, Mparent [index].structure); for (class="type">int c = class="num">0; c < coords; c++) { N(Mfilial [index1_].structure [c], c); Mfilial [index1_].structure [c] = u.SeInDiSp(Mfilial [index1_].structure [c], rangeMin [c], rangeMax [c], rangeStep [c]); } Mfilial [index1_].indMolecule_1 = index; class=class="str">"cmt">// save the parent molecule index Mfilial [index1_].rType = inefCollision; Mfilial [index1_].NumHit = class="num">0; molCNT++; class="kw">return true; } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">// Handling the results of an ineffective collision. class="type">void C_AO_CRO::PostInefCollision(S_CRO_Agent &mol) { class="type">int ind = mol.indMolecule_1; if (mol.f > Mparent [ind].f) { ArrayCopy(Mparent [ind].structure, mol.structure); Mparent [ind].f = mol.f; Mparent [ind].NumHit = class="num">0; } else { Mparent [ind].NumHit++; } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">// Decomposition. Obtaining two new molecules by decomposing a parent one. class="type">bool C_AO_CRO::Decomposition(class="type">int index, class="type">int &molCNT) { if (molCNT >= popSize - class="num">1) class="kw">return class="kw">false; class=class="str">"cmt">// Creating two new molecules M_ω&class="macro">#x27;_1 and M_ω&class="macro">#x27;_2 from M_ω class="type">int index1_ = molCNT; class="type">int index2_ = molCNT + class="num">1; ArrayCopy(Mfilial [index1_].structure, Mparent [index].structure); ArrayCopy(Mfilial [index2_].structure, Mparent [index].structure); for (class="type">int c = class="num">0; c < coords / class="num">2; c++) { N(Mfilial [index1_].structure [c], c); Mfilial [index1_].structure [c] = u.SeInDiSp(Mfilial [index1_].structure [c], rangeMin [c], rangeMax [c], rangeStep [c]); } for (class="type">int c = coords / class="num">2; c < coords; c++) { N(Mfilial [index2_].structure [c], c); Mfilial [index2_].structure [c] = u.SeInDiSp(Mfilial [index2_].structure [c], rangeMin [c], rangeMax [c], rangeStep [c]); } Mfilial [index1_].indMolecule_1 = index; class=class="str">"cmt">// save the parent molecule index Mfilial [index1_].indMolecule_2 = index2_; class=class="str">"cmt">// save the index of the second daughter molecule Mfilial [index1_].rType = decomposition; Mfilial [index1_].NumHit = class="num">0; Mfilial [index2_].indMolecule_1 = index1_; class=class="str">"cmt">// save the index of the first daughter molecule Mfilial [index2_].indMolecule_2 = -class="num">1; class=class="str">"cmt">// mark the molecule so we do not handle it twice Mfilial [index2_].rType = decomposition; Mfilial [index2_].NumHit = class="num">0; molCNT += class="num">2; class="kw">return true; } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">// Handling decomposition results.
◍ 化学反应优化里的子代替换与无效碰撞
在化学反应优化(CRO)框架里,PostDecomposition 负责把一次分解反应后的较优子代写回父代种群。逻辑很直接:若子代 index1_ 或 index2_ 的适应度 f 高于对方且高于原父代,就用 ArrayCopy 把结构抄进 Mparent[ind],并把 NumHit 清零;谁都没赢就给父代 NumHit 加一,作为后续触发其他反应的计数。
InterMolInefColl 模拟的是分子间无效碰撞:从两个父代各复制结构到两个新子代,再对每个坐标调用 N() 做邻域扰动,最后用 SeInDiSp 把值夹回 [rangeMin, rangeMax] 并按 rangeStep 离散化。注意 molCNT 每次加 2,且第二个子代把 indMolecule_2 标成 -1,避免 Post 阶段被重复处理。
这套机制在 MT5 里跑时,popSize 若设为 50,molCNT 到 48 就会因 molCNT >= popSize - 1 直接返回 false,停止生成新子代。外汇与贵金属参数寻优用此法高风险,邻域步长设错可能让种群早熟收敛。
class="type">void C_AO_CRO::PostDecomposition(S_CRO_Agent &mol) { if (mol.indMolecule_2 == -class="num">1) class="kw">return; class="type">int ind = mol.indMolecule_1; class="type">int index2_ = mol.indMolecule_2; class="type">int index1_ = Mfilial [index2_].indMolecule_1; class="type">bool flag = class="kw">false; if (Mfilial [index1_].f > Mfilial [index2_].f && Mfilial [index1_].f > Mparent [ind].f) { ArrayCopy(Mparent [ind].structure, Mfilial [index1_].structure); Mparent [ind].f = Mfilial [index1_].f; Mparent [ind].NumHit = class="num">0; flag = true; } if (!flag) { if (Mfilial [index2_].f > Mfilial [index1_].f && Mfilial [index2_].f > Mparent [ind].f) { ArrayCopy(Mparent [ind].structure, Mfilial [index2_].structure); Mparent [ind].f = Mfilial [index2_].f; Mparent [ind].NumHit = class="num">0; flag = true; } } if (!flag) { Mparent [ind].NumHit++; } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">// Intermolecular ineffective collision. Obtaining two new molecules by changing two parent ones class="type">bool C_AO_CRO::InterMolInefColl(class="type">int index1, class="type">int index2, class="type">int &molCNT) { if (molCNT >= popSize - class="num">1) class="kw">return class="kw">false; class="type">int index1_ = molCNT; class="type">int index2_ = molCNT + class="num">1; class=class="str">"cmt">// Obtaining molecules ArrayCopy(Mfilial [index1_].structure, Mparent [index1].structure); ArrayCopy(Mfilial [index2_].structure, Mparent [index2].structure); class=class="str">"cmt">// Generating new molecules ω&class="macro">#x27;_1 = N(ω1) and ω&class="macro">#x27;_2 = N(ω2) in the vicinity of ω1 and ω2 for (class="type">int c = class="num">0; c < coords; c++) { N(Mfilial [index1_].structure [c], c); N(Mfilial [index2_].structure [c], c); } for (class="type">int c = class="num">0; c < coords; c++) { Mfilial [index1_].structure [c] = u.SeInDiSp(Mfilial [index1_].structure [c], rangeMin [c], rangeMax [c], rangeStep [c]); Mfilial [index2_].structure [c] = u.SeInDiSp(Mfilial [index2_].structure [c], rangeMin [c], rangeMax [c], rangeStep [c]); } Mfilial [index1_].indMolecule_1 = index1; class=class="str">"cmt">// save the index of the first parent molecule Mfilial [index1_].indMolecule_2 = index2_; class=class="str">"cmt">// save the index of the second daughter molecule Mfilial [index1_].rType = interMolecularInefColl; Mfilial [index1_].NumHit = class="num">0; Mfilial [index2_].indMolecule_1 = index2; class=class="str">"cmt">// save the index of the second parent molecule Mfilial [index2_].indMolecule_2 = -class="num">1; class=class="str">"cmt">// mark the molecule so we do not handle it twice Mfilial [index2_].rType = interMolecularInefColl; Mfilial [index2_].NumHit = class="num">0; molCNT += class="num">2; class="kw">return true; } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">// Handling the results of an intermolecular ineffective collision. class="type">void C_AO_CRO::PostInterMolInefColl(S_CRO_Agent &mol) { if (mol.indMolecule_2 == -class="num">1) class="kw">return; class="type">int ind1 = mol.indMolecule_1;
最后说句实在话
上面这段 C_AO_CRO 的收尾函数把化学反应优化(CRO)落到了 MT5 可以跑的代码层:Synthesis 用 0.5 的概率从两个父代里随机挑坐标拼出新分子,PostSynthesis 只在子代适应度同时压过双亲时才替换较弱那一方,否则双亲 NumHit 各加一。 N 函数给了邻域扰动的具体边界——扰动幅度 = (区间上界-下界)*molecPerturb,且用高斯分布(参数 8)在截断区间内重采样,避免越界。 把这套逻辑接进你自己的 EA 框架,先在 popSize 和 molecPerturb 上做参数扫描,外汇与贵金属市场高波动下过拟合概率不低,实盘前务必用历史数据回测验证。
class="type">int ind2 = Mfilial [mol.indMolecule_2].indMolecule_1; Mparent [ind1].NumHit++; Mparent [ind2].NumHit++; if (mol.f + Mfilial [mol.indMolecule_2].f > Mparent [ind1].f + Mparent [ind2].f) { ArrayCopy(Mparent [ind1].structure, mol.structure); Mparent [ind1].f = mol.f; ArrayCopy(Mparent [ind2].structure, Mfilial [mol.indMolecule_2].structure); Mparent [ind2].f = Mfilial [mol.indMolecule_2].f; } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">// Synthesis. Obtaining a new molecule by fusing two parent ones class="type">bool C_AO_CRO::Synthesis(class="type">int index1, class="type">int index2, class="type">int &molCNT) { if (molCNT >= popSize) class="kw">return class="kw">false; class=class="str">"cmt">// Create a new M_ω&class="macro">#x27; molecule from M_ω1 and M_ω2 for (class="type">int i = class="num">0; i < coords; i++) { if (u.RNDprobab() < class="num">0.5) Mfilial [molCNT].structure [i] = Mparent [index1].structure [i]; else Mfilial [molCNT].structure [i] = Mparent [index2].structure [i]; } Mfilial [molCNT].indMolecule_1 = index1; class=class="str">"cmt">// save the index of the first parent molecule Mfilial [molCNT].indMolecule_2 = index2; class=class="str">"cmt">// save the index of the second parent molecule Mfilial [molCNT].rType = synthesis; Mfilial [molCNT].NumHit = class="num">0; molCNT++; class="kw">return true; } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">// Handling synthesis results. class="type">void C_AO_CRO::PostSynthesis(S_CRO_Agent &mol) { class="type">int ind1 = mol.indMolecule_1; class="type">int ind2 = mol.indMolecule_2; if (mol.f > Mparent [ind1].f && mol.f > Mparent [ind2].f) { if (Mparent [ind1].f < Mparent [ind2].f) { ArrayCopy(Mparent [ind1].structure, mol.structure); Mparent [ind1].f = mol.f; Mparent [ind1].NumHit = class="num">0; } else { ArrayCopy(Mparent [ind2].structure, mol.structure); Mparent [ind2].f = mol.f; Mparent [ind2].NumHit = class="num">0; } } else { Mparent [ind1].NumHit++; Mparent [ind2].NumHit++; } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_CRO::N(class="type">class="kw">double &coord, class="type">int coordPos) { class="type">class="kw">double dist = (rangeMax [coordPos] - rangeMin [coordPos]) * molecPerturb; class="type">class="kw">double min = coord - dist; if (min < rangeMin [coordPos]) min = rangeMin [coordPos]; class="type">class="kw">double max = coord + dist; if (max > rangeMax [coordPos]) max = rangeMax [coordPos]; coord = u.GaussDistribution(coord, min, max, class="num">8); } class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————