开发多币种 EA 交易 (第 13 部分):自动化第二阶段  分组选择·进阶篇
📘

开发多币种 EA 交易 (第 13 部分):自动化第二阶段 分组选择·进阶篇

第 2/3 篇

用字符串拼出虚拟策略组的初始化参数

在 MT5 里做多策略回测或虚拟下单时,往往不写死对象,而是用 StringFormat 把参数拼成类初始化串,再交给工厂函数实例化。下面这段就是先把单个策略实例的参数收进 strategyParams,再套一层 CVirtualStrategyGroup 的壳。 组初始化串模板里用了 %s 承接策略参数、%f 承接 scale_ 缩放系数,注意方括号和缩进只是可读性包装,运行时按字符串解析。若 scale_ 设 1.0,组与策略手数比例就是 1:1;调大它可能让虚拟订单批量放大,外汇与贵金属品种波动剧烈,放大前先在策略测试器里用小资金验证。 风险管理器 CVirtualRiskManager 这里暂时全填 0(0,0,0,0,0,0),代表不限制仓位与回撤,仅作骨架演示;实盘逻辑里这六个浮点/整型位通常对应模式、阈值、超时等。最后 CVirtualAdvisor 把组串、风控串和几个整型开关拼成 EA 初始化串,丢给虚拟顾问统一调度。 别把全 0 风控当可用配置 上面 riskManagerParams 的六个 0 只是占位,直接拿去跑黄金或欧美大概率会因无风控在极端行情亏穿。至少把回撤上限和单组订单数填真实值,再上 MT5 测试器跑 3 个月以上 tick 数据。

MQL5 / C++
class="type">class="kw">string groupParams = StringFormat(
                "class CVirtualStrategyGroup(\n"
                "      [\n"
                "        %s\n"
                "      ],%f\n"
                "  )",
                strategyParams, scale_
              );
class=class="str">"cmt">// Prepare the initialization class="type">class="kw">string for the risk manager
  class="type">class="kw">string riskManagerParams = StringFormat(
                                 "class CVirtualRiskManager(\n"
                                 "    %d,%.2f,%d,%.2f,%d,%.2f"
                                 "  )",
                                 class="num">0,class="num">0,class="num">0,class="num">0,class="num">0,class="num">0
                               );
class=class="str">"cmt">// Prepare the initialization class="type">class="kw">string for an EA with a group of a single strategy and the risk manager
  class="type">class="kw">string expertParams = StringFormat(
                                "class CVirtualAdvisor(\n"
                                "  %s,\n"
                                "  %s,\n"
                                "  %d,%s,%d\n"
                                ")",
                                groupParams,

「把参数塞进虚拟持仓引擎的初始化」

在 MT5 的 EA 初始化函数里,这段调用把风控参数、幻数(magic_)和标识名 "SimpleVolumesSingle" 一起丢给专家参数构造器,并打印出结构化参数串。注意第三个布尔量 true 通常代表允许虚拟持仓(不占真实账户仓位),外汇与贵金属品种开虚拟单同样有滑点漂移风险,回测和实盘可能不一致。 随后用 NEW(expertParams) 生成处理虚拟持仓的 expert 对象;若指针为空直接返回 INIT_FAILED,否则返回 INIT_SUCCEEDED。把这段贴进 OnInit 尾部,编译后从 Experts 日志能看到 Expert Params 的完整输出,确认参数没被截断。

MQL5 / C++
                                                                 riskManagerParams,
                                                                 magic_, "SimpleVolumesSingle", true
                                                                  );
   PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams);
class=class="str">"cmt">// Create an EA handling class="kw">virtual positions
   expert = NEW(expertParams);
   if(!expert) class="kw">return INIT_FAILED;
   class="kw">return(INIT_SUCCEEDED);
}

◍ 给第二阶段优化 EA 接上任务库

