English Русский 日本語
preview
Websockets für MetaTrader 5: Asynchrone Client-Verbindungen mit dem Windows-API

Websockets für MetaTrader 5: Asynchrone Client-Verbindungen mit dem Windows-API

MetaTrader 5Beispiele |
58 9
Francis Dube
Francis Dube

Einführung

Der Artikel, „Websockets für MetaTrader 5 — Unter Verwendung der Windows API“, veranschaulicht die Nutzung der Windows-API für die Implementierung eines Websocket-Clients in MetaTrader 5-Anwendungen. Die dort vorgestellte Implementierung war durch ihre synchrone Betriebsart eingeschränkt.

In diesem Artikel befassen wir uns mit der Anwendung der Windows-API, um einen Websocket-Client für MetaTrader 5-Programme zu erstellen, mit dem Ziel, asynchrone Client-Funktionalität zu erreichen. Eine praktische Methode zur Verwirklichung dieses Ziels besteht in der Erstellung einer nutzerdefinierten dynamisch verknüpften Bibliothek (DLL), die Funktionen exportiert, die für die Integration in MetaTrader 5-Anwendungen geeignet sind.

Dementsprechend wird in diesem Artikel der Entwicklungsprozess der DLL erörtert und anschließend eine Demonstration ihrer Anwendung anhand eines MetaTrader 5-Programmbeispiels vorgestellt.


WinHTTP asynchroner Modus

Es gibt zwei Voraussetzungen für einen asynchronen Betrieb innerhalb der WinHTTP-Bibliothek, wie sie in der Dokumentation beschrieben sind. Erstens muss beim Aufruf der Funktion WinHTTPOpen das Sitzungshandle entweder mit dem Flag WINHTTP_FLAG_ASYNC oder WINHTTP_FLAG_SECURE_DEFAULTS konfiguriert werden.

// Set hSession
hSession = WinHttpOpen(L"MyApp", WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC);
if (hSession == NULL)
        ErrorCode = ERROR_INVALID_HANDLE;
// Return error code
return ErrorCode;

Nach der Einrichtung eines gültigen Sitzungshandles müssen die Nutzer eine Callback-Funktion registrieren, um Benachrichtigungen über verschiedene Ereignisse zu erhalten, die mit bestimmten WinHTTP-Funktionsaufrufen verbunden sind. Diese Status-Callback-Funktion wird über den Fortschritt von asynchronen Operationen über Benachrichtigungsflags informiert.

Die Registrierung der Callback-Funktion erfolgt über die Funktion WinHttpSetStatusCallback, die auch die Angabe von Benachrichtigungsflags erlaubt, die der Callback verwalten soll. Die Nutzer können wählen, ob sie ein umfassendes Paket von Benachrichtigungen oder nur eine begrenzte Anzahl davon abonnieren möchten. Außerdem können für Session-, Request- und Websocket-Handles jeweils eigene Callback-Funktionen vorgesehen werden.

Es ist wichtig zu beachten, dass die Registrierung einer Callback-Funktion unmittelbar nach der Erstellung des Sitzungshandles nicht zwingend erforderlich ist; die WinHttpSetStatusCallback-Funktion kann für jedes gültige HINTERNET-Handle zu jedem Zeitpunkt vor oder während der Initialisierung der Websocket-Verbindung aufgerufen werden.

if (!WinHttpSetOption(hWebSocket, WINHTTP_OPTION_CONTEXT_VALUE, (LPVOID)this, sizeof(this)))
{
        // Handle error
        ErrorCode = GetLastError();
        return ErrorCode;
}

if (WinHttpSetStatusCallback(hWebSocket, WebSocketCallback, WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS, 0) == WINHTTP_INVALID_STATUS_CALLBACK)
{
        ErrorCode = GetLastError();
        return ErrorCode;
}

Die Signatur der nutzerdefinierten Callback-Funktion enthält einen Parameter, der auf eine nutzerdefinierte Datenstruktur verweist, die gemeinhin als Kontextwert bezeichnet wird. Dieser Mechanismus erleichtert die Übertragung von Daten aus der Callback-Funktion.

Der Kontextwert muss vor der Registrierung der Callback-Funktion durch einen Aufruf der Funktion WinHttpSetOption mit dem Optionsflag WINHTTP_OPTION_CONTEXT_VALUE angegeben werden. Es ist wichtig zu erwähnen, dass bei empirischen Tests Schwierigkeiten auftraten, einen registrierten Kontextwert mit dieser Methode zuverlässig abzurufen.

Obwohl die Möglichkeit eines Implementierungsfehlers nicht völlig ausgeschlossen werden kann, machte ein konsequentes Scheitern die Einführung einer globalen Variable als Alternative erforderlich, ein Detail, das in der nachfolgenden Diskussion der DLL-Implementierung behandelt wird.

Ein entscheidender Gesichtspunkt bei der nutzerdefinierten Callback-Funktion ist schließlich die Anforderung der Threadsicherheit. Da es sich bei der vorliegenden Arbeit jedoch um die Erstellung einer DLL zur Verwendung in der MetaTrader 5-Umgebung handelt, kann diese Einschränkung gelockert werden. Dies liegt daran, dass MetaTrader 5-Programme im Allgemeinen in einem einzigen Thread ausgeführt werden. Während der DLL-Code im Thread-Pool des Ladeprozesses ausgeführt wird, ist bei MetaTrader 5-Programmen nur ein einziger Thread aktiv.

void WebSocketCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength)


Implementierung der DLL

Die DLL wird in der Visual Studio-Umgebung unter Verwendung der Programmiersprache C++ erstellt. Dieser Prozess erfordert die Installation des Arbeitsaufwands für die C++-Desktop-Entwicklung in Visual Studio zusammen mit dem Windows 10 oder Windows 11 Software Development Kit (SDK). Das Windows SDK ist eine Voraussetzung, da es die WinHTTP-Bibliotheksdatei (.lib) bereitstellt, mit der die DLL während der Kompilierung verknüpft wird. Die resultierende DLL besteht aus mindestens drei grundlegenden Komponenten.

Die erste ist eine Klasse, die die wesentlichen WinHTTP-Websocket-Client-Funktionen kapselt. Die zweite ist eine einzelne Callback-Funktion, die neben einer globalen Variablen arbeitet und die Manipulation einer Websocket-Verbindung sowohl innerhalb des Geltungsbereichs der Callback-Funktion als auch von außen ermöglicht. Die dritte Komponente besteht aus einer Reihe von vereinfachten Funktions-Wrappern, die von der DLL zur Verwendung in MetaTrader 5-Programmen bereitgestellt werden. Die Implementierung beginnt mit dem in der Header-Datei asyncwebsocketclient.h definierten Code.

Diese Header-Datei beginnt mit der Deklaration der Klasse WebSocketClient, wobei jede Instanz eine individuelle Client-Verbindung darstellt.

// WebSocket client class
class WebSocketClient {
private:
        // Application session handle to use with this connection
        HINTERNET hSession;
        // Windows connect handle
        HINTERNET hConnect;
        // The initial HTTP request handle to start the WebSocket handshake
        HINTERNET hRequest;
        // Windows WebSocket handle
        HINTERNET hWebSocket;
        //initialization flag
        DWORD initialized;
        //sent bytes
        DWORD bytesTX;
        //last error code
        DWORD ErrorCode;
        //last completed websocket operation as indicated by callback function
        DWORD completed_websocket_operation;
        //internal queue of frames sent from a server
        std::queue<Frame>* frames;
        //client state;
        ENUM_WEBSOCKET_STATE status;
        //sets an hSession handle
        DWORD Initialize(VOID);
        // reset state of object
        /*
         reset_error: boolean flag indicating whether to rest the
         internal error buffers.
        */
        VOID  Reset(bool reset_error = true);
        
public:
        //constructor(s)
        WebSocketClient(VOID);
        WebSocketClient(const WebSocketClient&) = delete;
        WebSocketClient(WebSocketClient&&) = delete;
        WebSocketClient& operator=(const WebSocketClient&) = delete;
        WebSocketClient& operator=(WebSocketClient&&) = delete;
        //destructor
        ~WebSocketClient(VOID);
        //received bytes;
        DWORD bytesRX;
        // receive buffer
        std::vector<BYTE> rxBuffer;
        // received frame type;
        WINHTTP_WEB_SOCKET_BUFFER_TYPE rxBufferType;
        // Get the winhttp websocket handle
        /*
        return: returns the hWebSocket handle which is used to 
        identify a websocket connection instance
        */
        HINTERNET WebSocketHandle(VOID);

        // Connect to a server
        /* 
           hsession: HINTERNET session handle
           host: is the url
           port: prefered port number to use
           secure: 0 is false, non-zero is true
           return: DWORD error code, 0 indicates success
           and non-zero for failure
        */
        DWORD Connect(const WCHAR* host, const INTERNET_PORT port, const DWORD secure);
        
        // Send data to the WebSocket server
        /*
           bufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of the frame type
           pBuffer: pointer to the data to be sent
           dwLength: size of pBuffer data
           return: DWORD error code, 0 indicates success
           and non-zero for failure
        */
        DWORD Send(WINHTTP_WEB_SOCKET_BUFFER_TYPE bufferType, void* pBuffer, DWORD dwLength);

        // Close the connection to the server
        /*
           status: WINHTTP_WEB_SOCKET_CLOSE_STATUS enumeration of the close notification to be sent
           reason: character string of extra data sent with the close notification
           return: DWORD error code, 0 indicates success
           and non-zero for failure
        */
        DWORD Close(WINHTTP_WEB_SOCKET_CLOSE_STATUS status, CHAR* reason = NULL);

        // Retrieve the close status sent by a server
        /*
           pusStatus: pointer to a close status code that will be filled upon return.
           pvReason: pointer to a buffer that will receive a close reason
           dwReasonLength: The length of the pvReason buffer,
           pdwReasonLengthConsumed:The number of bytes consumed. If pvReason is NULL and dwReasonLength is 0,
       pdwReasonLengthConsumed will contain the size of the buffer that needs to be allocated
       by the calling application.
           return: DWORD error code, 0 indicates success
           and non-zero for failure
        */
        DWORD QueryCloseStatus(USHORT* pusStatus, PVOID pvReason, DWORD dwReasonLength, DWORD* pdwReasonLengthConsumed);

        // read from the server
        /*
           bufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of the frame type
           pBuffer: pointer to the data to be sent
           pLength: size of pBuffer
           bytesRead: pointer to number bytes read from the server
           pBufferType: pointer to type of frame sent from the server
           return: DWORD error code, 0 indicates success
           and non-zero for failure
        */
        DWORD Receive(PVOID pBuffer, DWORD pLength, DWORD* bytesRead, WINHTTP_WEB_SOCKET_BUFFER_TYPE* pBufferType);

        // Check client state
        /*
           return: ENUM_WEBSOCKET_STATE enumeration
        */
        ENUM_WEBSOCKET_STATE Status(VOID);

        // get frames cached in the internal queue
        /*
           pBuffer: User supplied container to which data is written to
           pLength: size of pBuffer
           pBufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of frame type 
        */
        VOID Read(BYTE* pBuffer, DWORD pLength, WINHTTP_WEB_SOCKET_BUFFER_TYPE* pBufferType);

        // get bytes received
        /*
           return: Size of most recently cached frame sent from a server
        */
        DWORD ReadAvailable(VOID);

        // get the last error
        /*
           return: returns the last error code
        */
        DWORD LastError(VOID);

        // activate callback function
        /*
           return: DWORD error code, 0 indicates success
           and non-zero for failure
        */
        DWORD EnableCallBack(VOID);
        // set error 
        /*
           message: Error description to be captured
           errorcode: new user defined error code
        */
        VOID SetError(const DWORD errorcode);
         
        // get the last completed operation
        /*
          returns: DWORD constant of last websocket operation
        */
        DWORD LastOperation(VOID);

        //deinitialize the session handle and free up resources
        VOID Free(VOID);

        //the following methods define handlers meant to be triggered by the callback function//
        
        // on error 
        /*
           result: pointer to WINHTTP_ASYNC_RESULT structure that 
           encapsulates the specific event that triggered the error
        */
        VOID OnError(const WINHTTP_ASYNC_RESULT* result);

        // read completion handler
        /*
           read: Number of bytes of data successfully read from the server
           buffertype: type of frame read-in.
           Called when successfull read is completed
        */
        VOID OnReadComplete(const DWORD read, const WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype);

        // websocket close handler
        /*
           Handles the a successfull close request
        */
        VOID OnClose(VOID);

        // Send operation handler
        /*
           sent: the number of bytes successfully sent to the server if any
           This is a handler for an asynchronous send that interacts with the
           callback function
        */
        VOID OnSendComplete(const DWORD sent);

        //set the last completed websocket operation 
        /*
          operation : constant defining the operation flagged as completed by callback function
        */
        VOID OnCallBack(const DWORD operation);
};

Neben der Klasse wird die Struktur Frame definiert, um einen Nachrichtenrahmen darzustellen.

struct Frame
{
        std::vector<BYTE>frame_buffer;
        WINHTTP_WEB_SOCKET_BUFFER_TYPE frame_type;
        DWORD frame_size;
};

Außerdem wird die Enumeration ENUM_WEBSOCKET_STATE deklariert, um die verschiedenen Zustände einer Websocket-Verbindung zu beschreiben.

// client state
enum ENUM_WEBSOCKET_STATE
{
        CLOSED = 0,
        CLOSING = 1,
        CONNECTING = 2,
        CONNECTED = 3,
        SENDING = 4,
        POLLING = 5
};

Als Nächstes wird in asyncwebsocketclient.h eine globale Variable namens clients deklariert. Bei dieser Variablen handelt es sich um einen Container, genauer gesagt um eine Karte, die dazu dient, aktive Websocket-Verbindungen zu speichern. Der globale Geltungsbereich dieses Map-Containers gewährleistet seine Zugänglichkeit für jede in der Bibliothek definierte Callback-Funktion.

// container for  websocket objects accessible to callback function
extern std::map<HINTERNET, std::shared_ptr<WebSocketClient>>clients;

Die Datei asyncwebsocketclient.h schließt mit der Definition einer Reihe von Funktionen ab, die durch WEBSOCK_API qualifiziert sind. Dieser Qualifier dient dazu, diese Funktionen für den Export durch die DLL zu kennzeichnen. Diese Funktionen bilden die bereits erwähnten Funktions-Wrapper und stellen die Schnittstelle dar, über die Entwickler mit er DLL innerhalb ihrer MetaTrader 5-Anwendungen interagieren.

// deinitializes a session handle
/*
   websocket_handle: HINTERNET the websocket handle to close
*/
VOID WEBSOCK_API client_reset(HINTERNET websocket_handle);

//creates a client connection to a server
/*   
           url: the url of the server
           port: port
           secure: use secure connection(non-zero) or not (zero)
           websocket_handle: in-out,HINTERNET non NULL session handle
           return: returns DWORD, zero if successful or non-zero on failure
           
*/

DWORD  WEBSOCK_API client_connect(const WCHAR* url, INTERNET_PORT port, DWORD secure, HINTERNET* websocket_handle);

//destroys a client connection to a server
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
*/

void WEBSOCK_API client_disconnect(HINTERNET websocket_handle);

//writes data to a server (non blocking)
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
           bufferType: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of the frame type
           message: pointer to the data to be sent
           length: size of pBuffer data
           return: DWORD error code, 0 indicates success
           and non-zero for failure
*/

DWORD WEBSOCK_API client_send(HINTERNET websocket_handle, WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype, BYTE* message, DWORD length);

//reads data sent from a server cached internally
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
           out: User supplied container to which data is written to
           out_size: size of out buffer
           buffertype: WINHTTP_WEB_SOCKET_BUFFER_TYPE enumeration of frame type 
           return: DWORD error code, 0 indicates success
           and non-zero for failure
*/

DWORD WEBSOCK_API client_read(HINTERNET websocket_handle, BYTE* out, DWORD out_size, WINHTTP_WEB_SOCKET_BUFFER_TYPE* buffertype);

//listens for a response from a server (non blocking)
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
           return: DWORD error code, 0 indicates success
           and non-zero for failure
*/

DWORD WEBSOCK_API client_poll(HINTERNET websocket_handle);

//gets the last generated error
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
           lasterror: container that will hold the error description
           length: the size of lasterror container
           lasterrornum: reference to which the last error code is written
           return: DWORD error code, 0 indicates success
           and non-zero for failure
*/

DWORD WEBSOCK_API  client_lasterror(HINTERNET websocket_handle);

//checks whether there is any data cached internally
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
           return: returns the size of the last received frame in bytes;
*/

DWORD WEBSOCK_API client_readable(HINTERNET websocket_handle);

//return the state of a websocket connection
/*
           websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
           return: ENUM_WEBSOCKET_STATE enumeration of the state of a client
*/

ENUM_WEBSOCKET_STATE WEBSOCK_API client_status(HINTERNET websocket_handle);

//return the last websocket operation
/*
     websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
     return : DWORD constant corresponding to a unique callback status value as defined in API    
*/
DWORD WEBSOCK_API client_lastcallback_notification(HINTERNET websocket_handle);

//return the websocket handle
/*
     websocket_handle: a valid (non NULL) websocket handle created by calling client_connect()
     return : HINTERNET returns the websocket handle for a client connection 
*/
HINTERNET WEBSOCK_API client_websocket_handle(HINTERNET websocket_handle);

Ein Blick auf die Definition der Funktion WebSocketCallback() zeigt die Verwendung der globalen Client-Variable für die Verwaltung von Benachrichtigungen. Die derzeitige Implementierung ist so konfiguriert, dass sie die in der WinHTTP-Dokumentation als „completion notifications“ bezeichneten Meldungen verarbeitet. Diese Benachrichtigungen werden nach dem erfolgreichen Abschluss eines asynchronen Vorgangs ausgelöst. WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE signalisiert zum Beispiel den Abschluss eines Sendevorgangs, während WINHTTP_CALLBACK_STATUS_READ_COMPLETE den Abschluss eines Lesevorgangs anzeigt.

void WebSocketCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength)
{
        if (WinHttpWebSocketClient::clients.find(hInternet)!= WinHttpWebSocketClient::clients.end())
        {
                WinHttpWebSocketClient::clients[hInternet]->OnCallBack(dwInternetStatus);
                switch (dwInternetStatus)
                {
                case WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE:
                        WinHttpWebSocketClient::clients[hInternet]->OnClose();
                        break;

                case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE:
                        WinHttpWebSocketClient::clients[hInternet]->OnSendComplete(((WINHTTP_WEB_SOCKET_STATUS*)lpvStatusInformation)->dwBytesTransferred);
                        break;

                case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
                        WinHttpWebSocketClient::clients[hInternet]->OnReadComplete(((WINHTTP_WEB_SOCKET_STATUS*)lpvStatusInformation)->dwBytesTransferred, ((WINHTTP_WEB_SOCKET_STATUS*)lpvStatusInformation)->eBufferType);
                        break;

                case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR:
                        WinHttpWebSocketClient::clients[hInternet]->OnError((WINHTTP_ASYNC_RESULT*)lpvStatusInformation);
                        break;

                default:
                        break;
                }
        }

}

Das Argument hInternet der Callback-Funktion dient als HINTERNET-Handle, mit dem der Callback ursprünglich registriert wurde. Dieser Handle wird verwendet, um ein Mitglied innerhalb des globalen Map-Containers, Clients, zu indizieren, was einen Zeiger auf eine WebSocketClient-Instanz ergibt. Die spezifische Callback-Benachrichtigung wird über das Argument dwInternetStatus übermittelt. Die Daten, die durch die Argumente lpvStatusInformation und dwStatusInformationLength dargestellt werden, variieren je nach dem Wert von dwInternetStatus.

Wenn dwInternetStatus beispielsweise die Werte WINHTTP_CALLBACK_STATUS_READ_COMPLETE oder WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE annimmt, enthält der Parameter lpvStatusInformation einen Zeiger auf eine WINHTTP_WEB_SOCKET_STATUS-Struktur, wobei dwStatusInformationLength die Größe der referenzierten Daten angibt.

Unsere Implementierung verarbeitet selektiv eine Teilmenge der von dieser Callback-Funktion bereitgestellten Benachrichtigungen, die jeweils zu einer Änderung des Zustands der entsprechenden WebSocketClient-Instanz führen. Insbesondere erfasst die Methode OnCallBack() die mit diesen Meldungen verbundenen Statuscodes. Diese Informationen werden in der WebSocketClient-Instanz aufbewahrt, wo sie den Nutzern über eine Wrapper-Funktion zugänglich gemacht werden können.

VOID WebSocketClient::OnCallBack(const DWORD operation)
{
        completed_websocket_operation = operation;
}

Die Methode OnReadComplete() innerhalb der Klasse WebSocketClient ist für die Übertragung von Rohdaten in einen Warteschlangenpuffer von Frames zuständig, aus dem die Nutzer anschließend die Verfügbarkeit von Daten zum Abruf abfragen können.

VOID WebSocketClient::OnReadComplete(const DWORD read, const WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype)
{
        bytesRX = read;
        rxBufferType = buffertype;
        status = ENUM_WEBSOCKET_STATE::CONNECTED;
        Frame frame;
        frame.frame_buffer.insert(frame.frame_buffer.begin(), rxBuffer.data(), rxBuffer.data() + read);
        frame.frame_type = buffertype;
        frame.frame_size = read;
        frames->push(frame);
}

Die Methode OnSendComplete() aktualisiert interne Felder, die einen erfolgreichen Sendevorgang kennzeichnen, was auch eine Änderung des Zustands des Websocket-Clients auslöst.

VOID WebSocketClient::OnSendComplete(const DWORD sent)
{
        bytesTX = sent;
        status = ENUM_WEBSOCKET_STATE::CONNECTED;
        return;
}

Schließlich erfasst die Methode OnError() alle fehlerbezogenen Informationen, die über das Argument lpvStatusInformation bereitgestellt werden.

VOID WebSocketClient::OnError(const WINHTTP_ASYNC_RESULT* result)
{
        SetError(result->dwError);
        Reset(false);
}

Der Aufbau einer Verbindung zu einem Server erfolgt über die Funktion client_connect(). Diese Funktion wird mit der Serveradresse (als String), der Portnummer (als Ganzzahl), einem booleschen Wert, der angibt, ob die Verbindung sicher sein soll, und einem Zeiger auf den Wert von HINTERNET aufgerufen. Die Funktion gibt einen DWORD-Wert zurück und setzt auch das HINTERNET-Argument. Im Falle eines Fehlers während des Verbindungsvorgangs setzt die Funktion das Argument HINTERNET auf NULL und gibt einen Fehlercode ungleich Null zurück.

Intern initialisiert die Funktion client_connect() eine Instanz der Klasse WebSocketClient und verwendet sie zum Aufbau der Verbindung. Nach erfolgreichem Verbindungsaufbau wird ein Zeiger auf diese WebSocketClient-Instanz im globalen Client-Container gespeichert, wobei das Websocket-Handle der Instanz als eindeutiger Schlüssel dient. Dieser Websocket-Handle wird verwendet, um eine bestimmte Websocket-Verbindung eindeutig zu identifizieren, sowohl innerhalb der internen Operationen der DLL als auch extern durch das aufrufende MetaTrader 5-Programm.

DWORD  WEBSOCK_API client_connect( const WCHAR* url, INTERNET_PORT port, DWORD secure, HINTERNET* websocketp_handle)
{
        DWORD errorCode = 0;
        auto client = std::make_shared<WebSocketClient>();

        if (client->Connect(url, port, secure) != NO_ERROR)
                errorCode = client->LastError();
        else
        {
                HINTERNET handle = client->WebSocketHandle();
                if (client->EnableCallBack())
                {
                        errorCode = client->LastError();
                        client->Close(WINHTTP_WEB_SOCKET_CLOSE_STATUS::WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS);
                        client->Free();
                        handle = NULL;
                }
                else
                {
                        clients[handle] = client;
                        *websocketp_handle = handle;
                }
        }
        return errorCode;
}

Die Initialisierung und der Aufbau von Websocket-Verbindungen werden von der Methode Connect() der Klasse WebSocketClient verwaltet. Der Verbindungsvorgang wird zunächst synchron durchgeführt. Danach wird die Callback-Funktion durch einen Aufruf der Methode EnableCallBack() auf dem Websocket-Handle registriert, wodurch asynchrone Ereignisbenachrichtigungen ermöglicht werden.

DWORD WebSocketClient::Connect(const WCHAR* host, const INTERNET_PORT port, const DWORD secure)
  {

   if((status != ENUM_WEBSOCKET_STATE::CLOSED))
     {
      ErrorCode = Close(WINHTTP_WEB_SOCKET_CLOSE_STATUS::WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS,NULL);
      return WEBSOCKET_ERROR_CLOSING_ACTIVE_CONNECTION;
     }

   status = ENUM_WEBSOCKET_STATE::CONNECTING;


// Return 0 for success
   if(hSession == NULL)
     {
      ErrorCode = Initialize();
      if(ErrorCode)
        {
         Reset(false);
         return ErrorCode;
        }
     }

// Cracked URL variable pointers
   URL_COMPONENTS UrlComponents;

// Create cracked URL buffer variables
   std::unique_ptr <WCHAR> scheme(new WCHAR[0x20]);
   std::unique_ptr <WCHAR> hostName(new WCHAR[0x100]);
   std::unique_ptr <WCHAR> urlPath(new WCHAR[0x1000]);

   DWORD dwFlags = 0;
   if(secure)
      dwFlags |= WINHTTP_FLAG_SECURE;

   if(scheme == NULL || hostName == NULL || urlPath == NULL)
     {
      ErrorCode = ERROR_NOT_ENOUGH_MEMORY;
      Reset();
      return ErrorCode;
     }

// Clear error's
   ErrorCode = 0;

// Setup UrlComponents structure
   memset(&UrlComponents, 0, sizeof(URL_COMPONENTS));
   UrlComponents.dwStructSize = sizeof(URL_COMPONENTS);
   UrlComponents.dwSchemeLength = -1;
   UrlComponents.dwHostNameLength = -1;
   UrlComponents.dwUserNameLength = -1;
   UrlComponents.dwPasswordLength = -1;
   UrlComponents.dwUrlPathLength = -1;
   UrlComponents.dwExtraInfoLength = -1;

// Get the individual parts of the url
   if(!WinHttpCrackUrl(host, NULL, 0, &UrlComponents))
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset();
      return ErrorCode;
     }

