English Русский 中文 Español 日本語 Português
preview
MQL5 Kochbuch — Dienste

MQL5 Kochbuch — Dienste

MetaTrader 5Beispiele | 31 Januar 2023, 11:34
374 0
Denis Kirichenko
Denis Kirichenko

Einführung

Seit kurzem gibt es im MetaTrader 5 einen neuen Programmtyp, der als Dienst oder Service bezeichnet wird. Nach Angaben des Entwicklers ermöglichen es die Dienste den Nutzern, nutzerdefinierte Preisfeeds für das Terminal zu erstellen, d.h. das Bereitstellen von Preis externer Systeme in Echtzeit zu implementieren, so wie es auf den Handelsservern der Makler der Fall ist. Dies ist nicht das einzige Merkmal der Dienste.

In diesem Artikel werde ich auf die Feinheiten der Arbeit mit Diensten eingehen. Der Artikel richtet sich hauptsächlich an Anfänger. Auf dieser Grundlage habe ich versucht, den Code vollständig reproduzierbar zu machen und von Beispiel zu Beispiel komplizierter zu werden.



1. Dämonen bei der Arbeit

Die Dienste in MQL5haben Ähnlichkeiten mit den Windows-Diensten. Wikipedia gibt die folgende Definition der Dienste

Der Windows-Dienst (service) ist ein Computerprogramm, das im Hintergrund arbeitet . Aus der Definition wird deutlich, dass es viel mit dem Konzept der Daemons in Unix gemeinsam hat.

In unserem Fall ist die externe Umgebung für die Dienste nicht das Betriebssystem selbst, sondern die Shell des MetaTrader5Terminals.

Ein paar Worte zu den Daemons.

Ein Daemon ist ein Computerprogramm das als Hintergrundprozess läuft und nicht unter der direkten Kontrolle eines interaktiven Nutzers steht.

Der Begriff wurde von den Programmierern des Projekts MAC des MIT geprägt. рус.Laut Fernando J. Corbató, der 1963 am Projekt MAC arbeitete, war sein Team das erste, das den Begriff Daemon verwendete, inspiriert von Maxwells Dämon, einem imaginären Agenten in der Physik und Thermodynamik, der half, Moleküle zu sortieren.UNIX Systeme haben diese Terminologie übernommen.

Maxwells Dämon steht im Einklang mit der griechischen Mythologie, die einen Dämon als übernatürliches Wesen versteht, das im Hintergrund wirkt. Wie im Unix System Administration Handbook beschrieben, war das Konzept der alten Griechen eines „persönlichen Daemons“ ähnlich dem modernen Konzept eines „Schutzengels“.

Obwohl die alten Griechen keine Computer besaßen, waren ihnen die Beziehungen zwischen den Einheiten klar.



2. Dienste – Informationen zur Dokumentation

Bevor wir uns mit dem Thema befassen, sollten wir die Dokumentationsunterlagen durchgehen und sehen, wie der Entwickler die Fähigkeiten der Dienste beschreibt.

2.1 Anwendungsarten

Auf der ersten Seite der Dokumentation, nämlich im Abschnitt Typen von MQL5-Anwendungen, wird ein Dienst als ein Typ von MQL5-Programm definiert:

  • Ein Dienst ist ein Programm, das im Gegensatz zu Indikatoren, Expert Advisors und Skripten nicht an einen Chart gebunden sein muss, um zu laufen. Wie Skripte reagieren Dienste nicht auf Ereignisse außer auf Auslöser (trigger). Um einen Dienst zu starten, sollte sein Code die Funktion OnStart enthalten. Dienste reagieren nicht auf andere Ereignisse als den Start, aber sie sind in der Lage, nutzerdefinierte Ereignisse an Charts zu senden, wenn sie die Funktion EventChartCustom verwenden. Dienste werden in <terminal_directory>\MQL5\Services gespeichert.

Dabei ist zu beachten, dass Dienste den Skripten sehr ähnlich sind. Der grundlegende Unterschied besteht darin, dass sie nicht an einen Chart gebunden sind.


2.2 Programmausführung

Der Abschnitt Durchführung der Programme bietet eine Übersicht über die Programme in MQL5:

Programm
Durchführung Hinweis
  Dienste
 In ihrem eigenen Ausführungs-Thread, wobei die Anzahl der Threads denen der Dienste entspricht
 Ein in einer Schleife laufender Dienst kann die Ausführung anderer Programme nicht unterbrechen.
  Skript
  In ihrem eigenen Ausführungs-Thread, wobei die Anzahl der Threads denen der Skripte entspricht
 Ein Skript in einer Schleife kann die Ausführung anderer Programme nicht unterbrechen.
  Expert Advisor
 In ihrem eigenen Ausführungs-Thread, wobei die Anzahl der Threads denen der EAs entspricht
 Ein Expert Advisor in einer Schleife kann die Ausführung anderer Programme nicht unterbrechen.
Indikator
 Ein Thread für alle Indikatoren eines Symbols. Die Anzahl der Threads ist gleich der Anzahl der Symbole mit Indikatoren
 Eine Endlosschleife in einem Indikator stoppt alle anderen Indikatoren für dieses Symbol

Mit anderen Worten, Dienste unterscheiden sich nicht von Skripten und EAs hinsichtlich der Methode zur Aktivierung des Ausführungsablaufs. Dienste ähneln auch insofern Skripten und EAs, als dass das Vorhandensein von Codeblöcken in Schleifen die Funktionsweise anderer mql5-Programme nicht beeinträchtigt.


2.3 Nutzungsverbot bestimmter Funktionen in den Diensten

Die Entwickler haben eine Liste von Funktionen erstellt, die von einem Dienst nicht verwendet werden dürfen:

Dies ist sinnvoll, da die Dienste den Expert Advisor nicht anhalten und mit dem Timer arbeiten können, da sie nur auf ein einziges, dem Start-Ereignis, reagieren. Sie können auch nicht mit den Funktionen der nutzerdefinierten Indikatoren arbeiten.

 
2.4 Starten und Beenden von Diensten

Der Abschnitt über die entsprechende Dokumentation enthält mehrere wichtige Punkte. Betrachten wir jeden einzelnen von ihnen.

Die Dienste werden beim Starten des Terminals geladen, wenn sie beim Beenden des Terminals noch liefen. Dienste enden unmittelbar nach Beendigung ihrer Arbeit.

Dies ist eine der bemerkenswerten Eigenschaften eines Dienstes. Dies muss nicht kontrolliert werden. Er führt seine Aufgaben automatisch aus, nachdem er einmal gestartet wurde.

Dienste haben nur die eine Funktion OnStart(), in der man eine endlose Datenempfangs- und -verarbeitungsschleife implementieren kann, z. B. zum Erstellen und Aktualisieren nutzerdefinierter Symbole mit Hilfe der Netzwerkfunktionen.

Wir können eine einfache Schlussfolgerung ziehen. Wenn ein Dienst eine Reihe von einmaligen Aktionen durchführen soll, ist es nicht notwendig, den Codeblock in einer Schleife auszuführen. Wenn die Aufgabe den konstanten oder regelmäßigen Betrieb des Dienstes beinhaltet, ist es notwendig, diesen Codeblock in eine Schleife einzuschließen. Wir werden später Beispiele für solche Aufgaben besprechen.

Im Gegensatz zu Expert Advisor, Indikatoren und Skripten sind die Dienste nicht an ein bestimmtes Chart gebunden und müssen daher über einen anderen Mechanismus gestartet werden.

 Vielleicht ist dies die zweite bemerkenswerte Eigenschaft des Dienstes. Es ist kein Zeitplan für deren Funktionieren erforderlich.

Eine neue Instanz eines Dienstes wird aus dem Navigator mit dem Befehl „Dienst hinzufügen“ erstellt. Die Instanz eines Dienstes kann über ein entsprechendes Menü gestartet und gestoppt werden. Um alle Instanzen zu verwalten, wird das Menü der Dienste verwendet.

Dies ist die dritte bemerkenswerte Eigenschaft des Dienstes. Da wir nur eine Programmdatei haben, können wir mehrere Instanzen davon gleichzeitig ausführen. Dies geschieht in der Regel, wenn verschiedene Parameter (Eingabevariablen) verwendet werden müssen.


3. Prototyp eines Dienstes

Die Terminal-Hilfe, die durch Drücken von F1geöffnet werden kann, beschreibt den Mechanismus zum Starten und Verwalten von Diensten. Deshalb werden wir uns jetzt nicht damit befassen.

Erstellen wir im MetaEditor eine Vorlage für Dienste und nennen sie dEmpty.mq5.

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   
  }
//+------------------------------------------------------------------+

