使用 WCF 服务将报价从 MetaTrader 5 导出至 .NET 应用程序的方法(基础篇)
📘

使用 WCF 服务将报价从 MetaTrader 5 导出至 .NET 应用程序的方法(基础篇)

第 1/3 篇

「把 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 用低流动性时段压测。

MQL5 / C++
    [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 前务必先单品种跑通再扩列表。

MQL5 / C++
    [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,二者都留了向客户端发通知的缺口,接上那段就能让小布面板实时刷可用品种列表。

MQL5 / C++
      _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
                    {

常见问题

可以用 WCF 服务加命名管道,在 MT5 端写导出类库把报价推给 .NET 客户端,不依赖 DDE。
一类是逐笔或定时的价格报价,一类是品种基础信息(点值、小数位等),缺了后者算不了盈亏。
小布可以帮你梳理 WCF 双向契约和命名管道配置要点,并对照你的代码指出生命周期管理的坑。
不会,只要服务端用实例池维护多客户端会话,就能在生命周期内给每个客户端独立推送。
是服务端生命周期和重连逻辑,很多人只发不保活,客户端掉线后报价就永久断了。