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

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

第 1/3 篇

◍ 让 EA 自动圈定交易分组

多币种 EA 到了第二阶段,核心不再是单品种死磕,而是按规则把品种自动归组。手动在 MT5 里勾选十几个货币对既慢又容易漏,尤其当监控池扩到 30 个以上时,人工维护分组基本不可行。 思路是在 EA 初始化阶段读取市场观察窗口的所有可见品种,再用波动率或相关性阈值做过滤。这样每次重开终端,分组都能跟着当前行情重新算出来,不用写死在代码里。 一个朴素的实现是先抓全部品种名,再筛掉点差异常或近期振幅过低的。下例在 OnInit 里完成分组选择,实盘前建议在策略测试器用 2024 年数据跑一遍,观察分组数量是否稳定在 8~15 个之间。

MQL5 / C++
class="type">class="kw">string groups[];
class="type">int OnInit()
{
   class="type">int total = SymbolsTotal(true);
   for(class="type">int i=class="num">0;i<total;i++)
   {
      class="type">class="kw">string sym = SymbolName(i, true);
      class="type">class="kw">double spread = SymbolInfoInteger(sym, SYMBOL_SPREAD);
      if(spread > class="num">0 && spread < class="num">30)
         groups[ArraySize(groups)] = sym;
   }
   class="kw">return(INIT_SUCCEEDED);
}

从单实例优化走向组合筛选

上一节把精力放在风险管理上,这一节重新回到测试自动化的主轴。系列前面已经跑完第一阶段:对单个交易策略实例做参数优化,结果落进数据库。 下一阶段要做的,是从一堆单实例里挑出能协同工作的一组,目标很实际——回撤可能压下来、余额曲线增长更偏向线性。第六部分手动干过这活:先筛掉负利润结果,再把八个实例的不同组合塞进一个 EA 在测试器里跑,看共同工作的参数。 手动之后接了自动:从 CSV 读参数列表,自动选单实例输入组合。即便最简单的情况,对八种组合跑一轮遗传优化,也达到了预期结果。 现在要改的是做组选择优化的 EA——让它直接吃数据库里第一阶段的结果,并把第二阶段结果也写回库;同时考虑往库里插一条“发起第二阶段优化”的任务记录,让流程能自己接上。

「远程代理跑优化时的数据搬运坑」

本地优化时把 CSV 丢进终端共享文件夹,所有本地代理都能读;但远程测试代理碰不到这个文件,得靠 #property tester_file 指令把指定文件塞进每个代理的数据文件夹。启动优化后,文件先从共享文件夹拷到本地代理,再自动分发到其余远程代理。 最初我想把 SQLite 主库也照此传递。CSV 大约 2 MB,而数据库文件超过 300 MB——差出两个数量级。原因有两点:库里存了每次通过的详尽统计,CSV 只留了少量参数;库覆盖了 3 个品种 × 3 个周期,通过量差不多是 CSV 的 9 倍。

  • 核服务器上每个代理都要独吞一份副本,光这一项就得预备超 9 GB 空间。品种和周期再往上加,文件会再翻几倍,磁盘和网络都扛不住。

实际上第二阶段用不上绝大多数历史存储,一次通过里真正必要的只有 EA 初始化字符串,外加按「品种+周期」各留一份策略副本。比如搜 EURGBP H1,其他品种和周期的数据全是噪音。 做法改成:每次优化新建一个只装必需信息的「任务数据库」,原有大库称「主数据库」。tester_file 只指任务库,代理跑的时候读主库摘要;本地数据帧收集模式仍回写主库。要实现这套流,得先改 CDatabase 类。

◍ 把数据库类改成可切换连接

