开发多币种 EA 交易(第 17 部分):为真实交易做进一步准备(基础篇)
📘

开发多币种 EA 交易(第 17 部分):为真实交易做进一步准备(基础篇)

第 1/3 篇

多币种EA真实交易前的最后一道关

多币种 EA 在策略测试器里跑通,不等于能直接挂真实账户。MT5 的测试环境默认用同步 tick 模拟多品种,和真实经纪商异步报价存在偏差,这种偏差在跨周期共振策略里会被放大。 原文作者 Yuriy Bykov 在 2025 年 5 月 15 日发布的第 17 部分中提及,社区帖获 1031 次浏览、7 条讨论,说明多币种实盘准备是高频痛点。开 MT5 调出「测试器→设置→使用真实分笔数据」选项,能部分抵消模拟与实盘的差异。 外汇与贵金属自带高杠杆与滑点风险,多币种同时持仓还会放大保证金占用,实盘前建议先用极小手数验证订单路由与品种同步逻辑。

「实盘前 EA 必须甩掉几 GB 数据库」

前面把精力放在策略测试器里跑出能看的 EA 结果,但真金白银的账户对 EA 的要求完全不一样。重启终端要能恢复、交易品种名可能带后缀、指标触达要自动平仓,这些只是基础。 更麻烦的是初始化字符串的来源:之前直接读那个存了策略实例和分组优化结果的数据库。现在这个库已经在共享终端目录里涨到几 GB,而且随着优化批次增加只会更大。 把几 GB 文件绑进 EA 当组成部分显然不合理——启动其实只需要其中极小一块数据。所以得在 EA 里写一套抽取和调用的机制,只把必要的那部分拎出来用。外汇和贵金属实盘杠杆高、滑点跳空频繁,这种初始化瘦身是直接关系 EA 能否稳定挂机的前提。

◍ 把优化结果喂给EA的三条路径

前两阶段已经把单品种单周期的策略参数、以及跨组的优选组合分别落到了数据库 passes 表。第三阶段不跑标准优化器,而是从表里挑出最佳组,直接交给 SimpleVolumesExpert.mq5 去跑实盘回测。 我们实际只覆盖了 EURGBP、EURUSD、GBPUSD 三个品种,配 H1、M30、M15 三个周期,理论应有 9 个入选组;为提速只取了 H1 上三个不同品种的最佳组。EA 通过输入参数 passes_ 接收来自 passes 表的行 ID 列表,代码见下。 [CODE] input string passes_ = "734469," "736121," "776928"; // - Comma-separated pass IDs [/CODE] 逐行拆解:第1行声明 input string 变量 passes_,表示它是可在测试器面板修改的输入字符串;赋值以逗号分隔三个 pass ID(734469、736121、776928),对应数据库里挑出的三组优化结果;末尾注释说明格式必须是逗号分隔的 ID 列表。 MQL5 对输入字符串长度卡得很死:按文档随参数名长短,上限在 191–253 字符之间,passes_ 这种名字实际可用上限是 247 字符。想塞进 40 多个组基本不可能,只能把 input 去掉改成源码内写死的 string。 若 MT5 跑 EA 时能连优化库,传 ID 列表即可,EA 自己拉初始化串;断库时得靠文件或 mqh 头文件硬编。但多组切换仍要手工改,扩展性差。更稳的思路是建一个内嵌的初始化串库,EA 用输入参数切组,不依赖外部文件。外汇与贵金属测试涉及高杠杆,回测结论仅代表历史概率,实盘可能显著偏离。 后续分三步走:先用工具选组并存初始化串及备注;再筛出要用的组生成包含文件;最后改 EA 把库编进去,彻底脱钩优化数据库。

MQL5 / C++
class="kw">input class="type">class="kw">string      passes_ = "class="num">734469,"
                      "class="num">736121,"
                      "class="num">776928";      class=class="str">"cmt">// - Comma-separated pass IDs

最佳组筛选与分组仍留白

