Marktsimulation: Erste Schritte mit SQL in MQL5 (V)
Einleitung
Hallo und herzlich willkommen zu einem weiteren Artikel der Reihe darüber, wie man ein Replay-/Simulationssystem erstellt.
Im vorherigen Artikel Marktsimulation: Erste Schritte mit SQL in MQL5 (IV) haben wir eine kleine Vorsichtsmaßnahme beschrieben, die bei der Verwendung von SQLite in MetaTrader 5 beachtet werden sollte. Auch wenn sich dieses Verhalten in Zukunft ändern könnte, sollten Sie vorsichtshalber immer erst Tests durchführen, bevor Sie SQLite in der MetaTrader 5-Umgebung verwenden. Jedenfalls haben wir dort gezeigt, wie vorzugehen ist, um einen Abfragemechanismus hinzuzufügen, sodass wir im MQL5-Code SQL in vollem Umfang nutzen und Ergebnisse mithilfe von SQL-SELECT-Abfragen abrufen konnten.
Aber es gibt noch eine letzte Funktion, die wir besprechen und implementieren müssen. Dabei handelt es sich um die Funktion DatabaseReadBind. Da ein korrektes Verständnis dieser Funktion eine etwas ausführlichere Erläuterung erfordert, wurde beschlossen, dieses Thema nicht im vorherigen Artikel, sondern im heutigen Artikel zu behandeln. Da dieses Thema recht umfangreich sein wird, gehen wir also direkt zum nächsten Abschnitt über.
Implementierung der Funktion zum Lesen von Abfrageergebnissen
Die Implementierung der Funktion, die für das Lesen der von einer SQL-Abfrage zurückgegebenen Daten zuständig ist, ist relativ einfach, doch es gibt ein Detail, das man auf keinen Fall vergessen darf. Die Lesefunktion liefert genau die Ergebnisse des zuvor ausgeführten Aufrufs von ExecRequestOfData. Daher muss ExecRequestOfData immer aufgerufen werden, bevor die zurückgegebenen Daten gelesen werden.
An dieser Stelle sollten wir jedoch einen kurzen Exkurs einlegen. Um das besser zu verstehen, müssen wir uns zunächst den Klassencode ansehen. Der vollständige Code der Klasse C_DB_SQL ist unten aufgeführt. Beachten Sie, dass dieser Code für die Nutzung der integrierten SQL-Unterstützung in MetaTrader 5 vorgesehen ist. Für andere SQL-Anwendungsszenarien müsste dieser Code angepasst werden, um die erforderlichen Funktionen hinzuzufügen.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "..\Service Graphics\Support\C_Array.mqh" 005. //+------------------------------------------------------------------+ 006. class C_DB_SQL 007. { 008. private : 009. C_Array m_Arr; 010. int m_handleDB, 011. m_Request; 012. //+------------------------------------------------------------------+ 013. void Convert(const char &buff[], const int size) 014. { 015. string sz0 = ""; 016. bool b0, b1, bs1, bs2, bc0, bc1, bc; 017. int nLine = 1; 018. 019. b0 = b1 = bs1 = bs2 = bc0 = bc1 = bc = false; 020. for (int count = 0, nC0 = nLine; count < size; count++) 021. { 022. switch (buff[count]) 023. { 024. case '\t': 025. sz0 += (bs1 || bs2 ? "\t" : ""); 026. break; 027. case '\n': 028. nC0++; 029. case '\r': 030. bc0 = false; 031. break; 032. case ';': 033. b0 = (bs1 || bs2 || bc0 || bc1 ? b0 : true); 034. default: 035. switch (buff[count]) 036. { 037. case '"': 038. bs1 = (bs2 || bc0 || bc1 ? bs1 : !bs1); 039. break; 040. case '\'': 041. bs2 = (bs1 || bc0 || bc1 ? bs2 : !bs2); 042. break; 043. } 044. if (((count + 1) < size) && (!bs1) && (!bs2)) 045. { 046. if (bc = ((buff[count] == '-') && (buff[count + 1] == '-'))) bc0 = true; 047. if (bc = ((buff[count] == '/') && (buff[count + 1] == '*'))) bc1 = true; 048. if (bc = ((buff[count] == '*') && (buff[count + 1] == '/'))) bc1 = false; 049. if (bc) 050. { 051. count += 1; 052. bc = false; 053. continue; 054. } 055. } 056. if (!(bc0 || bc1)) 057. { 058. if ((!b1) && (buff[count] > ' ')) 059. { 060. b1 = true; 061. nLine = nC0; 062. } 063. sz0 += (b1 ? StringFormat("%c", buff[count]) : ""); 064. } 065. } 066. if (b0) 067. { 068. m_Arr.Add(sz0, nLine); 069. sz0 = ""; 070. b0 = b1 = false; 071. } 072. } 073. } 074. //+------------------------------------------------------------------+ 075. const string ExecSQL(void) 076. { 077. string szCmd; 078. 079. for (int count = 0, nLine; count >= 0; count++) 080. { 081. szCmd = m_Arr.At(count, nLine); 082. if (nLine < 0) break; 083. if (!ExecCommandSQL(szCmd)) 084. return StringFormat("Execution of line %d of the SQL script failed...", nLine); 085. } 086. 087. return NULL; 088. } 089. //+------------------------------------------------------------------+ 090. public : 091. //+------------------------------------------------------------------+ 092. C_DB_SQL(const string szFileName = ":memory:") 093. { 094. m_handleDB = DatabaseOpen(szFileName, DATABASE_OPEN_CREATE | DATABASE_OPEN_READWRITE); 095. m_Request = INVALID_HANDLE; 096. } 097. //+------------------------------------------------------------------+ 098. ~C_DB_SQL() 099. { 100. DatabaseClose(m_handleDB); 101. } 102. //+------------------------------------------------------------------+ 103. const string ExecResourceSQL(const string szResource) 104. { 105. char buff[]; 106. int size; 107. 108. ArrayResize(buff, size = StringLen(szResource)); 109. StringToCharArray(szResource, buff); 110. Convert(buff, size); 111. ArrayFree(buff); 112. 113. return ExecSQL(); 114. } 115. //+------------------------------------------------------------------+ 116. const string ExecScriptSQL(const string szFileName) 117. { 118. int file, size; 119. char buff[]; 120. 121. if ((file = FileOpen(szFileName, FILE_READ | FILE_BIN)) == INVALID_HANDLE) 122. return StringFormat("Unable to open script file: %s", szFileName); 123. ArrayResize(buff, size = (int) FileSize(file)); 124. FileReadArray(file, buff); 125. FileClose(file); 126. Convert(buff, size); 127. ArrayFree(buff); 128. 129. return ExecSQL(); 130. } 131. //+------------------------------------------------------------------+ 132. bool ExecCommandSQL(const string szCmd) 133. { 134. return (m_handleDB == INVALID_HANDLE ? false : DatabaseExecute(m_handleDB, szCmd)); 135. } 136. //+------------------------------------------------------------------+ 137. bool ExecRequestOfData(const string szCmd) 138. { 139. if (m_Request != INVALID_HANDLE) DatabaseFinalize(m_Request); 140. return ((m_Request = DatabasePrepare(m_handleDB, szCmd)) != INVALID_HANDLE); 141. } 142. //+------------------------------------------------------------------+ 143. template <typename T> bool GetRegisterOfRequest(T &stObject, const bool Finish = true) 144. { 145. if (!DatabaseReadBind(m_Request, stObject)) 146. { 147. if (Finish) 148. { 149. DatabaseFinalize(m_Request); 150. m_Request = INVALID_HANDLE; 151. } 152. return false; 153. } 154. 155. return true; 156. } 157. //+------------------------------------------------------------------+ 158. }; 159. //+------------------------------------------------------------------+
C_DB_SQL.mqh-Code
Das ist also der vollständige Klassencode. Da jedoch nur ein kleiner Teil noch nicht erläutert bzw. – genauer gesagt – noch nicht vorgestellt wurde, werden wir keine Funktionen oder Punkte untersuchen, die bereits in anderen Artikeln behandelt wurden. Hier werden wir uns ausschließlich auf die Funktion in Zeile 143 konzentrieren. Dies ist die Funktion GetRegisterOfRequest. Die erste und völlig berechtigte Frage lautet: Warum wird diese Funktion genau so deklariert? Dieser Punkt lässt sich allein anhand von MQL5 nur schwer erklären.
Um dies im Detail zu verstehen, müssten wir ein sehr komplexes Konzept erläutern, das in C/C++ vorkommt. Dieses Konzept steht im Zusammenhang mit generischen Objektreferenzen und – in C/C++ – mit Mechanismen, die häufig im Zusammenhang mit Zeigern vom Typ void erläutert werden. Ja, in C/C++ können wir mit generischen Referenzen und zeigerähnlichen Mechanismen arbeiten, nicht nur mit gewöhnlichen Typen, wie viele Leute glauben. Da es jedoch äußerst schwierig ist, dieses Konzept in einem Artikel zu erklären, werden wir versuchen, Ihnen, lieber Leser, zu verdeutlichen, warum diese Funktion auf diese Weise deklariert ist, und dabei eine Erörterung von C/C++ so weit wie möglich vermeiden.
Beginnen wir mit der Feststellung, dass es hier um eine Typabhängigkeit geht. Nicht in der Funktionsdeklaration in Zeile 143, sondern in der Funktion DatabaseReadBind. Diese Abhängigkeit zwingt uns dazu, einen bestimmten Datentyp zu verwenden, damit der MQL5-Compiler beim Kompilieren des Codes keine Flut von Fehlern ausgibt. Viele Programmierer, die DatabaseReadBind verwenden wollen oder dies versuchen, kapseln es nicht in einen Aufruf wie den, den wir hier verwenden. Der Grund dafür ist genau der, dass sie die hier auftretende Typabhängigkeit nicht verstehen.
Was tun sie also normalerweise? Sie erstellen eine Prozedur oder Funktion, in der die Abfrage ausgeführt wird, das von SQL zurückgegebene Ergebnis analysiert wird und schließlich der Wert auf die übliche Weise an den Aufrufer zurückgegeben wird. Dieser Ansatz funktioniert und eignet sich gut für ganz bestimmte Fälle. In anderen Fällen verwenden Programmierer DatabaseReadBind einfach direkt im Hauptcode, wann immer dies erforderlich ist.
Aber warum verfolgen sie diesen Ansatz? Auch hier liegt der Grund in dieser Typabhängigkeit. In MQL5 ist das nicht ganz so einfach zu handhaben. Bei der Verwendung von C/C++ ändert sich die Situation jedoch etwas, da C/C++ Low-Level-Mechanismen für den Umgang mit generischen Objektreferenzen bereitstellt, darunter auch void-Zeiger. Ich glaube, Sie haben diesen Punkt vielleicht noch nicht ganz verstanden. Schauen wir uns also einmal an, wie die Funktion DatabaseReadBind in MQL5 deklariert ist. Sie sehen sie unten:
bool DatabaseReadBind( int request, // handle of the query created in DatabasePrepare void& struct_object // reference to the structure for reading the record );
Was wir hier sehen, ist in der Dokumentation zu den MQL5-Funktionen zu finden. Es mag Ihnen vielleicht nicht ganz klar sein, aber für einen C/C++-Programmierer ist diese Deklaration durchaus nachvollziehbar. Ohne näher auf die Details von C/C++ einzugehen, lässt sich der Grundgedanke wie folgt zusammenfassen: Die Funktion muss zwei Parameter entgegennehmen. Der erste Parameter ist ein ganzzahliger Wert, der das von DatabasePrepare zurückgegebene Handle darstellt. Darüber haben wir bereits im vorherigen Artikel gesprochen. Die entscheidende Rolle spielt hier jedoch der zweite Parameter. Der zweite Parameter ist vom Typ void, was bedeutet, dass die Funktion eine Referenz auf ein Objekt entgegennimmt, ohne in der Signatur explizit einen konkreten Typ anzugeben.
Was hier jedoch übergeben wird, ist keine Adresse im üblichen Sinne, sondern eine Referenz auf ein Objekt im Speicher. Das liegt daran, dass Zeiger in der Regel auf eine Adresse im Speicher verweisen. Wenn wir jedoch das kaufmännische Und-Zeichen (&) vor einem Namen verwenden, zeigen wir damit an, dass wir uns auf eine Variable beziehen und nicht auf eine Speicherposition. Wenn Sie das wirklich verstehen möchten, sollten Sie lernen, wie man Zeiger in C/C++ verwendet, da dieses Thema in Lehrbüchern zu C/C++ oft mehrere Kapitel einnimmt. Das ist ein ziemlich komplexes und verwirrendes Thema.
Da MQL5 die Verwendung von Zeigern nicht auf dieselbe Weise wie C/C++ zulässt, benötigen wir einen kleinen Trick, um auf die Funktion DatabaseReadBind zuzugreifen, wie im Code der Klasse C_DB_SQL gezeigt. Dieser Trick besteht genau darin, einen Template-Typmechanismus zu nutzen, der es dem Compiler ermöglicht, den passenden Typ abzuleiten. Auf den ersten Blick mag das sehr kompliziert und schwer zu lösen erscheinen. Glücklicherweise bietet uns MQL5 die Möglichkeit, etwas zu nutzen, das es auch in C/C++ gibt, nämlich die Deklaration:
template <typename T >
Hier gibt es allerdings eine Einschränkung. T kann beliebig gewählt werden, aber in der Programmierung verwenden wir üblicherweise T als Konvention. Entscheidend ist, dass diese vor dem Funktionsnamen stehende Deklaration dem Compiler mitteilt, dass er die richtige Typumwandlung vornehmen muss, um das Typisierungsproblem zu lösen. Beachten Sie nun Folgendes: Der erste Parameter von GetRegisterOfRequest ist genau T. Da wir den Typ void nicht verwenden können, ermöglicht uns diese Deklaration im Grunde, den Parametertyp zu verallgemeinern, anstatt uns wie bei C/C++-Mustern auf niedrigerer Ebene auf void zu verlassen.
„Na gut. Aber ich kann diese Logik immer noch nicht nachvollziehen. Nach Durchsicht der MQL5-Dokumentation zu DatabaseReadBind stellen wir fest, dass dort angegeben ist, dass der zweite Parameter eine Struktur sein muss. Selbst in den Beispielen, die die Verwendung dieser Funktion veranschaulichen, wird als zweiter Parameter ebenfalls eine Struktur verwendet. Ich verstehe also nicht, warum es diese Komplexität mit der Verwendung von template <typename T > und dieser seltsamen Deklaration in Zeile 143 gibt. Warum übergibt man die Struktur nicht einfach direkt an diese Funktion? Warum muss das alles so kompliziert sein?“
In der Tat, lieber Leser, teile ich Ihre Ansicht in dieser Frage voll und ganz. Sie haben recht. Wir könnten einfach eine Struktur als Funktionsparameter übergeben, was die Deklaration in Zeile 143 erheblich vereinfachen würde. Aber nun möchte ich Sie fragen: Wie viele Felder sollte diese Struktur haben, und von welchem Typ sollten diese Felder sein? Wenn Sie diese Frage so beantworten können, dass sich die Logik verallgemeinern lässt, würde ich den Aufruf in Zeile 143 sicherlich anders umsetzen.
Diese Struktur wird jedoch niemals so aufgebaut sein, dass sich damit alle Situationen verallgemeinern lassen. Das liegt daran, dass sich diese Zahl früher oder später ändern kann, selbst wenn wir die Anzahl der in der Datenbank vorhandenen Felder kennen. In diesem Fall müsste die gesamte Implementierung im MQL5-Code überarbeitet werden. Aus diesem Grund nutzen viele Anwender DatabaseReadBind auf eine andere Weise als hier gezeigt.
Die Funktion, die wir hier vorstellen und deren Zweck darin besteht, das von SQL Zurückgegebene an unseren MQL5-Code zurückzugeben, ist eine generische Funktion. Sie werden bald verstehen, warum. Auf jeden Fall werden wir in Zeile 145 den Aufruf ausführen, und je nach Rückgabewert ergeben sich zwei völlig unterschiedliche Situationen. Der erste Fall tritt ein, wenn DatabaseReadBind einen von SQL zurückgegebenen Datensatz lesen kann. In diesem Fall wird Zeile 155 ausgeführt. Es kann jedoch vorkommen, dass in diesem Block keine Datensätze mehr übrig sind. Dann haben wir zwei verschiedene Situationen. Beide Fälle werden jedoch in dem Zweig behandelt, der ausgelöst wird, wenn DatabaseReadBind false zurückgibt.
Bitte beachten Sie, dass der zweite Parameter von GetRegisterOfRequest standardmäßig den Wert true hat. Wenn die Überprüfung in Zeile 145 fehlschlägt, prüfen wir in Zeile 147, ob der zweite Parameter true ist oder nicht. Sofern der Aufrufer ihn nicht geändert hat, wird in Zeile 149 DatabaseFinalize aufgerufen. Schließlich ändern wir in Zeile 150 den Wert der Variablen m_Request, um anzuzeigen, dass keine Datensätze mehr verfügbar sind und eine neue Abfrage an die Datenbank erforderlich ist. Wenn der Aufrufer jedoch angibt, dass der Parameter Finish false ist, schlägt die Überprüfung in Zeile 147 fehl, und alles bleibt so, wie es ist. Das wird später im Hauptcode nützlich sein. In jedem Fall ist der Rückgabewert false, und dies wird in Zeile 152 behandelt.
Damit schließen wir den Hauptteil der Klasse C_DB_SQL vollständig ab. Von nun an kann jeder Code sie in unserem Replay- und Simulationssystem verwenden. Bevor wir das jedoch tun, müssen wir sicherstellen, dass alles ordnungsgemäß funktioniert. Dazu gehen wir nun zu einem neuen Thema über, damit wir die verschiedenen Aspekte klarer voneinander trennen können.
Testen der Klasse C_DB_SQL für die Verwendung mit SQLite in MetaTrader 5
Der größte Teil unserer Klasse C_DB_SQL wurde bereits getestet. Jetzt bleibt nur noch, den Teil zu testen, der sich auf Abfragen bezieht, und irgendwie genau zu verstehen, was SQL bei der Ausführung einer Abfrage zurückgibt.
Um dies richtig zu erklären und es gleichzeitig für alle leicht verständlich zu machen – insbesondere für Sie, lieber Leser –, werden wir einen Ausschnitt aus dem Hauptcode zeigen, das erhaltene Ergebnis veranschaulichen und erläutern, was dabei geschieht und warum wir genau dieses Ergebnis erhalten. Gegen Ende des Artikels stellen wir den vollständigen Code bereit, damit Sie lokal testen können, was wir hier tun werden.
Sobald wir diesen Teil behandelt haben, fahren wir mit dem ersten Test fort. Dazu verwenden wir das folgende Fragment:
18. //+------------------------------------------------------------------+ 19. void SELECT_Type_01(void) 20. { 21. struct st 22. { 23. int id; 24. string symbol; 25. }stLocal; 26. 27. Print("Executing type 1 data request..."); 28. if ((*SQL).ExecRequestOfData("SELECT * FROM tb_Symbols")) 29. { 30. Print("Request: OK..."); 31. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++) 32. Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol); 33. Print("Reload..."); 34. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 35. Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol); 36. }; 37. Print("Finish type 1..."); 38. } 39. //+------------------------------------------------------------------+
Fragment aus der Hauptdatei
Die Zeilennummerierung entspricht genau dem, was wir im vollständigen Code sehen werden. Das bedeutet, dass Sie jedes Fragment in Ihrem eigenen Tempo durchgehen können. Das Ausführungsergebnis dieses Fragments ist in der folgenden Animation zu sehen:

Beachten Sie, dass dies recht einfach und klar ist. In Zeile 21 haben wir eine Struktur deklariert. Denken Sie daran, dass wir innerhalb dieser Struktur zwei Variablen haben werden. Sie sollten sie nicht auf diese Weise betrachten. Betrachten Sie sie nicht als Variablen, denn das würde Sie verwirren und es Ihnen später erschweren, andere Dinge zu verstehen. Sie sollten als Felder oder Spalten innerhalb der von SQL zurückgegebenen Ergebnismenge behandelt werden. Wir haben also eine Struktur, die als Zeile in der SQL-Ergebnismenge behandelt werden sollte. In dieser Ergebniszeile werden zwei Felder erwartet.
In Zeile 28 versuchen wir also, mithilfe von SQL Daten aus der Datenbank abzufragen. Achten Sie genau darauf, was zwischen den doppelten Anführungszeichen (") steht, denn dieser Befehl wird von SQL ausgeführt. Sie müssen genau das angeben, was Sie zur Ausführung an SQL übergeben möchten. In diesem einfachsten Beispiel benötigen wir lediglich alle Werte, die in der Tabelle „tb_Symbols“ enthalten sind. Auch wenn diese Art der Abfrageerstellung für den praktischen Einsatz nicht geeignet ist, reicht sie für unser Beispiel völlig aus, da unsere Tabelle sehr klein ist und nur wenige Datensätze enthält.
Wenn unsere Abfrage erfolgreich ist, führen wir zunächst die for-Schleife in Zeile 31 aus. Beachten Sie nun, dass der Grund für das Beenden der Schleife genau darin liegt, dass keine neuen Datensätze mehr zurückgegeben werden. Beachten Sie außerdem, dass wir bei diesen Aufrufen in Zeile 31 GetRegisterOfRequest mitteilen, dass wir die aktuelle Ergebnismenge noch nicht schließen oder finalisieren möchten. Mit anderen Worten: Wenn keine Datensätze mehr verfügbar sind, lassen wir die aktuelle Ergebnismenge offen und verfügbar.
Anschließend geben wir in Zeile 32 das zurückgegebene Ergebnis im Terminal aus. In Zeile 33 geben wir im Terminal an, dass wir die zurückgegebenen Daten noch einmal durchgehen werden. Beachten Sie, dass wir hier fast dasselbe tun wie in Zeile 31. Dieses Mal werden wir jedoch im zweiten Parameter des Aufrufs von GetRegisterOfRequest überhaupt nichts übergeben. Sobald der Block vollständig gelesen wurde, wird daher alles verworfen, was von der SQL-Abfrage zurückgegeben wurde, sodass wir eine neue Abfrage ausführen müssen.
Die nächste Implementierungsmethode wird im folgenden Codeausschnitt gezeigt:
39. //+------------------------------------------------------------------+ 40. void SELECT_Type_02(void) 41. { 42. struct st 43. { 44. string of_day; 45. double price; 46. string symbol; 47. }stLocal; 48. 49. Print("Executing type 2 data request..."); 50. if ((*SQL).ExecRequestOfData("SELECT tq.of_day, tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 51. { 52. Print("Request: OK..."); 53. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 54. Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol); 55. } 56. Print("Finish type 2..."); 57. } 58. //+------------------------------------------------------------------+
Hauptcodeausschnitt
Das Ergebnis ist in der folgenden Animation zu sehen:

Beachten Sie, dass sich hier ein etwas anderes Bild ergibt. Zunächst deklarieren wir in Zeile 42 eine Struktur, die drei Felder enthält. Im vorherigen Fall hatte jede Zeile zwei Felder. Bitte beachten Sie jedoch, dass wir trotz der erhöhten Anzahl an Feldern nach wie vor Code verwenden, der dem vorherigen sehr ähnlich ist. Dieses Mal möchte ich jedoch, dass Sie sich den SQL-Befehl in Zeile 50 genau ansehen. Dort verknüpfen wir die Tabellen „tb_Quotes“ und „tb_Symbols“, um auf eine bestimmte Art und Weise darzustellen, was tatsächlich in der Datenbank vorhanden ist.
Achten Sie nun auf die Reihenfolge, in der die Felder in der SQL-Abfrage angegeben sind. Dies ist wichtig, da DatabaseReadBind beim Lesen der von SQL zurückgegebenen Daten genau dieser Reihenfolge folgt. Wenn wir also die Reihenfolge entweder im SQL-Befehl oder bei der Deklaration der Felder innerhalb der Struktur in Zeile 42 ändern, können wir in den zurückgegebenen Daten völlig unerwartete Ergebnisse sehen. Übersehen Sie solche Details also nicht.
Nun haben wir einen Fall, der auf den ersten Blick etwas komplexer erscheint. Aber lassen Sie sich nicht vom ersten Eindruck täuschen: Es ist genauso einfach wie alles andere auch. Wir werden lediglich die in MQL5 verfügbaren Funktionen etwas detaillierter nutzen, die dazu dienen, die Verwendung von SQL innerhalb von MQL5 zu ermöglichen. Das Fragment sehen Sie unten.
58. //+------------------------------------------------------------------+ 59. void SELECT_Type_03(void) 60. { 61. struct st0 62. { 63. double price; 64. string symbol; 65. }stLocal; 66. struct st1 67. { 68. string tmp1, 69. tmp2, 70. date; 71. }std; 72. 73. Print("Executing type 3 data request..."); 74. if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol, tq.of_day FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 75. { 76. Print("Request: OK..."); 77. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++) 78. Print(" Resq ", c, " ==> { ", stLocal.price, " } ", stLocal.symbol); 79. Print("Reload..."); 80. for (int c = 0; (*SQL).GetRegisterOfRequest(std); c++) 81. Print(" Resq ", c, " ==> < ", std.date, " > "); 82. 83. } 84. Print("Finish type 3..."); 85. } 86. //+------------------------------------------------------------------+
Hauptcodeausschnitt
Das Ausführungsergebnis dieses Codeausschnitts ist unten zu sehen.

Bisher haben wir eine Struktur verwendet, die einer Ergebniszeile mit zwei oder drei Feldern entspricht. Wie wir jedoch bereits im vorherigen Abschnitt erläutert haben, können wir noch viel mehr tun, nämlich dafür sorgen, dass SQL mehr Felder zurückgibt, als wir benötigen. Und indem wir die Felder, die wir tatsächlich benötigen, sorgfältig analysieren, können wir das nutzen, was SQL bereits zurückgegeben hat. Auf diese Weise müssen wir keine neue Abfrage erstellen, um die benötigten Daten zu finden, da diese bereits zusammen mit der vorherigen Abfrage geliefert wurden.
Hier gibt es ein wichtiges Detail, das man verstehen muss. Entscheidend ist, dass jede von SQL zurückgegebene Zeile die Werte der entsprechenden Felder enthält. Wie Sie jedoch in diesem Beispiel sehen werden, führen wir dieselbe Abfrage aus wie im vorherigen Ausschnitt. Allerdings gibt die Abfrage die Felder nun in einer anderen Reihenfolge zurück. Es ist sehr wichtig, dass Sie darauf achten. Der Grund dafür ist, dass die in Zeile 61 deklarierte Struktur zwei Felder enthält. Die in Zeile 66 deklarierte Struktur enthält drei Felder. Die SQL-Abfrage gibt drei Felder zurück. Das geschieht, weil der SQL-Befehl SQL anweist, diese drei Felder zurückzugeben.
Beachten Sie nun, dass wir von der Struktur st0 zur Struktur st1 wechseln. Das Feld namens date fehlt jedoch in der Struktur st0 und wird daher ignoriert, wenn wir mit dem Einlesen der von SQL zurückgegebenen Daten fortfahren. Dies geschieht in Zeile 77, wo wir eine Schleife erstellen, um die von SQL zurückgegebenen Daten zu lesen und im Terminal auszugeben.
In Zeile 80 machen wir jedoch dasselbe. Diesmal ignorieren wir jedoch die übrigen Daten, die von der SQL-Abfrage zurückgegeben werden, und zeigen nur das Feld date an. Das ist gar nicht so kompliziert. Es geht vor allem darum, zu verstehen, was wir tun und was SQL zurückgibt.
Sie denken jetzt sicher schon, dass wir tun können, was wir wollen, und dass unabhängig von unserem Vorgehen trotzdem ein Ergebnis erzielt wird. In Wirklichkeit trifft das jedoch nicht ganz zu. Um dies zu verdeutlichen, sehen Sie sich den folgenden Ausschnitt an:
086. //+------------------------------------------------------------------+ 087. void SELECT_Type_04(void) 088. { 089. struct st 090. { 091. string of_day; 092. double price; 093. string symbol; 094. }stLocal; 095. 096. Print("Executing type 4 data request..."); 097. if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 098. { 099. Print("Request: OK..."); 100. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 101. Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol); 102. } 103. Print("Finish type 4..."); 104. } 105. //+------------------------------------------------------------------+
Hauptcodeausschnitt
Das Ausführungsergebnis dieses Ausschnitts ist unten dargestellt; auf den ersten Blick wirkt es etwas seltsam, da wir ein völlig unerwartetes Ergebnis erhalten.

Das mag Ihnen etwas seltsam vorkommen. Das ist jedoch nichts Ungewöhnliches: Schauen Sie sich einfach die Deklaration in Zeile 89 an, in der wir genau angeben, welche Felder wir durch die SQL-Abfrage erhalten wollen. Schauen Sie sich nun Zeile 97 an und das, was wir von SQL abfragen. Beachten Sie, dass die erwartete Ergebnisstruktur nicht mit den von SQL abgefragten Spalten übereinstimmt.
Hier stellt sich eine Frage, die uns erneut dazu zwingt, die MQL5-Dokumentation zu konsultieren, um zu verstehen, warum das Ergebnis genau so ausgefallen ist, wie es in der Animation gezeigt wird. Wenn wir also in der Dokumentation der zugrunde liegenden Funktion nachschlagen, die von GetRegisterOfRequest verwendet wird, nämlich DatabaseReadBind, sehen wir den folgenden hervorgehobenen Ausschnitt:
Hinweis
Die Anzahl der Felder in der Struktur struct_object darf den Wert von DatabaseColumnsCount() nicht überschreiten. Wenn die Anzahl der Felder in struct_object kleiner ist als die Anzahl der Felder im Datensatz, erfolgt ein teilweises Auslesen. Die verbleibenden Daten können explizit mithilfe der entsprechenden Funktionen DatabaseColumnText(), DatabaseColumnInteger() usw. abgerufen werden.
„Aber Moment mal, wir verwenden DatabaseColumnsCount doch gar nicht. Deshalb ergibt das Ergebnis keinen Sinn.“ Nun, tatsächlich ergibt es Sinn, denn in der SQL-Abfrage geben wir eindeutig an, dass wir zwei Felder erhalten möchten. Beachten Sie, dass wir in Zeile 97 das Feld price und das Feld symbol abfragen, die jeweils aus einer anderen Tabelle stammen. Da GetRegisterOfRequest keine Methoden verwendet, um die Daten anzupassen und zwei der drei in der Struktur angegebenen Felder zu füllen, ignoriert DatabaseReadBind einfach jeden Versuch, die Daten den Feldern in der Struktur zuzuordnen. Das Ergebnis entspricht also genau dem, was in der Animation zu sehen ist.
Wir können das noch verbessern, aber vorerst bleibt es so, wie es ist, da wir noch einen weiteren Fall berücksichtigen müssen. Dies lässt sich am folgenden Ausschnitt erkennen.
105. //+------------------------------------------------------------------+ 106. void SELECT_Type_05(void) 107. { 108. struct st 109. { 110. double price; 111. }stLocal; 112. 113. Print("Executing type 5 data request..."); 114. if ((*SQL).ExecRequestOfData("SELECT tq.price FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = (SELECT ts.id FROM tb_Symbols WHERE ts.symbol = 'PETR4') AND tq.of_day = '2023-07-11';")) 115. { 116. Print("Request: OK..."); 117. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 118. Print(" Resq ", c, " ==> { ", stLocal.price, " } "); 119. } 120. Print("Finish type 5..."); 121. } 122. //+------------------------------------------------------------------+
Hauptcodeausschnitt
Als Ergebnis der Ausführung erhalten wir genau Folgendes:

Dies ist ein etwas extremerer Fall der Verwendung von SQL, bei dem wir SQL genau das tun lassen, wovon Sie vielleicht gedacht haben, dass es außerhalb von SQL erledigt werden sollte. Hier formulieren wir jedoch lediglich eine sehr spezifische Abfrage, um einen bestimmten Datensatz aus der Datenbank abzurufen. Achten Sie nun auf Zeile 114. Sie enthält den SQL-Befehl, den SQL ausführen muss. Bitte beachten Sie, dass wir Kurse für das Instrument PETR4 abfragen, und zwar speziell für den 11.07.2023. Natürlich entspricht das Datum dem von SQL erwarteten Format. Aber ich denke, Sie verstehen inzwischen bereits, wie SQL funktioniert.
Wenn wir uns die Datenbank ansehen, erhalten wir dasselbe Ergebnis wie im Terminal bei der Ausführung der SQL-Abfrage. In solchen Situationen muss nur ein Feld zurückgegeben werden. Beachten Sie, wie dies in Zeile 108 deklariert wird.
Hier könnte sich jedoch eine durchaus berechtigte Frage stellen: Könnten wir nicht einfach Zeile 110 verwenden, ohne die in Zeile 108 gezeigte Struktur zu deklarieren? Leider nein, lieber Leser. Nicht so, wie GetRegisterOfRequest deklariert wurde. Der entscheidende Punkt ist: Wenn wir versuchen würden, nur Zeile 110 zu verwenden, ohne eine Struktur anzulegen, würden wir die über DatabaseReadBind ausgeführte Lesefunktion tatsächlich nicht aufrufen. Dann würden wir etwas anderes verwenden. Als Programmierer können wir jedoch Code schreiben, der sowohl solche Fälle als auch die im vorherigen Ausschnitt beschriebene Situation bewältigen kann.
Auf jeden Fall können Sie diese Abfragen – genauer gesagt die SQL-Befehle, die wir hier betrachtet haben – direkt im MetaEditor testen, um noch etwas mehr mit der Erstellung von Abfragen zu experimentieren, die darauf ausgelegt sind, Ergebnisse wie die in diesem letzten Ausschnitt zu erhalten. Vergessen Sie jedoch nicht, dasselbe auch im endgültigen Code zu testen, der als MetaTrader-5-Anwendung verwendet wird. Den vollständigen Code zum Testen finden Sie unten:
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. #property description "Basic script for SQL database written in MQL5" 004. #property version "1.00" 005. #property script_show_inputs 006. //+------------------------------------------------------------------+ 007. #resource "\\Files\\Script 01.sql" as string SQL_Create 008. #resource "\\Files\\Script 02.sql" as string SQL_Insert 009. //+------------------------------------------------------------------+ 010. #include <Market Replay\SQL\C_DB_SQL.mqh> 011. //+------------------------------------------------------------------+ 012. enum eCall {eType_00, eType_01, eType_02, eType_03, eType_04, eType_05}; 013. //+------------------------------------------------------------------+ 014. input string user01 = "DataBase01"; //Database File Name 015. input ECall user02 = eType_01; //Search type 016. //+------------------------------------------------------------------+ 017. C_DB_SQL *SQL; 018. //+------------------------------------------------------------------+ 019. void SELECT_Type_01(void) 020. { 021. struct st 022. { 023. int id; 024. string symbol; 025. }stLocal; 026. 027. Print("Executing type 1 data request..."); 028. if ((*SQL).ExecRequestOfData("SELECT * FROM tb_Symbols")) 029. { 030. Print("Request: OK..."); 031. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++) 032. Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol); 033. Print("Reload..."); 034. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 035. Print(" Resq ", c, " ==> ", stLocal.id, " - ", stLocal.symbol); 036. }; 037. Print("Finish type 1..."); 038. } 039. //+------------------------------------------------------------------+ 040. void SELECT_Type_02(void) 041. { 042. struct st 043. { 044. string of_day; 045. double price; 046. string symbol; 047. }stLocal; 048. 049. Print("Executing type 2 data request..."); 050. if ((*SQL).ExecRequestOfData("SELECT tq.of_day, tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 051. { 052. Print("Request: OK..."); 053. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 054. Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol); 055. } 056. Print("Finish type 2..."); 057. } 058. //+------------------------------------------------------------------+ 059. void SELECT_Type_03(void) 060. { 061. struct st0 062. { 063. double price; 064. string symbol; 065. }stLocal; 066. struct st1 067. { 068. string tmp1, 069. tmp2, 070. date; 071. }std; 072. 073. Print("Executing type 3 data request..."); 074. if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol, tq.of_day FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 075. { 076. Print("Request: OK..."); 077. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal, false); c++) 078. Print(" Resq ", c, " ==> { ", stLocal.price, " } ", stLocal.symbol); 079. Print("Reload..."); 080. for (int c = 0; (*SQL).GetRegisterOfRequest(std); c++) 081. Print(" Resq ", c, " ==> < ", std.date, " > "); 082. 083. } 084. Print("Finish type 3..."); 085. } 086. //+------------------------------------------------------------------+ 087. void SELECT_Type_04(void) 088. { 089. struct st 090. { 091. string of_day; 092. double price; 093. string symbol; 094. }stLocal; 095. 096. Print("Executing type 4 data request..."); 097. if ((*SQL).ExecRequestOfData("SELECT tq.price, ts.symbol FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = ts.id ORDER BY price DESC;")) 098. { 099. Print("Request: OK..."); 100. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 101. Print(" Resq ", c, " ==> ", stLocal.of_day, " { ", stLocal.price, " } ", stLocal.symbol); 102. } 103. Print("Finish type 4..."); 104. } 105. //+------------------------------------------------------------------+ 106. void SELECT_Type_05(void) 107. { 108. struct st 109. { 110. double price; 111. }stLocal; 112. 113. Print("Executing type 5 data request..."); 114. if ((*SQL).ExecRequestOfData("SELECT tq.price FROM tb_Quotes AS tq, tb_Symbols AS ts WHERE tq.fk_id = (SELECT ts.id FROM tb_Symbols WHERE ts.symbol = 'PETR4') AND tq.of_day = '2023-07-11';")) 115. { 116. Print("Request: OK..."); 117. for (int c = 0; (*SQL).GetRegisterOfRequest(stLocal); c++) 118. Print(" Resq ", c, " ==> { ", stLocal.price, " } "); 119. } 120. Print("Finish type 5..."); 121. } 122. //+------------------------------------------------------------------+ 123. const string ExecScripts(void) 124. { 125. string szMsg = (*SQL).ExecResourceSQL(SQL_Create); 126. if (szMsg != NULL) return szMsg; 127. return (*SQL).ExecResourceSQL(SQL_Insert); 128. }; 129. //+------------------------------------------------------------------+ 130. void OnStart() 131. { 132. string szMsg = NULL; 133. 134. SQL = new C_DB_SQL(user01); 135. 136. if (user02 == eType_00) szMsg = ExecScripts(); 137. Print(szMsg == NULL ? "Result of executing the SQL script: Success" : szMsg); 138. 139. if (szMsg == NULL) switch (user02) 140. { 141. case eType_01: SELECT_Type_01(); break; 142. case eType_02: SELECT_Type_02(); break; 143. case eType_03: SELECT_Type_03(); break; 144. case eType_04: SELECT_Type_04(); break; 145. case eType_05: SELECT_Type_05(); break; 146. } 147. 148. delete SQL; 149. } 150. //+------------------------------------------------------------------+
Hauptcode in MQL5
Beachten Sie, dass alle Erläuterungen im Code enthalten sind und dieselbe Zeilennummerierung aufweisen. Der einzige kleine Unterschied zum Code, den wir in den vorherigen Artikeln gesehen haben, besteht in Zeile 139, wo wir – ausgehend von der in Zeile 15 deklarierten Variablen – den Typ auswählen, genauer gesagt, welcher Ausschnitt ausgeführt werden soll. Auf diese Weise können Sie dasselbe sehen wie in den einzelnen Animationen.
Abschließende Gedanken
In diesem Artikel haben wir gezeigt, wie man die von einem SQL-Befehl zurückgegebenen Daten bei Verwendung von SQLite in MetaTrader 5 ausliest. Wie Sie vielleicht beim Lesen des Artikels bemerkt haben, können wir unsere Klasse C_DB_SQL zwar noch weiter verbessern und dabei weiterhin den Schwerpunkt auf die Arbeit mit dem in MetaTrader 5 integrierten SQLite legen. Die wichtigsten Verbesserungen, die wir vornehmen können, sind folgende:
- Es soll möglich sein, die in der MQL5-Struktur definierten Felder auch dann zu füllen, wenn wir eine Abfrage mit weniger Spalten ausführen. Dadurch würde das unangenehme Ergebnis vermieden, dass ein Aufruf von GetRegisterOfRequest keine Informationen liefert.
- Eine weitere Verbesserung wäre, dass wir bei einer Abfrage, deren Ergebnis ein einzelner für uns relevanter Datensatz ist – wie im Ausschnitt SELECT_Type_05 –, die zurückgegebene Variable nicht innerhalb einer Struktur deklarieren müssten, sondern sie stattdessen direkt als normale, eigenständige Variable verwenden könnten.
Obwohl die Umsetzung dieser Verbesserungen interessant ist, bin ich mir noch nicht sicher, ob ich zeigen werde, wie das geht. Das liegt daran, dass wir bei der Programmierung einer Anwendung in MQL5 etwas vorsichtiger vorgehen können. Sollte jedenfalls entschieden werden, zu zeigen, wie diese Änderungen vorgenommen werden können, um die in den Ausschnitten dargestellten Fälle besser zu unterstützen, wird dies im nächsten Artikel geschehen. Unabhängig davon sollten Sie auf jeden Fall den nächsten Artikel dieser Reihe verfolgen, denn das Thema wird von Tag zu Tag interessanter.
| Datei | Beschreibung |
|---|---|
| Experts\Expert Advisor.mq5 | Veranschaulicht die Interaktion zwischen Chart Trade und dem Expert Advisor (für die Interaktion ist Mouse Study erforderlich). |
| Indicators\Chart Trade.mq5 | Erstellt ein Fenster zur Konfiguration der zu sendenden Order (für die Interaktion ist Mouse Study erforderlich). |
| Indicators\Market Replay.mq5 | Erstellt Steuerelemente für die Interaktion mit dem Replikations-/Simulationsdienst (für die Interaktion ist Mouse Study erforderlich). |
| Indicators\Mouse Study.mq5 | Ermöglicht die Interaktion zwischen grafischen Steuerelementen und dem Benutzer (sowohl für das Replay-/Simulationssystem als auch am realen Markt erforderlich). |
| Services\Market Replay.mq5 | Erstellt und verwaltet den Dienst für die Marktreplikation/-simulation (die Hauptdatei des gesamten Systems). |
| Code VS C++\Servidor.cpp | Erstellt und verwaltet einen in C++ entwickelten Socket-Server (Mini-Chat-Version). |
| Code in Python\Server.py | Erstellt und verwaltet einen Python-Socket-Server für die Kommunikation zwischen MetaTrader 5 und Excel. |
| Indicators\Mini Chat.mq5 | Ermöglicht die Implementierung eines Mini-Chats über einen Indikator (erfordert die Nutzung des Servers). |
| Experts\Mini Chat.mq5 | Ermöglicht die Implementierung eines Mini-Chats mithilfe eines Expert Advisors (erfordert den Server). |
| Scripts\SQLite.mq5 | Veranschaulicht die Verwendung eines SQL-Skripts über MQL5. |
| Files\Script 01.sql | Zeigt die Erstellung einer einfachen Tabelle mit einem Fremdschlüssel. |
| Files\Script 02.sql | Zeigt, wie man Werte in eine Tabelle einfügt. |
Übersetzt aus dem Portugiesischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/pt/articles/13117
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Neuronale Netze im Trading: Zerlegung strukturierter Komponenten (SCNN)
Marktsimulation: Erste Schritte mit SQL in MQL5 (I)
Neuronale Netze im Trading: Zerlegung strukturierter Komponenten (Encoder)
Vom Anfänger zum Fortgeschrittenen: Objekte (I)
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.