第二阶段的核心动作,是把第一阶段的产出收敛成一组可复选的单个策略实例。做法是以 OptGroupExpert.mq5 为基底改出 SimpleVolumesStage2.mq5,让 EA 在优化前先从主库抽数、写进独立的任务库,再按指定的索引组去选实例。 任务库只放两张字段:id_pass(第一阶段 tester pass ID)和 params(该 pass 的 EA 初始化串)。如果表已存在就先删再建,保证每次第二阶段重跑都拿到干净的新候选集。 从主库拉数的 SQL 会 join passes / tasks / jobs / stages 四张表,硬性过滤条件是 stage 名等于 "First"、job ID 等于传入的 idParentJob_。质量门槛设了三道可选线:规范化利润 > 2500、成交笔数 > 20、夏普比率 > 2;这三者是为兼顾候选量和策略质量,参数可按你自己的第一阶段分布去调。 实例数从 8 扩到 16,所以输入参数新增 i9_ 到 i12_(原文片段里 i12_ 实际对应索引 11,复制代码时要核对别串行),count_ 默认值同步改成 16。索引先塞进 CHashSet 去重,集合大小若小于数组长度就报输入错误、不启动。 LoadParams() 也重写了:不再全文件读入,而是按传入的索引列表拼 SQL 只取任务库里那几行,再把初始化串用逗号拼好返回。OnTesterInit() 里调 CreateTaskDB() 建库;单独测试跑时没有库就当场重建,避免空指针。外汇与贵金属市场杠杆高、滑点跳空频繁,这类多实例组合在历史回测里表现好,实盘仍可能显著回撤。

MQL5 / C++
class="macro">#define PARAMS_FILE "database892.stage2.sqlite"
class="macro">#class="kw">property tester_file PARAMS_FILE
class="kw">input group "::: Selection for the group"
class="kw">input class="type">class="kw">string  fileName_      = "database892.sqlite"; class=class="str">"cmt">// - File with the main database
class="kw">input class="type">int      idParentJob_  = class="num">1;                   class=class="str">"cmt">// - Parent job ID
class="kw">input class="type">int      count_        = class="num">16;                  class=class="str">"cmt">// - Number of strategies in the group(class="num">1 .. class="num">16)
class="kw">input class="type">int   i1_ = class="num">1;    class=class="str">"cmt">// - Strategy index #class="num">1
class="kw">input class="type">int   i2_ = class="num">2;    class=class="str">"cmt">// - Strategy index #class="num">2
class="kw">input class="type">int   i3_ = class="num">3;    class=class="str">"cmt">// - Strategy index #class="num">3
class="kw">input class="type">int   i4_ = class="num">4;    class=class="str">"cmt">// - Strategy index #class="num">4
class="kw">input class="type">int   i5_ = class="num">5;    class=class="str">"cmt">// - Strategy index #class="num">5
class="kw">input class="type">int   i6_ = class="num">6;    class=class="str">"cmt">// - Strategy index #class="num">6
class="kw">input class="type">int   i7_ = class="num">7;    class=class="str">"cmt">// - Strategy index #class="num">7
class="kw">input class="type">int   i8_ = class="num">8;    class=class="str">"cmt">// - Strategy index #class="num">8
class="kw">input class="type">int   i9_ = class="num">9;    class=class="str">"cmt">// - Strategy index #class="num">9
class="kw">input class="type">int   i10_ = class="num">10;  class=class="str">"cmt">// - Strategy index #class="num">10
class="kw">input class="type">int   i12_ = class="num">11;  class=class="str">"cmt">// - Strategy index #class="num">11

分阶段优化里的参数筛选与落库

在多阶段 EA 优化框架里,先把策略索引 12 到 16 用 input 暴露出来,方便在 MT5 参数页直接切换不同子策略的编号,而不必改代码重编译。 CreateTaskDB 做的是给「单独阶段任务」建一张干净的参数库:先连本地参数文件、删掉旧 passes 表再建新表,字段只有自增 id 和 params 文本,结构极简。 随后它回连主数据库,用一条 JOIN 四张表(passes / tasks / jobs / stages)的 SQL 把上一阶段叫 'First' 且父任务 id 匹配的结果捞出来。过滤条件很硬:custom_ontester 大于 2500、成交笔数 trades 大于 20、夏普比率 sharpe_ratio 大于 2,才进下一阶段。 这套写法意味着你跑完第一阶段优化后,只有约满足上述阈值的参数组合会被喂给后续阶段,能明显压住组合爆炸。外汇与贵金属品种波动大、滑点不确定,这类阈值只是历史回测筛选,实盘表现可能明显偏离。