Nach der Kompilierung können wir den Namen des Dienstes im Navigator sehen (Abb. 1).


Der Dienst dEmpty

Abb. 1. Der Dienst dEmpty im Unterfenster des Navigators

Nach dem Hinzufügen und Starten des dEmpty-Dienstes im Navigator-Subfenster erhalten wir die folgenden Einträge im Journal:

CS      0       19:54:18.590    Services        service 'dEmpty' started
CS      0       19:54:18.592    Services        service 'dEmpty' stopped

Die Protokolle zeigen, dass der Dienst gestartet und gestoppt wurde.  Da der Code keine Befehle enthält, gibt es auch keine Änderungen im Terminal. Nach dem Start des Dienstes werden wird einfach nichts geschehen.

Füllen wir die Vorlage der Dienste mit einigen Befehlen. Erstellen wir den Dienst dStart.mq5 und schreiben die folgenden Zeilen:

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   string program_name=::MQLInfoString(MQL_PROGRAM_NAME);
   datetime now=::TimeLocal();
   ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));   
  }
//+------------------------------------------------------------------+

Nach dem Start des Dienstes wird im Tab des Expertenlogs folgender Eintrag angezeigt:

CS      0       20:04:28.347    dStart       Service "dStart" starts at: 2022.11.30 20:04:28.

Der Dienst dStart hat uns also über seinen Start informiert und endete dann.

Erweitern wir die Möglichkeiten des vorherigen Dienstes und nennen wir den neuen Dienst dStartStop.mq5.

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   string program_name=::MQLInfoString(MQL_PROGRAM_NAME);
   datetime now=::TimeLocal();
   ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));
   ::Sleep(1000);
   now=::TimeLocal();
   ::PrintFormat("Service \"%s\" stops at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));
  }
//+------------------------------------------------------------------+

Der laufende Dienst informiert bereits nicht nur über seinen Start, sondern auch über das Ende seiner Tätigkeit.

Nach dem Start des Dienstes im Journal sehen wir folgende Einträge:

2022.12.01 22:49:10.324 dStartStop   Service "dStartStop" starts at: 2022.12.01 22:49:10
2022.12.01 22:49:11.336 dStartStop   Service "dStartStop" stops at: 2022.12.01 22:49:11

Es ist leicht zu erkennen, dass der erste und der zweite Zeitpunkt um eine Sekunde voneinander abweichen. Die integrierte Funktion Sleep() wurde zwischen dem ersten und dem letzten Befehl ausgelöst.

Erweitern wir nun die Fähigkeiten des aktuellen Dienstes so, dass er so lange läuft, bis er zwangsweise gestoppt wird.  Nennen wir den neuen Dienst dStandBy.mq5.

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   string program_name=::MQLInfoString(MQL_PROGRAM_NAME);
   datetime now=::TimeLocal();
   ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));
   do
     {
      ::Sleep(1);
     }
   while(!::IsStopped());
   now=::TimeLocal();
   ::PrintFormat("Service \"%s\" stops at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));
//--- final goodbye
   for(ushort cnt=0; cnt<5; cnt++)
     {
      ::PrintFormat("Count: %hu", cnt+1);
      ::Sleep(10000);
     }
  }
//+------------------------------------------------------------------+

Nach dem Verlassen der Schleife do while schreibt der Dienst aufgrund des Programmstopps noch mehrere Einträge in das Protokoll. Nach jedem dieser Einträge wird Sleep() mit einer Verzögerungszeit von 10 Sekunden aufgerufen.

Das Journal enthält die folgenden Einträge:

CS      0       23:20:44.478    dStandBy     Service "dStandBy" starts at: 2022.12.01 23:20:44
CS      0       23:20:51.144    dStandBy     Service "dStandBy" stops at: 2022.12.01 23:20:51
CS      0       23:20:51.144    dStandBy     Count: 1
CS      0       23:20:51.159    dStandBy     Count: 2
CS      0       23:20:51.175    dStandBy     Count: 3
CS      0       23:20:51.191    dStandBy     Count: 4
CS      0       23:20:51.207    dStandBy     Count: 5

Der Dienst wurde um 23:20:44 Uhr gestartet und um 23:20:51 Uhr zwangsweise beendet. Es ist auch leicht zu erkennen, dass die Intervalle zwischen den Zählerständen nicht mehr als 0,02 Sekunden betragen. Allerdings war für solche Intervalle bisher eine Verzögerung von 10 Sekunden vorgesehen.

Gemäß der Dokumentation zur Funktion Sleep():

Hinweis

Die Funktion Sleep() kann nicht von nutzerdefinierten Indikatoren aufgerufen werden, da Indikatoren im Schnittstellen-Thread ausgeführt werden und diesen nicht verlangsamen sollten. Die Funktion hat eine integrierte Überprüfung des Status des EA-Stopp-Flags alle 0,1 Sekunden.

In unserem Fall hat die Funktion Sleep() schnell erkannt, dass der Dienst zwangsweise gestoppt wurde, und die Ausführung des mql5-Programms verzögert.

Der Vollständigkeit halber sehen wir uns an, was die Dokumentation über den Rückgabewert der Statusprüfungsfunktion IsStopped() sagt:

Rückgabewert

Er gibt true zurück, wenn die Systemvariable _StopFlag einen anderen Wert als 0 enthält. Ein Wert ungleich Null wird _StopFlag zugewiesen, wenn ein mql5-Programm den Befehl erhalten hat, seine Operation zu beenden. In diesem Fall müssen wir das Programm sofort beenden, sonst wird das Programm nach 3 Sekunden von außen zwangsweise beendet.

So hat der Dienst nach einem erzwungenen Stopp 3 Sekunden Zeit, etwas anderes zu tun, bevor er vollständig deaktiviert wird. Überprüfen wir dieses Verhalten in der Praxis. Fügen wir dem Code des vorherigen Dienstes nach der Schleife eine Matrixberechnung hinzu. Die Berechnung dauert etwa eine Minute. Wir werden sehen, ob der Dienst Zeit hat, alles zu berechnen, nachdem er zwangsweise gestoppt wurde. Nennen wir den neuen Dienst srvcStandByMatrixMult.mq5.

Nach der Schleife zur Berechnung der Zählerwerte müssen wir den folgenden Block zum vorherigen Code hinzufügen:

//--- Matrix mult
//--- matrix A 1000x2000
   int rows_a=1000;
   int cols_a=2000;
//--- matrix B 2000x1000
   int rows_b=cols_a;
   int cols_b=1000;
//--- matrix C 1000x1000
   int rows_c=rows_a;
   int cols_c=cols_b;
//--- matrix A: size=rows_a*cols_a
   int size_a=rows_a*cols_a;
   int size_b=rows_b*cols_b;
   int size_c=rows_c*cols_c;
//--- prepare matrix A
   double matrix_a[];
   ::ArrayResize(matrix_a, rows_a*cols_a);
   for(int i=0; i<rows_a; i++)
      for(int j=0; j<cols_a; j++)
         matrix_a[i*cols_a+j]=(double)(10*::MathRand()/32767);
//--- prepare matrix B
   double matrix_b[];
   ::ArrayResize(matrix_b, rows_b*cols_b);
   for(int i=0; i<rows_b; i++)
      for(int j=0; j<cols_b; j++)
         matrix_b[i*cols_b+j]=(double)(10*::MathRand()/32767);
//--- CPU: calculate matrix product matrix_a*matrix_b
   double matrix_c_cpu[];
   ulong time_cpu=0;
   if(!MatrixMult_CPU(matrix_a, matrix_b, matrix_c_cpu, rows_a, cols_a, cols_b, time_cpu))
     {
      ::PrintFormat("Error in calculation on CPU. Error code=%d", ::GetLastError());
      return;
     }
   ::PrintFormat("time CPU=%d ms", time_cpu);

Wir starten den Dienst dStandByMatrixMult und halten ihn nach einigen Sekunden zwangsweise an. Die folgenden Zeilen erscheinen im Protokoll:

CS      0       15:17:23.493    dStandByMatrixMult   Service "dStandByMatrixMult" starts at: 2022.12.02 15:17:23
CS      0       15:18:17.282    dStandByMatrixMult   Service "dStandByMatrixMult" stops at: 2022.12.02 15:18:17
CS      0       15:18:17.282    dStandByMatrixMult   Count: 1
CS      0       15:18:17.297    dStandByMatrixMult   Count: 2
CS      0       15:18:17.313    dStandByMatrixMult   Count: 3
CS      0       15:18:17.328    dStandByMatrixMult   Count: 4
CS      0       15:18:17.344    dStandByMatrixMult   Count: 5
CS      2       15:18:19.771    dStandByMatrixMult   Abnormal termination

