English Русский 中文 Español 日本語 Português 한국어 Français Italiano Türkçe
Exportieren von Angeboten aus MetaTrader 5 in .NET-Anwendungen mithilfe von WCF-Services

Exportieren von Angeboten aus MetaTrader 5 in .NET-Anwendungen mithilfe von WCF-Services

MetaTrader 5Beispiele | 11 Februar 2016, 13:31
739 1
Alexander
Alexander

Einleitung

Programmierer, die den DDE-Service in MetaTrader 4 nutzen, haben wahrscheinlich gehört, dass dieser Service in der fünften Version nicht mehr unterstützt wird. Auch gibt es keine Standardlösung zum Exportieren von Angeboten. Als Lösung für dieses Problem schlagen die Entwickler von MQL5 vor, dass Sie Ihre eigene DLL nutzen, die dies umsetzt. Wenn wir die Umsetzung also erst schreiben müssen, dann machen wir das auf die intelligente Art!

Warum .NET?

Für mich als erfahrenen .NET-Programmierer war es sinnvoller, interessanter und einfacher, den Export von Angeboten mithilfe dieser Plattform umzusetzen. Leider gibt es in MQL5 keine native Unterstützung für .NET. Ich bin sicher, dass die Entwickler ihre Gründe dafür haben. Deshalb nutzen wir win32 dll als Zwischenlösung für die .NET-Unterstützung.

Warum WCF?

Ich habe mich aus mehreren Gründen für die Windows Communication Foundation-Technologie (WCF) entschieden: Sie ist einerseits einfach zu erweitern und anzupassen, andererseits wollte ich sie unter starker Auslastung ausprobieren. Außerdem ist WCF laut Microsoft etwas leistungsstärker als .NET Remoting.

Systemanforderungen

Überlegen wir uns, was wir von unserem System erwarten. Ich denke, es gibt zwei Grundanforderungen:

  1. Natürlich müssen wir Ticks exportieren, am besten mithilfe der nativen Struktur MqlTick;
  2. Die Liste aktuell exportierter Symbole sollte im Idealfall bekannt sein.

Legen wir los...

1. Allgemeine Klassen und Kontrakte

Erstellen wir zuallererst eine neue Klassenbibliothek und nennen diese QExport.dll. Wir definieren die MqlTick-Struktur als Datenkontrakt:

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

Anschließend definieren wir die Kontrakte des Services. Ich verwende ungern Konfigurationsklassen und generierte Proxy-Klassen, deshalb finden Sie hier auch keine solchen Funktionen.

Definieren wir also den ersten Serverkontrakt gemäß den oben angegebenen Anforderungen:

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

        [OperationContract]
        void Unsubscribe();

        [OperationContract]
        String[] GetActiveSymbols();
    }

Wie wir sehen können, gibt es ein Standardschema zum Abonnieren und Abbestellen von Serverbenachrichtigungen. Im Folgenden sehen Sie eine kurze Beschreibung der Operationen:

OperationBeschreibung
Subscribe()Abonnieren von Tick-Exporten
Unsubscribe()Abbestellen von Tick-Exporten
GetActiveSymbols()Ausgeben der Liste exportierter Symbole

Die folgenden Informationen sollten an den Client-Callback gesendet werden: das Angebot selbst und die Benachrichtigung über Veränderungen der Liste exportierter Symbole. Definieren wir die erforderlichen Operationen als "Unidirektionale Operationen", um die Leistung zu steigern:

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

        [OperationContract(IsOneWay = true)]
        void ReportSymbolsChanged();
    }
OperationBeschreibung
SendTick(String, MqlTick)Sendet Tick
ReportSymbolsChanged()Benachrichtigung des Clients über Veränderungen in der Liste exportierter Symbole

2. Serverumsetzung

Erstellen wir ein neues Build mit dem Namen Qexport.Service.dll für den Service mit der Umsetzung des Serverkontrakts.

Als Anbindung wählen wir NetNamesPipesBinding, da sie im Vergleich mit Standardanbindungen die größte Leistung bietet. Falls wir Angebote übertragen müssen, beispielsweise über ein Netzwerk, sollte NetTcpBinding verwendet werden.

