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

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

(2/3)· 数据库已膨胀到几 GB,EA 却只需其中一小撮 ID,本篇讲清抽取与导出的落地路径

新手友好 第 2/3 篇
不少人在接实盘时直接把整个优化数据库塞进 EA 共享目录,终端重启慢、加载卡顿还容易出错。其实启动 EA 只需要 passes 表里极少量行标识,学会抽取就能甩掉几 GB 包袱。

◍ 把回测帧塞进数据库的分支处理

在 EA 的 OnTester 或自定义统计逻辑里,是否走多帧聚合取决于运行模式。如果是优化 pass 且需要把当前结果作为一帧提交,就调 FrameAdd;空名字符串 "" 表示不绑定特定图表资源,后两个 0 分别是 frame 标识与预留参数,最后一个 s_fileName 指定结果落盘的文件名。 FrameAdd 返回 false 时不能闷头跳过,必须用 GetLastError 把底层错误码打出来,否则优化跑完你根本不知道哪一帧丢了。PrintFormat 里用 __FUNCTION__ 自动带函数名,能省掉手改日志前缀的麻烦。 非多帧场景(比如单次回测)就直接走 CTesterHandler::ProcessFrame(data),把当前 data 结构交给数据库处理方法。两种分支的区别在于:前者跨 pass 累积,后者仅本 pass 落地,开 MT5 跑优化时切到『每 pass 生成帧』就能验证这条路径。

MQL5 / C++
if(!FrameAdd("", class="num">0, class="num">0, s_fileName)) {
      PrintFormat(__FUNCTION__" | ERROR: Frame add error: %d", GetLastError());
   }
   } else {
   class=class="str">"cmt">// Otherwise, it is a single pass, call the method to add its results to the database
   CTesterHandler::ProcessFrame(data);
   }
}

用 RETURNING rowid 锁住并行优化的通过 ID

建库时别往 strategy_groups 表里塞冗余字段。它只留两个:id_pass 作为外键挂到 passes 表(ON DELETE CASCADE),name 用来生成策略组枚举。这样一条通过记录能反查到项目和策略组,不重复存创建日期之类的派生数据。 难点在取 ID。passes 表的 id_pass 是 INTEGER PRIMARY KEY AUTOINCREMENT,由 SQLite 隐式 rowid 托管,插入时我们传 NULL,代码里本来拿不到这个值。若用「插完选 MAX(id_pass)」的办法,在另一台终端正并行跑一阶段优化、结果也写同一库时,会误抓别人的 ID。 更稳的做法是改插入查询,末尾加 RETURNING rowid。GetInsertQuery() 里拼出的 SQL 从「VALUES (...)」变成「VALUES (...) RETURNING rowid;」,数据库直接回传刚写的行号。CDatabase 因此要新写一个 Insert() 静态方法,内部用 DatabasePrepare() 替代 DatabaseExecute(),把单行结果读出来返回。 CTesterHandler 加一个静态属性 s_idPass,Insert() 拿到 ID 后写进去,EA 任意位置都能读。库生成 EA(SimpleVolumesStage3.mq5)只需在测试结束时调 CGroupsLibrary::Add(id_pass, groupName_),groupName_ 为空就不存。外汇与贵金属策略库构建涉及自动优化,实盘前请在 MT5 策略测试器用样本数据验证 ID 链路,并行任务有写冲突风险。

MQL5 / C++
-- Table: strategy_groups
DROP TABLE IF EXISTS strategy_groups;
CREATE TABLE strategy_groups(
    id_pass INTEGER REFERENCES passes(id_pass) ON DELETE CASCADE
                                                  ON UPDATE CASCADE
                                                  PRIMARY KEY,
    name    TEXT
);
class="kw">input group "::: Saving to library"
class="kw">input class="type">class="kw">string groupName_    = "";        class=class="str">"cmt">// - Group name(if empty - no saving)
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Generate SQL query to insert pass results                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
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;) RETURNING rowid;",
                              s_idTask, pass, values, inputs,
                              TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS));
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class for handling the database                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CDatabase {
   ...
class="kw">public:
   ...
   class=class="str">"cmt">// Execute a query to the database for insertion with class="kw">return of the new entry ID
   class="kw">static class="type">ulong      Insert(class="type">class="kw">string query);
};

「把优化结果写回数据库的关键几步」

在 MT5 策略优化里,每跑完一个 pass 想落库,核心就是拼一条 SQL 然后取回 rowid。下面这段 Insert 方法先调 DatabasePrepare,句柄不是 INVALID_HANDLE 才继续,否则直接把错误码打出来,避免静默丢数据。 读结果用了一个极简 struct Row,只装了 rowid 一个字段,DatabaseReadBind 绑定后把 res 赋成该值返回。也就是说,调用方拿到的 ulong 就是这条插入记录在库里的自增主键,后面挂分组、回查都靠它。 CTesterHandler::ProcessFrame 展示了真实调用链:连库、用 values 拼 query、把 DB::Insert 的返回值存进静态变量 s_idPass、再关库。s_idPass 初始为 0,若某次插入失败返回 0,上层就能察觉这次 pass 没写进去。 CGroupsLibrary 的 Add(p_idPass, p_name) 正是用这个 id 把策略组名和对应 pass 绑一起。你在 EA 里接这套逻辑时,重点关注 s_idPass 是否为 0,比盲目信「写成功了」更靠谱。外汇与贵金属回测涉及高杠杆,落库数据仅作概率参考,不代表实盘表现。