Wie wir sehen, kam der Befehl zum Beenden der Ausführung des Programms mql5 um 15:18:17.282 an. Der Dienst selbst wurde um 15:18:19.771 Uhr zwangsweise beendet. Tatsächlich vergingen vom Zeitpunkt der Beendigung bis zum erzwungenen Anhalten des Dienstes 2,489 Sekunden. Die Tatsache, dass der Dienst zwangsweise gestoppt wurde, und zwar als Folge einer Notbeendigung, wird durch den Eintrag „Abnormal termination“ angezeigt.

Da nicht mehr als 3 Sekunden verbleiben, bevor der Dienst zwangsweise gestoppt wird (_StopFlag == true), ist es nicht empfehlenswert, ernsthafte Berechnungen oder Handelsaktionen nach der unterbrochenen Schleife durchzuführen.

Hier ist ein einfaches Beispiel. Angenommen, das Terminal verfügt über einen Dienst, der alle Positionen schließt, wenn das Terminal selbst geschlossen wird. Das Terminal wird geschlossen und der Dienst versucht, alle aktiven Positionen zu liquidieren. Nun ist das Terminal geschlossen aber einige Positionen sind offen geblieben, ohne dass wir das bemerkt haben.


4. Beispiele für die Verwendung

Bevor ich auf praktische Beispiele eingehe, möchte ich erörtern, was Dienste auf einem Handels-Terminal leisten können. Einerseits können wir fast jeden Code in den Dienst einbringen (mit Ausnahme der nicht erlaubten Funktionen), andererseits ist es wahrscheinlich sinnvoll, die Befugnisse abzugrenzen und den Diensten ihre eigene Nische im Umfeld des Handelsterminals zu geben.

Erstens sollten die Dienste die Arbeit anderer aktiver MQL5-Programme nicht kopieren: Expert Advisor, Indikatoren und Skripte. Nehmen wir an, es gibt einen Expert Advisor, der Limit-Orders durch ein Signal am Ende einer Handelssitzung platziert. Außerdem gibt es einen Dienst, der diese Limitaufträge erteilt. Infolgedessen könnte das System zur Abrechnung von Limit-Aufträgen im EA selbst gestört werden, oder im Falle unterschiedlicher Magic-Nummern könnte der EA die vom Dienst erteilten Aufträge aus den Augen verlieren.

Zweitens müssen wir die umgekehrte Situation vermeiden - den Konflikt von Diensten mit anderen MQL5-Programmen. Nehmen wir an, es gibt einen Expert Advisor, der Limit-Orders durch ein Signal am Ende einer Handelssitzung platziert. Und es gibt einen Dienst, der kontrolliert, dass am Ende des Handelstages alle Positionen geschlossen und ausstehende Aufträge entfernt werden.  Es besteht ein Interessenkonflikt: Der EA wird Aufträge erteilen, und der Dienst wird sie sofort entfernen. All dies kann in einer Art DDoS-Angriff auf einen Handelsserver enden.

Im Allgemeinen sollten die Dienste harmonisch in den Betrieb des Handelsterminals integriert werden, ohne die mql5-Programmezu beeinträchtigen, und stattdessen mit ihnen interagieren, um eine effizientere Nutzung der Handelsalgorithmen zu ermöglichen.


4.1 Löschen von Protokollen

Angenommen, der Dienst soll zu Beginn eines neuen Handelstages den Ordner mit den Protokollen (Journalen) löschen, die von einem oder mehreren Expert Advisors in der Vergangenheit (gestern, vorgestern usw.) erstellt wurden.

Welche Instrumente brauchen wir hier? Wir benötigen Dateioperationen und die Definition eines neuen Balkens. Weitere Informationen über die Klasse zur Erkennung eines neuen Balkens finden Sie im Artikel Die Ereignisverarbeitungsroutine „Neuer Balken“.

Kommen wir nun zu den Dateioperationen. Die integrierten Dateifunktionen werden hier nicht funktionieren, da wir an die Grenzen der Datei-Sandbox stoßen werden. Laut der Dokumentation:

Aus Sicherheitsgründen ist die Arbeit mit Dateien in der Sprache MQL5 streng kontrolliert. Dateien, die in Datei-Operationen mit Hilfe der MQL5-Sprache verwendet werden, können nicht außerhalb der Datei-Sandbox liegen.

Protokolldateien, die von MQL5-Programmen auf die Festplatte geschrieben werden, befinden sich in %MQL5\Logs. Glücklicherweise können wir die WinAPI mit ihren Dateioperationen verwenden.

WinAPI wird mit der folgenden Direktive eingebunden:

#include <WinAPI\winapi.mqh>

Wir werden acht Funktionen aus der Datei WinAPI verwenden:

  1. FindFirstFileW(),
  2. FindNextFileW(),
  3. CopyFileW(),
  4. GetFileAttributesW(),
  5. SetFileAttributesW(),
  6. DeleteFileW(),
  7. FindClose(),
  8. GetLastError().

Die erste Funktion sucht im angegebenen Ordner nach der ersten Datei mit dem angegebenen Namen. Es ist möglich, eine Maske als Namen zu verwenden. Um also die Protokolldateien in einem Ordner zu finden, genügt es, die Zeichenkette „.log“ als Namen anzugeben.

Die zweite Funktion setzt die mit der ersten Funktion begonnene Suche fort.

Die dritte Funktion kopiert eine bestehende Datei in eine neue Datei.

Die vierte Funktion ermittelt die Dateisystemattribute für die angegebene Datei oder das angegebene Verzeichnis.

Die fünfte Funktion legt solche Attribute fest.

Die sechste Funktion löscht die Datei mit dem angegebenen Namen.

Mit der siebten Funktion wird das Handle der Dateisuche geschlossen.

Die achte Funktion ruft den letzten Fehlercodewert ab.

Werfen wir einen Blick auf den Code des Dienstes dClearTradeLogs.mq5.

//--- include
#include <WinAPI\winapi.mqh>
#include "Include\CisNewBar.mqh"
//--- defines
#define ERROR_FILE_NOT_FOUND 0x2
#define ERROR_NO_MORE_FILES 0x12
#define INVALID_FILE_ATTRIBUTES 0xFFFFFFFF
#define FILE_ATTRIBUTE_READONLY 0x1
#define FILE_ATTRIBUTE_DIRECTORY 0x10
#define FILE_ATTRIBUTE_ARCHIVE 0x20
//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input string InpDstPath="G:" ; // Destination drive
//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   string program_name=::MQLInfoString(MQL_PROGRAM_NAME);
   datetime now=::TimeLocal();
   ::PrintFormat("Service \"%s\" starts at: %s", program_name, ::TimeToString(now, TIME_DATE|TIME_SECONDS));
//--- new bar
   CisNewBar daily_new_bar;
   daily_new_bar.SetPeriod(PERIOD_D1);
   daily_new_bar.SetLastBarTime(1);
//--- logs path
   string logs_path=::TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Logs\\";
   string mask_path=logs_path+"*.log";
//--- destination folder (if to copy files)
   string new_folder_name=NULL;
   uint file_attributes=0;
   if(::StringLen(InpDstPath)>0)
     {
      new_folder_name=InpDstPath+"\\Logs";
      //--- check whether a folder exists
      file_attributes=kernel32::GetFileAttributesW(new_folder_name);
      bool does_folder_exist=(file_attributes != INVALID_FILE_ATTRIBUTES) &&
                             ((file_attributes & FILE_ATTRIBUTE_DIRECTORY) != 0);
      if(!does_folder_exist)
        {
         //--- create a folder
         int create_res=kernel32::CreateDirectoryW(new_folder_name, 0);
         if(create_res<1)
           {
            ::PrintFormat("Failed CreateDirectoryW() with error: %x", kernel32::GetLastError());
            return;
           }
        }
     }
