English Русский 中文 Español Deutsch 日本語 Português 한국어 Français Türkçe
Come Esportare Quotazioni da МetaTrader 5 ad Applicazioni .NET Utilizzando i Servizi WCF

Come Esportare Quotazioni da МetaTrader 5 ad Applicazioni .NET Utilizzando i Servizi WCF

MetaTrader 5Esempi | 15 dicembre 2021, 17:06
88 0
Alexander
Alexander

Introduzione

I programmatori che utilizzano il servizio DDE in MetaTrader 4 probabilmente hanno sentito che nella quinta versione non è più supportato. E non esiste una soluzione standard per l'esportazione delle quotazioni. Come soluzione a questo problema, gli sviluppatori MQL5 suggeriscono di utilizzare la propria dll, che la implementa. Quindi, se dobbiamo scrivere l'implementazione, facciamolo in modo intelligente!

Perché .NET?

Per me con la mia lunga esperienza di programmazione in .NET, sarebbe più sensato, interessante e semplice implementare l'esportazione delle quotazioni utilizzando questa piattaforma. Sfortunatamente, non c'è alcun supporto nativo di .NET in MQL5 nella quinta versione. Sono sicuro che gli sviluppatori hanno alcune ragioni per questo. Pertanto, utilizzeremo la dll win32 come wrapper per il supporto .NET.

Perché WCF?

La Windows Communication Foundation Technology (WCF) è stata scelta da me per diversi motivi: da un lato, è facile da estendere e adattare, dall'altro volevo verificarla con un duro lavoro. Inoltre, secondo Microsoft, WCF ha prestazioni leggermente superiori rispetto a .NET Remoting.

Requisiti di Sistema

