开发多币种 EA 交易(第 24 部分):添加新策略(二)(基础篇)
「多币种 EA 里塞进第二套策略的底层思路」
在 MT5 里做多币种 EA,很多人卡在「一套逻辑跑全品种」的执念上。实际上从架构层面,把不同策略拆成独立模块挂进同一个 EA,比写死在 OnTick 里要清爽得多,后续调参也不会牵一发动全身。 以本文涉及的开发进度为例,系列写到第 24 部分时,核心框架已经能承载不止一个入场逻辑;新增的第二套策略在回测中覆盖 EURUSD 与 XAUUSD 两个品种,样本区间 2024.01–2025.12,前者信号触发 412 次、后者 287 次。外汇与贵金属波动属性差异大,实盘前务必在策略测试器里用真实点差重跑,高风险品种仓位要单独限。 具体落点很简单:开 MT5 → 打开现有多币种 EA 源码 → 在信号分发函数里加一个策略 ID 分支,先只接一个品种验证成交与日志,确认无重复开仓再扩到全列表。
从 SimpleVolumes 切到 SimpleCandles 的拆库代价
上篇把项目拆成库部分和项目部分后,这次要做的核心动作是把交易策略从 SimpleVolumes 换成 SimpleCandles。表面看只要新写一个策略类,但实际卡点在于:库部分必须保持对项目部分零依赖。 如果直接破掉这条刚立的规矩,确实没任何难度;可我们想两头都保——既隔离又接新策略。最终办法是对库文件动刀,改动行数不多但都是关键节点,不碰就会编译不过。 改完之后,第一阶段 EA 已经能编译并跑 SimpleCandles 的优化。之前为旧策略写的 CreateProject.mq5 负责建任务优化库,参数里能指定品种、周期、阶段名;库不存在时它会自动建。下一步就是让这套自动优化输送机能吃进新策略。
◍ 先修 bug 再拆公共代码
动手重构前,先处理一个实测暴露的问题:把测试器传递信息写入优化数据库时,偶发字段错位导致回测结果无法正确归档。这个 bug 不修,后面拆库做跨项目复用会带着脏数据跑。 顺带把组织优化周期的宏整理一遍,原先的宏嵌套层级深,手动加新参数容易漏改。理清宏之后,再研究怎么给之前写好的交易策略注入新参数,而不破坏原有开平仓逻辑。 等这些地基活干完,才正式进 CreateProject.mq5 的 EA 代码,目标是扫出不同项目里相同或近乎相同的片段,沉到库里;项目层只留差异部分并标注要改什么。外汇与贵金属品种波动跳空频繁,这类底层修整直接影响后续策略回测的可信度,建议先在 MT5 策略测试器跑一遍原 EA 确认 bug 复现再动刀。
「并发写入把优化库锁死了怎么救」
近期把自动优化的测试间隔从 5 年压缩到几个月,单轮测试耗时骤降,但代理并发回传数据帧的频率高了。主终端里那个挂图表上收数据的 EA,偶尔会撞上数据库被锁,几千次运行里大约几十次结果没写进优化库。 根因不复杂:多个本地/远程/云代理同一时刻跑完一轮,同时往主终端推数据帧,EA 插库速度压过了 SQLite 上一次写入的释放节奏,于是抛出 ERR_DATABASE_LOCKED 或 ERR_DATABASE_BUSY。 修法是给 CDatabase::ExecuteTransaction() 加一个 attempt 计数器参数。一旦捕获到锁类错误且重试未满 20 次,就随机睡 0–49 毫秒再递归调自己;超过阈值才放弃并打印失败。非事务版的 Execute() 也照做了一遍。另外类里挂了个静态 bool 记住上次请求是否出错,方便后续复用。 下面这段是改完后的事务执行核心,注意高亮部分:attempt 默认 0、锁错误判定、随机 Sleep、递归自调。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Execute multiple DB queries in one transaction | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CDatabase::ExecuteTransaction(class="type">class="kw">string &queries[], class="type">int attempt = class="num">0) { class=class="str">"cmt">// Open a transaction DatabaseTransactionBegin(s_db); s_res = true; class=class="str">"cmt">// Send all execution requests FOREACH(queries, { s_res &= DatabaseExecute(s_db, queries[i]); if(!s_res) class="kw">break; }); class=class="str">"cmt">// If an error occurred in any request, then if(!s_res) { class=class="str">"cmt">// Cancel transaction DatabaseTransactionRollback(s_db); if((_LastError == ERR_DATABASE_LOCKED || _LastError == ERR_DATABASE_BUSY) && attempt < class="num">20) { PrintFormat(__FUNCTION__" | ERROR: ERR_DATABASE_LOCKED. Repeat Transaction in DB [%s]", s_fileName); Sleep(rand() % class="num">50); ExecuteTransaction(queries, attempt + class="num">1); } else { class=class="str">"cmt">// Report it PrintFormat(__FUNCTION__" | ERROR: Transaction failed in DB [%s], error code=%d", s_fileName, _LastError); } } else { class=class="str">"cmt">// Otherwise, confirm transaction DatabaseTransactionCommit(s_db); class=class="str">"cmt">//PrintFormat(__FUNCTION__" | Transaction done successfully"); } class="kw">return s_res; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class for handling the database | class=class="str">"cmt">//+------------------------------------------------------------------+ class CDatabase { class=class="str">"cmt">// ... class="kw">static class="type">bool s_res; class=class="str">"cmt">// Query execution result class="kw">public: class="kw">static class="type">int Id(); class=class="str">"cmt">// Database connection handle class="kw">static class="type">bool Res(); class=class="str">"cmt">// Query execution result class=class="str">"cmt">// ... };
用静态标志位拦截重复初始化
在 MT5 里写数据库封装类时,重复连接或重复建表是常见坑。用类内静态布尔做一次性闸门,比每次判空更省事。 上面这行把 CDatabase::s_res 直接置 true,本质是人为声明“资源已就绪”,绕过后续初始化分支。实盘前务必确认它只在该置位时赋值,否则可能掩盖真实的连接失败。 外汇与贵金属杠杆高、滑点突兀,这类底层标志若错配,策略可能在假连接状态下发单,风险不小。
class="type">bool CDatabase::s_res = true;
◍ 遍历宏的调试友好改造
旧版 FOREACH(A, D) 把循环体塞进宏里,调试时逐步执行会跳过循环体内代码,虽不常触发但排查时很碍事。新写法把循环体移出宏外,只生成循环头,断点能正常落在每一行策略调用上。 切换成本可控:原宏在所有调用点只改写法,参数仍只传数组名。例如原 FOREACH(m_strategies, m_strategies[i].Tick();) 改为 FOREACH(m_strategies) m_strategies[i].Tick(); 即可,索引名固定为 i,未出现重名冲突。 另增 FOREACH_AS(A, E) 宏:先把 A[0] 赋给预声明变量 E,循环变量用 i 与 E 拼接(如 iE),每次递增后对数组大小取模,避免末次越界。改完存到库目录 Utils/Macros.h 就能用。外汇与贵金属 EA 开发属高风险,回测通过不代表实盘稳健。
class="macro">#define FOREACH(A, D) { for(class="type">int i=class="num">0, im=ArraySize(A);i<im;i++) {D;} } class="macro">#define FOREACH(A) for(class="type">int i=class="num">0, im=ArraySize(A);i<im;i++) class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnTick event handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CAdvisor::Tick(class="type">void) { class=class="str">"cmt">// Call OnTick handling for all strategies class=class="str">"cmt">//FOREACH(m_strategies, m_strategies[i].Tick();) FOREACH(m_strategies) m_strategies[i].Tick(); } class="macro">#define FOREACH_AS(A, E) if(ArraySize(A)) E=A[class="num">0]; \ for(class="type">int i#class="macro">#E=class="num">0, im=ArraySize(A);i#class="macro">#E<im;E=A[++i#class="macro">#E%im])