Verwendung von Netzwerkfunktionen oder MySQL ohne DLL: Teil II - Programm zur Überwachung von Änderungen der Signaleigenschaften

Serhii Shevchuk | 15 Mai, 2020

Inhalt


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:

  1. Periodische Abfrage der Eigenschaften aller im Terminal verfügbaren Signale
  2. Vergleich ihrer Werte mit den vorherigen
  3. Wenn Unterschiede festgestellt werden, wird das ganze Datenarray in die Datenbank eingetragen
  4. 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:


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:

Zumindest geschieht dies in der Terminalversion ab dem Zeitpunkt des Schreibens.

Die Signaleigenschaften, an denen wir interessiert sind, haben vier Typen:

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.

Betrachten wir den Fall, in dem das Senden einer Anfrage mit einem Fehler endet. Kehren wir dazu zum Quellcode der Funktion OnStart() zurück.
//--- 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.

Der Fehlertyp definiert, wie seine Beschreibung gebildet wird. Der Fehler ist wie folgt definiert.

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.

Der Dienst zum Sammeln von Signaleigenschaften

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.

Ansicht der Anwendung

a)

Webseite des Signals

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:


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).


Anzeige eines Fehlers in der Statusleiste

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).

    Die mehrfache Anfrage im Verkehrsanalysator

    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:

    Der Ping im konstanten Verbindungsmodus

    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:

    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.

    Die Anwendung in Aktion

    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: