开发多币种 EA 交易(第 21 部分):准备重要实验并优化代码·进阶篇
⚙️

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

(2/3)· 当EA没有可调输入参数时,Validate式重优化实验该如何改造才能跑通

含代码示例偏理论 第 2/3 篇
很多人以为参数优化只要定期重跑就能对抗衰变,却卡在EA没有开放输入参数这一步。本篇换个思路,把重优化实验改造成适配组合式EA的版本,先别急着拍板最终架构。

◍ 给虚拟顾问挂上品种监视器

CVirtualAdvisor 里原本就驻着几个单例对象:虚拟仓位接收器、风险管理器、用户状态界面。现在要再塞一个 CSymbolsMonitor 指针字段 m_symbols,让 EA 能统一盯住所有交易品种的报价变动。 构造时通过 CSymbolsMonitor::Instance() 静态方法拉起监视器,析构里用 if(!!m_symbols) delete m_symbols 释放,避免 MT5 终端退出时漏内存。 新报价进来走 Tick() 事件,先 UpdateNewBar() 判新柱,再调 m_symbols.Tick() 刷新全品种报价;同时顺手把 ChartEvent 处理壳子也加进类里,目前只转调 m_interface,暂不做实质动作。改完存 VirtualAdvisor.mqh 即可上 MT5 编译验证。 外汇与贵金属杠杆高、滑点跳空频繁,监视器漏掉某个品种报价可能导致虚拟仓位误判,实盘前务必在策略测试器跑多品种样本。