// Copy cracked URL hostName & UrlPath to buffers so they are separated
   if(wcsncpy_s(scheme.get(), 0x20, UrlComponents.lpszScheme, UrlComponents.dwSchemeLength) != 0 ||
      wcsncpy_s(hostName.get(), 0x100, UrlComponents.lpszHostName, UrlComponents.dwHostNameLength) != 0 ||
      wcsncpy_s(urlPath.get(), 0x1000, UrlComponents.lpszUrlPath, UrlComponents.dwUrlPathLength) != 0)
     {
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

   if(port == 0)
     {
      if((_wcsicmp(scheme.get(), L"wss") == 0) || (_wcsicmp(scheme.get(), L"https") == 0))
        {
         UrlComponents.nPort = INTERNET_DEFAULT_HTTPS_PORT;
        }
      else
         if((_wcsicmp(scheme.get(), L"ws") == 0) || (_wcsicmp(scheme.get(), L"http")) == 0)
           {
            UrlComponents.nPort = INTERNET_DEFAULT_HTTP_PORT;
           }
         else
           {
            ErrorCode = ERROR_INVALID_PARAMETER;
            Reset(false);
            return ErrorCode;
           }
     }
   else
      UrlComponents.nPort = port;

// Call the WinHttp Connect method
   hConnect = WinHttpConnect(hSession, hostName.get(), UrlComponents.nPort, 0);
   if(!hConnect)
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

// Create a HTTP request
   hRequest = WinHttpOpenRequest(hConnect, L"GET", urlPath.get(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, dwFlags);
   if(!hRequest)
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

// Set option for client certificate

   if(!WinHttpSetOption(hRequest, WINHTTP_OPTION_CLIENT_CERT_CONTEXT, WINHTTP_NO_CLIENT_CERT_CONTEXT, 0))
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

// Add WebSocket upgrade to our HTTP request
#pragma prefast(suppress:6387, "WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET does not take any arguments.")
   if(!WinHttpSetOption(hRequest, WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, 0, 0))
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

// Send the WebSocket upgrade request.
   if(!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, 0, 0, 0, 0))
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

// Receive response from the server
   if(!WinHttpReceiveResponse(hRequest, 0))
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

// Finally complete the upgrade
   hWebSocket = WinHttpWebSocketCompleteUpgrade(hRequest, NULL);
   if(hWebSocket == 0)
     {
      // Handle error
      ErrorCode = GetLastError();
      Reset(false);
      return ErrorCode;
     }

   status = ENUM_WEBSOCKET_STATE::CONNECTED;

// Return should be zero
   return ErrorCode;
  }

DWORD WebSocketClient::EnableCallBack(VOID)
  {
   if(!WinHttpSetOption(hWebSocket, WINHTTP_OPTION_CONTEXT_VALUE, (LPVOID)this, sizeof(this)))
     {
      // Handle error
      ErrorCode = GetLastError();
      return ErrorCode;
     }

   if(WinHttpSetStatusCallback(hWebSocket, WebSocketCallback, WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS, 0) == WINHTTP_INVALID_STATUS_CALLBACK)
     {
      ErrorCode = GetLastError();
      return ErrorCode;
     }

   return ErrorCode;
  }

Nachdem eine Verbindung zu einem Server hergestellt und ein gültiges Handle erhalten wurde, können die Nutzer die Kommunikation mit dem entfernten Endpunkt aufnehmen. Die Übermittlung von Daten an den Server erfolgt durch einen Aufruf der Funktion client_send(). Diese Funktion benötigt die folgenden Parameter: ein gültiges Websocket-Handle, einen Wert der Enumeration WINHTTP_WEB_SOCKET_BUFFER_TYPE, der den Typ des zu übertragenden Websocket-Frames angibt, ein BYTE-Array, das die Datennutzlast enthält, und ein ulong-Argument, das die Größe des Datenarrays angibt. Die Funktion gibt Null zurück, wenn keine unmittelbaren Fehler aufgetreten sind; andernfalls gibt sie einen bestimmten Fehlercode zurück.

Intern wird die Funktion WinHttpWebSocketSend() asynchron aufgerufen. Folglich stellt der Rückgabewert von client_send() einen Zwischenstatus dar, der die Abwesenheit von Vorfehlern bei der Einrichtung des Sendevorgangs anzeigt. Das Ergebnis der eigentlichen Datenübertragung wird nicht synchron zurückgesendet. Stattdessen wird das Ergebnis asynchron über eine Benachrichtigung mitgeteilt, die über die registrierte Callback-Funktion zugänglich ist. Im Rahmen eines erfolgreichen Sendevorgangs wird eine WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE-Meldung erwartet. Tritt dagegen während eines Vorgangs (Senden oder Empfangen) ein Fehler auf, wird in der Regel eine WINHTTP_CALLBACK_STATUS_REQUEST_ERROR-Benachrichtigung an die Callback-Funktion weitergeleitet.

DWORD WEBSOCK_API client_send(HINTERNET websocket_handle, WINHTTP_WEB_SOCKET_BUFFER_TYPE buffertype, BYTE* message, DWORD length)
  {
   DWORD out = 0;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = WEBSOCKET_ERROR_INVALID_HANDLE;
   else
      out = clients[websocket_handle]->Send(buffertype, message, length);

   return out;
  }

Von der internen Callback-Funktion empfangene Benachrichtigungen können mit der Funktion client_lastcallback_notification() abgerufen werden. Diese Funktion gibt die letzte Benachrichtigung zurück, die der Callback für eine bestimmte Verbindung erhalten hat, die durch das als einziges Argument angegebene Websocket-Handle identifiziert wird. Der folgende Codeabschnitt veranschaulicht einen möglichen Ansatz zur Handhabung dieser Benachrichtigungen innerhalb eines MetaTrader 5-Programms. Die symbolischen Konstanten, die diesen Benachrichtigungen entsprechen, sind in der Datei asyncwinhttp.mqh definiert, die von der ursprünglichen Header-Datei winhttp.h abgeleitet ist.

DWORD WEBSOCK_API client_lastcallback_notification(HINTERNET websocket_handle)
  {
   DWORD out = 0;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = WEBSOCKET_ERROR_INVALID_HANDLE;
   else
      out = clients[websocket_handle]->LastOperation();

   return out;
  }

Um die vom Server übermittelten Daten zu empfangen, muss der Client zunächst in einen so genannten Polling-Zustand versetzt werden, indem die Funktion client_poll() aufgerufen wird. Diese Aktion ruft intern die Funktion WinHttpWebSocketReceive() innerhalb der Klasse WebSocketClient auf. Ähnlich wie bei der Sendeoperation wird die WinHTTP-Funktion asynchron aufgerufen, was zur sofortigen Rückgabe eines Zwischenstatus führt.

Die Klasse WebSocketClient verfügt über interne Puffer, um die Rohdaten bei ihrer Ankunft aufzunehmen. Sobald ein Lesevorgang erfolgreich abgeschlossen ist, werden diese Daten in einer internen Datenstruktur in eine Warteschlange gestellt. Dieser Prozess wird von der Methode OnReadComplete() der Klasse WebSocketClient gesteuert. Nach Abschluss eines Lesevorgangs ändert sich der Status der Websocket-Verbindung, und sie hört auf, aktiv auf eingehende Nachrichten zu warten.

Dies bedeutet, dass eine asynchrone Leseanforderung nicht kontinuierlich ist und keine dauerhafte Abfrage darstellt. Um weitere Nachrichten vom Server abzurufen, muss die Funktion client_poll() erneut aufgerufen werden. Im Wesentlichen versetzt der Aufruf von client_poll() den Websocket-Client in einen temporären, nicht blockierenden Abfragezustand, der Daten erfasst, sobald sie verfügbar sind, und anschließend die WINHTTP_CALLBACK_STATUS_READ_COMPLETE-Benachrichtigung auslöst.

Der aktuelle Status des Websocket-Clients kann durch den Aufruf der Funktion client_status() abgefragt werden, die einen Wert der Enumeration ENUM_WEBSOCKET_STATE zurückgibt.

DWORD WEBSOCK_API client_poll(HINTERNET websocket_handle)
  {
   DWORD out = 0;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = WEBSOCKET_ERROR_INVALID_HANDLE;
   else
      out = clients[websocket_handle]->Receive(clients[websocket_handle]->rxBuffer.data(), (DWORD)clients[websocket_handle]->rxBuffer.size(), &clients[websocket_handle]->bytesRX, &clients[websocket_handle]->rxBufferType);

   return out;
  }

ENUM_WEBSOCKET_STATE WEBSOCK_API client_status(HINTERNET websocket_handle)
  {
   ENUM_WEBSOCKET_STATE out = {};
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = {};
   else
      out = clients[websocket_handle]->Status();

   return out;
  }

Der Abruf der vom Server empfangenen Rohdaten wird durch die Funktion client_read() erleichtert, die die folgenden Argumente akzeptiert:

  • ein gültiges HINTERNET-Websocket-Handle,
  • einen Verweis auf ein zuvor zugewiesenes BYTE-Array, einen ulong-Wert, der die Größe des genannten Arrays angibt, 
  • eine Referenz auf einen WINHTTP_WEB_SOCKET_BUFFER_TYPE-Wert.
Die empfangenen Daten werden in das bereitgestellte BYTE-Array geschrieben, und der Typ des Websocket-Frames wird in das Argument buffertype kopiert. Entscheidend ist, dass diese Funktion aus einer internen Warteschlange von empfangenen Frames liest und nicht direkt mit dem Netzwerk-Socket interagiert. Folglich ist client_read() eine synchrone Operation, unabhängig von den asynchronen Mechanismen der WinHTTP-Bibliothek. Ein Rückgabewert ungleich Null zeigt an, dass das Kopieren von Daten aus der internen Warteschlange fehlgeschlagen ist. Nach erfolgreichem Abruf eines Rahmens mit dieser Funktion wird der Rahmen aus der internen Warteschlange entfernt (dequeued). Die Funktion client_readable() kann aufgerufen werden, um die Größe des Datenrahmens zu ermitteln, der sich derzeit an der Spitze der Warteschlange der vom Server empfangenen Rahmen befindet.
DWORD WEBSOCK_API client_read(HINTERNET websocket_handle, BYTE* out, DWORD out_size, WINHTTP_WEB_SOCKET_BUFFER_TYPE* buffertype)
  {
   DWORD rout = 0;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      rout = WEBSOCKET_ERROR_INVALID_HANDLE;
   else
      clients[websocket_handle]->Read(out, out_size, buffertype);

   return rout;
  }