Hier sehen Sie einige Details über die Umsetzung des Serverkontrakts:

Die Klassendefinition. Zuallererst muss sie mit dem Attribut ServiceBehavior mit den folgenden Modifikatoren gekennzeichnet werden:

  • InstanceContextMode = InstanceContextMode.Single. Dies ermöglicht die Nutzung einer Service-Instanz für alle verarbeiteten Anfragen und erhöht die Leistung der Lösung. Zusätzlich erhalten wir die Möglichkeit, die Liste exportierter Symbole zu steuern und zu verwalten;
  • ConcurrencyMode = ConcurrencyMode.Multiplefür die parallele Verarbeitung aller Anfragen des Clients;
  • UseSynchronizationContext = false bedeutet, dass wir uns nicht an den GUI-Thread anbinden, um ein Aufhängen zu vermeiden. Dies ist für unsere Aufgabe hier nicht notwendig, wird aber benötigt, wenn wir den Service mithilfe von Windows-Anwendungen hosten wollen.
  • IncludeExceptionDetailInFaults = true zum Einbinden der Ausnahmedetails zum Objekt FaultException bei der Übergabe an den Client.

Der ExportService selbst enthält zwei Schnittstellen: IExportService, IDisposable. Die erste setzt alle Funktionen des Services um, die zweite setzt das Standardmodell der Freigabe von .NET-Ressourcen um.

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

Beschreiben wir die Variablen des Services:

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

Definieren wir die Open()- und Close()-Methoden, die unseren Service öffnen und schließen: 

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

            // ...
        }

Als Nächstes die Umsetzung der IExportService-Methoden: 

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

Nun müssen wir Methoden zum Senden von Ticks und zum Registrieren und Löschen der exportierten Symbole hinzufügen. 

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

Fassen wir die Liste der wichtigsten Serverfunktionen (nur solche, die wir brauchen) zusammen:

MethodenBeschreibung
Open()Startet den Server
Close()Stoppt den Server
RegisterSymbol(String)Fügt ein Symbol zur Liste exportierter Symbole hinzu
UnregisterSymbol(String)Löscht ein Symbol aus der Liste exportierter Symbole
GetActiveSymbols()Gibt die Anzahl exportierter Symbole aus
SendTick(String, MqlTick)Sendet Tick an Clients

 

3. Umsetzung des Clients

Da wir nun auf den Server eingegangen sind, ist es an der Zeit, auf den Client einzugehen. Erstellen wir die Qexport.Client.dll. Darin wird der Client-Kontrakt umgesetzt. Zuerst sollte er mit dem Attribut CallbackBehavior gekennzeichnet werden, das sein Verhalten festlegt. Er hat die folgenden Modifikatoren:

  • ConcurrencyMode = ConcurrencyMode.Multiple bedeutet die parallele Verarbeitung aller Callbacks und Serverantworten. Dieser Modifikator ist sehr wichtig. Stellen Sie sich vor, der Server will den Client über Veränderungen in der Liste exportierter Symbole benachrichtigen, indem er den Callback ReportSymbolsChanged() aufruft. Und der Client will (in seinem Callback) die neue Liste exportierter Symbole erhalten, indem er die Servermethode GetActiveSymbols() aufruft. Es stellt sich heraus, dass der Client keine Antwort vom Server erhalten kann, weil er ein Callback verarbeitet, der auf die Antwort des Servers wartet. Der Client erhält somit ein Timeout.
  • UseSynchronizationContext = false legt fest, dass wir uns nicht an die GUI anbinden, um ein Aufhängen zu vermeiden. Standardmäßig sind wcf-Callbacks an den übergeordneten Thread angebunden. Wenn der übergeordnete Thread eine GUI hat, kann es zu der Situation kommen, dass ein Callback auf die Fertigstellung der Methode wartet, von der er aufgerufen wurde, die Methode allerdings nicht fertiggestellt werden kann, weil sie auf das Ende des Callbacks wartet. Es besteht eine gewisse Ähnlichkeit zum vorher genannten Fall, obwohl dies zwei verschiedene Dinge sind.

Wie der Server setzt auch der Client zwei Schnittstellen um: IExportClient und IDisposable: 

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

Beschreiben wir die Variablen des Services:

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

  Jetzt erstellen wir Ereignisse für unsere Callback-Methoden. Die Client-Anwendung muss die Ereignisse abonnieren und Benachrichtigungen über Veränderungen des Zustands des Clients erhalten können. 

        // calls when tick received
        public event EventHandler<TickRecievedEventArgs> TickRecieved;

        // call when symbol list has changed
        public event EventHandler ActiveSymbolsChanged;

Wir bestimmen auch die Open()- und Close()-Methoden für den 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;
            }
            // ...
        }

Beachten Sie, dass die Verbindung mit und Trennung von Feeds aufgerufen wird, wenn ein Client geöffnet oder geschlossen wird. Deshalb ist es nicht notwendig, sie direkt aufzurufen.

Und nun schreiben wir den Kontrakt des Clients. Dessen Umsetzung führt zur Generierung der folgenden Ereignisse: 

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

        public void ReportSymbolsChanged()
        {
            // firing event ActiveSymbolsChanged        
        }

Letztendlich sind die wichtigsten Eigenschaften und Methoden des Clients definiert, wie folgt:

EigenschaftBeschreibung
ServiceKommunikationskanal des Services
ChannelInstanz des Servicekontrakts IExportService 


MethodeBeschreibung
Open()Verbindung zum Server
Close()Trennung vom Server

 

EreignisBeschreibung
TickRecievedGenerierung nach Eingang eines neuen Angebots
ActiveSymbolsChangedGenerierung nach Veränderung der Liste aktiver Symbole

 

4. Übertragungsgeschwindigkeit zwischen zwei .NET-Anwendungen

Ich fand es interessant, die Übertragungsgeschwindigkeit zwischen zwei .NET-Anwendungen, genauer gesagt, den Datendurchsatz zu messen, der in Ticks pro Sekunde gemessen wird. Ich habe einige Konsolenanwendungen geschrieben, um die Leistung des Services zu messen: eine für den Server, die zweite für den Client. Ich habe den folgenden Code in die Main()-Funktion des Servers geschrieben: 

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

Wie wir sehen, führt der Code zehn Messungen des Durchsatzes durch.  Auf meinem Athlon 3000+ habe ich die folgenden Testergebnisse erhalten: 

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 pro Sekunde – ich glaube, das reicht zum Exportieren von Angeboten für 100 Symbole (natürlich nur in der Theorie, denn wer will schon so viele Diagramme öffnen und Experts anhängen? =)). Zudem wird mit steigender Anzahl an Clients die maximale Anzahl an exportierten Symbolen pro Client reduziert.

5. Erstellen einer "Zwischenschicht"

Nun müssen wir uns überlegen, wie wir das alles mit dem Client Terminal verbinden. Sehen wir uns an, was wir beim ersten Aufruf der Funktion in MetaTrader 5 erhalten: Die .NET-Laufzeitumgebung (CLR) wird in den Prozess geladen und die Anwendungsdomain wird standardmäßig erstellt. Interessant ist, dass dies alles nach der Ausführung des Codes nicht wieder ausgeladen wird.

Die einzige Möglichkeit, CLR aus dem Prozess auszuladen, ist, den Prozess (das Client Terminal) zu schließen. Dadurch wird Windows gezwungen, alle Prozessressourcen zu bereinigen. Also können wir unsere Objekte erstellen und sie bestehen, bis die Anwendungsdomain ausgeladen wird oder bis sie durch den Garbage Collector zerstört wird.

Sie könnten anmerken, dass das ja alles schön und gut ist, aber auch wenn wir die Zerstörung von Objekten durch den Garbage Collector verhindern, können wir dennoch nicht über MQL5 auf die Objekte zugreifen. Glücklicherweise kann ein solcher Zugriff einfach ermöglicht werden. Der Trick ist folgender: Für jede Anwendungsdomain gibt es eine Tabelle von Garbage-Collector-Handles (GC handle table), die die Anwendung nutzt, um die Lebensdauer des Objekts zu überwachen, und seine manuelle Verwaltung ermöglicht.

