Verwendung von Textdateien zum Speichern von Eingabeparametern von Expert Advisors, Indikatoren und Skripts

Andrei Novichkov | 21 Juli, 2016

Einführung

Beim Entwickeln und Verwenden verschiedener Tradingtools, treffen wir manchmal auf Fälle, in denen die üblichen Mittel der Eingabe via extern und input Modifikatoren nicht ausreichen. Obwohl wir eine universelle Lösung haben, um alle unsere Bedürfnisse zu befriedigen, stellt sich manchmal heraus, dass das eher umständlich und unflexibel ist. Als Beispiel betrachten wir die folgenden Fälle.

Wie man Parameter in Textdateien speichert

Reine Text-Dateien können als Ergänzung zur herkömmlichen Methode der Erstellung und Speicherung von Eingabeparametern verwendet werden. Sie können alles was sie möchten dort verwenden, außerdem können sie leicht bearbeitet und verschoben werden. Ihre Struktur kann ähnlich wie in INI-Dateien angelegt werden. Hier sieht man beispielsweise wie ein ganzzahliger Array Typ in einer Textdatei gespeichert aussehen kann:

/* Arraygröße */
{array_size},5
/*array*/
{array_init},0,1,2,3,4

Im obigen Beispiel wurde der "Anker" des "Abschnittsnamens" am Beginn des Strings gefolgt vom Abschnittsinhalt (getrennt durch Komma) geschrieben. Jeder eindeutige String von Zeichen kann als "Anker" dienen. Eine solche Datei wird in der Terminal "Sandbox" generiert und gespeichert. Dann öffnen wir dieses Datei im Nur-Lesen-Modus (als CSV-Datei) im Initialisierungsblock des Indikators, Experts oder Skripts.

Wenn es an der Zeit ist, das gespeicherte Array zu lesen, suchen wir nach dem "Anker" in der Datei mit dem bekannten Namen{array_size}. Wir sollten den Dateizeiger auf den Anfang der Datei durch Aufruf von FileSeek(handle,0,SEEK_SET) setzen. Nun suchen wir nach dem nächsten "Anker" unter Verwendung eines bekannten Namens {array_init}. Wenn dieser gefunden wird, lesen wir einfach die notwendige Anzahl von String und wandeln sie entsprechend dem Arraytyp um (in diesem Fall wird die Umwandlung nach Integer ausgeführt). Die ConfigFiles.mqh Include-Datei enthält ein paar einfache Funktionen für die Implementierung der "Anker"-Suche und das Lesen der Daten.

Anders ausgedrückt: ein Objekt das wir in die Datei speichern sollte allgemein durch den "Anker" beschrieben werden dem ein String mit spaltengetrennten Daten folgt, so wie es das CSV-Format verlangt. Sie können mehrere "Anker" und nachfolgende Datenstrings verwenden falls notwendig, beachten Sie aber, dass der "Anker" nur mit einem String korrespondiert.

Sowohl Kommentare als auch verschiedene Bemerkungen und Anweisungen können in eine Datei in beinaher jeder beliebigen Form und jedem beliebigen Platz geschrieben werden.  Es gibt aber ein offensichtliches Erfordernis: der Text sollte nicht die "Anker"-Datenfolge unterbrechen. Eine weitere wünschenswerte Bedingung: jeder große Text sollte am Ende der Datei platziert werden um die Suche nach "Ankern" zu vereinfachen.

Unten können Sie die mögliche Anordnung zum Speichern des oben erwähnten Zeitplans für den EA sehen:

….......
{d0},0,0,22
{d1},0,0,22
{d2},1,0,22
…........
{d6},0,0,22

Hier spiegel der "Anker"-Name die Tage der Woche durch Verwendung einer bestimmten Nummer wider (zum Beispiel steht {d1} für Tag Nummer eins, etc.). Der "Anker"-Name wird von einem Wert des Bool-Typs gefolgt der festlegt, ob der EA an diesem Tag handeln soll oder nicht (in unserem Fall handelt er nicht bei d1). Wenn kein Handel stattfindet, können die folgenden Werte ignoriert werden sie wurden aber trotzdem stehen gelassen. Schlussendlich stehen die letzten zwei Werte des Typs Integer für den Anfangs- und Endzeitpunkt des EA in Stunden.