前面第 9 部分第 8 阶段给出的原型,是把不同策略、品种、时段跑出来的最优参数组收进一个成品 EA。但“怎么算最优组”本身还没细说。 按总利润、夏普比率或归一化平均年利润挑表面最优,是最直接的做法;可若用复杂标准筛,或某些最优组塞进去反而拖累整体结果,答案就变复杂了。这部分只能留待自行深挖。 分组方式也有类似疑点。第 5 部分 manually 选了 9 个策略实例,3 个品种各 3 个。实测先把每品种做成 3 个标准化三策略组、再合成最终组,比直接把 9 个合一的回测表现略好,但没法证明这就是最优切法。 好在这两类问题都能往后放。真要系统搜参,得等没写好的辅助工具就位,否则纯手工效率太低、耗时太多。外汇与贵金属测试本身高波动高风险,结论都只是倾向性。

「单次回测结果为何漏进数据库」

跑过 MT5 优化的人都知道,优化结果能自动写库,但单独跑一次 EA 测试(single pass)却查不到记录。根子在事件模型:OnTesterPass() 只在「数据帧收集模式」下被调用,而单次通过根本不触发这个 handler,所以原 CTesterHandler::ProcessFrames() 里的上传逻辑形同虚设。 如果为了存单次结果去硬造一次「只跑第一遍就停」的优化,代码丑陋且浪费算力。更直接的办法是在 OnTester() 对应的 CTesterHandler::Tester() 里动手脚——这个方法在单次通过和优化结束时都会被调用。麻烦的是,单次模式下用 FrameAdd() 塞进去的帧,FrameNext() 读不到,等于白建。 因此要在 Tester() 里先判断当前是 single pass 还是优化的一部分:前者直接拼 SQL 写 passes 表,后者才走原来的帧发送主 EA 流程。我们把插入语句抽成 GetInsertQuery(),新增 ProcessFrame(string values) 负责连库执行,避免两段代码重复。 对交易者来说,passes 表里最有价值的字段是归一化策略组的初始化字符串(init string),后续挑组全靠它。但给 passes 扩列不划算——绝大多数行是优化通过,不需要附加信息,所以另建一张选定结果表更干净,这部分留到库构建阶段再做。 下面这段是原文里 ProcessFrames() 的骨架,以及类里新增的两个方法声明,注意 INSERT 语句把 pass、values、inputs 和本地时间都落了库:

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Handling incoming frames                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTesterHandler::ProcessFrames(class="type">void) {
class=class="str">"cmt">// Open the database
  DB::Connect();
class=class="str">"cmt">// Variables for reading data from frames
  ...
class=class="str">"cmt">// Go through frames and read data from them
  while(FrameNext(pass, name, id, value, data)) {
    class=class="str">"cmt">// Convert the array of characters read from the frame into a class="type">class="kw">string
    values = CharArrayToString(data);
    
    class=class="str">"cmt">// Form a class="type">class="kw">string with names and values of the pass parameters
    inputs = GetFrameInputs(pass);
    class=class="str">"cmt">// Form an SQL query from the received data
    query = StringFormat("INSERT INTO passes "
                                "VALUES(NULL, %d, %d, %s,
&class="macro">#x27;%s&class="macro">#x27;,
&class="macro">#x27;%s&class="macro">#x27;);",
                                 s_idTask, pass, values, inputs,
                                 TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS));
    class=class="str">"cmt">// Add it to the SQL query array
    APPEND(queries, query);
   }
class=class="str">"cmt">// Execute all requests
  DB::ExecuteTransaction(queries);