MQL5 / C++
class="kw">input class="type">int   i11_ = class="num">12;    class=class="str">"cmt">// - Strategy index #class="num">12
class="kw">input class="type">int   i13_ = class="num">13;    class=class="str">"cmt">// - Strategy index #class="num">13
class="kw">input class="type">int   i14_ = class="num">14;    class=class="str">"cmt">// - Strategy index #class="num">14
class="kw">input class="type">int   i15_ = class="num">15;    class=class="str">"cmt">// - Strategy index #class="num">15
class="kw">input class="type">int   i16_ = class="num">16;    class=class="str">"cmt">// - Strategy index #class="num">16
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Creating a database for a separate stage task                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CreateTaskDB(const class="type">class="kw">string fileName, const class="type">int idParentJob) {
class=class="str">"cmt">// Create a new database for the current optimization task
   DB::Connect(PARAMS_FILE, class="num">0);
   DB::Execute("DROP TABLE IF EXISTS passes;");
   DB::Execute("CREATE TABLE passes(id_pass INTEGER PRIMARY KEY AUTOINCREMENT, params TEXT);");
   DB::Close();
class=class="str">"cmt">// Connect to the main database
   DB::Connect(fileName);
class=class="str">"cmt">// Request to obtain the required information from the main database
   class="type">class="kw">string query = StringFormat(
                     "SELECT DISTINCT  p.params"
                     "  FROM passes p"
                     "       JOIN"
                     "       tasks t ON p.id_task = t.id_task"
                     "       JOIN"
                     "       jobs j ON t.id_job = j.id_job"
                     "       JOIN"
                     "       stages s ON j.id_stage = s.id_stage"
                     " WHERE(s.name=&class="macro">#x27;First&class="macro">#x27; AND "
                     "       j.id_job = %d AND"
                     "       p.custom_ontester > class="num">2500 AND "
                     "       trades > class="num">20 AND "
                     "       p.sharpe_ratio > class="num">2)"
                     " ORDER BY s.id_stage ASC,"
                     "          j.id_job ASC,"
                     "          p.custom_ontester DESC;", idParentJob);
class=class="str">"cmt">// Execute the request
   class="type">int request = DatabasePrepare(DB::Id(), query);

「把优化任务拆进独立数据库的实现细节」

在 MT5 的优化器里做分布式任务切分,核心是把每一组策略参数落地到单独的 SQLite 数据库,而不是全堆在主库。下面这段逻辑先判断 SQL 请求句柄是否合法,无效就打印函数名、查询语句和错误码后断开连接直接返回,避免脏数据继续写入。 [CODE]if(request == INVALID_HANDLE) { PrintFormat(__FUNCTION__" | ERROR: request \n%s\nfailed with code %d", query, GetLastError()); DB::Close(); return; } // Structure for query results struct Row { string params; } row; // Array for requests to insert data into a new database string queries[]; // Fill the request array: we will only save the initialization strings while(DatabaseReadBind(request, row)) { APPEND(queries, StringFormat("INSERT INTO passes VALUES(NULL, '%s');", row.params)); } // Reconnect to the new database and fill it DB::Connect(PARAMS_FILE, 0); DB::ExecuteTransaction(queries); // Reconnect to the main database DB::Connect(fileName); DB::Close(); } //+------------------------------------------------------------------+

//Initialization before optimization

//+------------------------------------------------------------------+ int OnTesterInit(void) { // Create a database for a separate stage task CreateTaskDB(fileName_, idParentJob_); // Get the number of strategy parameter sets int totalParams = GetParamsTotal(); // If nothing is loaded, report an error if(totalParams == 0) { PrintFormat(__FUNCTION__" | ERROR: Can't load data from file %s.\n" "Check that it exists in data folder or in common data folder.", fileName_); return(INIT_FAILED); } // Set scale_ to 1 ParameterSetRange("scale_", false, 1, 1, 1, 2); // Set the ranges of change for the parameters of the set index iteration for(int i = 1; i <= 16; i++) { if(i <= count_) { ParameterSetRange("i" + (string) i + "_", true, 0, 1, 1, totalParams); } else { // Disable the enumeration for extra indices ParameterSetRange("i" + (string) i + "_", false, 0, 1, 1, totalParams); } } return CVirtualAdvisor::TesterInit(idTask_); } //+------------------------------------------------------------------+

//Number of strategy parameter sets in the task database

//+------------------------------------------------------------------+ int GetParamsTotal() { int paramsTotal = 0; // If the task database is open, if(DB::Connect(PARAMS_FILE, 0)) { // Create a request to get the number of passes for this task string query = "SELECT COUNT(*) FROM passes p"; int request = DatabasePrepare(DB::Id(), query);[/CODE] 逐行拆解几个关键点:struct Row 只存了 params 字符串,说明落库时只保留初始化参数本身;while(DatabaseReadBind(...)) 把主库读出的每一行绑定到 row,再用 APPEND 拼成 INSERT 语句塞进 queries 数组,最后靠 DB::ExecuteTransaction 一次性写新库,比逐条执行更省 IO。 OnTesterInitParameterSetRange("scale_", false, 1, 1, 1, 2) 把 scale_ 锁死为单值,而循环到 i<=16 时,只有 i<=count_ 的索引才开放枚举范围 0~totalParams,其余强制 disable——这意味着你最多并行调度 16 个参数维度,超出的部分不会参与优化器遍历。 GetParamsTotalSELECT COUNT(*) FROM passes p 查任务库里的参数组数,若文件缺失或连不上,OnTesterInit 会返回 INIT_FAILED 并提示去 data folder 或 common data folder 核查。外汇与贵金属品种做这类批量优化时杠杆和滑点敏感,回测结果和实际跑盘可能有偏差,属于高风险验证,建议先拿 1~2 组参数在 MT5 策略测试器里手动跑通再扩量。

MQL5 / C++
if(request == INVALID_HANDLE) {
   PrintFormat(__FUNCTION__" | ERROR: request \n%s\nfailed with code %d", query, GetLastError());
   DB::Close();
   class="kw">return;
}
class=class="str">"cmt">// Structure for query results
class="kw">struct Row {
   class="type">class="kw">string      params;
} row;
class=class="str">"cmt">// Array for requests to insert data into a new database
class="type">class="kw">string queries[];
class=class="str">"cmt">// Fill the request array: we will only save the initialization strings
class="kw">while(DatabaseReadBind(request, row)) {
   APPEND(queries, StringFormat("INSERT INTO passes VALUES(NULL, &class="macro">#x27;%s&class="macro">#x27;);", row.params));
}
class=class="str">"cmt">// Reconnect to the new database and fill it
DB::Connect(PARAMS_FILE, class="num">0);
DB::ExecuteTransaction(queries);
class=class="str">"cmt">// Reconnect to the main database
DB::Connect(fileName);
DB::Close();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Initialization before optimization                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnTesterInit(class="type">void) {
class=class="str">"cmt">// Create a database for a separate stage task
   CreateTaskDB(fileName_, idParentJob_);
class=class="str">"cmt">// Get the number of strategy parameter sets
   class="type">int totalParams = GetParamsTotal();
class=class="str">"cmt">// If nothing is loaded, report an error
   if(totalParams == class="num">0) {
      PrintFormat(__FUNCTION__" | ERROR: Can&class="macro">#x27;t load data from file %s.\n"
                  "Check that it exists in data folder or in common data folder.",
                  fileName_);
      class="kw">return(INIT_FAILED);
   }
class=class="str">"cmt">// Set scale_ to class="num">1
   ParameterSetRange("scale_", false, class="num">1, class="num">1, class="num">1, class="num">2);
class=class="str">"cmt">// Set the ranges of change for the parameters of the set index iteration
   for(class="type">int i = class="num">1; i <= class="num">16; i++) {
      if(i <= count_) {
       ParameterSetRange("i" + (class="type">class="kw">string) i + "_", true, class="num">0, class="num">1, class="num">1, totalParams);
      } else {
       class=class="str">"cmt">// Disable the enumeration for extra indices
       ParameterSetRange("i" + (class="type">class="kw">string) i + "_", false, class="num">0, class="num">1, class="num">1, totalParams);
      }
   }
   class="kw">return CVirtualAdvisor::TesterInit(idTask_);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Number of strategy parameter sets in the task database           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int GetParamsTotal() {
   class="type">int paramsTotal = class="num">0;
class=class="str">"cmt">// If the task database is open,
   if(DB::Connect(PARAMS_FILE, class="num">0)) {
   class=class="str">"cmt">// Create a request to get the number of passes for this task
      class="type">class="kw">string query = "SELECT COUNT(*) FROM passes p";
      class="type">int request = DatabasePrepare(DB::Id(), query);

◍ 从本地库按索引抽取参数集

把优化后的多组参数落库后,真正在 EA 里调用时只需按输入索引把对应行读出来拼成逗号串。下面这段 LoadParams 就是干这个:先拿总组数,再拼 IN 子句,最后逐行 bind 拼接。 别把索引拼串想简单了 原文在 FOREACH 拼完 indexes 后硬加了一个 "0",原因是循环里每个索引后都带了逗号,若不补一个不存在的 id,最后会多出一个逗号导致 SQL 语法错。这个坑在 MT5 的 DatabasePrepare 阶段就会报 INVALID_HANDLE,而不是执行期才炸。 读结果用 while 而不是 if GetParamsTotal 里用 DatabaseReadBind 读单行 total 用 if 足够;但 LoadParams 面对多组参数必须用 while 循环把每行 params 累加。若错写成 if,只会拿到第一组,EA 实际加载的也就一组,回测和实盘信号可能明显偏离优化预期。外汇与贵金属杠杆品种下,这种参数缺失会放大滑点与隔夜风险。 直接在 MT5 里建个 PARAMS_FILE 的 sqlite,跑一遍 LoadParams(Indexes[]) 看返回的 params 字符串组数是否等于输入索引数,就能验证这套读取链路通不通。

MQL5 / C++
class="type">class="kw">string LoadParams(class="type">int &indexes[]) {
  class="type">class="kw">string params = NULL;
class=class="str">"cmt">// Get the number of sets
  class="type">int totalParams = GetParamsTotal();
class=class="str">"cmt">// If they exist, then
  if(totalParams > class="num">0) {
    if(DB::Connect(PARAMS_FILE, class="num">0)) {
      class=class="str">"cmt">// Form a class="type">class="kw">string from the indices of the comma-separated sets taken from the EA inputs
      class=class="str">"cmt">// for further substitution into the SQL query
      class="type">class="kw">string strIndexes = "";
      FOREACH(indexes, strIndexes += IntegerToString(indexes[i]) + ",");
      strIndexes += "class="num">0"; class=class="str">"cmt">// Add a non-existent index so as not to remove the last comma
      class=class="str">"cmt">// Form a request to obtain sets of parameters with the required indices
      class="type">class="kw">string query = StringFormat("SELECT params FROM passes p WHERE id_pass IN(%s)", strIndexes);
      class="type">int request = DatabasePrepare(DB::Id(), query);
      if(request != INVALID_HANDLE) {
        class=class="str">"cmt">// Data structure for query results
        class="kw">struct Row {
           class="type">class="kw">string  params;
        } row;
        class=class="str">"cmt">// Read the query results and join them with a comma
        class="kw">while(DatabaseReadBind(request, row)) {
           params += row.params + ",";
        }
      } else {
        PrintFormat(__FUNCTION__" | ERROR: request \n%s\nfailed with code %d",
                     query, GetLastError());
      }
      DB::Close();
    }
  }
  class="kw">return params;
}

常见问题

把每个币种的手数、止损、魔法码等拼成带分隔符的字符串,再按组拆解写入虚拟持仓引擎的初始化函数,避免逐个硬编码。
在优化循环结束处把参数集和对应绩效按索引写入独立数据库,下次启动直接读库抽取,不必重跑全量。
可以,小布能读取你的参数字符串与持仓引擎初始化结果,标出分组错位或越界项,省去人工比对。
按币种+阶段做索引落本地库,抽取时用索引定位参数集,不要混在一张表里,后期回抽更快。
抽数前先校验索引前缀与币种标签一致,只载对应阶段的记录,可杜绝串组加载。