Pensiamo a cosa vogliamo dal nostro sistema. Penso che ci siano due requisiti principali:

    1. Ovviamente bisogna esportare i tick, meglio utilizzando la struttura nativa MqlTick;
    2. È preferibile conoscere l'elenco dei simboli attualmente esportati.

    Iniziamo...

    1. Classi generali e contratti

    Prima di tutto, creiamo una nuova libreria di classi e la chiamiamo QExport.dll. Definiamo la struttura MqlTick come 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; }
        }
    

    Poi definiremo i contratti del servizio. Non mi piace usare le classi di configurazione e le classi proxy generate, quindi non incontrerai tali funzionalità qui.

    Definiamo il primo contratto server in base ai requisiti descritti sopra:

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

    Come notiamo, esiste uno schema standard di iscrizione e annullamento dell'iscrizione alle notifiche del server. Si riportano di seguito i brevi dettagli delle operazioni:

    OperazioneDescrizione
    Abbonati()Iscriviti all'esportazione di tick
    Annulla abbonamento()Annulla l'iscrizione all'esportazione di tick
    GetActiveSymbols()Restituisce l'elenco dei simboli esportati


    E le seguenti informazioni dovrebbero essere inviate al callback del cliente: la citazione stessa e la notifica sui cambiamenti dei lits dei simboli esportati. Definiamo le operazioni necessarie come "Operazioni One Way" per aumentare le prestazioni:

        [ServiceContract]
        public interface IExportClient
        {
            [OperationContract(IsOneWay = true)]
            void SendTick(String symbol, MqlTick tick);
    
            [OperationContract(IsOneWay = true)]
            void ReportSymbolsChanged();
        }
    
    OperazioneDescrizione
    SendTick(String, MqlTick)Invia tick
    ReportSymbolsChanged()Notifica al cliente le modifiche nell'elenco dei simboli esportati

    2. Implementazione del server

    Creiamo una nuova build con nome Qexport.Service.dll per il servizio con l'implementazione del contratto del server.

    Scegliamo il NetNamedPipesBinding per un'associazione, perché ha le prestazioni più elevate rispetto agli attacchi standard. Se abbiamo bisogno di trasmettere quotazioni, ad esempio su una rete, bisognerebbe usare il NetTcpBinding.

    Di seguito sono riportati alcuni dettagli dell'implementazione del contratto del server:

    La definizione di classe Prima di tutto, dovrebbe essere contrassegnato con l'attributo ServiceBehavior con i seguenti modificatori:

    • InstanceContextMode = InstanceContextMode.Single - fornire l'utilizzo di un'istanza del servizio per tutte le richieste elaborate, aumenterà le prestazioni della soluzione. Inoltre, avremo la possibilità di servire e gestire l'elenco dei simboli esportati;
    • ConcurrencyMode = ConcurrencyMode.Multiple - indica l'elaborazione parallela per tutte le richieste del client;
    • UseSynchronizationContext = false – significa che non ci colleghiamo al thread della GUI per prevenire situazioni di blocco. Non è necessario qui per il nostro compito, ma è necessario se vogliamo ospitare il servizio utilizzando le applicazioni Windows.
    • IncludeExceptionDetailInFaults = true – per includere i dettagli dell'eccezione all'oggetto FaultException quando passato al cliente.

    Lo stesso ExportService contiene due interfacce: IExportService, IDisposable. Il primo implementa tutte le funzioni di servizio, il secondo implementa il modello standard di rilascio delle risorse .NET.

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

    Descriviamo le variabili del servizio:

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

    Definiamo i metodi Open() e Close(), che aprono e chiudono il nostro servizio:

            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;
                }
    
                // ...
            }
    

    Successivamente, l'implementazione dei metodi 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();
            }
    

    Ora abbiamo bisogno di aggiungere metodi per inviare tick e per registrare ed eliminare i simboli esportati.

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

    Riassumiamo l'elenco delle principali funzioni del server (solo quelle di cui abbiamo bisogno):

    MetodiDescrizione
    Open()Esegue il server
    Close()Arresta il server
    RegisterSymbol(String)Aggiunge il simbolo all'elenco dei simboli esportati
    UnregisterSymbol (String)Elimina il simbolo dall'elenco dei simboli esportati
    GetActiveSymbols()Restituisce il numero di simboli esportati
    SendTick(String, MqlTick)Invia tick ai clienti

     

    3. Implementazione del cliente

    Abbiamo considerato il server, penso che sia chiaro, quindi è il momento di considerare il client. Creiamo il Qexport.Client.dll. Il contratto con il cliente sarà implementato lì. Innanzitutto, dovrebbe essere contrassegnato con l'attributo CallbackBehavior, che ne definisce il comportamento. Ha i seguenti modificatori:

      • ConcurrencyMode = ConcurrencyMode.Multiple - indica l'elaborazione parallela per tutti i callback e le risposte del server. Questo modificatore è molto importante. Immagina che il server voglia notificare al client le modifiche nell'elenco dei simboli esportati chiamando il callback ReportSymbolsChanged(). E il client (nella sua callback) vuole ricevere la nuova lista dei simboli esportati chiamando il metodo del server GetActiveSymbols(). Quindi risulta che il client non può ricevere risposta dal server perché sta procedendo alla richiamata con l'attesa della risposta del server. Di conseguenza il client cadrà a causa del timeout.
      • UseSynchronizationContext = false - specifica che non ci colleghiamo alla GUI per prevenire situazioni di blocco. Di default, i callback wcf sono allegati al thread padre. Se il thread padre ha una GUI, la situazione è possibile quando la richiamata attende il completamento del metodo da cui è stata chiamata, ma il metodo non può finire perché attende la fine della richiamata. È qualcosa di simile al caso precedente, anche se si tratta di due cose diverse.

      Per quanto riguarda il caso del server, anche il client implementa due interfacce: IExportClient and IDisposable:

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

      Descriviamo le variabili del servizio:

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

      Ora creeremo eventi per i nostri metodi di callback. È necessario che l'applicazione client sia in grado di iscriversi agli eventi e ricevere notifiche sui cambiamenti dello stato del client.

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

      Definire anche i metodi Open() e Close() per il client:

              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;
                  }
                  // ...
              }
      

      Nota che la connessione e la disconnessione dai feed vengono chiamate quando un client viene aperto o chiuso, quindi non è necessario chiamarli direttamente.

      E ora, scriviamo il contratto del cliente. La sua implementazione porta alla generazione dei seguenti eventi:

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

      Infine, le principali proprietà e modalità del client sono definite come segue:

      PropertyDescrizione
      ServizioCanale di comunicazione di servizio
      CanaleIstanza del contratto di servizio IExportService 


      MetodoDescrizione
      Open()Si connette al server
      Close()Si disconnette dal server

       

      EventoDescrizione
      TickRecievedGenerato dopo la ricezione del nuovo preventivo
      ActiveSymbolsChangedGenerato dopo le modifiche nell'elenco dei simboli attivi

       

      4. Velocità di trasferimento tra due applicazioni .NET

      È stato interessante per me misurare la velocità di trasferimento tra due applicazioni .NET, infatti è il throughput, che viene misurato in tick al secondo. Ho scritto diverse applicazioni console per misurare le prestazioni del servizio: la prima è per il server, la seconda è per il client. Ho scritto il seguente codice nella funzione Main() del server:

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

      Come si vede, il codice esegue dieci misurazioni del throughput.  Ho ottenuto i seguenti risultati del test sul mio 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 tick al secondo - Penso che sia sufficiente esportare quotazioni per 100 simboli (ovviamente, virtualmente, perché sembra che nessuno voglia aprire così tanti grafici e allegare esperti =)) Inoltre, con l'aumento del numero di clienti, il numero massimo di i simboli esportati per ogni cliente sono ridotti.

      5. Creare uno "strato"

      Ora è il momento di pensare a come collegarlo al terminale client. Vediamo cosa abbiamo alla prima chiamata della funzione in MetaTrader 5: l'ambiente di runtime .NET (CLR) viene caricato nel processo e il dominio dell'applicazione viene creato per impostazione predefinita. È interessante che non venga scaricato dopo l'esecuzione del codice.

      L'unico modo per scaricare CLR dal processo è terminarlo (chiudi il terminale client), che costringerà Windows a cancellare tutto le risorse del processo. Quindi, possiamo creare i nostri oggetti e esisteranno fino a che il dominio dell'applicazione non viene aggiunto o finché non viene distrutto da Garbage Collector.

      Puoi dire che sembra buono, ma anche se impediamo la distruzione dell'oggetto da parte di Garbage Collector, non possiamo essere in grado di accedere agli oggetti da MQL5. Fortunatamente, tale accesso può essere organizzato facilmente. Il trucco è il seguente: per ogni dominio applicativo esiste una tabella di handle di Garbage Collector (tabella di handle GC), che viene utilizzata dall'applicazione per tenere traccia della durata dell'oggetto e consente di gestirlo manualmente.

      L'applicazione aggiunge ed elimina elementi dalla tabella utilizzando il tipo System.Runtime.InteropServices.GCHandle. Tutto ciò di cui abbiamo bisogno è avvolgere il nostro oggetto con un tale descrittore e abbiamo accesso ad esso attraverso la proprietà GCHandle.Target. Quindi possiamo ottenere il riferimento all'oggetto GCHandle, che è nella tabella degli handle ed è garantito che non verrà spostato o eliminato da Garbage Collector. L'oggetto avvolto eviterà anche il riciclaggio a causa del riferimento per descrittore.

      Ora è il momento di testare la teoria nella pratica. Per farlo, creiamo una nuova dll win32 con nome QExpertWrapper.dll e aggiungiamo il supporto CLR, System.dll, QExport.dll, Qexport.Service.dll al riferimento di build. Inoltre, creiamo una classe ausiliaria ServiceManaged per scopi di gestione: per eseguire il marshalling, ricevere oggetti tramite handles, ecc.

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

      Consideriamo l'implementazione di questi metodi. Il metodo CreateExportService crea il servizio, lo avvolge in GCHandle utilizzando GCHandle.Alloc e ne restituisce il riferimento. Se qualcosa va storto, mostra un MessageBox con un errore. L'ho usato per il debug, quindi non sono sicuro che sia davvero necessario, ma l'ho lasciato qui per ogni evenienza.

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

      Il metodoDestroyExportService ottiene il puntatore al GCHandle del servizio, ottiene il servizio dalla proprietà Target e chiama il suo metodo Close(). È importante rilasciare l'oggetto di servizio chiamando il suo metodo Free(). Altrimenti rimarrà in memoria, il Garbage Collector non lo rimuoverà.

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

      Il metodo RegisterSymbol aggiunge un simbolo all'elenco dei simboli esportati:

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

      Il metodo UnregisterSymbolelimina un simbolo dall'elenco:

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

      E ora il metodo SendTick. Come si vede, il puntatore viene trasformato nella struttura MqlTick utilizzando la classe Marshal. Un altro punto: non c'è alcun codice nel blocco catch - è fatto per evitare i ritardi della coda di tick generale in caso di errore.

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

      Consideriamo l'implementazione delle funzioni, che verranno chiamate dai nostri programmi 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));
      }
      

      Il codice è pronto, ora dobbiamo compilarlo e costruirlo. Specifichiamo la directory di output come "C:\Program Files\MetaTrader 5\MQL5\Libraries" nelle opzioni del progetto. Dopo la compilazione appariranno tre librerie nella cartella specificata.

      Il programma mql5 ne utilizza solo uno, ovvero QExportWrapper.dll, altre due librerie vengono utilizzate da esso. Per questo motivo abbiamo bisogno di mettere le librerie Qexport.dll e Qexport.Service.dll nella cartella principale di MetaTrader. Non è conveniente.

      La soluzione è creare un file di configurazione e specificare il percorso per le librerie lì. Creiamo il file con nome terminal.exe.confignella cartella principale di MetaTrader e scriviamoci le seguenti stringhe:

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

      È pronto. Ora CLR cercherà le librerie nella cartella che abbiamo specificato.

       6. Implementazione della parte server in MQL5

      Infine, siamo giunti alla programmazione della parte server in mql5. Creiamo un nuovo file QService.mqh e definiamo le funzioni importate di 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
       
      

      È fantastico che mql5 abbia classi perché è una funzionalità ideale per incapsulare tutte le logiche all'interno, che semplifica notevolmente il lavoro e la comprensione del codice. Pertanto progettiamo una classe che sarà una shell per i metodi della libreria.

      Inoltre, per evitare la situazione con la creazione di un servizio per ogni simbolo, organizziamo il controllo del servizio funzionante con tale nome, e lo lavoreremo in tal caso. Un metodo ideale per fornire queste informazioni sono le variabili globali, per i seguenti motivi:

      • le variabili globali scompaiono dopo la chiusura del terminale client. Lo stesso è con il servizio;
      • possiamo servire il numero di oggetti Qservice, che utilizza il servizio. Consente di chiudere il servizio fisico solo dopo la chiusura dell'ultimo oggetto.

      Quindi, creiamo una classe 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 classe contiene i seguenti metodi:

      MetodoDescrizione
      Crea (stringa costante)Avvia il servizio
      Close()Chiude il servizio
      SendTick(const string, MqlTick&)Invia preventivo

       

      Si noti inoltre che i metodi privati EnterCriticalSection() e LeaveCriticalSection() consentono di eseguire le sezioni di codice critiche tra di essi.

      Ci solleverà dai casi del simultaneo chiamate della funzione Create() e creazione di nuovi servizi per ciascuno QService.

      Quindi, abbiamo descritto la classe per lavorare con il servizio, ora scriviamo un Expert Advisor per la trasmissione delle quotazioni. L'Expert Advisor è stato scelto per la sua possibilità di processare tutti i tick arrivati.

      //+------------------------------------------------------------------+
      //|                                                    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. Test delle prestazioni di comunicazione tra ex5 e client .NET

      È evidente che le prestazioni complessive del servizio diminuiranno se i preventivi arriveranno direttamente dal terminale del cliente, quindi mi interessa misurarlo. Ero sicuro che avrebbe dovuto diminuire a causa dell'inevitabile perdita di tempo della CPU per il marshalling e il typecast.

      A questo scopo ho scritto un semplice script che è lo stesso del primo test. La funzione Start() ha il seguente aspetto:

         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;
      

      Ho i seguenti risultati:

      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 tick/sec contro 1900 tick/sec. Il 25% è il prezzo che dovrebbe essere pagato per l'utilizzo dei servizi da MT5, ma comunque è sufficiente. È interessante notare che le prestazioni possono essere aumentate utilizzando il pool di thread e il metodo statico System.Threading.ThreadPool.QueueUserWorkItem.

      Usando questo metodo, ho ottenuto la velocità di trasferimento fino a 10000 tick al secondo. Ma il suo lavoro in un duro test è stato instabile a causa del fatto che il Garbage Collector non ha tempo per eliminare gli oggetti - di conseguenza la memoria, allocata da MetaTrader, cresce rapidamente e alla fine si blocca. Ma è stato un duro test, lontano dal reale, quindi non c'è niente di pericoloso nell'usare il pool di thread.

       8. Test in tempo reale

      Ho creato un esempio di tabella tick utilizzando il servizio. Il progetto è allegato nell'archivio e denominato WindowsClient. Il risultato del suo lavoro è presentato di seguito:

      Fig. 1. Finestra principale dell'applicazione WindowsClient con la tabella delle quotazioni

      Conclusione

      In questo articolo ho descritto uno dei metodi per esportare le quotazioni in applicazioni .NET. Tutto il necessario è stato implementato e ora abbiamo classi pronte che possono essere utilizzate nelle tue applicazioni. L'unica cosa che non è conveniente allegare script a ciascuno dei grafici necessari.

      Al momento penso che questo problema possa essere risolto utilizzando i profili MetaTrader. Dall'altro lato, se non hai bisogno di tutte le virgolette, puoi organizzarlo con uno script che trasmette le virgolette per i simboli necessari. Come capisci, la trasmissione di profondità di mercato o anche l'accesso bilaterale possono essere organizzati allo stesso modo.

      Descrizione degli archivi:

      Bin.rar - archivio con una soluzione pronta. Per gli utenti che vogliono vedere come funziona. Nota inoltre che .NET Framework 3.5 (forse funzionerà anche con la versione 3.0) dovrebbe essere installato sul tuo computer.

      Src.rar - codice sorgente completo del progetto. Per lavorare con esso avrai bisogno di MetaEditor e Visual Studio 2008.

      QExportDemoProfile.rar- Profilo Metatrader, che collega lo script a 10 grafici, come mostrato in Fig. 1.


      Tradotto dal russo da MetaQuotes Ltd.
      Articolo originale: https://www.mql5.com/ru/articles/27

      File allegati |
      bin.rar (33.23 KB)
      src.rar (137.41 KB)
      Implementazione Pratica di Filtri Digitali in MQL5 per Principianti Implementazione Pratica di Filtri Digitali in MQL5 per Principianti
      L'idea di filtrare il segnale digitale è stata ampiamente discussa negli argomenti del forum sulla costruzione di sistemi di trading. E sarebbe imprudente non creare un codice standard di filtri digitali in MQL5. In questo articolo l'autore descrive la trasformazione del semplice codice dell'indicatore SMA dal suo articolo "Indicatori personalizzati in MQL5 per principianti" in codice di filtro digitale più complicato e universale. Questo articolo è un seguito logico all'articolo precedente. Indica anche come sostituire il testo nel codice e come correggere gli errori di programmazione.
      Disegnare le Emissioni di Indicatori in MQL5 Disegnare le Emissioni di Indicatori in MQL5
      In questo articolo, prenderemo in considerazione l'emissione di indicatori: un nuovo approccio alla ricerca di mercato. Il calcolo dell'emissione si basa sull'intersezione di diversi indicatori: dopo ogni tick, compaiono sempre più punti con colori e forme differenti. Formano numerosi ammassi come nebulose, nuvole, tracce, righe, archi, ecc. Queste forme aiutano a rilevare le molle e le forze invisibili che influenzano il movimento dei prezzi di mercato.
      MQL5: Analisi ed Elaborazione dei Report della Commodity Futures Trading Commission (CFTC) in MetaTrader 5 MQL5: Analisi ed Elaborazione dei Report della Commodity Futures Trading Commission (CFTC) in MetaTrader 5
      In questo articolo, svilupperemo uno strumento per l'analisi dei report CFTC. Risolveremo il seguente problema: sviluppare un indicatore che consenta di utilizzare i dati del report CFTC direttamente dai file di dati forniti dalla Commissione senza un'elaborazione e una conversione intermedie. Inoltre, può essere utilizzato per i diversi scopi: tracciare i dati come indicatore, procedere con i dati negli altri indicatori, negli script per l'analisi automatizzata, negli Expert Advisor per l'uso nelle strategie di trading.
      Scambio di Dati tra Indicatori: È Facile Scambio di Dati tra Indicatori: È Facile
      Vogliamo creare un tale ambiente che fornisca l'accesso ai dati degli indicatori allegati a un grafico e che abbia le seguenti proprietà: assenza di copia dei dati; modifica minima del codice dei metodi disponibili, se è necessario utilizzarli; È preferibile il codice MQL (ovviamente dobbiamo usare DLL, ma useremo solo una dozzina di stringhe di codice C++). L'articolo descrive un metodo semplice per sviluppare un ambiente di programma per il terminale MetaTrader, che fornirebbe i mezzi per accedere ai buffer degli indicatori da altri programmi MQL.