DWORD WEBSOCK_API client_readable(HINTERNET websocket_handle)
  {
   DWORD out = 0;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = 0;
   else
      out = clients[websocket_handle]->ReadAvailable();

   return out;
  }

Die Fehlercodes können durch Aufruf der Funktion client_lasterror() ermittelt werden. Die Funktion gibt einen DWORD-Wert für den zuletzt aufgetretenen Fehler zurück. Nutzer können den Wert des aktuellen Websocket-Handles auch mit client_websocket_handle() abrufen. Dies könnte nützlich sein, um festzustellen, ob ein Handle geschlossen wurde oder nicht.

DWORD WEBSOCK_API  client_lasterror(HINTERNET websocket_handle)
  {
   DWORD out = 0;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = WEBSOCKET_ERROR_INVALID_HANDLE;
   else
      out = clients[websocket_handle]->LastError();

   return out;
  }

HINTERNET WEBSOCK_API client_websocket_handle(HINTERNET websocket_handle)
  {
   HINTERNET out = NULL;
   if(websocket_handle == NULL || clients.find(websocket_handle) == clients.end())
      out = NULL;
   else
      out = clients[websocket_handle]->WebSocketHandle();
   return out;
  }

Das ordnungsgemäße Beenden einer Verbindung zu einem Server wird durch den Aufruf der Funktion client_disconnect() eingeleitet. Diese Funktion gibt keinen Wert zurück. Der Zustand des Websocket-Clients wird jedoch sofort in einen schließenden Zustand überführt. Wird anschließend ein entsprechender Close-Frame vom Server empfangen, wird die WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE-Benachrichtigung ausgelöst, wodurch der Websocket-Zustand auf geschlossen geändert wird.

void WEBSOCK_API client_disconnect(HINTERNET websocket_handle)
  {

   if(clients.find(websocket_handle) != clients.end())
     {
      if(clients[websocket_handle]->WebSocketHandle() != NULL)
        {
         clients[websocket_handle]->Close(WINHTTP_WEB_SOCKET_CLOSE_STATUS::WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS);
        }
     }

   return;
  }

Die letzte von der DLL exportierte Funktion ist client_reset(). Idealerweise sollte diese Funktion nach dem Trennen der Verbindung mit einem Server aufgerufen werden. Sie dient dazu, die mit der geschlossenen Verbindung verbundenen internen Speicherpuffer freizugeben. Der Aufruf dieser Funktion ist zwar nicht unbedingt erforderlich, kann aber nützlich sein, um Speicherressourcen zurückzugewinnen, die während der Programmausführung an anderer Stelle benötigt werden. Der Aufruf von client_reset() macht alle Daten ungültig, die mit dem angegebenen Websocket-Handle verbunden sind, einschließlich Fehlercodes, Fehlermeldungen und ungelesene Daten, die in der internen Warteschlange von Frames verbleiben.

VOID WEBSOCK_API client_reset(HINTERNET websocket_handle)
  {
   if(clients.find(websocket_handle) != clients.end())
     {
      clients[websocket_handle]->Free();
      clients.erase(websocket_handle);
     }
  }


Neudefinition der CWebsocket-Klasse

Bevor eine MetaTrader 5-Anwendung untersucht wird, die die im vorangegangenen Abschnitt beschriebenen Funktionen nutzt, wird eine Neudefinition der Klasse CWebsocket vorgenommen, die bereits im oben genannten Artikel behandelt wurde. Mit dieser Neudefinition wird die Klasse neu ausgerichtet, um den neu entwickelten asynchronen Websocket-Client zu nutzen. Der Quellcode für diese Anpassung befindet sich in der Datei asyncwebsocket.mqh.

Die Enumeration ENUM_WEBSOCKET_STATE wurde erweitert, um zusätzliche Zustände aufzunehmen, die die asynchrone Natur des Clients widerspiegeln. Der POLLING-Zustand wird aktiviert, wenn ein asynchroner Lesevorgang eingeleitet wird. Sobald der zugrundeliegende Socket Daten empfängt und zum Abruf bereitstellt, signalisiert die Callback-Funktion den Abschluss des asynchronen Lesevorgangs, und der Zustand des Websocket-Clients geht in seinen Standardzustand über: CONNECTED. In ähnlicher Weise geht ein asynchroner Sendevorgang in den Zustand SENDING über. Das Ergebnis dieser Operation wird asynchron über die Callback-Funktion kommuniziert, wobei eine erfolgreiche Übertragung zu einer Rückkehr in den Standardzustand CONNECTED führt.

//+------------------------------------------------------------------+
//|   client state enumeration                                       |
//+------------------------------------------------------------------+

// client state
enum ENUM_WEBSOCKET_STATE
  {
   CLOSED = 0,
   CLOSING = 1,
   CONNECTING = 2,
   CONNECTED = 3,
   SENDING = 4,
   POLLING = 5
  };

Mehrere neue Methoden wurden in die CWebsocket-Klasse integriert, um ihre erweiterten Möglichkeiten zu berücksichtigen. Die übrigen Methoden behalten ihre ursprünglichen Signaturen bei, wobei nur ihre internen Implementierungen geändert werden, um die neue DLL-Abhängigkeit einzubeziehen. Diese Methoden sind wie folgt:

Connect(): Diese Methode dient als erster Interaktionspunkt für den Aufbau einer Verbindung zu einem Server. Sie akzeptiert die folgenden Parameter:

  •  _serveraddress: Die vollständige Adresse des Servers (Datentyp String).
  •  _port: Die Portnummer des Servers (ushort-Datentyp).
  •  _secure: Ein boolescher Wert, der angibt, ob eine sichere Verbindung hergestellt werden soll (boolescher Datentyp).

Die Implementierung dieser Methode wurde erheblich vereinfacht, da der Großteil der Logik für den Verbindungsaufbau nun von der zugrunde liegenden DLL übernommen wird.

//+------------------------------------------------------------------------------------------------------+
//|Connect method used to set server parameters and establish client connection                          |
//+------------------------------------------------------------------------------------------------------+
bool CWebsocket::Connect(const string _serveraddress,const INTERNET_PORT port=443, bool secure = true)
  {
   if(initialized)
     {
      if(StringCompare(_serveraddress,serveraddress,false))
         Close();
      else
         return(true);
     }

   serveraddress = _serveraddress;

   int dot=StringFind(serveraddress,".");

   int ss=(dot>0)?StringFind(serveraddress,"/",dot):-1;

   serverPath=(ss>0)?StringSubstr(serveraddress,ss+1):"/";

   int sss=StringFind(serveraddress,"://");

   if(sss<0)
      sss=-3;

   serverName=StringSubstr(serveraddress,sss+3,ss-(sss+3));
   serverPort=port;


   DWORD connect_error = client_connect(serveraddress,port,ulong(secure),hWebSocket);

   if(hWebSocket<=0)
     {
      Print(__FUNCTION__," Connection to ", serveraddress, " failed. \n", GetErrorDescription(connect_error));
      return(false);
     }
   else
      initialized = true;

   return(true);
  }

Wenn die Methode Connect() den booleschen Wert true zurückgibt, was eine erfolgreiche Verbindung anzeigt, kann die Datenübertragung über den WebSocket-Client beginnen. Zu diesem Zweck sind zwei Methoden vorgesehen:

  •  SendString(): Diese Methode nimmt eine Zeichenkette als Eingabe an.
  •  Send(): Diese Methode akzeptiert ein Array von Zeichen ohne Vorzeichen als einzigen Parameter.

Beide Methoden geben bei erfolgreicher Einleitung des Sendevorgangs ein boolesches true zurück und rufen intern die private Methode clientsend() auf, die alle Sendevorgänge für die Klasse verwaltet.

//+------------------------------------------------------------------+
//|public method for sending raw string messages                     |
//+------------------------------------------------------------------+
bool CWebsocket::SendString(const string msg)
  {
   if(!initialized || hWebSocket == NULL)
     {
      Print(__FUNCTION__, " No websocket connection ");
      return(false);
     }

   if(StringLen(msg)<=0)
     {
      Print(__FUNCTION__, " Message buffer is empty ");
      return(false);
     }

   BYTE msg_array[];

   StringToCharArray(msg,msg_array,0,WHOLE_ARRAY);

   ArrayRemove(msg_array,ArraySize(msg_array)-1,1);

   DWORD len=(ArraySize(msg_array));

   return(clientsend(msg_array,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE));
  }

//+------------------------------------------------------------------+
//|Public method for sending data prepackaged in an array            |
//+------------------------------------------------------------------+
bool CWebsocket::Send(BYTE &buffer[])
  {
   if(!initialized || hWebSocket == NULL)
     {
      Print(__FUNCTION__, " No websocket connection ");
      return(false);
     }

   return(clientsend(buffer,WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE));
  }

Der Aufruf der Methode Poll() initiiert eine asynchrone Leseoperation auf der unteren Ebene und versetzt den Websocket-Client in den POLLING-Zustand. Dieser Zustand bedeutet, dass der Client auf eine Antwort des Servers wartet.

