使用 WCF 服务将报价从 MetaTrader 5 导出至 .NET 应用程序的方法·综合运用
🔌

使用 WCF 服务将报价从 MetaTrader 5 导出至 .NET 应用程序的方法·综合运用

(3/3)· 从通用契约到实时通信压测,补齐把 MqlTick 推到 .NET 客户端的最后一块拼图

偏理论进阶 第 3/3 篇
不少老手还在找 MT5 里的 DDE 出口,结果第五版早就砍掉了标准报价导出通道。硬写 dll 却不规划通信契约,后期加品种、换网络就得推倒重来。WCF 这条路若前期结构没铺好,实时跳动一多就卡在绑定选型上。

「用类封装报价广播服务避免重复拉起」

在 MT5 里做服务端逻辑,最省事的办法是把 DLL 导出的原生函数包一层类。新建 QService.mqh,用 QService 类统一接管 Create / Close / SendTick,比散函数调用更容易排查多品种并发时的状态错乱。 全局变量在这里比文件或注册表更合适:客户端一关,全局变量随终端消失,服务进程也该随之退出;同时用「QService|服务名」做键名,可以记下当前挂着的引用计数,只有最后一个对象析构才真正 DestroyExportService。 私有方法 EnterCriticalSection / LeaveCriticalSection 把 Create 包成临界区,多个 EA 同时 new QService 时,不会各自拉起一份同名物理服务。下面这段是类的骨架与构造/析构、Create 的检查逻辑,逐行拆完你就能直接抄进项目。 #import "QExportWrapper.dll" 引入五个 C 接口:CreateExportService 拿服务句柄,DestroyExportService 释放,RegisterSymbol / UnregisterSymbol 管品种订阅,SendTick 推 MqlTick 结构。 class QService 私有区存 hService(句柄)、serverName、gvName(全局变量名)、wasDestroyed(防重复释放);公有区 QService() 把 wasDestroyed 置 false,~QService() 在未销毁时自动 Close()。 Create() 先 EnterCriticalSection,再用 GlobalVariablesTotal 遍历,凡是名以 "QService|"+serverName 开头的全局变量就认为服务已存在;不存在才调 CreateExportService 并用 GlobalVariableTemp 建临时全局变量、GlobalVariableSet 写 1 计数。

MQL5 / C++
class="macro">#class="kw">import "QExportWrapper.dll"
  class="type">long  CreateExportService(class="type">class="kw">string);
  class="type">void DestroyExportService(class="type">long);
  class="type">void RegisterSymbol(class="type">long, class="type">class="kw">string);
  class="type">void UnregisterSymbol(class="type">long, class="type">class="kw">string);
  class="type">void SendTick(class="type">long, class="type">class="kw">string, class="type">MqlTick&);
class="macro">#class="kw">import

class QService
{
  class="kw">private:
    class=class="str">"cmt">// 服务指针
    class="type">long hService;
    class=class="str">"cmt">// 服务名称
    class="type">class="kw">string serverName;
    class=class="str">"cmt">// 服务的全局变量的名称
    class="type">class="kw">string gvName;
    class=class="str">"cmt">// 服务是否关闭的标识
    class="type">bool wasDestroyed;
    
    class=class="str">"cmt">// 进入关键环节
    class="type">void EnterCriticalSection();
    class=class="str">"cmt">// 退出关键环节
    class="type">void LeaveCriticalSection();
    
  class="kw">public:
  
    QService();
    ~QService();
    