MQL5 / C++
class="type">ulong CDatabase::Insert(class="type">class="kw">string query) {
   class="type">ulong res = class="num">0;
   class=class="str">"cmt">// Execute the request
   class="type">int request = DatabasePrepare(s_db, query);
   class=class="str">"cmt">// If there is no error
   if(request != INVALID_HANDLE) {
      class=class="str">"cmt">// Data structure for reading a single class="type">class="kw">string of a query result
      class="kw">struct Row {
         class="type">int      rowid;
      } row;
      class=class="str">"cmt">// Read data from the first result class="type">class="kw">string
      if(DatabaseReadBind(request, row)) {
         res = row.rowid;
      } else {
         class=class="str">"cmt">// Report an error if necessary
         PrintFormat(__FUNCTION__" | ERROR: Reading row for request \n%s\nfailed with code %d", query, GetLastError());
      }
   } else {
      class=class="str">"cmt">// Report an error if necessary
      PrintFormat(__FUNCTION__" | ERROR: Request \n%s\nfailed with code %d", query, GetLastError());
   }
   class="kw">return res;
}

class CTesterHandler {
   ...
class="kw">public:
   ...
   class="kw">static class="type">ulong      s_idPass;
};
...
class="type">ulong CTesterHandler::s_idPass = class="num">0;
...
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
   s_idPass = DB::Insert(query);
   class=class="str">"cmt">// Close the database
   DB::Close();
}

class CGroupsLibrary {
class="kw">private:
   class=class="str">"cmt">// Exporting group names and initialization strings extracted from the database as MQL5 code
   class="kw">static class="type">void      ExportParams(class="type">class="kw">string &p_names[], class="type">class="kw">string &p_params[]);
class="kw">public:
   class=class="str">"cmt">// Add the pass name and ID to the database
   class="kw">static class="type">void      Add(class="type">ulong p_idPass, class="type">class="kw">string p_name);
   class=class="str">"cmt">// Export passes to mqh file

◍ 把优化批次写进数据库的实现细节

在 MT5 多品种、多参数优化里,想按「组」归类每一遍回测结果,得在 EA 侧落地一个轻量数据库写入逻辑。下面这段 C++ 风格的类方法,就是把每次 pass 的 ID 和自定义组名插进 strategy_groups 表,方便后续用 SQL 拉取某组的全部绩效。 CGroupsLibrary::Add 先拼出 INSERT 语句,再走 DB::Connect → Execute → Close 三步。注意它只在连接成功时才写库,连不上就静默跳过,不会中断优化任务。 OnTester 是框架回调,每完成一个优化批次触发一次。它先拿 expert.Tester() 的返回值作为该 pass 的评测分数,再判断 groupName_ 非空才调用 Add 落库。也就是说,如果你没给当前批次设组名,这一遍就不会进分组表,后续按组统计会漏掉它。 实盘前建议在策略测试器里跑 10~20 个 pass 验证:打开生成的 sqlite 文件,执行 SELECT * FROM strategy_groups,应当看到对应条数的记录。外汇与贵金属优化结果受点差和滑点影响大,分组数据仅反映历史回测概率,不代表未来收益。

MQL5 / C++
class="kw">static class="type">void Export(class="type">class="kw">string p_idPasses);
class=class="str">"cmt">// Array to fill with initialization strings from mqh file
class="kw">static class="type">class="kw">string s_params[];
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Add the pass name and ID to the database                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGroupsLibrary::Add(class="type">ulong p_idPass, class="type">class="kw">string p_name) {
  class="type">class="kw">string query = StringFormat("INSERT INTO strategy_groups VALUES(%d, &class="macro">#x27;%s&class="macro">#x27;)",
                              p_idPass, p_name);
class=class="str">"cmt">// Open the database
  if(DB::Connect()) {
    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">//| Test results                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double OnTester(class="type">void) {
  class=class="str">"cmt">// Handle the completion of the pass in the EA object
  class="type">class="kw">double res = expert.Tester();
  class=class="str">"cmt">// If the group name is not empty, save the pass to the library
  if(groupName_ != "") {
    CGroupsLibrary::Add(CTesterHandler::s_idPass, groupName_);
  }
  class="kw">return res;
}

把筛选出的策略实例拼成组库

在 MT5 测试器里跑 SimpleVolumesStage3.ex5,从 passes_ 参数挑出前面筛出的九个合格 ID,做多种组合跑几遍;groupName_ 里写清楚这组由哪些策略实例凑成,方便后面溯源。 跑完去 strategy_groups 表看结果,用一条 SQL 把每组对应的年化、夏普、回撤拉出来对照。name 字段直接暴露构成:像「EUR-GBP-USD」说明组里挂了 EURGBP、EURUSD、GBPUSD 三个品种的策略;「Only EURGBP」就只做这一个品种。名称尾巴的「3x16 items」表示由 3 个各含 16 策略的标准化子组并起来。 custom_ontester 列是每组的归一化年平均利润。这里有个反常现象值得记:该值波动区间明显超出预期,且纯 GBPUSD 组的数值显著高于多品种混合组。第 20 行存的是当前最优组——它把每个品种、各时段表现最靠前的子组都收进来了。外汇与贵金属组合策略回测存在过拟合高风险,组间差异仅代表历史样本倾向。 下面这条查询就是拼库时实际用的抽取语句,逐行看:SELECT 先点名要取的字段;sg.id_pass 与 sg.name 来自组表,p 表带出 custom_ontester、sharpe_ratio、profit、profit_factor、equity_dd_relative;FROM 锁定 strategy_groups 别名 sg,JOIN passes 别名 p 用 id_pass 对齐,把组和数据行拼成一张可排序的参考表。

MQL5 / C++
SELECT sg.id_pass,
       sg.name,
       p.custom_ontester,
       p.sharpe_ratio,
       p.profit,
       p.profit_factor,
       p.equity_dd_relative
  FROM strategy_groups sg
       JOIN
       passes p ON sg.id_pass = p.id_pass;

「把组库从数据库导成可挂载的 mqh」

组库存在数据库里没法直接给 EA 用,得先落成一个 mqh 文件。CGroupsLibrary 里的 Export() 负责从库里把组名和初始化串捞出来,塞进两个数组,再交给 ExportParams() 拼代码。 ExportParams() 干的事很具体:先生成带 ENUM_GROUPS_LIBRARY 枚举的代码,每个枚举元素挂组名注释;再声明静态数组 CGroupsLibrary::s_params[],把初始化串填进去。填之前要做预处理——换行符换成空格,双引号前加反斜杠,否则生成的代码根本编不过。 拼完的字符串写进 ExportedGroupsLibrary.mqh。关键点在于:GroupsLibrary.mqh 会直接 #include 这个导出文件,所以最终 EA 只需包含前者。麻烦的是编译导出 EA 时该文件必须已存在,但内容为空就行,先建个空文件骗过编译器即可。 跑导出的辅助 EA 靠 passes_ 参数决定导出哪些组、什么顺序。在图表上跑一次,终端数据文件夹里就出 ExportedGroupsLibrary.mqh,手动挪到项目目录就能用。外汇与贵金属自动化涉及高风险,参数导出逻辑错误可能导致 EA 加载失败。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Exporting passes to mqh file                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CGroupsLibrary::Export(class="type">class="kw">string p_idPasses) {
class=class="str">"cmt">// Array of group names
  class="type">class="kw">string names[];
class=class="str">"cmt">// Array of group initialization strings
  class="type">class="kw">string params[];
class=class="str">"cmt">// If the connection to the main database is established,
  if(DB::Connect()) {
    class=class="str">"cmt">// Form a request to receive passes with the specified IDs
    class="type">class="kw">string query = "SELECT sg.id_pass,"
                 "       sg.name,"
                 "       p.params"
                 "  FROM strategy_groups sg"
                 "   JOIN"
                 "   passes p ON sg.id_pass = p.id_pass";
    query = StringFormat("%s "
                         "WHERE p.id_pass IN(%s);",
                         query, p_idPasses);
    class=class="str">"cmt">// Prepare and execute the request
    class="type">int request = DatabasePrepare(DB::Id(), query);
    class=class="str">"cmt">// If the request is successful
    if(request != INVALID_HANDLE) {
      class=class="str">"cmt">// Structure for reading results
      class="kw">struct Row {
        class="type">ulong       idPass;
        class="type">class="kw">string      name;
        class="type">class="kw">string      params;
      } row;
      class=class="str">"cmt">// For all query results, add the name and initialization class="type">class="kw">string to the arrays
      while(DatabaseReadBind(request, row)) {
        APPEND(names, row.name);
        APPEND(params, row.params);
      }
    }
    DB::Close();
    class=class="str">"cmt">// Export to mqh file
    ExportParams(names, params);
  }
}
把库导出检查交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 EA 初始化字符串是否超长、组 ID 是否缺失,把重复劳动交给小布,你专注决策。

常见问题

数据库体积已达数 GB 且持续增长,EA 启动仅需其中一小部分 passes 表行标识,全量携带不合理且拖慢终端。
依据语言规范,字符串输入值最大长度约 191 到 253 字符,具体取决于参数名长度,本例上限为 247 字符。
通过输入参数传递来自 passes 表的行标识符子集,可在测试前修改该参数选用不同组组合。
可以,小布盯盘内置 AIGC 诊断,能直接显示品种页上 EA 初始化字符串长度与组 ID 完整性,减少手动核对。
第一阶段单实例参数优化结果入 passes 表,第二阶段组选择结果也回存数据库相应组信息表。