Integration von MQL-basierten Expert Advisors und Datenbanken (SQL Server, .NET und C#)

Сергей Ткаченко | 12 September, 2018

Einführung, Experten für MQL und Datenbanken

In den Foren erscheinen oft Fragen zur Integration einer Datenbank in einen Expert Advisor in MQL5. Das Interesse an diesem Thema ist nicht überraschend. Datenbanken sind sehr gut geeignet, Daten zu speichern. Im Gegensatz zu den Logdateien des Terminals verschwinden die Daten einer Datenbank nicht. Sie sind einfach zu sortieren, zu filtern und gewünschten lassen sich leicht auswählen. Eine Datenbank kann verwendet werden, um die notwendigen Informationen an einen Experten weiterzugeben - zum Beispiel bestimmte Befehle. Und vor allem - die gewonnenen Daten können aus verschiedenen Perspektiven analysiert und statistisch verarbeitet werden. Das Schreiben einer einzeiligen Abfrage reicht beispielsweise aus, um den durchschnittlichen und den Gesamtgewinn für eine bestimmte Zeit für jedes Währungspaar zu ermitteln. Und stellen Sie sich nun vor, wie lange es dauern würde, wenn Sie dies für die Handelshistorie eines Kontos im Terminal manuell einrichten müssten.

Leider bietet MetaTrader keine integrierten Werkzeuge für die Verwendung von Datenbankservern. Das Problem kann nur durch den Import von Funktionen aus DLL-Dateien gelöst werden. Die Aufgabe ist nicht einfach, aber machbar.

Ich habe dies mehrmals getan, und ich habe beschlossen, meine Erfahrungen in diesem Artikel zu teilen. So ist beispielsweise die Interaktion von MQL5-Experten mit dem Microsoft SQL Server Datenbankserver organisiert. Um eine DLL-Datei für die Experten zu erstellen, die Funktionen für die Arbeit mit der Datenbank importieren, wurde die Microsoft.NET-Plattform und die Sprache C# verwendet. Der Artikel beschreibt den Prozess der Erstellung und Vorbereitung einer DLL-Datei und anschließend den Import ihrer Funktionen in einen in MQL5 geschriebenen Experten. Der als Beispiel angegebene Expertencode ist sehr einfach. Es sind nur minimale Änderungen erforderlich, damit er auch in MQL4 kompiliert werden kann.

Arbeitsvorbereitung

Die folgenden Punkte werden für die Arbeit benötigt.

  1. Ein installiertes MetaTrader 5 Terminal mit einem aktiven Handelskonto. Sie können nicht nur Demokonten, sondern auch reale Konten verwenden - die Verwendung des Testexperten stellt kein Risiko für die Einzahlung dar.
  2. Ein installierte Instanz des Microsoft SQL Server Datenbankservers. Sie können eine Datenbank auf einem anderen Computer verwenden und sich über das Netzwerk mit ihr verbinden. Die kostenlose Express Edition kann von der Microsoft-Website heruntergeladen werden, ihre Einschränkungen sind für die meisten Benutzer nicht von Bedeutung. Sie können es von hier herunterladen: https://www.microsoft.com/en-us/sql-server/sql-server-editions-express. Microsoft ändert aber manchmal die Links auf seiner Website. Wenn der direkte Link nicht funktioniert, geben Sie einfach einen Ausdruck wie "SQL Server Express download" in eine Suchmaschine ein. Wenn Sie SQL Server zum ersten Mal installieren, kann es zu einigen Schwierigkeiten bei der Installation kommen. Insbesondere kann es erforderlich sein, dass zusätzliche Komponenten (insbesondere PowerShell und .NET 4.5) auf älteren Versionen des Betriebssystems installiert werden. Außerdem können SQL Server und VS C++ 2017 manchmal Konflikte verursachen, wobei das Installationsprogramm die Wiederherstellung von С++ anfordert. Sie können dies über "Systemsteuerung", "Programme", "Programme und Funktionen", "VS C++ 2017", "Ändern", "Reparieren" tun. Diese Probleme treten nicht immer auf und können leicht gelöst werden.
  3. Integrierte Entwicklungsumgebung mit .NET und C#. Ich benutze persönlich Microsoft Visual Studio (das auch eine kostenlose Version hat), daher werden die Beispiele dafür angegeben. Sie können eine andere Entwicklungsumgebung und sogar eine andere Programmiersprache verwenden. Aber dann müssen Sie sich überlegen, wie Sie die gegebenen Beispiele in der Umgebung und der Sprache Ihrer Wahl umsetzen können.
  4. Tool zum Exportieren von Funktionen aus DLL-Dateien, die in .NET geschrieben wurden, für andere Programmiersprachen. MQL-basierte Experten können nicht mit .NET-Code arbeiten. Daher sollte die resultierende DLL-Datei speziell vorbereitet werden, um Funktionen exportieren zu können. Im Web werden verschiedene Möglichkeiten beschrieben, dies zu erreichen. Ich habe das Paket "UnmanagedExports" von Robert Giesecke verwendet. Wenn Sie Microsoft Visual Studio Version 2012 oder höher verwenden, können Sie es direkt aus dem IDE-Menü zum Projekt hinzufügen. Wie dies erreicht wird, wird weiter unten besprochen.

Neben der Installation der erforderlichen Programme ist es auch notwendig, eine weitere vorbereitende Operation durchzuführen. Aus verschiedenen Gründen kann "UnmanagedExports" nicht funktionieren, wenn in den Spracheinstellungen Ihres Computers "Russisch (Russland)" als Sprache für Nicht-Unicode-Programme ausgewählt ist. Es kann auch mit anderen Sprachen Probleme geben, es sei denn, es handelt sich um "Englisch (US)". Um das zu installieren, öffnen Sie das Bedienfeld. Navigieren Sie zur Registerkarte "Regional and Language Options" und gehen Sie dann zur Registerkarte "Advanced". Sie auf der Registerkarte "language for non-Unicode programs" auf "Change system locale...". Wenn dort "English (US)" eingestellt ist, ist alles in Ordnung. Wenn es noch etwas gibt, ändern Sie es in "English (US)" und starten Sie den Computer neu.

Wenn dies nicht geschieht, tritt bei der Kompilierung des Projekts in der Ausführungsphase der Skripte "UnmanagedExports" ein Syntaxfehler in den Dateien ".il" auf. Diese können nicht korrigiert werden. Selbst wenn Ihr Projekt sehr einfach ist und es sicherlich keine Fehler im C#-Code gibt, treten immer noch Fehler in ".il"-Dateien auf, und dann kann man die Funktionen aus dem Projekt nicht in nicht verwalteten Code exportieren.

Dies gilt nur für 64-Bit-Anwendungen. 32-Bit-Anwendungen können mit anderen Mitteln behandelt werden, die keine Änderung des Gebietsschemas (system locale) erfordern. Sie können beispielsweise das Programm DllExporter.exe verwenden, das Sie hier herunterladen können: https://www.codeproject.com/Articles/37675/Simple-Method-of-DLL-Export-without-C-CLI.

Das Ändern des Gebietsschemas führt dazu, dass einige Anwendungen inoperabel sind. Leider müssen diese Unannehmlichkeiten überwunden werden, aber es dauert nicht lange. Das Ändern des Gebietsschemas ist nur notwendig, wenn Sie das Projekt kompilieren. Nach erfolgreicher Kompilierung kann das Gebietsschema wieder zurückgeschaltet werden.

Erstellen einer DLL-Datei

Öffnen Sie Visual Studio und erstellen Sie ein neues Visual C#-Projekt, indem Sie "Class Library" als Typ auswählen. Wir nennen es MqlSqlDemo. In den Projekteigenschaften, im Abschnitt "Build", ist es notwendig, das "Platform target" zu konfigurieren. Dort ist es notwendig, "Any CPU" auf "x64" zu ändern (sowohl in der Debug-Konfiguration als auch in der Release-Konfiguration). Dies liegt an den Besonderheiten beim Export von Funktionen in 'unmanaged' Code - die Angabe des Prozessortyps ist zwingend erforderlich.

Installieren Sie das.NET-Framework Version 4.5. Es ist in der Regel standardmäßig ausgewählt.

Beim Erstellen eines Projekts wird die Datei "Class1.cs" mit der Klasse "Class1" automatisch dem Projekt hinzugefügt. Benennen Sie sowohl die Datei als auch die Klasse in "MqlSqlDemo.cs" und "MqlSqlDemo" um. Funktionen, die aus der DLL-Datei exportiert werden sollen, können nur statisch sein - dies ist wiederum für den Export in nicht 'unmanaged' Code erforderlich.

Streng genommen können Sie auch nicht-statische Funktionen exportieren. Aber um dies zu tun, ist es notwendig, auf C++/CLI-Tools zu verweisen, die in diesem Artikel nicht berücksichtigt werden.

Da alle Funktionen in unserer Klasse statisch sein müssen, ist es logisch, die Klasse selbst statisch zu machen. In diesem Fall, wenn für eine Funktion der Modifikator "static" fehlt, wird dies sofort beim Kompilieren des Projekts festgestellt. Wir erhalten die folgende Klassendefinition:

public static class MqlSqlDemo
{
    // ...
}

Nun müssen die Abhängigkeiten des Projekts konfiguriert werden (Abschnitt "References" im "Solution Explorer"). Wir entfernen alle redundanten Optionen, so dass nur "System" und "System.Data" übrig bleiben.

Jetzt kann das Paket "UnmanagedExports" hinzugefügt werden.

Die Beschreibung des Pakets steht auf der Seite des Autors: https://sites.google.com/site/robertgiesecke/Home/uploads/unmanagedexports.

Der einfachste Weg, es hinzuzufügen, ist die Verwendung des NuGet-Paketmanagers. Anweisungen zum Hinzufügen finden Sie auf der NuGet-Seite: https://www.nuget.org/packages/UnmanagedExports

In unserem Fall wird nur eine dieser Anweisungen benötigt:

Install-Package UnmanagedExports -Version 1.2.7

Wählen Sie im Menü von Visual Studio die Abschnitte "Tools", dann "NuGet Package Manager" und "Package Manager Console". Die Befehlszeile wird unten angezeigt. Fügen Sie dort die kopierte Anleitung "Install-Package UnmanagedExports - Version 1.2.7" ein und drücken Sie "Enter". Der Paketmanager verbindet sich für eine Weile mit dem Internet, lädt das Paket herunter, fügt es dann dem Projekt hinzu und druckt Folgendes aus:

PM> Install-Package UnmanagedExports -Version 1.2.7
Installing 'UnmanagedExports 1.2.7'.
Successfully installed 'UnmanagedExports 1.2.7'.
Adding 'UnmanagedExports 1.2.7' to MqlSqlDemo.
Successfully added 'UnmanagedExports 1.2.7' to MqlSqlDemo.

PM> 

Dies bedeutet, dass das Paket erfolgreich hinzugefügt wurde.

Danach können wir den Code direkt in die Klassendefinitionsdatei MqlSqlDemo.cs schreiben.

Konfigurieren wir die verwendeten Namensräume (namespaces).

Hier sind die resultierenden Einstellungen:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Runtime.InteropServices;
using RGiesecke.DllExport;

Hinzufügen der benötigten Variablen. Variablen in einer 'static' Klasse können auch 'static' sein. Wir benötigen die Objekte für die Arbeit mit der Datenbank — das Objekt für die Verbindung und für die Anweisungen:

private static SqlConnection conn = null;
private static SqlCommand com = null;

Wir benötigen auch eine Zeichenkette für die detaillierten Nachrichten der Ergebnisse der Funktionsausführung:

private static string sMessage = string.Empty;

Wir deklarieren Konstanten mit den Werten 0 und 1 — sie dienen als Rückgabewerte der meisten Funktionen. Ist eine Funktionsausführung erfolgreich wird 0 zurückgegeben, sonst — 1. Dadurch ist der Code leichter zu verstehen.

public const int iResSuccess = 0;
public const int iResError = 1;

Jetzt zu den Funktionen.

Für Funktionen, die für die Verwendung in MQL5 exportiert werden sollen, gibt es Einschränkungen.

  1. Wie bereits erwähnt, sollten die Funktionen 'static' sein.
  2. Vorlagen für Klassen von Kollektionen (die System.Collections.Generic Namensraum) sind verboten. Der Code, der sie enthält, wird normal kompiliert, aber unerklärliche Fehler können zur Laufzeit auftreten. Diese Klassen können in anderen Funktionen verwendet werden, die nicht importiert werden, aber es ist besser, auf sie komplett zu verzichten. Man kann normale Arrays verwenden. Unser Projekt ist nur zu Informationszwecken geschrieben, so dass es keine solchen Klassen (sowie Arrays) enthält.

Dieses Demo-Projekt wird nur mit einfachen Datentypen - Zahlen oder Zeichenketten - funktionieren. Theoretisch wäre es auch möglich, boolsche Werte zu übergeben, die auch intern als ganze Zahlen dargestellt werden. Die Werte dieser Zahlen können jedoch von verschiedenen Systemen (MQL und .NET) unterschiedlich interpretiert werden. Dies führt zu Fehlern. Daher beschränken wir uns auf drei Arten von Daten - int, string und double. Gegebenenfalls sollten boolesche Werte als int übergeben werden.

In realen Projekten ist es möglich, komplexe Datenstrukturen zu übergeben, aber wir benötigen das nicht für die Arbeit mit dem SQL Server.

Um mit der Datenbank arbeiten zu können, müssen wir zunächst eine Verbindung herstellen. Dies geschieht mit der Funktion CreateConnection. Die Funktion benötigt einen Parameter - eine Zeichenkette mit den Parametern, um eine Verbindung zur SQL Server-Datenbank herzustellen. Es wird eine Ganzzahl zurückgegeben, die anzeigt, ob die Verbindung erfolgreich war. Wenn die Verbindung erfolgreich war, wird iResSuccess zurückgegeben, d.h. 0. Wenn es fehlschlägt - iResError, d.h. 1. Detailliertere Informationen werden in den Nachrichtenstring eingefügt - sMessage.

Hier ist das Ergebnis:

[DllExport("CreateConnection", CallingConvention = CallingConvention.StdCall)]
    public static int CreateConnection(
            [MarshalAs(UnmanagedType.LPWStr)] string sConnStr)
    {
        // Zeichenkette der Nachricht löschen:
        sMessage = string.Empty;
        // Bei Verbindung - schließen und erneuern
        // der Zeichenkette der Verbindung, wenn nicht -
        // erneuern der Objekte von Verbindung und Command:
        if (conn != null)
        {
                conn.Close();
                conn.ConnectionString = sConnStr;
        }
        else
        {
                conn = new SqlConnection(sConnStr);
                com = new SqlCommand();
                com.Connection = conn;
        }
        // Öffnungsversuch der Verbindung:
        try
        {
                conn.Open();
        }
        catch (Exception ex)
        {
                // Verbindung öffnete sich nicht, warum auch immer.
                // Fehlermeldung in die Zeichenkette der Nachricht schreiben:
                sMessage = ex.Message;
                // Ressourcen freigeben und Objekte zurücksetzen:
                com.Dispose();
                conn.Dispose();
                conn = null;
                com = null;
                // Error:
                return iResError;
        }
        // Alles ging gut, Verbindung ist offen:
        return iResSuccess;
}

Jede zu exportierende Funktion wird durch das Attribut DllExport vor der Funktionsdefinition gekennzeichnet. Sie befindet sich im Namensraum RGiesecke.DllExport, die aus der Zusammenstellung RGiesecke.DllExport.Metadata importiert wurde. Die Zusammenstellung wird dem Projekt automatisch hinzugefügt, wenn der NuGet-Manager das Paket UnmanagedExports installiert. An dieses Attribut sollten zwei Parameter übergeben werden:

  • Der Funktionsname, unter dem es exportiert werden soll. Dieser Name wird von externen Programmen (einschließlich MetaTrader 5) verwendet, um ihn aus der DLL aufzurufen. Der Name der exportierten Funktion kann mit dem Funktionsnamen im Code - CreateConnection übereinstimmen;
  • Der zweite Parameter gibt an, welcher Funktionsaufrufmechanismus verwendet wird. CallingConvention.StdCall ist für alle unsere Funktionen geeignet.

Achten Sie auf das Attribut [MarshalAs(UnmanagedType.LPWStr)]. Er steht vor dem Parameter ConnStringIn, einer Zeichenkette, der von der Funktion übernommen wird. Dieses Attribut zeigt, wie die Zeichenkette gesendet werden soll. Zum Zeitpunkt der Erstellung dieses Artikels arbeiten MetaTrader 5 und MetaTrader 4 mit Unicode Zeichenketten - UnmanagedType.LPWStr.

Zum Zeitpunkt des Funktionsaufrufs kann der Text, der den Fehler beim vorherigen Verbindungsversuch beschreibt, im Nachrichtenstring verbleiben, so dass die Zeichenkette zu Beginn der Funktion gelöscht wird. Außerdem kann die Funktion aufgerufen werden, wenn die vorherige Verbindung noch nicht geschlossen ist. Überprüfen wir daher zunächst, ob die Verbindungs- und Befehlsobjekte vorhanden sind. In diesem Fall können wir die Verbindung schließen und und die Objekte wiederverwenden. Wenn nicht, müssen neue Objekte angelegt werden.

Open wird für die Verbindung verwendete und liefert kein Ergebnis. Daher ist es nur durch das Abfangen von Ausnahmen möglich, herauszufinden, ob die Verbindung erfolgreich war. Im Fehlerfall geben wir die Ressourcen frei, setzen die Objekte auf Null, schreiben die Informationen in den Nachrichtenstring und geben iResError zurück. Wenn alles in Ordnung ist, geben wir iResSuccess zurück.

Wenn die Verbindung nicht geöffnet werden kann, muss der Roboter die im sMessage-String enthaltene Nachricht lesen, um die Fehlerursache zu ermitteln. Dazu fügen Sie die Funktion GetLastMessage hinzu. Sie wird eine Zeichenkette mit der Nachricht zurückgegeben:

[DllExport("GetLastMessage", CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.LPWStr)]
public static string GetLastMessage()
{
        return sMessage;
}

Wie die Funktion zum Verbindungsaufbau ist auch diese Funktion mit dem Exportattribut DllExport gekennzeichnet. Das Attribut [return: MarshalAs(UnmanagedType.LPWStr)] gibt an, wie das zurückgegebene Ergebnis übergeben werden soll. Da das Ergebnis eine Zeichenkette ist, sollte sie auch in Unicode an MetaTrader 5 übergeben werden. Daher wird auch hier UnmanagedType.LPWStr verwendet.

Sobald die Verbindung geöffnet ist, können wir mit der Datenbank arbeiten. Fügen wir die Möglichkeit hinzu, Abfragen in der Datenbank auszuführen. Dies geschieht mit der Funktion ExecuteSql:

[DllExport("ExecuteSql", CallingConvention = CallingConvention.StdCall)]
public static int ExecuteSql(
        [MarshalAs(UnmanagedType.LPWStr)] string sSql)
{
        // Zeichenkette der Nachricht löschen:
        sMessage = string.Empty;
        // Zuerst prüfen, ob die Verbindung steht.
        if (conn == null)
        {
                // Die Verbindung ist noch nicht offen.
                // Fehlermeldung und Rückgabe des Fehlerflags:
                sMessage = "Connection is null, call CreateConnection first.";
                return iResError;
        }
        // Die Verbindung steht, versuchen wir die Ausführung eines Befehls.
        try
        {
                com.CommandText = sSql;
                com.ExecuteNonQuery();
        }
        catch (Exception ex)
        {
                // Fehler bei der Befehlsausführung.
                // Fehlermeldung in die Zeichenkette der Nachricht schreiben:
                sMessage = ex.Message;
                // Rückgabe des Fehlerflags:
                return iResError;
        }
        // Alles ging gut - Rückgabe des Erfolgsflags:
        return iResSuccess;
}

Der Abfragetext wird über einen Parameter der Funktion übergeben. Überprüfen wir vor dem Ausführen der Abfrage, ob die Verbindung geöffnet ist. Wie bei der Funktion zum Öffnen einer Verbindung gibt diese Funktion iResSuccess zurück, wenn sie erfolgreich ist und iResError im Fehlerfall. Um genauere Informationen darüber zu erhalten, was den Fehler verursacht hat, ist es notwendig, die Funktion GetLastMessage zu verwenden. Mit der Funktion ExecuteSql können beliebige Abfragen ausgeführt werden - Daten schreiben, löschen oder ändern. Es ist auch möglich, mit der Datenbankstruktur zu arbeiten. Leider erlaubt es kein Lesen von Daten - die Funktion gibt kein Ergebnis zurück und speichert die gelesenen Daten nirgendwo. Die Abfrage wird ausgeführt, aber wir können nicht sehen, was gelesen wurde. Deshalb haben wir zwei weitere Funktionen zum Lesen von Daten hinzugefügt.

Die erste Funktion ist so konzipiert, dass sie eine ganze Zahl aus der Datenbanktabelle liest.

[DllExport("ReadInt", CallingConvention = CallingConvention.StdCall)]
public static int ReadInt(
        [MarshalAs(UnmanagedType.LPWStr)] string sSql)
{
        // Zeichenkette der Nachricht löschen:
        sMessage = string.Empty;
        // Zuerst prüfen, ob die Verbindung steht.
        if (conn == null)
        {
                // Die Verbindung ist noch nicht offen.
                // Fehlermeldung und Rückgabe des Fehlerflags:
                sMessage = "Connection is null, call CreateConnection first.";
                return iResError;
        }
        // Variable für die Ergebnisrückgabe:
        int iResult = 0;
        // Die Verbindung steht, versuchen wir die Ausführung eines Befehls.
        try
        {
                com.CommandText = sSql;
                iResult = (int)com.ExecuteScalar();
        }
        catch (Exception ex)
        {
                // Fehler bei der Befehlsausführung.
                // Fehlermeldung in die Zeichenkette der Nachricht schreiben:
                sMessage = ex.Message;
        }
        // Rückgabe des erhaltenen Ergebnisses:
        return iResult;
}

Das Lesen von Daten ist viel schwieriger zu implementieren als das einfache Ausführen von Befehlen. Diese Funktion ist stark vereinfacht und verwendet die Funktion ExecuteScalar der KlasseSqlCommand. Sie gibt den Wert der ersten Spalte der ersten Zeile zurück, die von der Abfrage zurückgegeben wird. Daher sollte die vom Parameter übergebene SQL-Abfrage so gestaltet sein, dass der zurückgegebene Datensatz Zeilen aufweist und die erste Spalte eine ganze Zahl enthält. Darüber hinaus sollte die Funktion irgendwie die gelesene Zahl zurückgeben. Daher wird sein Ergebnis nicht mehr eine Nachricht über den Erfolg der Ausführung sein. Um zu verstehen, ob die Abfrage erfolgreich war und um die Daten zu lesen, ist es auf jeden Fall notwendig, die letzte zu analysieren, indem man GetLastMessage aufruft. Wenn die letzte Nachricht leer ist, dann ist kein Fehler aufgetreten und die Daten wurden gelesen. Wenn dort etwas geschrieben wird, bedeutet das, dass ein Fehler aufgetreten ist und die Daten nicht gelesen werden konnten.

Die zweite Funktion liest auch einen Wert aus der Datenbank, aber von einem anderen Typ - nicht eine ganze Zahl, sondern eine Zeichenkette. Zeichenketten können genau wie Zahlen gelesen werden; der Unterschied liegt nur in der Art des zurückgegebenen Ergebnisses. Da die Funktion eine Zeichenkette zurückgibt, sollte sie mit dem Attribut [return: MarshalAs(UnmanagedType.LPWStr)] markiert werden. Hier ist der Code für diese Funktion:

[DllExport("ReadString", CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.LPWStr)]
public static string ReadString(
        [MarshalAs(UnmanagedType.LPWStr)] string sSql)
{
        // Zeichenkette der Nachricht löschen:
        sMessage = string.Empty;
        // Zuerst prüfen, ob die Verbindung steht.
        if (conn == null)
        {
                // Die Verbindung ist noch nicht offen.
                // Fehlermeldung und Rückgabe des Fehlerflags:
                sMessage = "Connection is null, call CreateConnection first.";
                return string.Empty;
        }
        // Variable für die Ergebnisrückgabe:
        string sResult = string.Empty;
        // Die Verbindung steht, versuchen wir die Ausführung eines Befehls.
        try
        {
                com.CommandText = sSql;
                sResult = com.ExecuteScalar().ToString();
        }
        catch (Exception ex)
        {
                // Fehler bei der Befehlsausführung.
                // Fehlermeldung in die Zeichenkette der Nachricht schreiben:
                sMessage = ex.Message;
        }
        // Rückgabe des erhaltenen Ergebnisses:
        return sResult;
}

Solche Datenlesefähigkeiten sind für ein Demo-Projekt ausreichend. Für einen echten Experten könnte es überhaupt nicht notwendig sein - es ist wichtiger, dass die Experten Daten zur weiteren Analyse in die Datenbank schreiben. Wenn es dennoch notwendig ist, die Daten zu lesen, können diese Funktionen verwendet werden - sie sind durchaus arbeitsfähig. Manchmal müssen jedoch viele Zeilen aus der Tabelle gelesen werden, die mehrere Spalten enthalten.

Dies kann auf zwei Arten geschehen. Aus der Funktion können komplexe Datenstrukturen zurückgeben werden (diese Möglichkeit ist für MQL4 nicht geeignet). Es kann auch eine statische Variable der Klasse DataSet in unserer Klasse deklariert werden. Zum Lesen müssen die Daten aus der Datenbank in dieses DataSet geladen werden und sie dann mit anderen Funktionen von da auszulesen, je Zelle ein Funktionsaufruf. Dieser Ansatz wird im unten genannten Projekt HerdOfRobots umgesetzt. Er kann im Projektcode detailliert untersucht werden. Um den Artikel nicht aufzublähen, wird das Lesen von Daten mehrerer Zeilen hier nicht berücksichtigt.

Nachdem die Arbeit mit der Datenbank abgeschlossen ist, muss die Verbindung geschlossen und die verwendeten Ressourcen freigegeben werden. Dies geschieht durch die Funktion CloseConnection:

[DllExport("CloseConnection", CallingConvention = CallingConvention.StdCall)]
public static void CloseConnection()
{
        // Zuerst prüfen, ob die Verbindung steht.
        if (conn == null)
                // Die Verbindung ist noch nicht offen - heißt, sie muss nicht geschlossen werden:
                return;
        // Die Verbindung ist offen - sie muss geschlossen werden.
        com.Dispose();
        com = null;
        conn.Close();
        conn.Dispose();
        conn = null;
}

Diese einfache Funktion nimmt keine Parameter an und liefert kein Ergebnis.

Alle notwendigen Funktionen sind bereit. Kompilieren wir das Projekt.

Da die Funktion nicht aus anderen.NET-Anwendungen, sondern aus dem MetaTrader (der kein.NET verwendet) verwendet werden soll, erfolgt die Kompilierung in zwei Schritten. Im ersten Schritt wird alles wie bei allen .NET-Projekten gemacht. Es wird ein normaler Build erstellt, der später vom UnmanagedExports-Paket verarbeitet wird. Das Paket beginnt zu funktionieren, nachdem der Build kompiliert wurde. Zuerst startet der IL-Decompiler, der den resultierenden Build in einen IL-Code zerlegt. Der IL-Code wird geändert - Referenzen auf die DllExport-Attribute werden entfernt und Anweisungen für den Export der durch dieses Attribut markierten Funktionen hinzugefügt. Danach wird die Datei mit dem IL-Code neu kompiliert und die ursprüngliche DLL überschrieben.

Alle diese Aktionen werden automatisch ausgeführt. Aber, wie oben erwähnt, wenn Russisch für Nicht-Unicode-Programme in den Betriebssystemeinstellungen ausgewählt ist, werden Versuche, die Datei mit dem geänderten IL-Code zu kompilieren, wahrscheinlich zu einen Fehler von UnmanagedExports führen und nichts wird durchgeführt.

Wenn bei der Kompilierung keine Fehlermeldungen entstehen, dann ist alles gut gelaufen und die erhaltene DLL kann von Experten verwendet werden. Wenn UnmanagedExports eine DLL erfolgreich verarbeitet, fügt es außerdem zwei weitere Dateien mit den Erweiterungen ".exp" und ".lib" hinzu (in diesem Fall "MqlSqlDemo.exp" und "MqlSqlDemo.lib"). Wir brauchen sie nicht, aber ihre Anwesenheit kann darauf hinweisen, dass UnmanagedExports erfolgreich abgeschlossen wurden.

Es sei darauf hingewiesen, dass das Demoprojekt eine sehr große Einschränkung hat: Es erlaubt, nur einen Experten, der mit der Datenbank arbeitet, in einem MetaTrader-Terminal auszuführen. Alle Experten verwenden eine Instanz der geladenen DLL. Da unsere Klasse statisch gemacht ist, wird sie die einzige für alle laufenden Experten sein. Die Variablen werden auch gemeinsam sein. Wenn Sie mehrere Experten ausführen, verwenden alle die gleiche Verbindung und das gleiche Befehlsobjekt. Wenn mehrere Experten versuchen, diese Objekte gleichzeitig zu behandeln, können Probleme auftreten.

Aber ein solches Projekt reicht aus, um die Funktionsweise zu erklären und die Verbindung zur Datenbank zu testen. Jetzt haben wir eine DLL-Datei mit den Funktionen. Wir können mit dem Schreiben eines Experten für MQL5 fortfahren.

Erstellen eines Expert Advisors in MQL5

Erstellen wir einen einfachen Experten für MQL5. Sein Code kann auch im MQL4-Editor kompiliert werden, indem die Erweiterung von "mq5" auf "mq4" geändert wird. Dieser Experte dient nur zur Demonstration der erfolgreichen Arbeit mit der Datenbank, er führt daher keine Handelsoperationen durch.

Starten Sie den MetaEditor, drücken Sie die Schaltfläche "Neu". Wählen Sie "Expert Advisor (Vorlage)" und drücken Sie "Weiter". Geben Sie den Namen "MqlSqlDemo" an. Füge auch einen Parameter hinzu - "ConnectionString" vom Typ "string". Dies ist die Verbindungszeichenfolge, die angibt, wie Sie sich mit Ihrem Datenbankserver verbinden. So können Sie beispielsweise diesen Initialwert für den Parameter einstellen:

Server=localhost;Database=master;Integrated Security=True

Diese Zeichenkette für die Verbindung ermöglicht die Verbindung zu einem unbenannten ("Default Instance") Datenbankserver, der auf demselben Computer installiert ist, auf dem das MetaTrader-Terminal läuft. Es ist nicht erforderlich, Login und Passwort anzugeben — es wird die Autorisierung durch das Windows-Konto verwendet.

Wenn Sie SQL Server Express heruntergeladen und auf Ihrem Computer installiert haben, ohne die Parameter zu ändern, dann ist Ihr SQL Server eine "benannte Instanz". Sie erhält den Namen "SQLEXPRESS". Sie wird eine andere Zeichenkette für die Verbindung erhalten:

Server=localhost\\SQLEXPRESS;Database=master;Integrated Security=True

Wenn Sie einen Zeichenkettenparameter zur Vorlage des Expert Advisors hinzufügen, ist die Länge der Zeichenkette beschränkt. Eine längere Zeichenkette für die Verbindung (z.B. zu einem benannten Server "SQLEXPRESS") passt möglicherweise nicht. Aber das ist kein Problem - der Parameterwert kann zu diesem Zeitpunkt leer gelassen werden. Er kann später bei der Bearbeitung des Expertencodes auf einen beliebigen Wert geändert werden. Es ist auch möglich, beim Start des Experten die gewünschte Zeichenkette der Verbindung anzugeben.

Klicken Sie auf "Weiter". Es müssen keine weiteren Funktionen hinzugefügt werden, also lassen wir alle Kontrollkästchen auf dem nächsten Bildschirm deaktiviert. Drücken Sie erneut "Weiter" und erhalten Sie den generierten Anfangscode des Experten.

Der Zweck des Experten ist es nur, die Verbindung zur Datenbank nachzuweisen und damit zu arbeiten. Dazu genügt es, nur die Initialisierungsfunktion — OnInit — zu verwenden. Entwürfe für andere Funktionen — OnDeinit und OnTick — können sofort entfernt werden.

Als Ergebnis erhalten wir folgendes:

//+------------------------------------------------------------------+ //|                                                   MqlSqlDemo.mq5 | //|                        Copyright 2018, MetaQuotes Software Corp. | //|                                             https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2018, MetaQuotes Software Corp." #property link      "https://www.mql5.com" #property version   "1.00" #property strict //--- Eingabeparameter input string   ConnectionString = "Server=localhost\\SQLEXPRESS;Database=master;Integrated Security=True"; //+------------------------------------------------------------------+ //| Initialisierungsfunktion des Experten                            | //+------------------------------------------------------------------+ int OnInit()   { //--- //---    return(INIT_SUCCEEDED);   }

Bitte beachten Sie: Bei der Verbindung zu einer benannten Instanz (in diesem Fall "SQLEXPRESS") ist es notwendig, das Zeichen "\" zweimal zu wiederholen: "localhost\\SQLEXPRESS". Dies ist sowohl beim Hinzufügen des Parameters zur Expertenvorlage als auch im Code erforderlich. Wird das Zeichen nur einmal angegeben, behandelt der Compiler es so, als ob die Escape-Sequenz (Sonderzeichen) "\S" in der Zeichenkette angegeben wäre, und meldet, dass es bei der Kompilierung nicht erkannt wurde.

Wenn Sie jedoch einen kompilierten Roboter an ein Diagramm anhängen, haben seine Parameter nur ein "\"-Zeichen, obwohl zwei von ihnen im Code angegeben sind. Dies geschieht, weil alle Escape-Sequenzen in Zeichenketten während der Kompilierung in entsprechende Zeichen umgewandelt werden. Die Sequenz "\\" wird in ein einziges "\"-Zeichen umgewandelt, und Benutzer (die nicht mit dem Code arbeiten müssen) sehen die normale Zeichenkette. Wenn Sie also die Verbindungszeichenfolge nicht im Code, sondern beim Start des Expert Advisors angeben, sollte nur ein einziges "\"-Zeichen in der Verbindungszeichenfolge angegeben werden.

Server=localhost\SQLEXPRESS;Database=master;Integrated Security=True

Fügen wir das nun dem Entwurf des Experten die Funktionen hinzu. Zunächst ist es notwendig, die Funktionen für die Arbeit mit der Datenbank aus der erstellten DLL zu importieren. Fügen wir den Importabschnitt vor der Funktion OnInit() hinzu. Die importierten Funktionen werden fast so beschrieben, wie sie im C#-Code deklariert sind. Es ist nur notwendig, alle Modifikatoren und Attribute zu entfernen:

// Beschreibung der importierten Funktionen.
#import "MqlSqlDemo.dll"

// Funktion zum Öffnen der Verbindung:
int CreateConnection(string sConnStr);
// Funktion zum Lesen der letzten Nachricht:
string GetLastMessage();
// Funktion zum Ausführen eines Befehls:
int ExecuteSql(string sSql);
// Funktion zum Lesen einer Ganzzahl:
int ReadInt(string sSql);
// Funktion zum Lesen einer Zeichenkette:
string ReadString(string sSql);
// Funktion zum Öffnen der Verbindung:
void CloseConnection();

// Ende des Imports:
#import

Zur besseren Übersichtlichkeit des Codes deklarieren wir die Konstanten der Ergebnisse der Ausführung von Funktionen. Wie in der DLL ist dies 0 bei erfolgreicher Ausführung und 1 bei einem Fehler:

// Erfolgreiche Ausführung einer Funktion
#define iResSuccess  0
// Fehler bei Funktionsausführung:
#define iResError 1

Jetzt können wir Funktionsaufrufe zur Datenbankabfrage der Initialisierungsfunktion OnInit() hinzufügen. So sieht das dann aus:

int OnInit()
  {
   // Versuch einer Verbindungseröffnung:
   if (CreateConnection(ConnectionString) != iResSuccess)
   {
      // Fehler bei Verbindungseröffnung.
      // Nachricht ausdrucken und beenden:
      Print("Error when opening connection. ", GetLastMessage());
      return(INIT_FAILED);
   }
   Print("Connected to database.");
   // Die Verbindung wurde erfolgreich eröffnet.
   // Versuch eine Ausführung einer Abfrage.
   // Tabelle erstellen und Einschreiben der Daten:
   if (ExecuteSql(
      "create table DemoTest(DemoInt int, DemoString nvarchar(10));")
      == iResSuccess)
      Print("Created table in database.");
   else
      Print("Failed to create table. ", GetLastMessage());
   if (ExecuteSql(
      "insert into DemoTest(DemoInt, DemoString) values(1, N'Test');")
      == iResSuccess)
      Print("Data written to table.");
   else
      Print("Failed to write data to table. ", GetLastMessage());
   // Lesen der Daten. Lesen einer Ganzzahl aus der Datenbank:
   int iTestInt = ReadInt("select top 1 DemoInt from DemoTest;");
   string sMessage = GetLastMessage();
   if (StringLen(sMessage) == 0)
      Print("Number read from database: ", iTestInt);
   else // Failed to read number.
      Print("Failed to read number from database. ", GetLastMessage());
   // Lesen einer Zeichenkette:
   string sTestString = ReadString("select top 1 DemoString from DemoTest;");
   sMessage = GetLastMessage();
   if (StringLen(sMessage) == 0)
      Print("String read from database: ", sTestString);
   else // Failed to read string.
      Print("Failed to read string from database. ", GetLastMessage());
   // Die Tabelle wird nicht mehr benötigt - sie kann gelöscht werden.
   if (ExecuteSql("drop table DemoTest;") != iResSuccess)
      Print("Failed to delete table. ", GetLastMessage());
   // Aufgabe erfüllt - Verbindung schließen:
   CloseConnection();
   // Initialisierung beenden:
   return(INIT_SUCCEEDED);
  }

Kompilieren wir den Experten. Das war's, der Testexperte ist bereit. Wir können ihn laufen lassen. Bevor wir den Experten ausführen, ist es notwendig, die DLL in den Bibliotheksordner des von uns verwendeten MetaTrader-Profils hinzuzufügen. Starten Sie MetaTrader, wählen Sie im Menü "Datei" die Option "Datenordner öffnen". Öffnen Sie den Ordner "MQL5" (für MetaTrader 4 den Ordner "MQL4"), dann den Ordner "Libraries". Kopieren Sie die erstellte DLL-Datei (MqlSqlDemo.dll) in diesen Ordner. Der Experte sollte bereits zu diesem Zeitpunkt kompiliert und einsatzbereit sein. Natürlich sollte das Ausführen eines Expert Advisors und das Importieren von Funktionen aus der DLL in den MetaTrader 5 Einstellungen erlaubt sein, sonst gibt es sofort beim Start einen Fehler.

Starten Sie den Experten und ändern Sie die Werte der Verbindungszeichenfolge in die Zugriffsparameter Ihres Datenbankservers. Wenn alles richtig gemacht wird, gibt der Experte folgendes im Protokoll aus:

2018.07.10 20:36:21.428    MqlSqlDemo (EURUSD,H1)    Connected to database.
2018.07.10 20:36:22.187    MqlSqlDemo (EURUSD,H1)    Created table in database.
2018.07.10 20:36:22.427    MqlSqlDemo (EURUSD,H1)    Data written to table.
2018.07.10 20:36:22.569    MqlSqlDemo (EURUSD,H1)    Number read from database: 1
2018.07.10 20:36:22.586    MqlSqlDemo (EURUSD,H1)    String read from database: Test

Die Verbindung zur Datenbank, das Ausführen der SQL-Kommandos, das Lesen und Schreiben der Daten — alles wurde erfolgreich ausgeführt.

Schlussfolgerung

Eine Komplettlösung für Visual Studio - das Archiv mit allen notwendigen Dateien ist dem Artikel unter dem Namen "MqlSqlDemo.zip" angehängt. Das Paket "UnmanagedExports" ist bereits installiert. Der Testexperte MqlSqlDemo.mq5 und seine Variante für MQL4 befinden sich im Unterordner "MQL".

Der in diesem Artikel beschriebene Ansatz ist voll funktionsfähig. Basierend auf den oben genannten Prinzipien wurden Anwendungen entwickelt, die es ermöglichen, mit Tausenden und sogar Zehntausenden von gleichzeitig gestarteten Experten zu arbeiten. Alles wurde mehrfach getestet und funktioniert bis heute.

Sowohl die DLL-Datei als auch der in diesem Artikel erstellte Experte sind nur für Bildungs- und Evaluierungszwecke bestimmt. Natürlich können Sie die DLL auch in realen Projekten verwenden. Es ist jedoch sehr wahrscheinlich, dass sie aufgrund seiner erheblichen Einschränkungen bald nicht mehr ausreicht. Wenn Sie Ihren Experten die Möglichkeit geben möchten, mit Datenbanken zu arbeiten, werden Sie höchstwahrscheinlich mehr Funktionen benötigen. In diesem Fall ist es notwendig, den Code selbst hinzuzufügen, wobei dieser Artikel als Beispiel dient. Wenn Sie irgendwelche Schwierigkeiten haben, bitten Sie um Hilfe in den Kommentaren zum Artikel, kontaktiere Sie mich über Skype (cansee378) oder über die Kontaktseite auf meiner Website: http://life-warrior.org/contact.

Wenn Sie nicht die Zeit oder den Wunsch haben, den C#-Code durchzugehen, können Sie das fertige Projekt herunterladen. Das kostenlose Open-Source-Programm namens HerdOfRobots ist nach den gleichen Prinzipien implementiert, die im Artikel beschrieben sind. Das Installationspaket enthält die kompletten Dateien mit den importierten Funktionen sowohl für MetaTrader 5 als auch für MetaTrader 4 zusammen mit dem Programm. Diese Bibliotheken verfügen über wesentlich leistungsfähigere Funktionen. So können beispielsweise bis zu 63 Experten in einem Terminal (das mit verschiedenen Datenbanken verbunden sein kann) arbeiten, Daten aus Tabellen Zeile für Zeile lesen, Datums-/Uhrzeitwerte in die Datenbank schreiben.

Das Programm HerdOfRobots bietet komfortable Funktionen zur Steuerung der mit der Datenbank verbundenen Experten und zur Analyse der von ihnen geschriebenen Daten. Das Paket enthält ein Handbuch, das alle Aspekte der Arbeit sehr detailliert beschreibt. Das Archiv mit dem Programm Installer - SetupHerdOfRobots.zip - ist ebenfalls dem Artikel beigefügt. Wenn Sie den Code des Programms sehen möchten, das für die Verbindung zur Datenbank des MqlToSql64-Projekts (MqlToSql für MT4) verwendet wird, um die genannten erweiterten Funktionen später in Ihren Projekten zu nutzen, kann der Code kostenlos aus offenen Repositorien heruntergeladen werden:

https://bitbucket.org/CanSeeThePain/herdofrobots

https://bitbucket.org/CanSeeThePain/mqltosql64

https://bitbucket.org/CanSeeThePain/mqltosql