开发多币种 EA 交易(第 8 部分):负载测试和处理新柱·综合运用
⚙️

开发多币种 EA 交易(第 8 部分):负载测试和处理新柱·综合运用

(3/3)·从8到512个策略实例的压测曲线,看清内存与单次通过时间的真实拐点

含代码示例 第 3/3 篇
很多人以为EA实例数翻一倍,测试时间也线性翻倍,实际在512实例附近内存占用会先一步失控。把全部策略塞进一个EA做优化,往往等来的不是结果而是终端卡死。先测边界,再谈组合。

◍ 用新柱事件卡住无效分时

把策略里的关键动作包进 IsNewBar() 判断,就能让它们只在对应品种和周期出现新柱时触发。比如 H1 新柱到了才跑必要逻辑,D1 新柱到了才重算一次 ATR——省掉每笔报价都重算均幅的浪费。 在 EA 的 OnTick 里也可以直接拦截:给 CVirtualAdvisor 加一个 m_useOnlyNewBar 属性,默认 false。置 true 后,只要没有任何跟踪品种/周期出新柱,Tick() 提前 return,策略实例根本拿不到分时。 策略类侧因此能瘦身:构造函数里用 m_symbol 和 PERIOD_M1 调一次 IsNewBar() 做订阅,Tick() 内不必再手写分钟柱检查。下面这段代码把「非 M1 新柱就退出」写死在策略 Tick 开头,外汇与贵金属杠杆品种波动剧烈、滑点风险高,只在新柱运算可明显降低无效下单概率。 改完记得存到 SimpleVolumesStrategy.mqh,MT5 里重新编译 EA 即可验证行为差异。