Die Anwendung fügt Elemente hinzu und löscht Elemente mithilfe des Typen System.Runtime.InteropServices.GCHandle. Wir müssen unser Objekt lediglich mit einem solchen Deskriptor verpacken und haben über die Eigenschaft GCHandle.Target Zugriff darauf. Somit können wir einen Verweis auf das Objekt GCHandle erhalten, das sich in der Tabelle der Handles befindet, und es ist garantiert, dass es durch den Garbage Collector nicht verschoben oder gelöscht wird. Durch den Deskriptor-Verweis wird auch die Wiederverwertung des verpackten Objekts vermieden.

Nun ist es an der Zeit, die Theorie in der Praxis zu erproben. Dazu erstellen wir eine neue win32 dll mit dem Namen QExpertWrapper.dll und erweitern sie um die CLR-Unterstützung durch Hinzufügen von System.dll, QExport.dll, Qexport.Service.dll zum Build-Verweis. Zu Verwaltungszwecken erstellen wir außerdem die Hilfsklasse ServiceManaged für die Durchführung von Marshalling, zum Abrufen von Objekten durch Handles usw. 

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

Betrachten wir die Umsetzung dieser Methoden. Die Methode CreateExportService erstellt den Service, verpackt ihn mithilfe von GCHandle.Alloc in GCHandle und gibt seinen Verweis aus. Falls etwas schiefgeht, wird eine Fehlermeldung angezeigt. Ich habe dies zu Debugging-Zwecken genutzt und bin nicht sicher, ob es wirklich notwendig ist, aber für den Fall des Falles habe ich es nicht entfernt.

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

Die MethodeDestroyExportService erhält den Pointer zum GCHandle des Services, erhält den Service von der Eigenschaft Target und ruft dessen Methode Close() auf. Es ist wichtig, das Serviceobjekt durch den Aufruf der Methode Free() freizugeben. Andernfalls verbleibt es im Speicher und wird nicht durch den Garbage Collector entfernt. 

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

Die Methode RegisterSymbol fügt ein Symbol zur Liste exportierter Symbole hinzu: 

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

Die Methode UnregisterSymbol löscht ein Symbol aus der Liste: 

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

Und nun die Methode SendTick. Wie wir sehen können, wird der Pointer mithilfe der Marshal-Klasse in die MqlTick-Struktur umgewandelt. Ein weiterer Punkt: Im catch-Block befindet sich kein Code. Das dient dazu, im Fall von Fehlern Verzögerungen in der allgemeinen Tick-Warteschlange zu vermeiden. 

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

Betrachten wir die Umsetzung der Funktionen, die aus unseren ex5-Programmen aufgerufen werden: 

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

Nun ist der Code fertig und wir müssen ihn kompilieren und aufbauen. Definieren wir das Ausgabeverzeichnis in den Projektoptionen als "C:\Programme\MetaTrader 5\MQL5\Libraries". Nach der Kompilierung erscheinen drei Bibliotheken im angegebenen Ordner.

Da das mql5-Programm nur eine davon nutzt, nämlich QExportWrapper.dll, werden die beiden anderen Bibliotheken von ihr verwendet. Aus diesem Grund müssen wir die Bibliotheken Qexport.dll und Qexport.Service.dll ins Stammverzeichnis von MetaTrader legen. Das ist ziemlich unpraktisch.

Die Lösung ist, eine Konfigurationsdatei zu erstellen und den Pfad für die Bibliotheken dort festzulegen. Erstellen wir eine Datei mit dem Namen terminal.exe.config im Stammverzeichnis von MetaTrader und schreiben die folgenden Strings hinein: 

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

Fertig. Nun sucht CLR im von uns angegebenen Ordner nach den Bibliotheken.

 6. Umsetzung des Serverteils in MQL5

Nun kommen wir endlich zur Programmierung des Serverteils in mql5. Erstellen wir eine neue Datei mit dem Namen QService.mqh und legen die importierten Funktionen von QExpertWrapper.dll fest:

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

