使用 WCF 服务将报价从 MetaTrader 5 导出至 .NET 应用程序的方法·综合运用
(3/3)· 从通用契约到实时通信压测,补齐把 MqlTick 推到 .NET 客户端的最后一块拼图
「用类封装报价广播服务避免重复拉起」
在 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 计数。
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 共存时的引用变化。
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内部自己兜底。
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 分配内存疯涨直至崩终端。 这种崩溃只出现在远超真实跳动频率的严苛循环里,正常报价节奏下线程池方案并不危险。外汇与贵金属杠杆高、滑点无常,任何中间层转发都先在小账户验证再上实盘。
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 收不到数据,说明管道两端版本要对齐。外汇与贵金属行情通过这类桥接做二次分析仍属高风险操作,环境没调顺之前别直接接实盘。 真要长期用,不必全市场广播,只让脚本推你盯的那几个品种就行;市场深度双向访问也是同一套命名管道思路,能扩但别贪多。