MQL5 / C++
class=class="str">"cmt">// If a new bar arrived on H1 for the current strategy symbol, then
if(IsNewBar(m_symbol, PERIOD_H1)) {
class=class="str">"cmt">// perform the necessary actions
  ...
}
class=class="str">"cmt">// If a new bar arrived on D1 for the current strategy symbol, then
if(IsNewBar(m_symbol, PERIOD_H1)) {
  CalcATR(); class=class="str">"cmt">// call our ATR calculation function
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| "Tick" event handler function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CSimpleVolumesStrategy::Tick() class="kw">override {
class=class="str">"cmt">// If there is no new bar on M1, 
  if(!IsNewBar(m_symbol, PERIOD_M1)) class="kw">return;
class=class="str">"cmt">// If their number is less than allowed
  if(m_ordersTotal < m_maxCountOfOrders) {
    class=class="str">"cmt">// Get an open signal
    class="type">int signal = SignalForOpen();
    if(signal == class="num">1 class=class="str">"cmt">/* || m_ordersTotal < class="num">1 */) {           class=class="str">"cmt">// If there is a buy signal, then
      OpenBuyOrder();       class=class="str">"cmt">// open the BUY_STOP order
    } else if(signal == -class="num">1) { class=class="str">"cmt">// If there is a sell signal, then
      OpenSellOrder();      class=class="str">"cmt">// open the SELL_STOP order
    }
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| OnTick event handler                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualAdvisor::Tick(class="type">void) {
class=class="str">"cmt">// Define a new bar for all required symbols and timeframes
  class="type">bool isNewBar = UpdateNewBar();
class=class="str">"cmt">// If there is no new bar anywhere, and we only work on new bars, then exit
  if(!isNewBar && m_useOnlyNewBar) {
    class="kw">return;
  }
class=class="str">"cmt">// Receiver handles class="kw">virtual positions
  m_receiver.Tick();
class=class="str">"cmt">// Start handling in strategies
  CAdvisor::Tick();
class=class="str">"cmt">// Adjusting market volumes
  m_receiver.Correct();
class=class="str">"cmt">// Save status
  Save();
class=class="str">"cmt">// Render the interface
  m_interface.Redraw();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class of the EA handling class="kw">virtual positions(orders)              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CVirtualAdvisor : class="kw">public CAdvisor {
class="kw">protected:
  ...
  class="type">bool                m_useOnlyNewBar;   class=class="str">"cmt">// Handle only new bar ticks
class="kw">public:
                     CVirtualAdvisor(class="type">class="kw">ulong p_magic = class="num">1, class="type">class="kw">string p_name = "",
class="type">bool p_useOnlyNewBar = class="kw">false); class=class="str">"cmt">// Constructor
  ...
};

虚拟顾问与量能策略的初始化咬合

把策略挂上虚拟交易框架,第一步是构造 CVirtualAdvisor 对象。它用 magic 号绑定静态接收器与静态接口,默认 p_useOnlyNewBar 为 false 意味着每一 tick 都可能触发检查,而不是只在新 K 线出现后才动作;回测环境下文件名会自动追加 .test 后缀,实盘则是普通 csv,方便你 afterwards 区分数据源。 CSimpleVolumesStrategy 的初始化列表里干了三件实打实的事:用 iVolumes 抓 tick 成交量、把 m_volumes 数组尺寸锁成 m_signalPeriod 并设为时间序列寻址、调用 IsNewBar(m_symbol, PERIOD_M1) 注册一分钟新柱事件。这三步少一个,后续 SignalForOpen 拿到的量能窗口就会错位。

Tick() 里的开仓闸门很直白:m_ordersTotal 小于 m_maxCountOfOrders 才放信号,signal==1 走 OpenBuyOrder,signal==-1 走卖侧逻辑。被注释掉的 `m_ordersTotal < 1` 说明作者曾考虑过无持仓强制首单,现在默认关掉,你若想测“首单必发”可把注释揭开看成交分布变化。外汇与贵金属杠杆高,这类虚拟回测结论只代表历史样本概率,实盘可能显著偏离。
MQL5 / C++
CVirtualAdvisor::CVirtualAdvisor(class="type">class="kw">ulong p_magic = class="num">1,
                                 class="type">class="kw">string p_name = "",
                                 class="type">bool p_useOnlyNewBar = class="kw">false) :
  m_receiver(CVirtualReceiver::Instance(p_magic)),
  m_interface(CVirtualInterface::Instance(p_magic)),
  m_lastSaveTime(class="num">0),
  m_useOnlyNewBar(p_useOnlyNewBar) {
  m_name = StringFormat("%s-%d%s.csv",
                        (p_name != "" ? p_name : "Expert"),
                        p_magic,
                        (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                        );
};

CSimpleVolumesStrategy::CSimpleVolumesStrategy(
  ...) :
  ... {
  CVirtualReceiver::Get(GetPointer(this), m_orders, m_maxCountOfOrders);
  m_iVolumesHandle = iVolumes(m_symbol, m_timeframe, VOLUME_TICK);
  ArrayResize(m_volumes, m_signalPeriod);
  ArraySetAsSeries(m_volumes, true);
  IsNewBar(m_symbol, PERIOD_M1);
}

class="type">void CSimpleVolumesStrategy::Tick() class="kw">override {
  if(m_ordersTotal < m_maxCountOfOrders) {
    class="type">int signal = SignalForOpen();
    if(signal == class="num">1 class=class="str">"cmt">/* || m_ordersTotal < class="num">1 */) {
      OpenBuyOrder();
    } else if(signal == -class="num">1) {

「挂单函数里的卖单触发细节」

上面这段 MQL5 片段只做了一件事:在条件满足时调用 OpenSellOrder() 并附上注释 // open the SELL_STOP order,说明此处意图是挂一个 SELL_STOP 卖单。 外层还有未展示的 if 判断与大括号嵌套,意味着该挂单动作被包在某种信号过滤逻辑里,不会每根 K 线都发单。 在 MT5 里验证时,重点看 OpenSellOrder() 内部是否真的用了 ORDER_TYPE_SELL_STOP,以及挂单价是否低于当前 Bid,否则注释与实单类型不符。 外汇与贵金属杠杆高,SELL_STOP 触发后若遇跳空可能滑点放大,下单前应在策略测试器用真实点差回测确认。

MQL5 / C++
 OpenSellOrder();       class=class="str">"cmt">// open the SELL_STOP order
   }
  }
}

◍ 只跑新柱开盘的报价反而更赚

给 BenchmarkInstancesExpert.mq5 加了个输入参数 useOnlyNewBars_,用来控制 EA 是否处理那些不在新柱形开始时出现的分时报价。初始化时把这个值直接丢进 EA 的构造函数,逻辑上等于告诉虚拟持仓管理器:要么每个分时都干活,要么只在柱头动一次。 用 256 个策略实例在小周期上跑「基于真实分时的每个分时报价」模式做了对比。useOnlyNewBars_ = false 时,EA 每个分时都跑,利润 296 美元,耗时 04:15;改成 true、只认新柱开盘那一个分时后,利润 434 美元,00:25 就跑完。计算开销压到约十分之一,利润还略高一点。 别把这次结果当通解。换一套策略,跳过分时能不能保利润完全未知,每种逻辑都得单独拉回测看。外汇和贵金属杠杆高,这类加速手段只解决算力浪费,不替你扛回撤风险。 代码里参数默认就设成 true,想复现第一种慢模式得手动改回 false 再编译。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Inputs                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
...
input group "::: Other parameters"
sinput class="type">class="kw">ulong  magic_        = class="num">27183;   class=class="str">"cmt">// - Magic
input class="type">bool    useOnlyNewBars_ = true;     class=class="str">"cmt">// - Work only at bar opening
...
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit() {
  ...
class=class="str">"cmt">// Create an EA handling class="kw">virtual positions
  expert = new CVirtualAdvisor(magic_, "SimpleVolumes_BenchmarkInstances", useOnlyNewBars_);
  ...
}

把实例初始化拉回正轨

这轮压测把 EA 在大量策略实例并发下的表现摸清楚了:多品种、多周期、多逻辑可以塞进同一个 EA 跑,类库顺手补了新柱事件跟踪。该功能在当前 Volume 策略里用不上,但留着能接别的策略;更实际的是,把 EA 计算锁在柱开盘瞬间,能压低开销,也让不同分时模拟模式的回测更接近。 附带五个文件可直接下:BenchmarkInstancesExpert.mq5(13.23 KB)、NewBarEvent.mqh(11.52 KB)、SimpleVolumesStrategy.mqh(33.62 KB)、VirtualAdvisor.mqh(14.64 KB)、VirtualOrder.mqh(39.52 KB)。外汇与贵金属杠杆高、滑点跳空频繁,并发 EA 回测漂亮不等于实盘稳,上 MT5 前先单品种裸跑一遍。 项目又滑出了原定路线,不过偏离本身也在给终局铺路。接下来该用字符串常量重新初始化策略实例,并搭一套优化结果存储,把自动测试的主线接回来。

把压测监控交给小布
这些实例数与内存占用的诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时负载参考,你只需专注参数分组决策。

常见问题

实测中实例数上升到一定规模后,内存消耗增速超过线性,分时报价模拟模式下的数据缓存成为瓶颈,导致单日模拟耗时跳升,建议分组不超过8实例做优化。
当多个品种新柱几乎同时到达,若只在主图OnTick里判断新柱,子符号可能错过首根K线信号,需要按符号分别维护上次柱时间。
可以,小布盯盘对应品种页已内置AIGC诊断,会给出实例规模下的内存与耗时倾向,省去手动跑BenchmarkExpert的重复劳动。
会损失全局联合优化视角,但换来几小时而非几周的耗时,实务中可先分组筛优再对候选组做小规模联合验证。
文中基准显示低精度模式单实例日耗时远低于逐笔模式,32实例下差距放大数倍,非必要不启逐笔模拟。