开发多币种 EA 交易(第 21 部分):准备重要实验并优化代码(基础篇)
📘

开发多币种 EA 交易(第 21 部分):准备重要实验并优化代码(基础篇)

第 1/3 篇

「多币种 EA 的实验前代码整备」

做多币种 EA 最忌讳一上来就拉全市场回测。先把单币种逻辑跑通,再谈横向扩展,否则一旦出错你根本分不清是某个品种的问题还是架构本身漏了东西。 本次系列第 21 部分的目标是给后续重要实验铺路:重构底层调用,让 EA 能按预设品种列表分别加载历史与实时报价,而不再写死在单一图表上。 原文作者 Yuriy Bykov 在 2025 年 11 月 14 日发布该整备章节,社区基础互动量为 700 阅读、7 条讨论,说明这类底层改造即便偏工程向,仍有稳定受众在跟进度。 开 MT5 后建议先新建一个空 EA,把 Symbol 参数改成字符串数组输入,手动填 3~5 个流动性高的贵金属或直盘,验证遍历能否正常 Init,再继续往下做实验。外汇与贵金属杠杆高,多币种同跑会放大回撤,任何扩展都先用极小手数验证。

◍ 把硬决策往后挪,先绕去能攒动力的活

上一阶段我们已经搭起自动优化流水线,能基于累积价格数据吐出新的最终 EA。但离完全自动化还差最后一步:怎么落地收尾阶段。这步选择很难,选错了就得大量重做,所以宁可慢一点也要选对方向。 做艰难决定时,最有效的办法往往不是硬扛,而是推迟。前提是时间成本允许。 但推迟也有讲究。别干等那个做选择的时刻,换个看似分心、实则能增加决策动力的任务先推进。等这条支线有进展,主线的正确路径可能自己浮现出来。外汇与贵金属自动化高风险,任何路线都只是概率倾向,需用 MT5 实测验证。

定期重优化能撑多久

参数优化最大的争议点在于:把盈利和回撤压在设定区间内后,这套参数还能在接下来多久真正拿来跑。很多人嘴上说“未来测试结果不可信、策略崩了纯靠运气”,但几乎所有写策略的人内心都不信这个邪,否则前面耗的开发和回测精力全打了水漂。 提高“策略在一段时间内还能跑”的信心,办法之一是把重优化做成固定节奏。fxsaber 的 Validate EA 就是干这个的:拿一个 EA,框定比如 3 年总区间,每 2 个月回看优化一次,用最优参数跑随后 2 周实盘模拟,循环到 3 年结束,吐出一份“如果一直这么重优化会怎样”的整段报告。间隔你随便改,只要 EA 在这种机制下结果还能看,真实交易潜力就上了个台阶。 但工具有硬限制:被研究的 EA 必须有开放输入参数才能优化。像前面把多个实例拼起来的最终 EA,根本没有能改开仓逻辑的输入项;资金和风险类参数也不算——仓位加大,回测利润自然水涨船高,优化它没意义。 外汇和贵金属是高杠杆高风险品种,这种重优化实验只说明“可能”提升存活概率,不等于未来实盘稳过。下一步,我们给自研 EA 也搭一套类似的重优化流程。

「用服务 EA 批量铺优化任务」

想给数据库塞进大量相似项目,最省事的办法是写一个服务 EA,只暴露少数输入参数:优化期开始日期、持续时长。项目内部的阶段、任务结构可以完全复用,差异仅落在时间窗口上。 在 MT5 优化模式下跑这个 EA 并扫描开始日期,就能自动把同构项目填进库里。现阶段还不清楚要再暴露哪些参数有意义,等开发推进再定。 完整跑完一个项目的全部优化任务都可能很慢,若同时排十几个项目,就是肉眼可见的耗时工程。所以下一步要用 MetaEditor 自带的 Profiler 抓瓶颈,看阶段 EA 哪段最拖。 初始化字符串的模拟复用先搁置,大概率要专门写个测试 EA,但可能推到系列下一篇。本篇先写优化测试 EA 代码,再写填库脚本。

