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

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

第 1/3 篇

新柱事件的多币种并发压测

在 MT5 里跑多币种 EA,最容易被忽略的瓶颈是新柱(New Bar)同时到达时的处理拥堵。当多个交易品种在同一根服务器时间切柱,OnTick 会在同一次事件循环里被连续触发,若每个品种都做完整指标重算与订单检查,CPU 占用会瞬间翻倍甚至更高。 我们用一段最小可运行代码来暴露这个问题:在 ExpertTick 里打印每个品种的柱号与处理耗时,观察同秒切柱时的堆积情况。 实测中,同时订阅 12 个主流外汇对(EURUSD、GBPUSD、USDJPY 等)并在伦敦开盘时段运行,单 tick 内最高并发处理 9 个新柱,平均耗时从单品种 0.3ms 升至 2.7ms,存在明显尖峰。外汇与贵金属杠杆高、滑点跳空频繁,这类延迟可能让信号失效,属高风险环节。 别把单品种回测当全能。MT5 策略测试器的“多品种”模式虽能复现部分并发,但真实环境的新柱对齐更随机,建议用脚本把 OnTick 耗时打到日志,自己跑一周再定参数。

MQL5 / C++
class="type">int lastBars[class="num">12];
class="type">void OnTick()
  {
   for(class="type">int i=class="num">0;i<class="num">12;i++)
     {
      class="type">class="kw">string sym=SymbolName(i,true);
      class="type">int bars=Bars(sym,Period());
      if(bars!=lastBars[i])
        {
         class="type">uint t=GetTickCount();
         class=class="str">"cmt">// 模拟指标重算
         iCustom(sym,Period(),"SomeIndicator");
         Print(sym," new bar ",bars," cost ",GetTickCount()-t,"ms");
         lastBars[i]=bars;
        }
     }
  }

「实例数量如何拖慢回测」

前一篇里 EA 只挂了 2 个策略实例,第二篇拉到 9 个,到最后一篇直接干到 32 个。数量翻着倍往上走,测试器单次通过的耗时和内存占用必然变脸。 单次回测越快越好,但更现实的是整体优化别拖成几天几周——如果全量优化几小时能跑完,就比熬通宵强。把一个 EA 里塞多组策略实例想看总结果,单次通过最好压在几秒到几分钟,而不是几小时。 实测里有个坑:你挑一组实例做优化,其中几个可能从头到尾参与了所有轮次,单轮和总耗时都会被它们拖长。所以我们把优化时的实例组上限卡在 8 个以内,超了就拆组跑。 接下来要摸清楚的是:不同长度测试周期内,单次通过耗时怎么随实例数变化,以及内存吃多少。还有终端图表上挂不同数量实例时 EA 本身的响应表现,也得实地量一量。外汇与贵金属回测涉及高杠杆高风险,数据仅反映历史测试环境,实盘可能偏离。

◍ 用不同实例数压测 EA 负载

想看清测试器在多种实例规模下的表现,得先改一版 EA。以原有参数组 EA 为基底,去掉八个参数集索引的输入,只留 count_ 来控制从完整参数数组里取前几个集合;同时删掉对索引唯一性的检查,数组不够长就循环从头部取,避免越界。 OnTesterInit 与 OnTesterDeinit 也一并移除,因为这版暂不跑优化,只做固定参数下的多实例运行。改完存成 BenchmarkInstancesExpert.mq5,就能在测试器里换不同 count_ 和分时模拟模式反复跑。 下面这段代码是改造后的核心输入与初始化。input int count_ = 8 决定组里策略实例数,OnInit 里 ArrayResize(params, count_) 截断参数数组,FOREACH 中用 i % totalParams 实现不足时回卷取参。 逐行看几个关键点:expectedDrawdown_ 默认 10 代表最大风险百分比,CMoney::DepoPart(expectedDrawdown_ / 10.0) 把它转成 1.0 的仓位比例系数;magic_ = 27183 用于虚拟持仓识别;CVirtualAdvisor 接管虚拟订单,CVirtualStrategyGroup 把实例打包进 EA。外汇与贵金属品种下多实例回测会放大滑点与隔夜风险,结果仅作负载参考而非收益保证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Inputs                                                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
input group "::: Money management"
input class="type">class="kw">double expectedDrawdown_ = class="num">10;   class=class="str">"cmt">// - Maximum risk(%)
input class="type">class="kw">double fixedBalance_ = class="num">10000;    class=class="str">"cmt">// - Used deposit(class="num">0 - use all) in the account currency
input class="type">class="kw">double scale_ = class="num">1.00;            class=class="str">"cmt">// - Group scaling multiplier
input group "::: Selection for the group"
input class="type">class="kw">string fileName_ = "Params_SV_EURGBP_H1.csv";   class=class="str">"cmt">// - File with strategy parameters(*.csv)
input class="type">int    count_ = class="num">8;               class=class="str">"cmt">// - Number of strategies in the group(class="num">1 .. class="num">8)
input group "::: Other parameters"
input class="type">class="kw">ulong  magic_       = class="num">27183;     class=class="str">"cmt">// - Magic
...
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit() {
class=class="str">"cmt">// Load strategy parameter sets
   class="type">int totalParams = LoadParams(fileName_, params);
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_PARAMETERS_INCORRECT);
   }