Es ist großartig, dass mql5 über Klassen verfügt, da dies eine ideale Funktion zum Kapseln sämtlicher Logik darstellt, was die Arbeit mit und das Verständnis von Codes wesentlich erleichtert. Erstellen wir also eine Klasse, die als Hülle für die Bibliotheksmethoden dienen wird.

Um Situationen zu vermeiden, in denen ein Service für jedes Symbol erstellt wird, richten wir außerdem die Überprüfung des aktiven Services mit diesem Namen ein und arbeiten in diesem Fall damit. Eine ideale Methode zum Sichern dieser Informationen sind globale Variablen, und zwar aus folgenden Gründen:

  • globale Variablen verschwinden nach dem Schließen des Client Terminals. Das gilt auch für den Service;
  • wir können die Anzahl der Qservice-Objekte, die diesen Service nutzen, sichern. Dies ermöglicht ein Schließen des physischen Service erst nach dem Schließen des letzten Objekts.

Erstellen wir also die Klasse 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");
}

Die Klasse beinhaltet die folgenden Methoden:

MethodeBeschreibung
Create(const string)Startet den Service
Close()Schließt den Service
SendTick(const string, MqlTick&)Sendet Angebot

 

Beachten Sie auch, dass Sie mithilfe der privaten Methoden EnterCriticalSection() und LeaveCriticalSection() die kritischen Codeabschnitte zwischen ihnen ausführen können.

Dies erspart uns Situationen, in denen die Funktion Create() gleichzeitig mit der Erstellung neuer Services für jeden QService aufgerufen wird.

Nun, da wir die Klasse für die Arbeit mit dem Service beschrieben haben, schreiben wir einen Expert Advisor für die Übertragung von Angeboten. Der Expert Advisor wurde aufgrund seiner Fähigkeit, alle eingehenden Ticks zu verarbeiten, gewählt. 

//+------------------------------------------------------------------+
//|                                                    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. Testen der Kommunikationsleistung zwischen ex5 und .NET-Client

Es ist selbsterklärend, dass die Gesamtleistung des Services sinken wird, wenn die Angebote direkt aus dem Client Terminal eingehen. Deshalb war ich daran interessiert, dies zu messen. Ich war mir sicher, dass die Leistung aufgrund des unvermeidlichen CPU-Zeitverlustes für Marshalling und Type-Casting sinken würde.

Zu diesem Zweck habe ich ein simples Script geschrieben, das identisch mit dem für den ersten Test ist. Die Start()-Funktion sieht so aus: 

   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;

Ich habe die folgenden Ergebnisse erhalten: 

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/Sek. im Vergleich mit 1900 Ticks/Sek. 25 %. Das ist der Preis, der für die Nutzung von Services aus MT5 gezahlt werden muss, aber das ist auf jeden Fall genug. Es sollte beachtet werden, dass die Leistung durch die Verwendung des Thread-Pools und der statischen Methode System.Threading.ThreadPool.QueueUserWorkItem erhöht werden kann.

Mithilfe dieser Methode habe ich Übertragungsgeschwindigkeiten von bis zu 10000 Ticks pro Sekunde erreicht. Doch die Arbeit dieser Methode in einer schwierigen Testumgebung war instabil, weil der Garbage Collector keine Zeit hat, Objekte zu löschen. Somit wird dem MetaTrader immer mehr Speicher zugeordnet, bis er abstürzt. Doch es handelte sich um eine schwierige Testumgebung, die weit von der Realität entfernt war. Die Nutzung des Thread-Pools ist also völlig ungefährlich.

 8. Echtzeit-Tests

Ich habe eine musterhafte Tabelle von Ticks, die den Service nutzt, erstellt. Das Projekt ist im Archiv angehängt und heißt WindowsClient. Das Ergebnis seiner Arbeit wird im Folgenden vorgestellt:

Abb. 1 Hauptfenster der Anwendung WindowsClient mit Angebotstabelle

Fazit

In diesem Beitrag habe ich eine der Methoden zum Exportieren von Angeboten in .NET-Anwendungen beschrieben. Alles Notwendige wurde umgesetzt und nun verfügen wir über fertige Klassen, die in Ihren eigenen Anwendungen genutzt werden können. Das einzig Unpraktische ist, dass Scripts an jedes erforderliche Diagramm angehängt werden müssen.

