Verwendung von Netzwerkfunktionen oder MySQL ohne DLL: Teil II - Programm zur Überwachung von Änderungen der Signaleigenschaften
Inhalt
- Einführung
- Datenerfassung
- Anwendung zur Anzeige von Eigenschaftsdynamiken
- Erklärung des Problems
- Umsetzung
- Mehrere Abfragen
- Filter
- Der konstante Verbindungsmodus Keep Alive
- Datenabruf
- Schlussfolgerung
Einführung
Im vorherigen Teil, haben wir die Implementierung des MySQL-Konnektors besprochen. Jetzt ist es an der Zeit, sich den Beispielen für seine Anwendung zuzuwenden. Das einfachste und offensichtlichste ist die Sammlung von Signaleigenschaftswerten mit der Möglichkeit, ihre weiteren Änderungen einzusehen. Für die meisten Konten sind über 100 Signale im Terminal verfügbar, wobei jedes Signal mehr als 20 Parameter hat. Das bedeutet, dass wir genügend Daten haben werden. Das implementierte Beispiel ist praktisch sinnvoll, wenn Nutzer Änderungen an Eigenschaften beobachten müssen, die auf der Webseite des Signals nicht angezeigt werden. Diese Änderungen können Hebelwirkung, Bewertung, Anzahl der Abonnenten und vieles mehr umfassen.
Um Daten zu sammeln, implementieren wir einen Dienst, der periodisch Signaleigenschaften abfragt, sie mit früheren Werten vergleicht und das gesamte Array an die Datenbank sendet, falls Unterschiede festgestellt werden.
Um die Eigenschaftsdynamik zu betrachten, schreiben wir einen EA, der eine Änderung in einer ausgewählten Eigenschaft als Diagramm anzeigt. Implementieren wir auch die Möglichkeit, Signale nach Werten einiger Eigenschaften mit Hilfe von bedingten Datenbankabfragen zu sortieren.
Während der Implementierung werden wir herausfinden, warum es in einigen Fällen wünschenswert ist, den konstanten Verbindungsmodus Keep Alive und mehrere Abfragen zu verwenden.
Datenerfassung
Die Ziele des Dienstes sind wie folgt:
- Periodische Abfrage der Eigenschaften aller im Terminal verfügbaren Signale
- Vergleich ihrer Werte mit den vorherigen
- Wenn Unterschiede festgestellt werden, wird das ganze Datenarray in die Datenbank eingetragen
- Informieren des Nutzers im Falle eines Fehlers
Erstellen wir einen neuen Dienst im Editor und nennen ihn signals_to_db.mq5. Die Argumente sind wie folgt:
input string inp_server = "127.0.0.1"; // MySQL server address input uint inp_port = 3306; // TCP port input string inp_login = "admin"; // Login input string inp_password = "12345"; // Password input string inp_db = "signals_mt5"; // Database name input bool inp_creating = true; // Allow creating tables input uint inp_period = 30; // Signal loading period input bool inp_notifications = true; // Send error notifications
Zusätzlich zu den Netzwerkeinstellungen gibt es hier mehrere Optionen:
- inp_creating — Berechtigung zum Anlegen von Tabellen in der Datenbank. Wenn sich der Dienst auf eine nicht existierende Tabelle bezieht, kann diese Tabelle angelegt werden, wenn der Parameter true ist.
- inp_period — Frist für die Anforderung von Signaleigenschaften in Sekunden
- inp_notifications - Erlaubnis, Benachrichtigungen über Fehler zu senden, die während der Arbeit mit dem MySQL-Server aufgetreten sind
Abrufen der Eigenschaftswerte eines Signals
Damit der Dienst korrekt funktioniert, ist es wichtig zu wissen, wann die Signaleigenschaftswerte im Terminal aktualisiert werden. Dies geschieht in zwei Fällen:
- Beim Start des Dienstes.
- Periodisch während des Terminalbetriebs vorausgesetzt, dass die Registerkarte Signale im Toolbox-Fenster aktiv ist. Die Frist der Datenaktualisierung beträgt 3 Stunden.
Zumindest geschieht dies in der Terminalversion ab dem Zeitpunkt des Schreibens.
Die Signaleigenschaften, an denen wir interessiert sind, haben vier Typen:
- ENUM_SIGNAL_BASE_DOUBLE
- ENUM_SIGNAL_BASE_INTEGER
- ENUM_SIGNAL_BASE_DATETIME (der Typ ist abgeleitet von ENUM_SIGNAL_BASE_INTEGER)
- ENUM_SIGNAL_BASE_STRING
Der Typ ENUM_SIGNAL_BASE_DATETIME wird erstellt, um die Eigenschaften ENUM_SIGNAL_BASE_INTEGER zu erkennen, die als Zeit und nicht als ganze Zahl in die Zeichenfolge konvertiert werden sollten.
Der Einfachheit halber zerlegen wir die Enumerationswerte von Eigenschaften desselben Typs in Arrays (vier Eigenschaftstypen — vier Arrays). Jede Enumeration wird von einer Textbeschreibung der Eigenschaft begleitet, die auch der Name des entsprechenden Feldes in der Datenbanktabelle ist. Erstellen wir die entsprechenden Strukturen, um all dies zu erreichen:
//--- Structures of signal properties description for each type struct STR_SIGNAL_BASE_DOUBLE { string name; ENUM_SIGNAL_BASE_DOUBLE id; }; struct STR_SIGNAL_BASE_INTEGER { string name; ENUM_SIGNAL_BASE_INTEGER id; }; struct STR_SIGNAL_BASE_DATETIME { string name; ENUM_SIGNAL_BASE_INTEGER id; }; struct STR_SIGNAL_BASE_STRING { string name; ENUM_SIGNAL_BASE_STRING id; };
Als Nächstes deklarieren wir die Struktur-Arrays (unten ist ein Beispiel für ENUM_SIGNAL_BASE_DOUBLE, für andere Typen ist es ähnlich):
const STR_SIGNAL_BASE_DOUBLE tab_signal_base_double[]= { {"Balance", SIGNAL_BASE_BALANCE}, {"Equity", SIGNAL_BASE_EQUITY}, {"Gain", SIGNAL_BASE_GAIN}, {"Drawdown", SIGNAL_BASE_MAX_DRAWDOWN}, {"Price", SIGNAL_BASE_PRICE}, {"ROI", SIGNAL_BASE_ROI} };Um nun die Werte der ausgewählter Signaleigenschaften zu erhalten, müssen wir uns nur noch durch die vier Schleifen bewegen:
//--- Read signal properties void Read(void) { for(int i=0; i<6; i++) props_double[i] = SignalBaseGetDouble(ENUM_SIGNAL_BASE_DOUBLE(tab_signal_base_double[i].id)); for(int i=0; i<7; i++) props_int[i] = SignalBaseGetInteger(ENUM_SIGNAL_BASE_INTEGER(tab_signal_base_integer[i].id)); for(int i=0; i<3; i++) props_datetime[i] = datetime(SignalBaseGetInteger(ENUM_SIGNAL_BASE_INTEGER(tab_signal_base_datetime[i].id))); for(int i=0; i<5; i++) props_str[i] = SignalBaseGetString(ENUM_SIGNAL_BASE_STRING(tab_signal_base_string[i].id)); }
Im obigen Beispiel ist Read() eine Methode der Struktur SignalEigenschaften, die alles enthält, was Sie für die Arbeit mit Signaleigenschaften benötigen. Dies sind die Puffer für jeden der Typen, sowie die Methoden zum Lesen und Vergleichen der aktuellen Werte mit den vorherigen:
//--- Structure for working with signal properties struct SignalProperties { //--- Property buffers double props_double[6]; long props_int[7]; datetime props_datetime[3]; string props_str[5]; //--- Read signal properties void Read(void) { for(int i=0; i<6; i++) props_double[i] = SignalBaseGetDouble(ENUM_SIGNAL_BASE_DOUBLE(tab_signal_base_double[i].id)); for(int i=0; i<7; i++) props_int[i] = SignalBaseGetInteger(ENUM_SIGNAL_BASE_INTEGER(tab_signal_base_integer[i].id)); for(int i=0; i<3; i++) props_datetime[i] = datetime(SignalBaseGetInteger(ENUM_SIGNAL_BASE_INTEGER(tab_signal_base_datetime[i].id))); for(int i=0; i<5; i++) props_str[i] = SignalBaseGetString(ENUM_SIGNAL_BASE_STRING(tab_signal_base_string[i].id)); } //--- Compare signal Id with the passed value bool CompareId(long id) { if(id==props_int[0]) return true; else return false; } //--- Compare signal property values with the ones passed via the link bool Compare(SignalProperties &sig) { for(int i=0; i<6; i++) { if(props_double[i]!=sig.props_double[i]) return false; } for(int i=0; i<7; i++) { if(props_int[i]!=sig.props_int[i]) return false; } for(int i=0; i<3; i++) { if(props_datetime[i]!=sig.props_datetime[i]) return false; } return true; } //--- Compare signal property values with the one located inside the passed buffer (search by Id) bool Compare(SignalProperties &buf[]) { int n = ArraySize(buf); for(int i=0; i<n; i++) { if(props_int[0]==buf[i].props_int[0]) // Id return Compare(buf[i]); } return false; } };
In die Datenbank eintragen
Um mit der Datenbank zu arbeiten, müssen wir zunächst die Instanz der Klasse CMySQLTransaction deklarieren:
//--- Include MySQL transaction class #include <MySQL\MySQLTransaction.mqh> CMySQLTransaction mysqlt;
Als Nächstes setzen wir die Verbindungsparameter in der Funktion OnStart(). Dazu rufen wir die Methode Config auf:
//+------------------------------------------------------------------+ //| Service program start function | //+------------------------------------------------------------------+ void OnStart() { //--- Configure MySQL transaction class mysqlt.Config(inp_server,inp_port,inp_login,inp_password); ... }
Der nächste Schritt ist die Erstellung des Tabellennamens. Da die Menge der Signale von einem Broker und dem Typ eines Handelskontos abhängt, sollten diese Parameter dazu verwendet werden. Wir ersetzen im Servernamen den Punkt, den Bindestrich und das Leerzeichen durch einen Unterstrich, fügen das Kontologin hinzu und ersetzen alle Buchstaben durch Kleinbuchstaben. Im Fall von Metaquotes-DemoBroker-Server und dem Login von 17273508 lautet der Tabellenname beispielsweise Metaquotes_demo__17273508.
Dies sieht im Code dann wie folgt aus:
//--- Assign a name to the table //--- to do this, get the trade server name string s = AccountInfoString(ACCOUNT_SERVER); //--- replace the space, period and hyphen with underscores string ss[]= {" ",".","-"}; for(int i=0; i<3; i++) StringReplace(s,ss[i],"_"); //--- assemble the table name using the server name and the trading account login string tab_name = s+"__"+IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN)); //--- set all letters to lowercase StringToLower(tab_name); //--- display the result in the console Print("Table name: ",tab_name);
Dann tragen wir die Daten des letzten Eintrags in die Datenbank ein. Dies geschieht, damit es möglich ist, die erhaltenen Eigenschaften mit etwas zu vergleichen, um Unterschiede beim Neustart des Dienstes festzustellen.
Die Funktion DB_Read() liest die Eigenschaften aus der Datenbank.
//+------------------------------------------------------------------+ //| Read signal properties from the database | //+------------------------------------------------------------------+ bool DB_Read(SignalProperties &sbuf[],string tab_name) { //--- prepare a query string q="select * from `"+inp_db+"`.`"+tab_name+"` "+ "where `TimeInsert`= ("+ "select `TimeInsert` "+ "from `"+inp_db+"`.`"+tab_name+"` order by `TimeInsert` desc limit 1)"; //--- send a query if(mysqlt.Query(q)==false) return false; //--- if the query is successful, get the pointer to it CMySQLResponse *r = mysqlt.Response(); if(CheckPointer(r)==POINTER_INVALID) return false; //--- read the number of rows in the accepted response uint rows = r.Rows(); //--- prepare the array if(ArrayResize(sbuf,rows)!=rows) return false; //--- read property values to the array for(uint n=0; n<rows; n++) { //--- read the pointer to the current row CMySQLRow *row = r.Row(n); if(CheckPointer(row)==POINTER_INVALID) return false; for(int i=0; i<6; i++) { if(row.Double(tab_signal_base_double[i].name,sbuf[n].props_double[i])==false) return false; } for(int i=0; i<7; i++) { if(row.Long(tab_signal_base_integer[i].name,sbuf[n].props_int[i])==false) return false; } for(int i=0; i<3; i++) sbuf[n].props_datetime[i] = MySQLToDatetime(row[tab_signal_base_datetime[i].name]); for(int i=0; i<5; i++) sbuf[n].props_str[i] = row[tab_signal_base_string[i].name]; } return true; }Funktionsargumente sind Referenzen auf den Signalpuffer und den Tabellennamen, die wir während der Initialisierung gebildet haben. Das erste, was wir im Funktionskörper tun, ist, eine Abfrage vorzubereiten. In diesem Fall müssen wir alle Eigenschaften der Signale lesen, die die maximale Zeit zum Hinzufügen in die Datenbank haben. Da wir das Array mit allen Werten gleichzeitig schreiben, müssen wir die maximale Zeit in der Tabelle finden und alle Zeichenketten lesen, bei denen die Additionszeit gleich der gefundenen ist. Wenn beispielsweise der Datenbankname signals_mt5 lautet, während der Tabellenname metaquotes_demo__17273508 lautet, sieht die Abfrage wie folgt aus:
select * from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`= ( select `TimeInsert` from `signals_mt5`.`metaquotes_demo__17273508` order by `TimeInsert` desc limit 1)
Die Unterabfrage, die das Maximum der Spalte `TimeInsert`, d.h. den Zeitpunkt des letzten Hinzufügens in die Datenbank zurückgibt, wird rot hervorgehoben. Die grün hervorgehobene Abfrage gibt alle Strings zurück, bei denen der Wert in `TimeInsert` mit dem gefundenen übereinstimmt.
Wenn die Transaktion erfolgreich ist, beginnt das Lesen der erhaltenen Daten. Um dies zu tun, holen wir den Zeiger auf die Antwortklasse CMySQLResponse des Servers, dann die Anzahl der Zeilen in der Antwort. Abhängig von diesem Parameter ändern wir die Signalpuffergröße.
Jetzt müssen wir die Eigenschaften lesen. Um dies zu tun, empfangen wir den Zeiger auf die aktuelle Zeile unter Verwendung des Index. Danach lesen wir die Werte für jeden Eigenschaftstyp. Beispiel: Um die ENUM_SIGNAL_BASE_DOUBLE-Eigenschaften zu lesen, verwenden wir die Methode CMySQLRow::Double(), wobei das erste Argument (Feldname) ein Eigenschaftsname als Text ist.
//--- Declare the buffer of signal properties SignalProperties sbuf[]; //--- Raise the data from the database to the buffer bool exit = false; if(DB_Read(sbuf,tab_name)==false) { //--- if the reading function returns an error, //--- the table is possibly missing if(mysqlt.GetServerError().code==ER_NO_SUCH_TABLE && inp_creating==true) { //--- if we need to create a table and this is allowed in the settings if(DB_CteateTable(tab_name)==false) exit=true; } else exit=true; }
Im Falle eines Fehlers überprüfen wir zunächst, ob er nicht durch das Fehlen der Tabelle verursacht wurde. Dieser Fehler tritt auf, wenn die Tabelle noch nicht angelegt, entfernt oder umbenannt wurde. Der Fehler ER_NO_SUCH_TABLE bedeutet, dass die Tabelle angelegt werden sollte (falls zulässig).
Die Funktion DB_CteateTable() Tabellenerstellung ist recht einfach:
//+------------------------------------------------------------------+ //| Create the table | //+------------------------------------------------------------------+ bool DB_CteateTable(string name) { //--- prepare a query string q="CREATE TABLE `"+inp_db+"`.`"+name+"` ("+ "`PKey` BIGINT(20) NOT NULL AUTO_INCREMENT,"+ "`TimeInsert` DATETIME NOT NULL,"+ "`Id` INT(11) NOT NULL,"+ "`Name` CHAR(50) NOT NULL,"+ "`AuthorLogin` CHAR(50) NOT NULL,"+ "`Broker` CHAR(50) NOT NULL,"+ "`BrokerServer` CHAR(50) NOT NULL,"+ "`Balance` DOUBLE NOT NULL,"+ "`Equity` DOUBLE NOT NULL,"+ "`Gain` DOUBLE NOT NULL,"+ "`Drawdown` DOUBLE NOT NULL,"+ "`Price` DOUBLE NOT NULL,"+ "`ROI` DOUBLE NOT NULL,"+ "`Leverage` INT(11) NOT NULL,"+ "`Pips` INT(11) NOT NULL,"+ "`Rating` INT(11) NOT NULL,"+ "`Subscribers` INT(11) NOT NULL,"+ "`Trades` INT(11) NOT NULL,"+ "`TradeMode` INT(11) NOT NULL,"+ "`Published` DATETIME NOT NULL,"+ "`Started` DATETIME NOT NULL,"+ "`Updated` DATETIME NOT NULL,"+ "`Currency` CHAR(50) NOT NULL,"+ "PRIMARY KEY (`PKey`),"+ "UNIQUE INDEX `TimeInsert_Id` (`TimeInsert`, `Id`),"+ "INDEX `TimeInsert` (`TimeInsert`),"+ "INDEX `Currency` (`Currency`, `TimeInsert`),"+ "INDEX `Broker` (`Broker`, `TimeInsert`),"+ "INDEX `AuthorLogin` (`AuthorLogin`, `TimeInsert`),"+ "INDEX `Id` (`Id`, `TimeInsert`)"+ ") COLLATE='utf8_general_ci' "+ "ENGINE=InnoDB "+ "ROW_FORMAT=DYNAMIC"; //--- send a query if(mysqlt.Query(q)==false) return false; return true; }Die Abfrage selbst hat die Zeit des Hinzufügens von `TimeInsert`, weiters die Feldnamen, die die Namen der Signaleigenschaften sind. Dies ist die lokale Terminalzeit zum Zeitpunkt des Empfangs der aktualisierten Eigenschaften. Außerdem gibt es einen eindeutigen Schlüssel für die Felder `TimeInsert` und `Id` sowie Indizes, die zur Beschleunigung der Abfrageausführung erforderlich sind.
Wenn das Erstellen der Tabelle fehlschlägt, wird die Fehlerbeschreibung angezeigt und der Dienst beendet.
if(exit==true) { if(GetLastError()==(ERR_USER_ERROR_FIRST+MYSQL_ERR_SERVER_ERROR)) { // in case of a server error Print("MySQL Server Error: ",mysqlt.GetServerError().code," (",mysqlt.GetServerError().message,")"); } else { if(GetLastError()>=ERR_USER_ERROR_FIRST) Print("Transaction Error: ",EnumToString(ENUM_TRANSACTION_ERROR(GetLastError()-ERR_USER_ERROR_FIRST))); else Print("Error: ",GetLastError()); } return; }
Es können drei Arten von Fehlern auftreten.
- Ein vom MySQL-Server zurückgegebene Fehler (keine Tabelle, keine Datenbank, ungültiger Login oder ungültiges Passwort)
- Einen Laufzeitfehler (ungültiger Host, Verbindungsfehler)
- Den Fehler ENUM_TRANSACTION_ERROR
Der Fehlertyp definiert, wie seine Beschreibung gebildet wird. Der Fehler ist wie folgt definiert.
- Wenn die Funktion GetLastError() einen Wert kleiner als ERR_USER_ERROR_FIRST zurückgibt, handelt es sich um einen Laufzeitfehler
- Wenn GetLastError() einen Wert größer als ERR_USER_ERROR_FIRST zurückgibt, sind dies ENUM_TRANSACTION_ERROR Fehler
- Ein Wert gleich ERR_USER_ERROR_FIRST+MYSQL_ERR_SERVER_ERROR bedeutet einen Serverfehler. Um Details zu erhalten, rufen wir die Methode mysqlt.GetServerError() auf.
Wenn die Transaktion ohne Fehler durchläuft, gelangen wir in die Hauptprogrammschleife:
//--- set the time label of the previous reading of signal properties datetime chk_ts = 0; ... //--- Main loop of the service operation do { if((TimeLocal()-chk_ts)<inp_period) { Sleep(1000); continue; } //--- it is time to read signal properties chk_ts = TimeLocal(); ... } while(!IsStopped());
Unser Dienst soll sich in dieser Endlosschleife befinden, bis er entladen ist. Die Eigenschaften werden gelesen, Vergleiche mit den vorherigen Werten durchgeführt und das Schreiben in die Datenbank erfolgt (falls erforderlich) mit einer festgelegten Periodizität.
Angenommen, wir haben Signaleigenschaften erhalten, die sich von den vorherigen Werten unterscheiden. Als Nächstes geschieht Folgendes:
if(newdata==true) { bool bypass = false; if(DB_Write(buf,tab_name,chk_ts)==false) { //--- if we need to create a table and this is allowed in the settings if(mysqlt.GetServerError().code==ER_NO_SUCH_TABLE && inp_creating==true) { if(DB_CteateTable(tab_name)==true) { //--- if the table is created successfully, send the data if(DB_Write(buf,tab_name,chk_ts)==false) bypass = true; // sending failed } else bypass = true; // failed to create the table } else bypass = true; // there is no table and it is not allowed to create one } if(bypass==true) { if(GetLastError()==(ERR_USER_ERROR_FIRST+MYSQL_ERR_SERVER_ERROR)) { // in case of a server error PrintNotify("MySQL Server Error: "+IntegerToString(mysqlt.GetServerError().code)+" ("+mysqlt.GetServerError().message+")"); } else { if(GetLastError()>=ERR_USER_ERROR_FIRST) PrintNotify("Transaction Error: "+EnumToString(ENUM_TRANSACTION_ERROR(GetLastError()-ERR_USER_ERROR_FIRST))); else PrintNotify("Error: "+IntegerToString(GetLastError())); } continue; } } else continue;
Hier sehen wir das bekannte Codefragment, das die Existenz der Tabelle prüft und ggf. sie anschließend erstellt. Dies ermöglicht die korrekte Handhabung der Tabellenlöschung durch einen Dritten bei laufendem Dienst. Beachten Sie auch, dass Print() durch PrintNotify() ersetzt wird. Diese Funktion dupliziert die Zeichenfolge, die in der Konsole als Benachrichtigung angezeigt wird, wenn dies in den Eingaben erlaubt ist:
//+------------------------------------------------------------------+ //| Print to console and send notification | //+------------------------------------------------------------------+ void PrintNotify(string text) { //--- display in the console Print(text); //--- send a notification if(inp_notifications==true) { static datetime ts = 0; // last notification sending time static string prev_text = ""; // last notification text if(text!=prev_text || (text==prev_text && (TimeLocal()-ts)>=(3600*6))) { // identical notifications are sent one after another no more than once every 6 hours if(SendNotification(text)==true) { ts = TimeLocal(); prev_text = text; } } } }
Wenn wir eine Eigenschaftsaktualisierungen erkennen, rufen wir die Funktion zum Schreiben in die Datenbank auf:
//+------------------------------------------------------------------+ //| Write signal properties to the database | //+------------------------------------------------------------------+ bool DB_Write(SignalProperties &sbuf[],string tab_name,datetime tc) { //--- prepare a query string q = "insert ignore into `"+inp_db+"`.`"+tab_name+"` ("; q+= "`TimeInsert`"; for(int i=0; i<6; i++) q+= ",`"+tab_signal_base_double[i].name+"`"; for(int i=0; i<7; i++) q+= ",`"+tab_signal_base_integer[i].name+"`"; for(int i=0; i<3; i++) q+= ",`"+tab_signal_base_datetime[i].name+"`"; for(int i=0; i<5; i++) q+= ",`"+tab_signal_base_string[i].name+"`"; q+= ") values "; int sz = ArraySize(sbuf); for(int s=0; s<sz; s++) { q+=(s==0)?"(":",("; q+= "'"+DatetimeToMySQL(tc)+"'"; for(int i=0; i<6; i++) q+= ",'"+DoubleToString(sbuf[s].props_double[i],4)+"'"; for(int i=0; i<7; i++) q+= ",'"+IntegerToString(sbuf[s].props_int[i])+"'"; for(int i=0; i<3; i++) q+= ",'"+DatetimeToMySQL(sbuf[s].props_datetime[i])+"'"; for(int i=0; i<5; i++) q+= ",'"+sbuf[s].props_str[i]+"'"; q+=")"; } //--- send a query if(mysqlt.Query(q)==false) return false; //--- if the query is successful, get the pointer to it CMySQLResponse *r = mysqlt.Response(0); if(CheckPointer(r)==POINTER_INVALID) return false; //--- the Ok type packet should be received as a response featuring the number of affected rows; display it if(r.Type()==MYSQL_RESPONSE_OK) Print("Added ",r.AffectedRows()," entries"); // return true; }
Traditionell beginnt der Funktionscode mit dem Formieren einer Anfrage. Aufgrund der Tatsache, dass wir gleichartige Eigenschaften in Arrays zerlegt haben, erfolgt der Erhalt der Liste von Feldern und Werten in Schleifen und sieht im Code sehr kompakt aus.
Nach dem Senden einer Anfrage erwarten wir die Antwort vom Server vom Typ Ok. Aus dieser Antwort erhalten wir die Anzahl der betroffenen Zeilen mit der Methode AffectedRows(). Diese Anzahl wird in der Konsole angezeigt. Im Falle eines Fehlers gibt die Funktion false zurück, wobei die Fehlermeldung in der Konsole angezeigt und als Benachrichtigung gesendet wird, wenn dies in den Einstellungen erlaubt ist. Die erhaltenen Eigenschaften werden nicht in den Hauptpuffer kopiert. Nach dem angegebenen Zeitraum wird eine erneute Änderung ihrer Werte festgestellt und ein Versuch, sie in die Datenbank zu schreiben, unternommen.
Abb. 1. Der gestartete Dienst zur Erfassung von Signaleigenschaften
Abb. 1 zeigt den gestarteten Dienst signals_to_db, wie er im Navigator-Fenster zu sehen ist. Vergessen Sie nicht, wie oben erwähnt, die Registerkarte Signale zu wählen, da der Dienst sonst keine neuen Daten erhält.
Anwendung zur Anzeige von Eigenschaftsdynamiken
Anwendung zur Anzeige von Eigenschaftsdynamiken Im vorigen Abschnitt haben wir den Dienst implementiert, der der Datenbank die Werte von Signaleigenschaften hinzufügt, wenn eine Änderung festgestellt wird. Der nächste Schritt ist die Vorbereitung einer Anwendung, mit der die ausgewählte Eigenschaftsdynamik innerhalb eines bestimmten Zeitintervalls als Diagramm angezeigt werden soll.
Da die Anwendung eine fortgeschrittene GUI haben soll, ist als Basis die EasyAndFastGUI-Bibliothek zur Erstellung grafischer Oberflächen zu verwenden von Anatoli Kazharski.
a)
b)
Abb. 2. Die Nutzeroberfläche des Programms: das dynamische Equity-Diagramm des ausgewählten Signals (a); dasselbe Diagramm von der Webseite des Signals (b)
Abb. 2a zeigt das Aussehen der Programmoberfläche. Der linke Teil enthält den Datumsbereich, während der rechte Teil den Graphen des Kapitals des ausgewählten Signals zeigt. Zum Vergleich zeigt Abb. 2b den Screenshot einer Webseite des Signals mit dem Diagramm Equity. Der Grund für die kleinen Diskrepanzen liegt in den Datenbank-"Löchern", die sich während der PC-Idle-Zeit gebildet haben, sowie in der relativ großen Periode der Aktualisierung der Werte der Signaleigenschaften im Terminal.
Erklärung des Problems
Geben wir also der Anwendung folgende Funktionsweisen:
- Wenn Daten aus der Tabelle ausgewählt wurden, lässt sich:
- ein Datenbereich festlegen,
- eine Bedingung durch die Eigenschaftswerte SIGNAL_BASE_CURRENCY, SIGNAL_BASE_AUTHOR_LOGIN und SIGNAL_BASE_BROKER setzen,
- ein Bereich mit gültigen Werten der Eigenschaften SIGNAL_BASE_EQUITY, SIGNAL_BASE_GAIN, SIGNAL_BASE_MAX_DRAWDOWN und SIGNAL_BASE_SUBSCRIBERS festlegen.
- Wir konstruieren den Graphen einer angegebenen Eigenschaft, wenn wir den Wert SIGNAL_BASE_ID auswählen.
Umsetzung
Von allen grafischen Elementen benötigen wir einen Block von zwei Kalendern, um "von" und "bis" Daten festzulegen, eine Gruppe von Kombinationsfeldern, um Werte aus Listen auszuwählen, und den Block von Eingabefeldern, um extreme Eigenschaftswerte zu bearbeiten, falls ein Bereich festgelegt werden soll. Um die Bedingungen zu deaktivieren, verwenden wir für Listen den Schlüsselwert "All", der sich ganz am Anfang befindet. Statten wir auch die Blöcke der Eingabefelder auch mit einem Kontrollkästchen aus, das standardmäßig deaktiviert ist.
Der Datumsbereich sollte zu jeder Zeit angegeben werden. Alles andere kann nach Bedarf angepasst werden. Abb. 2a zeigt, dass eine Währung und ein Broker im String-Eigenschaftsblock starr festgelegt sind, während der Name eines Signalautors nicht geregelt ist (Alle).
Jede Combobox-Liste wird anhand von Daten gebildet, die bei der Bearbeitung einer Abfrage erhalten werden. Dies gilt auch für die Extrema der Eingabefelder. Nachdem die Liste der Signal-IDs gebildet und einige ihrer Elemente ausgewählt wurden, wird die Abfrage nach Daten zur Darstellung eines Diagramms einer bestimmten Eigenschaft gesendet.
Um mehr Informationen darüber zu erhalten, wie das Programm mit dem MySQL-Server interagiert, zeigen Sie die Zähler der akzeptierten und gesendeten Bytes sowie die Zeit der letzten Transaktion (Abb. 2) in der Statusleiste an. Wenn eine Transaktion fehlschlägt, zeigen wir den Fehlercode (Abb. 3).
Abb. 3. Anzeige eines Fehlers in der Statusleiste und der Meldung in der Registerkarte Experten
Da die meisten Textbeschreibungen von Serverfehlern nicht in den Fortschrittsbalken passen, zeigen wir sie in der Registerkarte des Experten an.
Da der aktuelle Artikel nichts mit Grafiken zu tun hat, werde ich hier nicht auf die Implementierung der Nutzeroberfläche eingehen. Die Arbeit mit der Bibliothek wird von ihrem Autor in der Artikelserie ausführlich beschrieben. Ich habe einige Änderungen in einigen Dateien des als Grundlage genommenen Beispiels vorgenommen, nämlich
- MainWindow.mqh — Aufbau einer grafischen Oberfläche
- Program.mqh — Interaktion mit der grafischen Oberfläche
- Main.mqh — Arbeiten mit der Datenbank (hinzugefügt)
Mehrere Abfragen
Die bei der Ausführung des Programms verwendeten Datenbankabfragen lassen sich grob in drei Gruppen einteilen:
- Abfragen zum Erhalten der Listenwerte der Combobox
- Abfragen zum Erhalten von Extremwerten von Eingabefeldblöcken
- Abfragen von Daten zur Erstellung eines Diagramms
Während in den beiden letzteren Fällen eine einzige SELECT-Abfrage ausreicht, muss im ersten Fall für jede der Listen eine separate Abfrage gesendet werden. Irgendwann können wir die Zeit für die Datenbeschaffung nicht mehr verlängern. Im Idealfall sollten alle Werte gleichzeitig aktualisiert werden. Es ist auch nicht möglich, nur einen Teil der Listen zu aktualisieren. Verwenden wir dazu also eine Mehrfachabfrage. Selbst wenn sich eine Transaktion (einschließlich Bearbeitung und Übertragung) verzögert, wird die Schnittstelle erst aktualisiert, nachdem alle Server-Antworten akzeptiert wurden. Im Falle eines Fehlers wird die teilweise Aktualisierung der Listen der grafischen Elemente der Schnittstelle deaktiviert.
Unten sehen Sie ein Beispiel für eine Mehrfachabfrage, die sofort beim Start des Programms gesendet wird.
select `Currency` from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`>='2019-11-01 00:00:00' AND `TimeInsert`<='2019-12-15 23:59:59' group by `Currency`; select `Broker` from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`>='2019-11-01 00:00:00' AND `TimeInsert`<='2019-12-15 23:59:59' group by `Broker`; select `AuthorLogin` from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`>='2019-11-01 00:00:00' AND `TimeInsert`<='2019-12-15 23:59:59' group by `AuthorLogin`; select `Id` from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`>='2019-11-01 00:00:00' AND `TimeInsert`<='2019-12-15 23:59:59' group by `Id`; select Min(`Equity`) as EquityMin, Max(`Equity`) as EquityMax, Min(`Gain`) as GainMin, Max(`Gain`) as GainMax, Min(`Drawdown`) as DrawdownMin, Max(`Drawdown`) as DrawdownMax, Min(`Subscribers`) as SubscribersMin, Max(`Subscribers`) as SubscribersMax from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`>='2019-11-01 00:00:00' AND `TimeInsert`<='2019-12-15 23:59:59'
Wie wir sehen können, handelt es sich um eine Folge von fünf "SELECT"-Abfragen, die durch ";" getrennt sind. Die ersten vier Abfragen fordern die Listen von individuellen Werten bestimmter Eigenschaften (Währung, Broker, AuthorLogin und Id) in einem bestimmten Zeitintervall an. Die fünfte Abfrage ist so konzipiert, dass sie die Minima und Maxima von vier Eigenschaften (Equity, Gain, Drawdown und Subscribers) aus dem gleichen Zeitintervall erhält.
Wenn wir uns den Datenaustausch mit dem MySQL-Server ansehen, können wir folgendes feststellen: Die Anfrage (1) wurde in einem einzigen TCP-Paket gesendet, während die Antworten darauf (2) in verschiedenen TCP-Paketen geliefert wurden (siehe Abb. 4).
Abb. 4. Die mehrfache Anfrage im Verkehrsanalysator
Beachten Sie, dass, wenn eine der verschachtelten "SELECT"-Abfragen einen Fehler verursacht, die nachfolgenden nicht abgearbeitet werden. Mit anderen Worten: der MySQL-Server bearbeitet Anfragen bis zum ersten Fehler.
Filter
Der Einfachheit halber fügen wir die Filter hinzu, die die Liste der Signale reduzieren und nur diejenigen übrig lassen, die die definierten Anforderungen erfüllen. Wir sind zum Beispiel an Signalen mit einer bestimmten Basiswährung, einem bestimmten Wachstumsbereich oder einer von Null verschiedenen Anzahl von Abonnenten interessiert. Wir verwenden dazu den Operator WHERE in der Abfrage:
select `Broker` from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`>='2019-11-01 00:00:00' AND `TimeInsert`<='2019-12-15 23:59:59' AND `Currency`='USD' AND `Gain`>='100' AND `Gain`<='1399' group by `Broker`; select `AuthorLogin` from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`>='2019-11-01 00:00:00' AND `TimeInsert`<='2019-12-15 23:59:59' AND `Currency`='USD' AND `Gain`>='100' AND `Gain`<='1399' group by `AuthorLogin`; select `Id` from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`>='2019-11-01 00:00:00' AND `TimeInsert`<='2019-12-15 23:59:59' AND `Currency`='USD' AND `Gain`>='100' AND `Gain`<='1399' group by `Id`; select Min(`Equity`) as EquityMin, Max(`Equity`) as EquityMax, Min(`Gain`) as GainMin, Max(`Gain`) as GainMax, Min(`Drawdown`) as DrawdownMin, Max(`Drawdown`) as DrawdownMax, Min(`Subscribers`) as SubscribersMin, Max(`Subscribers`) as SubscribersMax from `signals_mt5`.`metaquotes_demo__17273508` where `TimeInsert`>='2019-11-01 00:00:00' AND `TimeInsert`<='2019-12-15 23:59:59' AND `Currency`='USD' AND `Gain`>='100' AND `Gain`<='1399'
Die obige Abfrage dient dazu, Combobox-Listen und Extrema von Eingabefeldern abzufragen, vorausgesetzt, die Basiswährung ist USD, während der Wachstumswert im Bereich von 100-1399 liegt. Hier müssen wir zunächst darauf achten, dass keine Abfrage nach Werten aus der Liste Currency erfolgt. Dies ist logisch, da wir alle Werte ausschließen, während wir einen bestimmten Wert in der Combobox-Liste auswählen. Gleichzeitig wird die Abfrage nach Werten immer für Eingabefelder durchgeführt, auch wenn sie in der Bedingung verwendet werden. Dies geschieht, damit ein Nutzer einen echten Wertebereich sehen kann. Angenommen, wir haben den Mindestwert für den Zuwachs von 100 eingeführt. Unter Berücksichtigung des Datensatzes, der die ausgewählten Kriterien erfüllt, ist der nächstliegende Mindestwert jedoch 135. Das bedeutet, dass nach Erhalt der Serverantwort der Wert 100 durch 135 ersetzt wird.
Nachdem eine Abfrage mit angegebenen Filtern durchgeführt wurde, wird die Liste der Signal-ID Kombinationsfeldwerte deutlich reduziert. Es ist möglich, ein Signal auszuwählen und die Änderungen seiner Eigenschaften im Diagramm zu verfolgen.
Der konstante Verbindungsmodus Keep Alive
Wenn wir uns Abb. 4 genau ansehen, können wir feststellen, dass es dort keinen Verbindungsabbau gibt. Der Grund dafür ist, dass das Programm zur Betrachtung der Dynamik der Signaleigenschaften den konstanten Verbindungsmodus anwendet, den wir hier besprechen.
Bei der Entwicklung des Datenerfassungsdienstes haben wir den Parameter "konstante Verbindung" deaktiviert gelassen. Die Daten wurden nur selten aufgezeichnet, und es hatte keinen Sinn, die Verbindung beizubehalten. Dies ist hier nicht der Fall. Nehmen wir an, dass ein Nutzer ein geeignetes Signal anhand des Dynamikgraphen einer bestimmten Eigenschaft sucht. Die Abfrage wird jedes Mal an die Datenbank gesendet, wenn eines der Steuerelemente geändert wird. Es wäre in diesem Fall nicht ganz korrekt, die Verbindung jedes Mal herzustellen und wieder zu schließen.
Um den konstanten Verbindungsmodus zu aktivieren, setzen Sie dessen Timeout gleich 60 Sekunden.
if(CheckPointer(mysqlt)==POINTER_INVALID) { mysqlt = new CMySQLTransaction; mysqlt.Config(m_mysql_server,m_mysql_port,m_mysql_login,m_mysql_password,60000); mysqlt.PingPeriod(10000); }
Das bedeutet, dass die Verbindung geschlossen wird, wenn ein Nutzer länger als 60 Sekunden nichts tut.
Sehen wir uns an, wie das in der Praxis aussieht. Nehmen wir an, dass ein Nutzer einen bestimmten Parameter geändert hat, der danach eine Minute lang untätig bleibt. Die Erfassung der Netzwerkpakete sieht dann wie folgt aus:
Abb. 5. Erfassen der Pakete beim Arbeiten im konstanten Verbindungsmodus
Das Bild zeigt die Abfrage (1), die Ping-Serie mit einem Zeitabstand von 10 Sekunden (2) und das Schließen der Verbindung nach Ablauf einer Minute nach der Abfrage (3). Hätte der Nutzer die Arbeit fortgesetzt und wären Anfragen öfter als einmal pro Minute gesendet worden, wäre die Verbindung nicht geschlossen worden.
Die Angabe von Transaktionsklassenparametern wurde auch von der Ping-Periode von 10 Sekunden begleitet. Brauchen wir das? Zunächst einmal ist es notwendig, damit der Server die Verbindung nicht von seiner Seite aus entsprechend dem in der Konfiguration eingestellten Timeout schließt, vorausgesetzt, der Timeout-Wert kann mit der folgenden Abfrage ermittelt werden:
show variables where `Variable_name`='interactive_timeout'
Meistens sind es 3 600 Sekunden. Theoretisch reicht es aus, einen Ping mit einer Periode zu senden, die kürzer als der Server-Timeout ist, um zu verhindern, dass die Verbindung von ihrer Seite geschlossen wird. In diesem Fall würden wir aber erst beim Senden der nächsten Anfrage über den Verbindungsverlust Bescheid wissen. Anders herum, wenn der Wert von 10 Sekunden eingestellt ist, können wir fast sofort über den Verbindungsverlust Bescheid wissen.
Datenabruf
Schauen wir uns die Serverantwort auf eine Mehrfachabfrage am Beispiel der Implementierung der GetData-Methode an. Die Methode ist für die Aktualisierung des Inhalts von Dropdown-Listen, Extremwerten von Eingabefeldern sowie des Dynamikgraphen einer ausgewählten Eigenschaft konzipiert.
void CMain::GetData(void) { if(CheckPointer(mysqlt)==POINTER_INVALID) { mysqlt = new CMySQLTransaction; mysqlt.Config(m_mysql_server,m_mysql_port,m_mysql_login,m_mysql_password,60000); mysqlt.PingPeriod(10000); } //--- Save signal id string signal_id = SignalId(); if(signal_id=="Select...") signal_id=""; //--- Make a query string q = ""; if(Currency()=="All") { q+= "select `Currency` from `"+m_mysql_db+"`.`"+m_mysql_table+"` where "+Condition()+" group by `Currency`; "; } if(Broker()=="All") { q+= "select `Broker` from `"+m_mysql_db+"`.`"+m_mysql_table+"` where "+Condition()+" group by `Broker`; "; } if(Author()=="All") { q+= "select `AuthorLogin` from `"+m_mysql_db+"`.`"+m_mysql_table+"` where "+Condition()+" group by `AuthorLogin`; "; } q+= "select `Id` from `"+m_mysql_db+"`.`"+m_mysql_table+"` where "+Condition()+" group by `Id`; "; q+= "select Min(`Equity`) as EquityMin, Max(`Equity`) as EquityMax"; q+= ", Min(`Gain`) as GainMin, Max(`Gain`) as GainMax"; q+= ", Min(`Drawdown`) as DrawdownMin, Max(`Drawdown`) as DrawdownMax"; q+= ", Min(`Subscribers`) as SubscribersMin, Max(`Subscribers`) as SubscribersMax from `"+m_mysql_db+"`.`"+m_mysql_table+"` where "+Condition(); //--- Display the transaction result in the status bar if(UpdateStatusBar(mysqlt.Query(q))==false) return; //--- Set accepted values in the combo box lists and extreme values of the input fields uint responses = mysqlt.Responses(); for(uint j=0; j<responses; j++) { if(mysqlt.Response(j).Fields()<1) continue; if(UpdateComboBox(m_currency,mysqlt.Response(j),"Currency")==true) continue; if(UpdateComboBox(m_broker,mysqlt.Response(j),"Broker")==true) continue; if(UpdateComboBox(m_author,mysqlt.Response(j),"AuthorLogin")==true) continue; if(UpdateComboBox(m_signal_id,mysqlt.Response(j),"Id",signal_id)==true) continue; // UpdateTextEditRange(m_equity_from,m_equity_to,mysqlt.Response(j),"Equity"); UpdateTextEditRange(m_gain_from,m_gain_to,mysqlt.Response(j),"Gain"); UpdateTextEditRange(m_drawdown_from,m_drawdown_to,mysqlt.Response(j),"Drawdown"); UpdateTextEditRange(m_subscribers_from,m_subscribers_to,mysqlt.Response(j),"Subscribers"); } GetSeries(); }
Stellen wir zunächst eine Anfrage. In Bezug auf Combobox-Listen werden nur die Listen mit dem aktuell gewählten Wert von "All" in die Abfrage einbezogen. Die Bedingungen werden in der separaten Methode Condition() zusammengesetzt:
string CMain::Condition(void) { //--- Add the time interval string s = "`TimeInsert`>='"+time_from(TimeFrom())+"' AND `TimeInsert`<='"+time_to(TimeTo())+"' "; //--- Add the remaining conditions if required //--- For drop-down lists, the current value should not be equal to All if(Currency()!="All") s+= "AND `Currency`='"+Currency()+"' "; if(Broker()!="All") { string broker = Broker(); //--- the names of some brokers contain characters that should be escaped StringReplace(broker,"'","\\'"); s+= "AND `Broker`='"+broker+"' "; } if(Author()!="All") s+= "AND `AuthorLogin`='"+Author()+"' "; //--- A checkbox should be set for input fields if(m_equity_from.IsPressed()==true) s+= "AND `Equity`>='"+m_equity_from.GetValue()+"' AND `Equity`<='"+m_equity_to.GetValue()+"' "; if(m_gain_from.IsPressed()==true) s+= "AND `Gain`>='"+m_gain_from.GetValue()+"' AND `Gain`<='"+m_gain_to.GetValue()+"' "; if(m_drawdown_from.IsPressed()==true) s+= "AND `Drawdown`>='"+m_drawdown_from.GetValue()+"' AND `Drawdown`<='"+m_drawdown_to.GetValue()+"' "; if(m_subscribers_from.IsPressed()==true) s+= "AND `Subscribers`>='"+m_subscribers_from.GetValue()+"' AND `Subscribers`<='"+m_subscribers_to.GetValue()+"' "; return s; }
Wenn die Transaktion erfolgreich ist, erhalten wir die Anzahl der Antworten, die wir dann in einer Schleife analysieren.
Die Methode UpdateComboBox() ist für die Aktualisierung von Daten in Comboboxen vorgesehen. Sie erhält einen Zeiger auf die Antwort und den entsprechenden Feldnamen. Wenn das Feld in der Antwort vorhanden ist, werden die Daten in die Combobox-Liste aufgenommen, und die Methode gibt true zurück. Das Argument set_value enthält den Wert aus der vorherigen Liste, den der Nutzer während der Abfrage ausgewählt hat. Er sollte in der neuen Liste gefunden und als die aktuelle Liste gesetzt werden. Wenn der angegebene Wert in der neuen Liste nicht vorhanden ist, wird der Wert unter dem Index von 1 gesetzt (nach "Select...").
bool CMain::UpdateComboBox(CComboBox &object, CMySQLResponse *p, string name, string set_value="") { int col_idx = p.Field(name); if(col_idx<0) return false; uint total = p.Rows()+1; if(total!=object.GetListViewPointer().ItemsTotal()) { string tmp = object.GetListViewPointer().GetValue(0); object.GetListViewPointer().Clear(); object.ItemsTotal(total); object.SetValue(0,tmp); object.GetListViewPointer().YSize(18*((total>16)?16:total)+3); } uint set_val_idx = 0; for(uint i=1; i<total; i++) { string value = p.Value(i-1,col_idx); object.SetValue(i,value); if(set_value!="" && value==set_value) set_val_idx = i; } //--- if there is no specified value, but there are others, select the topmost one if(set_value!="" && set_val_idx==0 && total>1) set_val_idx=1; //--- ComboSelectItem(object,set_val_idx); //--- return true; }
Die Methode UpdateTextEditRange() aktualisiert die Extrema der Texteingabefelder.
bool CMain::UpdateTextEditRange(CTextEdit &obj_from,CTextEdit &obj_to, CMySQLResponse *p, string name) { if(p.Rows()<1) return false; else return SetTextEditRange(obj_from,obj_to,p.Value(0,name+"Min"),p.Value(0,name+"Max")); }
Vor dem Verlassen von GetData() wird die Methode GetSeries() aufgerufen, die Daten anhand einer Signal-ID und eines Eigenschaftsnamens auswählt:
void CMain::GetSeries(void) { if(SignalId()=="Select...") { // if a signal is not selected ArrayFree(x_buf); ArrayFree(y_buf); UpdateSeries(); return; } if(CheckPointer(mysqlt)==POINTER_INVALID) { mysqlt = new CMySQLTransaction; mysqlt.Config(m_mysql_server,m_mysql_port,m_mysql_login,m_mysql_password,60000); mysqlt.PingPeriod(10000); } string q = "select `"+Parameter()+"` "; q+= "from `"+m_mysql_db+"`.`"+m_mysql_table+"` "; q+= "where `TimeInsert`>='"+time_from(TimeFrom())+"' AND `TimeInsert`<='"+time_to(TimeTo())+"' "; q+= "AND `Id`='"+SignalId()+"' order by `TimeInsert` asc"; //--- Send a query if(UpdateStatusBar(mysqlt.Query(q))==false) return; //--- Check the number of responses if(mysqlt.Responses()<1) return; CMySQLResponse *r = mysqlt.Response(0); uint rows = r.Rows(); if(rows<1) return; //--- copy the column to the graph data buffer (false - do not check the types) if(r.ColumnToArray(Parameter(),y_buf,false)<1) return; //--- form X axis labels if(ArrayResize(x_buf,rows)!=rows) return; for(uint i=0; i<rows; i++) x_buf[i] = i; //--- Update the graph UpdateSeries(); }
Im Allgemeinen ähnelt seine Implementierung der oben diskutierten Methode GetData(). Aber es gibt zwei Dinge, auf die man achten sollte:
- Wenn ein Signal nicht ausgewählt wird (der Wert der Combobox ist gleich "Select..."), wird der Graph gelöscht und es geschieht nichts weiter.
- Die Verwendung der Methode ColumnToArray()
Die erwähnte Methode ist genau für die Fälle vorgesehen, in denen die Spaltendaten in den Puffer kopiert werden sollen. Im aktuellen Fall ist Typüberprüfung deaktiviert, da die Spaltendaten entweder vom ganzzahligen oder vom reellen Typ sein können. In beiden Fällen sollten sie in den 'doppelten' Puffer kopiert werden.
Die Methoden GetData() und GetSeries() werden aufgerufen, wenn eines der grafischen Elemente geändert wird:
//+------------------------------------------------------------------+ //| Handler of the value change event in the "Broker" combo box | //+------------------------------------------------------------------+ void CMain::OnChangeBroker(void) { m_duration=0; GetData(); } ... //+------------------------------------------------------------------+ //| Handler of the value change event in the "SignalID" combo box | //+------------------------------------------------------------------+ void CMain::OnChangeSignalId(void) { m_duration=0; GetSeries(); }
Die Quellcodes für die Behandlung der Comboboxen von Broker und Signal ID werden oben angezeigt. Die übrigen sind in ähnlicher Weise implementiert. Wenn ein anderer Broker ausgewählt wird, wird die Methode GetData() aufgerufen, während GetSeries() nacheinander von dort aufgerufen wird. Wenn ein anderes Signal ausgewählt wird, wird GetSeries() sofort aufgerufen.
In der Variablen m_duration wird die Gesamtzeit der Bearbeitung aller Abfragen einschließlich der Übertragung akkumuliert und dann in der Statusleiste angezeigt. Die Ausführungszeit von Abfragen ist ein wichtiger Parameter. Ihre steigenden Werte weisen auf Fehler in der Datenbankoptimierung hin.
Die Anwendung in Aktion ist in Abb. 6 dargestellt.
Abb. 6. Das Programm zum Betrachten der Dynamik von Signaleigenschaften in Aktion
Schlussfolgerung
In diesem Artikel haben wir die Beispiele für die Anwendung des vorher in Betracht gezogenen MySQL-Konnektors betrachtet. Bei der Implementierung der Aufgaben haben wir festgestellt, dass die Verwendung der konstanten Verbindung bei häufigen Abfragen an die Datenbank die vernünftigste Lösung ist. Wir haben auch die Bedeutung eines Pings hervorgehoben, um den Verbindungsabbruch von der Serverseite her zu verhindern.
Was die Netzwerkfunktionen betrifft, so ist die Arbeit mit MySQL nur ein kleiner Teil dessen, was mit ihrer Hilfe ohne Rückgriff auf dynamische Bibliotheken implementiert werden kann. Wir leben im Zeitalter der Netzwerktechnologien, und die Hinzufügung der Funktionsgruppe Socket ist zweifellos ein bedeutender Meilenstein in der Entwicklung der MQL5-Sprache.
Die beigefügten Archivinhalte:
- Services\signals_to_db.mq5 — Der Quellcode des Dienstes zur Datenerhebung
- Experts\signals_from_db\ — Der Quellcode des Programms zur dynamischen Anzeige der Signaleigenschaften
- Include\MySQL\ — Der Quellcode des MySQL-Konnektors
- Include\EasyAndFastGUI\ — Die Bibliothek zum Erstellen eines grafischen Interfaces (zum Datum der Veröffentlichung des Artikels)
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/7495
- 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.