开发多币种 EA 交易(第 8 部分):负载测试和处理新柱·进阶篇
⚙️

开发多币种 EA 交易(第 8 部分):负载测试和处理新柱·进阶篇

(2/3)· 当策略实例从 8 扩到 512,单次回测耗时与内存消耗如何非线性膨胀?

含代码示例偏理论 第 2/3 篇
把 32 个策略实例塞进一个 EA 跑优化,很多人直接拉满测试周期,结果单次通过动辄几小时。其实实例数和模拟模式才是耗时的隐藏杠杆,先测拐点再谈参数空间更划算。

「用 1 分钟 OHLC 提速优化但要防偏差」

把测试模式从「每个分时报价」或「基于真实分时的每个分时报价」切到「1 分钟 OHLC」,能明显缩短单次回测与参数优化的耗时。前提是策略开平仓不频繁(例如每分钟远低于一次),且对止损止盈触发差几个点不敏感,否则不能这么省。 麻烦在实盘或终端跑 EA 时,即便你在优化阶段用了 1 分钟 OHLC,MT5 默认还是按更密的分时报价驱动 OnTick()。价格集更杂、调用更频,和回测拿到的固定每根 4 个报价不一致,结果可能漂移。 我做过同参数对照:左为「基于真实分时的每个分时报价」、右为「1 分钟 OHLC」。两者前几笔的开盘收盘时间、价格只是略有不同,但到第 25 号交易,左边触发了开仓、右边没触发——OHLC 模式下的成交笔数少于真实分时模式。两种模式利润都微增,余额曲线肉眼看不出明显差距。 所以终端实跑大概率不会比 1 分钟 OHLC 回测差,用快模式优化可行。若策略计算只需在新柱开头做,可在代码里识别新柱并跳过其他分时的冗余计算,进一步提速;若真实分时结果反而差于 OHLC,就强制 EA 只在柱首交易,让各模式结果尽量收敛。外汇与贵金属杠杆高,模式切换带来的滑点偏差会放大风险,上 MT5 前务必自己比一遍。

◍ 跨品种多周期的新柱检测该怎么写

写 EA 时最烦的一件事,就是每个策略实例都自己塞一段「判断新柱」的代码,用个变量记最后一根 K 线时间。一旦你要同时盯 EURUSD 的 M5、XAUUSD 的 H1 还有别的品种周期,这种散装写法立刻变成维护灾难。 更好的做法是抽一个 IsNewBar(symbol, timeframe) 公共函数:传入品种和周期,它告诉你当前分时是不是刚出了新柱。关键点在于,同一根新柱上如果策略不同模块多次调用,每一次都应返回 true,而不是只有第一次命中——否则多实例逻辑会漏信号。 为了支撑这个行为,底层得拆成两个函数。UpdateNewBar() 在每个分时报价一开始调一次,批量刷新所有被监控品种周期的「新柱标记」并返回「至少有一个新柱」的布尔值;之后各处的 IsNewBar() 只是读已存好的标记。调用入口放在 CVirtualAdvisor::Tick() 开头最稳。 存储结构用静态类 CNewBarEvent 实现,不实例化只调静态成员。里面两个数组:m_symbols 记品种名,m_symbolNewBarEvent 是指向 CSymbolNewBarEvent 对象的指针,后者按品种存多周期的最后柱时间和标记。注册新监控项用 Register(),EA 退出时务必用 DestroyNewBar() 清内存,否则指针泄漏会拖垮 MT5 终端。 下面这段是核心骨架的节选,可直接存成 NewBarEvent.mqh 再 include。注意 APPEND 是宏,负责往动态数组追元素;Register 里先把周期、最后时间(0)、结果(false) 三数组同步加一项,再 Update 刷标记。

