并行粒子群优化·进阶篇
(2/3)· 遗传算法不够用?这篇把 PSO 类结构与并行骨架拆给你看
接上篇铺垫的优化器动机,我们继续深挖:标准测试器只给枚举和遗传两条路,想换粒子群就得自己造轮子。多数人卡在不会把算法类嵌进 MT5 代理体系,结果本地多核白白闲置。
「用基准函数验证粒子群优化没跑偏」
拿 testpso.mq5 脚本跑一遍,是确认 PSO 优化真正生效的最低成本办法。它依赖 ParticleSwarmParallel.mqh,里面除了基础类,还塞了后面要拆的改进版逻辑。 测试框架走的是 OOP:基类 BaseFunctor 管目标函数,派生类对象在构造时通过 PSOTests::register 自动挂进测试清单,run 方法统一触发所有对象的 test。脚本里内置了 rosenbrock、griewank、sphere 等经典基准函数。 标准基准函数都是求最小化,但这套算法按最大化搜 EA 性能,所以 calculate 里统一取负。函数连续、无离散步长。sphere 示例里搜索范围 ±100,三维期望极值就是 (0,0,0)。 实跑日志能直接看穿精度:sphere 在 99 个 cycle 后目标值压到 -8.16e-07,输出坐标约 (-0.00059, -0.00048, 0.00048),贴近精确解。因为粒子随机初始化,每次数值会抖,精度由输入参数决定。 群规模与组数由经验规则自动选,日志行 PSO[3] created: 15/3 表示 3 维空间、15 粒子、3 组。你改 BaseFunctor 构造里的维数或循环次数,就能在 MT5 里复现不同收敛曲线。外汇与贵金属实盘接这套优化属高风险,回测优不等同实盘稳。
class BaseFunctor: class="kw">public Functor { class="kw">protected: class="kw">const class="type">int params; class="type">class="kw">double max[], min[], steps[]; class="kw">public: BaseFunctor(class="kw">const class="type">int p): params(p) class=class="str">"cmt">// number of parameters { ArrayResize(max, params); ArrayResize(min, params); ArrayResize(steps, params); ArrayInitialize(steps, class="num">0); PSOTests::register(&this); } class="kw">virtual class="type">void test(class="kw">const class="type">int loop) class=class="str">"cmt">// worker method { Swarm swarm(params, max, min, steps); swarm.optimize(this, loop); class="type">class="kw">double result[]; swarm.getSolution(result); for(class="type">int i = class="num">0; i < params; i++) { Print(i, " ", result[i]); } } }; class PSOTests { class="kw">static BaseFunctor *testCases[]; class="kw">public: class="kw">static class="type">void register(BaseFunctor *f) { class="type">int n = ArraySize(testCases); ArrayResize(testCases, n + class="num">1); testCases[n] = f; } class="kw">static class="type">void run(class="kw">const class="type">int loop = class="num">100) { for(class="type">int i = class="num">0; i < ArraySize(testCases); i++) { testCases[i].test(loop); } } }; class Sphere: class="kw">public BaseFunctor { class="kw">public: Sphere(): BaseFunctor(class="num">3) class=class="str">"cmt">// expected global minimum(class="num">0, class="num">0, class="num">0) { for(class="type">int i = class="num">0; i < params; i++) { max[i] = class="num">100; min[i] = -class="num">100; } } class="kw">virtual class="type">void test(class="kw">const class="type">int loop) { Print("Optimizing " + class="kw">typename(this)); BaseFunctor::test(loop); } class="kw">virtual class="type">class="kw">double calculate(class="kw">const class="type">class="kw">double &vec[]) { class="type">int dim = ArraySize(vec); class="type">class="kw">double sum = class="num">0; for(class="type">int i = class="num">0; i < dim; i++) sum += pow(vec[i], class="num">2); class="kw">return -sum; class=class="str">"cmt">// negative for maximization } }; class="type">void OnStart() { PSOTests::Sphere sphere; PSOTests::Griewank griewank; PSOTests::Rosenbrock rosenbrock; PSOTests::run(); }
粒子群跑完千次后的收敛残差
一段 PSO 寻优日志里,第 90 轮迭代完成后 skipped 0 of 10,累计残差 -0.0005473814163781119;第 99 轮 skipped 仍为 0,残差收敛到 -7.255520122486163e-06,量级已压到 1e-6。 PSO Finished 982 of 1000 planned calculations: true,说明计划千次计算里实际跑了 982 次,未达满额但返回成功状态,这种截断在 MT5 策略测试器里常见于步长约束或早停阈值触发。 末尾两行是寻得的最优参数映射:索引 0 对应 1.001858172119364,索引 1 对应 1.003524791491219,可直接抄进输入参数做复验。外汇与贵金属参数寻优属高风险行为,残差小不代表实盘稳健,请用历史数据重跑确认。
Cycle class="num">90 done, skipped class="num">0 of class="num">10 / -class="num">0.0005473814163781119 Cycle class="num">99 done, skipped class="num">0 of class="num">10 / -class="num">7.255520122486163e-06 PSO Finished class="num">982 of class="num">1000 planned calculations: true class="num">0 class="num">1.001858172119364 class="num">1 class="num">1.003524791491219
◍ 把粒子群拆进多核测试代理
单线程跑 PSO 只是验证思路,MT5 终端真正值钱的是能把计算摊到所有处理器核心上。原 Swarm 类在一个实例里管多个组,没法直接并行,得改成交由多个 EA 实例各带一个组、各自占一个本地测试代理,组间再交换已探过的参数点。 并行组数至少等于核心数,也可以更高但建议取核心数的整数倍;空间维度越大往往越需要更多组,不过核心数太少会让简单测试也卡住。每个实例跑完用 getSolution 把小群结果以帧发回终端,终端挑最优过程,这部分和标准优化通道一致。 组间不能靠终端回传数据——测试器架构只允许代理到终端的受控传输,反向是封闭分发。所以索引二叉树改用 FILE_COMMON 共享文件夹落地:每个代理写自己的索引文件,别的进程随时读并并回本地树。MQL 里文件不关闭别的进程读不到刷新内容,FILE_SHARE_READ/WRITE 和 FileFlush 都救不了,因此必须写完就关。 树导出用了 visitor 模式,Exporter 按遍历顺序把节点值写 CSV 一行一个。默认不排有序遍历(PSO_DEBUG_BINTREE 关着),而是用节点层级和值异或决定左右访问顺序,避免从文件重载时退化成长分支。等跑过的进程变多,共享历史里唯一点不断并回本地索引,后续组启动后 calculate 命中已查点概率上升,PSO 周期倾向加速,接近空间全覆盖时 _unique 会逼近可能参数组合数。 下面这段是核心改造的 MQL5 骨架,注意 Swarm 构造里 MQL_OPTIMIZATION 下把组数硬编码成 1,以及 Exporter 的无序遍历分支。
Swarm(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> size, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> globals, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> params, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">double</span> &max[], <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">double</span> &min[], <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">double</span> &step[]) { <span class="keyword">if</span>(<span class="functions">MQLInfoInteger</span>(<span class="macro">MQL_OPTIMIZATION</span>)) { init(size == class="num">0 ? params * AUTO_SIZE_FACTOR : size, class="num">1, params, max, min, step); } ... <span class="keyword">class="kw">template</span><<span class="keyword">class="kw">typename</span> T> <span class="keyword">class </span>Visitor { <span class="keyword">class="kw">public</span>: <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span> visit(TreeNode<T> *node) = class="num">0; }; <span class="keyword">class="kw">template</span><<span class="keyword">class="kw">typename</span> T> <span class="keyword">class </span>Exporter: <span class="keyword">class="kw">public</span> Visitor<T> { <span class="keyword">class="kw">private</span>: <span class="keyword">class="type">int</span> file; <span class="keyword">class="type">uint</span> level; <span class="keyword">class="kw">public</span>: Exporter(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> name): level(class="num">0) { file = <span class="functions">FileOpen</span>(name, <span class="macro">FILE_READ</span> | <span class="macro">FILE_WRITE</span> | <span class="macro">FILE_CSV</span> | <span class="macro">FILE_ANSI</span> | <span class="macro">FILE_SHARE_READ</span>| <span class="macro">FILE_SHARE_WRITE</span> | <span class="macro">FILE_COMMON</span>, &class="macro">#x27;,&class="macro">#x27;); } ~Exporter() { <span class="functions">FileClose</span>(file); } <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span> visit(TreeNode<T> *node) <span class="keyword">class="kw">override</span> { <span class="keyword">class="macro">#ifdef</span> PSO_DEBUG_BINTREE <span class="keyword">if</span>(node.getLeft()) visit(node.getLeft()); <span class="functions">FileWrite</span>(file, node.getValue()); <span class="keyword">if</span>(node.getRight()) visit(node.getRight()); <span class="keyword">class="macro">#else</span> <span class="keyword">class="kw">const</span> T v = node.getValue(); <span class="functions">FileWrite</span>(file, v); level++; <span class="keyword">if</span>((level | (<span class="keyword">class="type">uint</span>)v) % class="num">2 == class="num">0) { <span class="keyword">if</span>(node.getLeft()) visit(node.getLeft()); <span class="keyword">if</span>(node.getRight()) visit(node.getRight()); } <span class="keyword">else</span> { <span class="keyword">if</span>(node.getRight()) visit(node.getRight()); <span class="keyword">if</span>(node.getLeft()) visit(node.getLeft()); } level--; <span class="keyword">class="macro">#endif</span> } }; <span class="keyword">class="kw">template</span><<span class="keyword">class="kw">typename</span> T> <span class="keyword">class </span>BinaryTree { ... <span class="keyword">class="type">void</span> visit(Visitor<T> *visitor) { visitor.visit(root); } }; <span class="keyword">class="type">void</span> exportIndex(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> id) { <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> name = sharedName(id); Exporter<<span class="keyword">class="type">class="kw">ulong</span>> exporter(name); index.visit(&exporter); } <span class="keyword">class="macro">#define</span> PPSO_FILE_PREFIX <span class="class="type">class="kw">string">"PPSO-"</span> <span class="keyword">class="type">class="kw">string</span> sharedName(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> id, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> prefix = PPSO_FILE_PREFIX, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> ext = <span class="class="type">class="kw">string">".csv"</span>) { <span class="keyword">class="type">class="kw">ushort</span> array[];
「多核索引的合并与文件读取实现」
这段逻辑解决了一个实际问题:在 MT5 里用多核回测或并行计算时,每个核心会写出带自身编号的索引文件,启动时要把它们并回一张总表。 共享名生成用了终端路径做 crc64 散列,再拼上程序名与核心 id;当 id 为 -1 时退化为通配符形式,方便后面一次性扫全部核心的落盘文件。 restoreIndex 先调 FileFindFirst 用通配符在 FILE_COMMON 目录里捞文件名,再对每个命中文件开一个 FileReader 把内容喂给当前对象;do-while 配 FileFindNext 把目录扫干净后关句柄,返回恒为 true。
| FileReader 构造即 FileOpen,标志位叠了 FILE_READ | FILE_CSV | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_ANSI,说明索引是 ANSI 编码的 CSV,且允许别的进程同时读写——这对实时盯盘下的热恢复很关键。 |
|---|
Swarm::feed 是合并的落点:每读一行先 _read++;若 index.add 返回 false 表示树里还没有该值,计 _restored;否则再试 merge.add,失败则说明是重复值,计 _unique。三个计数器直接暴露了「已读 / 新恢复 / 冗余」的体量,开 MT5 跑一遍就能在日志里看到具体分布。 外汇与贵金属市场高杠杆、滑点无常,任何本地索引恢复机制都只是辅助,不能替代对行情风险的独立判断。
StringToShortArray(TerminalInfoString(TERMINAL_PATH), array); class="kw">const class="type">class="kw">string program = MQLInfoString(MQL_PROGRAM_NAME) + "-"; if(id != -class="num">1) { class="kw">return prefix + program + StringFormat("%08I64X-%04d", crc64(array), id) + ext; } class="kw">return prefix + program + StringFormat("%08I64X-*", crc64(array)) + ext; } class="type">bool restoreIndex() { class="type">class="kw">string name; class="kw">const class="type">class="kw">string filter = sharedName(-class="num">1); class=class="str">"cmt">// use wildcards to merge multiple indices for all cores class="type">long h = FileFindFirst(filter, name, FILE_COMMON); if(h != INVALID_HANDLE) { do { FileReader reader(name, FILE_COMMON); reader.read(this); } while(FileFindNext(h, name)); FileFindClose(h); } class="kw">return true; } class Feed { class="kw">public: class="kw">virtual class="type">bool feed(class="kw">const class="type">int dump) = class="num">0; }; class FileReader { class="kw">protected: class="type">int dump; class="kw">public: FileReader(class="kw">const class="type">class="kw">string name, class="kw">const class="type">int flags = class="num">0) { dump = FileOpen(name, FILE_READ | FILE_CSV | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_ANSI | flags, &class="macro">#x27;,&class="macro">#x27;); } class="kw">virtual class="type">bool isReady() class="kw">const { class="kw">return dump != INVALID_HANDLE; } class="kw">virtual class="type">bool read(Feed &pass) { if(!isReady()) class="kw">return class="kw">false; while(!FileIsEnding(dump)) { if(!pass.feed(dump)) { class="kw">return class="kw">false; } } class="kw">return true; } }; class Swarm: class="kw">public Feed { class="kw">private: ... class="type">int _read; class="type">int _unique; class="type">int _restored; BinaryTree<class="type">class="kw">ulong> merge; class="kw">public: ... class="kw">virtual class="type">bool feed(class="kw">const class="type">int dump) class="kw">override { class="kw">const class="type">class="kw">ulong value = (class="type">class="kw">ulong)FileReadString(dump); _read++; if(!index.add(value)) _restored++; class=class="str">"cmt">// just added into the tree else if(!merge.add(value)) _unique++; class=class="str">"cmt">// was already indexed, hitting _unique in total class="kw">return true; } ...
把粒子群丢进 MT5 测试器做并行压测
把旧脚本改成 PPSO.mq5 的 EA 形态,先不开交易,只在“Math calculations”模式下跑数学函数,目的是验证 PSO 能不能在多线程优化里用。输入里可选 Cycles、SwarmSize、GroupCount,后两个填 0 时由任务维度自动定,不填死。 所有并行活儿从 OnTester 起手:GroupCount 既当测试器迭代组织单位,也当随机种子偏移,保证不同线程里的粒子群初始不一样;按 TestCase 建好 functor 后调 test(),再用 getSolution() 把结果打成帧发回终端。OnTesterPass 负责收帧比大小,OnTesterDeinit 落最终解。 实跑一组:Cycles=100、TestCase=Griewank、SwarmSize=100、GroupCount=10,图表上单线程日志显示第 50 轮目标值已到 -0.00458,第 99 轮收在 -1.34e-05,说明收敛在前半段就基本完成。 换到测试器做优化时,把 GroupCount 设成 0→3、步长 1,生成 4 个组(约等于物理核数),SwarmSize 保持 100。若代理真并行,总规模不变,精度会因多组交叉检查而提升。Griewank 这种算例一两秒就完,计时没意义,真实交易任务再补。 EA 也能挂普通图表,此时走 OnInit+OnTimer 的单线程分支,PSO 照跑只是不并行。下面代码是并行测试核心骨架,注意 FrameAdd 只在优化模式触发,单图表走 Print 直出。
<span class="keyword">enum</span> TEST { Sphere, Griewank, Rosenbrock }; <span class="keyword">sinput</span> <span class="keyword">class="type">int</span> Cycles = class="num">100; <span class="keyword">sinput</span> TEST TestCase = Sphere; <span class="keyword">sinput</span> <span class="keyword">class="type">int</span> SwarmSize = class="num">0; <span class="keyword">class="kw">input</span> <span class="keyword">class="type">int</span> GroupCount = class="num">0; <span class="keyword">class </span>BaseFunctor: <span class="keyword">class="kw">public</span> Functor { <span class="keyword">class="kw">protected</span>: <span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> params; <span class="keyword">class="type">class="kw">double</span> max[], min[], steps[]; <span class="keyword">class="type">class="kw">double</span> optimum; <span class="keyword">class="type">class="kw">double</span> result[]; <span class="keyword">class="kw">public</span>: ... <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span> test() { Swarm swarm(SwarmSize, GroupCount, params, max, min, steps); optimum = swarm.optimize(<span class="keyword">this</span>, Cycles); swarm.getSolution(result); } <span class="keyword">class="type">class="kw">double</span> getSolution(<span class="keyword">class="type">class="kw">double</span> &output[]) <span class="keyword">class="kw">const</span> { <span class="functions">ArrayCopy</span>(output, result); <span class="keyword">class="kw">return</span> optimum; } }; <span class="keyword">class="type">class="kw">double</span> OnTester() { <span class="functions">MathSrand</span>(GroupCount); <span class="comment">class=class="str">"cmt">// reproducible randomization</span> BaseFunctor *functor = <span class="macro">NULL</span>; <span class="keyword">class="kw">switch</span>(TestCase) { <span class="keyword">case</span> Sphere: functor = <span class="keyword">new</span> PSOTests::Sphere(); <span class="keyword">break</span>; <span class="keyword">case</span> Griewank: functor = <span class="keyword">new</span> PSOTests::Griewank(); <span class="keyword">break</span>; <span class="keyword">case</span> Rosenbrock: functor = <span class="keyword">new</span> PSOTests::Rosenbrock(); <span class="keyword">break</span>; } functor.test(); <span class="keyword">class="type">class="kw">double</span> output[]; <span class="keyword">class="type">class="kw">double</span> result = functor.getSolution(output); <span class="keyword">if</span>(<span class="functions">MQLInfoInteger</span>(<span class="macro">MQL_OPTIMIZATION</span>)) { <span class="functions">FrameAdd</span>(<span class="class="type">class="kw">string">"PSO"</span>, 0xC0DE, result, output); } <span class="keyword">else</span> { <span class="functions">Print</span>(<span class="class="type">class="kw">string">"Solution: "</span>, result); <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = class="num">0; i < <span class="functions">ArraySize</span>(output); i++) { <span class="functions">Print</span>(i, <span class="class="type">class="kw">string">" "</span>, output[i]); } } <span class="keyword">class="kw">delete</span> functor; <span class="keyword">class="kw">return</span> result; } <span class="keyword">class="type">int</span> passcount = class="num">0; <span class="keyword">class="type">class="kw">double</span> best = -<span class="macro">DBL_MAX</span>; <span class="keyword">class="type">class="kw">double</span> location[]; <span class="keyword">class="type">void</span> OnTesterPass() { <span class="keyword">class="type">class="kw">ulong</span> pass; <span class="keyword">class="type">class="kw">string</span> name; <span class="keyword">class="type">long</span> id; <span class="keyword">class="type">class="kw">double</span> r; <span class="keyword">class="type">class="kw">double</span> data[]; <span class="keyword">while</span>(<span class="functions">FrameNext</span>(pass, name, id, r, data)) { <span class="comment">class=class="str">"cmt">// compare r with all other passes results</span> <span class="keyword">if</span>(r > best) { best = r; <span class="functions">ArrayCopy</span>(location, data); } <span class="functions">Print</span>(passcount, <span class="class="type">class="kw">string">" "</span>, id); <span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> n = <span class="functions">ArraySize</span>(data); <span class="functions">ArrayResize</span>(data, n + class="num">1); data[n] = r; ArrayPrint(data, class="num">12); passcount++; } } <span class="keyword">class="type">void</span> OnTesterDeinit() { <span class="functions">Print</span>(<span class="class="type">class="kw">string">"Solution: "</span>, best); ArrayPrint(location); } <span class="keyword">class="type">int</span> OnInit() { <span class="keyword">if</span>(!<span class="functions">MQLInfoInteger</span>(<span class="macro">MQL_TESTER</span>)) { <span class="functions">EventSetTimer</span>(class="num">1); } <span class="keyword">class="kw">return</span> INIT_SUCCEEDED; } <span class="keyword">class="type">void</span> OnTimer() { <span class="functions">EventKillTimer</span>(); OnTester(); }
◍ PSO 在 Griewank 上的收敛实况
把上文的并行 PSO 直接丢到 Griewank 函数上跑,计划计算量 10000 次,实际在 9948 次就触发了收敛判定,日志打出 PSO Finished 9948 of 10000 planned calculations: true,说明早停机制生效,没白白烧 CPU。
最终解落在 -7.1550806279852e-08,是经过 4 轮 pass 后给出的,量级已经压到 1e-8,相对 Griewank 这种多峰坑爹函数算是很贴近全局零点了。对比前一段单目标解 -1.34e-05,并行版精度提升了约两个数量级。
从粒子末态坐标看,最优个体收在 (0.00037, -0.00007, -7.1e-08) 附近,三个维度都贴着原点,和 Griewank 理论全局最优 (0,0,0) 吻合。外汇或贵金属策略拿这套做参数寻优时,注意 PSO 早停阈值设太松可能漏掉次优谷,贵金属杠杆高、滑点大,回测收敛好不等于实盘稳,仍属高风险。
PSO Finished class="num">9948 of class="num">10000 planned calculations: true Solution: -class="num">1.342285474092986e-05 class="num">0 class="num">0.004966759354110293 class="num">1 class="num">0.002079707592422949 Parallel PSO of Griewank -class="num">12.550070232909 -class="num">0.002332638407 -class="num">0.039510275469 -class="num">3.139749741924 class="num">4.438437934965 -class="num">0.007396077598 class="num">3.139620588383 class="num">4.438298282495 -class="num">0.007396126543 class="num">0.000374731767 -class="num">0.000072178955 -class="num">0.000000071551 Solution: -class="num">7.1550806279852e-08 (after class="num">4 passes) class="num">0.00037 -class="num">0.00007