Ich sehe aktuell nur eine Lösung, nämlich die Verwendung von MetaTrader-Profilen. Andererseits – wenn Sie nicht alle Angebote brauchen, können Sie alles mit einem Script organisieren, das Angebote für die erforderlichen Symbole überträgt. Wie Sie verstehen, können auf die gleiche Weise die Übertragung der Markttiefe und sogar der zweiseitige Zugriff organisiert werden.

Beschreibung der Archive:

Bin.rar – Archiv mit vorgefertigter Lösung. Für Benutzer, die sehen möchten, wie es funktioniert. Beachten Sie dennoch, dass .NET Framework 3.5 auf Ihrem Computer installiert sein muss (möglicherweise funktioniert es auch mit Version 3.0).

Scr.rar – vollständiger Quellcode des Projekts. Um damit arbeiten zu können, benötigen Sie MetaEditor und Visual Studio 2008.

QExportDemoProfile.rar – MetaTrader-Profil, das Scripts an 10 Diagramme anhängt, wie in Abb. 1 vorgeführt.


Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/27

Beigefügte Dateien |
bin.rar (33.23 KB)
src.rar (137.41 KB)
Letzte Kommentare | Zur Diskussion im Händlerforum (1)
MAWO
MAWO | 14 Juni 2019 in 13:12

Hallo,


ich hbabe deine fertige Datei getestet, In MT5 sieht alles gut aus. Nur es kommen keine Daten im WinClient an. 







Genetische Algorithmen - Leicht gemacht! Genetische Algorithmen - Leicht gemacht!
Der Verfasser behandelt in diesem Beitrag evolution&auml;re Berechnungen mit Hilfe eines pers&ouml;nlich entwickelten, genetischen Algorithmus. Er zeigt die Funktionsweise dieses Algorithmus anhand von Beispielen und gibt praktische Empfehlungen f&uuml;r seine Verwendung.
Zeichnen von Indikatoremissionen in MQL5 Zeichnen von Indikatoremissionen in MQL5
In diesem Beitrag behandeln wir die Emission von Indikatoren, eine neuen Herangehensweise an die Marktforschung. Die Berechnung von Emissionen basiert auf den Schnittpunkten verschiedener Indikatoren: Nach jedem Tick erscheinen immer mehr Punkte mit unterschiedlichen Farben und Formen. Sie formen zahlreiche Cluster wie Nebel, Wolken, Bahnen, Linien, Bögen usw. Diese Formen helfen uns beim Finden der unsichtbaren Kräfte, die die Bewegung von Marktpreisen beeinflussen.
MQL für Anfänger: Wie man Objektklassen entwirft und baut MQL für Anfänger: Wie man Objektklassen entwirft und baut
Durch Erstellung eines Beispielprogramms von visuellen Designs, zeigen wir, wie man in MQL5 Klassen entwirft und baut. Dieser Beitrag richtet sich an Programmierer im Anfängerstadium, die auf MT5 Anwendung arbeiten. Wir schlagen hier eine einfache und leicht zu verstehende Technologie zur Erzeugung von Klassen vor, ohne dass man dazu tief in den Theorie des Objekt-orientieren Progammierens einsteigen muss.
Sehr einfach: Der Datenaustausch zwischen Indikatoren Sehr einfach: Der Datenaustausch zwischen Indikatoren
Wir möchten eine Umgebung erschaffen, die den Zugriff auf Daten von Indikatoren ermöglicht, die an ein Diagramm angehängt sind, und deshalb die folgenden Eigenschaften aufweist: kein Kopieren von Daten; minimale Veränderung des Codes verfügbarer Methoden, wenn wir sie nutzen müssen; MQL-Code ist zu bevorzugen (natürlich müssen wir DLL nutzen, doch wir verwenden nur ein Dutzend Strings C++-Code). Dieser Beitrag beschreibt eine einfache Methode zur Entwicklung einer Programmumgebung für das MetaTrader-Terminal, die eine Zugriffsmöglichkeit auf Indikatorpuffer aus anderen MQL-Programmen bietet.