最初写 CDatabase 时只打算让单个 EA 守着一个库,避免以后搞不清数据存哪。实际跑下来发现同一套代码要碰多个库,静态类写法改动最小:不实例化,直接把公有方法当命名空间里的函数用,私有成员照常发挥作用。 原来的 Open() 写死读 s_fileName 常量,永远只开那个固定名。现在重命名为 Connect(),核心区别是允许把库名当参数传进来;不传就沿用旧行为,传了非空名不仅当场打开,还会回写 s_fileName,后续无参调用就接着开这个最后指定的库。 除了库名,Connect() 还接一个是否走共享文件夹的标志。主库放终端公共数据目录更顺手,任务库放测试代理目录,所以得靠 DATABASE_OPEN_COMMON 区分。类里新增静态成员 s_common,默认就是 DATABASE_OPEN_COMMON。 方法体内先查当前是否已开库,开了就 Close();再判是否给了新文件名,给了就同时存名称和共享标志;最后用 DatabaseOpen 尝试以读写加共享标志打开。原先 Connect 里强制建表填数的逻辑挪走了,新库交给 CleanDatabase 脚本初始化,日常主要连现成库。 默认主库文件名在代码里是 database892.sqlite,s_common 初值即 DATABASE_OPEN_COMMON,改完存回 Database.mqh 就能编译验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class for handling the database                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CDatabase {
  class="kw">static class="type">int        s_db;          class=class="str">"cmt">// DB connection handle
  class="kw">static class="type">class="kw">string     s_fileName;    class=class="str">"cmt">// DB file name
  class="kw">static class="type">int        s_common;      class=class="str">"cmt">// Flag for using shared data folder

class="kw">public:
  class="kw">static class="type">int        Id();          class=class="str">"cmt">// Database connection handle
  class="kw">static class="type">bool       IsOpen();      class=class="str">"cmt">// Is the DB open?
  class="kw">static class="type">void       Create();      class=class="str">"cmt">// Create an empty DB
  class=class="str">"cmt">// Connect to the database with a given name and location
  class="kw">static class="type">bool       Connect(class="type">class="kw">string p_fileName = NULL,
                            class="type">int p_common = DATABASE_OPEN_COMMON
                           );
  class="kw">static class="type">void       Close();       class=class="str">"cmt">// Closing DB
  ...
};
class="type">int    CDatabase::s_db      =  INVALID_HANDLE;
class="type">class="kw">string CDatabase::s_fileName = "database892.sqlite";
class="type">int    CDatabase::s_common  =  DATABASE_OPEN_COMMON;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check connection to the database with the given name            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CDatabase::Connect(class="type">class="kw">string p_fileName, class="type">int p_common) {
  class=class="str">"cmt">// If the database is open, close it 
  if(IsOpen()) {
    Close();
  }
  class=class="str">"cmt">// If a file name is specified, save it together with the shared folder flag
  if(p_fileName != NULL) {
    s_fileName = p_fileName;
    s_common = p_common;
  }
  class=class="str">"cmt">// Open the database
  class=class="str">"cmt">// Try to open an existing DB file
  s_db = DatabaseOpen(s_fileName, DATABASE_OPEN_READWRITE | s_common);

数据库打不开就现建一个

在 MT5 的 SQLite 封装里,常见的坑是数据库文件不存在时直接连接失败。上面这段逻辑先把判断交给 IsOpen(),没开就走 DatabaseOpen 并带上 DATABASE_OPEN_CREATE 标志,等于告诉引擎:文件没有就现场建。 注意 s_common 这个变量,它决定数据库是存在终端私有的 sandbox 目录,还是所有程序共用的 common 数据目录;少写这一位,回测和实盘可能各读各的文件。 如果第二次 IsOpen() 仍然为假,说明磁盘权限或路径有问题,代码用 PrintFormat 把文件名和 GetLastError() 的错误码打出来然后返回 false。开 MT5 跑这段时,若日志出现「Connect failed with code 2」,基本就是文件目录被系统拦了,换 common 标志通常能解。外汇与贵金属 EA 涉及文件读写,权限异常会让策略静默失效,属高风险环节。

MQL5 / C++
  class=class="str">"cmt">// If the DB file is not found, try to create it when opening
  if(!IsOpen()) {
      s_db = DatabaseOpen(s_fileName,
                           DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | s_common);
      class=class="str">"cmt">// Report an error in case of failure
      if(!IsOpen()) {
         PrintFormat(__FUNCTION__" | ERROR: %s Connect failed with code %d",
                     s_fileName, GetLastError());
         class="kw">return false;
      }
  }
  class="kw">return true;
}