Bei der Eröffnung einer Tageskerze liest der EA den Zeitplan für den neuen Tag. Natürlich kann der Datenstring für jeden Tag verändert und durch beliebige andere Werte erweitert werden, je nach Programmlogik. So werden nur die aktuellen Tagesdaten im Arbeitsspeicher gespeichert.

Sie können jeden beliebigen"Anker"-Namen festlegen, aber ich empfehle gesundem Menschenverstand walten zu lassen. Es gibt keinen Grund abstrakte Namen für "Anker" zu vergeben. Stattdessen sollten sie einen Sinn ergeben, während sie gleichzeitig aber einzigartig bleiben. Bedenken Sie bitte, dass die Datensuche nach Namen durchgeführt wird. Abhängig von der jeweiligen Implementierung kann eine einzelne Datei verschiedene "Anker" des selben Namens beinhalten.

Betrachten Sie die Code-Fragmente, die dem voll funktionsfähigen Indikator entnommen wurden. Der Indikator braucht Daten von verschiedenen Währungspaaren für die korrekte Verarbeitung. Er bezieht daher über den Timer Daten und verarbeitet sie gemäß seiner Logik (die Indikatorlogik hat hier keine Bedeutung für uns). Bitte beachten sie, dass Broker manchmal verschiedene Suffixes und Präfixes an die Symbolnamen anhängen (zum Beispiel könnte EURUSD in #.EURUSD.ch umbenannt werden). Das sollte man berücksichtigen, sodass der EA korrekt auf andere Symbole verweisen kann. Die Reihenfolge unserer Handlungen ist wie folgt.

1. Erstell eine Textdatei broker.cfg mit folgendem Inhalt: 

[PREFIX_SUFFIX],#.,.ch
2. Erstelle eine Textdatei <indicator_name>.cfg mit folgendem Inhalt:
/*Anzahl von Währungspaaren mit dem "Anker"-String [CHARTNAMES] */
[CHARTCOUNT],7
/*Namen von Währungspaaren deren Charts vom Indikator zum Lesen der Daten verwendet werden sollen*/
[CHARTNAMES],USDCAD,AUDCAD,NZDCAD,GBPCAD,EURCAD,CADCHF,CADJPY
/*Position der Basiswährung in einem Paar (in diesem Fall CAD) - vorn oder hinten*/
[DIRECT],0,0,0,0,0,1,1
3. Platziere beide Dateien in der "Sandbox".

4. Einige Zusatzfunktionen im Indikator-Code definieren (oder die ConfigFiles.mqh-Datei einbinden):

// Öffnen der Datei mit einem bestimmten Namen wie eine CSV-Datei und Handle zurückgeben
   int __OpenConfigFile(string name)
     {
      int h= FileOpen(name,FILE_READ|FILE_SHARE_READ|FILE_CSV,',');
      if(h == INVALID_HANDLE) PrintFormat("Ungültiger Dateiname %s",name);
      return (h);
     }

//+-------------------------------------------------------------------------------------+
//| Die Funktion liest einen iRes-Wert des Typs long des beinhaltenden Abschnitts       |
//| strSec "Anker" Name in den Dateihandle. Bei Erfolg,                                 |
//| wird true zurückgegeben, sonst – false.                                             |
//+-------------------------------------------------------------------------------------+
   bool _ReadIntInConfigSection(string strSec,int handle,long &iRes)
     {
      if(!FileSeek(handle,0,SEEK_SET) ) return (false);
      string s;
      while(!FileIsEnding(handle))
        {
         if(StringCompare(FileReadString(handle),strSec)==0)
           {
            iRes=StringToInteger(FileReadString(handle));
            return (true);
           }
        }
      return (false);
     }

//+-------------------------------------------------------------------------------------+
//| Die Funktion liest sArray entsprechend der Zählergröße im beinhaltenden Abschnitt   |
//| strSec "Anker" Name in den Dateihandle. Bei Erfolg,                                 |
//| wird true zurückgegeben, sonst – false.                                             |
//+-------------------------------------------------------------------------------------+
   bool _ReadStringArrayInConfigSection(string strSec,int handle,string &sArray[],int count)
     {
      if(!FileSeek(handle,0,SEEK_SET) ) return (false);
      while(!FileIsEnding(handle))
        {
         if(StringCompare(FileReadString(handle),strSec)==0)
           {
            ArrayResize(sArray,count);
            for(int i=0; i<count; i++) sArray[i]=FileReadString(handle);
            return (true);
           }
        }
      return (false);
     }

//+---------------------------------------------------------------------------------------------------+
//| Die Funktion liest bArray des bool Typs entsprechend der Zählergröße im beinhaltenden Abschnitt   |
//| strSec "Anker" Name in den Dateihandle. Bei Erfolg,                                               |
//| wird true zurückgegeben, sonst – false.                                                           |
//+---------------------------------------------------------------------------------------------------+
   bool _ReadBoolArrayInConfigSection(string strSec,int handle,bool &bArray[],int count)
     {
      string sArray[];
      if(!_ReadStringArrayInConfigSection(strSec, handle, sArray, count) ) return (false);
      ArrayResize(bArray,count);
      for(int i=0; i<count; i++)
        {
         bArray[i]=(bool)StringToInteger(sArray[i]);
        }
      return (true);
     }
Außerdem bauen Sie den folgenden Code in den Indikator ein:
   …..
   input string strBrokerFname  = "broker.cfg";       // Name der Datei die die Brokerdaten beinhaltet
   input string strIndiPreFname = "some_name.cfg";    // Name der Datei die die Indikator Einstellungen beinhaltet
   …..

   string strName[];         // Array mit Symbolnamen ([CHARTNAMES])
   int    iCount;            // Anzahl von Währungspaaren
   bool   bDir[];            // "Sequenz" Array ([DIRECT])

   …..
   int OnInit(void) 
     {

      string prefix,suffix;     // Präfix und Suffix. Lokale Variablen die notwendig sind für 
                                // Initialisierung, aber nur einmal verwendet werden.
      prefix= ""; suffix = "";
      int h = _OpenConfigFile(strBrokerFname);
      // Lies Daten über Präfix und Suffix von der Konfigurationsdatei. Wenn Fehler 
      // behoben werden , setze mit Standardwerten fort.
      if(h!=INVALID_HANDLE) 
        {
         if(!_GotoConfigSection("[PREFIX_SUFFIX]",h)) 
           {
            PrintFormat("Fehler in der Konfigurationsdatei %s",strBrokerFname);
              } else {
            prefix = FileReadString(h);
            suffix = FileReadString(h);
           }
         FileClose(h);
        }
      ….
      // Lese die Indikator-Einstellungen. 
      if((h=__OpenConfigFile(strIndiPreFname))==INVALID_HANDLE)
         return (INIT_FAILED);

      // Anzahl der Symbole lesen
      if(!_ReadIntInConfigSection("CHARTCOUNT]",h,iCount)) 
        {
         FileClose(h);
         return (INIT_FAILED);
        }
      // lies das Array mit Symbolnamen nachdem deren Anzahl gelesen wurde
      if(!_ReadStringArrayInConfigSection("[CHARTNAMES]",h,strName,iCount)) 
        {
         FileClose(h);
         return (INIT_FAILED);
        }

      // Wandle Symbolnamen in den erforderliche Darstellung um
      for(int i=0; i<iCount; i++) 
        {
         strName[i]=prefix+strName[i]+suffix;
        }

      // lies Array des Bool-Typ Parameter
      if(!_ReadBoolArrayInConfigSection("[DIRECT]",h,bDir,iCount)) 
        {
         FileClose(h);
         return (INIT_FAILED);
        }
      ….
      return(INIT_SUCCEEDED);
     }

Zwei dynamische Arrays und zwei kritische lokale Variablen wurden während der Code-Ausführung initialisiert. Die erzeugte broker.cfg Datei wird ziemlich oft nützlich sein, indem sie Ihnen erspart, jedesmal Präfix und Suffix händisch einzugeben.

Mögliche Anwendungsgebiete. Nachteile und Alternativen

Abgesehen von den bereits erwähnten Fällen kann die vorgeschlagene Methode bequem die Verwaltung von mehreren Instanzen eines Indikators oder EA's die auf verschiedenen Währungspaare arbeiten, ermöglichen. Sie kann auch für ähnliche Aufgaben verwendet werden, die ein einzelnes "Kontrollzentrum" erfordern. Die Methode kann auch nützlich sein, wenn Sie nur eine Textdatei bearbeiten oder ersetzen müssen um eine Remote-Konfiguration durchzuführen, ohne Zugriff auf das Terminal selbst zu haben. Es ist nicht ungewöhnlich, dass FTP die einzige Möglichkeit ist, Zugang zu einem PC mit dem Terminal zu bekommen.

Ein weiterer möglicher Anwendungsbereich entsteht, wenn ein Händler ein Dutzend Terminals verwaltet, die sich an verschiedenen Orten befinden (vielleicht sogar in verschiedenen Ländern). Bereiten Sie einfach die Konfigurationsdatei einmal vor und senden Sie diese an die "Sandboxes" aller Terminals. Wenn wir zum zuvor erwähnten Beispiel des Zeitplans zurückzukehren, ist es möglich seine Auslieferung einmal pro Woche zwecks synchronen Betrieb von EAs auf allen PCs auszuführen. Der Indikator, dessen Code-Fragmente oben dargestellt wurden, funktioniert auf 28 Paaren und wird synchron von den beiden genannten Dateien verwaltet.

Ein weiteres interessantes Anwendungsgebiet beinhaltet noch die Möglichkeit eine Variable gleichzeitig in einer Datei und in den Eigenschaften gleichzeitig zu speichern. In diesem Fall, können Sie eine Art Logik für die Priorität für das Lesen implementieren. Um dies zu illustireren, betrachten wir ein Pseudocode-Fragment basierend auf der Initialisierung einer Magic-Number:

…..
extern int Magic=0;
…..
int OnInit(void) 
  {
   …..
   if(Magic==0) 
     {
      // Das bedeutet, dass der User das Magic nicht initialisiert hat und
      // der Standardwert stehen blieb. In diesem Fall holen wir uns die Magic-Variable aus einer Datei
      …...
     }
   if(Magic==0) 
     {
      // Immer noch Null. Es gibt also keine solche Variable in der Datei
      // die aktuelle Situation verarbeiten,
      // Ausstieg mit Fehler
      return (INIT_FAILED);
     }

Dieses Beispiel zeigt einene weiteren Vorteil dieser Methode. Für den Fall, dass die Datei für die Verwaltung mehrere Indikator-/EA-Instanzen verwendet wird, kann die Methode eine gründlichere Feinabstimmung der einzelnen Instanzen getrennt unter Umgehung der zentralisierten Konfiguration aus der Datei durchführen. 

Um objektiv zu bleiben, lassen Sie uns auch die Nachteile der Methode beschreiben.

Der erste ist die geringe Geschwindigkeit. Allerdings wird das keine große Rolle spielen, wenn Sie die Methode zur Initialisierung eines Indikators/EA's oder nur periodisch (zum Beispiel bei einer neuen Tageskerze) verwenden.

Der zweite – gründliche und sorgfältige Vorgehensweis bei der zugehörigen Dokumentation ist kritisch. Es kann sehr schwierig sein, Händlern darzulegen, wie man Werkzeuge verwendet, die auf diese Weise angepasst wurden. Dies ist ein weiterer Grund, nur selten zu ändernde Einstellungen in diesen Dateien zu speichern. Natürlich sollte die größtmögliche Aufmerksamkeit der Vorbereitung und Bearbeitung der Textdateien selbst gewidmet werden. Fehler in diesem Stadium können schwerwiegende finanzielle Folgen haben.

Nun, betrachten wir alternative Lösungen für die genannten Aufgaben.

Die erste ist die Verwendung von INI-Dateien. Dies ist eine zuverlässige Methode zur Speicherung von Daten. INI-Dateien werden verwendet, wenn Sie nicht in die Registrierung schreiben wollen. Ihre Struktur ist klar und leicht verständlich. Unten sehen Sie unser erstes Beispiel (Zeitplan) als INI-Datei:

….......
[d0]
Allowed=0
BeginWork=0
EndWork=22
.........
[d2]
Allowed=1
BeginWork=0
EndWork=22

In allen anderen Aspekten ist die Arbeit mit INI-Dateien ähnlich – sie sollten in die "Sandbox" gesetzt werden und ihre Verarbeitungsgeschwindigkeit ist auch eher gering (wenn auch etwas besser als die von CSV-Dateien). Genau wie mit CSV-Dateien empfiehlt es sich, sie bei einer Initialisierung oder einer neuen Kerze zu verwenden. Denken Sie aber daran, dass Sie Kommentare in einer INI-Datei nicht beliebig unterbringen können, da INI-Dateien ihre eigenen Regeln in Bezug auf Kommentare haben. Wie auch immer, werden Sie wahrscheinlich keine Probleme mit der entsprechenden Dokumentation bei diesem Dateiformat haben.

Der Nachteil dieses Formats ist, dass Sie Drittanbieter-Bibliotheken einbinden müssen. Die MQL-Sprache bietet keine Mittel für die Arbeit INI-Dateien direkt, daher müssen Sie diese aus der kernell32.dll importieren. Sie finden mehrere Bibliotheken auf der MQL5 Website finden, die solche Möglichkeit bieten, daher ist es nicht notwendig hier eine weitere anzubieten. Solche Bibliotheken sind einfach, da nur zwei Funktionen importiert werden. Sie können jedoch immer noch Fehler enthalten. Außerdem können Sie nie sicher sein, ob die gesamte Konstruktion mit Linux kompatibel ist. Wenn Sie Drittanbieter-Bibliotheken nicht fürchten, dann sind Sie herzlich eingeladen INI-Dateien gleichberechtigt mit CSV-Dateien zu verwenden.

Abgesehen von INI-Dateien sind andere Lösungen möglich. Betrachten wir sie in Kürze.

  1. Verwendung der Registry. Aus meiner Sicht ist dies die fragwürdigste Lösung, da die Registry für den normalen Betrieb des Betriebssystems kritisch ist. Daher wäre es strategisch falsch, Skripte und Indikatoren dort hinein schreiben zu lassen.
  2. Verwendung der Datenbank. Dies ist eine zuverlässige Methode zur Speicherung einer Datenmenge. Aber braucht ein EA oder Indikator wirklich solche Datenmengen? In den meisten Fällen ist die Antwort "Nein". Die Funktionalität der Datenbanken erübrigt sich eindeutig für die Erreichung unserer Ziele. Datenbanken sind jedoch immer noch praktisch für den Fall, dass Sie mehrere Gigabyte an Daten speichern müssen.
  3. Verwendung von XML. In der Tat handelt es sich um die gleichen Text-Dateien mit unterschiedlichen Syntax, die Sie im Vorfeld meistern müssen. Darüber hinaus sollten Sie eine Bibliothek entwickeln (oder eine fertige laden) um mit XML-Dateien zu arbeiten. Die schwierigste Aufgabe ist die Erstellung der Dokumentation am Ende der Arbeit. Ich glaube, dass in diesem Fall die Ergebnisse kaum der Mühe wert sind.

Fazit

Abschließend möchte ich darauf hinweisen, dass Text-Dateien in verschiedenen Aspekten des Handels, zum Beispiel bei Mechanismen zum Kopieren von Trades, bereits weit verbreitet sind. MetaTrader-Terminals erzeugen und verwenden eine Vielzahl von verschiedenen Text-Dateien, einschließlich Konfigurationsdateien, verschiedene Protokolle, e-Mails und Vorlagen.

Der Artikel stellt einen Versuch dar, gemeinsame CSV-Text-Dateien bei der Konfiguration von Trading-Tools – EAs, Indikatoren und Skripte anzuwenden. Ich habe ein paar einfache Beispiele verwendet, um neue Möglichkeiten solcher Dateien zu veranschaulichen. Darüber hinaus wurden einige Alternativen und erkannte Nachteile analysiert.

Jedenfalls sollte die Methode zur Speicherung von Eingaben jeweils den Aufgaben des Entwicklers entsprechend ausgewählt werden. Die Effizienz einer solchen Entscheidung ist einer der Faktoren, die das Know-how des Entwickler demonstrieren.