Como exportar cotizaciones desde MetaTrader 5 a aplicaciones .NET usando los servicios de WCF

Alexander | 22 enero, 2014

Introducción

Los programadores que utilizan el servicio DDE en MetaTrader 4 han oído probablemente que en la quinta versión ya no está soportada. Y no hay una solución estándar para exportar cotizaciones. Como solución a este problema, los desarrolladores de MQL5 recomiendan usar nuestras propias dll para implementarlo. Por tanto, si tenemos que escribir la implementación, ¡hagámoslo de forma inteligente!

¿Por qué .NET?

Para mí, con mi dilatada experiencia como programador en .NET, sería más razonable, interesante y simple implementar la exportación de cotizaciones usando esta plataforma. Por desgracia, no hay soporte nativo de .NET en MQL5 en la quinta versión. Estoy seguro de que los desarrolladores tienen razones para ello. Por tanto, vamos a usar win32dll como envolvente para el soporte .NET.

¿Por qué WCF?

He elegido Windows Communication Foundation Technology (WCT) por varias razones: por un lado, es fácil de aplicar y adaptar y, por otro, quería probarla bajo condiciones complejas. Además, según Microsoft, WCF tiene un rendimiento algo superior comparado con .NET Remoting.


Requisitos del sistema

Vamos a pensar qué queremos para nuestro sistema. Creo que hay dos requisitos principales:

    1. Por supuesto, necesitamos exportar ticks y mejor que usemos la estructura nativa MqlTick;
    2. Es preferible conocer la lista de símbolos exportados actualmente.

    Vamos a empezar...


    1. Clases y contratos generales

    En primer lugar vamos a crear una nueva librería de clase y la llamaremos QExport.dll. Definimos la estructura MqlTick como un 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; }
        }
    

    Y luego definiremos los contratos del servicio. No me gusta usar clases de configuración y proxy-clases generadas, luego no incluiremos estas características aquí.

    Vamos a definir el primer contrato de servidos según los requisitos descritos anteriormente:

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

    Como vemos, hay un esquema estándar para suscribirse y anular la subscripción de las notificaciones de servidor. A continuación se describen los detalles resumidos de las operaciones:

    OperaciónDescripción
    Subscribe()Subscribirse a la exportación de ticks
    Unsubscribe()Anular la suscripción a la exportación de ticks
    GetActiveSymbols()Devuelve la lista de símbolos exportados


    Y la siguiente información debe enviarse para la rellamada: la propia cotización y la notificación sobre los cambios de las listas de símbolos exportados. Vamos a definir las operaciones requeridas como "operaciones one way" para incrementar el rendimiento:

        [ServiceContract]
        public interface IExportClient
        {
            [OperationContract(IsOneWay = true)]
            void SendTick(String symbol, MqlTick tick);
    
            [OperationContract(IsOneWay = true)]
            void ReportSymbolsChanged();
        }
    
    OperaciónDescripción
    SendTick(String, MqlTick)Envía el tick
    ReportSymbolsChanged()Notifica al cliente los cambios en la lista de símbolos exportados

    2. Implementación del servidor

    Vamos a crear una nueva construcción con el nombre Qexport.Service.dll para el servicio con la implementación del contrato de servidor.

    Vamos a elegir NetNamedPipesBinding para una consolidación ya que tiene el mayor rendimiento comparado con las consolidaciones estándar. Si necesitamos comunicar cotizaciones, en una red por ejemplo, debe usarse NetTcpBinding.

    Estos son algunos detalles de la implementación del contrato de servidor:

    La definición de la clase. En primer lugar debe marcarse con el atributo de ServiceBehavior con los siguientes modificadores:

    • InstanceContextMode = InstanceContextMode.Single- para facilitar el uso de una instancia de servicio para todas las solicitudes procesadas, aumentará el rendimiento de la solución. Además, tendremos la posibilidad de servir y gestionar la lista de símbolos exportados;
    • ConcurrencyMode = ConcurrencyMode.Multiple -significa el procesamiento paralelo de todas las solicitudes de clientes;
    • UseSynchronizationContext = false– significa que no adjuntamos al hilo de la GUI para evitar situaciones de parada. No es necesario aquí para nuestra tarea, pero es necesario si queremos alojar el servicio usando aplicaciones Windows.
    • IncludeExceptionDetailInFaults = true– para incluir los detalles de la excepción al objeto FaultExceptioncuando se pasa al cliente.

    El propio ExportService contiene dos interfaces: IExportService, IDisposable. La primera implementa todas las funciones de servicio y la segunda implementa el modelo estándar de la liberación de los recursos .NET.

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

    Vamos a describir las variables del servicio:

            // full address of service in format net.pipe://localhost/server_name
            private readonly String _ServiceAddress;
    
            // service host
            private ServiceHost _ExportHost;
    
            // active clients callbacks collection
            private Collection<IExportClient> _Clients = new Collection<IExportClient>();
    
            // active symbols list
            private List<String> _ActiveSymbols = new List<string>();
            
            // object for locking
            private object lockClients = new object();
    

    Vamos a definir los métodos Open() y Close() que abren y cierran nuestro servicio:

            public void Open()
            {
                _ExportHost = new ServiceHost(this);
    
                // point with service
                _ExportHost.AddServiceEndpoint(typeof(IExportService),  // contract
                    new NetNamedPipeBinding(),                          // binding
                    new Uri(_ServiceAddress));                          // address
    
                // remove the restriction of 16 requests in queue
                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
                {
                    // closing channel for each client
                    // ...
    
                    // closing host
                    _ExportHost.Close();
                }
                finally
                {
                    _ExportHost = null;
                }
    
                // ...
            }
    

    A continuación la implementación de los métodos IExportService:

            public void Subscribe()
            {
                // get the callback channel
                IExportClient cl = OperationContext.Current.GetCallbackChannel<IExportClient>();
                lock (lockClients)
                    _Clients.Add(cl);
            }
    
            public void Unsubscribe()
            {
                // get the callback chanell
                IExportClient cl = OperationContext.Current.GetCallbackChannel<IExportClient>();
                lock (lockClients)
                    _Clients.Remove(cl);
            }
    
            public String[] GetActiveSymbols()
            {
                return _ActiveSymbols.ToArray();
            }
    

    Ahora necesitamos añadir métodos para enviar ticks y para registrar y borrar los símbolos exportados.

         public void RegisterSymbol(String symbol)
            {
                if (!_ActiveSymbols.Contains(symbol))
                    _ActiveSymbols.Add(symbol);
    
                  // sending notification to all clients about changes in the list of active symbols
                  //...
            }
    
            public void UnregisterSymbol(String symbol)
            {
                _ActiveSymbols.Remove(symbol);
    
                 // sending notification to all clients about the changes in the list of active symbols
                 //...
            }
    
            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)
                        {
                            // it seems that connection with client has lost - we just remove the client
                            _Clients.RemoveAt(i);
                            i--;
                        }
            }
    

    Vamos a resumir la lista de principales funciones de servidor (solo las que necesitamos):

    MétodosDescripción
    Open()Ejecuta el servidor
    Close()Para el servidor
    RegisterSymbol(String)Añade el símbolo a la lista de símbolos exportados
    UnregisterSymbol(String)Borra el símbolo de la lista de símbolos exportados
    GetActiveSymbols()Devuelve el número de símbolos exportados
    SendTick(String, MqlTick)Envía ticks a los clientes

     

    3. Implementación del cliente

    Hemos considerado el servidor, creo que está claro, ahora es el momento de considerar el cliente. Vamos a construir Qexport.Client.dll. El contrato del cliente se implementará aquí. Primero, debe marcarse con el atributo CallbackBehavior, que define su comportamiento. Tiene los siguientes modificadores:

    En cuanto al servidor, el cliente también implementa dos interfaces: IExportClient and IDisposable:

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

    Vamos a describir las variables de servicio:

            // full service address
            private readonly String _ServiceAddress;
    
            // service object 
            private IExportService _ExportService;
    
            // Returns service instance
            public IExportService Service
            {
                get
                {
                    return _ExportService;
                }
            }
    
            // Returns communication channel
            public IClientChannel Channel
            {
                get
                {
                    return (IClientChannel)_ExportService;
                }
            }
    

    Ahora creamos eventos para los métodos de nuestras rellamadas. Es necesario para que la aplicación del cliente pueda suscribirse a los eventos y obtener notificaciones sobre los cambios del estado del cliente.

            // calls when tick received
            public event EventHandler<TickRecievedEventArgs> TickRecieved;
    
            // call when symbol list has changed
            public event EventHandler ActiveSymbolsChanged;
    

    También define los métodos Open() y Close() para el cliente:

            public void Open()
            {
                // creating channel factory
                var factory = new DuplexChannelFactory<IExportService>(
                    new InstanceContext(this),
                    new NetNamedPipeBinding());
    
                // creating server channel
                _ExportService = factory.CreateChannel(new EndpointAddress(_ServiceAddress));
    
                IClientChannel channel = (IClientChannel)_ExportService;
                channel.Open();
    
                // connecting to feeds
                _ExportService.Subscribe();
            }
    
            public void Close()
            {
                Dispose(true);
            }
    
            private void Dispose(bool disposing)
            {
                try
                {
                    // unsubscribe feeds
                    _ExportService.Unsubscribe();
                    Channel.Close();
    
                }
                finally
                {
                    _ExportService = null;
                }
                // ...
            }
    

    Observe que la conexión y desconexión de la alimentación se llama cuando se abre o se cierra un cliente, por lo que no es necesario llamarlos directamente.

    Y ahora, vamos a escribir el contrato del cliente. Su implementación lleva a la generación de los eventos siguientes:

            public void SendTick(string symbol, MqlTick tick)
            {
                // firing event TickRecieved
            }
    
            public void ReportSymbolsChanged()
            {
                // firing event ActiveSymbolsChanged        
            }
    

    Finalmente, se definen las principales propiedades y métodos del cliente de la siguiente forma:

    PropiedadDescripción
    ServicioCanal de comunicación del servicio
    CanalInstancia del contrato de servicio IExportService 


    MétodoDescripción
    Open()Se conecta al servidor
    Close()Se desconecta del servidor

     

    EventoDescripción
    TickRecievedGenerado después de recibir la nueva cotización
    ActiveSymbolsChangedGenerado después de los cambios en la lista de símbolos activos

     

    4. Trasferir la velocidad entre dos aplicaciones .NET

    Me resultó interesante medir la velocidad de transferencia entre dos aplicaciones .NET, de hecho, su rendimiento específico medido en ticks por segundo. Escribí varias aplicaciones para medir el rendimiento del servicio: la primera es para el servidor y la segunda para el cliente. Escribí el siguiente código en la función Main() del servidor:

                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();
    

    Como vemos, el código realiza diez medidas del rendimiento.  Tengo los siguientes resultados de las pruebas en mi 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 ticks por segundo, creo que es suficiente para exportar cotizaciones para 100 símbolos (por supuesto, virtualmente, porque parece que nadie quiere abrir tantos gráficos y adjuntar expertos =)). Además con el creciente número de clientes, el número máximo de símbolos exportados para cada cliente se reduce.


    5. Crear un "estrato"

    Ahora es el momento de pensar cómo conectarlo con el terminal de cliente. Vamos a ver lo que tenemos en la primera llamada de la función en MetaTrader 5: el entorno de ejecución de .NET (CLR) se carga al proceso y se crea el dominio de la aplicación por defecto. Nos interesa que no se descargue después de la ejecución del código.

    La única forma de descargar CLR del proceso es finalizándolo (cerrar el terminal de cliente), lo que forzará a Windows a liberar todos los recursos del proceso. Podemos pues crear nuestros objetos y estos existirán hasta que se descargue el dominio de la aplicación o hasta que sea destruido por el recogedor de basura.

    Puede parecer bueno, pero si evitamos la destrucción del objeto por el recogedor de basura, no podremos acceder a los objetos desde MQL5. Por suerte, dicho acceso puede organizarse fácilmente. El truco es el siguiente: para cada dominio de la aplicación hay una tabla de controladores del recogedor de basura (tabla de controladores del GC -Garbage Collector), que es usado por la aplicación para seguir la pista de la vida del objeto y permitir su gestión manual.

    La aplicación añade y borra elementos de la tabla usando el tipo System.Runtime.InteropServices.GCHandle. Todo lo que necesitamos es envolver nuestro objeto con dicho descriptor y tendremos acceso a él en toda la propiedad GCHandle.Target. De esta forma, podemos obtener la referencia al objeto GCHandle, que está en la tabla de controladores y es seguro que el recogedor de basura no lo moverá ni lo borrará. El objeto envuelto también evitará el reciclado, debido a la referencia del descriptor.

    Es el momento de probar la teoría en la práctica. Para hacer esto vamos a crear una nueva win32 dll con el nombre QExpertWrapper.dll y añadir el soporte CLR, System.dll, QExport.dll, Qexport.Service.dll a la referencia construida. También creamos una clase auxiliar ServiceManaged con finalidad de gestión - para llevar a cabo la ordenación, para recibir objetos por los controladores, etc.

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

    Vamos a ver la implementación de estos métodos. El método CreateExportService crea el servicio, lo envuelve dentro de GCHandle usando GCHandle. Lo ubica y devuelve su referencia. Si algo va mal muestra un MessageBox con un error. Lo he usado con finalidad de depuración, por lo que no estoy seguro de que sea realmente necesario, pero lo dejo ahí por si acaso.

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

    El método DestroyExportService lleva el puntero al GCHandle del servicio, obtiene el servicio de la propiedadTarget y llama a su método Close(). Es importante liberar el objeto del servicio llamando a su método Free(). De no ser así, se quedaría en la memoria, el recogedor de basura no lo eliminaría.

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

    El métodoRegisterSymbol añade un símbolo a la lista de símbolos exportados:

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

    El método UnregisterSymbol borra un símbolo de la lista:

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

    Y ahora el método SendTick. Como vemos, el puntero se ha transformado en la estructura MqlTick usando la clase Marshal. Otro punto: no hay ningún código en el bloque de captura, se hace para evitar los retrasos de la cola de tick general en caso de error.

    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 (...)
            {
            }
    }
    

    Vamos a ver la implementación de funciones que serán llamadas desde nuestros programas ex5:

    #define _DLLAPI extern "C" __declspec(dllexport)
    
    // ---------------------------------------------------------------
    // Creates and opens service 
    // Returns its pointer
    // ---------------------------------------------------------------
    _DLLAPI long long __stdcall CreateExportService(const wchar_t* serverName)
    {
            IntPtr hService = ServiceManaged::CreateExportService(gcnew String(serverName));
            
            return (long long)hService.ToPointer(); 
    }
    
    // ----------------------------------------- ----------------------
    // Closes service
    // ---------------------------------------------------------------
    _DLLAPI void __stdcall DestroyExportService(const long long hService)
    {
            ServiceManaged::DestroyExportService(IntPtr((HANDLE)hService));
    }
    
    // ---------------------------------------------------------------
    // Sends tick
    // ---------------------------------------------------------------
    _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));
    }
    
    // ---------------------------------------------------------------
    // Registers symbol to export
    // ---------------------------------------------------------------
    _DLLAPI void __stdcall RegisterSymbol(const long long hService, const wchar_t* symbol)
    {
            ServiceManaged::RegisterSymbol(IntPtr((HANDLE)hService), gcnew String(symbol));
    }
    
    // ---------------------------------------------------------------
    // Removes symbol from list of exported symbols
    // ---------------------------------------------------------------
    _DLLAPI void __stdcall UnregisterSymbol(const long long hService, const wchar_t* symbol)
    {
            ServiceManaged::UnregisterSymbol(IntPtr((HANDLE)hService), gcnew String(symbol));
    }
    

    El código está listo y ahora necesitamos compilarlo y construirlo. Vamos a especificar el directorio de salida en las opciones del proyecto como "C:\Program Files\MetaTrader 5\MQL5\Libraries". Después de la compilación aparecerán tres librerías en la carpeta especificada.

    El programa mql5 solo usa una de ellas, QExportWrapper.dll y las otras dos son usadas por esta. Por esta razón necesitamos poner las librerías Qexport.dll y Qexport.Service.dll en la carpeta raíz de MetaTrader. No es aconsejable.

    La solución es crear el archivo de configuración y especificar la ruta para las librerías aquí. Vamos a crear al archivo con el nombre terminal.exe.config en la carpeta raíz de MetaTrader y a escribir ahí los siguientes strings:

    <?xml version="1.0" encoding="UTF-8" ?>
    <configuration>
       <runtime>
          <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
             <probing privatePath="mql5\libraries" />
          </assemblyBinding>
       </runtime>
    </configuration>
    

    Está listo. Ahora, CLR buscará las librerías en la carpeta que hemos especificado.

     

    6. Implementación de la parte del servidor en MQL5

    Finalmente, hemos llegado a la programación de la parte del servidor en mql5. Vamos a crear un nuevo archivo QService.mqh y a definir las funciones importadas de 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
     
    

    Menos mal que mql5 tiene clases porque es una característica ideal para encapsular dentro toda la lógica que simplifica enormemente el trabajo y la comprensión del código. Por tanto, vamos a diseñar una clase que será una envolvente para los métodos de la librería.

    Además, para evitar tener que crear el servicio para cada símbolo, vamos a organizar la comprobación del servicio con dicho nombre y trabajaremos con él en dicho caso. Un método ideal para obtener esta información son las variables globales, por las siguientes razones:

    Vamos a crear una clase Qservice:

    class QService
    {
       private:
          // service pointer
          long hService;
          // service name
          string serverName;
          // name of the global variable of the service
          string gvName;
          // flag that indicates is service closed or not
          bool wasDestroyed;
          
          // enters the critical section
          void EnterCriticalSection();
          // leaves the critical section
          void LeaveCriticalSection();
          
       public:
       
          QService();
          ~QService();
          
          // opens service
          void Create(const string);
          // closes service
          void Close();
          // sends tick
          void SendTick(const string, MqlTick&);
    };
    
    //--------------------------------------------------------------------
    QService::QService()
    {
       wasDestroyed = false;
    }
    
    //--------------------------------------------------------------------
    QService::~QService()
    {
       // close if it hasn't been destroyed
       if (!wasDestroyed)
          Close();
    }
    
    //--------------------------------------------------------------------
    QService::Create(const string serviceName)
    {
       EnterCriticalSection();
       
       serverName = serviceName;
       
       bool exists = false;
       string name;
       
       // check for the active service with such name
       for (int i = 0; i < GlobalVariablesTotal(); i++)
       {
          name = GlobalVariableName(i);
          if (StringFind(name, "QService|" + serverName) == 0)
          {
             exists = true;
             break;
          }
       }
       
       if (!exists)   // if not exists
       {
          // starting service
          hService = CreateExportService(serverName);
          // adding a global variable
          gvName = "QService|" + serverName + ">" + (string)hService;
          GlobalVariableTemp(gvName);
          GlobalVariableSet(gvName, 1);
       }
       else          // the service is exists
       {
          gvName = name;
          // service handle
          hService = (int)StringSubstr(gvName, StringFind(gvName, ">") + 1);
          // notify the fact of using the service by this script
          // by increase of its counter
          GlobalVariableSet(gvName, NormalizeDouble(GlobalVariableGet(gvName), 0) + 1);
       }
       
       // register the chart symbol
       RegisterSymbol(hService, Symbol());
       
       LeaveCriticalSection();
    }
    
    //--------------------------------------------------------------------
    QService::Close()
    {
       EnterCriticalSection();
       
       // notifying that this script doen't uses the service
       // by decreasing of its counter
       GlobalVariableSet(gvName, NormalizeDouble(GlobalVariableGet(gvName), 0) - 1);
         
       // close service if there isn't any scripts that uses it
       if (NormalizeDouble(GlobalVariableGet(gvName), 0) < 1.0)
       {
          GlobalVariableDel(gvName);
          DestroyExportService(hService);
       }  
       else UnregisterSymbol(hService, Symbol()); // unregistering 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");
    }

    La clase contiene los siguientes métodos:

    MétodoDescripción
    Create(const string)Inicia el servicio
    Close()Cierra el servicio
    SendTick(const string, MqlTick&)Envía la cotización

     

    Observe también que los métodos privados EnterCriticalSection() y LeaveCriticalSection() le permiten ejecutar las secciones de código crítico en ellos.

    Nos evitará los casos de llamadas simultáneas a la función Create() y la creación de nuevos servicios para cada QService.

    Hemos descrito la clase para trabajar con el servicio, ahora vamos a escribir un asesor experto para la transmisión de las cotizaciones. Se ha elegido el asesor experto por su capacidad para procesar todos los ticks que han llegado.

    //+------------------------------------------------------------------+
    //|                                                    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 parameters
    input string  ServerName = "mt5";
    
    QService* service;
    
    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
    {
       service = new QService();
       service.Create(ServerName);
       return(0);
    }
    
    //+------------------------------------------------------------------+
    //| Expert deinitialization function                                 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
    {
       service.Close();
       delete service;
       service = NULL;
    }
    
    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
    {
       MqlTick tick;
       SymbolInfoTick(Symbol(), tick);
       
       service.SendTick(Symbol(), tick);
    }
    //+------------------------------------------------------------------+
    


    7. Probando el funcionamiento de la comunicación entre ex5 y el cliente .NET

    Es evidente que el rendimiento total del servicio disminuirá si las cotizaciones llegan directamente desde el terminal de cliente, luego me interesa medirlo. Estaba seguro de que iba a disminuir por la pérdida inevitable de tiempo de CPU para la organización y el encasillamiento.

    Con esta finalidad escribí un script simple que es el mismo que para la primera prueba. La función Start() es la siguiente:

       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;
    

    Tengo los siguientes resultados:

    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 ticks/seg vs 1.900 ticks/seg. 25% es el precio que debe pagarse por el uso de los servicios de MT5, pero de todas formas es suficiente. Es interesante ver que el rendimiento puede aumentarse usando el conjunto de hilos y el método estático System.Threading.ThreadPool.QueueUserWorkItem.

    Usando este método obtengo una velocidad de transferencia de hasta 10.000 ticks por segundo. Pero su funcionamiento en una prueba fue inestable debido al hecho de que el recogedor de basura no tenía tiempo de borrar objetos y, como consecuencia, la memoria asignada por MetaTrader 5 creció rápidamente y finalmente se colapsó. Pero fue una prueba dura, muy alejada de la realidad, por lo que no hay nada peligroso en usar el conjunto de hilos.


    8. Pruebas en tiempo real.

    He creado un ejemplo de tabla de ticks usando el servicio. El proyecto se adjunta al archivo y se le da el nombre de WindowsClient. El resultado de su funcionamiento se muestra a continuación:

    Fig 1. Ventana principal de la aplicación WindowsClient con la tabla de cotizaciones

    Conclusión

    En este artículo he descrito uno de los métodos de exportación de cotizaciones a las aplicaciones .NET. Todo lo necesario se ha implementado y ahora tenemos clases preparadas que pueden usarse en sus propias aplicaciones. Lo único que no es aconsejable es adjuntar scripts a cada uno de los gráficos necesarios.

    En el momento actual creo que este problema puede resolverse usando los perfiles de MetaTrader. Por otro lado, si no necesita todas las cotizaciones puede organizarlas con un script que las transmita a los símbolos necesarios. Como comprenderá, la transmisión de la profundidad del mercado o incluso un acceso desde dos lados puede organizarse del mismo modo.

    Descripción de los archivos:

    Bin.rar - archivo con una solución preparada. Para los usuarios que no quieren ver cómo funciona. Tenga en cuenta que .NET Framework 3.5 (puede que también funcione con la versión 3.0) debe instalarse en su ordenador.

    Src.rar - todo el código fuente del proyecto. Para trabajar con el necesitará MetaEditor y Visual Studio 2008.

    QExportDemoProfile.rar -  perfil de Metatrader que adjunta el script a 10 gráficos como se muestra en la Fig. 1.

    Traducido desde el ruso por MetaQuotes Software Corp.
    Artículo original: https://www.mql5.com/es/articles/27