//--- main processing loop
   do
     {
      MqlDateTime sToday;
      ::TimeTradeServer(sToday);
      sToday.hour=sToday.min=sToday.sec=0;
      datetime dtToday=::StructToTime(sToday);
      if(daily_new_bar.isNewBar(dtToday))
        {
         ::PrintFormat("\nToday is: %s", ::TimeToString(dtToday, TIME_DATE));
         string todays_log_file_name=::TimeToString(dtToday, TIME_DATE);
         int replaced=::StringReplace(todays_log_file_name, ".", "");
         if(replaced>0)
           {
            todays_log_file_name+=".log";
            //--- log files
            FIND_DATAW find_file_data;
            ::ZeroMemory(find_file_data);
            HANDLE hFind=kernel32::FindFirstFileW(mask_path, find_file_data);
            if(hFind==INVALID_HANDLE)
              {
               ::PrintFormat("Failed FindFirstFile (hFind) with error: %x", kernel32::GetLastError());
               continue;
              }
            // List all the files in the directory with some info about them
            int result=0;
            uint files_cnt=0;
            do
              {
               string name="";
               for(int i=0; i<MAX_PATH; i++)
                  name+=::ShortToString(find_file_data.cFileName[i]);
               //--- delete any file except today's
               if(::StringCompare(name, todays_log_file_name))
                 {
                  string file_name=logs_path+name;
                  //--- if to copy a file before deletion
                  if(::StringLen(new_folder_name)>0)
                    {                     
                     string new_file_name=new_folder_name+"\\"+name;
                     if(kernel32::CopyFileW(file_name, new_file_name, 0)==0)
                       {
                        ::PrintFormat("Failed CopyFileW() with error: %x", kernel32::GetLastError());
                       }
                     //--- set READONLY attribute
                     file_attributes=kernel32::GetFileAttributesW(new_file_name);
                     if(file_attributes!=INVALID_FILE_ATTRIBUTES)
                        if(!(file_attributes & FILE_ATTRIBUTE_READONLY))
                          {
                           file_attributes=kernel32::SetFileAttributesW(new_file_name, file_attributes|FILE_ATTRIBUTE_READONLY);
                           if(!(file_attributes & FILE_ATTRIBUTE_READONLY))
                              ::PrintFormat("Failed SetFileAttributesW() with error: %x", kernel32::GetLastError());
                          }
                    }
                  int del_ret=kernel32::DeleteFileW(file_name);
                  if(del_ret>0)
                     files_cnt++;
                 }
               //--- next file
               ::ZeroMemory(find_file_data);
               result= kernel32::FindNextFileW(hFind, find_file_data);
              }
            while(result!=0);
            uint kernel32_last_error=kernel32::GetLastError();
            if(kernel32_last_error>0)
               if(kernel32_last_error!=ERROR_NO_MORE_FILES)
                  ::PrintFormat("Failed FindNextFileW (hFind) with error: %x", kernel32_last_error);
            ::PrintFormat("Deleted log files: %I32u", files_cnt);
            int file_close=kernel32::FindClose(hFind);
           }
        }
      ::Sleep(15000);
     }
   while(!::IsStopped());
   now=::TimeLocal();
   ::PrintFormat("Service \"%s\" stops at: %s", program_name, ::TimeToString(now, TIME_DATE|TIME_SECONDS));
  }
//+------------------------------------------------------------------+

Wenn das Laufwerk, auf das die Dateien kopiert werden sollen, in der Eingabevariablen angegeben ist, erstellen wir einen Ordner, in dem die Log-Dateien gespeichert werden, nachdem wir überprüft haben, ob dieser Ordner existiert.

In der Hauptverarbeitungsschleife prüfen wir zunächst, ob ein neuer Tag begonnen hat. Danach suchen und löschen wir die Log-Dateien in derselben Schleife, wobei die heutigen Dateien übersprungen werden. Wenn wir eine Datei kopieren müssen, prüfen wir diese Möglichkeit und setzen nach dem Kopieren das Attribut „Nur Lesen“ für die neue Datei

In der Schleife legen wir eine Pause von 15 Sekunden fest. Dies ist wahrscheinlich eine relativ optimale Frequenz für die Bestimmung eines neuen Tages.

Vor dem Start des Dienstes sah der Ordner %MQL5\Logs im Explorer also wie folgt aus (Abb. 2).

“%MQL5\Logs“ Explorer, Ordner vor dem Löschen von Dateien

Abb. 2: Explorer, Ordner „%MQL5\Logs“ vor dem Löschen von Dateien


Nach dem Starten des Dienstes erscheinen die folgenden Meldungen im Protokoll:

2022.12.05 23:26:59.960 dClearTradeLogs Service "dClearTradeLogs" starts at: 2022.12.05 23:26:59
2022.12.05 23:26:59.960 dClearTradeLogs 
2022.12.05 23:26:59.960 dClearTradeLogs Today is: 2022.12.05
2022.12.05 23:26:59.985 dClearTradeLogs Deleted log files: 6

Es ist leicht zu erkennen, dass der Dienst nichts in das Protokoll über das Ende seiner Arbeit geschrieben hat. Der Grund dafür ist, dass sich der Dienst noch beendet hat. Er dreht sich einfach in der Schleife, bis sie unterbrochen wird.

„%MQL5\Logs“ Explorer, Ordner nach dem Löschen von Dateien

Abb. 3: Explorer, Ordner „%MQL5\Logs“ nach dem Löschen von Dateien

Nach dem Löschen der Protokolle verbleibt also nur eine Datei in dem angegebenen Ordner (Abb. 3). Natürlich kann die Aufgabe des Löschens von Dateien verbessert und flexibler gestaltet werden. So können Sie beispielsweise vor dem Löschen von Dateien diese auf einen anderen Datenträger kopieren, um die notwendigen Informationen nicht endgültig zu verlieren. Im Allgemeinen hängt die Implementierung bereits von den spezifischen Anforderungen an den Algorithmus ab. Im vorliegenden Beispiel wurden die Dateien in den Ordner G:\Logs kopiert (Abb. 4).

Explorer, Ordner „G:\Logs“ nach dem Kopieren von Dateien

Abb. 4. Explorer, Ordner „G:\Logs“ nach dem Kopieren von Dateien

Damit ist die Arbeit mit des Skripts abgeschlossen. Im folgenden Beispiel weisen wir dem Dienst die Aufgabe der Anzeige von Chart zu.


4.2 Verwaltung der Charts

Stellen wir uns vor, wir stellen uns folgende Aufgabe. Das Terminal sollte die Charts der aktuell gehandelten Symbole anzeigen, d.h. derjenigen, die offene Positionen aufweisen.

Die Regeln für offene Charts sind sehr einfach. Wenn es eine offene Position für eines der Symbole gibt, öffnen wir den Chart dieses Symbols. Wenn es keine Position gibt, gibt es auch keinen Chart. Wenn es mehrere Positionen für ein Symbol gibt, wird nur ein Chart geöffnet.

Außerdem könnten wir ein paar Farben hinzufügen. Wenn die Position im Gewinn ist, wird die Hintergrundfarbe des Charts hellblau sein, und wenn sie im roten Bereich ist, wird sie hellrosa sein. Ist der Gewinn Null, wird die Farbe Lavendel verwendet.


Um diese Aufgabe zu erfüllen, benötigen wir also zunächst eine Schleife in dem Dienst, in der wir den Status von Positionen und Charts überwachen. Die Schleife ist groß genug geworden. Analysieren wir also den Code Block für Block.

Der Zyklus ist in zwei Blöcke unterteilt.

Der erste Block befasst sich mit der Situation, wenn es keine Positionen gibt:

int positions_num=::PositionsTotal();
//--- if there are no positions
if(positions_num<1)
  {
   // close all the charts
   CChart temp_chart_obj;
   temp_chart_obj.FirstChart();
   long temp_ch_id=temp_chart_obj.ChartId();
   for(int ch_idx=0; ch_idx<MAX_CHARTS && temp_ch_id>-1; ch_idx++)
     {
      long ch_id_to_close=temp_ch_id;
      temp_chart_obj.NextChart();
      temp_ch_id=temp_chart_obj.ChartId();
      ::ChartClose(ch_id_to_close);
     }
  }

In diesem Block gehen wir durch die offenen Charts (falls vorhanden) und schließen sie. Hier und im Folgenden werde ich die Klasse CChart verwenden, um die Eigenschaften des Preischarts zu verwalten.

Der zweite Block ist etwas komplexer:

//--- if there are some positions
else
   {
   //--- collect unique position symbols
   CHashSet<string> pos_symbols_set;
   for(int pos_idx=0; pos_idx<positions_num; pos_idx++)
      {
      string curr_pos_symbol=::PositionGetSymbol(pos_idx);
      if(!pos_symbols_set.Contains(curr_pos_symbol))
         {
         if(!pos_symbols_set.Add(curr_pos_symbol))
            ::PrintFormat("Failed to add a symbol \"%s\" to the positions set!", curr_pos_symbol);
         }
      }
   string pos_symbols_arr[];
   int unique_pos_symbols_num=pos_symbols_set.Count();
   if(pos_symbols_set.CopyTo(pos_symbols_arr)!=unique_pos_symbols_num)
      continue;
   //--- collect unique chart symbols and close duplicates
   CHashMap<string, long> ch_symbols_map;
   CChart map_chart_obj;
   map_chart_obj.FirstChart();
   long map_ch_id=map_chart_obj.ChartId();
   for(int ch_idx=0; ch_idx<MAX_CHARTS && map_ch_id>-1; ch_idx++)
      {
      string curr_ch_symbol=map_chart_obj.Symbol();
      long ch_id_to_close=0;
      if(!ch_symbols_map.ContainsKey(curr_ch_symbol))
         {
         if(!ch_symbols_map.Add(curr_ch_symbol, map_ch_id))
            ::PrintFormat("Failed to add a symbol \"%s\" to the charts map!", curr_ch_symbol);
         }
      else
         {
         //--- if there's a duplicate
         ch_id_to_close=map_chart_obj.ChartId();
         }
      //--- move to the next chart
      map_chart_obj.NextChart();
      map_ch_id=map_chart_obj.ChartId();
      if(ch_id_to_close>0)
         {
         ::ChartClose(ch_id_to_close);
         }
      }
   map_chart_obj.Detach();
   //--- looking for a chart if there's a position
   for(int s_pos_idx=0; s_pos_idx<unique_pos_symbols_num; s_pos_idx++)
      {
      string curr_pos_symbol=pos_symbols_arr[s_pos_idx];
      //--- if there's no chart of the symbol
      if(!ch_symbols_map.ContainsKey(curr_pos_symbol))
         if(::SymbolSelect(curr_pos_symbol, true))
            {
            //--- open a chart of the symbol
            CChart temp_chart_obj;
            long temp_ch_id=temp_chart_obj.Open(curr_pos_symbol, PERIOD_H1);
            if(temp_ch_id<1)
               ::PrintFormat("Failed to open a chart of the symbol \"%s\"!", curr_pos_symbol);
            else
               {
               if(!ch_symbols_map.Add(curr_pos_symbol, temp_ch_id))
                  ::PrintFormat("Failed to add a symbol \"%s\" to the charts map!", curr_pos_symbol);
               temp_chart_obj.Detach();
               }
            }
      }
   string ch_symbols_arr[];
   long ch_ids_arr[];
   int unique_ch_symbols_num=ch_symbols_map.Count();
   if(ch_symbols_map.CopyTo(ch_symbols_arr, ch_ids_arr)!=unique_ch_symbols_num)
      continue;
   //--- looking for a position if there's a chart
   for(int s_ch_idx=0; s_ch_idx<unique_ch_symbols_num; s_ch_idx++)
      {
      string curr_ch_symbol=ch_symbols_arr[s_ch_idx];
      long ch_id_to_close=ch_ids_arr[s_ch_idx];
      CChart temp_chart_obj;
      temp_chart_obj.Attach(ch_id_to_close);
      //--- if there's no position of the symbol
      if(!pos_symbols_set.Contains(curr_ch_symbol))
         {
         temp_chart_obj.Close();
         }
      else
         {
         CPositionInfo curr_pos_info;
         //--- calculate  a position profit
         double curr_pos_profit=0.;
         int pos_num=::PositionsTotal();
         for(int pos_idx=0; pos_idx<pos_num; pos_idx++)
            if(curr_pos_info.SelectByIndex(pos_idx))
               {
               string curr_pos_symbol=curr_pos_info.Symbol();
               if(!::StringCompare(curr_ch_symbol, curr_pos_symbol))
                  curr_pos_profit+=curr_pos_info.Profit()+curr_pos_info.Swap();
               }
         //--- apply a color
         color profit_clr=clrLavender;
         if(curr_pos_profit>0.)
            {
            profit_clr=clrLightSkyBlue;
            }
         else if(curr_pos_profit<0.)
            {
            profit_clr=clrLightPink;
            }
         if(!temp_chart_obj.ColorBackground(profit_clr))
            ::PrintFormat("Failed to apply a profit color for the symbol \"%s\"!", curr_ch_symbol);
         temp_chart_obj.Redraw();
         }
      temp_chart_obj.Detach();
      }
   //--- tile windows (Alt+R)
   uchar vk=VK_MENU;
   uchar scan=0;
   uint flags[]= {0, KEYEVENTF_KEYUP};
   ulong extra_info=0;
   uchar Key='R';
   for(int r_idx=0; r_idx<2; r_idx++)
      {
      user32::keybd_event(vk, scan, flags[r_idx], extra_info);
      ::Sleep(10);
      user32::keybd_event(Key, scan, flags[r_idx], extra_info);
      }
   }

Erstens werden eindeutige Werte der Symbole , mit offenen Positionen gesammelt. Die Merkmale der Klasse CHashSet<T> sind für diese Aufgabe geeignet. Die Klasse ist eine Implementierung des ungeordneten dynamischen Datensatzes vom Typ T, mit der erforderlichen Eindeutigkeit jedes Wertes. Die erhaltenen eindeutigen Werte kopieren wir in ein String-Array, um später einen vereinfachten Zugriff darauf zu haben.

In der nächsten Phase sammeln wir die eindeutigen IDs der für die Symbole geöffneten Charts. Gegebenenfalls schließen wir doppelt geöffnete Charts auf dem Weg. Angenommen, wir haben zwei EURUSD-Charts. Das bedeutet, dass wir ein Chart offen lassen und das anderes schließen. Hier wird die Instanz der Klasse CHashMap<TKey,TValue> verwendet. Die Klasse ist eine Implementierung einer dynamischen Hashtabelle, deren Daten als ungeordnete Schlüssel/Wertpaare gespeichert werden, wobei die Eindeutigkeit eines Schlüssels erforderlich ist.

Es bleiben nur zwei Schleifen übrig. Wir bewegen uns im ersten Schritt entlang der Reihe der offenen Positionssymbole und prüfen, ob es ein Chart für sie gibt. Wenn nicht, öffnen wir es. In der zweiten Schleife bewegen wir uns durch die Reihe der geöffneten Charts der Symbole und prüfen, ob die offene Position jedem Symbol entspricht. Angenommen, es gibt ein offenes USDJPY-Chart aber kein Position dieses Symbols. Dann wird USDJPY geschlossen. In derselben Schleife berechnen wir den Gewinn der Position, um die Hintergrundfarbe festzulegen, die zu Beginn der Aufgabe bestimmt wurde. Um auf die Positionseigenschaften zuzugreifen und ihre Werte zu erhalten, wurde die Klasse CPositionInfo der Standardbibliothek verwendet.

Zum Schluss wollen wir die Chartfenster als Kacheln anordnen, um sie optisch ansprechender zu gestalten. Um dies zu erreichen, verwenden wir die WinAPI, nämlich die Funktionkeybd_event(), die Tastendrücke simuliert .

Das war's. Nun muss nur noch der Dienst dActivePositionsCharts gestartet werden.


4.3 Nutzerdefiniertes Symbol, Preise

Einer der Vorteile des Dienstes ist die Möglichkeit, im Hintergrund zu arbeiten, ohne die Kurstabelle zu verwenden. Als Beispiel werde ich in diesem Abschnitt zeigen, wie der Dienst verwendet werden kann, um ein nutzerdefiniertes Symbol und seine Tick-Historie zu erstellen und neue Ticks zu erzeugen.

Ich werde den US-Dollar-Index als nutzerdefiniertes Symbol verwenden.

4.3.1 USD-Index, Zusammensetzung

Der US-Dollar-Index ist ein synthetischer Index, der den Wert des USD im Verhältnis zu einem Korb von sechs anderen Währungen widerspiegelt:

  1. 57.6 * 100.000 / 100 = 1.000 EUR
  2. JPY (13,6%);
  3. GBP (11,9%);
  4. CAD
  5. SEK (4,2%);
  6. CHF (3,6%).

Die Indexgleichung ist der geometrisch gewichtete Durchschnitt der Wechselkurse des USD gegenüber diesen Währungen mit einem Korrekturfaktor:

USDX = 50.14348112 * EURUSD-0.576 * USDJPY0.136 * GBPUSD-0.119 * USDCAD0.091 * USDSEK0.042 * USDCHF0.036

Anhand der Gleichung kann man annehmen, dass der Kurs des Paares mit einer negativen Zahl potenziert wird, wenn der USD in der Notierung eine notierte Währung ist, und, wenn der USD in der Notierung eine Basiswährung ist, derKurs des Paares eine positive Potenz hat.

Der Währungskorb kann wie folgt schematisch dargestellt werden (Abb. 5).



USD-Index-Währungskorb (DXY)

Abb. 5. USD-Index (DXY) Währungskorb


Der USD-Index ist der Basiswert für Futures, die an der Intercontinental Exchange (ICE) gehandelt wird. Index-Futures werden etwa alle 15 Sekunden berechnet. Für die Berechnung werden der höchste Geldkurs (bid) und der niedrigste Briefkurs (ask) in der Markttiefe des im Index enthaltenen Währungspaares herangezogen.


4.3.2 USD-Index, Dienst

