开发多币种 EA 交易(第 24 部分):添加新策略(二)(基础篇)
📘

开发多币种 EA 交易(第 24 部分):添加新策略(二)(基础篇)

第 1/3 篇

「多币种 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、递归自调。

MQL5 / C++
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,本质是人为声明“资源已就绪”,绕过后续初始化分支。实盘前务必确认它只在该置位时赋值,否则可能掩盖真实的连接失败。 外汇与贵金属杠杆高、滑点突兀,这类底层标志若错配,策略可能在假连接状态下发单,风险不小。

MQL5 / C++
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 开发属高风险,回测通过不代表实盘稳健。

MQL5 / C++
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])

常见问题

先修现有 bug 再动公共模块,把两套策略各自依赖的部分下沉到独立子库,共用逻辑抽成基础层,避免相互污染。
拆库代价主要在数据结构和接口映射,要逐一对齐字段并保留旧库兼容层,否则历史优化结果无法复用。
可以,小布能基于你的 EA 结构提示并发写入冲突点,并给出用静态标志位拦截重复初始化的改法建议。
在初始化入口设一个静态 bool 标志,首次通过后置 true,后续调用直接 return,避免重复建池和加锁。
把遍历宏改成带调试分支的版本,用条件编译输出当前品种和索引,不改业务逻辑也能快速定位越界或空值。