外汇EA编写教程:使用 WCF 服务将报价从 MetaTrader 5 导出至 .NET 应用程序的方法

简介

使用 MetaTrader 4 中 DDE 服务的编程人员也许听说过第五版不再支持该服务。目前尚无标准的解决方案用于导出报价。作为此问题的一种解决方案,MQL5 开发人员建议使用您自己的 dll 进行实施。因此,如果我们不得不编写实施程序,我们不妨巧妙一点!

为什么选择 .NET?

对有着长期 .NET 编程经验的笔者而言,使用该平台实施报价的导出无疑更合理、有趣和容易。遗憾的是,第五版中没有对 .NET 提供任何 MQL5 原生支持。我相信开发人员这样做有其自身的考虑。因此,我们将使用 win32 dll 作为支持 .NET 的包装程序。

为什么选择 WCF?

我之所以选择 Windows Communication Foundation (WCF) 技术是出于以下几点考虑:一方面,它易于扩展和适应,另一方面,我想在繁重的工作下对其进行检测。此外,根据 Microsoft 的说法,WCF 相比 .NET Remoting 在性能上略胜一筹。

系统要求

让我们思考一下,我们希望从我们的系统中得到什么。我认为,有两个主要的要求:

    1. 当然,我们需要导出价格跳动,最好是使用自有结构 MqlTick
    2. 最好知道当前导出的交易品种列表。

    我们开始吧…

    1. 通用类和契约

    首先,我们创建一个新的类库,并将其命名为 QExport.dll我们将 MqlTick 结构定义为 DataContract:

        [StructLayout(LayoutKind.Sequential)]
        [DataContract]
        public struct MqlTick
        {
            [DataMember] 
            public Int64 Time { get; set; }
            [DataMember]
            public Double Bid { get; set; }
            [DataMember]
            public Double Ask { get; set; }
            [DataMember]
            public Double Last { get; set; }
            [DataMember]
            public UInt64 Volume { get; set; }
        
    }
    
    

    然后我们将定义服务的契约。我不喜欢使用配置类和生成的代理类,所以它们不会出现在本文中。

    让我们根据上述要求定义第一个服务器契约:

        [ServiceContract(CallbackContract = typeof(IExportClient))]
        public interface IExportService
        {
            [OperationContract]
            void Subscribe();
    
            [OperationContract]
            void Unsubscribe();
    
            [OperationContract]
            String[] GetActiveSymbols();
        
    }
    
    

    正如我们看到的,有一套订阅和取消订阅服务器通知的标准方案。下面是操作细节的简短说明:

    操作 描述
    Subscribe() 订阅价格跳动导出
    Unsubscribe() 取消订阅价格跳动导出
    GetActiveSymbols() 返回导出的交易品种列表

    以下信息应发送至客户端回调:报价本身以及有关导出交易品种列表变更的通知接下来我们将要求的操作定义为“单向操作”以提高性能:

        [ServiceContract]
        public interface IExportClient
        {
            [OperationContract(IsOneWay = true)]
            void SendTick(String symbol, MqlTick tick);
    
            [OperationContract(IsOneWay = true)]
            void ReportSymbolsChanged();
        
    }
    
    

    操作 描述
    SendTick(String, MqlTick) 发送价格跳动
    ReportSymbolsChanged() 通知客户端有关导出交易品种列表的变更


    2. 服务器实施

    让我们通过服务器契约实施为服务创建名为 Qexport.Service.dll 的新的结构。

    我们选择 NetNamedPipesBinding 进行绑定,因为相比标准绑定,它具有最优的性能。如果我们需要广播报价,例如通过网络,则应使用 NetTcpBinding。

    以下是服务器契约实施的一些细节:

    类定义。首先,应使用以下修饰符将其标记为 ServiceBehavior 属性:

    • InstanceContextMode = InstanceContextMode.Single– 为所有处理的请求提供一个服务实例的使用,这将提高解决方案的性能。此外,我们将获得服务和管理导出交易品种列表的可能性;
    • ConcurrencyMode = ConcurrencyMode.Multiple – 表示并行处理所有客户端的请求;
    • UseSynchronizationContext = false– 表示我们没有结合 GUI 线程以防止发生挂起情况。对于我们在这里的任务而言,这并不是必须的,但如果我们想要使用 Windows 应用程序来托管服务,这就是必要的。
    • IncludeExceptionDetailInFaults = true– 传递对象 FaultException 至客户端时包含异常情况的详细信息。

    ExportService 本身包含两个接口: IExportService 和 IDisposable。前者实施所有服务函数,后者实施 .NET 资源释放的标准模型。

        [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,
            ConcurrencyMode = ConcurrencyMode.Multiple,
            UseSynchronizationContext = false,
            IncludeExceptionDetailInFaults = true)]
        public class ExportService : IExportService, IDisposable
        {
    
    

    对服务变量的描述如下所示:

            // net.pipe形式的服务的完整地址 ://localhost/server_name
            private readonly String _ServiceAddress;
    
            // 服务主机
            private ServiceHost _ExportHost;
    
            // 激活的客户端回调Collection
            private Collection<IExportClient> _Clients = new Collection<IExportClient>();
    
            // 激活交易品种列表
            private List<String> _ActiveSymbols = new List<string>();
            
            // 用于锁定的对象
            private object lockClients = new object();
    
    

    让我们定义 Open() 和 Close() 方法,这些方法用于启动和关闭我们的服务:

            public void Open()
            {
                _ExportHost = new ServiceHost(this);
    
                // 服务节点
                _ExportHost.AddServiceEndpoint(typeof(IExportService),  // 协议
                    new NetNamedPipeBinding(),                          // 绑定
                    new Uri(_ServiceAddress));                          // 地址
    
                // 去除队列16个请求的限制
                ServiceThrottlingBehavior bhvThrot = new ServiceThrottlingBehavior();
                bhvThrot.MaxConcurrentCalls = Int32.MaxValue;
                _ExportHost.Description.Behaviors.Add(bhvThrot);
    
                _ExportHost.Open();
            
    }
    
            public void Close()
            {
                Dispose(true);
            
    }
           
            private void Dispose(bool disposing)
            {
                try
                {
                    // 关闭每个客户端的通道
                    // ...
    
                    // 关闭主机
                    _ExportHost.Close();
                
    }
                finally
                {
                    _ExportHost = null;
                
    }
    
                // ...
            
    }
    
    

    接下来,是 IExportService 方法的实施:

            public void Subscribe()
            {
                // 获取回调通道
                IExportClient cl = OperationContext.Current.GetCallbackChannel<IExportClient>();
                lock (lockClients)
                    _Clients.Add(cl);
            
    }
    
            public void Unsubscribe()
            {
                // 获取回调通道
                IExportClient cl = OperationContext.Current.GetCallbackChannel<IExportClient>();
                lock (lockClients)
                    _Clients.Remove(cl);
            
    }
    
            public String[] GetActiveSymbols()
            {
                return _ActiveSymbols.ToArray();
            
    }
    
    

    现在,我们需要添加发送价格跳动和注册及删除导出交易品种的方法。

         public void RegisterSymbol(String symbol)
            {
                if (!_ActiveSymbols.Contains(symbol))
                    _ActiveSymbols.Add(symbol);
    
                  // 向所有客户端发送激活的交易品种改变的通知
                  //...
            
    }
    
            public void UnregisterSymbol(String symbol)
            {
                _ActiveSymbols.Remove(symbol);
    
                 // 向所有客户端发送激活的交易品种改变的通知
                 //...
            
    }
    
            public void SendTick(String symbol, MqlTick tick)
            {
                lock (lockClients)
                    for (int i = 0; i < _Clients.Count; i++)
                        try
                        {
                            _Clients[i].SendTick(symbol, tick);
                        
    }
                        catch (CommunicationException)
                        {
                            // 客户端连接已经丢失-移除客户端
                            _Clients.RemoveAt(i);
                            i--;
                        
    }
            
    }
    
    

    我们总结了主要服务器函数列表,如下所示(仅限我们需要的函数):

    方法 描述
    Open() 运行服务器
    Close() 停止服务器
    RegisterSymbol(String) 将交易品种添加至导出交易品种列表
    UnregisterSymbol(String) 从导出交易品种列表删除交易品种
    GetActiveSymbols() 返回导出的交易品种数量
    SendTick(String, MqlTick) 发送价格跳动至客户端

     

    3. 客户端实施

    我认为我们已经对服务器进行了充分的讨论,接下来是时候考虑客户端了。让我们来构建 Qexport.Client.dll。客户端契约将在此实施。首先,应使用定义其行为的 CallbackBehavior 属性对其加以标记。它具有以下修饰符:

      • ConcurrencyMode = ConcurrencyMode.Multiple – 表示并行处理所有回调和服务器响应。此修饰符十分重要。试想一下,服务器希望通过调用回调 ReportSymbolsChanged() 通知客户端有关导出交易品种列表的变更。而客户端(在其回调中)希望通过调用服务器方法 GetActiveSymbols() 接收导出交易品种的新列表。因此客户端无法接收来自服务器的响应,因为客户端通过等待服务器响应继续执行回调。结果客户端将因为超时而崩溃。
      • UseSynchronizationContext = false  – 表示我们没有结合 GUI 以防止发生挂起情况。默认情况下,WCF 回调附加在父线程上。如果父线程具有 GUI,则有可能出现以下情形:回调等待其调用的方法完成,而方法无法完成,因为它在等待回调结束。这与之前的情形有些类似,虽然这是两种不同的东西。

      对于服务器情形,客户端同样部署有两个接口:IExportClientIDisposable:

       [CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Multiple,
              UseSynchronizationContext = false)]
          public class ExportClient : IExportClient, IDisposable
          {
      
      

      我们来描述服务变量:

              // 完整的服务地址
              private readonly String _ServiceAddress;
      
              // 服务对象
              private IExportService _ExportService;
      
              // 返回服务实例
              public IExportService Service
              {
                  get
                  {
                      return _ExportService;
                  
      }
              
      }
      
              // 返回通信通道
              public IClientChannel Channel
              {
                  get
                  {
                      return (IClientChannel)_ExportService;
                  
      }
              
      }
      
      

      现在,我们来为回调方法创建事件。这要求客户端应用程序能够订阅事件和获得有关客户端状态变更的通知。

              // 当报价到来时调用
              public event EventHandler<TickRecievedEventArgs> TickRecieved;
      
              // 当交易品种改变时调用
              public event EventHandler ActiveSymbolsChanged;
      
      

      还需为客户端定义 Open() 和 Close() 方法:

              public void Open()
              {
                  // 创建通道
                  var factory = new DuplexChannelFactory<IExportService>(
                      new InstanceContext(this),
                      new NetNamedPipeBinding());
      
                  // 创建服务通道
                  _ExportService = factory.CreateChannel(new EndpointAddress(_ServiceAddress));
      
                  IClientChannel channel = (IClientChannel)_ExportService;
                  channel.Open();
      
                  // 连接数据源
                  _ExportService.Subscribe();
              
      }
      
              public void Close()
              {
                  Dispose(true);
              
      }
      
              private void Dispose(bool disposing)
              {
                  try
                  {
                      // 退订数据源
                      _ExportService.Unsubscribe();
                      Channel.Close();
      
                  
      }
                  finally
                  {
                      _ExportService = null;
                  
      }
                  // ...
              
      }
      
      

      请注意,连接源以及从源断开连接是在客户端开启或关闭时调用,因此没有必要直接调用。

      现在,我们来编写客户端契约。它的实施会导致以下事件的生成:

              public void SendTick(string symbol, MqlTick tick)
              {
                  // 触发事件TickRecieved
              
      }
      
              public void ReportSymbolsChanged()
              {
                  // 触发事件ActiveSymbolsChanged        
              
      }
      
      

      最后,客户端的主要属性和方法定义如下:

      属性 描述
      Service 服务通信信道
      Channel 服务契约 IExportService 的实例 


      方法 描述
      Open() 连接至服务器
      Close() 从服务器断开连接

       

      事件 描述
      TickRecieved 接收到新报价时生成
      ActiveSymbolsChanged 活动交易品种列表变更时生成

       

      4. 两个 .NET 应用程序间的传送速度

      对我而言,衡量两个 .NET 应用程序间的传送速度十分有趣;事实上,它的吞吐量通过每秒价格跳动次数衡量。我编写了几个控制台应用程序来衡量服务性能:第一个应用程序用于服务器,另外一个用于客户端。我在服务器的 Main() 函数中添加了以下代码:

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

      我们可以看到,代码执行了十个吞吐量测量值。我在我的 Athlon 3000+ 上获得下面的十个测试结果:

      2600 ticks per second
      3400 ticks per second
      3300 ticks per second
      2500 ticks per second
      2500 ticks per second
      2500 ticks per second
      2400 ticks per second
      2500 ticks per second
      2500 ticks per second
      2500 ticks per second
      Average 2670,00 ticks per second
      
      

      每秒 2500 次价格跳动 – 我认为它足够为 100 个交易品种导出报价(当然,实际上,似乎没有人会打开这么多图表并附加 EA 交易程序)。此外,随着客户端增多,每个客户端的导出交易品种的最大数量将下降。

      5. 创建“层次”

      现在,是时候考虑如何将其连接至客户端了。我们来看一下在 MetaTrader 5 中第一次调用函数的情形:.NET 运行环境 (CLR) 加载至进程,应用程序域默认创建。有趣的是,CLR 在代码执行后并未卸载。

      从进程卸载 CLR 的唯一方法是将其终止(关闭客户端),这将迫使 Windows 清除所有进程资源。因此,我们可以创建我们的对象,这些对象将一直存在,直到它们被“垃圾回收器”销毁或应用程序域卸载。

      您可以说这看上去不错,但即使我们防止对象被“垃圾回收器”销毁,我们也无法从 MQL5 访问对象。幸运的是,这样的访问可以轻松实现。诀窍如下:对应于每个应用程序域都存在一个“垃圾回收器”句柄表(GC 句柄表),应用程序使用该表来追踪对象的使用期以及手动控制对象。

      应用程序通过使用类型System.Runtime.InteropServices.GCHandle 从表中添加和删除元素。我们要做的就是使用这样的描述符包装我们的对象,并且我们可以通过属性 GCHandle.Target 对其进行访问。因此,我们可以获得对对象 GCHandle 的引用,该对象位于句柄表中并保证不会被“垃圾回收器”移动或删除。由于通过描述符引用,包装的对象还会避免回收。

      现在到了我们实践检验理论时候了。为此,我们创建一个名为 QExpertWrapper.dll 的新 win32 dll,并将 CLR 支持、System.dllQExport.dllQexport.Service.dll 添加至结构引用。我们还需要创建一个辅助类 ServiceManaged 用于管理 – 执行编组,通过句柄等接收对象。

      ref class ServiceManaged
      {
              public:
                      static IntPtr CreateExportService(String^);
                      static void DestroyExportService(IntPtr);
                      static void RegisterSymbol(IntPtr, String^);
                      static void UnregisterSymbol(IntPtr, String^);
                      static void SendTick(IntPtr, String^, IntPtr);
      };
      
      

      让我们来看一看这些方法的实施。CreateExportService 方法创建服务,通过使用 GCHandle.Alloc 将其包装在 GCHandle 中并返回其引用。如果出现故障,将显示出错的 MessageBox 。我将它用于调试,我不确定是不是真有必要,将它留在这里是为了以防万一。

      IntPtr ServiceManaged::CreateExportService(String^ serverName)
      {
              try
              {
                      ExportService^ service = gcnew ExportService(serverName);
                      service->Open();
              
                      GCHandle handle = GCHandle::Alloc(service);
                      return GCHandle::ToIntPtr(handle);
              
      }
              catch (Exception^ ex)
              {
                      MessageBox::Show(ex->Message, "CreateExportService");
              
      }
      
      }
      
      

      DestroyExportService 方法获得指向服务的 GCHandle 的指针,从目标属性获得服务并调用其方法 Close()。通过调用其方法 Free() 来释放服务对象是很重要的。否则它将留在内存中,“垃圾回收器”不会删除它。

      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");
              
      }
      
      }
      
      

      RegisterSymbol 方法将交易品种添加至导出交易品种列表:

      void ServiceManaged::RegisterSymbol(IntPtr hService, String^ symbol)
      {
              try
              {
                      GCHandle handle = GCHandle::FromIntPtr(hService);
                      ExportService^ service = (ExportService^)handle.Target;
      
                      service->RegisterSymbol(symbol);
              
      }
              catch (Exception^ ex)
              {
                      MessageBox::Show(ex->Message, "RegisterSymbol");
              
      }
      
      }
      
      

      UnregisterSymbol 方法从交易品种列表删除交易品种:

      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");
              
      }
      
      }
      
      

      接下来是 SendTick 方法。我们可以看到,指针通过 Marshal 类转换成 MqlTick 结构。另一要点:catch 代码块中没有任何代码 – 这样做是为了在发生错误时避免一般价格跳动队列的滞后。

      void ServiceManaged::SendTick(IntPtr hService, String^ symbol, IntPtr hTick)
      {
              try
              {
                      GCHandle handle = GCHandle::FromIntPtr(hService);
                      ExportService^ service = (ExportService^)handle.Target;
              
                      MqlTick tick = (MqlTick)Marshal::PtrToStructure(hTick, MqlTick::typeid);
      
                      service->SendTick(symbol, tick);
              
      }
              catch (...)
              {
              
      }
      
      }
      
      

      接下来我们讨论函数的实施,这些函数将从我们的 ex5 程序中调用:

      #define _DLLAPI extern "C" __declspec(dllexport)
      
      // ---------------------------------------------------------------
      // 创建并打开服务 
      // 返回指针
      // ---------------------------------------------------------------
      _DLLAPI long long __stdcall CreateExportService(const wchar_t* serverName)
      {
              IntPtr hService = ServiceManaged::CreateExportService(gcnew String(serverName));
              
              return (long long)hService.ToPointer(); 
      
      }
      
      // ----------------------------------------- ----------------------
      // 关闭服务
      // ---------------------------------------------------------------
      _DLLAPI void __stdcall DestroyExportService(const long long hService)
      {
              ServiceManaged::DestroyExportService(IntPtr((HANDLE)hService));
      
      }
      
      // ---------------------------------------------------------------
      // 发送报价
      // ---------------------------------------------------------------
      _DLLAPI void __stdcall SendTick(const long long hService, const wchar_t* symbol, const HANDLE hTick)
      {
              ServiceManaged::SendTick(IntPtr((HANDLE)hService), gcnew String(symbol), IntPtr((HANDLE)hTick));
      
      }
      
      // ---------------------------------------------------------------
      // 注册输出的交易品种
      // ---------------------------------------------------------------
      _DLLAPI void __stdcall RegisterSymbol(const long long hService, const wchar_t* symbol)
      {
              ServiceManaged::RegisterSymbol(IntPtr((HANDLE)hService), gcnew String(symbol));
      
      }
      
      // ---------------------------------------------------------------
      // 将交易品种从输出交易品种列表中移除
      // ---------------------------------------------------------------
      _DLLAPI void __stdcall UnregisterSymbol(const long long hService, const wchar_t* symbol)
      {
              ServiceManaged::UnregisterSymbol(IntPtr((HANDLE)hService), gcnew String(symbol));
      
      }
      
      

      代码已准备就绪,现在我们需要对其进行编译和构建。在项目选项中,我们将输出目录指定为 “C:/Program Files/MetaTrader 5/MQL5/Libraries”。编译后,指定的文件夹中将出现三个库。

      mql5 程序仅使用其中一个库,即 QExportWrapper.dll,另外的两个库由它使用。由于这个原因,我们需要将库 Qexport.dll 和 Qexport.Service.dll 放在 MetaTrader 的根文件夹中。这不是很方便。

      解决方案是创建配置文件,并为库指定路径。我们在 MetaTrader 的根文件夹中创建名为 terminal.exe.config 的文件,并写下以下内容:

      
      <configuration>
         <runtime>
            <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
               <probing privatePath="mql5/libraries" />
            </assemblyBinding>
         </runtime>
      </configuration>
      
      

      好了,现在 CLR 将在文件夹中搜索我们指定的库。

       6. 以 MQL5 实施服务器部分

      最后,我们讨论以 mql5 进行的服务器部分编程。我们创建新文件 QService.mqh 并定义 QExpertWrapper.dll 的导入函数:

      #import "QExportWrapper.dll"
         long  CreateExportService(string);
         void DestroyExportService(long);
         void RegisterSymbol(long, string);
         void UnregisterSymbol(long, string);
         void SendTick(long, string, MqlTick&);
      #import
       
      
      

      MQL5 具有类,类是将所有逻辑囊括在内的理想功能,大幅简化了相应工作和对代码的理解。因此,我们来设计一个类,作为库方法的外壳。

      此外,为避免为每个交易品种创建服务的情形,我们可以对具有该名称的运行服务进行检查,并且我们将解决这种情况。提供此信息的理想方法是全局变量,原因如下:

      • 全局变量在客户端关闭后消失。服务亦是如此;
      • 我们可以提供使用服务的对象数量 Qservice。它仅当最后一个对象关闭后允许关闭物理服务。

      因此,我们创建类 Qservice:

      class QService
      {
         private:
            // 服务指针
            long hService;
            // 服务名称
            string serverName;
            // 服务的全局变量的名称
            string gvName;
            // 服务是否关闭的标识
            bool wasDestroyed;
            
            // 进入关键环节
            void EnterCriticalSection();
            // 退出关键环节
            void LeaveCriticalSection();
            
         public:
         
            QService();
            ~QService();
            
            // 打开服务
            void Create(const string);
            // 关闭服务
            void Close();
            // 发送报价
            void SendTick(const string, MqlTick&);
      };
      
      //--------------------------------------------------------------------
      QService::QService()
      {
         wasDestroyed = false;
      
      }
      
      //--------------------------------------------------------------------
      QService::~QService()
      {
         // 如果没有被销毁则关闭
         if (!wasDestroyed)
            Close();
      
      }
      
      //--------------------------------------------------------------------
      QService::Create(const string serviceName)
      {
         EnterCriticalSection();
         
         serverName = serviceName;
         
         bool exists = false;
         string name;
         
         // 用此名称检查活动的服务
         for (int i = 0; i < GlobalVariablesTotal(); i++)
         {
            name = GlobalVariableName(i);
            if (StringFind(name, "QService|" + serverName) == 0)
            {
               exists = true;
               break;
            
      }
         
      }
         
         if (!exists)   // 如果不存在
         {
            // 启动服务
            hService = CreateExportService(serverName);
            // 添加一个全局变量
            gvName = "QService|" + serverName + ">" + (string)hService;
            GlobalVariableTemp(gvName);
            GlobalVariableSet(gvName, 1);
         
      }
         else          // 服务存在
         {
            gvName = name;
            // 服务的句柄
            hService = (int)StringSubstr(gvName, StringFind(gvName, ">") + 1);
            // 通过此脚本来使用此服务
             
            GlobalVariableSet(gvName, NormalizeDouble(GlobalVariableGet(gvName), 0) + 1);
         
      }
         
         // 注册图表上的交易品种
         RegisterSymbol(hService, Symbol());
         
         LeaveCriticalSection();
      
      }
      
      //--------------------------------------------------------------------
      QService::Close()
      {
         EnterCriticalSection();
         
         // 通知此脚本不使用此服务
         // 
         GlobalVariableSet(gvName, NormalizeDouble(GlobalVariableGet(gvName), 0) - 1);
           
         // 如果没有任何脚本使用,关闭服务
         if (NormalizeDouble(GlobalVariableGet(gvName), 0) < 1.0)
         {
            GlobalVariableDel(gvName);
            DestroyExportService(hService);
         
      }  
         else UnregisterSymbol(hService, Symbol()); // 未注册的交易品种
          
         wasDestroyed = true;
         
         LeaveCriticalSection();
      
      }
      
      //--------------------------------------------------------------------
      QService::SendTick(const string symbol, MqlTick& tick)
      {
         if (!wasDestroyed)
            SendTick(hService, symbol, tick);
      
      }
      
      //--------------------------------------------------------------------
      QService::EnterCriticalSection()
      {
         while (GlobalVariableCheck("QService_CriticalSection") > 0)
            Sleep(1);
         GlobalVariableTemp("QService_CriticalSection");
      
      }
      
      //--------------------------------------------------------------------
      QService::LeaveCriticalSection()
      {
         GlobalVariableDel("QService_CriticalSection");
      
      }
      

      该类包含以下方法:

      方法 描述
      Create(const string) 启动服务
      Close() 关闭服务
      SendTick(const string, MqlTick&) 发送报价

       

      亦请注意,私有方法 EnterCriticalSection() 和 LeaveCriticalSection() 允许您运行它们之间的关键代码段。

      它可将我们从同时调用函数 Create() 以及为每个 QService 创建新服务的情形中解放出来。

      因此,我们已为使用服务对类进行描述,现在我们为报价广播编写“EA 交易”。选择“EA 交易”是因其具有处理所有到达价格跳动的可能性。

      //+------------------------------------------------------------------+
      //|                                                    QExporter.mq5 |
      //|                                             Copyright GF1D, 2010 |
      //|                                             garf1eldhome@mail.ru |
      //+------------------------------------------------------------------+
      #property copyright "GF1D, 2010"
      #property link      "garf1eldhome@mail.ru"
      #property version   "1.00"
      
      #include "QService.mqh"
      //--- 输入参数
      input string  ServerName = "mt5";
      
      QService* service;
      
      //+------------------------------------------------------------------+
      //| EA交易初始化函数                                                  |
      //+------------------------------------------------------------------+
      int OnInit()
      {
         service = new QService();
         service.Create(ServerName);
         return(0);
      
      }
      
      //+------------------------------------------------------------------+
      //| EA交易去初始化函数                                                 |
      //+------------------------------------------------------------------+
      void OnDeinit(const int reason)
      {
         service.Close();
         delete service;
         service = NULL;
      
      }
      
      //+------------------------------------------------------------------+
      //| EA的订单函数                                                          |
      //+------------------------------------------------------------------+
      void OnTick()
      {
         MqlTick tick;
         SymbolInfoTick(Symbol(), tick);
         
         service.SendTick(Symbol(), tick);
      
      }
      //+------------------------------------------------------------------+
      
      

      7. 测试 ex5 和 .NET 客户端之间的通信性能

      显而易见,如果报价从客户端直接到达,服务的总体性能将下降,因此我很有兴趣对此加以衡量。我肯定总体性能将会下降,因为编组和类型转换会带来不可避免的 CPU 时间损耗。

      为此,我编写了一个简单的脚本,和为第一个测试编写的脚本相同。Start() 函数如下所示:

         QService* serv = new QService();
         serv.Create("mt5");
      
         MqlTick tick;
         SymbolInfoTick("GBPUSD", tick);
       
         int total = 0;
         
         for(int c = 0; c < 10; c++)
         {
            int calls = 0;
            
            int ticks = GetTickCount();
      
            while(GetTickCount() - ticks < 1000)
            {
               for(int i = 0; i < 100; i++) serv.SendTick("GBPUSD", tick);
               calls++;
            
      }
            
            Print(calls * 100," calls per second");
            
            total += calls * 100;
         
      }
           
         Print("Average ", total / 10," calls per second");
      
         serv.Close();
         delete serv;
      
      

      我获得如下结果:

      1900  calls per second
      2400  calls per second
      2100  calls per second
      2300  calls per second
      2000  calls per second
      2100  calls per second
      2000  calls per second
      2100  calls per second
      2100  calls per second
      2100  calls per second
      Average  2110  calls per second
      
      

      2500 价格跳动/秒与 1900 价格跳动/秒。使用来自 MT5 的服务的代价是性能下降 25%,但无论如何,这一性能业已足够。有意思的是,我们发现可使用线程池和静态方法 System.Threading.ThreadPool.QueueUserWorkItem 来提高性能。

      使用此方法,我获得高达每秒 10000 次价格跳动的传送速度。但它在严苛测试中的表现并不稳定,因为“垃圾回收器”没有时间来删除对象,使得 MetaTrader 分配的内存快速增长而最后导致崩溃。但这种情况发生在与现实有很大差距的严苛测试中,因此使用线程池并不会带来危险。

       8. 实时测试

      我已经使用服务创建了一个价格跳动表的示例。项目已附于档案并命名为 WindowsClient。其运行结果如下所示:

      图 1. 含报价表的 WindowsClient 应用程序主窗口

      总结

      在本文中,我对导出报价至 .NET 应用程序的方法之一进行了说明。所有要求均已实施,现在这些就绪的类可用于您自己的应用程序中。唯一不太方便的是,需要附加脚本至每一必要图表。

      目前,我认为此问题可使用 MetaTrader 配置文件加以解决。从另一方面来说,如果您不需要全部的报价,您可以使用脚本来为必要交易品种广播报价。如您所知,市场深度广播甚至双向访问可使用同样的方法实现。

      对档案描述如下:

      Bin.rar – 具有就绪解决方案的档案。适用于想看看它是如何工作的用户。仍请注意,您的电脑需要安装 .NET Framework 3.5(也许版本 3.0 亦可)。

      Src.rar – 项目的完整源代码。要使用它,您需要 MetaEditor 和 Visual Studio 2008。

      QExportDemoProfile.rar – 附加脚本至 10 个图表的 Metatrader 配置文件,如图 1 所示。

      本文译自 MetaQuotes Software Corp. 撰写的俄文原文
      原文地址: https://www.mql5.com/ru/articles/27

      附加的文件 |

      bin.zip
      (30.69 KB)
      src.zip
      (157.77 KB)

       

       


      MyFxtop迈投(www.myfxtop.com)-靠谱的外汇跟单社区,免费跟随高手做交易!

       

      免责声明:本文系转载自网络,如有侵犯,请联系我们立即删除,另:本文仅代表作者个人观点,与迈投财经无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。

      著作权归作者所有。
      商业转载请联系作者获得授权,非商业转载请注明出处。

      風險提示

      MyFxtops邁投所列信息僅供參考,不構成投資建議,也不代表任何形式的推薦或者誘導行為。MyFxtops邁投非外匯經紀商,不接觸妳的任何資金。 MYFXTOPS不保證客戶盈利,不承擔任何責任。從事外彙和差價合約等金融產品的槓桿交易具有高風險,損失有可能超過本金,請量力而行,入市前需充分了解潛在的風險。過去的交易成績並不代表以後的交易成績。依據各地區法律法規,MyFxtops邁投不向中國大陸、美國、加拿大、朝鮮居民提供服務。

      邁投公眾號

      聯繫我們

      客服QQ:981617007
      Email: service@myfxtop.com

      MyFxtops 邁投