Wir haben alles, was wir für die Berechnungen brauchen, also ist es an der Zeit, mit der Codierung des Dienstes zu starten. Zunächst möchte ich jedoch darauf hinweisen, dass der Dienst in mehreren Phasen arbeiten wird. In der ersten Phase bildet es die Historie von Ticks und Balken für den synthetischen Index, und in der zweiten Phase verarbeitet es die neuen Ticks. Die erste Stufe ist natürlich mit der Vergangenheit verbunden, die zweite mit der Gegenwart.

Lassen Sie uns eine MQL5-Programmvorlage (Dienst) mit dem Namen dDxySymbol.mq5 erstellen.

Definieren wir die folgenden Variablen als Eingabevariablen:

input datetime InpStartDay=D'01.10.2022'; // Start date
input int InpDxyDigits=3;                 // Index digits

Die Erste definiert den Beginn der Geschichte der Preise, die wir versuchen abzurufen, um unser Symbol zu erstellen. Mit anderen Worten: Wir werden den Verlauf der Preise ab dem 1. Oktober 2022 herunterladen.

Mit der Zweiten wird die Genauigkeit der Symbolpreise festgelegt.

Um mit dem Index arbeiten zu können, müssen wir also ein nutzerdefiniertes Symbol erstellen, das die Grundlage für die Anzeige eines synthetischen Kurses bildet. DXY ist eine Bezeichnung für das Indexsymbol. Die Ressource enthält eine Menge Material über nutzerdefinierte Symbole. Ich werde mich der Klasse CiCustomSymbol zuwenden, die in dem Artikel „Das MQL5-Kochbuch: Stresstests von Handelsstrategien unter Verwendung nutzerdefinierter Symbole“ definiert ist.

Hier ist der Code-Block, in dem der synthetischer DXY-Index erstellt wird:

//--- create a custom symbol
string custom_symbol="DXY",
       custom_group="Dollar Index";
CiCustomSymbol custom_symbol_obj;
const uint batch_size = 1e6;
const bool is_selected = true;
int code = custom_symbol_obj.Create(custom_symbol, custom_group, NULL, batch_size, is_selected);
::PrintFormat("Custom symbol \"%s\", code: %d", custom_symbol, code);
if(code < 0)
   return;

Wurde das DXY-Symbol noch nicht erstellt und befindet es sich nicht in der Liste der nutzerdefinierten Terminalsymbole, so gibt die Methode CiCustomSymbol::Create() den Code 1 zurück. Befindet sich das DXY-Symbol bereits unter den Symbolen, so erhalten wir den Code 0. Wenn es nicht möglich ist, das Symbol zu erstellen, dann erhalten wir einen Fehler - Code -1. Wenn bei der Erstellung eines nutzerdefinierten Symbols ein Fehler auftritt, wird der Dienst sich beenden.

Nach der Erstellung des synthetischen Index werden wir mehrere Eigenschaften für sie festlegen.

