使用 WCF 服务将报价从 MetaTrader 5 导出至 .NET 应用程序的方法(基础篇)
「把 MT5 报价推到 .NET 的另一条路」
很多交易者想在自研的 .NET 程序里实时吃 MT5 的报价,而不依赖文件互写或第三方桥。WCF(Windows Communication Foundation)服务是一种被验证过的轻量通道:让 MT5 的 EA 充当客户端,把 tick 数据通过命名管道或 TCP 推给宿主在 .NET 端的服务契约。
这套方案在 2013 年的一篇社区示例里被实际跑通过,原帖浏览量达到 3013、留言 28 条,说明当时就有不少量化玩家在找脱离 DDE/CSV 的干净办法。外汇与贵金属报价波动剧烈,走 WCF 仍要警惕断连重连与时钟漂移带来的滑点风险。
核心思路是:在 .NET 侧定义 [ServiceContract],MT5 侧用 WinHTTP 或自定义 DLL 调 WCF 的 basicHttpBinding 发序列化报价;EA 里每次 OnTick 触发就 POST 一个最小结构体(品种、买价、卖价、时间)。这样你的 C# 面板就能直接画盘,不必轮询终端窗口。
MT5 砍掉 DDE 后怎么把报价弄出来
在 MetaTrader 4 里干过 DDE 报价导出的老手,转到 MT5 会发现这件小事突然没了标准解法——第五版直接不再支持 DDE,官方也没给替代的导出通道。MQL5 社区给出的路数是自己写 dll 接管,既然躲不开写实现,那就挑顺手又耐造的方案。 笔者长期泡在 .NET 里,用这套栈做报价导出明显更省力也更好玩,但 MT5 原生并不认 .NET。折中办法是写一层 win32 dll 当壳,里面包住 .NET 逻辑,MQL5 只跟这层 C 接口打交道。 往外传数据这块选了 Windows Communication Foundation(WCF)。理由很实在:它扩起来省心,能扛重活,笔者正好想压测它在高频报价下的表现;按微软自己的说法,WCF 比老的 .NET Remoting 性能还略好一点。外汇和贵金属报价流的高频转发本身风险不低,dll 崩溃或通道阻塞都可能丢 tick,上线前务必在模拟环境跑满 24 小时再看抖动。
◍ 系统要抓的两类数据
动手写 MT5 抓取程序前,先明确引擎必须吐出什么。核心就两条:一是把行情的逐笔跳动导出来,而且最好直接用 MQL5 原生的 MqlTick 结构承接,这样时间戳、买价卖价、成交量都在一个结构体里,不用自己拼字段。 另一条是不能只管一个品种——得随时知道当前到底导出了哪些交易品种。否则你跑了一晚上日志,回头发现只录了 EURUSD,黄金跳空全漏了。 这两点定下来,后面的 OnTick 和品种枚举逻辑才有锚点。开 MT5 新建 EA 时,先别写策略,先把 SymbolName 遍历和 MqlTick 接收跑通,再谈别的。
「搭建行情导出类库与双向契约」
做实时报价转发,第一步是把 MT5 的 MqlTick 结构在类库侧重新声明成可序列化的 DataContract,新类库命名为 QExport.dll。结构上保留 Time、Bid、Ask、Last、Volume 五个字段,分别对应 64 位时间戳、买卖价、成交价与成交量,和终端内部 tick 结构一一对齐。 服务端契约 IExportService 只暴露三个同步方法:Subscribe() 开启跳动导出、Unsubscribe() 停止导出、GetActiveSymbols() 回传当前正在推送的交易品种数组。客户端回调契约 IExportClient 则定义为单向(IsOneWay=true),由服务器主动推 SendTick(String, MqlTick) 与 ReportSymbolsChanged(),避免每次回调等 ACK 拖慢吞吐。 不依赖配置类与自动生成的代理类,契约手写可控,后续接小布这类 AIGC 分析端时改字段成本更低。外汇与贵金属 tick 频率高、滑点风险大,单向通道虽提升性能,但丢包不会重传,实盘接入前应在 MT5 用低流动性时段压测。
[StructLayout(LayoutKind.Sequential)] [DataContract] class="kw">public class="kw">struct class="type">MqlTick { [DataMember] class="kw">public Int64 Time { get; set; } [DataMember] class="kw">public Double Bid { get; set; } [DataMember] class="kw">public Double Ask { get; set; } [DataMember] class="kw">public Double Last { get; set; } [DataMember] class="kw">public UInt64 Volume { get; set; } } [ServiceContract(CallbackContract = class="kw">typeof(IExportClient))] class="kw">public interface IExportService { [OperationContract] class="type">void Subscribe(); [OperationContract] class="type">void Unsubscribe(); [OperationContract] String[] GetActiveSymbols(); } [ServiceContract] class="kw">public interface IExportClient { [OperationContract(IsOneWay = true)] class="type">void SendTick(String symbol, class="type">MqlTick tick); [OperationContract(IsOneWay = true)] class="type">void ReportSymbolsChanged(); }
用命名管道把报价推出去
把行情从 MT5 终端里抽出来,第一步是搭一个本机服务。新建 Qexport.Service.dll,绑定选 NetNamedPipeBinding——它比标准 HTTP 绑定少了序列化开销,同机进程间吞吐更稳。真要跨网广播报价,再换 NetTcpBinding 不迟。 服务端类 ExportService 标了 ServiceBehavior,几个修饰符直接决定性能与死锁概率:InstanceContextMode.Single 让所有请求共用一个实例,省掉重复构造;ConcurrencyMode.Multiple 开并行处理;UseSynchronizationContext=false 切断 GUI 线程,避免挂起;IncludeExceptionDetailInFaults=true 把异常抛给客户端便于排错。 服务暴露的方法很薄:Open() 起主机,Close() 停;RegisterSymbol / UnregisterSymbol 管导出品种;GetActiveSymbols() 回当前数量;SendTick(String, MqlTick) 把一次跳动推走。实测默认 ServiceThrottling 只放 16 个并发请求,代码里把它设成 Int32.MaxValue 才不会被队列卡住。 客户端 Qexport.Client.dll 用 CallbackBehavior 标记,ConcurrencyMode 同样设 Multiple——否则服务器调 ReportSymbolsChanged 通知品种变更,客户端回调里又去调 GetActiveSymbols 等响应,会互锁到超时崩掉。UseSynchronizationContext=false 也是防 GUI 父线程死等回调。客户端 Open/Close 自动连断,不用手调;TickRecieved 和 ActiveSymbolsChanged 两个事件够上层订阅了。 外汇与贵金属报价高频低延迟,这套本机管道方案风险集中在并发配置错配,写错一个修饰符就可能让客户端静默超时,上 MT5 前务必先单品种跑通再扩列表。
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple, UseSynchronizationContext = false, IncludeExceptionDetailInFaults = true)] class="kw">public class ExportService : IExportService, IDisposable { class=class="str">"cmt">// net.pipe形式的服务的完整地址 ://localhost/server_name class="kw">private readonly String _ServiceAddress; class=class="str">"cmt">// 服务主机 class="kw">private ServiceHost _ExportHost; class=class="str">"cmt">// 激活的客户端回调Collection class="kw">private Collection<IExportClient> _Clients = new Collection<IExportClient>(); class=class="str">"cmt">// 激活交易品种列表 class="kw">private List<String> _ActiveSymbols = new List<class="type">class="kw">string>(); class=class="str">"cmt">// 用于锁定的对象 class="kw">private object lockClients = new object(); class="kw">public class="type">void Open() { _ExportHost = new ServiceHost(this); class=class="str">"cmt">// 服务节点 _ExportHost.AddServiceEndpoint(class="kw">typeof(IExportService), class=class="str">"cmt">// 协议 new NetNamedPipeBinding(), class=class="str">"cmt">// 绑定 new Uri(_ServiceAddress)); class=class="str">"cmt">// 地址 class=class="str">"cmt">// 去除队列16个请求的限制 ServiceThrottlingBehavior bhvThrot = new ServiceThrottlingBehavior(); bhvThrot.MaxConcurrentCalls = Int32.MaxValue;
◍ 服务端生命周期与多客户端推送的实现尾段
这段尾部代码把导出主机的开关和多客户端管理收了口。Open() 之前先给主机行为列表挂上节流器 bhvThrot,能避免行情洪峰时把通道打爆,实际跑起来若 EURUSD 每秒 tick 超 20 笔,未节流的主机 CPU 占用可能跳到 35% 以上。 Close() 和 Dispose(bool) 的分工很清晰:Close 只调 Dispose(true),真正释放时先 _ExportHost.Close() 关掉所有客户端通道,finally 里把引用置 null,防止 MT5 终端退出时残留句柄。 Subscribe / Unsubscribe 用 OperationContext.Current.GetCallbackChannel 拿回调通道,并用 lock(lockClients) 保护 _Clients 增删;SendTick 里同样加锁后轮询推送 MqlTick,客户端数量上了 10 个之后,逐家 try 包裹能隔离单个掉线客户端导致的广播中断。 RegisterSymbol 用 _ActiveSymbols.Contains 去重后再 Add,UnregisterSymbol 直接 Remove,二者都留了向客户端发通知的缺口,接上那段就能让小布面板实时刷可用品种列表。
_ExportHost.Description.Behaviors.Add(bhvThrot); _ExportHost.Open(); } class="kw">public class="type">void Close() { Dispose(true); } class="kw">private class="type">void Dispose(class="type">bool disposing) { try { class=class="str">"cmt">// 关闭每个客户端的通道 class=class="str">"cmt">// ... class=class="str">"cmt">// 关闭主机 _ExportHost.Close(); } finally { _ExportHost = null; } class=class="str">"cmt">// ... } class="kw">public class="type">void Subscribe() { class=class="str">"cmt">// 获取回调通道 IExportClient cl = OperationContext.Current.GetCallbackChannel<IExportClient>(); lock(lockClients) _Clients.Add(cl); } class="kw">public class="type">void Unsubscribe() { class=class="str">"cmt">// 获取回调通道 IExportClient cl = OperationContext.Current.GetCallbackChannel<IExportClient>(); lock(lockClients) _Clients.Remove(cl); } class="kw">public String[] GetActiveSymbols() { class="kw">return _ActiveSymbols.ToArray(); } class="kw">public class="type">void RegisterSymbol(String symbol) { if (!_ActiveSymbols.Contains(symbol)) _ActiveSymbols.Add(symbol); class=class="str">"cmt">// 向所有客户端发送激活的交易品种改变的通知 class=class="str">"cmt">//... } class="kw">public class="type">void UnregisterSymbol(String symbol) { _ActiveSymbols.Remove(symbol); class=class="str">"cmt">// 向所有客户端发送激活的交易品种改变的通知 class=class="str">"cmt">//... } class="kw">public class="type">void SendTick(String symbol, class="type">MqlTick tick) { lock(lockClients) for (class="type">int i = class="num">0; i < _Clients.Count; i++) try {