「第一阶段 EA 的瘦身与参数冻结」

这一阶段的 EA 不打算直接跑实盘,但代码要保持前后连贯,所以先做减法。上一篇文章里塞进去的风险管理器输入参数被整组拿掉——第一阶段优化根本不会去碰风控变量,留到后面某个优化阶段再接回,初始化时即便管理器处于非活动态也直接把对象建出来。 幻数、固定交易余额、缩放因子这类在第一阶段不参与寻优的变量,声明时把 input 前缀摘掉,变成内部常量。这样 MT5 优化器不会把它们列进参数组合,回测扫描范围更干净。改动后文件另存为 SimpleVolumesStage1.mq5 即可。 从代码看,magic_ 固定为 27181,fixedBalance_ 写死 10000、scale_ 为 1,意味着此阶段所有测试都基于 1 万单位虚拟本金、不缩放仓位。信号端仍暴露可优化项:signalPeriod_ 默认 130 根 K 线均量、signalDeviation_ 0.9 与 signaAddlDeviation_ 1.4 控制挂单触发偏离,openDistance_ 231 点、stopLevel_ 3750 点、takeLevel_ 50 点,最大同开 3 单。 外汇与贵金属波动剧烈,上述参数仅用于历史数据优化实验,实盘映射存在过拟合可能,任何结果都只是概率倾向而非确定性结论。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Inputs                                                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">input class="type">int            idTask_                 = class="num">0;
class="kw">input group "===    Opening signal parameters"
class="kw">input class="type">int            signalPeriod_          = class="num">130;   class=class="str">"cmt">// Number of candles for volume averaging
class="kw">input class="type">class="kw">double         signalDeviation_       = class="num">0.9;   class=class="str">"cmt">// Relative deviation from the average to open the first order 
class="kw">input class="type">class="kw">double         signaAddlDeviation_    = class="num">1.4;   class=class="str">"cmt">// Relative deviation from the average for opening the second and subsequent orders
class="kw">input group "===    Pending order parameters"
class="kw">input class="type">int            openDistance_          = class="num">231;   class=class="str">"cmt">// Distance from price to pending order
class="kw">input class="type">class="kw">double         stopLevel_             = class="num">3750; class=class="str">"cmt">// Stop Loss(in points)
class="kw">input class="type">class="kw">double         takeLevel_             = class="num">50;   class=class="str">"cmt">// Take Profit(in points)
class="kw">input class="type">int            ordersExpiration_      = class="num">600;   class=class="str">"cmt">// Pending order expiration time(in minutes)
class="kw">input group "===    Money management parameters"
class="kw">input class="type">int            maxCountOfOrders_      = class="num">3;     class=class="str">"cmt">// Maximum number of simultaneously open orders
class="type">ulong                magic_                 = class="num">27181; class=class="str">"cmt">// Magic
class="type">class="kw">double               fixedBalance_          = class="num">10000;
class="type">class="kw">double               scale_                 = class="num">1;
class="type">class="kw">datetime fromDate = TimeCurrent();
CAdvisor            *expert;       class=class="str">"cmt">// Pointer to the EA object
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit() {
   CMoney::FixedBalance(fixedBalance_);
   CMoney::DepoPart(class="num">1.0);
class=class="str">"cmt">// Prepare the initialization class="type">class="kw">string for a single strategy instance
   class="type">class="kw">string strategyParams = StringFormat(
                           "class CSimpleVolumesStrategy(\"%s\",%d,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d)",

常见问题

在 EA 输入参数里加一个分组字符串(如 "EUR,GBP"),初始化时按前缀或后缀匹配品种,只把命中品种加入交易数组即可。
把第一阶段已验证的固定参数设为只读常量,仅放开分组选择与新筛选阈值,避免优化器跨品种乱调基线。
小布可接入你的品种页,自动汇总各分组信号与报错,把重复巡检交给它,你只看决策摘要。
代码里检测连接失败就调用建库函数现建一个本地库,先把结果落盘,回来再合并,别让任务空跑。
先抽掉历史回测专用日志与未用指标句柄,冻结非核心参数,只留分组选择与下单通道,体积和延迟都会降。