中心引力优化(CFO)算法(基础篇)
「用中心引力优化给EA寻参」
中心引力优化(CFO)是一种受物理引力启发的群体智能寻参思路,在 MT5 的策略测试中可绕过传统的网格遍历,用较少评估次数逼近较优参数区。它把每个候选参数集看作一个带质量的粒子,质量由适应度(如净值或夏普)决定,粒子间按引力相互吸引并迭代更新位置。 在外汇与贵金属品种上跑 CFO 要清醒:这类市场高杠杆、跳空频繁,回测甜点参数在实盘可能迅速失效,任何优化结果都只是「概率上更优」,不是必胜解。 实际落地时,可在 MT5 的「智能交易系统测试器」里把优化模式切到对应算法(若构建版支持),先用小样本历史(例如 2024 年 1 月至 6 月 EURUSD M15)跑 50 代、种群 30,观察适应度曲线是否在 20 代内收敛;若未收敛,再考虑扩种群而非盲目加代。
◍ 把引力搬进优化空间
CFO(中心引力优化)把天体引力那套运动学直接搬进了数值优化。解空间里撒一批探测器,每个探测器带一个“质量”,质量正比于它所在点的目标函数值——好解就是大质量天体,差解是小质量天体,前者对后者产生虚拟引力场。 跟遗传、粒子群这类靠随机数翻滚的元启发式不同,CFO 核心循环是确定性的:加速度由其他探测器的合力决定,位移走牛顿运动学方程,不投骰子。这意味着同一起点同参数,跑出来结果可复现,方便在 MT5 策略优化器外先做离线条带对比。 外汇与贵金属品种波动率高、过拟合风险大,用这类确定性算法搜参数,仍要对不同年份样本做前向验证,不能默认历史引力轨迹代表未来。
把中心引力优化写成 MT5 类
中心引力优化(CFO)在 MT5 里落地时,核心是一个继承结构:S_CFO_Agent 结构体继承 S_AO_Agent,自动拿到坐标数组 c 与适应度 f,自己只补一个动态数组 a 存加速度。Init() 会按传入 coords 维度重设 c 与 a 的大小,把 a 清 0,并把 f 设成 DBL_MIN,保证任何真实评价值都能覆盖初始值。 C_AO_CFO 类包住整个种群与迭代逻辑。构造时写入 popSize、g、alpha、beta、initialFrep、finalFrep、noiseFactor;其中 frep 重定位因子从 initialFrep 起算,后面随代数线性降到 finalFrep。Init() 先调 StandardInit 校验参数区间,失败直接返 false,通过后才把 probe[] 扩到 popSize 并逐个体初始化。 Moving() 是每代主步:epochNow 自增,首次且 revision=false 时只做初始分布然后返回;之后把适应度拷到临时数组,调 UpdateRepFactor 与 CalculateAccelerations,再 UpdatePositions 并回写主数组。注意原始 CFO 是确定性算法——同初始分布跑两次结果一致,但作者在 UpdatePositions 里加了随 epoch 衰减的噪声偏移(currentNoiseFactor 从 noiseFactor 线性缩),用来躲局部极值,这让它偏离了纯确定性。 引力计算在 CalculateAccelerations 内:对每对 p、k,仅当 probe[k].f - probe[p].f > 0 才产生单向吸引;距离用 CalculateDistanceSquared 取平方省 sqrt,加速度累加公式为 probe[p].a[c] += g * MathPow(massDiff, alpha) * direction / MathPow(distance, beta),alpha 与 beta 原文默认都取 2。边界外位置由 UpdatePositions 里的 rangeMin/rangeMax 夹回,并可用 SeInDiSp 吸附到 rangeStep 倍数。外汇或贵金属参数寻优用这套高风险,实盘前务必在 MT5 策略测试器用历史数据复跑确认收敛行为。
「Revision 如何锁定群体最优解」
循环跑完之后,距离计算函数交回的是两点间距平方的累加值(sum),这个量在 CFO 里只是引力评估的底料,真正决定进化方向的是 Revision() 对全局最优的接管。 Revision 方法会挨个扫一遍 popSize 个代理,默认群体规模是 30。对每个代理 p,只要 a[p].f 比当前记录的 fB 更大,就把 fB 刷新成该代理的适应度,同时把它的坐标 cB 拷进全局缓存。 这么做的直接后果是:只要某一代理在本次迭代里把结果往前推了一点,全局参数立刻跟进,不会漏掉中途出现的局部峰值。你在 MT5 里把 popSize 从 30 调到 60,Revision 的扫描成本翻倍,但错过优解的概率倾向降低,外汇与贵金属参数优化属高风险操作,实盘前请用历史数据验证。 代理结构体里 f 初始化为 -DBL_MAX,意味着第一个被评估的合法解必然上位,不用额外写“首解保护”逻辑。
class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//--- CFO probe structure class="kw">struct S_CFO_Agent : class="kw">public S_AO_Agent { class="type">class="kw">double a []; class=class="str">"cmt">// acceleration vector class="type">void Init(class="type">int coords) { ArrayResize(c, coords); class=class="str">"cmt">// coordinates ArrayResize(a, coords); class=class="str">"cmt">// acceleration ArrayInitialize(a, class="num">0.0); class=class="str">"cmt">// reset accelerations f = -DBL_MAX; class=class="str">"cmt">// fitness function value } }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//--- Main class of the CFO algorithm class C_AO_CFO : class="kw">public C_AO { class="kw">public: class=class="str">"cmt">//-------------------------------------------------------------------- ~C_AO_CFO() { } C_AO_CFO() { ao_name = "CFO"; ao_desc = "Central Force Optimization"; ao_link = "[MQL5官方文档] popSize = class="num">30; class=class="str">"cmt">// number of probes g = class="num">1.0; class=class="str">"cmt">// gravitational constant alpha = class="num">0.1; class=class="str">"cmt">// mass power beta = class="num">0.1; class=class="str">"cmt">// degree for distance initialFrep = class="num">0.9; class=class="str">"cmt">// initial repositioning factor finalFrep = class="num">0.1; class=class="str">"cmt">// final repositioning factor noiseFactor = class="num">1.0; class=class="str">"cmt">// random noise factor frep = initialFrep; class=class="str">"cmt">// current repositioning factor ArrayResize(params, class="num">7); params [class="num">0].name = "popSize"; params [class="num">0].val = popSize; params [class="num">1].name = "g"; params [class="num">1].val = g; params [class="num">2].name = "alpha"; params [class="num">2].val = alpha; params [class="num">3].name = "beta"; params [class="num">3].val = beta; params [class="num">4].name = "initialFrep"; params [class="num">4].val = initialFrep; params [class="num">5].name = "finalFrep"; params [class="num">5].val = finalFrep; params [class="num">6].name = "noiseFactor"; params [class="num">6].val = noiseFactor; } class="type">void SetParams() { popSize = (class="type">int)MathMax(class="num">1, params [class="num">0].val); g = params [class="num">1].val;