class=class="str">"cmt">// Close the database
  DB::Close();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Optimization event handling class                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CTesterHandler {
  
...
  class="kw">static class="type">void       ProcessFrame(class="type">class="kw">string values);  class=class="str">"cmt">// Handle single pass data
  class=class="str">"cmt">// Generate SQL query to insert pass results
  class="kw">static class="type">class="kw">string     GetInsertQuery(class="type">class="kw">string values, class="type">class="kw">string inputs, class="type">ulong pass = class="num">0);
class="kw">public:
  ...
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Generate SQL query to insert pass results                           |
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 把优化结果落进本地库的关键几步

在 MT5 策略测试器里跑多 pass 优化时,最麻烦的不是算,而是把每一遍的参数和结果稳稳收下来。上面这段逻辑给出了一种直接写 SQL 的做法:用 GetInsertQuery 拼出 INSERT 语句,把任务 ID、pass 序号、数值串、输入参数和本地时间一次性塞进 passes 表。 ProcessFrames 里用 FrameNext 逐个读帧,CharArrayToString 把帧里的字符数组转成字符串,再调 GetInsertQuery 生成查询并 APPEND 进数组。这里有个细节:TimeToString(TimeLocal(), TIME_DATE|TIME_SECONDS) 会把时间戳精确到秒,多 agent 并发回传时靠它区分写入时刻,避免同 pass 覆盖。

如果是单 pass 后处理,ProcessFrame 走的是更短路径:连库、拼查询(inputs 传空、pass 传 0)、Execute、关库。优化模式下 Tester 函数则先把 params 拼进 data,用 FileOpen 写 ANSI 文本再发帧回主终端——文件用 FILE_WRITEFILE_TXTFILE_ANSI,意味着中文参数描述在默认 codepage 下可能乱码,跨境品种名建议先转 ASCII。

开 MT5 把这段塞进你自己的 CTesterHandler,把 s_idTask 换成当前任务常量,就能在优化结束后直接 SELECT 出全量 pass 分布,省掉手动导 CSV。外汇与贵金属优化本身高风险,回测顺不代表实盘同分布,参数敏感区尤其要复核。

MQL5 / C++
class="type">class="kw">string CTesterHandler::GetInsertQuery(class="type">class="kw">string values, class="type">class="kw">string inputs, class="type">ulong pass) {
  class="kw">return StringFormat("INSERT INTO passes "
                             "VALUES(NULL, %d, %d, %s,\n&class="macro">#x27;%s&class="macro">#x27;,\n&class="macro">#x27;%s&class="macro">#x27;);",
                       s_idTask, pass, values, inputs,
                       TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS));
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Handling incoming frames                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTesterHandler::ProcessFrames(class="type">void) {
  ...
class=class="str">"cmt">// Go through frames and read data from them
  while(FrameNext(pass, name, id, value, data)) {
    class=class="str">"cmt">// Convert the array of characters read from the frame into a class="type">class="kw">string
    values = CharArrayToString(data);
    class=class="str">"cmt">// Form a class="type">class="kw">string with names and values of the pass parameters
    inputs = GetFrameInputs(pass);
    class=class="str">"cmt">// Form an SQL query from the received data
    query = GetInsertQuery(values, inputs, pass);
    class=class="str">"cmt">// Add it to the SQL query array
    APPEND(queries, query);
   }
  ...
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Handle single pass data                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTesterHandler::ProcessFrame(class="type">class="kw">string values) {
class=class="str">"cmt">// Open the database
  DB::Connect();
class=class="str">"cmt">// Form an SQL query from the received data
  class="type">class="kw">string query = GetInsertQuery(values, "", class="num">0);
class=class="str">"cmt">// Execute the request
  DB::Execute(query);
class=class="str">"cmt">// Close the database
  DB::Close();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Handling completion of tester pass for agent                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTesterHandler::Tester(class="type">class="kw">double custom,    class=class="str">"cmt">// Custom criteria
                            class="type">class="kw">string params     class=class="str">"cmt">// Description of EA parameters in the current pass
                            ) {
...
class=class="str">"cmt">// Generate a class="type">class="kw">string with pass data
  data = StringFormat("%s,&class="macro">#x27;%s&class="macro">#x27;", data, params);
class=class="str">"cmt">// If this is a pass within the optimization,
  if(MQLInfoInteger(MQL_OPTIMIZATION)) {
    class=class="str">"cmt">// Open a file to write a frame data
    class="type">int f = FileOpen(s_fileName, FILE_WRITE | FILE_TXT | FILE_ANSI);
    class=class="str">"cmt">// Write a description of the EA parameters
    FileWriteString(f, data);
    class=class="str">"cmt">// Close the file
    FileClose(f);
    class=class="str">"cmt">// Create a frame with data from the recorded file and send it to the main terminal

常见问题

过大的数据库会拖慢EA加载与下单响应,实盘前只保留必要品种与周期数据,把冗余历史清掉能降延迟、避卡顿。
可通过参数文件直接载入、本地配置表读取、或外部数据库导入三种方式,按你的部署环境选最稳的一条即可。
可以,小布能替你扫描冗余数据库、核对优化结果落库情况,并提示哪些品种组还没筛完,你只看结论做决策。
多数是落库步骤没触发或路径写错,需在EA里显式调用存储接口并确认目录权限,否则只跑不存。
不建议,分组留白会导致EA按默认逻辑交易,风险不可控,先把品种分组和参数边界定清再上线。