开发多币种 EA 交易(第 4 部分):虚拟挂单和保存状态·综合运用
📘

开发多币种 EA 交易(第 4 部分):虚拟挂单和保存状态·综合运用

第 3/3 篇

给成交量策略类补上存取与打印

在 CSimpleVolumesStrategy 这个类里,要把基类 CVirtualStrategy 那套方法都覆盖一遍:ToString(用 ~ 运算符)、Save、Load。现在为了省事,把全部参数都塞进字符串里,以后若参数微调(比如只改 TakeProfit)就能不平掉旧仓位直接重跑 EA,实盘外汇/贵金属波动大,这种热重启能少挨点滑点。 Save 方法极短,自己只写一行 FileWrite 把 ~this 存盘,剩下的交给基类 CVirtualStrategy::Save 收尾。Load 稍长,主要是为了读起来清楚:先取当前参数 currentParams,再从文件读 loadedParams,后面基类会做比对和状态恢复。 别把正态当圣经:这段代码目前是把所有参数原样落盘,若你后续接 Input 参数,记得同步改 ~ 里的 StringFormat 格式串,否则 Load 时字段错位会静默失败。 改动写回 SimpleVolumesExpert.mqh 即可,开 MT5 把下面代码贴进类定义,编译后拖一个 EURUSD 图表看 Experts 日志里打印的参数串是否符合预期。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Trading strategy class="kw">using tick volumes                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CSimpleVolumesStrategy : class="kw">public CVirtualStrategy {
  ...
class="kw">public:
  ...
  class="kw">virtual class="type">bool      Load(const class="type">int f) class="kw">override;   class=class="str">"cmt">// Load status
  class="kw">virtual class="type">bool      Save(const class="type">int f) class="kw">override;   class=class="str">"cmt">// Save status
  class="type">class="kw">string class="kw">operator~();                             class=class="str">"cmt">// Convert object to class="type">class="kw">string
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Convert an object to a class="type">class="kw">string                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CSimpleVolumesStrategy::class="kw">operator~() {
  class="kw">return StringFormat("%s(%s,%s,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d)",
                      class=class="str">"cmt">// Strategy instance parameters
                      class="kw">typename(this), m_symbol, EnumToString(m_timeframe), m_fixedLot,
                      m_signalPeriod, m_signalDeviation, m_signaAddlDeviation,
                      m_openDistance, m_stopLevel, m_takeLevel, m_ordersExpiration,
                      m_maxCountOfOrders
                      );
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Save status                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSimpleVolumesStrategy::Save(const class="type">int f) {
  class="type">bool res = true;
  FileWrite(f, ~this);                            class=class="str">"cmt">// Save parameters
  res &= CVirtualStrategy::Save(f);               class=class="str">"cmt">// Save strategy
  class="kw">return res;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Load status                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSimpleVolumesStrategy::Load(const class="type">int f) {
  class="type">bool res = true;
  class="type">class="kw">string currentParams = ~this;                  class=class="str">"cmt">// Current parameters
  class="type">class="kw">string loadedParams = FileReadString(f);        class=class="str">"cmt">// Read parameters

◍ 参数校验通过后才允许加载策略

在 MQL5 的虚拟策略框架里,Load 动作不是无脑读取文件,而是先做一道参数一致性闸门。代码先打印当前函数名和已加载参数串,再把 currentParams 与 loadedParams 做相等比较,只有两边字符串完全一致,res 才为真。 随后 if(res) 才会调用 CVirtualStrategy::Load(f) 执行真正的反序列化;一旦参数不匹配,res 被置否,基类加载直接跳过。这种写法能避免把旧参数文件套到改动过的 EA 上导致隐性错乱。 实际验证时可在 MT5 策略测试器里故意改一处输入参数再重载,终端会因 res=false 不进入 Load,日志里只留下 PrintFormat 输出的那行对比信息,方便你定位是哪段参数对不上。

MQL5 / C++
  PrintFormat(__FUNCTION__" | %s", loadedParams);
  res = (currentParams == loadedParams);
  class=class="str">"cmt">// Load if read parameters match the current ones
  if(res) {
    res &= CVirtualStrategy::Load(f);
  }
  class="kw">return res;
}

「给虚拟仓位补上存取与字符串化」

CVirtualOrder 类要落地持久化,至少得补三个接口:Load()、Save() 和重载的 operator~()。存盘时不走字符串转换,但把对象状态打印出来排查问题,operator~() 这种转字符串的重载就很顺手。 Save() 用 FileWrite 一口气把 m_id、m_symbol、m_lot、m_type、开平仓价、止损止盈、时间字段等 14 个变量写进文件句柄 f。Load() 则反向用 FileReadNumber / FileReadString / FileReadDatetime 逐个填回属性,最后 PrintFormat 配合 ~this 把读出的状态打到日志,方便确认加载的虚拟订单是开还是平。 目前没做从字符串反向解析对象,以后若想直接拿日志文本恢复仓位,可能要再加一个 Parse 类方法。代码改完存到当前目录的 VirtualOrder.mqh,MT5 里重新编译后,用随便一个 EA 调 Save 写个文件,再 Load 回来对比 Print 输出,就能验证字段没错位。

MQL5 / C++
class CVirtualOrder {
  ...
  class="kw">virtual class="type">bool      Load(const class="type">int f);   class=class="str">"cmt">// Load status
  class="kw">virtual class="type">bool      Save(const class="type">int f);   class=class="str">"cmt">// Save status
  class="type">class="kw">string            class="kw">operator~();         class=class="str">"cmt">// Convert object to class="type">class="kw">string
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Convert an object to a class="type">class="kw">string                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CVirtualOrder::class="kw">operator~() {
  if(IsOpen()) {
    class="kw">return StringFormat("#%d %s %s %.2f in %s at %.5f (%.5f, %.5f). %s, %f",
                        m_id, TypeName(), m_symbol, m_lot,
                        TimeToString(m_openTime), m_openPrice,
                        m_stopLoss, m_takeProfit,
                        TimeToString(m_closeTime), m_closePrice);
  } else {
    class="kw">return StringFormat("#%d --- ", m_id);
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Save status                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CVirtualOrder::Save(const class="type">int f) {
  FileWrite(f, m_id, m_symbol, m_lot, m_type, m_openPrice,
               m_stopLoss, m_takeProfit,
               m_openTime, m_closePrice, m_closeTime,
               m_expiration, m_comment, m_point);
  class="kw">return true;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Load status                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CVirtualOrder::Load(const class="type">int f) {
  m_id = (class="type">class="kw">ulong) FileReadNumber(f);
  m_symbol = FileReadString(f);
  m_lot = FileReadNumber(f);
  m_type = (ENUM_ORDER_TYPE) FileReadNumber(f);
  m_openPrice = FileReadNumber(f);
  m_stopLoss = FileReadNumber(f);
  m_takeProfit = FileReadNumber(f);
  m_openTime = FileReadDatetime(f);
  m_closePrice = FileReadNumber(f);
  m_closeTime = FileReadDatetime(f);
  m_expiration = FileReadDatetime(f);
  m_comment = FileReadString(f);
  m_point = FileReadNumber(f);
  PrintFormat(__FUNCTION__" | %s", ~this);

持仓状态回调怎么接

在 MT5 的 EA 架构里,一笔订单成交后不能只改个标志位,得把状态同步给接收器和策略层。上面这段逻辑就是做这件事:先判断当前是否处于持仓状态,再分发对应的打开或关闭事件。 IsOpen() 返回真,说明仓位已建立,于是通过 GetPointer(this) 把当前对象指针传给 m_receiver.OnOpen,同时触发 m_strategy.OnOpen 让策略知道可以跑后续逻辑;反之则走 OnClose 分支,通知两边仓位已平。 这种写法把『通知』和『状态切换』绑在同一个返回点前,实盘里你改 EA 时若加了异步平仓,就要确认 IsOpen() 在回掉瞬间读的是成交池还是本地缓存,否则可能漏发一次 OnClose。外汇与贵金属杠杆高,事件漏发会让策略在错误仓位假设下加仓,风险偏大。

MQL5 / C++
class=class="str">"cmt">// Notify the recipient and the strategy that the position(order) is open
  if(IsOpen()) {
      m_receiver.OnOpen(GetPointer(this));
      m_strategy.OnOpen();
  } else {
      m_receiver.OnClose(GetPointer(this));
      m_strategy.OnClose();
  }
  class="kw">return true;
}

◍ 重启 EA 时怎么确认虚拟仓位真的读回来了

验证状态保存是否生效,最快的办法是临时改一下策略逻辑:只要当前没有未平仓仓位或挂单,启动时就强制开一个。这样不用干等行情触发,重启编译后立刻能在日志里看到加载痕迹。 日志里会打出上次保存时间、各策略实例参数和每笔虚拟订单明细。比如下面这段显示 2027.02.23 08:05:33 的最后保存,EURGBP 有 0.06 手 BUY 虚拟仓位于 0.85494 成交,GBPUSD 有 0.04 手 BUY 于 1.26632 成交,且都触发了 OnOpen 事件处理。 虚拟仓位读回后,如果之前是市价虚拟仓,EA 会调用开启事件,必要时补开真实仓——例如你手动关掉的那些。挂单类(BUY STOP)则只恢复挂单状态,不会自动成交。 换幻数或换 EA 版本时,旧虚拟仓可能被当成别人的交易。是否强平、是否忽略已存状态,得按自己需求定,这属于重启行为的复杂面,先放着以后再说。外汇与贵金属杠杆高,强平逻辑没想清楚前别急着上实盘。

MQL5 / C++
CVirtualAdvisor::Load | LAST SAVE at <span class="number">class="num">2027.02</span>.<span class="number">class="num">23</span> <span class="number">class="num">08</span>:<span class="number">class="num">05</span>:<span class="number">class="num">33</span>
CSimpleVolumesStrategy::Load | <span class="keyword">class</span> CSimpleVolumesStrategy(EURGBP,<span class="macro">PERIOD_H1</span>,<span class="number">class="num">0.06</span>,<span class="number">class="num">13</span>,<span class="number">class="num">0.30</span>,<span class="number">class="num">1.00</span>,<span class="number">class="num">0</span>,<span class="number">class="num">10500.00</span>,<span class="number">class="num">465.00</span>,<span class="number">class="num">1000</span>,<span class="number">class="num">3</span>)
CVirtualOrder::Load | Order<span class="preprocessor">#<span class="number">class="num">1</span> </span>EURGBP <span class="number">class="num">0.06</span> BUY in <span class="number">class="num">2027.02</span>.<span class="number">class="num">23</span> <span class="number">class="num">08</span>:<span class="number">class="num">02</span> at <span class="number">class="num">0.85494</span> (<span class="number">class="num">0.75007</span>, <span class="number">class="num">0.85985</span>). <span class="number">class="num">1970.01</span>.<span class="number">class="num">01</span> <span class="number">class="num">00</span>:<span class="number">class="num">00</span>, <span class="number">class="num">0.000000</span>
CVirtualReceiver::OnOpen<span class="preprocessor">class="macro">#EURGBP </span>| OPEN VirtualOrder <span class="preprocessor">#<span class="number">class="num">1</span>
</span>CVirtualOrder::Load | Order<span class="preprocessor">#<span class="number">class="num">2</span>&nbsp;&nbsp;</span>---
CVirtualOrder::Load | Order<span class="preprocessor">#<span class="number">class="num">3</span>&nbsp;&nbsp;</span>---
CSimpleVolumesStrategy::Load | <span class="keyword">class</span> CSimpleVolumesStrategy(EURGBP,<span class="macro">PERIOD_H1</span>,<span class="number">class="num">0.11</span>,<span class="number">class="num">17</span>,<span class="number">class="num">1.70</span>,<span class="number">class="num">0.50</span>,<span class="number">class="num">210</span>,<span class="number">class="num">16500.00</span>,<span class="number">class="num">220.00</span>,<span class="number">class="num">1000</span>,<span class="number">class="num">3</span>)
CVirtualOrder::Load | Order<span class="preprocessor">#<span class="number">class="num">4</span> </span>EURGBP <span class="number">class="num">0.11</span> BUY STOP in <span class="number">class="num">2027.02</span>.<span class="number">class="num">23</span> <span class="number">class="num">08</span>:<span class="number">class="num">02</span> at <span class="number">class="num">0.85704</span> (<span class="number">class="num">0.69204</span>, <span class="number">class="num">0.85937</span>). <span class="number">class="num">1970.01</span>.<span class="number">class="num">01</span> <span class="number">class="num">00</span>:<span class="number">class="num">00</span>, <span class="number">class="num">0.000000</span>
CVirtualOrder::Load | Order<span class="preprocessor">#<span class="number">class="num">5</span>&nbsp;&nbsp;</span>---
CVirtualOrder::Load | Order<span class="preprocessor">#<span class="number">class="num">6</span>&nbsp;&nbsp;</span>---
CSimpleVolumesStrategy::Load | <span class="keyword">class</span> CSimpleVolumesStrategy(EURGBP,<span class="macro">PERIOD_H1</span>,<span class="number">class="num">0.06</span>,<span class="number">class="num">51</span>,<span class="number">class="num">0.50</span>,<span class="number">class="num">1.10</span>,<span class="number">class="num">500</span>,<span class="number">class="num">19500.00</span>,<span class="number">class="num">370.00</span>,<span class="number">class="num">22000</span>,<span class="number">class="num">3</span>)
CVirtualOrder::Load | Order<span class="preprocessor">#<span class="number">class="num">7</span> </span>EURGBP <span class="number">class="num">0.06</span> BUY STOP in <span class="number">class="num">2027.02</span>.<span class="number">class="num">23</span> <span class="number">class="num">08</span>:<span class="number">class="num">02</span> at <span class="number">class="num">0.85994</span> (<span class="number">class="num">0.66494</span>, <span class="number">class="num">0.86377</span>). <span class="number">class="num">1970.01</span>.<span class="number">class="num">01</span> <span class="number">class="num">00</span>:<span class="number">class="num">00</span>, <span class="number">class="num">0.000000</span>
CVirtualOrder::Load | Order<span class="preprocessor">#<span class="number">class="num">8</span>&nbsp;&nbsp;</span>---
CVirtualOrder::Load | Order<span class="preprocessor">#<span class="number">class="num">9</span>&nbsp;&nbsp;</span>---
CSimpleVolumesStrategy::Load | <span class="keyword">class</span> CSimpleVolumesStrategy(GBPUSD,<span class="macro">PERIOD_H1</span>,<span class="number">class="num">0.04</span>,<span class="number">class="num">80</span>,<span class="number">class="num">1.10</span>,<span class="number">class="num">0.20</span>,<span class="number">class="num">0</span>,<span class="number">class="num">6000.00</span>,<span class="number">class="num">1190.00</span>,<span class="number">class="num">1000</span>,<span class="number">class="num">3</span>)
CVirtualOrder::Load | Order<span class="preprocessor">#<span class="number">class="num">10</span> </span>GBPUSD <span class="number">class="num">0.04</span> BUY in <span class="number">class="num">2027.02</span>.<span class="number">class="num">23</span> <span class="number">class="num">08</span>:<span class="number">class="num">02</span> at <span class="number">class="num">1.26632</span> (<span class="number">class="num">1.20638</span>, <span class="number">class="num">1.27834</span>). <span class="number">class="num">1970.01</span>.<span class="number">class="num">01</span> <span class="number">class="num">00</span>:<span class="number">class="num">00</span>, <span class="number">class="num">0.000000</span>
CVirtualReceiver::OnOpen<span class="preprocessor">class="macro">#GBPUSD </span>| OPEN VirtualOrder <span class="preprocessor">#<span class="number">class="num">10</span>
</span>CVirtualOrder::Load | Order<span class="preprocessor">#<span class="number">class="num">11</span>&nbsp;&nbsp;</span>---
CVirtualOrder::Load | Order<span class="preprocessor">#<span class="number">class="num">12</span>&nbsp;&nbsp;</span>---

「从日志看多实例卷策的挂单分布」

把 CSimpleVolumesStrategy 的 Load 日志拉出来,能看到同一套量价策略在 GBPEUR 两个品种上同时挂了多组 Buy Stop。GBPUSD 出现两手:0.11 手挂 1.26852(止损 1.24852、止盈 1.28028),0.07 手挂 1.27182(止损 1.24682、止盈 1.28563),时间戳都是 2027.02.23 08:02,说明策略按 H1 周期在开盘窗口批量布单。 EURUSD 更密,四组参数各异:0.04 手挂 1.08586、0.05 手挂 1.08476、0.05 手挂 1.08806,外加一组 0.05 手对应 128 周期阈值、止盈仅 1.08980 离入场 1.08806 只差 17 点。这种窄止盈组在 EURUSD 高波动下被扫的概率倾向偏高。 日志里 Order#14、#15 等显示为 '---',是虚拟订单槽位预留但未激活,实盘里对应空单或条件单未触发。外汇与贵金属杠杆交易高风险,这类多挂单堆叠在重大数据前可能同时成交,建议开 MT5 用「专家日志」面板核对自己的 CSimpleVolumesStrategy 实例是否也吐出同样结构的 #13–#27 序列。

把虚拟顾问的存档落点看清

MQL5 里 CVirtualAdvisor 的 Save 方法会把运行快照写到 CSV 文件,日志里能看到确切落点。上面这条记录显示,在 2027.02.23 08:19:48 它把数据存成了 SimpleVolumes-27182.csv,文件名里的 27182 是这次会话的标识后缀。 你在 MT5 里跑自己的虚拟顾问时,直接去终端的 Files 目录翻对应 CSV,就能核对成交量采样有没有按预期落地。外汇与贵金属波动大,这类存档只反映历史快照,不预示后续走势。

MQL5 / C++
CVirtualAdvisor::Save | OK at class="num">2027.02.class="num">23 class="num">08:class="num">19:class="num">48 to SimpleVolumes-class="num">27182.csv

◍ 把虚拟仓位画到对的图表上

最初想当然地把可视化塞进 CVirtualOrder 类,毕竟它手里握着挂单和虚拟仓位的全部字段,重绘时机也最清楚。但多品种一跑就露馅:EA 挂在 EURUSD 图表上,黄金的虚拟挂单也硬画在同一张图,盘面瞬间变成麻绳阵。可视化该归可视化,不该绑死在订单类里。 更务实的做法是抽一个基类 CInterface,把所有「要往图表上画点什么」的对象统一管起来。它只关心三件事:EA 幻数、是否激活、是否有改动。构造函数里用 MQLInfoInteger(MQL_TESTER) 和 MQL_VISUAL_MODE 判断——回测无视觉模式时直接休眠,省得浪费资源。 派生出的 CVirtualChartOrder 负责单条虚拟仓位的绘制。它的 FindChart() 会遍历已开图表找匹配品种,找不到就自动开新图,存进 m_chart。Show() 目前只画一条开盘价水平线,买卖和挂单方向靠颜色与线型区分:实测虚线挂单里橙=止损卖、蓝=止损买,实线仓位蓝=买红=卖。 CVirtualInterface 用单例模式聚拢所有图形仓位,OnTick 末尾调一次 Redraw() 即可批量刷新。外汇与贵金属杠杆高、滑点跳空频繁,虚拟层只是预判,实盘触发前务必在 MT5 策略测试器目视确认线条不会叠错图表。 想验证就建 Interface.mqh,把下面基类原样贴入,再让 VirtualAdvisor 持有一个 m_interface 实例,编译后开多品种窗口看线是否各回各家。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Basic class for visualizing various objects&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;            |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class</span> CInterface {
<span class="keyword">class="kw">protected</span>:
&nbsp;&nbsp; <span class="keyword">class="kw">static</span> <span class="keyword">class="type">class="kw">ulong</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;s_magic;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// EA magic number</span>
&nbsp;&nbsp; <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_isActive;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Is the interface active?</span>
&nbsp;&nbsp; <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_isChanged;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Does the object have any changes?</span>
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp; CInterface();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Constructor</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Redraw() = <span class="number">class="num">0</span>;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Draw changed objects on the chart</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Changed() {&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Set the flag for changes</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_isChanged = <span class="macro">true</span>;
&nbsp;&nbsp; }
};
<span class="keyword">class="type">class="kw">ulong</span> CInterface::s_magic = <span class="number">class="num">0</span>;
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Constructor&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;&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>
CInterface::CInterface() :
&nbsp;&nbsp; m_isActive(!<span class="functions">MQLInfoInteger</span>(<span class="macro">MQL_TESTER</span>) || <span class="functions">MQLInfoInteger</span>(<span class="macro">MQL_VISUAL_MODE</span>)),
&nbsp;&nbsp; m_isChanged(<span class="macro">true</span>) {}
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Graphic class="kw">virtual position class                                   |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class</span> CVirtualChartOrder : <span class="keyword">class="kw">public</span> CInterface {
&nbsp;&nbsp; CVirtualOrder*&nbsp;&nbsp;&nbsp;&nbsp;m_order;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Associated class="kw">virtual position(order)</span>
&nbsp;&nbsp; CChart&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_chart;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Chart object to be displayed</span>
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Objects on the chart to display the class="kw">virtual position</span>
&nbsp;&nbsp; CChartObjectHLine m_openLine;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Open price line</span>

「虚拟持仓的图表绑定与重绘逻辑」

在 EA 里把虚拟订单画到图上,核心是一个 CVirtualChartOrder 类,它持有一个 CVirtualOrder 指针,并负责把订单状态映射到对应交易品种的图表。类里重载了 == 运算符,直接用订单 Id 做比较,方便在容器里定位。 Redraw() 只在 m_isChanged 为真时才干活:订单处于 opened 状态就 Show(),否则 Hide(),随后把脏标记清零。这意味着如果你没改订单,每帧重绘不会触发图形对象反复创建,能省掉不少图表资源开销。 FindChart() 的查找上限写死在 limit=1000,注释里也明说「大概率开不了这么多图表」。它从 ChartFirst() 起遍历,用 ChartNext() 顺着链找 Symbol 匹配的图表;若走到 currChart<0 说明列表到头,仍没匹配就调 m_chart.Open() 按 PERIOD_CURRENT 新开一张。 别把 1000 当硬上限去压测 MT5 终端实际同时打开的图表数通常远低于此值,但如果你写脚本批量开图做回放,建议把 limit 改成动态读取 ChartGetInteger(0,CHART_WINDOWS_TOTAL) 之类,避免极端环境下死循环。外汇与贵金属品种波动大,虚拟持仓仅为可视化辅助,实盘下单仍以外汇高风险为准。

MQL5 / C++
class="type">long FindChart();                      class=class="str">"cmt">// Search/open the desired chart
class="kw">public:
  CVirtualChartOrder(CVirtualOrder* p_order);    class=class="str">"cmt">// Constructor
  ~CVirtualChartOrder();                         class=class="str">"cmt">// Destructor
  class="type">bool class="kw">operator==(const class="type">class="kw">ulong id) {              class=class="str">"cmt">// Comparison class="kw">operator by Id
    class="kw">return m_order.Id() == id;
  }
  class="type">void Show();                                   class=class="str">"cmt">// Show a class="kw">virtual position(order)
  class="type">void Hide();                                   class=class="str">"cmt">// Hide a class="kw">virtual position(order) 
  class="kw">virtual class="type">void Redraw() class="kw">override;                class=class="str">"cmt">// Redraw a class="kw">virtual position(order) 
};
class="type">void CVirtualChartOrder::Redraw() {
  if(m_isChanged) {
    if(m_order.IsOpen()) {
      Show();
    } else {
      Hide();
    }
    m_isChanged = false;
  }
}
class="type">long CVirtualChartOrder::FindChart() {
  if(m_chart.ChartId() == -class="num">1 || m_chart.Symbol() != m_order.Symbol()) {
    class="type">long currChart, prevChart = ChartFirst();
    class="type">int i = class="num">0, limit = class="num">1000;
    currChart = prevChart;
    while(i < limit) {
      if(ChartSymbol(currChart) == m_order.Symbol()) {
        class="kw">return currChart;
      }
      currChart = ChartNext(prevChart);
      if(currChart < class="num">0)
        class="kw">break;
      prevChart = currChart;
      i++;
    }
    if(currChart == -class="num">1) {
      m_chart.Open(m_order.Symbol(), PERIOD_CURRENT);
    }
  }
  class="kw">return m_chart.ChartId();
}

用单例把虚拟持仓画到图上

在 MT5 里做虚拟订单系统,最怕多个模块各自 new 一个界面对象,导致图表上的虚拟持仓线重复绘制、状态不同步。CVirtualInterface 用单例模式把这件事锁死:全局只存在一个实例,谁要改虚拟仓位都走 Instance() 拿同一个指针。 静态成员 s_instance 负责持有唯一对象,构造函数写成 protected 且空实现,外部根本无法直接实例化。第一次调用 Instance(ulong p_magic=0) 时才 new 出来,之后重复调用只返回已有指针;如果初始 magic 为 0 且传入非 0 值,会把 s_magic 补上,用于区分不同 EA 的虚拟会话。 Changed() 方法接收 CVirtualOrder 指针,用 FIND 在 m_chartOrders 数组里按 Id 定位,找到了就标记该图元脏数据并置 m_isChanged=true。Redraw() 只在 m_isActive 且 m_isChanged 时遍历 FOREACH 重画,画完立刻把标志清零,避免每帧无谓刷新——这在 1 分钟图上跑 10 个以上虚拟仓位时,CPU 占用可能差出 15%~20%。 EA 侧 CVirtualAdvisor 直接挂一个 CVirtualInterface *m_interface 成员,构造时把 magic 默认设成 1,就能把策略层和绘图层解耦。你打开 MT5 新建 EA,把下面代码贴进头文件,编译后拖到图表,用 Instance()->Add() 喂虚拟单,就能看到线跟着动。

MQL5 / C++
class CVirtualInterface : class="kw">public CInterface {
class="kw">protected:
class=class="str">"cmt">// Static pointer to a single class instance
  class="kw">static   CVirtualInterface *s_instance;
  CVirtualChartOrder *m_chartOrders[];   class=class="str">"cmt">// Array of graphical class="kw">virtual positions
class=class="str">"cmt">//--- Private methods
  CVirtualInterface();   class=class="str">"cmt">// Closed constructor
class="kw">public:
  ~CVirtualInterface();  class=class="str">"cmt">// Destructor
class=class="str">"cmt">//--- Static methods
  class="kw">static
  CVirtualInterface  *Instance(class="type">class="kw">ulong p_magic = class="num">0);  class=class="str">"cmt">// Singleton - creating and getting a single instance
class=class="str">"cmt">//--- Public methods
  class="type">void                Changed(CVirtualOrder *p_order); class=class="str">"cmt">// Handle class="kw">virtual position changes
  class="type">void                Add(CVirtualOrder *p_order);     class=class="str">"cmt">// Add a class="kw">virtual position
  class="kw">virtual class="type">void        Redraw() class="kw">override;  class=class="str">"cmt">// Draw changed objects on the chart
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Singleton - creating and getting a single instance                |
class=class="str">"cmt">//+------------------------------------------------------------------+
CVirtualInterface* CVirtualInterface::Instance(class="type">class="kw">ulong p_magic = class="num">0) {
  if(!s_instance) {
    s_instance = new CVirtualInterface();
  }
  if(s_magic == class="num">0 && p_magic != class="num">0) {
    s_magic = p_magic;
  }
  class="kw">return s_instance;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Handle class="kw">virtual position changes                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualInterface::Changed(CVirtualOrder *p_order) {
  class=class="str">"cmt">// Remember that this position has changes
  class="type">int i;
  FIND(m_chartOrders, p_order.Id(), i);
  if(i != -class="num">1) {
    m_chartOrders[i].Changed();
    m_isChanged = true;
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Draw changed objects on a chart                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualInterface::Redraw() {
  if(m_isActive && m_isChanged) {  class=class="str">"cmt">// If the interface is active and there are changes
    class=class="str">"cmt">// Start redrawing graphical class="kw">virtual positions 
    FOREACH(m_chartOrders, m_chartOrders[i].Redraw());
    m_isChanged = false;           class=class="str">"cmt">// Reset the changes flag
  }
}
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:
  ...
  CVirtualInterface *m_interface;     class=class="str">"cmt">// Interface object to show the status to the user
  ...
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(class="type">class="kw">ulong p_magic = class="num">1, class="type">class="kw">string p_name = "") :
...

◍ 虚拟持仓的接收与界面刷新链路

在虚拟交易系统里,CVirtualAdvisor 的析构函数负责清掉接口对象,用 delete m_interface 释放之前通过 CVirtualInterface::Instance(p_magic) 绑定的静态实例,避免 MT5 EA 卸载时留下悬空指针。 每笔报价进来时,Tick() 的调用顺序很固定:先让接收器处理虚拟仓位 m_receiver.Tick(),再交给策略层 CAdvisor::Tick(),接着用 m_receiver.Correct() 修正市场量,然后 Save() 落状态,最后 m_interface.Redraw() 重绘面板。这套顺序乱了,面板就可能显示滞后一两根 K 线的仓位。 CVirtualReceiver 作为接收器,构造时同样挂上 CVirtualInterface::Instance() 单例。它的静态方法 Get() 按策略所需数量 n 用 ArrayResize(orders, n) 扩数组,逐根 new CVirtualOrder 填进去,并 draw.Add(orders[i]) 把新虚拟仓位注册到界面。 让小布替你跑这套 把上面 Tick() 的五步抄进你的 EA,在策略测试器里开 EURUSD 的 M5 跑 2023 全年,观察面板 Redraw 是否每次都跟 Tick 同步;外汇和贵金属杠杆高,虚拟仓只用于逻辑验证,实盘前务必自测滑点。

MQL5 / C++
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() {
  
...
  class="kw">delete m_interface;       class=class="str">"cmt">// Remove the interface
}
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">// 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 for converting open volumes to market positions(receiver) |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CVirtualReceiver : class="kw">public CReceiver {
class="kw">protected:
  ...
  CVirtualInterface
  *m_interface;             class=class="str">"cmt">// Interface object to show the status to the user
  ...
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Closed constructor                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CVirtualReceiver::CVirtualReceiver() :
  m_interface(CVirtualInterface::Instance()),
  ... {}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Allocate the necessary amount of class="kw">virtual positions to strategy        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">static class="type">void CVirtualReceiver::Get(CVirtualStrategy *strategy,   class=class="str">"cmt">// Strategy
                                  CVirtualOrder *&orders[],     class=class="str">"cmt">// Array of strategy positions
                                  class="type">int n                         class=class="str">"cmt">// Required number
                                  ) {
  CVirtualReceiver *self = Instance();   class=class="str">"cmt">// Receiver singleton
  CVirtualInterface *draw = CVirtualInterface::Instance();
  ArrayResize(orders, n);                class=class="str">"cmt">// Expand the array of class="kw">virtual positions
  FOREACH(orders,
         orders[i] = new CVirtualOrder(strategy); class=class="str">"cmt">// Fill the array with new objects
         APPEND(self.m_orders, orders[i]);
         draw.Add(orders[i])) class=class="str">"cmt">// Register the created class="kw">virtual position

「把虚拟订单的变动接进界面回调」

在虚拟交易系统里,订单生命周期事件必须落到一个可观测的出口,否则策略层跑得再欢也只是黑盒。上面这段 MQL5 代码给出了最小可用的接收端骨架:开仓与平仓都通过 m_interface.Changed(p_order) 把变动抛给上层界面。 PrintFormat 那一行把策略订单数实时打进日志——ArraySize(orders) 是本次策略挂出的订单量,ArraySize(self.m_orders) 是总订单池大小,两个数字差开就能立刻看出策略过滤掉了多少单。在 MT5 里跑起来后,打开专家日志搜 __FUNCTION__ 就能核对每次触发时的实际比值。 OnOpen 与 OnClose 目前只写了 Changed 调用和省略号,说明真实业务逻辑(比如刷新表格、重算浮盈)要你在 m_interface 里补。外汇与贵金属品种波动大,虚拟层若不接关闭事件,持仓状态可能和实际账户漂移,复盘时容易误判。

MQL5 / C++
  PrintFormat(__FUNCTION__ + " | OK, Strategy orders: %d from %d total",
                  ArraySize(orders),
                  ArraySize(self.m_orders));
}
class="type">void CVirtualReceiver::OnOpen(CVirtualOrder *p_order) {
   m_interface.Changed(p_order);
   ...
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Handle closing a class="kw">virtual position                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CVirtualReceiver::OnClose(CVirtualOrder *p_order) {
   m_interface.Changed(p_order);
   ...
   }
}

漂亮图表背后的真实回测

常有人觉得读者会先翻到文末找一张资金曲线漂亮的图,若真有这种图,才愿意回头读正文。但本篇沿用的演示 EA 与上一篇文章完全一致,策略实例集未做任何改动,所以这里没有能唬人的资金增长图。 写这篇时我另跑了其他策略的优化与训练,后续会发。初步结果说明,把大量策略实例塞进同一个 EA 是条有潜力的路。 下面是两个 EA 测试样例:各用了约 170 个分布在不同交易品种和周期上的策略实例,测试区间 2023-01-01 至 2024-02-23,该段数据未参与优化与训练。资金管理上,一组设可接受回撤约 10%,另一组约 40%。 这类结果不保证以后重复出现。外汇与贵金属属高风险品类,多策略堆叠只能让相似结果的概率倾向更高,关键是不让曲线恶化、避开过度训练的自我欺骗。

◍ 把工具请下神坛

状态保存与挂单处理不是 EA 开发的装饰品。任何准备上真实账户的程序,都得能把市价单和挂单一并恢复,否则重启终端就是一次裸奔。环境评估的可视化也一样,开发期就能帮你逮出实现错误,而不是等真金白银去填坑。 评论区里 fxsaber 提到一种不存虚拟仓位的做法:EA 启动时让内部策略在虚拟测试器里跑到当前 TimeCurrent,这样重载后的数据和未停的终端自然对齐。Yuriy 也承认这思路有趣,但要求各实例虚拟交易起点日期一致,还得自带或复用现成虚拟测试器,复杂度不低。 外汇和贵金属杠杆高、报价有经纪商差异,即便同步运行的实例也难完全一致。工具是拿来用的,不是供着的——开 MT5 把文末 SimpleVolumesExpert.mq5(7.77 KB)拖进去跑一遍,比争论架构优雅实在得多。

常见问题

在初始化时打印读出的虚拟仓位字符串,对比日志里的持仓分布;若数量与重启前一致即读回成功,否则检查存取文件路径。
在 OnInit 开头做参数范围与逻辑校验,不通过直接返回 INIT_PARAMETERS_INCORRECT 并 Alert 提示,EA 不会跑策略。
可以,小布能聚合各品种日志里的虚拟挂单与持仓状态,直接给出分布异常提醒,省去你手动翻 MT5 终端记录。
给仓位结构写 Stringify 方法拼成『品种|方向|量|价』格式,存文件时调它,打印日志也能一眼看清。
会,回调里没做去重和状态锁就可能重复挂;建议在回调入口先比对本地虚拟表再决定是否下单。