开发多币种 EA 交易 (第 13 部分):自动化第二阶段 分组选择(基础篇)
◍ 让 EA 自动圈定交易分组
多币种 EA 到了第二阶段,核心不再是单品种死磕,而是按规则把品种自动归组。手动在 MT5 里勾选十几个货币对既慢又容易漏,尤其当监控池扩到 30 个以上时,人工维护分组基本不可行。 思路是在 EA 初始化阶段读取市场观察窗口的所有可见品种,再用波动率或相关性阈值做过滤。这样每次重开终端,分组都能跟着当前行情重新算出来,不用写死在代码里。 一个朴素的实现是先抓全部品种名,再筛掉点差异常或近期振幅过低的。下例在 OnInit 里完成分组选择,实盘前建议在策略测试器用 2024 年数据跑一遍,观察分组数量是否稳定在 8~15 个之间。
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 就能编译验证。
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 涉及文件读写,权限异常会让策略静默失效,属高风险环节。
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 单。 外汇与贵金属波动剧烈,上述参数仅用于历史数据优化实验,实盘映射存在过拟合可能,任何结果都只是概率倾向而非确定性结论。
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)",