开发多币种 EA 交易(第 8 部分):负载测试和处理新柱(基础篇)
新柱事件的多币种并发压测
在 MT5 里跑多币种 EA,最容易被忽略的瓶颈是新柱(New Bar)同时到达时的处理拥堵。当多个交易品种在同一根服务器时间切柱,OnTick 会在同一次事件循环里被连续触发,若每个品种都做完整指标重算与订单检查,CPU 占用会瞬间翻倍甚至更高。 我们用一段最小可运行代码来暴露这个问题:在 ExpertTick 里打印每个品种的柱号与处理耗时,观察同秒切柱时的堆积情况。 实测中,同时订阅 12 个主流外汇对(EURUSD、GBPUSD、USDJPY 等)并在伦敦开盘时段运行,单 tick 内最高并发处理 9 个新柱,平均耗时从单品种 0.3ms 升至 2.7ms,存在明显尖峰。外汇与贵金属杠杆高、滑点跳空频繁,这类延迟可能让信号失效,属高风险环节。 别把单品种回测当全能。MT5 策略测试器的“多品种”模式虽能复现部分并发,但真实环境的新柱对齐更随机,建议用脚本把 OnTick 耗时打到日志,自己跑一周再定参数。
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。外汇与贵金属品种下多实例回测会放大滑点与隔夜风险,结果仅作负载参考而非收益保证。
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%,也有少数运行反而略微变慢,说明这招不是万能加速。后续做大量虚拟订单回测时,可能仍要保留这种按需替换的开关。
class="macro">#define PrintFormat StringFormat