//--- Integer properties
//--- sector
ENUM_SYMBOL_SECTOR symbol_sector = SECTOR_CURRENCY;
if(!custom_symbol_obj.SetProperty(SYMBOL_SECTOR, symbol_sector))
   {
   ::PrintFormat("Failed to set a sector for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- background color
color symbol_background_clr = clrKhaki;
if(!custom_symbol_obj.SetProperty(SYMBOL_BACKGROUND_COLOR, symbol_background_clr))
   {
   ::PrintFormat("Failed to set a background color for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- chart mode
ENUM_SYMBOL_CHART_MODE symbol_ch_mode=SYMBOL_CHART_MODE_BID;
if(!custom_symbol_obj.SetProperty(SYMBOL_CHART_MODE, symbol_ch_mode))
   {
   ::PrintFormat("Failed to set a chart mode for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- digits
if(!custom_symbol_obj.SetProperty(SYMBOL_DIGITS, InpDxyDigits))
   {
   ::PrintFormat("Failed to set digits for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- trade mode
ENUM_SYMBOL_TRADE_MODE symbol_trade_mode = SYMBOL_TRADE_MODE_DISABLED;
if(!custom_symbol_obj.SetProperty(SYMBOL_TRADE_MODE, symbol_trade_mode))
   {
   ::PrintFormat("Failed to disable trade for the custom symbol \"%s\"", custom_symbol);
   return;
   }

Die folgenden Eigenschaften sind vom Typ ENUM_SYMBOL_INFO_INTEGER:

  • SYMBOL_SECTOR,
  • SYMBOL_BACKGROUND_COLOR,
  • SYMBOL_CHART_MODE,
  • SYMBOL_DIGITS,
  • SYMBOL_TRADE_MODE.

Die letzte Eigenschaft ist für den Handelsmodus verantwortlich. Der synthetischer Index darf nicht gehandelt werden, daher wird die Eigenschaft auf SYMBOL_TRADE_MODE_DISABLED gesetzt. Wenn wir eine Strategie nach Symbolen im Tester prüfen müssen, dann sollte die Eigenschaft aktiviert sein (SYMBOL_TRADE_MODE_FULL).

//--- Double properties
//--- point
double symbol_point = 1./::MathPow(10, InpDxyDigits);
if(!custom_symbol_obj.SetProperty(SYMBOL_POINT, symbol_point))
   {
   ::PrintFormat("Failed to to set a point value for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- tick size
double symbol_tick_size = symbol_point;
if(!custom_symbol_obj.SetProperty(SYMBOL_TRADE_TICK_SIZE, symbol_tick_size))
   {
   ::PrintFormat("Failed to to set a tick size for the custom symbol \"%s\"", custom_symbol);
   return;
   }

Die folgenden Eigenschaften sind vom Typ ENUM_SYMBOL_INFO_DOUBLE:

  • SYMBOL_POINT,
  • SYMBOL_TRADE_TICK_SIZE.
Da zuvor festgelegt wurde, dass das Symbol nicht gehandelt wird, gibt es nur wenige Eigenschaften des Typs ‚double‘.

//--- String properties
//--- category
string symbol_category="Currency indices";
if(!custom_symbol_obj.SetProperty(SYMBOL_CATEGORY, symbol_category))
   {
   ::PrintFormat("Failed to to set a category for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- country
string symbol_country= "US";
if(!custom_symbol_obj.SetProperty(SYMBOL_COUNTRY, symbol_country))
   {
   ::PrintFormat("Failed to to set a country for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- description
string symbol_description= "Synthetic US Dollar Index";
if(!custom_symbol_obj.SetProperty(SYMBOL_DESCRIPTION, symbol_description))
   {
   ::PrintFormat("Failed to to set a description for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- exchange
string symbol_exchange= "ICE";
if(!custom_symbol_obj.SetProperty(SYMBOL_EXCHANGE, symbol_exchange))
   {
   ::PrintFormat("Failed to to set an exchange for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- page
string symbol_page = "https://www.ice.com/forex/usdx";
if(!custom_symbol_obj.SetProperty(SYMBOL_PAGE, symbol_page))
   {
   ::PrintFormat("Failed to to set a page for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- path
string symbol_path="Custom\\"+custom_group+"\\"+custom_symbol;
if(!custom_symbol_obj.SetProperty(SYMBOL_PATH, symbol_path))
   {
   ::PrintFormat("Failed to to set a path for the custom symbol \"%s\"", custom_symbol);
   return;
   }

Die folgenden Eigenschaften sind vom Typ ENUM_SYMBOL_INFO_STRING:

  • SYMBOL_CATEGORY,
  • SYMBOL_COUNTRY,
  • SYMBOL_DESCRIPTION,
  • SYMBOL_EXCHANGE,
  • SYMBOL_PAGE,
  • SYMBOL_PATH.

Die letzte Eigenschaft ist für den Pfad im Symbolbaum zuständig. Bei der Erstellung eines synthetischen Index wurden eine Gruppe von Zeichen und ein Symbolname angegeben. Daher kann diese Eigenschaft übersprungen werden, da sie identisch ist.

Natürlich könnte ich die Gleichung für den synthetischen Index auch direkt aufstellen, ohne Ticks zu sammeln. Aber dann würde der Sinn des Beispiels verloren gehen. Außerdem wird der Preis des Indexes periodisch berechnet. Im vorliegenden Beispiel beträgt die Periode 10 Sekunden.

Gehen wir nun zum nächsten Block über - dies ist eine Prüfung der Handelshistorie. Hier werden wir zwei Aufgaben lösen: den Verlauf der Balken prüfen und die Ticks laden. Überprüfen wir die Balken auf folgende Weise:

//--- check quotes history
CBaseSymbol base_symbols[BASE_SYMBOLS_NUM];
const string symbol_names[]=
  {
   "EURUSD", "USDJPY", "GBPUSD", "USDCAD", "USDSEK", "USDCHF"
  };
ENUM_TIMEFRAMES curr_tf=PERIOD_M1;
::Print("\nChecking of quotes history is running...");
for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
  {
   CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
   string curr_symbol_name=symbol_names[s_idx];
   if(ptr_base_symbol.Init(curr_symbol_name, curr_tf, InpStartDay))
     {
      ::PrintFormat("\n   Symbol #%hu: \"%s\"", s_idx+1, curr_symbol_name);
      ulong start_cnt=::GetTickCount64();
      int check_load_code=ptr_base_symbol.CheckLoadHistory();
      ::PrintFormat("   Checking code: %I32d", check_load_code);
      ulong time_elapsed_ms=::GetTickCount64()-start_cnt;
      ::PrintFormat("   Time elapsed: %0.3f sec", time_elapsed_ms/MS_IN_SEC);
      if(check_load_code<0)
        {
         ::PrintFormat("Failed to load quotes history for the symbol \"%s\"", curr_symbol_name);
         return;
        }
     }
  }

Wir benötigen 6 Symbole, die wir in einer Schleife durchlaufen und deren Preise wir verarbeiten müssen. Der Einfachheit halber wurde die Klasse CBaseSymbol geschaffen.

//+------------------------------------------------------------------+
//| Class CBaseSymbol                                                |
//+------------------------------------------------------------------+
class CBaseSymbol : public CObject
  {
      //--- === Data members === ---
   private:
      CSymbolInfo    m_symbol;
      ENUM_TIMEFRAMES m_tf;
      matrix         m_ticks_mx;
      datetime       m_start_date;
      ulong          m_last_idx;
      //--- === Methods === ---
   public:
      //--- constructor/destructor
      void           CBaseSymbol(void);
      void          ~CBaseSymbol(void) {};
      //---
      bool           Init(const string _symbol, const ENUM_TIMEFRAMES _tf, datetime start_date);
      int            CheckLoadHistory(void);
      bool           LoadTicks(const datetime _stop_date, const uint _flags);
      matrix         GetTicks(void) const
        {
         return m_ticks_mx;
        };
      bool           SearchTickLessOrEqual(const double _dbl_time, vector &_res_row);
      bool           CopyLastTick(vector &_res_row);
  };

Die Klasse befasst sich mit der Historie von Balken und Ticks, was eine äußerst wichtige Aufgabe ist, da sonst keine Daten für die Erstellung von synthetischen Symbolen vorhanden sind. 

Laden wir die Ticks:

//--- try to load ticks
::Print("\nLoading of ticks is running...");
now=::TimeCurrent();
uint flags=COPY_TICKS_INFO | COPY_TICKS_TIME_MS | COPY_TICKS_BID | COPY_TICKS_ASK;
double first_tick_dbl_time=0.;
for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
   {
   CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
   string curr_symbol_name=symbol_names[s_idx];
   ::PrintFormat("\n   Symbol #%hu: \"%s\"", s_idx+1, curr_symbol_name);
   ulong start_cnt=::GetTickCount64();
   ::ResetLastError();
   if(!ptr_base_symbol.LoadTicks(now, flags))
      {
      ::PrintFormat("Failed to load ticks for the symbol \"%s\" , error: %d", curr_symbol_name, ::GetLastError());
      return;
      }
   ulong time_elapsed_ms=::GetTickCount64()-start_cnt;
   ::PrintFormat("   Time elapsed: %0.3f sec", time_elapsed_ms/MS_IN_SEC);
   //--- looking for the 1st tick
   matrix ticks_mx=ptr_base_symbol.GetTicks();
   double tick_dbl_time=ticks_mx[0][0];
   if(tick_dbl_time>first_tick_dbl_time)
      first_tick_dbl_time=tick_dbl_time;
   }

Die integrierte Funktion matrix::CopyTicksRange() wurde zum Laden von Ticks verwendet. Der Vorteil ist, dass wir nur die Spalten in der von Flags definierten Tickstruktur laden können. Die Frage der Ressourceneinsparung ist äußerst wichtig, wenn wir Millionen von Ticks anfordern.

COPY_TICKS_INFO    = 1,       // ticks caused by Bid and/or Ask changes
COPY_TICKS_TRADE   = 2,       // ticks caused by Last and Volume changes
COPY_TICKS_ALL     = 3,       // all ticks that have changes
COPY_TICKS_TIME_MS = 1<<8,    // time in milliseconds
COPY_TICKS_BID     = 1<<9,    // Bid price
COPY_TICKS_ASK     = 1<<10,   // Ask price
COPY_TICKS_LAST    = 1<<11,   // Last price
COPY_TICKS_VOLUME  = 1<<12,   // volume
COPY_TICKS_FLAGS   = 1<<13,   // tick flags

Die Phasen der Überprüfung der Historie und des Ladens von Ticks in das Protokoll werden in Bezug auf die Zeitkosten beschrieben.

CS      0       12:01:11.802    dDxySymbol      Checking of quotes history is running...
CS      0       12:01:11.802    dDxySymbol      
CS      0       12:01:11.802    dDxySymbol         Symbol #1: "EURUSD"
CS      0       12:01:14.476    dDxySymbol         Checking code: 1
CS      0       12:01:14.476    dDxySymbol         Time elapsed: 2.688 sec
CS      0       12:01:14.476    dDxySymbol      
CS      0       12:01:14.476    dDxySymbol         Symbol #2: "USDJPY"
CS      0       12:01:17.148    dDxySymbol         Checking code: 1
CS      0       12:01:17.148    dDxySymbol         Time elapsed: 2.672 sec
CS      0       12:01:17.148    dDxySymbol      
CS      0       12:01:17.148    dDxySymbol         Symbol #3: "GBPUSD"
CS      0       12:01:19.068    dDxySymbol         Checking code: 1
CS      0       12:01:19.068    dDxySymbol         Time elapsed: 1.922 sec
CS      0       12:01:19.068    dDxySymbol      
CS      0       12:01:19.068    dDxySymbol         Symbol #4: "USDCAD"
CS      0       12:01:21.209    dDxySymbol         Checking code: 1
CS      0       12:01:21.209    dDxySymbol         Time elapsed: 2.140 sec
CS      0       12:01:21.209    dDxySymbol      
CS      0       12:01:21.209    dDxySymbol         Symbol #5: "USDSEK"
CS      0       12:01:22.631    dDxySymbol         Checking code: 1
CS      0       12:01:22.631    dDxySymbol         Time elapsed: 1.422 sec
CS      0       12:01:22.631    dDxySymbol      
CS      0       12:01:22.631    dDxySymbol         Symbol #6: "USDCHF"
CS      0       12:01:24.162    dDxySymbol         Checking code: 1
CS      0       12:01:24.162    dDxySymbol         Time elapsed: 1.531 sec
CS      0       12:01:24.162    dDxySymbol      
CS      0       12:01:24.162    dDxySymbol      Loading of ticks is running...
CS      0       12:01:24.162    dDxySymbol      
CS      0       12:01:24.162    dDxySymbol         Symbol #1: "EURUSD"
CS      0       12:02:27.204    dDxySymbol         Time elapsed: 63.032 sec
CS      0       12:02:27.492    dDxySymbol      
CS      0       12:02:27.492    dDxySymbol         Symbol #2: "USDJPY"
CS      0       12:02:32.587    dDxySymbol         Time elapsed: 5.094 sec
CS      0       12:02:32.938    dDxySymbol      
CS      0       12:02:32.938    dDxySymbol         Symbol #3: "GBPUSD"
CS      0       12:02:37.675    dDxySymbol         Time elapsed: 4.734 sec
CS      0       12:02:38.285    dDxySymbol      
CS      0       12:02:38.285    dDxySymbol         Symbol #4: "USDCAD"
CS      0       12:02:43.223    dDxySymbol         Time elapsed: 4.937 sec
CS      0       12:02:43.624    dDxySymbol      
CS      0       12:02:43.624    dDxySymbol         Symbol #5: "USDSEK"
CS      0       12:03:18.484    dDxySymbol         Time elapsed: 34.860 sec
CS      0       12:03:19.596    dDxySymbol      
CS      0       12:03:19.596    dDxySymbol         Symbol #6: "USDCHF"
CS      0       12:03:24.317    dDxySymbol         Time elapsed: 4.719 sec

Nachdem wir die Ticks erhalten haben, bilden wir die Tick-Historie für den synthetischen DXY. Dieser Prozess findet im folgenden Block statt:

//--- create a custom symbol ticks history
::Print("\nCustom symbol ticks history is being formed...");
long first_tick_time_sec=(long)(first_tick_dbl_time/MS_IN_SEC);
long first_tick_time_ms=(long)first_tick_dbl_time%(long)MS_IN_SEC;
::PrintFormat("   First tick time: %s.%d", ::TimeToString((datetime)first_tick_time_sec,
              TIME_DATE|TIME_SECONDS), first_tick_time_ms);
double active_tick_dbl_time=first_tick_dbl_time;
double now_dbl_time=MS_IN_SEC*now;
uint ticks_cnt=0;
uint arr_size=0.5e8;
MqlTick ticks_arr[];
::ArrayResize(ticks_arr, arr_size);
::ZeroMemory(ticks_arr);
matrix base_prices_mx=matrix::Zeros(BASE_SYMBOLS_NUM, 2);
do
   {
   //--- collect base symbols ticks
   bool all_ticks_ok=true;
   for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
      {
      CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
      vector tick_prices_vc;
      bool to_break_loop=false;
      if(!ptr_base_symbol.SearchTickLessOrEqual(active_tick_dbl_time, tick_prices_vc))
         to_break_loop=true;
      else
         {
         if(!base_prices_mx.Row(tick_prices_vc, s_idx))
            to_break_loop=true;
         }
      if(to_break_loop)
         {
         all_ticks_ok=false;
         break;
         }
      }
   //--- calculate index prices
   if(all_ticks_ok)
      {
      MqlTick last_ind_tick;
      CalcIndexPrices(active_tick_dbl_time, base_prices_mx, last_ind_tick);
      arr_size=ticks_arr.Size();
      if(ticks_cnt>=arr_size)
         {
         uint new_size=(uint)(arr_size+0.1*arr_size);
         if(::ArrayResize(ticks_arr, new_size)!=new_size)
            continue;
         }
      ticks_arr[ticks_cnt]=last_ind_tick;
      ticks_cnt++;
      }
   active_tick_dbl_time+=TICK_PAUSE;
   }
while(active_tick_dbl_time<now_dbl_time);
::ArrayResize(ticks_arr, ticks_cnt);
int ticks_replaced=custom_symbol_obj.TicksReplace(ticks_arr, true);

Legen wir einen vorläufigen Zeitpunkt (active_tick_dbl_time) fest, zu dem wir am Ende der Schleife 10 Sekunden hinzufügen. Dies ist eine Art „Zeitstempel“, um Ticks für alle Symbole zu erhalten, aus denen der Index besteht.

Die Suche nach dem gewünschten Tick für jedes Symbol basiert also auf einem bestimmten Zeitpunkt in der Vergangenheit. Die Methode CBaseSymbol::SearchTickLessOrEqual() liefert einen Tick, der nicht später als der Wert active_tick_dbl_time eingetroffen ist.

Wenn Ticks von jeder Komponente des Index empfangen werden, befinden sich die Tick-Preise bereits in der base_prices_mx-Matrix

Die Funktion CalcIndexPrices() gibt den bereits vorbereiteten Wert des Index-Ticks zum aktuellen Zeitpunkt zurück. 

Wenn Ticks erstellt werden, wird die Tick-Datenbank mit der Methode CiCustomSymbol::TicksReplace() aktualisiert.

Der Dienst befasst sich dann erst im nächsten Block mit der Gegenwart:

//--- main processing loop
::Print("\nA new tick processing is active...");
do
   {
   ::ZeroMemory(base_prices_mx);
   //--- collect base symbols ticks
   bool all_ticks_ok=true;
   for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
      {
      CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
      vector tick_prices_vc;
      bool to_break_loop=false;
      if(!ptr_base_symbol.CopyLastTick(tick_prices_vc))
         to_break_loop=true;
      else
         {
         if(!base_prices_mx.Row(tick_prices_vc, s_idx))
            to_break_loop=true;
         }
      if(to_break_loop)
         {
         all_ticks_ok=false;
         break;
         }
      }
   //--- calculate index prices
   if(all_ticks_ok)
      {
      MqlTick last_ind_tick, ticks_to_add[1];
      now=::TimeCurrent();
      now_dbl_time=MS_IN_SEC*now;
      CalcIndexPrices(now_dbl_time, base_prices_mx, last_ind_tick);
      ticks_to_add[0]=last_ind_tick;
      int ticks_added=custom_symbol_obj.TicksAdd(ticks_to_add, true);
      }
   ::Sleep(TICK_PAUSE);
   }
while(!::IsStopped());

Die Aufgabe des Blocks ist ähnlich wie die des vorherigen Blocks, wenn auch etwas einfacher. Alle 10 Sekunden müssen wir die Tickdaten der Symbole abrufen und die Indexpreise berechnen. Im Index wird der Geldkurs (bid) auf der Grundlage der Geldkurse aller Symbole und der Briefkurs (ask) auf der Grundlage aller Briefkurse berechnet.

Nach dem Start des Dienstes dDxySymbol kann nach einiger Zeit der Chart des nutzerdefinierte DXY-Symbols geöffnet werden (Abb. 6). 

Abb. 6. DXY-Symbolchart mit Feiertagen

Abb. 6. DXY Custom Symbol Chart mit Feiertagen 


In der Grafik sind die Samstage durch rote vertikale Segmente hervorgehoben. Es hat sich herausgestellt, dass der Dienst an Samstagen und Sonntagen weiterhin Ticks in der Historie berechnet, was wahrscheinlich nicht ganz korrekt ist. Es ist notwendig, den Servicecode durch eine zeitliche Begrenzung (auf die Wochentage) zu ergänzen. Weisen wir diese Aufgabe der Funktion CheckDayOfWeek() zu.

Das synthetische Chart sieht nun wie folgt aus (Abb. 7). Es sieht so aus, als ob der Fehler behoben wurde.

Chart von DXY, dem nutzerdefinierten Symbol ohne Feiertage

Abb. 7. DXY Custom Symbol Chart ohne Feiertage 

Damit ist die Arbeit mit dem Dienst dDxySymbol abgeschlossen.


Schlussfolgerung

Der Artikel hat einige Merkmale eines MQL5-Programmtyps hervorgehoben, der als Dienst bekannt ist. Diese Art eines MQL5-Programms unterscheidet sich dadurch, dass es nicht an einen Chart gebunden ist, sondern unabhängig arbeitet. Es liegt in der Natur der Dienste, dass sie mit anderen EAs, Skripten und wahrscheinlich in geringerem Maße auch mit Indikatoren in Konflikt geraten können. Daher obliegt es dem Entwickler, die Rechte und Pflichten der Software eines Dienstes in der MetaTrader 5-Umgebung zu definieren.

Das Archiv enthält Quellen, die im Ordner %MQL5\Services abgelegt werden können.

Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/11826

Beigefügte Dateien |
code.zip (23.7 KB)
DoEasy. Steuerung (Teil 28): Balkenstile im ProgressBar-Steuerelement DoEasy. Steuerung (Teil 28): Balkenstile im ProgressBar-Steuerelement
In diesem Artikel werde ich Anzeigestile und Beschreibungstext für die Fortschrittsleiste des Steuerelements der ProgressBar entwickeln.
Techniken des MQL5-Assistenten, die Sie kennen sollten (Teil 05): Markov-Ketten Techniken des MQL5-Assistenten, die Sie kennen sollten (Teil 05): Markov-Ketten
Markov-Ketten sind ein leistungsfähiges mathematisches Werkzeug, das zur Modellierung und Vorhersage von Zeitreihendaten in verschiedenen Bereichen, einschließlich des Finanzwesens, verwendet werden kann. In der Finanzzeitreihenmodellierung und -prognose werden Markov-Ketten häufig zur Modellierung der zeitlichen Entwicklung von Finanzwerten wie Aktienkursen oder Wechselkursen verwendet. Einer der Hauptvorteile von Markov-Kettenmodellen ist ihre Einfachheit und Nutzerfreundlichkeit.
Algorithmen zur Optimierung mit Populationen Cuckoo-Optimierungsalgorithmus (COA) Algorithmen zur Optimierung mit Populationen Cuckoo-Optimierungsalgorithmus (COA)
Der nächste Algorithmus, den ich besprechen werde, ist die Optimierung der Kuckuckssuche (Cockoo) mit Levy-Flügen. Dies ist einer der neuesten Optimierungsalgorithmen und ein neuer Spitzenreiter in der Rangliste.
Die Kategorientheorie in MQL5 (Teil 1) Die Kategorientheorie in MQL5 (Teil 1)
Die Kategorientheorie ist ein vielfältiger und expandierender Zweig der Mathematik, der in der MQL-Gemeinschaft noch relativ unentdeckt ist. In dieser Artikelserie sollen einige der Konzepte vorgestellt und untersucht werden, mit dem übergeordneten Ziel, eine offene Bibliothek einzurichten, die zu Kommentaren und Diskussionen anregt und hoffentlich die Nutzung dieses bemerkenswerten Bereichs für die Strategieentwicklung der Händler fördert.