//+------------------------------------------------------------------+
//|  asynchronous read operation (polls for server response)         |
//+------------------------------------------------------------------+
ulong CWebsocket::Poll(void)
  {
   if(hWebSocket!=NULL)
      return client_poll(hWebSocket);
   else
      return WEBSOCKET_ERROR_INVALID_HANDLE;
  }

Um festzustellen, ob die Daten vom Client empfangen und erfolgreich gelesen wurden, stehen dem Nutzer zwei Möglichkeiten zur Verfügung:

  • CallBackResult(): Diese Methode prüft die letzte von der Callback-Funktion erhaltene Meldung. Ein erfolgreicher Lesevorgang sollte zu einer Meldung über den Abschluss des Lesevorgangs führen.
  • ReadAvailable(): Diese Methode gibt die Größe (in Bytes) der Daten zurück, die derzeit im internen Puffer zum Abruf bereitstehen.

//+------------------------------------------------------------------+
//| call back notification                                           |
//+------------------------------------------------------------------+
ulong CWebsocket::CallBackResult(void)
  {
   if(hWebSocket!=NULL)
      return client_lastcallback_notification(hWebSocket);
   else
      return WINHTTP_CALLBACK_STATUS_DEFAULT;
  }
//+------------------------------------------------------------------+
//|  check if any data has read from the server                      |
//+------------------------------------------------------------------+
ulong CWebsocket::ReadAvailable(void)
  {
   if(hWebSocket!=NULL)
      return(client_readable(hWebSocket));
   else
      return 0;
  }

Auf die vom Server übermittelten Rohdaten kann dann entweder mit den Methoden Read() oder ReadString() zugegriffen werden. Beide Methoden geben die Größe der empfangenen Daten zurück. ReadString() erfordert eine per Referenz übergebene String-Variable, in die die empfangenen Daten geschrieben werden, während Read() die Daten in ein vorzeichenloses Zeichenarray schreibt.

//+------------------------------------------------------------------+
//|public method for reading data sent from the server               |
//+------------------------------------------------------------------+
ulong CWebsocket::Read(BYTE &buffer[],WINHTTP_WEB_SOCKET_BUFFER_TYPE &buffertype)
  {
   if(!initialized || hWebSocket == NULL)
     {
      Print(__FUNCTION__, " No websocket connection ");
      return(false);
     }

   ulong bytes_read_from_socket=0;

   clientread(buffer,buffertype,bytes_read_from_socket);

   return(bytes_read_from_socket);

  }
//+------------------------------------------------------------------+
//|public method for reading data sent from the server               |
//+------------------------------------------------------------------+
ulong CWebsocket::ReadString(string &_response)
  {
   if(!initialized || hWebSocket == NULL)
     {
      Print(__FUNCTION__, " No websocket connection ");
      return(false);
     }

   ulong bytes_read_from_socket=0;

   ZeroMemory(rxbuffer);

   WINHTTP_WEB_SOCKET_BUFFER_TYPE rbuffertype;

   clientread(rxbuffer,rbuffertype,bytes_read_from_socket);

   _response=(bytes_read_from_socket)?CharArrayToString(rxbuffer):"";

   return(bytes_read_from_socket);
  }

Wenn der WebSocket-Client nicht mehr benötigt wird, kann die Verbindung zum Server entweder mit der Methode Close() beendet werden. Die Methode Abort() unterscheidet sich von Close() dadurch, dass sie das Schließen einer Websocket-Verbindung erzwingt, indem sie die zugrundeliegenden Handles schließt, wodurch auch die Werte der internen Klasseneigenschaften auf ihre Standardwerte zurückgesetzt werden. Die Methode kann explizit aufgerufen werden, um eine Ressourcenbereinigung durchzuführen.

Schließlich gibt die Methode WebSocketHandle() das zugrunde liegende HINTERNET-Websocket-Handle zurück.

//+------------------------------------------------------------------+
//| Closes a websocket client connection                             |
//+------------------------------------------------------------------+
void CWebsocket::Close(void)
  {
   if(!initialized || hWebSocket == NULL)
      return;
   else
      client_disconnect(hWebSocket);
  }
//+--------------------------------------------------------------------------+
//|method for abandoning a client connection. All previous server connection |
//|   parameters are reset to their default state                            |
//+--------------------------------------------------------------------------+
void CWebsocket::Abort(void)
  {
   client_reset(hWebSocket);
   reset();
  }
//+------------------------------------------------------------------+
//|   websocket handle                                               |
//+------------------------------------------------------------------+
HINTERNET CWebsocket::WebSocketHandle(void)
  {
   if(hWebSocket!=NULL)
      return client_websocket_handle(hWebSocket);
   else
      return NULL;
  }


Verwendung der DLL

In diesem Abschnitt wird ein Beispielprogramm vorgestellt, das die Verwendung der asyncwinhttpwebsockets.dll veranschaulichen soll. Das Programm enthält eine grafische Nutzeroberfläche (GUI), die eine Verbindung zum Websocket-Echo-Dienst herstellt, der auf https://echo.websocket.org gehostet wird, einer Ressource, die speziell für das Testen von Websocket-Client-Implementierungen bereitgestellt wird. Für die Erstellung dieser Anwendung wird die frei verfügbare Bibliothek Easy And Fast GUI MQL5 benötigt. Das Programm ist als Expert Advisor (EA) in der MetaTrader 5 Umgebung implementiert. Die Nutzeroberfläche verfügt über zwei Schaltflächen, mit denen eine Verbindung zum gewünschten Server hergestellt und beendet werden kann. Zusätzlich steht ein Texteingabefeld zur Verfügung, in das die Nutzer Nachrichten zur Übertragung an den Server eingeben können.

Die vom Websocket-Client durchgeführten Vorgänge werden aufgezeichnet und angezeigt, wobei jeder Protokolleintrag mit einem Zeitstempel versehen wird, um den Zeitpunkt des Auftretens anzugeben. Der Quellcode für dieses Programm ist weiter unten zu finden.

//+------------------------------------------------------------------+
//|                                                         Echo.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 <EasyAndFastGUI\WndCreate.mqh>
#include <asyncwebsocket.mqh>
//+------------------------------------------------------------------+
//| Gui application class                                            |
//+------------------------------------------------------------------+
class CApp:public CWndCreate
  {
protected:

   CWindow           m_window;                   //main window

   CTextEdit         m_rx;                       //text input to specify messages to be sent

   CTable            m_tx;                       //text box displaying received messages from the server

   CButton           m_connect;                  //connect button

   CButton           m_disconnect;               //disconnect button

   CTimeCounter      m_timer_counter;                //On timer objects

   CWebsocket*        m_websocket;               //websocket connection
public:
                     CApp(void);                 //constructor
                    ~CApp(void);                 //destructor

   void              OnInitEvent(void);
   void              OnDeinitEvent(const int reason);

   virtual void      OnEvent(const int id, const long &lparam, const double &dparam,const string &sparam);
   void              OnTimerEvent(void);
   bool              CreateGUI(void);

protected:

private:
   uint              m_row_index;
   void              EditTable(const string newtext);
  };
//+------------------------------------------------------------------+
//|  constructor                                                     |
//+------------------------------------------------------------------+
CApp::CApp(void)
  {
   m_row_index = 0;
   m_timer_counter.SetParameters(10,50);
   m_websocket = new CWebsocket();
  }
//+------------------------------------------------------------------+
//|   destructor                                                     |
//+------------------------------------------------------------------+
CApp::~CApp(void)
  {
   if(CheckPointer(m_websocket) == POINTER_DYNAMIC)
      delete m_websocket;
  }
//+------------------------------------------------------------------+
//| On initialization                                                |
//+------------------------------------------------------------------+
void CApp::OnInitEvent(void)
  {
  }
//+------------------------------------------------------------------+
//| On DeInitilization                                               |
//+------------------------------------------------------------------+
void CApp::OnDeinitEvent(const int reason)
  {
   CWndEvents::Destroy();
  }
//+------------------------------------------------------------------+
//| on timer event                                                   |
//+------------------------------------------------------------------+
void CApp::OnTimerEvent(void)
  {
   CWndEvents::OnTimerEvent();

   if(m_timer_counter.CheckTimeCounter())
     {

      ENUM_WEBSOCKET_STATE client_state = m_websocket.ClientState();
      ulong operation = m_websocket.CallBackResult();

      switch((int)operation)
        {
         case WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE:
            if(client_state == CLOSED)
              {
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Disconnected]");
               m_websocket.Abort();
              }
            break;
         case WINHTTP_CALLBACK_STATUS_WRITE_COMPLETE:
            if(client_state!=POLLING)
              {
               m_websocket.Poll();
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Send Complete]");
              }
            break;

         case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
            if(m_websocket.ReadAvailable())
              {
               string response;
               m_websocket.ReadString(response);
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Received]-> "+response);
              }
            break;
         case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR:
            EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Error]-> "+m_websocket.LastErrorMessage());
            m_websocket.Abort();
            break;
         default:
            break;
        }
     }
  }