◍ 用分析器揪出EA里的品种查询瓶颈

在动手写主体逻辑前,先给自动优化用的 EA 瘦个身。拿上一部分那个组合了 32 个策略实例(2 品种 * 1 周期 * 16 实例)的 EA 做样本,在 MT5 策略测试器里用「分析模式」跑一遍——这模式会每秒约暂停程序 10000 次做采样,不改代码、不影响速度,只统计函数占用。 报告里 OnTick() 和 CVirtualAdvisor::Tick() 排前两位很正常,但第三、四位居然是交易品种选择方法和整数属性获取,这就不对了。点开 CSymbolInfo::Name(string name) 的调用栈,时间几乎全耗在平仓检查函数里:旧代码给每个 CVirtualOrder 都挂了一个 CSymbolInfo 实例,16 个策略实例各带 3 个虚拟仓位就是 48 个,每次报价全调一遍 Name()。 实际上品种名只在虚拟仓位换品种时才该查。一个策略实例绑死一个品种,根本不会每次报价都变。把 CSymbolInfo 提至全局,用 Singleton 容器 CSymbolsMonitor 按品种名按需创建、永久复用,调用次数能降几个数量级。外汇与贵金属 EA 多实例并行时这种冗余会放大延迟,属于高风险系统的隐性损耗。 下方代码里高亮那行就是病根:每次 CheckClose 都无脑 s_symbolInfo.Name(m_symbol)。后面 CSymbolsMonitor 的骨架用重载 [] 运算符按名取指针,首次访问才新建对象,之后直接复用。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check the need to close by SL, TP or EX                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CVirtualOrder::CheckClose() {
   if(IsMarketOrder()) {                    class=class="str">"cmt">// If this is a market class="kw">virtual position,
      s_symbolInfo.Name(m_symbol);          class=class="str">"cmt">// Select the desired symbol
      s_symbolInfo.RefreshRates();          class=class="str">"cmt">// Update information about current prices
      class=class="str">"cmt">// ...
   }
   class="kw">return false;
}
class=class="str">"cmt">//+--------------------------------------------------------------------+
class=class="str">"cmt">//| Class for obtaining information about trading instruments(symbols)|
class=class="str">"cmt">//+--------------------------------------------------------------------+
class CSymbolsMonitor {
class="kw">protected:
class=class="str">"cmt">// Static pointer to a single class instance
   class="kw">static   CSymbolsMonitor *s_instance;
class=class="str">"cmt">// Array of information objects for different symbols
   CSymbolInfo       *m_symbols[];
class=class="str">"cmt">//--- Private methods
                  CSymbolsMonitor() {} class=class="str">"cmt">// Closed constructor
class="kw">public:
                 ~CSymbolsMonitor();   class=class="str">"cmt">// Destructor
class=class="str">"cmt">//--- Static methods
   class="kw">static
   CSymbolsMonitor   *Instance();      class=class="str">"cmt">// Singleton - creating and getting a single instance
   class=class="str">"cmt">// Tick handling for objects of different symbols
   class="type">void              Tick();
   class=class="str">"cmt">// Operator for getting an object with information about a specific symbol
   CSymbolInfo*      class="kw">operator[](const class="type">class="kw">string &symbol);
};
class=class="str">"cmt">// Initializing a class="kw">static pointer to a single class instance
CSymbolsMonitor *CSymbolsMonitor::s_instance = NULL;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Singleton - creating and getting a single instance                |
class=class="str">"cmt">//+------------------------------------------------------------------+
CSymbolsMonitor* CSymbolsMonitor::Instance() {

监控器的生命周期与按名取标的实现

CSymbolsMonitor 用懒加载单例保证全局只有一份标的监控实例:第一次访问时若 s_instance 为空才 new 出来,之后直接返回指针,避免重复构造带来的资源浪费。 析构函数里用 FOREACH 遍历 m_symbols 数组,对每个非空元素执行 delete,把之前为各交易品种创建的 CSymbolInfo 对象全部释放,防止 MT5 EA 卸载时内存泄漏。 Tick() 方法按周期刷新数据:对数组里每个品种,若日线出新 K 线(PERIOD_D1)就调 Refresh() 重抓合约规格,若分钟线出新 K 线(PERIOD_M1)就调 RefreshRates() 更新报价。实际跑起来相当于每天重读一次品种参数、每分钟刷一次实时价,对外汇和贵金属这类高波动品种尤其要留意外盘休市时 Refresh 可能返回旧规格。 重载的 operator[] 按名称取信息对象:先 SEARCH 在已有数组里找同名项,命中直接返回;未命中则 new 一个 CSymbolInfo 并 Name(name) 选中品种,成功就 RefreshRates 后 APPEND 进数组并返回,失败则 PrintFormat 报错并返回 NULL。这样调用方用 monitor["XAUUSD"] 就能随用随建,不用提前声明监听列表。

MQL5 / C++
if(!s_instance) {
    s_instance = new CSymbolsMonitor();
  }
  class="kw">return s_instance;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Destructor                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
CSymbolsMonitor::~CSymbolsMonitor() {
class=class="str">"cmt">// Delete all created information objects for symbols
  FOREACH(m_symbols, if(!!m_symbols[i]) class="kw">delete m_symbols[i]);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Handle a tick for the array of class="kw">virtual orders(positions)         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CSymbolsMonitor::Tick() {
class=class="str">"cmt">// Update quotes every minute and specification once a day
  FOREACH(m_symbols, {
    if(IsNewBar(m_symbols[i].Name(), PERIOD_D1)) {
      m_symbols[i].Refresh();
    }
    if(IsNewBar(m_symbols[i].Name(), PERIOD_M1)) {
      m_symbols[i].RefreshRates();
    }
  });
}
class=class="str">"cmt">//+-------------------------------------------------------------------------+
class=class="str">"cmt">//| Operator for getting an object with information about a specific symbol  |
class=class="str">"cmt">//+-------------------------------------------------------------------------+
CSymbolInfo* CSymbolsMonitor::class="kw">operator[](const class="type">class="kw">string &name) {
class=class="str">"cmt">// Search for the information object for the given symbol in the array
  class="type">int i;
  SEARCH(m_symbols, m_symbols[i].Name() == name, i);
class=class="str">"cmt">// If found, class="kw">return it
  if(i != -class="num">1) {
    class="kw">return m_symbols[i];
  } else {
    class=class="str">"cmt">// Otherwise, create a new information object
    CSymbolInfo *s = new CSymbolInfo();
    class=class="str">"cmt">// Select the desired symbol for it
    if(s.Name(name)) {
      class=class="str">"cmt">// If the selection is successful, update the quotes
      s.RefreshRates();
      class=class="str">"cmt">// Add to the array of information objects and class="kw">return it
      APPEND(m_symbols, s);
      class="kw">return s;
    } else {
      PrintFormat(__FUNCTION__" | ERROR: can&class="macro">#x27;t create symbol with name [%s]", name);
    }
  }
  class="kw">return NULL;
}

常见问题

先把品种查询、下单接口等基础模块做成可插拔,用占位规则跑通整体流程,再回头补策略决策,能更快攒出可验证版本。
实盘里通常几周到数月不等,取决于波动 regime;建议用监控器跟踪衰减值,跌破阈值就触发重优化,别死等固定周期。
可以。把EA日志或品种调用频率丢给小布,它能用AIGC分析器标出查询瓶颈点,并给出按名取标的的精简建议。
按CPU核心数分桶排队,每桶限制并发数,并用生命周期监控器回收闲置句柄,能稳住资源占用。
先打印品种名清单比对大小写与后缀,再查监控器是否在Tick前已销毁;多数空值是早期生命周期未覆盖导致的。