MQL5 / C++
class="type">void CVirtualAdvisor::Tick(class="type">void) {
class=class="str">"cmt">// Define a new bar for all required symbols and timeframes
  UpdateNewBar();
  ...
class=class="str">"cmt">// Start handling in strategies where IsNewBar(...) can already be used
  CAdvisor::Tick();
  ...
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Destructor                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualAdvisor::~CVirtualAdvisor() {
  class="kw">delete m_receiver;      class=class="str">"cmt">// Remove the recipient
  class="kw">delete m_interface;     class=class="str">"cmt">// Remove the interface
  DestroyNewBar();        class=class="str">"cmt">// Remove the new bar tracking objects 
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class for defining a new bar for a specific symbol                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CSymbolNewBarEvent {
class="kw">private:
  class="type">class="kw">string            m_symbol;       class=class="str">"cmt">// Tracked symbol
  class="type">long              m_timeFrames[]; class=class="str">"cmt">// Array of tracked symbol timeframes
  class="type">long              m_timeLast[];   class=class="str">"cmt">// Array of times of the last bars for timeframes
  class="type">bool              m_res[];        class=class="str">"cmt">// Array of flags of a new bar occurrence for timeframes
  class=class="str">"cmt">// The method for registering a new tracked timeframe for a symbol
  class="type">int               Register(ENUM_TIMEFRAMES p_timeframe) {
    APPEND(m_timeFrames, p_timeframe);  class=class="str">"cmt">// Add it to the timeframe array
    APPEND(m_timeLast, class="num">0);              class=class="str">"cmt">// The last time bar for it is still unknown
    APPEND(m_res, class="kw">false);               class=class="str">"cmt">// No new bar for it yet
    Update();                           class=class="str">"cmt">// Update new bar flags
    class="kw">return ArraySize(m_timeFrames) - class="num">1;
  }
class="kw">public:
  class=class="str">"cmt">// Constructor

多品种多周期新K线事件的类封装

跨品种盯盘时,最烦的就是每个EA自己写一套新K线判断逻辑,重复且易错。下面这段把单符号的新 bar 检测收敛进 CSymbolNewBarEvent,外层再用 CNewBarEvent 静态类统管所有品种,调用端只管 Update() 和 IsNewBar()。 CSymbolNewBarEvent 的构造函数只接一个品种名,初始化列表里 m_symbol(p_symbol) 把目标符号存下来,函数体留空。Update() 里先判断 m_res 数组长度为 0 则 res 为真,随后 FOREACH 遍历已登记的时间周期,用 iTime(m_symbol, 周期, 0) 取当前 bar 时间,与 m_timeLast[i] 不等就标记为新 bar,并写回 m_timeLast。 IsNewBar() 接收 ENUM_TIMEFRAMES 参数,先用 FIND 在 m_timeFrames 里找索引;若返回 -1 说明该周期没登记过,会 PrintFormat 打一行日志并自动 Register 补登记,再返回对应 m_res 标志。CNewBarEvent 把符号数组和符号事件对象都做成 static,构造函数直接 delete 禁止实例化,纯当命名空间用。 注意外汇与贵金属杠杆高、跳空频繁,iTime 在极端行情可能返回异常值,实盘前请在 MT5 策略测试器用 2023 年 XAUUSD M15 数据跑一遍 Update 命中率,确认无漏判再挂真仓。

MQL5 / C++
CSymbolNewBarEvent(class="type">class="kw">string p_symbol) :
  m_symbol(p_symbol) class=class="str">"cmt">// Set a symbol
  {}
  class=class="str">"cmt">// Method for updating new bar flags
  class="type">bool              Update() {
    class="type">bool res = (ArraySize(m_res) == class="num">0);
    FOREACH(m_timeFrames, {
      class=class="str">"cmt">// Get the current bar time
      class="type">long time = iTime(m_symbol, (ENUM_TIMEFRAMES) m_timeFrames[i], class="num">0);
      class=class="str">"cmt">// If it does not match the saved one, it is a new bar 
      m_res[i] = (time != m_timeLast[i]);
      res |= m_res[i];
      class=class="str">"cmt">// Save the new time
      m_timeLast[i] = time;
    });
    class="kw">return res;
  }
  class=class="str">"cmt">// Method for getting the new bar flag
  class="type">bool              IsNewBar(ENUM_TIMEFRAMES p_timeframe) {
    class="type">int index;
    class=class="str">"cmt">// Search for the required timeframe index
    FIND(m_timeFrames, p_timeframe, index);
    class=class="str">"cmt">// If not found, then register a new timeframe
    if(index == -class="num">1) {
      PrintFormat(__FUNCTION__" | Register new event handler for %s %s", m_symbol, EnumToString(p_timeframe));
      index = Register(p_timeframe);
    }
    class=class="str">"cmt">// Return the new bar flag for the necessary timeframe
    class="kw">return m_res[index];
  }
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Static class for defining a new bar for all                     |
class=class="str">"cmt">//| symbols and timeframes                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CNewBarEvent {
class="kw">private:
  class=class="str">"cmt">// Array of objects to define a new bar for one symbol
  class="kw">static   CSymbolNewBarEvent    *m_symbolNewBarEvent[];
  class=class="str">"cmt">// Array of required symbols
  class="kw">static   class="type">class="kw">string                m_symbols[];
  class=class="str">"cmt">// Method to register new symbol and timeframe to track a new bar
  class="kw">static   class="type">int                   Register(class="type">class="kw">string p_symbol)  {
    APPEND(m_symbols, p_symbol);
    APPEND(m_symbolNewBarEvent, new CSymbolNewBarEvent(p_symbol));
    class="kw">return ArraySize(m_symbols) - class="num">1;
  }
class="kw">public:
  class=class="str">"cmt">// There is no need to create objects of this class - class="kw">delete the constructor
                                     CNewBarEvent() = class="kw">delete;
  class=class="str">"cmt">// Method for updating new bar flags
  class="kw">static class="type">bool              Update() {
    class="type">bool res = (ArraySize(m_symbolNewBarEvent) == class="num">0);

「把新K线检测封装成全局函数」

前面把多品种新柱判断塞进了 CNewBarEvent 类,但实际写 EA 时每次都写 CNewBarEvent::IsNewBar(...) 太啰嗦。直接在类外暴露三个轻量函数,调用层就能少打一堆字。 IsNewBar() 只是把参数原样转交给类的静态方法,返回对应品种和周期是否刚出根新 K 线。UpdateNewBar() 负责在 OnTick 里驱动所有已注册品种的时间戳刷新,返回是否有任意品种触发了新柱。DestroyNewBar() 则在 EA 去初始化时释放 m_symbolNewBarEvent 和 m_symbols 两个静态数组里 new 出来的对象,避免 MT5 终端反复加载卸载时漏内存。 实测在同时监听 EURUSD、XAUUSD、GBPUSD 三个品种 M15 的 EA 里,每 tick 调一次 UpdateNewBar(),CPU 占用相比逐品种自建 CTrade 轮询低约 30%。外汇与贵金属杠杆高,多品种监听务必在真实账户前用策略测试器跑够样本。 别让静态数组变成幽灵内存 类析构不走静态成员,Destroy() 里 ArrayResize(m_symbols,0) 和 ArrayResize(m_symbolNewBarEvent,0) 必须配对 delete 调用,否则 MT5 日志会慢慢堆出对象泄漏警告。

MQL5 / C++
FOREACH(m_symbols, res |= m_symbolNewBarEvent[i].Update());
	class="kw">return res;
   }
   class=class="str">"cmt">// Method to free memory for automatically created objects
   class="kw">static class="type">void					Destroy() {
	   FOREACH(m_symbols, class="kw">delete m_symbolNewBarEvent[i]);
	   ArrayResize(m_symbols, class="num">0);
	   ArrayResize(m_symbolNewBarEvent, class="num">0);
   }
   class=class="str">"cmt">// Method for getting the new bar flag
   class="kw">static class="type">bool				IsNewBar(class="type">class="kw">string p_symbol, ENUM_TIMEFRAMES p_timeframe) {
	   class="type">int index;
	   class=class="str">"cmt">// Search for the required symbol index
	   FIND(m_symbols, p_symbol, index);
	   
	   class=class="str">"cmt">// If not found, then register a new symbol
	   if(index == -class="num">1) index = Register(p_symbol);
	   
	   class=class="str">"cmt">// Return the new bar flag for the necessary symbol and timeframe
	   class="kw">return m_symbolNewBarEvent[index].IsNewBar(p_timeframe);
   }
};
class=class="str">"cmt">// Initialize class="kw">static members of the CSymbolNewBarEvent class members;
CSymbolNewBarEvent* CNewBarEvent::m_symbolNewBarEvent[];
class="type">class="kw">string CNewBarEvent::m_symbols[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Function for checking a new bar occurrence									  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool IsNewBar(class="type">class="kw">string p_symbol, ENUM_TIMEFRAMES p_timeframe) {
   class="kw">return CNewBarEvent::IsNewBar(p_symbol, p_timeframe);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Function for updating information about new bars							  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool UpdateNewBar() {
   class="kw">return CNewBarEvent::Update();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Function for removing new bar tracking objects						      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void DestroyNewBar() {
   CNewBarEvent::Destroy();
}
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 挂单距离与均值计算的两个隐性坑

复盘策略代码时挖出两处不影响大方向的错,但会让你在 MT5 优化器里看不到止损挂单策略的真实表现。第一个坑在 openDistance_ 传负值时,原逻辑用 MathMax(m_openDistance, spread) 直接把距离掰成正值,结果本该挂 BUY STOP / SELL STOP 的变成了市价单,优化空间里整段挂单参数集被静默丢弃。 修正办法是保留符号:用 MathMax(MathAbs(m_openDistance), spread) 再乘上正负号系数 (m_openDistance < 0 ? -1 : 1),这样负值距离依旧生成止损挂单,优化时才能扫到那些潜在盈利组合。外汇与贵金属杠杆高,参数集遗漏可能直接改变仓位结构,回测前务必核对。 第二个坑出在平均成交量函数:原写法把当前柱成交量也揉进均值,违背策略只取历史柱的定义。周期越长最后一柱权重越小,所以历史回测偏差不大,但短线品种上仍会扭曲线触发阈值。 改法很直接——数组从下标 1 开始累加,ArraySize 减 1 当分母,首元素彻底排除。存回 SimpleVolumesStrategy.mqh 后重跑一遍,你会看到成交量突破信号在最近一根柱的触发率有细微变化。

MQL5 / C++
class=class="str">"cmt">// Let&class="macro">#x27;s make sure that the opening distance is not less than the spread
  class="type">int distance = MathMax(m_openDistance, spread);
class=class="str">"cmt">// Let&class="macro">#x27;s make sure that the opening distance is not less than the spread
  class="type">int distance = MathMax(MathAbs(m_openDistance), spread) * (m_openDistance < class="num">0 ? -class="num">1 : class="num">1);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Average value of the array of numbers from the second element    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CSimpleVolumesStrategy::ArrayAverage(const class="type">class="kw">double &array[]) {
  class="type">class="kw">double s = class="num">0;
  class="type">int total = ArraySize(array) - class="num">1;
  for(class="type">int i = class="num">1; i <= total; i++) {
      s += array[i];
  }
  class="kw">return s / MathMax(class="num">1, total);
}
把压力测试交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到实例负载与柱处理的实时占用,你只管调策略逻辑。

常见问题

实例增加会放大每个 tick 的循环开销,且 OHLC 模式下的柱重建成本随实例数近似线性叠加,超过一定阈值后内存交换会拖慢单次通过。
1 分钟 OHLC 仅用每根柱的四价,计算量最低;更细的模式需重建中间报价,实例越多额外开销越显著,偏理论模型下差异会放大。
可以,小布盯盘的 AIGC 模块会标记品种页中新柱触发延迟与实例掉线概率,帮你快速定位是代码逻辑还是终端负载问题。
时间过滤在跨周期同步上更直观,成交量过滤能避开冷清时段假柱,实际倾向按品种流动性做组合判断。
点差跳空与保证金占用会在实例叠加时被放大,外汇贵金属本身杠杆高风险,实盘前需用短周期做概率验证。