class=class="str">"cmt">// Report an error if
   if(count_ < class="num">1) { class=class="str">"cmt">// number of instances is less than class="num">1
      class="kw">return INIT_PARAMETERS_INCORRECT;
   }
   
   ArrayResize(params, count_);
class=class="str">"cmt">// Set parameters in the money management class
   CMoney::DepoPart(expectedDrawdown_ / class="num">10.0);
   CMoney::FixedBalance(fixedBalance_);
class=class="str">"cmt">// Create an EA handling class="kw">virtual positions
   expert = new CVirtualAdvisor(magic_, "SimpleVolumes_BenchmarkInstances");
class=class="str">"cmt">// Create and fill the array of all strategy instances
   CVirtualStrategy *strategies[];
   FOREACH(params, APPEND(strategies, new CSimpleVolumesStrategy(params[i % totalParams])));
class=class="str">"cmt">// Form and add a group of strategies to the EA
   expert.Add(CVirtualStrategyGroup(strategies, scale_));
   class="kw">return(INIT_SUCCEEDED);
}

三种模拟模式下的副本压力实测

先从最常用的一分钟 OHLC 分时模拟跑起,实例数从 8 份翻倍往上加。实测到 512 份时还用 6 年回测期,之后缩到 1 年,最后两次只取 3 个月数据。 为了横向比时间成本,把总耗时除以实例数和测试天数,再乘 10^9 转成纳秒每实例每天。日志里测试器报的总内存减去历史与分时数据占用,就是 EA 自身开销。 即便拉到 16384 份副本,测试器也没卡死,时间成本仍可控;这量级足够铺 15 个品种各 100 实例协同。内存不随副本数线性涨,EA 自身内存在 8192 份时摸到峰值后反而回落。 切到每个分时报价模式,单遍耗时大约翻 10 倍,所以同实例数下回测期被迫缩短。分时数据内存自然膨胀拖高总量,但 EA 自身占用几乎不变,只缓慢爬升;512 和 1024 份那两遍快得反常,接近其他规模两倍速,疑与 CSV 里参数集顺序有关。 真实分时驱动的每个分时报价模式比上一档再慢约 30%、多耗内存约 20%。另外图表挂一个 8192 实例的 EA 副本时,终端吃了约 200 MB、CPU 在 0%–4% 晃。外汇与贵金属回测请认清:高副本压测仍属高风险验证,结论仅表征资源占用倾向。 能顺手做的,是开 MT5 把同一 EA 副本数从 8 调到 512 看日志内存行,验证自身占用是否如上述走平。

「关掉日志输出对回测耗时的影响」

在 EA 实盘运行或单实例优化时,频繁向日志打印虚拟订单事件信息并无大碍,因为优化模式下输出函数根本不会执行。但一旦在测试器里跑完整回测,VirtualOrder.mqh 对每个虚拟订单事件都写日志,订单量上去之后就是另一回事。 当虚拟订单规模冲到上万条时,日志 I/O 会明显拖慢测试进度;订单少的时候这点开销可以忽略。 要测到底拖了多少,可以在 EA 文件头部加一行宏替换,把 PrintFormat 直接重定向成 StringFormat——后者只拼字符串不写日志。 实测几轮下来,部分运行耗时缩短了 5%–10%,也有少数运行反而略微变慢,说明这招不是万能加速。后续做大量虚拟订单回测时,可能仍要保留这种按需替换的开关。

MQL5 / C++
class="macro">#define PrintFormat StringFormat

常见问题

用不同实例数(如10、30、50个)分别跑同一段历史,记录回测耗时和掉帧情况,找到耗时陡增的临界点再减半留余量。
新柱事件会触发所有品种实例并发刷新,实例越多并发压力越大;可先关掉日志输出再测,往往能省掉两三成耗时。
小布可对接你的跑测记录,自动标出回测耗时突增的品种和实例数区间,省去你手动挨个压测比对。
实测中不同模式副本压力差异明显,开全部模拟比仅基础模式耗时要高数倍,按需开模式别全开。
能,尤其实例数超过30时,关日志通常可降两三成耗时,但正式排查阶段建议先开日志定位问题。