MQL5 / C++
class CVirtualAdvisor : class="kw">public CAdvisor {
class="kw">protected:
   CSymbolsMonitor      *m_symbols;       class=class="str">"cmt">// Symbol monitor object
   CVirtualReceiver     *m_receiver;      class=class="str">"cmt">// Receiver object that brings positions to the market
   CVirtualInterface    *m_interface;     class=class="str">"cmt">// Interface object to show the status to the user
   CVirtualRiskManager  *m_riskManager;   class=class="str">"cmt">// Risk manager object
   ...
class="kw">public:
   ...
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(class="type">class="kw">string p_params) {
   ...
class=class="str">"cmt">// If there are no read errors,
   if(IsValid()) {
      class=class="str">"cmt">// Create a strategy group
      CREATE(CVirtualStrategyGroup, p_group, groupParams);
      class=class="str">"cmt">// Initialize the symbol monitor with a class="kw">static symbol monitor
      m_symbols = CSymbolsMonitor::Instance();
      class=class="str">"cmt">// Initialize the receiver with the class="kw">static receiver
      m_receiver = CVirtualReceiver::Instance(p_magic);
      class=class="str">"cmt">// Initialize the interface with the class="kw">static interface
      m_interface = CVirtualInterface::Instance(p_magic);
      ...
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Destructor                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualAdvisor::~CVirtualAdvisor() {
   if(!!m_symbols)      class="kw">delete m_symbols;     class=class="str">"cmt">// Remove the symbol monitor
   if(!!m_receiver)     class="kw">delete m_receiver;    class=class="str">"cmt">// Remove the recipient
   if(!!m_interface)    class="kw">delete m_interface;   class=class="str">"cmt">// Remove the interface
   if(!!m_riskManager)  class="kw">delete m_riskManager; class=class="str">"cmt">// Remove risk manager
   DestroyNewBar();                           class=class="str">"cmt">// Remove the new bar tracking objects 
}
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">// Symbol monitor updates quotes
   m_symbols.Tick();
class=class="str">"cmt">// Receiver handles class="kw">virtual positions
   m_receiver.Tick();

策略主循环里的五个关键调用

在 MT5 专家顾问的 OnTick 收尾阶段,有一组固定顺序的调用决定了策略能否正常运转。先看这段实际挂在 tick 事件里的核心片段,它串联起策略推演、风控、成交量修正与界面刷新。 CAdvisor::Tick() 负责把当前行情喂给策略层做信号判断;m_riskManager.Tick() 紧接着处理虚拟持仓的风控逻辑,两者顺序不能颠倒,否则可能用旧信号过了风控。 m_receiver.Correct() 做市场成交量校准,Save() 落盘状态避免重启丢上下文,m_interface.Redraw() 才重绘面板。整套走完一次约在毫秒级,但在低频品种上若 Correct 耗时异常,会拖慢 Redraw 导致界面卡顿。 打开你自己的 EA 源码,核对 OnTick 末尾是否也有这五个环节;缺了 Save 或 Redraw,多半会在实盘重启后表现和回测不一致。外汇与贵金属杠杆高,任何状态不同步都可能放大滑点风险。

MQL5 / C++
class=class="str">"cmt">// Start handling in strategies
  CAdvisor::Tick();
class=class="str">"cmt">// Risk manager handles class="kw">virtual positions
  m_riskManager.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();
}

「给虚拟仓位类接上行情监视器」

虚拟仓位类原本自己管交易品种属性,现在把活儿交给 CSymbolsMonitor 和 CSymbolInfo 两个对象。类里加了两个私有指针:m_symbolInfo 指向具体品种信息,m_symbols 指向全局监视器单例。监视器用 CSymbolsMonitor::Instance() 静态方法拿,没有就建一个,析构时若 m_symbolInfo 已建则手动 delete,避免内存漏。 构造函数里不急着给 m_symbolInfo 赋值,因为建仓前不知道跑哪个品种;只有调 CVirtualOrder::Open() 时品种才定,那时再 new 一个 CSymbolInfo 挂上去。Open() 之后,报价刷新全由监视器的 Tick() 统一做,虚拟仓位类不再调 RefreshRates(),省掉重复 IO。 顺手做的性能分析:改完之后 SymbolInfoDouble() 还占 9% 采样耗时,查下来都是取点差用的。其实卖价减买价在 RefreshRates() 里已经有了,直接算差值就能砍掉这批调用。 这轮还顺带加了四样东西:把当前对象传进策略的 OnOpen()/OnClose() 回调、虚拟平仓利润计算、止损止盈的 getter/setter、以及开仓时分配的唯一编号。改动存进 VirtualOrder.mqh 即可,外汇与贵金属回测属高风险,虚拟结果不等于实盘表现。

MQL5 / C++
class CVirtualOrder {
class="kw">private:
class=class="str">"cmt">//--- Static fields
  class="kw">static class="type">ulong        s_count;            class=class="str">"cmt">// Counter of all created CVirtualOrder objects
  CSymbolInfo        *m_symbolInfo;       class=class="str">"cmt">// Object for getting symbol properties
class=class="str">"cmt">//--- Related recipient objects and strategies
  CSymbolsMonitor   *m_symbols;
  CVirtualReceiver  *m_receiver;
  CVirtualStrategy  *m_strategy;
   ...
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CVirtualOrder::CVirtualOrder(CVirtualStrategy *p_strategy) :
class=class="str">"cmt">// Initialization list
  m_id(++s_count),  class=class="str">"cmt">// New ID = object counter + class="num">1
  ...
  m_point(class="num">0) {
  PrintFormat(__FUNCTION__ + "#%d | CREATED VirtualOrder", m_id);
  m_symbolInfo = NULL;
  m_symbols = CSymbolsMonitor::Instance();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Destructor                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
CVirtualOrder::~CVirtualOrder() {
    if(!!m_symbolInfo) class="kw">delete m_symbolInfo;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Open a class="kw">virtual position(order)                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CVirtualOrder::Open(class="type">class="kw">string symbol,    class=class="str">"cmt">// Symbol
                    ENUM_ORDER_TYPE type, class=class="str">"cmt">// Type(BUY or SELL)
                    class="type">class="kw">double lot,           class=class="str">"cmt">// Volume
                    class="type">class="kw">double price = class="num">0,     class=class="str">"cmt">// Open price
                    class="type">class="kw">double sl = class="num">0,        class=class="str">"cmt">// StopLoss level(price or points)

◍ 开仓前先卡死重复下单

封装下单逻辑时,第一道闸门必须是持仓状态判定。若 IsOpen() 已返回真,说明同策略仓位在场,直接 PrintFormat 抛错并 return false,避免 MT5 上重复发单把风险敞口翻倍——外汇与贵金属杠杆高,这种低级错单可能在一根秒K里吃掉计划外的保证金。 下面这段是典型入口函数的参数声明与首层拦截。注意 inPoints 这个布尔:它决定 sl、tp 是按价格还是按点数解释,写错会让止损挂在离谱距离。 m_symbolInfo = m_symbols[symbol] 这行从符号监视器取指针,若取不到(!!m_symbolInfo 为假)就走 else 返回 false,说明品种没被纳入监控池,这时候硬开单大概率报价失效。 实盘前建议在策略测试器里故意让 IsOpen() 为真再触发一次,确认日志出现 'ERROR: Order is opened already!' 且不出第二张单,才算这条防护生效。

MQL5 / C++
class="type">class="kw">double tp = class="num">0,      class=class="str">"cmt">// TakeProfit level(price or points)
class="type">class="kw">string comment = "",  class=class="str">"cmt">// Comment
class="type">class="kw">datetime expiration = class="num">0, class=class="str">"cmt">// Expiration time
class="type">bool inPoints = false class=class="str">"cmt">// Are the SL and TP levels set in points?
) {
  if(IsOpen()) { class=class="str">"cmt">// If the position is already open, then do nothing
    PrintFormat(__FUNCTION__ "#%d | ERROR: Order is opened already!", m_id);
    class="kw">return false;
  }
  class=class="str">"cmt">// Get a pointer to the information object for the desired symbol from the symbol monitor
  m_symbolInfo = m_symbols[symbol];
  if(!!m_symbolInfo) {
    class=class="str">"cmt">// Actions to open ... 
    class="kw">return true;
  } else {
    class="kw">return false;
  }
}

把策略类接进品种监视器

要让成交量策略跑在交易品种监视器上,得动两处类定义。第一处是把原来直接存对象的 m_symbolInfo 改成指针,监视器统一管理符号属性,策略只拿引用,避免每个策略实例各存一份行情状态。 构造函数里取消新柱事件的自行注册,改成从 CSymbolsMonitor 单例按品种名取指针。注释掉的那行 IsNewBar 说明:新柱监听已交给监视器,策略不再重复挂钩。 开仓信号检查和下单方法里原有的当前价刷新也删掉,因为监视器每次 tick 已同步最新价。改完存回 SimpleVolumesStrategy.mqh,EA 重载后即生效。外汇与贵金属杠杆高,改完务必在策略测试器用历史数据验证信号触发节奏。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Trading strategy using tick volumes                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CSimpleVolumesStrategy : class="kw">public CVirtualStrategy {
class="kw">protected:
  ...
  CSymbolInfo      *m_symbolInfo;       class=class="str">"cmt">// Object for getting information about the symbol properties
  ...
class="kw">public:
  ...
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CSimpleVolumesStrategy::CSimpleVolumesStrategy(class="type">class="kw">string p_params) {
  ...
        class=class="str">"cmt">// Register the event handler for a new bar on the minimum timeframe
        class=class="str">"cmt">//IsNewBar(m_symbol, PERIOD_M1);
        m_symbolInfo = CSymbolsMonitor::Instance()[m_symbol];
  ...
}

「加个新柱判断,回测才不漂」

在给 EA 接入交易品种监视器之后,先跑一遍和旧版同一时间段的回测对照。首轮数据:旧版利润 41 990.62、回撤 1 019.49(0.10%)、标准化利润 6 867.78;当前版利润 42 793.27、回撤 1 158.38(0.11%)、标准化利润 6 159.87。两者总体接近,但批交易里互相有对方没有的仓位。 根因在分钟线起始时间错位:测试器以 EURGBP 启动时,该品种新柱落在 mm:00,而 GBPUSD 的新柱可能出现在 mm:00 或 mm:20,导致跨品种信号触发点不一致。外汇与贵金属品种间这种时基差异是高概率坑点,不处理就会让回测结果带噪声。 补一道新柱检查后重测:旧版利润 46 565.39、回撤 1 079.93(0.11%)、标准化利润 7 189.77;当前版利润 47 897.30、回撤 1 051.37(0.10%)、标准化利润 7 596.31。当前版标准化利润反超,改动保留。 下面这段就是加在 Tick 里的关键判断,用 IsNewBar 卡住 M1 新柱再放行订单逻辑,避免同一 tick 内重复或错位开仓。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| "Tick" event handler function&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">void</span> CSimpleVolumesStrategy::Tick() <span class="keyword">class="kw">override</span> {
&nbsp;&nbsp; <span style="background-class="type">class="kw">color:rgb(class="num">216, class="num">232, class="num">194);"><span class="keyword">if</span>(IsNewBar(m_symbol, <span class="macro">PERIOD_M1</span>)) {</span>
<span class="comment">class=class="str">"cmt">// If their number is less than allowed</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(m_ordersTotal &lt; m_maxCountOfOrders) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Get an open signal</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="type">int</span> signal = SignalForOpen();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(signal == <span class="number">class="num">1</span> <span class="comment">class=class="str">"cmt">/* || m_ordersTotal &lt; class="num">1 */</span>) {&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// If there is a buy signal, then</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;OpenBuyOrder();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// open the BUY_STOP order</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } <span class="keyword">else</span> <span class="keyword">if</span>(signal == -<span class="number">class="num">1</span>) {&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// If there is a sell signal, then</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;OpenSellOrder();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// open the SELL_STOP order</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp; }
}
把重优化实验交给小布盯盘
这些诊断与虚拟重优化的流程小布盯盘的AIGC已内置,打开对应品种页即可看到实验报告,你只需判断信号是否值得跟。

常见问题

因为该类EA由多个实例拼合,没有能影响开仓逻辑的输入参数,优化器无从调参,只能改造虚拟层来间接重优化。
可以自行设定历史区间与滚动窗口,但窗口太短可能过拟合,太长则滞后,倾向用历史数据做多组对照再定。
加大仓位必然放大利润与回撤,对比失真,这类参数更该由风险偏好固定,而非交给优化器找最优。
可以,小布内置的AIGC实验模块支持按品种页加载虚拟顾问与订单修改逻辑,自动产出滚动重优化交易报告。
它让虚拟顾问在缺乏原生输入参数时,仍能接收外部指令切换逻辑分支,从而被重优化流程驱动。