//+------------------------------------------------------------------+
//| create the gui                                                   |
//+------------------------------------------------------------------+
bool CApp::CreateGUI(void)
  {
//---check the websocket object
   if(CheckPointer(m_websocket) == POINTER_INVALID)
     {
      Print(__FUNCTION__," Failed to create websocket client object ", GetLastError());
      return false;
     }
//---initialize window creation
   if(!CWndCreate::CreateWindow(m_window,"Connect to https://echo.websocket.org Echo Server ",1,1,750,300,true,false,true,false))
      return(false);
//---
   if(!CWndCreate::CreateTextEdit(m_rx,"",m_window,0,false,0,25,750,750,"Click connect button below, input your message here, then press enter key to send"))
      return(false);
//---
   if(!CWndCreate::CreateButton(m_connect,"Connect",m_window,0,5,50,240,false,false,clrNONE,clrNONE,clrNONE,clrNONE,clrNONE))
      return(false);
//---create text edit for width in frequency units
   if(!CWndCreate::CreateButton(m_disconnect,"Disonnect",m_window,0,500,50,240,false,false,clrNONE,clrNONE,clrNONE,clrNONE,clrNONE))
      return(false);
//---create text edit for amount of padding
   string tableheader[1] = {"Client Operations Log"};
   if(!CWndCreate::CreateTable(m_tx,m_window,0,1,10,tableheader,5,75,0,0,true,true,5))
      return(false);
//---
   m_tx.TextAlign(0,ALIGN_LEFT);
   m_tx.ShowTooltip(false);
   m_tx.DataType(0,TYPE_STRING);
   m_tx.IsDropdown(false);
   m_tx.SelectableRow(false);
   int cwidth[1] = {740};
   m_tx.ColumnsWidth(cwidth);
//---init events
   CWndEvents::CompletedGUI();
//---
   return(true);
  }
//+------------------------------------------------------------------+
//| edit the table                                                   |
//+------------------------------------------------------------------+
void CApp::EditTable(const string newtext)
  {
   if(newtext==NULL)
      return;

   if((m_row_index+1)==m_tx.RowsTotal())
     {
      m_tx.AddRow(m_row_index+1,true);
      m_tx.Update();
     }

   m_tx.SetValue(0,m_row_index++,newtext,0,true);
   m_tx.Update(true);
  }
//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CApp::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==CHARTEVENT_CUSTOM+ON_END_EDIT)
     {
      if(lparam==m_rx.Id())
        {
         if(m_websocket.ClientState() == CONNECTED)
           {
            string textinput = m_rx.GetValue();
            if(StringLen(textinput)>0)
              {
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Sending]-> "+textinput);
               m_websocket.SendString(textinput);
              }
           }
        }
      return;
     }
   else
      if(id == CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
        {
         if(lparam==m_connect.Id())
           {
            if(m_websocket.ClientState() != CONNECTED)
              {
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Connecting]");
               if(m_websocket.Connect("https://echo.websocket.org/"))
                 {
                  EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Connected]");
                  m_websocket.Poll();
                 }
               else
                  EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[FailedToConnect]");
              }
            return;
           }
         if(lparam==m_disconnect.Id())
           {
            if(m_websocket.ClientState() != CLOSED)
              {
               EditTable("["+TimeToString(TimeCurrent(),TIME_MINUTES|TIME_SECONDS)+"]"+"[Disconnecting]");
               m_websocket.Close();
              }
           }
         return;
        }
  }
