
Connexus Observer (Teil 8): Hinzufügen eines Request Observer
Einführung
Dieser Artikel ist die Fortsetzung einer Reihe von Artikeln, in denen wir eine Bibliothek namens Connexus aufbauen werden. Im ersten Artikel haben wir die grundlegende Funktionsweise der WebRequest-Funktion verstanden, jeden ihrer Parameter kennengelernt und auch einen Beispielcode erstellt, der die Verwendung dieser Funktion und ihre Schwierigkeiten demonstriert. Im letzten Artikel haben wir die Client-Schicht erstellt, eine einfache und intuitive Klasse, die für das Senden von Anfragen, den Empfang eines Anfrageobjekts (CHttpRequest) und die Rückgabe einer Antwort (CHttpResponse) verantwortlich ist, die Informationen über die Anfrage enthält, wie z. B. Statuscode, Dauer, Textkörper und Antwort-Header. Außerdem haben wir die Klasse von der WebRequest-Funktion entkoppelt, um die Bibliothek flexibler zu machen, indem wir eine neue Schicht namens CHttpTransport geschaffen haben.
In diesem achten Artikel der Serie werden wir einen Observer in der Bibliothek verstehen und implementieren, um die Verwaltung von mehreren Anfragen durch den Client zu erleichtern. Los geht's!
Um Sie an den aktuellen Stand der Bibliothek zu erinnern, sehen Sie hier das aktuelle Diagramm:
Was ist ein Observer?
Stellen Sie sich den Observer (Beobachter) als den Freund vor, der halb versteckt aus der Ferne zusieht und alles mitbekommt, ohne sich einzumischen. In der Welt der Programmierung tut das Observer-Muster etwas sehr Ähnliches: Es ermöglicht bestimmten Objekten, „benachrichtigt“ zu werden, wenn sich etwas ändert, ohne dass sie genau wissen müssen, wer die Änderung verursacht hat. Es ist fast wie eine magische Berührung: Jemand bewegt ein Teil und derjenige, der es gerade braucht, weiß es schon. Dieses Muster ist einer der Klassiker, wenn es darum geht, das Räderwerk der Geschäftslogik und der Nutzeroberfläche nebeneinander laufen zu lassen. Auf diese Weise erhält das System eine gewisse Fluidität, da sich mehrere Teile automatisch an Ereignisse anpassen.
Die Idee wurde geboren, um eines dieser lästigen Probleme in sehr starren Systemen zu lösen, bei denen ein Objekt von einem anderen abhängt und an diesem „klebt“, ohne viel Spielraum zum Atmen zu haben. Die Lösung? Entkoppeln, es machte es einfacher. Bevor der Observer einen Namen und Nachnamen bekam, suchten die Programmierer bereits nach leichteren Systemen. Damals, im Jahr 1994, stellten **Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides** in ihrer Arbeit Entwurfsmuster (Buch): Elemente wiederverwendbarer objektorientierter Software (1994) den Observer als ideale Möglichkeit vor, mehrere Objekte bei Änderungen an einem einzelnen Objekt auf dem neuesten Stand zu halten, ohne die Ketten einer starken Bindung.
Warum das Observer-Muster verwenden?
Der Observer ist perfekt, wenn wir eine Entkopplung brauchen, wenn wir die Dinge unabhängiger voneinander machen wollen. Das Subjekt muss nicht wissen, wer es beobachtet, es muss einfach nur „Veränderung in Sicht“ schreien und weitermachen. Er kann auch für Echtzeit-Updates**,** nützlich sein. Systeme, die sofort aktualisiert werden müssen, wie z. B. interaktive Schnittstellen oder automatische Benachrichtigungen, sind mit dem Observer viel beweglicher.
Komponenten des Observer-Musters
- Thema: Dies ist der „Besitzer des Stücks“, dessen Zustand sich ändert und der die Observer über diese Änderungen informieren muss. Es führt eine Liste von Observer und verfügt über Methoden, um jemanden aus der Liste hinzuzufügen oder zu entfernen.
- Observer: Jeder Observer ist wie ein „Zuhörer“, der immer bereit ist, auf die Veränderungen des Subjekts zu reagieren. Es implementiert eine Aktualisierungsmethode, die das Subjekt jedes Mal aufruft, wenn eine Änderung eintritt.
Ich füge unten ein Diagramm hinzu, das zeigt, wie das Observer-Muster funktioniert:
- Hauptklassen
- Subject: Diese Klasse verwaltet eine Sammlung von Observer (observerCollection) und bietet Methoden zur Verwaltung dieser Observer. Seine Aufgabe ist es, die Observer zu benachrichtigen, wenn eine Zustandsänderung eintritt.
- Methoden:
- registerObserver(observer) : Fügt der Sammlung einen Observer hinzu.
- unregisterObserver(observer) : Entfernt einen Observer aus der Sammlung.
- notifyObservers() : Benachrichtigt alle Observer durch Aufruf der Methode update() jedes Observers in der observerCollection .
- Methoden:
- Observer: Dies ist eine Schnittstelle oder abstrakte Klasse, die die Methode update() definiert. Alle konkreten Observer-Klassen (Konkretisierungen von Observer ) müssen diese Methode implementieren, die aufgerufen wird, wenn das Subjekt Änderungen meldet.
- Subject: Diese Klasse verwaltet eine Sammlung von Observer (observerCollection) und bietet Methoden zur Verwaltung dieser Observer. Seine Aufgabe ist es, die Observer zu benachrichtigen, wenn eine Zustandsänderung eintritt.
- Konkrete Klassen
- ConcreteObserverA und ConcreteObserverB: Dies sind konkrete Implementierungen der Observer-Schnittstelle. Jede implementiert update(), die die spezifische Reaktion auf eine Änderung in Subject definiert.
- Beziehung zwischen Subjekt und Observer
- Das Subjekt verwaltet eine Liste von Observern und benachrichtigt sie, indem es observer.update() für jeden Observer in der Sammlung aufruft.
- Die konkreten Observer reagieren auf Änderungen, die im Subjekt auftreten, gemäß ihrer spezifischen Implementierung in update().
Wie kann dies in der Connexus-Bibliothek genutzt werden? Wir werden dieses Muster verwenden, um den Client-Code zu informieren, wenn eine Anfrage gesendet wurde, wenn eine Antwort empfangen wurde oder sogar wenn ein unerwarteter Fehler aufgetreten ist. Mit diesem Muster wird der Kunde darüber informiert, dass dies geschehen ist. Dies erleichtert die Verwendung der Bibliothek, da Bedingungen im Code vermieden werden, wie z. B. „wenn ein Fehler erzeugt wurde, dann tu dies“, „wenn eine Anfrage gestellt wurde, dann tu dies“, „wenn eine Antwort empfangen wurde, dann tu dies“.
Praktischer Code
Zunächst zeige ich ein Diagramm, das zeigt, wie wir dieses Muster in den Kontext der Bibliothek einfügen werden:
Lassen Sie uns besser verstehen, wie die Umsetzung aussehen wird.
- Beachten Sie, dass das Diagramm die gleiche Struktur hat wie das Referenzdiagramm
- Ich habe zwei Methoden hinzugefügt, auf die die Observer Zugriff haben werden:
- OnSend() → Wenn eine Anfrage gesendet wird.
- OnRecv() → Wenn eine Antwort eingegangen ist.
- IHttpObserver wird keine abstrakte Klasse sein, sondern eine Schnittstelle.
Erstellen der Schnittstelle IHttpClient
Zuerst erstellen wir die Schnittstelle IHttpClient unter dem Pfad <Connexus/Interface/IHttpClient.mqh> . Und wir definieren die beiden Benachrichtigungsfunktionen
//+------------------------------------------------------------------+ //| IHttpObserver.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ #include "../Core/HttpRequest.mqh" #include "../Core/HttpResponse.mqh" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ interface IHttpObserver { void OnSend(CHttpRequest *request); void OnRecv(CHttpResponse *response); }; //+------------------------------------------------------------------+
Erstellen der Liste der Observer in CHttpClient
Binden wir die Schnittstelle ein.
#include "../Interface/IHttpObserver.mqh"Erstellen wir nun das Array der Observer im privaten Bereich der Klasse. Denken Sie daran, dass dieses Array Zeiger speichern soll, also müssen wir den „*“ vor den Variablennamen setzen. Wir werden auch öffentliche Methoden zum Hinzufügen, Entfernen und Benachrichtigen aller Observer erstellen, die im Array gespeichert sind.
//+------------------------------------------------------------------+ //| HttpClient.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "HttpRequest.mqh" #include "HttpResponse.mqh" #include "../Constants/HttpMethod.mqh" #include "../Interface/IHttpTransport.mqh" #include "../Interface/IHttpObserver.mqh" #include "HttpTransport.mqh" //+------------------------------------------------------------------+ //| class : CHttpClient | //| | //| [PROPERTY] | //| Name : CHttpClient | //| Heritage : No heritage | //| Description : Class responsible for linking the request and | //| response object with the transport layer. | //| | //+------------------------------------------------------------------+ class CHttpClient { private: IHttpObserver *m_observers[]; // Array of observers public: //--- Observers void RegisterObserver(IHttpObserver *observer); void UnregisterObserver(IHttpObserver *observer); void OnSendNotifyObservers(CHttpRequest *request); void OnRecvNotifyObservers(CHttpResponse *response); }; //+------------------------------------------------------------------+ //| Add observer pointer to observer list | //+------------------------------------------------------------------+ void CHttpClient::RegisterObserver(IHttpObserver *observer) { int size = ArraySize(m_observers); ArrayResize(m_observers,size+1); m_observers[size] = observer; } //+------------------------------------------------------------------+ //| Remove observer pointer to observer list | //+------------------------------------------------------------------+ void CHttpClient::UnregisterObserver(IHttpObserver *observer) { int size = ArraySize(m_observers); for(int i=0;i<size;i++) { if(GetPointer(m_observers[i]) == GetPointer(observer)) { ArrayRemove(m_observers,i,1); break; } } } //+------------------------------------------------------------------+ //| Notifies observers that a request has been made | //+------------------------------------------------------------------+ void CHttpClient::OnSendNotifyObservers(CHttpRequest *request) { int size = ArraySize(m_observers); for(int i=0;i<size;i++) { m_observers[i].OnSend(request); } } //+------------------------------------------------------------------+ //| Notifies observers that a response has been received | //+------------------------------------------------------------------+ void CHttpClient::OnRecvNotifyObservers(CHttpResponse *response) { int size = ArraySize(m_observers); for(int i=0;i<size;i++) { m_observers[i].OnRecv(response); } } //+------------------------------------------------------------------+Schließlich rufen wir die Benachrichtigungsfunktion innerhalb der Funktion auf, die die Anfrage sendet, damit die Observer tatsächlich informiert werden:
//+------------------------------------------------------------------+ //| class : CHttpClient | //| | //| [PROPERTY] | //| Name : CHttpClient | //| Heritage : No heritage | //| Description : Class responsible for linking the request and | //| response object with the transport layer. | //| | //+------------------------------------------------------------------+ class CHttpClient { private: IHttpObserver *m_observers[]; // Array of observers public: //--- Basis function bool Send(CHttpRequest &request, CHttpResponse &response); }; //+------------------------------------------------------------------+ //| Basis function | //+------------------------------------------------------------------+ bool CHttpClient::Send(CHttpRequest &request, CHttpResponse &response) { //--- Request uchar body_request[]; request.Body().GetAsBinary(body_request); //--- Response uchar body_response[]; string headers_response; //--- Notify observer of request this.OnSendNotifyObservers(GetPointer(request)); //--- Send ulong start = GetMicrosecondCount(); int status_code = m_transport.Request(request.Method().GetMethodDescription(),request.Url().FullUrl(),request.Header().Serialize(),request.Timeout(),body_request,body_response,headers_response); ulong end = GetMicrosecondCount(); //--- Notify observer of response this.OnRecvNotifyObservers(GetPointer(response)); //--- Add data in Response response.Clear(); response.Duration((end-start)/1000); response.StatusCode() = status_code; response.Body().AddBinary(body_response); response.Header().Parse(headers_response); //--- Return is success return(response.StatusCode().IsSuccess()); } //+------------------------------------------------------------------+
Die Arbeit ist getan, und es ist einfacher als es scheint, wenn wir den Code schreiben, nicht wahr? Damit haben wir die gesamte Implementierung innerhalb der Bibliothek abgeschlossen. Wir müssen die Observer erstellen, d. h. die konkreten Klassen, die den IHttpObserver implementieren. Wir werden dies im nächsten Abschnitt, den Tests, tun.
Tests
Jetzt müssen wir nur noch die Bibliothek nutzen. Dazu erstelle ich eine neue Testdatei namens TestObserver.mq5 im Pfad <Experts/Connexus/Tests/TestObserver.mq5>. Wir importieren die Bibliothek und belassen nur OnInit().
//+------------------------------------------------------------------+ //| TestObserver.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ #include <Connexus/Core/HttpClient.mqh> //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Direkt unter dem Einbinden werde ich eine konkrete Klasse erstellen, die die IHttpClient-Schnittstelle implementiert und die Daten, die über die Bibliothek gesendet und empfangen wurden, auf der Terminal-Konsole ausgibt:
//+------------------------------------------------------------------+ //| TestObserver.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ #include <Connexus/Core/HttpClient.mqh> //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ class CMyObserver : public IHttpObserver { public: CMyObserver(void); ~CMyObserver(void); void OnSend(CHttpRequest *request); void OnRecv(CHttpResponse *response); }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CMyObserver::CMyObserver(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CMyObserver::~CMyObserver(void) { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CMyObserver::OnSend(CHttpRequest *request) { Print("-----------------------------------------------"); Print("Order sent notification received in CMyObserver"); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CMyObserver::OnRecv(CHttpResponse *response) { Print("-----------------------------------------------"); Print("Response notification received in CMyObserver"); } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Create objects CHttpRequest request; CHttpResponse response; CHttpClient client; CMyObserver *my_observer = new CMyObserver(); //--- Configure request request.Method() = HTTP_METHOD_GET; request.Url().Parse("https://httpbin.org/get"); //--- Adding observer client.RegisterObserver(my_observer); //--- Send client.Send(request,response); //--- Delete pointer delete my_observer; return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Wenn wir dies auf das Diagramm anwenden, erhalten wir dieses Ergebnis:
Dies zeigt, dass die Funktionen der Klasse CMyObserver innerhalb der Bibliothek aufgerufen wurden, dies ändert alles, wir vervollständigen die Bibliothek mit dem erreichten Hauptziel, dass sie flexibel ist.
Der interessanteste Teil ist, dass wir mehrere Observer in verschiedenen Teilen des Codes haben können, wenn wir einen EA haben, der in mehrere Klassen unterteilt ist, können wir jede dieser Klassen eine Implementierung von IHttpObserver erstellen lassen und das war's! Wir werden benachrichtigt, sobald die Anfrage abgeschickt oder eine Antwort eingegangen ist.
Mit diesen Ergänzungen der Observer ergibt sich nun das aktuelle Diagramm der Bibliothek:
Umstrukturierung der Verzeichnisse
Derzeit sieht das Verzeichnis aller Bibliotheksdateien wie folgt aus:
|--- Connexus |--- |--- Constants |--- |--- |--- HttpMethod.mqh |--- |--- |--- HttpStatusCode.mqh |--- |--- Core |--- |--- |--- HttpClient.mqh |--- |--- |--- HttpRequest.mqh |--- |--- |--- HttpResponse.mqh |--- |--- |--- HttpTransport.mqh |--- |--- Data |--- |--- |--- Json.mqh |--- |--- Header |--- |--- |--- HttpBody.mqh |--- |--- |--- HttpHeader.mqh |--- |--- Interface |--- |--- |--- IHttpObserver.mqh |--- |--- |--- IHttpTransport.mqh |--- |--- URL |--- |--- |--- QueryParam.mqh |--- |--- |--- URL.mqh
Wir nehmen zwei Anpassungen vor: Wir fügen die Dateien aus dem URL-Ordner in den Data-Ordner ein und benennen sie in Utils um, sodass beide Ordner, die Dateien mit ähnlichem Zweck enthalten, vereinfacht werden. Wir fügen auch den Ordner interfaces in den Ordner Core ein, da die Schnittstellen zum Kern der Bibliothek gehören. Am Ende sieht die Ordnerstruktur der Bibliothek wie folgt aus:
|--- Connexus |--- |--- Constants |--- |--- |--- HttpMethod.mqh |--- |--- |--- HttpStatusCode.mqh |--- |--- Core |--- |--- |--- Interface |--- |--- |--- |--- IHttpObserver.mqh |--- |--- |--- |--- IHttpTransport.mqh |--- |--- |--- HttpClient.mqh |--- |--- |--- HttpRequest.mqh |--- |--- |--- HttpResponse.mqh |--- |--- |--- HttpTransport.mqh |--- |--- Utils |--- |--- |--- Json.mqh |--- |--- |--- QueryParam.mqh |--- |--- |--- URL.mqh |--- |--- Header |--- |--- |--- HttpBody.mqh |--- |--- |--- HttpHeader.mqh
Umbenennung einiger Methoden
Wenn es darum geht, einen Code zu schreiben, der leicht zu verstehen, zu pflegen und zu verbessern ist, macht die Übernahme eines standardisierten Stils der Codierung den entscheidenden Unterschied. Ein einheitlicher Standard beim der Erstellung von Bibliotheken geht weit über die reine Ästhetik hinaus. Er schafft Klarheit, Vorhersehbarkeit und eine solide Grundlage für alle, die den Code heute oder in Zukunft verwenden oder mit ihm zusammenarbeiten. Dieser einheitliche Stil ist nicht nur eine Frage der Organisation, sondern eine Investition in die Qualität, die Robustheit und das gesunde Wachstum der Bibliothek im Laufe der Zeit. Und auch wenn es zunächst nur wie ein Detail aussieht, ist es am Ende der rote Faden, der den Code sicher und entwicklungsfähig macht.
Warum ist ein Standardstil unerlässlich?
- Konsistenz und Lesbarkeit: Gut strukturierter Code mit einem einheitlichen Stil macht das Lesen flüssiger und verständlicher für jeden Entwickler. Mit einem klar definierten Standard müssen die Mitarbeiter keine Zeit mit der Entschlüsselung von Abweichungen oder Inkonsistenzen verschwenden, sondern können sich auf das konzentrieren, was wirklich wichtig ist: die Logik des Codes. Aspekte wie Abstände, Einrückungen und Benennungen sind Details, die zusammen ein intuitiveres und unkompliziertes Erlebnis schaffen. Alles ist aufeinander abgestimmt, was die Navigation erleichtert und die durch unterschiedliche und unzusammenhängende Stile verursachten Stolpersteine reduziert.
- Leichte Wartung und Erweiterung: Bibliotheken bleiben selten konstant; neue Herausforderungen entstehen, und es ist ganz natürlich, dass sie angepasst werden müssen. Mit einem standardisierten Kodierungsstil wird die Wartung einfacher und weniger fehleranfällig. Das spart nicht nur Zeit bei der Fehlerbehebung, sondern erleichtert auch neuen Entwicklern das schnelle Verständnis des Codes und die effiziente Zusammenarbeit. Und natürlich ist eine Bibliothek, die von Anfang an gut strukturiert ist, viel leichter zu skalieren, da jede neue Funktion eine vorhersehbare und organisierte Umgebung vorfindet, in die sie integriert werden kann.
Dennoch sollten wir einige Standards im Code festlegen, vor allem bei der Benennung von Funktionen. Einige andere Normen wurden bereits angewandt, wie z. B.:
- Alle Klassen verwenden das Präfix „C“ vor dem Namen.
- Alle Schnittstellen verwenden das Präfix „I“ vor dem Namen.
- Private Variablen verwenden das Präfix „m_“.
- Methoden müssen immer mit Großbuchstaben beginnen
- ENUM-Werte müssen in Großbuchstaben geschrieben werden
Alle diese Standards wurden bereits während der Entwicklung der Bibliothek angewandt, aber wir werden weitere hinzufügen, wie zum Beispiel:
- Methoden zum Setzen/Lesen von Attributen einer Klasse müssen das Präfix Get oder Set verwenden.
- Methoden, die die Größe des Arrays zurückgeben, müssen den Namen „Size“ tragen.
- Methoden, die die Klassenattribute zurücksetzen, müssen den Namen „Clear“ tragen.
- Methoden zur Konvertierung in einen String müssen „ToString“ heißen.
- Vermeiden Sie Namensredundanz in Kontextklassen. Die Klasse CQueryParam hat zum Beispiel die Methode AddParam(), die keinen Sinn macht. Ideal wäre nur Add(), da wir uns bereits im Kontext der Parameter befinden.
Ich werde also nicht alle Methoden in der Bibliothek auflisten, die ich umbenennen werde, und ich werde auch nicht den Quellcode zur Verfügung stellen, da ich nicht die Implementierung der Methode ändere, sondern nur den Namen. Aber mit den Änderungen, werde ich ein Diagramm unten, dass alle Klassen in der Bibliothek mit den Namen der aktualisierten Methoden und ihre Beziehungen zeigt verlassen.
Schlussfolgerung
Mit diesem letzten Artikel schließen wir die Serie über die Erstellung der Connexus-Bibliothek ab, die zur Vereinfachung der HTTP-Kommunikation entwickelt wurde. Es war eine ganz schöne Reise: Wir gingen die Grundlagen durch, vertieften uns in fortgeschrittenere Design- und Code-Verfeinerungstechniken und erforschten das Observer-Muster, um Connexus die Reaktivität zu verleihen, die für dynamische und flexible Systeme unerlässlich ist. Wir haben dieses Muster in die Praxis umgesetzt, sodass verschiedene Teile der Anwendung automatisch auf Änderungen reagieren können, wodurch eine robuste und anpassungsfähige Struktur entsteht.
Zusätzlich zum Observer haben wir die gesamte Datei- und Ordnerarchitektur organisiert und den Code modular und intuitiv gestaltet. Wir haben auch Methoden umbenannt, um die Übersichtlichkeit zu erhöhen und die Verwendung der Bibliothek direkter und konsistenter zu machen - Details, die den Unterschied ausmachen, wenn es um sauberen Code und langfristige Wartung geht.
Connexus wurde entwickelt, um die HTTP-Integration so einfach und intuitiv wie möglich zu gestalten. Wir hoffen, dass wir in dieser Serie jeden wichtigen Punkt des Prozesses aufzeigen konnten und die Design-Entscheidungen, die dies möglich gemacht haben. Mit diesem letzten Artikel hoffe ich, dass Connexus nicht nur Ihre HTTP-Integrationen vereinfacht, sondern auch zu kontinuierlichen Verbesserungen anregt. Danke, dass Sie sich mit mir auf diese Reise begeben haben. Möge Connexus ein Verbündeter bei Ihren Projekten sein!
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/16377





- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.