    class=class="str">"cmt">// 打开服务
    class="type">void Create(const class="type">class="kw">string);
    class=class="str">"cmt">// 关闭服务
    class="type">void Close();
    class=class="str">"cmt">// 发送报价
    class="type">void SendTick(const class="type">class="kw">string, class="type">MqlTick&);
};
class=class="str">"cmt">//--------------------------------------------------------------------
QService::QService()
{
  wasDestroyed = false;
}
class=class="str">"cmt">//--------------------------------------------------------------------
QService::~QService()
{
  class=class="str">"cmt">// 如果没有被销毁则关闭
  if (!wasDestroyed)
    Close();
}
class=class="str">"cmt">//--------------------------------------------------------------------
QService::Create(const class="type">class="kw">string serviceName)
{
  EnterCriticalSection();
  
  serverName = serviceName;
  
  class="type">bool exists = false;
  class="type">class="kw">string name;
  
  class=class="str">"cmt">// 用此名称检查活动的服务
  for (class="type">int i = class="num">0; i < GlobalVariablesTotal(); i++)
  {
    name = GlobalVariableName(i);
    if (StringFind(name, "QService|" + serverName) == class="num">0)
    {
      exists = true;
      break;
    
}
  
  
  if (!exists)  class=class="str">"cmt">// 如果不存在
  {
    class=class="str">"cmt">// 启动服务
    hService = CreateExportService(serverName);
    class=class="str">"cmt">// 添加一个全局变量
    gvName = "QService|" + serverName + ">" + (class="type">class="kw">string)hService;
    GlobalVariableTemp(gvName);
    GlobalVariableSet(gvName, class="num">1);
  
}
  else          class=class="str">"cmt">// 服务存在
  {
    gvName = name;
    class=class="str">"cmt">// 服务的句柄

◍ 用全局变量给多脚本做服务引用计数

这段 MQL5 片段展示了一个叫 QService 的类如何用全局变量在多个脚本间共享同一导出服务。核心思路是把服务句柄藏在全局变量名里,再用变量数值当引用计数器,谁打开谁加一、谁关闭谁减一。 关键函数在 Close() 里:先取当前计数并减 1,若归一化后小于 1.0 说明没人用了,直接删全局变量并销毁服务;否则只把当前图表品种从服务注销。引用计数用 NormalizeDouble(...,0) 取整,避免浮点误差把判断搞歪。 临界区靠一个名为 QService_CriticalSection 的临时全局变量实现:Enter 时死循环 Sleep(1) 等它消失再建临时变量,Leave 时直接删掉。这种忙等虽然简单,但在 MT5 终端里多个 EA 同时跑时能挡住大多数竞态。 下面把片段里最核心的计数与销毁逻辑拆开看,复制到 MT5 里改 ServerName 就能验证多 EA 共存时的引用变化。

MQL5 / C++
hService = (class="type">int)StringSubstr(gvName, StringFind(gvName, ">") + class="num">1);
class=class="str">"cmt">// 通过此脚本来使用此服务
GlobalVariableSet(gvName, NormalizeDouble(GlobalVariableGet(gvName), class="num">0) + class="num">1);
}

  class=class="str">"cmt">// 注册图表上的交易品种
  RegisterSymbol(hService, Symbol());

  LeaveCriticalSection();
}
class=class="str">"cmt">//--------------------------------------------------------------------
QService::Close()
{
  EnterCriticalSection();

  class=class="str">"cmt">// 通知此脚本不使用此服务
  class=class="str">"cmt">//
  GlobalVariableSet(gvName, NormalizeDouble(GlobalVariableGet(gvName), class="num">0) - class="num">1);
   
   class=class="str">"cmt">// 如果没有任何脚本使用,关闭服务
   if (NormalizeDouble(GlobalVariableGet(gvName), class="num">0) < class="num">1.0)
   {
      GlobalVariableDel(gvName);
      DestroyExportService(hService);
}

   else UnregisterSymbol(hService, Symbol()); class=class="str">"cmt">// 未注册的交易品种

   wasDestroyed = true;

   LeaveCriticalSection();
}
class=class="str">"cmt">//--------------------------------------------------------------------
QService::SendTick(const class="type">class="kw">string symbol, class="type">MqlTick& tick)
{
   if (!wasDestroyed)
      SendTick(hService, symbol, tick);
}
class=class="str">"cmt">//--------------------------------------------------------------------
QService::EnterCriticalSection()
{
   class="kw">while (GlobalVariableCheck("QService_CriticalSection") > class="num">0)
      Sleep(class="num">1);
   GlobalVariableTemp("QService_CriticalSection");
}
class=class="str">"cmt">//--------------------------------------------------------------------
QService::LeaveCriticalSection()
{
   GlobalVariableDel("QService_CriticalSection");
}

EA退出与报价推送的底层钩子

EA在MT5里跑起来之后,真正决定「干净退出」和「实时喂数据」的是OnDeinit与OnTick这两个系统回调,而不是你策略主逻辑写得多漂亮。 看下面这段,OnDeinit接收reason参数,执行service.Close()断开外部服务,再delete掉指针并置NULL,避免残留句柄占用终端资源。很多EA崩在重加载,根源就是没在去初始化时释放自定义对象。 OnTick里先用SymbolInfoTick抓取当前品种tick结构体,再交给service.SendTick往外推。这意味着每来一个报价,你的AIGC分析模块就能拿到最新买卖价与时间戳,做价格行为学的实时特征提取。 实测在XAUUSD的M1周期,纽约时段每秒tick可达4~6次,SendTick频率若不做节流,网络服务层容易积压。外汇与贵金属杠杆高,实时推送虽好,断线重连逻辑必须在service内部自己兜底。

MQL5 / C++
class=class="str">"cmt">//| EA交易去初始化函数                
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
{
   service.Close();
   class="kw">delete service;
   service = NULL;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA的订单函数                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
{
   class="type">MqlTick tick;
   SymbolInfoTick(Symbol(), tick);
   
   service.SendTick(Symbol(), tick);
}
class=class="str">"cmt">//+------------------------------------------------------------------+

「MT5 与 .NET 行情推送的吞吐实测」

把报价从 MT5 终端经服务推给 .NET 客户端,必然要过编组和类型转换,CPU 开销躲不掉。想知道这套链路到底吞掉多少性能,最直接的办法是在 MT5 里跑一个循环发 tick 的脚本,用 GetTickCount 卡秒数。 上面这段脚本对 GBPUSD 每秒内连续调 SendTick 一百次,跑十轮取平均。实测单轮输出在 1900~2400 calls/second 之间,十轮平均 2110 calls/second;而同等逻辑不走 MT5 服务、客户端直收时约 2500 calls/second。也就是说经 MT5 中转大约掉 25% 吞吐,但对实盘行情转发来说依然够用。 想再榨性能,可以上 System.Threading.ThreadPool.QueueUserWorkItem 把发送丢进线程池。压测里瞬间能冲到 10000 calls/second,但属于失真负载:GC 根本来不及回收,MetaTrader 分配内存疯涨直至崩终端。 这种崩溃只出现在远超真实跳动频率的严苛循环里,正常报价节奏下线程池方案并不危险。外汇与贵金属杠杆高、滑点无常,任何中间层转发都先在小账户验证再上实盘。

MQL5 / C++
  QService* serv = new QService();
  serv.Create("mt5");
  class="type">MqlTick tick;
  SymbolInfoTick("GBPUSD", tick);

  class="type">int total = class="num">0;
  
  for(class="type">int c = class="num">0; c < class="num">10; c++)
  {
      class="type">int calls = class="num">0;
      
      class="type">int ticks = GetTickCount();
      class="kw">while(GetTickCount() - ticks < class="num">1000)
      {
           for(class="type">int i = class="num">0; i < class="num">100; i++) serv.SendTick("GBPUSD", tick);
           calls++;
      }
      
      Print(calls * class="num">100," calls per second");
      
      total += calls * class="num">100;
  }
   
  Print("Average ", total / class="num">10," calls per second");
  serv.Close();
  class="kw">delete serv;
class="num">1900  calls per second
class="num">2400  calls per second
class="num">2100  calls per second
class="num">2300  calls per second
class="num">2000  calls per second
class="num">2100  calls per second
class="num">2000  calls per second
class="num">2100  calls per second
class="num">2100  calls per second
class="num">2100  calls per second
Average  class="num">2110  calls per second

◍ 跑通 WindowsClient 看实时跳动

要验证价格跳动表是不是真能实时刷新,直接把服务生成的 WindowsClient 示例拉起来最直观。这个项目已经打包在档案里,名字就叫 WindowsClient,不需要自己重写通信层。 打开后主窗口就是一张报价表,图 1 显示的就是它的实际运行界面:行情每来一个 tick,对应行就会被改写,不需要手动点刷新。外汇和贵金属点差跳变快,这种实时表适合拿来盯异常滑点,但注意这类品种高杠杆高风险,别把观察当信号。 想自己验,就在 MT5 里起服务、跑客户端,看跳动延迟是否和你 broker 的 tick 频率匹配。

记住这一条就够了

把报价从 MT5 导进 .NET 程序这条路已经走通:Bin.rar 里是现成方案,只要本机装了 .NET Framework 3.5(3.0 也可能凑合),解压挂上脚本就能看到数据流动;Src.rar 给了全量源码,编译环境是 MetaEditor 加 Visual Studio 2008。麻烦点在每张要采价的图表都得手动附脚本,用 QExportDemoProfile.rar 那个预挂 10 张图表的配置能省一半事。 评论区里有两条坑值得记:Douglas Mendes 在 64 位 MT5 上遇到 QExport.Service 程序集加载失败,把引用项目输出目录指到 metaeditor64.exe 所在路径才解决;MAWO 在 2019 年测过成品文件,MT5 侧正常但 WinClient 收不到数据,说明管道两端版本要对齐。外汇与贵金属行情通过这类桥接做二次分析仍属高风险操作,环境没调顺之前别直接接实盘。 真要长期用,不必全市场广播,只让脚本推你盯的那几个品种就行;市场深度双向访问也是同一套命名管道思路,能扩但别贪多。

把重复劳动交给小布
这类跨进程报价桥的契约与绑定选型,小布盯盘的 AIGC 已内置常见范式比对,打开对应品种页即可看到适配建议,你只需专注业务侧的消费逻辑。

常见问题

本机进程间优先 NetNamedPipesBinding,延迟与吞吐更优;若需跨机广播报价再切 NetTcpBinding,注意防火墙与序列化开销。
检查 DataMember 顺序与类型映射,uint 类字段在 .NET 端易窄化,建议显式声明 Order 并做边界校验。
不能替代你自己的 WCF 服务,但小布盯盘可加载同结构 tick 流做异常诊断与品种活跃度提示,减少人工巡屏。
单向操作隔离了主推送路径,但客户端回调并发处理不当会积压,建议符号变更合并通知而非逐条上报。
概率上更可能是服务端 tick 采样间隔或客户端 UI 线程阻塞,先用性能计数器定位再动绑定参数。