//+------------------------------------------------------------------+
CApp app;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(void)
  {
   ulong tick_counter=::GetTickCount();
//---
   app.OnInitEvent();
//---
   if(!app.CreateGUI())
     {
      ::Print(__FUNCTION__," > error");
      return(INIT_FAILED);
     }

   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {

   app.OnDeinitEvent(reason);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(void)
  {
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer(void)
  {
   app.OnTimerEvent();
  }
//+------------------------------------------------------------------+
//| Trade function                                                   |
//+------------------------------------------------------------------+
void OnTrade(void)
  {
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int    id,
                  const long   &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   app.ChartEvent(id,lparam,dparam,sparam);
  }
//+------------------------------------------------------------------+

Die folgende Grafik zeigt, wie das Programm funktioniert.

Echo EA-Demonstration


Schlussfolgerung

Dieser Artikel beschreibt die Entwicklung eines WebSocket-Clients für MetaTrader 5 unter Verwendung der WinHTTP-Bibliothek in einem asynchronen Betriebsmodus. Zur Kapselung dieser Funktionalität wurde eine eigene Klasse entwickelt, deren Implementierung in einem Expert Advisor (EA) demonstriert wurde, der für die Interaktion mit dem unter echo.websocket.org gehosteten Echo-Server konzipiert ist. Der vollständige Quellcode, einschließlich der dynamischen Link-Bibliothek, ist im Zusatzmaterial enthalten. Konkret befinden sich die C++-Quelldateien zusammen mit einer CMakeLists.txt-Build-Konfigurationsdatei im angegebenen C++-Verzeichnis. Zusätzlich enthält das MQL5-Verzeichnis in den ergänzenden Materialien eine vorkompilierte asyncwinhttpwebsockets.dll-Datei für den sofortigen Einsatz.

Für Nutzer, die die Client-Bibliothek aus dem bereitgestellten Quellcode erstellen möchten, steht das benötigte Build-System von CMake zur Verfügung. Wenn CMake installiert ist, kann die grafische Nutzeroberfläche (cmake-gui) aufgerufen werden. Der Nutzer muss dann das Quellcode-Verzeichnis angeben, das dem Ort der CMakeLists.txt-Datei entspricht (d.h. MqlWebsocketsClientDLL\Source\C++), und ein separates Build-Verzeichnis, das an einem beliebigen Ort erstellt werden kann.

Anschließend wird durch Anklicken der Schaltfläche „Konfigurieren“ der Konfigurationsprozess eingeleitet. In einem Dialogfenster wird der Nutzer aufgefordert, „den Generator für dieses Projekt anzugeben“, wobei die entsprechende Version von Visual Studio, die auf dem System installiert ist, ausgewählt werden sollte. Unter der Einstellung „Optionale Plattform für den Generator“ kann der Nutzer „Win32“ angeben, um eine 32-Bit-Version der DLL zu kompilieren; bleibt dieses Feld leer, wird standardmäßig eine 64-Bit-Kompilierung durchgeführt. Wenn Sie auf „Fertig stellen“ klicken, wird CMake die Erstkonfiguration durchführen.

Es erscheint dann eine Fehlermeldung, die auf die Notwendigkeit hinweist, bestimmte Einträge in der Datei CMakeLists.txt zu konfigurieren. Um dies zu beheben, sollte der Nutzer den Eintrag mit der Bezeichnung „ADDITIONAL_LIBRARY_DEPENDENCIES“ suchen, in das nebenstehende Feld klicken und zu dem Verzeichnis navigieren, das die Datei winhttp.lib enthält.

Anschließend sollte der Nutzer den Eintrag „OUTPUT_DIRECTORY_Xxx_RELEASE“ suchen (wobei „Xxx“ die Architektur, X64 oder X86, bezeichnet) und den entsprechenden Pfad zum Ordner „Libraries“ einer MetaTrader-Installation festlegen.

Nachdem Sie diese Optionen konfiguriert haben, sollten Sie erneut auf „Konfigurieren“ klicken, um den Konfigurationsprozess ohne weitere Fehlermeldungen abzuschließen. Die Build-Datei kann dann durch Klicken auf „Generate“ erstellt werden. Bei erfolgreicher Generierung wird die Schaltfläche „Projekt öffnen“ aktiviert, die beim Anklicken die generierte Visual Studio-Projektdatei öffnet.

Um die DLL zu erstellen, muss der Nutzer in Visual Studio „Build“ und dann „Build Solution“ wählen. Die resultierende DLL wird innerhalb weniger Sekunden verfügbar sein.

Name der Datei oder des Ordners
Beschreibung
MqlWebsocketsClientDLL\Source\C++
Der Ordner enthält die vollständigen Quellcode-Dateien für asycnwinhttpwebsockets.dll.
MqlWebsocketsClientDLL\Source\MQL5\Include\asyncwinhttp.mqh Enthält die Import-Direktive, die alle von der asycnwinhttpwebsockets.dll bereitgestellten Funktionen auflistet.
MqlWebsocketsClientDLL\Source\MQL5\Include\asyncwebsocket.mqh Enthält die Definition der Klasse CWebsocket, die die von den zugrundeliegenden DLL-Funktionen bereitgestellten Funktionen umhüllt.
MqlWebsocketsClientDLL\Source\MQL5\Experts\Echo.mq5 Die Codedatei für einen EA, der die Anwendung der DLL veranschaulicht.
MqlWebsocketsClientDLL\Source\MQL5\Experts\Echo.ex5 Der kompilierte EA, der die Anwendung der DLL demonstriert.
MqlWebsocketsClientDLL\Source\MQL5\Libraries\asycnwinhttpwebsockets.dll Die kompilierte DLL mit asynchroner Winhttp-Websocket-Funktionalität.

Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/17877

Beigefügte Dateien |
asyncwinhttp.mqh (13.42 KB)
asyncwebsocket.mqh (19.09 KB)
Echo.mq5 (10.84 KB)
Echo.ex5 (306.48 KB)
Letzte Kommentare | Zur Diskussion im Händlerforum (9)
Shephard Mukachi
Shephard Mukachi | 2 Mai 2025 in 21:17
Ryan L Johnson #:

Es könnte hilfreich sein, einige Beispielcodes zu sehen:

Die gleiche Grundvoraussetzung gilt für einen EA.

Das ist eine ausgezeichnete Idee.

CapeCoddah
CapeCoddah | 15 Mai 2025 in 23:47

Ich danke Ihnen beiden für Ihre Antwort auf meine Frage. Ich muss die überladenen Funktionsdefinitionen verpasst und nur über die erste gelesen haben. Wissen Sie vielleicht, ob Terminal intelligent genug ist, um die iCustom-Aufrufe parallel zu verarbeiten, um die Prozessorauslastung zu maximieren, da ich plane, die Symbolparameter für jedes der 28 Paare zu variieren und mehrere iCustom-Aufrufe wie die Brooky Trend Strength zu haben.

Kann mir jemand von Ihnen auch sagen, wo ich Kommentare zu Fehlern in MQ5 posten kann und wo ich Vorschläge für die MQ-Administratoren finden kann? Ich habe einige gefunden, zuletzt den Unterschied zwischen den Balken im Terminal und im Strategietester. Außerdem habe ich ein 3-Bildschirm-Setup mit der Hauptanzeige ganz links. Ich versuche, ein Panel zu verschieben. Der Mauszeiger befindet sich auf der linken Seite des Bildschirms, aber das zu verschiebende Panel befindet sich in der Mitte. Ich denke, dass entweder das Terminal oder Windows verrückt spielt, wenn sich die Maus um ein Pixel bewegt und dann die Anzeige wechselt, um das Panel um ein Pixel zu verschieben und dann wieder zurück.

Silk Road Trading LLC
Ryan L Johnson | 16 Mai 2025 in 00:13
CapeCoddah Strategietester.

Bars() hat Schluckauf, wenn Preisdaten fehlen, was bei rates_total nicht der Fall ist. Wenn ich mich richtig erinnere, was ich in der Vergangenheit gelesen habe, kann Bars() behoben werden, indem Zeitstempel referenziert werden. Könnte eine Suche wert sein.

CapeCoddah #:
Ich habe ein 3-Bildschirm-Setup mit der Hauptanzeige ganz links. Der Versuch, ein Panel, wie z.B. den Navigator oder die Markt-Panels, von links nach rechts zu verschieben, ist sehr mühsam. Der Mauszeiger befindet sich auf der linken Seite des Bildschirms, aber das zu verschiebende Panel ist in der Mitte. Ich denke, dass entweder das Terminal oder Windows verrückt spielt, wenn sich die Maus um ein Pixel bewegt und dann die Anzeige wechselt, um das Panel um ein Pixel zu verschieben und wieder zurück.

Ich weiß es wirklich nicht. Ich habe 3 Computer, jeder mit eigenem Monitor und Terminal. Ich weiß, dass Windows in der Regel über Einstellungen für die Anzeige auf mehreren Monitoren verfügt, einschließlich Bild-im-Bild vielleicht als Workaround.

Kann sich jemand anderes mit mehreren Monitoren auf einem einzigen Computer hier einschalten?

CapeCoddah
CapeCoddah | 16 Mai 2025 in 09:42

Tolle Informationen!!!

Danke Ryan, dein Kommentar bezüglich bars vs rates_total ist zutreffend. Mein Problem ist, dass die beiden im Terminal identisch sind, aber im STrategy Tester Visualize, Bars ist ein größerer, was zu meinem Bobo geführt hat, weil ich die Dokumentation nicht bis zum Ende gelesen habe. Ich werde deinen Input nehmen und ihn für iCustom verwenden. Ich nehme an, dass es für jede Kombination von Symbol- und Zeitangaben eine eigene iCustom-Adresse geben muss.

Gibt es auch eine Möglichkeit für einen EA, Text auf dem Bildschirm im Strategy Tester anzuzeigen? In Mq4 tat er das automatisch, aber jetzt nicht mehr. Ich verwende eine Menge Klassenobjekte, um Informationen anzuzeigen, und eine zweite Kopie in der Vorlage verlangsamt den Strategy Tester noch mehr.

Was die 3-Bildschirme betrifft, so denke ich, dass das Problem darin besteht, dass das Terminal die Monitorposition nicht richtig aktualisiert, wenn die Maus von Bildschirm 2 zu Bildschirm 1 wechselt.

Ich habe 2 Mini-PCs, die jeweils 3 Monitore unterstützen, also habe ich die 3 Bildschirme an beide Mini-PCs angeschlossen und verwende HDMI1 für den einen und HDMI2 für den anderen. Funktioniert hervorragend mit 43"-Fire-Fernsehern, obwohl man sicherstellen muss, dass die Fernbedienungen richtig konfiguriert sind, um nur einen Monitor zu steuern (rufen Sie den Amazon-Support an). Der einzige Nachteil ist, dass die Ein-Aus-Taste alle Monitore ausschaltet und ich manchmal den Stecker ziehen muss, um die Stromversorgung zu synchronisieren.


CapeCoddah

Silk Road Trading LLC
Ryan L Johnson | 16 Mai 2025 in 19:58
CapeCoddah STrategy Tester Visualize, Bars ist ein größeres, die zu meinem Bobo durch nicht lesen der Dokumentation bis zum Ende geführt. Ich werde Ihre Eingabe zu nehmen und verwenden Sie es für iCustom. Ich vermute, dass es eine separate iCustom-Adresse für jede Kombination von Symbol und Zeitangaben sein muss.

  1. Eine einzige Indikator-Datei in einem einzigen Verzeichnis kann von mehreren Instanzen von iCustom() wiederverwendet werden.
  2. Ein einzelnes Indikator-Handle kann von mehreren Instanzen von CopyBuffer() wiederverwendet werden.
  3. Ich verstehe jetzt, warum Sie Bars() verwenden, da rates_total allein auf einen einzigen Zeitrahmen beschränkt ist. Vermutlich verwenden Sie Bars() in einer separaten Schleife für jeden Zeitrahmen.

CapeCoddah #:
Gibt es auch eine Möglichkeit für einen EA, Text auf dem Bildschirm im Strategy Tester anzuzeigen? In Mq4 tat er das automatisch, aber jetzt nicht mehr. Ich verwende eine Menge Klassenobjekte, um Informationen anzuzeigen, und eine zweite Kopie in die Vorlage zu legen, verlangsamt den Strategy Tester noch mehr.
Nicht dass ich wüsste. Sie verwenden bereits die einzige Methode, die ich von der MT5-Hilfeseite Testing Visualization kenne.
CapeCoddah #:
Auf dem 3-Panel-Display liegt das Problem meiner Meinung nach darin, dass das Terminal die Monitorposition nicht richtig aktualisiert, wenn die Maus von Bildschirm 2 auf Bildschirm 1 wechselt.
Leider habe ich keine Möglichkeit, dies mit meinem eigenen Setup zu testen. Haben Sie einen einzigen MT5-Terminal-Bildschirm für alle Monitore verwendet? Ich habe gesehen, wie andere das Problem auf diese Weise gelöst haben.
Datenwissenschaft und ML (Teil 37): Mit Kerzenmustern und AI den Markt schlagen Datenwissenschaft und ML (Teil 37): Mit Kerzenmustern und AI den Markt schlagen
Kerzenmuster helfen Händlern, die Marktpsychologie zu verstehen und Trends auf den Finanzmärkten zu erkennen. Sie ermöglichen fundiertere Handelsentscheidungen, die zu besseren Ergebnissen führen können. In diesem Artikel werden wir untersuchen, wie man Kerzenmuster mit KI-Modellen nutzen kann, um eine optimale Handelsperformance zu erzielen.
Aufbau eines nutzerdefinierten Systems zur Erkennung von Marktregimen in MQL5 (Teil 1): Der Indikator Aufbau eines nutzerdefinierten Systems zur Erkennung von Marktregimen in MQL5 (Teil 1): Der Indikator
Dieser Artikel beschreibt die Erstellung eines MQL5-Systems zur Erkennung von Marktregimen unter Verwendung statistischer Methoden wie Autokorrelation und Volatilität. Es enthält Code für Klassen zur Klassifizierung von Trend-, Spannen- und Volatilitätsbedingungen sowie einen nutzerdefinierten Indikator.
Aufbau eines nutzerdefinierten Systems zur Erkennung von Marktregimen in MQL5 (Teil 2): Expert Advisor Aufbau eines nutzerdefinierten Systems zur Erkennung von Marktregimen in MQL5 (Teil 2): Expert Advisor
Dieser Artikel beschreibt den Aufbau eines adaptiven Expert Advisors (MarketRegimeEA) unter Verwendung des Regime-Detektors aus Teil 1. Er wechselt automatisch die Handelsstrategien und Risikoparameter für steigende, volatile oder Seitwärtsmärkte. Praktische Optimierung, Handhabung von Übergängen und ein Indikator für mehrere Zeitrahmen sind enthalten.
Datenwissenschaft und ML (Teil 36): Der Umgang mit verzerrten Finanzmärkten Datenwissenschaft und ML (Teil 36): Der Umgang mit verzerrten Finanzmärkten
Die Finanzmärkte sind nicht vollkommen ausgeglichen. Einige Märkte steigen, andere fallen, und wieder andere zeigen ein gewisses Schwankungsverhalten, das auf Unsicherheit in beide Richtungen hindeutet. Diese unausgewogenen Informationen können beim Trainieren von Machine-Learning-Modellen irreführend sein, da sich die Märkte häufig ändern. In diesem Artikel werden wir verschiedene Möglichkeiten erörtern, dieses Problem zu lösen.