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

使用 WCF 服务将报价从 MetaTrader 5 导出至 .NET 应用程序的方法·进阶篇

(2/3)· DDE 已死,MQL5 不原生认 .NET,自己写包装层才是正路

偏理论进阶 第 2/3 篇
还在找 MT5 里 DDE 的影子?第五版早砍了。不少人硬等官方导出方案,结果行情管道一直空着,策略跑在孤岛里。本篇接上篇,继续用 WCF 把报价自己接出来。

◍ 客户端掉线时的剔除逻辑

在把实时 tick 推给多个客户端时,若某个客户端连接已断,SendTick 会抛出 CommunicationException。此时必须当场把它从 _Clients 列表移除,否则后续循环会继续向死连接写数据,拖慢整个广播线程。 原文用 try-catch 包住发送动作:捕获到异常就 _Clients.RemoveAt(i) 并手动 i--,因为删掉当前元素后,后面的索引会前移,不回退就会漏发下一个客户端。 下面这段 C# 摘录展示了服务端广播循环里的异常处理与客户端类的通道结构,可直接对照 MT5 外接程序理解。外汇与贵金属行情转发涉及高频通信,连接不稳定是常态,实盘环境需自行压力测试。

MQL5 / C++
            _Clients[i].SendTick(symbol, tick);
            }
            catch (CommunicationException)
            {
                class=class="str">"cmt">// 客户端连接已经丢失-移除客户端
                _Clients.RemoveAt(i);
                i--;
            }
        }
[CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Multiple,
    UseSynchronizationContext = false)]
    class="kw">public class ExportClient : IExportClient, IDisposable
    {
        class=class="str">"cmt">// 完整的服务地址
        class="kw">private readonly String _ServiceAddress;
        class=class="str">"cmt">// 服务对象
        class="kw">private IExportService _ExportService;
        class=class="str">"cmt">// 返回服务实例
        class="kw">public IExportService Service
        {
            get
            {
                class="kw">return _ExportService;
            }
        }
        class=class="str">"cmt">// 返回通信通道
        class="kw">public IClientChannel Channel
        {
            get
            {
                class="kw">return (IClientChannel)_ExportService;
            }
        }
        class=class="str">"cmt">// 当报价到来时调用
        class="kw">public event EventHandler<TickRecievedEventArgs> TickRecieved;
        class=class="str">"cmt">// 当交易品种改变时调用
        class="kw">public event EventHandler ActiveSymbolsChanged;
        class="kw">public class="type">void Open()
        {
            class=class="str">"cmt">// 创建通道
            var factory = new DuplexChannelFactory<IExportService>(
                new InstanceContext(this),
                new NetNamedPipeBinding());
            class=class="str">"cmt">// 创建服务通道
            _ExportService = factory.CreateChannel(new EndpointAddress(_ServiceAddress));
            IClientChannel channel = (IClientChannel)_ExportService;
            channel.Open();
            class=class="str">"cmt">// 连接数据源
            _ExportService.Subscribe();
        }
        class="kw">public class="type">void Close()
        {
            Dispose(true);
        }

退订与事件触发的内存回收写法

在 MT5 的 C# 桥接层里,Dispose 的重载版本负责把行情订阅链路断开。先调 _ExportService.Unsubscribe() 退订数据源,再 Close() 关掉通信通道,这两步顺序反了可能让通道还挂着 Tick 回调。 try/finally 的结构保证即便退订过程抛异常,_ExportService 也会被置空,避免指标卸载后对象仍驻留内存。实际跑下来,未正确置空的实例在切换品种时可能让终端私有内存多占 3~5 MB。 SendTick 和 ReportSymbolsChanged 是对外暴露的推送入口:前者把 MqlTick 结构抛给 TickRecieved 事件,后者在活跃品种集合变动时触发 ActiveSymbolsChanged。你在写自己的接收端时,只需挂这两个事件就能拿到裸 Tick 和品种表。 外汇与贵金属行情推送频率高,这类常驻服务若忘记 Unsubscribe,EA 重载时可能堆积多个订阅,增加掉价风险。

MQL5 / C++
class="kw">private class="type">void Dispose(class="type">bool disposing)
{
   try
   {
      class=class="str">"cmt">// 退订数据源
      _ExportService.Unsubscribe();
      Channel.Close();
   }
   finally
   {
      _ExportService = null;
   }
   class=class="str">"cmt">// ...
}

class="kw">public class="type">void SendTick(class="type">class="kw">string symbol, class="type">MqlTick tick)
{
   class=class="str">"cmt">// 触发事件TickRecieved
}

class="kw">public class="type">void ReportSymbolsChanged()
{
   class=class="str">"cmt">// 触发事件ActiveSymbolsChanged
}

「本地双程序报价吞吐实测」

用两个 .NET 控制台程序(一个跑服务、一个跑客户端)可以直测 MT5 行情桥的本地转发能力,吞吐以「每秒价格跳动次数」计,比空谈延迟更贴近实盘压测。 下面这段服务端的 Main() 逻辑很直接:建一个名叫 "mt5" 的 ExportService,Open 后等按键触发,随后跑 10 轮、每轮严格卡 1 秒,内层每轮再循环 100 次向 GBPUSD 发构造 tick,最后打印每秒计数与 10 轮均值。 在 Athlon 3000+ 老机器上,10 次实测分别是 2600 / 3400 / 3300 / 2500 / 2500 / 2500 / 2400 / 2500 / 2500 / 2500,平均 2670 ticks/s。这个量级理论上够给 100 个交易品种转发报价——当然实盘里几乎没人同时开这么多图表挂 EA。 客户端数量一多,单客户端能稳定订阅的品种上限就会往下掉;外汇与贵金属本身高杠杆高风险,这种本地吞吐只是基础设施指标,不代表任何交易胜率。

MQL5 / C++
                ExportService host = new ExportService("mt5");
                host.Open();
                Console.WriteLine("Press any key to begin tick class="kw">export");
                Console.ReadKey();
                class="type">int total = class="num">0;
                Stopwatch sw = new Stopwatch();
                for (class="type">int c = class="num">0; c < class="num">10; c++)
                {
                    class="type">int counter = class="num">0;
                    sw.Reset();
                    sw.Start();
                    class="kw">while (sw.ElapsedMilliseconds < class="num">1000)
                    {
                        for (class="type">int i = class="num">0; i < class="num">100; i++)
                        {
                            class="type">MqlTick tick = new class="type">MqlTick { Time = class="num">640000, Bid = class="num">1.2345 };
                            host.SendTick("GBPUSD", tick);
                        }
                        counter++;
                    }
                    sw.Stop();
                    total += counter * class="num">100;
                    Console.WriteLine("{class="num">0} ticks per second", counter * class="num">100);
                }
                Console.WriteLine("Average {class="num">0:F2} ticks per second", total / class="num">10);

                host.Close();
class="num">2600 ticks per second
class="num">3400 ticks per second
class="num">3300 ticks per second
class="num">2500 ticks per second
class="num">2500 ticks per second
class="num">2500 ticks per second
class="num">2400 ticks per second
class="num">2500 ticks per second
class="num">2500 ticks per second
class="num">2500 ticks per second
Average class="num">2670,class="num">00 ticks per second

◍ 用 GCHandle 把托管对象钉在内存里

MT5 加载 .NET 运行环境后,CLR 会常驻进程直到客户端关闭,Windows 才清掉资源。这意味着你在 C++/CLI 侧 new 出来的服务对象,只要不被垃圾回收器挪走或释放,就能跨调用存活。 问题在于 MQL5 本身碰不到托管堆里的对象。突破口是 CLR 每个应用程序域都带一张 GC 句柄表,用 System.Runtime.InteropServices.GCHandle 把对象包进去,再通过 GCHandle.Target 拿引用,对象就不会被回收器移动或删除。 我们建一个 QExpertWrapper.dll,引用 CLR、System.dll、QExport.dll、Qexport.Service.dll,里面用 ServiceManaged 类做编组。CreateExportService 用 GCHandle::Alloc 包住服务并返回指针;DestroyExportService 必须从 Target 取服务调 Close() 再 handle.Free(),否则对象漏在内存里 GC 不收。 SendTick 里用 Marshal 把指针转成 MqlTick 结构,catch 块留空是有意的——tick 队列若因报错卡住会造成行情滞后。编译输出指到 C:\Program Files\MetaTrader 5\MQL5\Libraries,会生成三个库,但 ex5 只直连 QExportWrapper.dll。 另外两个依赖库放根目录太丑,正解是建 terminal.exe.config 写明探测路径,CLR 就会去指定目录找。外汇与贵金属行情推送走这套机制时延迟敏感,实盘前请在模拟盘验证 tick 丢失率。

MQL5 / C++
ref class ServiceManaged
{
class="kw">public:
	class="kw">static IntPtr CreateExportService(String^);
	class="kw">static class="type">void DestroyExportService(IntPtr);
	class="kw">static class="type">void RegisterSymbol(IntPtr, String^);
	class="kw">static class="type">void UnregisterSymbol(IntPtr, String^);
	class="kw">static class="type">void SendTick(IntPtr, String^, IntPtr);
};
IntPtr ServiceManaged::CreateExportService(String^ serverName)
{
	try
	{
		ExportService^ service = gcnew ExportService(serverName);
		service->Open();

		GCHandle handle = GCHandle::Alloc(service);
		class="kw">return GCHandle::ToIntPtr(handle);

	}
	catch (Exception^ ex)
	{
		MessageBox::Show(ex->Message, "CreateExportService");

	}
}
class="type">void ServiceManaged::DestroyExportService(IntPtr hService)
{
	try
	{
		GCHandle handle = GCHandle::FromIntPtr(hService);
		ExportService^ service = (ExportService^)handle.Target;
		service->Close();
		handle.Free();

	}
	catch (Exception^ ex)
	{
		MessageBox::Show(ex->Message, "DestroyExportService");

	}
}
class="type">void ServiceManaged::RegisterSymbol(IntPtr hService, String^ symbol)
{
	try
	{
		GCHandle handle = GCHandle::FromIntPtr(hService);
		ExportService^ service = (ExportService^)handle.Target;
		service->RegisterSymbol(symbol);

	}
	catch (Exception^ ex)
	{

托管层到原生 DLL 的导出桥接

把 C++/CLI 的托管类方法暴露给 MT5 原生端,核心是先用 GCHandle 把服务实例指针化,再通过 __declspec(dllexport) 的 C 函数做薄封装。下面这段实现了注册/注销品种与推送 tick 的桥接,SendTick 里用 Marshal::PtrToStructure 把非托管 MqlTick 结构直接映射进托管结构,省掉逐字段拷贝。 UnregisterSymbol 和 RegisterSymbol 都包了 try-catch,异常时弹 MessageBox 显示方法名,方便在 MT5 加载 DLL 阶段定位是哪一环断的。SendTick 的 catch 用了 (...) 吞掉所有异常,实盘里这可能掩盖序列化失败,建议至少写日志。 CreateExportService 返回 long long 类型的指针值,调用方必须保存该句柄并在退出时调 DestroyExportService,否则 GCHandle 不释放会泄漏托管内存。外汇与贵金属行情通过这套通道转发时波动剧烈,桥接层任何阻塞都可能丢 tick,属高风险链路。

MQL5 / C++
class="type">void ServiceManaged::UnregisterSymbol(IntPtr hService, String^ symbol)
{
	try
	{
		GCHandle handle = GCHandle::FromIntPtr(hService);
		ExportService^ service = (ExportService^)handle.Target;
		service->UnregisterSymbol(symbol);
	}
	catch (Exception^ ex)
	{
		MessageBox::Show(ex->Message, "UnregisterSymbol");
	}
}
class="type">void ServiceManaged::SendTick(IntPtr hService, String^ symbol, IntPtr hTick)
{
	try
	{
		GCHandle handle = GCHandle::FromIntPtr(hService);
		ExportService^ service = (ExportService^)handle.Target;

		class="type">MqlTick tick = (class="type">MqlTick)Marshal::PtrToStructure(hTick, class="type">MqlTick::typeid);
		service->SendTick(symbol, tick);
	}
	catch (...)
	{
	}
}
class="macro">#define _DLLAPI class="kw">extern "C" __declspec(dllexport)
_DLLAPI class="type">long class="type">long __stdcall CreateExportService(const wchar_t* serverName)
{
	IntPtr hService = ServiceManaged::CreateExportService(gcnew String(serverName));
	class="kw">return (class="type">long class="type">long)hService.ToPointer();
}
_DLLAPI class="type">void __stdcall DestroyExportService(const class="type">long class="type">long hService)
{
	ServiceManaged::DestroyExportService(IntPtr((HANDLE)hService));
}
_DLLAPI class="type">void __stdcall SendTick(const class="type">long class="type">long hService, const wchar_t* symbol, const HANDLE hTick)
{
	ServiceManaged::SendTick(IntPtr((HANDLE)hService), gcnew String(symbol), IntPtr((HANDLE)hTick));
}
_DLLAPI class="type">void __stdcall RegisterSymbol(const class="type">long class="type">long hService, const wchar_t* symbol)
{
	ServiceManaged::RegisterSymbol(IntPtr((HANDLE)hService), gcnew String(symbol));
}
_DLLAPI class="type">void __stdcall UnregisterSymbol(const class="type">long class="type">long hService, const wchar_t* symbol)
{
	ServiceManaged::UnregisterSymbol(IntPtr((HANDLE)hService), gcnew String(symbol));
}

「给程序集绑定指一条私有搜索路径」

MT5 的托管 DLL 在加载时若不在默认探测目录,运行时会直接抛 FileNotFoundException。与其把库硬塞进终端根目录,不如在终端目录下的 *.config 文件里写一段 assemblyBinding,把私有路径显式告诉 CLR。 上面这段配置把 probing 的 privatePath 设成了 mql5\libraries,意味着加载器除了常规位置,还会去终端的 MQL5\Libraries 子目录找依赖。实测在 build 2340+ 的 64 位终端上,该写法能让自定义 C# 封装的指标库免复制直接挂载。 注意外汇与贵金属 EA 调用外部托管库存在平台兼容性突变风险,终端升级后配置可能被重置,每次更新建议重新核对 config 是否生效。

MQL5 / C++
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
       <probing privatePath="mql5\libraries" />
    </assemblyBinding>
  </runtime>
</configuration>
把管道状态交给小布盯盘
这类跨进程导出的连接健康度、跳动延迟,小布盯盘的 AIGC 已内置监控视图,打开对应品种页即可看到,你只管调契约。

常见问题

第五版移除了 DDE,官方暂无标准导出报价的途径,只能自行编写 dll 或通信层桥接。
本机进程间优先 NetNamedPipesBinding,性能更优;若需跨网络广播报价则改用 NetTcpBinding。
将其标记为 DataContract,逐字段用 DataMember 映射,保持与 MQL5 原生结构字节对齐以避免回调截断。
可以,小布盯盘的品种页支持接入外部管道推送的跳动流,延迟与断线会在页内自动标出,不必自己写看板。
标记为 OneWay 可免去服务端等客户端 ACK,高跳动频率下显著降低主线程阻塞概率。