English 日本語
preview
Beherrschen von Dateioperationen in MQL5: Von Basic I/O bis zum Erstellen eines nutzerdefinierten CSV-Readers

Beherrschen von Dateioperationen in MQL5: Von Basic I/O bis zum Erstellen eines nutzerdefinierten CSV-Readers

MetaTrader 5Indikatoren | 16 April 2025, 08:22
118 0
Sahil Bagdi
Sahil Bagdi

Einführung

In der heutigen Welt des automatisierten Handels sind Daten alles. Vielleicht müssen Sie nutzerdefinierte Parameter für Ihre Strategie laden, eine Watchlist mit Symbolen lesen oder historische Daten aus externen Quellen integrieren. Wenn Sie mit MetaTrader 5 arbeiten, wird es Sie freuen zu hören, dass MQL5 es ziemlich einfach macht, Dateien direkt in Ihrem Code zu verarbeiten.

Aber seien wir ehrlich: Wenn man sich durch die Dokumentation wühlt, um herauszufinden, wie eine Datei funktioniert, kann das anfangs etwas überwältigend sein. Deshalb werden wir in diesem Artikel die Grundlagen in einer freundlichen Weise schrittweisen aufschlüsseln. Nachdem wir die Grundlagen behandelt haben – z. B. wie die „Sandbox“ von MQL5 Ihre Dateien schützt, wie man Dateien im Text- oder Binärmodus öffnet und wie man Zeilen sicher liest und aufteilt – werden wir das alles in die Praxis umsetzen, indem wir eine einfache Klasse zum Lesen erstellen.

Warum CSV-Dateien? Denn sie sind allgegenwärtig – einfach, menschenlesbar und werden von unzähligen Tools unterstützt. Mit einem CSV-Reader können Sie externe Parameter, Symbollisten oder andere nutzerdefinierte Daten direkt in Ihren Expert Advisor oder Ihr Skript importieren und so das Verhalten Ihrer Strategie anpassen, ohne jedes Mal den Code ändern zu müssen.

Wir werden Sie nicht in jedem winzigen Detail der MQL5-Dateifunktionen ertränken, aber wir werden das behandeln, was Sie wissen müssen. Am Ende werden Sie ein klares Beispiel dafür haben, wie man eine CSV-Datei im Textmodus öffnet, wie man ihre Zeilen bis zum Ende der Datei liest, wie man jede Zeile durch ein gewähltes Trennzeichen in Felder aufteilt, wie man diese Felder nach Spaltennamen oder Index speichert und abruft und wie man jedes dieser Felder versteht.

Hier ist der Plan für diesen Artikel:

  1. Grundlagen der MQL5-Dateioperationen
  2. Entwurf der Klasse des CSV-Readers
  3. Fertigstellung der Implementierung der Klasse des CSV-Readers
  4. Tests und Nutzungsszenarien
  5. Schlussfolgerung


Grundlagen der MQL5-Dateioperationen

Bevor wir unseren CSV-Reader implementieren, wollen wir uns einige grundlegende Konzepte der Dateiverarbeitung in MQL5 genauer ansehen und sie mit Code veranschaulichen. Wir werden uns auf das Verständnis der Sandbox-Einschränkungen, die Modi zum Öffnen von Dateien, das zeilenweise Lesen und die grundlegende Fehlerbehandlung konzentrieren. Diese Grundlagen in Aktion zu sehen, wird es später einfacher machen, unseren CSV-Reader zu bauen und zu debuggen.

Zunächst müssen wir die Sandbox und den eingeschränkten Dateizugriff verstehen. MQL5 erzwingt ein Sicherheitsmodell, das Dateioperationen auf bestimmte Verzeichnisse, die so genannte „Sandbox“, beschränkt. Normalerweise können Sie nur Dateien lesen und schreiben, die sich in <TerminalDataFolder>/MQL5/Files befinden. Wenn Sie versuchen, auf Dateien außerhalb dieses Verzeichnisses zuzugreifen, wird die Funktion FileOpen() fehlschlagen.

Wenn Sie zum Beispiel eine Datei mit dem Namen data.csv im Ordner MQL5/Files Ihres MT5-Terminals ablegen, können Sie sie wie folgt öffnen:

int fileHandle = FileOpen("data.csv", FILE_READ|FILE_TXT);
if(fileHandle == INVALID_HANDLE)
  {
   Print("Error: Could not open data.csv. LastError=", _LastError);
   // _LastError can help diagnose if it's a path or permission issue
   return;
  }

// Successfully opened the file, now we can read from it.

Sie fragen sich vielleicht, was diese Fehlercodes bedeuten. Zum Beispiel bedeutet _LastError = 5004 typischerweise so etwas wie „Datei nicht gefunden“ oder „Kann die Datei nicht öffnen“, was oft auf einen Tippfehler im Dateinamen oder darauf zurückzuführen ist, dass die Datei nicht in MQL5/Files enthalten ist. Wenn Sie einen anderen Code sehen, kann Ihnen ein kurzer Blick in die MQL5-Dokumentation oder die Community-Foren helfen, die Nachricht zu entschlüsseln. Manchmal ist es nur ein Pfadproblem, manchmal ist die Datei durch ein anderes Programm gesperrt. Wenn externe Daten für Ihren EA von entscheidender Bedeutung sind, sollten Sie einen schnellen Wiederholungsversuch oder einen detaillierten Fehlerausdruck hinzufügen, damit Sie Probleme schnell beheben können.

Beim Öffnen einer Datei haben wir viele Möglichkeiten. Wenn Sie FileOpen() aufrufen, geben Sie Flags an, um zu steuern, wie auf die Datei zugegriffen wird. Häufige Flaggen sind:

  • FILE_READ : Öffnet die Datei zum Lesen.
  • FILE_WRITE : Offnet zum Schreiben.
  • FILE_BIN : Binärmodus (keine Textverarbeitung).
  • FILE_TXT : Textmodus (behandelt Zeilenenden und Textumwandlungen).
  • FILE_CSV : Spezieller Textmodus, der die Datei wie eine CSV-Datei behandelt.

Zum Lesen einer Standard-CSV-Datei ist FILE_READ|FILE_TXT ein guter Ausgangspunkt. Der Textmodus sorgt dafür, dass FileReadString() bei Zeilenumbrüchen stoppt, was die zeilenweise Verarbeitung von Dateien vereinfacht:

int handle = FileOpen("params.txt", FILE_READ|FILE_TXT);
if(handle != INVALID_HANDLE)
  {
   Print("File opened in text mode.");
   // ... read lines here ...
   FileClose(handle);
  }
else
  {
   Print("Failed to open params.txt");
  }

Sobald die Datei im Textmodus geöffnet ist, ist das Lesen von Zeilen ein Kinderspiel. Verwenden Sie FileReadString(), um bis zum nächsten Zeilenumbruch zu lesen. Wenn die Datei endet, gibt FileIsEnding() true zurück. Betrachten Sie diese Schleife:

int handle = FileOpen("list.txt", FILE_READ|FILE_TXT);
if(handle == INVALID_HANDLE)
  {
   Print("Error opening list.txt");
   return;
  }

while(!FileIsEnding(handle))
  {
   string line = FileReadString(handle);
   if(line == "" && _LastError != 0)
     {
      // If empty line and there's an error, break
      Print("Read error or unexpected end of file. _LastError=", _LastError);
      break;
     }
   
   // Process the line
   Print("Line read: ", line);
}

FileClose(handle);

In diesem Ausschnitt lesen wir kontinuierlich Zeilen, bis wir das Ende der Datei erreichen. Wenn ein Fehler auftritt, halten wir an. Leerzeilen sind erlaubt, wenn Sie sie also überspringen wollen, prüfen Sie einfach if(line==““) continue; . Dieser Ansatz ist bei der Bearbeitung von CSV-Zeilen sehr nützlich.

Beachten Sie, dass Textdateien nicht immer einheitlich sind. Die meisten verwenden \n oder \r\n für Zeilenenden, und MQL5 verarbeitet diese normalerweise sauber. Wenn Sie eine Datei aus einer ungewöhnlichen Quelle erhalten, sollten Sie dennoch überprüfen, ob die Zeilen korrekt gelesen werden. Wenn FileReadString() merkwürdige Ergebnisse liefert (z. B. zusammenhängende Zeilen), öffnen Sie die Datei in einem Texteditor und überprüfen Sie die Kodierung und den Zeilenumbruchstil. Vergessen wir nicht extrem lange Zeilen – bei kleinen CSV-Dateien zwar eher selten, aber möglich. Eine Längenkontrolle oder ein Trimmen kann dazu beitragen, dass Ihr EA nicht über unerwartete Formate stolpert.

Bei der Verarbeitung von CSV-Daten wird jede Zeile anhand eines Begrenzungszeichens, häufig ein Komma oder Semikolon, in Felder unterteilt. Die Funktion StringSplit() von MQL5 hilft dabei:

string line = "EURUSD;1.2345;Some Comment";
string fields[];
int count = StringSplit(line, ';', fields);

if(count > 0)
  {
   Print("Found ", count, " fields");
   for(int i=0; i<count; i++)
     Print("Field[", i, "] = ", fields[i]);
  }
else
  {
   Print("No fields found in line: ", line);
}

Dieser Code gibt jedes Feld aus. Beim Lesen von CSV-Dateien werden diese Felder nach der Aufteilung im Speicher abgelegt, damit Sie später über den Spaltenindex oder -namen darauf zugreifen können.

Während StringSplit() für einfache Trennzeichen gut funktioniert, sollten Sie bedenken, dass CSV-Formate kompliziert werden können. Einige haben Felder mit Anführungszeichen oder ungewöhnlichen Begrenzungszeichen, die wir hier nicht behandeln. Wenn Ihre Datei einfach ist – ohne Anführungszeichen oder ausgefallene Tricks – reicht StringSplit() aus. Wenn Felder nachstehende Leerzeichen oder ungerade Satzzeichen enthalten, sollten Sie nach dem Aufteilen StringTrim() verwenden. Solche kleinen Überprüfungen sorgen dafür, dass Ihr EA auch dann stabil bleibt, wenn Ihre Datenquelle kleinere Formatierungsfehler aufweist.

Viele CSV-Dateien haben eine Kopfzeile, in der die Spaltennamen definiert sind. Wenn _hasHeader in unserem kommenden CSV-Reader wahr ist, wird die erste gelesene Zeile aufgeteilt und in einer Hash-Map gespeichert, die die Spaltennamen den Indizes zuordnet.

Zum Beispiel:

// Assume header line: "Symbol;MaxLot;MinSpread"
string header = "Symbol;MaxLot;MinSpread";
string cols[];
int colCount = StringSplit(header, ';', cols);

// Suppose we have a CHashMap<string,uint> Columns;
for(int i=0; i<colCount; i++)
  Columns.Add(cols[i], i);

// Now we can quickly find the index for "MinSpread" or any other column name.
uint idx;
bool found = Columns.TryGetValue("MinSpread", idx);
if(found)
  Print("MinSpread column index: ", idx);
else
  Print("Column 'MinSpread' not found");
Wenn keine Kopfzeile vorhanden ist, werden wir uns auf numerische Indizes beschränken. Die erste gelesene Zeile ist eine Datenzeile, und die Spalten werden durch ihre Position referenziert.


Die Hash-Map (CHashMap) für Spaltennamen ist ein kleines Detail, das einen großen Unterschied macht. Ohne diese Funktion müssten Sie jedes Mal, wenn Sie einen Spaltenindex benötigen, eine Schleife durch die Überschriftenfelder ziehen. Bei einer Hash-Map erhalten Sie mit TryGetValue() sofort den Index. Wenn eine Spalte nicht gefunden wird, können Sie einen Fehlerwert zurückgeben - einfach und elegant. Wenn Sie sich Sorgen über doppelt vorkommende Spalten machen, können Sie beim Lesen der Kopfzeile eine Schnellprüfung hinzufügen und eine Warnung ausgeben, wenn Duplikate auftreten. Kleine Verbesserungen wie diese sorgen dafür, dass Ihr Code stabil bleibt, wenn Ihre CSV-Dateien mit der Zeit immer komplexer werden.

Für die Datenspeicherung halten wir es einfach: Jede analysierte Zeile (nach dem Splitting) wird zu einer Zeile. Wir werden CArrayString verwenden, um Felder einer einzelnen Zeile zu speichern, und CArrayObj, um mehrere Zeilen zu speichern:

#include <Arrays\ArrayObj.mqh>
#include <Arrays\ArrayString.mqh>

CArrayObj Rows;

// after splitting line into fields:
CArrayString *row = new CArrayString;
for(int i=0; i<count; i++)
  row.Add(fields[i]);

Rows.Add(row);

Später, um einen Wert abzurufen:

// Access row 0, column 1
CArrayString *aRow = Rows.At(0);
string val = aRow.At(1);
Print("Row0 Col1: ", val);
// Access row 0, column 1
CArrayString *aRow = Rows.At(0);
string val = aRow.At(1);
Print("Row0 Col1: ", val);

Wir müssen sicherstellen, dass die Indizes gültig sind, bevor wir auf sie zugreifen. 

Gehen Sie immer davon aus, dass Dateien oder Spalten fehlen könnten. Wenn FileOpen() zum Beispiel INVALID_HANDLE zurückgibt, wird das protokolliert und zurückgekehrt. Wenn ein angeforderter Spaltenname nicht existiert, wird ein Standardwert zurückgegeben. Unsere endgültige Klasse des CSV-Readers kapselt diese Prüfungen, sodass der Haupt-EA-Code aufgeräumt bleibt.

Mit diesen Grundlagen – Sandbox-Regeln, Öffnen von Dateien, Lesen von Zeilen, Aufteilen von Feldern und Speichern von Ergebnissen – haben wir alle Bausteine, die wir brauchen. In den nächsten Abschnitten werden wir unsere Klasse des CSV-Readers anhand dieser Konzepte Schritt für Schritt entwerfen und implementieren. Wenn wir uns jetzt auf die Klarheit und die Fehlerbehandlung konzentrieren, wird die spätere Implementierung reibungsloser und zuverlässiger sein.


Entwurf der Klasse des CSV-Readers

Nachdem wir nun die Grundlagen aufgefrischt haben, wollen wir die Struktur unserer Klasse des CSV-Readers skizzieren und mit der Implementierung der wichtigsten Teile beginnen. Wir werden eine Klasse mit dem Namen CSimpleCSVReader erstellen, die Folgendes ermöglicht:

  1. Öffnen einer angegebene CSV-Datei im Text-Lese-Modus.
  2. Falls gewünscht, wird die erste Zeile als Kopfzeile behandelt, die Spaltennamen werden gespeichert und eine Zuordnung von Spaltennamen zu Indizes erstellt.
  3. Alle nachfolgenden Zeilen in den Speicher einlesen, wobei jede Zeile in ein Array von Strings aufgeteilt ist (einer pro Spalte).
  4. Bereitstellung von Methoden zur Abfrage von Daten nach Spaltenindex oder -name.
  5. Geben Sie Standard- oder Fehlerwerte zurück, wenn etwas fehlt.

Wir werden dies Schritt für Schritt tun. Betrachten wir zunächst die Datenstrukturen, die wir intern verwenden werden:

  • Eine CHashMap<string,uint> zum Speichern der Spaltennamen -> Indexzuordnung, wenn Header vorhanden sind.
  • Ein dynamisches Array von CArrayString* für Zeilen, wobei jeder CArrayString eine Zeile von Feldern darstellt.
  • Einige gespeicherte Eigenschaften wie _hasHeader , _filename , _separator , und vielleicht _rowCount und _colCount.

Die Verwendung von CArrayObj und CArrayString ist nicht nur praktisch, sondern hilft Ihnen auch, Kopfschmerzen bei der Größenänderung von Arrays auf niedriger Ebene zu vermeiden. Native Arrays sind leistungsstark, können aber bei komplexen Datensätzen unübersichtlich werden. Mit CArrayString ist das Hinzufügen von Feldern einfach, und mit CArrayObj können Sie mühelos eine wachsende Liste von Zeilen speichern. Durch eine Hash-Map für Spaltennamen wird vermieden, dass die Kopfzeile immer wieder überprüft wird. Das Design ist sowohl einfach als auch skalierbar und macht das Leben einfacher, wenn Ihre CSV-Datei wächst oder sich Ihre Datenanforderungen ändern.

Bevor wir die gesamte Klasse codieren, wollen wir einige Code-Bausteine schreiben, um zu zeigen, wie man eine Datei öffnet und Zeilen liest. Später werden wir diese Teile in den endgültigen Klassencode integrieren. Lassen Sie uns eine Datei öffnen:

int fileHandle = FileOpen("data.csv", FILE_READ|FILE_TXT);
if(fileHandle == INVALID_HANDLE)
  {
   Print("Error: Could not open file data.csv. Error code=", _LastError);
   return;
  }

// If we reach here, the file is open successfully.

Dieses Snippet versucht, data.csv aus dem Verzeichnis MQL5/Files zu öffnen. Schlägt es fehl, gibt es eine Fehlermeldung aus und kehrt zurück. Die Variable _LastError kann Aufschluss darüber geben, warum die Datei nicht geöffnet werden konnte. Zum Beispiel bedeutet 5004: CANNOT_OPEN_FILE . Lesen wir nun die Datei, bis sie zu Ende ist:

string line;
while(!FileIsEnding(fileHandle))
  {
   line = FileReadString(fileHandle);
   if(line == "" && _LastError != 0) // If empty line and error occurred
     {
      Print("Error reading line. Possibly end of file or another issue. Error=", _LastError);
      break;
     }

   // Process the line here, e.g., split it into fields
}

Hier wird eine Schleife durchlaufen, bis FileIsEnding() true zurückgibt. Bei jeder Iteration wird eine Zeile gelesen. Wenn wir eine leere Zeile erhalten und ein Fehler auftritt, hören wir auf. Wenn es wirklich das Ende der Datei ist, wird die Schleife natürlich beendet. Denken Sie daran, dass eine völlig leere Zeile in der Datei immer noch eine leere Zeichenkette ergeben würde, sodass Sie dieses Szenario je nach CSV-Format behandeln sollten.

Angenommen, unsere CSV-Datei verwendet ein Semikolon (;) als Trennzeichen. Wir können das:

string line = "Symbol;Price;Volume";
string fields[];
int fieldCount = StringSplit(line, ';', fields);

if(fieldCount < 1)
  {
   Print("No fields found in line: ", line);
  }
else
  {
   // fields now contains each piece of data
   for(int i=0; i<fieldCount; i++)
     Print("Field[", i, "] = ", fields[i]);
}

StringSplit() gibt die Anzahl der gefundenen Bestandteile zurück. Nach diesem Aufruf enthält fields jedes Token getrennt durch (;). Wenn die Zeile EURUSD;1.2345;10000 lauten würde, wäre fields[0] EURUSD, fields[1] wäre 1.2345 und fields[2] wäre 10000.

Wenn _hasHeader wahr ist, ist die erste Zeile, die wir lesen, etwas Besonderes. Wir werden sie aufteilen und die Spaltennamen in einer CHashMap speichern. Zum Beispiel:

#include <Generic\HashMap.mqh>

CHashMap<string,uint> Columns; // columnName -> columnIndex

// Assume line is the header line
string columns[];
int columnCount = StringSplit(line, ';', columns);

for(int i=0; i<columnCount; i++)
  Columns.Add(columns[i], i);

Die Hash-Map für Spaltennamen ist ein kleines Detail, das sich aber sehr auszahlt. Ohne diese Funktion müssten Sie jedes Mal eine Schleife durch die Spaltenüberschriften ziehen, wenn Sie einen Index benötigen. Bei einer Hash-Map liefert ein kurzer TryGetValue()-Aufruf den Index, und wenn eine Spalte nicht gefunden wird, können Sie einfach einen Standardwert zurückgeben. Wenn Duplikate oder seltsame Spaltennamen auftauchen, können Sie diese im Voraus erkennen. So bleiben die Suchvorgänge schnell und der Code sauber, und selbst wenn sich die Größe Ihrer CSV-Datei verdoppelt, bleibt der Abruf von Spaltenindizes einfach.

Columns ordnet nun jeden Spaltennamen seinem Index zu. Wenn wir später den Index für einen bestimmten Spaltennamen benötigen, können wir das tun:

uint idx;
bool found = Columns.TryGetValue("Volume", idx);
if(found)
  Print("Volume column index = ", idx);
else
  Print("Column 'Volume' not found");

Jede Datenzeile sollte in einem CArrayString-Objekt gespeichert werden, und wir werden ein dynamisches Array von Zeigern auf diese Zeilen behalten. Etwa so:

#include <Arrays\ArrayString.mqh>
#include <Arrays\ArrayObj.mqh>

CArrayObj Rows; // holds pointers to CArrayString objects

// After reading and splitting a line into fields:
// (Assume fields[] array is populated)

CArrayString *row = new CArrayString;
for(int i=0; i<ArraySize(fields); i++)
  row.Add(fields[i]);

Rows.Add(row);

Später, um einen Wert abzurufen, würden wir etwas tun wie:

CArrayString *aRow = Rows.At(0); // get the first row
string value = aRow.At(1);       // get second column
Print("Value at row=0, col=1: ", value);

Natürlich müssen wir immer die Grenzen überprüfen, um Fehler zu vermeiden, die außerhalb des Bereichs liegen.

Wenn unsere CSV-Datei eine Kopfzeile hat, können wir die Spalten-Zuordnung verwenden, um die Spaltenindizes nach Namen zu suchen:

string GetValueByName(uint rowNumber, string colName, string errorValue="")
  {
   uint idx;
   if(!Columns.TryGetValue(colName, idx))
     return errorValue; // column not found

   return GetValueByIndex(rowNumber, idx, errorValue);
  }

string GetValueByIndex(uint rowNumber, uint colIndex, string errorValue="")
  {
   if(rowNumber >= Rows.Total())
     return errorValue; // invalid row
   CArrayString *aRow = Rows.At(rowNumber);
   if(colIndex >= (uint)aRow.Total())
     return errorValue; // invalid column index

   return aRow.At(colIndex);
  }

Dieser Pseudocode zeigt, wie wir zwei Zugriffsfunktionen implementieren könnten. GetValueByName verwendet die Hash-Map, um den Spaltennamen in einen Index umzuwandeln, und ruft dann GetValueByIndex auf. GetValueByIndex prüft Grenzen und gibt je nach Bedarf Werte oder Fehlervorgaben zurück.

Konstruktor und Destruktor: Wir können alles in eine Klasse packen. Der Konstruktor initialisiert möglicherweise nur interne Variablen, und der Destruktor sollte Speicher freigeben. Zum Beispiel:

class CSimpleCSVReader
  {
private:
   bool              _hasHeader;
   string            _separator;
   CHashMap<string,uint> Columns;
   CArrayObj         Rows;

public:
                    CSimpleCSVReader() { _hasHeader = true; _separator=";"; }
                   ~CSimpleCSVReader() { Clear(); }

   void             SetHasHeader(bool hasHeader) { _hasHeader = hasHeader; }
   void             SetSeparator(string sep) { _separator = sep; }

   uint             Load(string filename);
   string           GetValueByName(uint rowNum, string colName, string errorVal="");
   string           GetValueByIndex(uint rowNum, uint colIndex, string errorVal="");

private:
   void             Clear()
                     {
                      for(int i=0; i<Rows.Total(); i++)
                        {
                         CArrayString *row = Rows.At(i);
                         if(row != NULL) delete row;
                        }
                      Rows.Clear();
                      Columns.Clear();
                     }
  };

Diese Skizze einer Klasse zeigt eine mögliche Struktur. Wir haben Load() noch nicht implementiert, aber wir werden es bald tun. Beachten Sie, dass wir eine Methode Clear() beibehalten, um Speicher freizugeben. Nach dem Aufruf von „delete row;“ müssen wir auch „Rows.Clear()“ aufrufen, um das Array von Zeigern zurückzusetzen.

Implementieren wir nun die Methode Load(). Load() öffnet die Datei, liest die erste Zeile (möglicherweise den Header), liest alle übrigen Zeilen und analysiert sie:

uint CSimpleCSVReader::Load(string filename)
  {
   // Clear any previous data
   Clear();

   int fileHandle = FileOpen(filename, FILE_READ|FILE_TXT);
   if(fileHandle == INVALID_HANDLE)
     {
      Print("Error opening file: ", filename, " err=", _LastError);
      return 0;
     }

   if(_hasHeader)
     {
      // read first line as header
      if(!FileIsEnding(fileHandle))
        {
         string headerLine = FileReadString(fileHandle);
         string headerFields[];
         int colCount = StringSplit(headerLine, StringGetCharacter(_separator,0), headerFields);
         for(int i=0; i<colCount; i++)
           Columns.Add(headerFields[i], i);
        }
     }

   uint rowCount=0;
   while(!FileIsEnding(fileHandle))
     {
      string line = FileReadString(fileHandle);
      if(line == "") continue; // skip empty lines

      string fields[];
      int fieldCount = StringSplit(line, StringGetCharacter(_separator,0), fields);
      if(fieldCount<1) continue; // no data?

      CArrayString *row = new CArrayString;
      for(int i=0; i<fieldCount; i++)
        row.Add(fields[i]);
      Rows.Add(row);
      rowCount++;
     }

   FileClose(fileHandle);
   return rowCount;
  }

Diese Load()-Funktion:

  • Löscht alte Daten.
  • Öffnet die Datei.
  • Wenn _hasHeader true ist, wird die erste Zeile als Kopfzeile gelesen und die Spalten ausgefüllt.
  • Dann liest sie die Zeilen bis zum Ende der Datei und teilt sie in Felder auf.
  • Sie erstellt für jede Zeile einen CArrayString, füllt ihn auf und fügt ihn zu den Zeilen, „Rows“, hinzu.
  • Sie gibt die Anzahl der gelesenen Datenzeilen zurück.

Um das Ganze zusammenzufassen, haben wir nun einen großen Teil der Logik skizziert. In den nächsten Abschnitten werden wir den Code verfeinern und finalisieren, die fehlenden Zugriffsmethoden hinzufügen und das endgültige vollständige Code-Listing zeigen. Wir werden auch Anwendungsbeispiele demonstrieren, z. B. wie man überprüft, wie viele Zeilen man hat, welche Spalten vorhanden sind und wie man Werte sicher abruft.

Wenn Sie diese Codeschnipsel durchgehen, können Sie sehen, wie die logischen Teile zusammenpassen. Der endgültige CSV-Reader wird in sich geschlossen und leicht zu integrieren sein: Erstellen Sie einfach eine Instanz, rufen Sie Load(“myfile.csv“) auf und verwenden Sie dann GetValueByName() oder GetValueByIndex(), um die benötigten Informationen abzurufen.

Im nächsten Abschnitt werden wir die gesamte Klassenimplementierung vervollständigen und einen letzten Codeschnipsel zeigen, den Sie kopieren und anpassen können. Danach werden wir einige Anwendungsbeispiele und abschließende Bemerkungen anführen.


Fertigstellung der Implementierung der Klasse des CSV-Readers

In den vorangegangenen Abschnitten haben wir die Struktur unseres CSV-Readers skizziert und verschiedene Teile des Codes durchgearbeitet. Jetzt ist es an der Zeit, alles in einer einzigen, zusammenhängenden Implementierung zusammenzufassen. Danach zeigen wir Ihnen kurz, wie Sie es verwenden können. In der endgültigen Artikelstruktur werden wir den gesamten Code hier auf einmal präsentieren, damit Sie einen klaren Bezug haben.

Wir werden die besprochenen Hilfsfunktionen – Laden von Dateien, Aufteilen der Kopfzeilen, Speichern von Zeilen und Zugriffsmethoden – in eine einzige MQL5-Klasse integrieren. Dann zeigen wir einen kurzen Abschnitt, der demonstriert, wie die Klasse in einem EA oder Skript verwendet werden kann. Erinnern Sie sich, dass diese Klasse:

  • Liest eine CSV-Datei aus dem Verzeichnis MQL5/Files.
  • Wenn _hasHeader true ist, werden die Spaltennamen aus der ersten Zeile extrahiert.
  • Nachfolgende Zeilen bilden Zeilen von Daten, die in CArrayString gespeichert werden.
  • Sie können Werte nach Spaltennamen (wenn eine Überschrift vorhanden ist) oder nach Spaltenindex abrufen.

Wir werden auch einige Fehlerprüfungen und Standardeinstellungen einbauen. Lassen Sie uns jetzt den vollständigen Code präsentieren. Bitte beachten Sie, dass es sich bei diesem Code um ein anschauliches Beispiel handelt, das je nach Ihrer Umgebung geringfügige Anpassungen erfordert. Wir gehen davon aus, dass die Dateien HashMap.mqh, ArrayString.mqh und ArrayObj.mqh in den Standard-MQL5-Include-Verzeichnissen verfügbar sind.

Hier ist das vollständige Code-Listing des CSV-Readers:

//+------------------------------------------------------------------+
//|  CSimpleCSVReader.mqh                                            |
//|  A simple CSV reader class in MQL5.                              |
//|  Assumes CSV file is located in MQL5/Files.                      |
//|  By default, uses ';' as the separator and treats first line as  |
//|  header. If no header, columns are accessed by index only.       |
//+------------------------------------------------------------------+
#include <Generic\HashMap.mqh>
#include <Arrays\ArrayObj.mqh>
#include <Arrays\ArrayString.mqh>

class CSimpleCSVReader
  {
private:
   bool                  _hasHeader;
   string                _separator;
   CHashMap<string,uint> Columns;
   CArrayObj             Rows;          // Array of CArrayString*, each representing a data row

public:
                        CSimpleCSVReader()
                          {
                           _hasHeader = true;
                           _separator = ";";
                          }
                       ~CSimpleCSVReader()
                          {
                           Clear();
                          }

   void                 SetHasHeader(bool hasHeader) {_hasHeader = hasHeader;}
   void                 SetSeparator(string sep) {_separator = sep;}

   // Load: Reads the file, returns number of data rows.
   uint                 Load(string filename);

   // GetValue by name or index: returns specified cell value or errorVal if not found
   string               GetValueByName(uint rowNum, string colName, string errorVal="");
   string               GetValueByIndex(uint rowNum, uint colIndex, string errorVal="");

   // Returns the number of data rows (excluding header)
   uint                 RowCount() {return Rows.Total();}

   // Returns the number of columns. If no header, returns column count of first data row
   uint                 ColumnCount()
                         {
                          if(Columns.Count() > 0)
                            return Columns.Count();
                          // If no header, guess column count from first row if available
                          if(Rows.Total()>0)
                            {
                             CArrayString *r = Rows.At(0);
                             return (uint)r.Total();
                            }
                          return 0;
                         }

   // Get column name by index if header exists, otherwise return empty or errorVal
   string               GetColumnName(uint colIndex, string errorVal="")
                         {
                          if(Columns.Count()==0)
                            return errorVal;
                          // Extract keys and values from Columns
                          string keys[];
                          int vals[];
                          Columns.CopyTo(keys, vals);
                          if(colIndex < (uint)ArraySize(keys))
                            return keys[colIndex];
                          return errorVal;
                         }

private:
   void                 Clear()
                         {
                          for(int i=0; i<Rows.Total(); i++)
                            {
                             CArrayString *row = Rows.At(i);
                             if(row != NULL) delete row;
                            }
                          Rows.Clear();
                          Columns.Clear();
                         }
  };

//+------------------------------------------------------------------+
//| Implementation of Load() method                                  |
//+------------------------------------------------------------------+
uint CSimpleCSVReader::Load(string filename)
  {
   Clear(); // Start fresh

   int fileHandle = FileOpen(filename, FILE_READ|FILE_TXT);
   if(fileHandle == INVALID_HANDLE)
     {
      Print("CSVReader: Error opening file: ", filename, " err=", _LastError);
      return 0;
     }

   uint rowCount=0;

   // If hasHeader, read first line as header
   if(_hasHeader && !FileIsEnding(fileHandle))
     {
      string headerLine = FileReadString(fileHandle);
      if(headerLine != "")
        {
         string headerFields[];
         int colCount = StringSplit(headerLine, StringGetCharacter(_separator,0), headerFields);
         for(int i=0; i<colCount; i++)
           Columns.Add(headerFields[i], i);
        }
     }

   while(!FileIsEnding(fileHandle))
     {
      string line = FileReadString(fileHandle);
      if(line == "") continue; // skip empty lines

      string fields[];
      int fieldCount = StringSplit(line, StringGetCharacter(_separator,0), fields);
      if(fieldCount<1) continue; // no data?

      CArrayString *row = new CArrayString;
      for(int i=0; i<fieldCount; i++)
        row.Add(fields[i]);
      Rows.Add(row);
      rowCount++;
     }

   FileClose(fileHandle);
   return rowCount;
  }

//+------------------------------------------------------------------+
//| GetValueByIndex Method                                           |
//+------------------------------------------------------------------+
string CSimpleCSVReader::GetValueByIndex(uint rowNum, uint colIndex, string errorVal="")
  {
   if(rowNum >= Rows.Total())
     return errorVal;
   CArrayString *aRow = Rows.At(rowNum);
   if(aRow == NULL) return errorVal;
   if(colIndex >= (uint)aRow.Total())
     return errorVal;
   string val = aRow.At(colIndex);
   return val;
  }

//+------------------------------------------------------------------+
//| GetValueByName Method                                            |
//+------------------------------------------------------------------+
string CSimpleCSVReader::GetValueByName(uint rowNum, string colName, string errorVal="")
  {
   if(Columns.Count() == 0)
     {
      // No header, can't lookup by name
      return errorVal;
     }

   uint idx;
   bool found = Columns.TryGetValue(colName, idx);
   if(!found) return errorVal;

   return GetValueByIndex(rowNum, idx, errorVal);
  }

//+------------------------------------------------------------------+

Werfen wir einen genaueren Blick auf Load() . Es löscht alte Daten, versucht die Datei zu öffnen und liest, wenn _hasHeader true ist, eine Zeile als Header. Anschließend werden die Spaltennamen aufgeteilt und gespeichert. Danach wird die Datei Zeile für Zeile durchlaufen, wobei leere Zeilen ignoriert und gültige Zeilen in Felder aufgeteilt werden. Jeder Satz von Feldern wird zu einer CArrayString-Zeile in Rows. Am Ende wissen Sie genau, wie viele Zeilen Sie haben, und „Columns“ steht bereit als ein namensbasiertes Nachschlagewerk. Dieser unkomplizierte Ablauf bedeutet, dass sich Ihr EA leicht anpassen kann, wenn die zukünftige CSV-Datei mehr Zeilen oder eine leicht abweichende Formatierung aufweist.

Was die Methoden GetValueByName() und GetValueByIndex() betrifft, so sind diese Zugriffsmethoden Ihre Hauptschnittstelle zu den Daten. Sie sind sicher, weil sie immer die Grenzen prüfen. Wenn Sie eine Zeile oder Spalte anfordern, die nicht existiert, erhalten Sie eine harmlose Voreinstellung und keinen Absturz. Wenn keine Kopfzeile vorhanden ist, gibt GetValueByName() einen Fehlerwert zurück. Selbst wenn in Ihrer CSV-Datei etwas fehlt oder _hasHeader falsch eingestellt ist, wird Ihr EA auf diese Weise nicht zusammenbrechen. Sie können eine kurze Print()-Anweisung hinzufügen, wenn Sie diese Unstimmigkeiten protokollieren möchten, aber das ist optional. Das Wichtigste ist, dass diese Methoden Ihren Arbeitsablauf reibungslos und fehlerfrei gestalten.

Wenn params.csv wie folgt aussieht:

Symbol;MaxLot;MinSpread
EURUSD;0.20;1
GBPUSD;0.10;2

Ausgabe:

Loaded 2 data rows.
First Row: Symbol=EURUSD MaxLot=0.20 MinSpread=1

Und wenn Sie über den Index statt über den Namen zugreifen wollen:

// Access second row, second column (MaxLot) by index:
string val = csv.GetValueByIndex(1, 1, "N/A");
Print("Second row, second column:", val);

Dies sollte 0,10 ausgeben, was dem MaxLot von GBPUSD entspricht.

Was ist, wenn keine Kopfzeile vorhanden ist? Wenn _hasHeader auf false gesetzt ist, wird die Erstellung der Columns-Map übersprungen. Dann müssen Sie sich auf GetValueByIndex() verlassen, um auf Daten zuzugreifen. Wenn Ihre CSV-Datei zum Beispiel keine Kopfzeilen hat und jede Zeile drei Felder enthält, wissen Sie das:

  • Spalte 0: Symbol
  • Spalte 1: Preis
  • Spalte 2: Kommentar

Sie können csv.GetValueByIndex(rowNum, 0) direkt aufrufen, um das Symbol zu erhalten.

Wie sieht es mit der Fehlerbehandlung aus? Unser Code gibt Standardwerte zurück, wenn etwas fehlt, z. B. eine nicht existierende Spalte oder Zeile. Es gibt auch Fehler aus, wenn die Datei nicht geöffnet werden kann. In der Praxis werden Sie vielleicht eine robustere Protokollierung wünschen. Wenn Sie z. B. stark auf externe Daten angewiesen sind, sollten Sie prüfen, ob rows = csv.Load(“file.csv“) ist, und wenn rows == 0 ist, dies entsprechend behandeln. Vielleicht brechen Sie Ihre EA-Initialisierung ab oder kehren zu den internen Standardeinstellungen zurück.

Wir haben keine extreme Fehlerbehandlung für missgebildete CSV-Dateien oder ungewöhnliche Kodierungen implementiert. Für komplexere Szenarien fügen Sie Prüfungen hinzu. Wenn ColumnCount() gleich Null ist, kann eine Warnung ausgegeben werden. Wenn eine benötigte Spalte nicht vorhanden ist, drucken Sie eine Meldung auf der Registerkarte Experten.

Werfen wir einen Blick auf die Leistung: Für kleine bis mittelgroße CSV-Dateien ist dieser Ansatz völlig in Ordnung. Wenn Sie extrem große Dateien verarbeiten müssen, sollten Sie effizientere Datenstrukturen oder einen Streaming-Ansatz in Betracht ziehen. Für eine typische EA-Nutzung – wie das Lesen von einigen hundert oder tausend Zeilen – reicht dies jedoch aus.

Wir haben jetzt einen vollständigen CSV-Reader. Im nächsten (und letzten) Abschnitt werden wir kurz auf das Testen eingehen, einige Anwendungsszenarien vorstellen und mit abschließenden Bemerkungen schließen. Sie erhalten einen gebrauchsfertige CSV-Reader, die sich nahtlos in Ihre MQL5 EAs oder Skripte integrieren lässt.


Tests und Nutzungsszenarien

Nachdem die Implementierung des CSV-Readers abgeschlossen ist, sollten Sie überprüfen, ob alles wie vorgesehen funktioniert. Testen ist ganz einfach: Erstellen Sie eine kleine CSV-Datei, legen Sie sie in MQL5/Files ab und schreiben Sie einen EA, der sie lädt und einige Ergebnisse ausgibt. Sie können dann auf der Registerkarte Experten überprüfen, ob die Werte korrekt sind. Hier sind einige Testvorschläge:

  1. Basistest mit Kopfzeile: Erstellen eine test.csv mit:

    Symbol;Spread;Comment
    EURUSD;1;Major Pair
    USDJPY;2;Another Major

    Laden durch:

    CSimpleCSVReader csv;
    csv.SetHasHeader(true);
    csv.SetSeparator(";");
    uint rows = csv.Load("test.csv");
    Print("Rows loaded: ", rows);
    Print("EURUSD Spread: ", csv.GetValueByName(0, "Spread", "N/A"));
    Print("USDJPY Comment: ", csv.GetValueByName(1, "Comment", "N/A"));
    

    Prüfen der Ausgabe. Wenn es „Rows loaded: 2“, „EURUSD-Spread: 1“ und „USDJPY Comment: Another Major“, dann funktioniert alles.

    Was ist, wenn die CSV-Datei nicht vollkommen einheitlich ist? Angenommen, eine Zeile hat weniger Spalten als erwartet. Unser Ansatz zwingt nicht zur Konsistenz. Wenn in einer Zeile ein Feld fehlt, wird bei der Abfrage dieser Spalte ein Standardwert zurückgegeben. Dies ist gut, wenn Sie mit unvollständigen Daten umgehen können, aber wenn Sie eine strenge Formatierung benötigen, sollten Sie die Anzahl der Spalten nach Load() überprüfen. Bei großen Dateien funktioniert diese Methode immer noch gut, aber wenn Sie Zehntausende von Zeilen bearbeiten, sollten Sie über Leistungsoptimierungen oder partielles Laden nachdenken. Für den alltäglichen Bedarf – kleine bis mittlere CSVs – ist diese Einrichtung mehr als ausreichend.

  2. Test ohne Kopfzeile: Wenn Sie csv.SetHasHeader(false); einstellen und eine Datei ohne Kopfzeile verwenden:

    EURUSD;1;Major Pair
    USDJPY;2;Another Major
    
    Jetzt müssen Sie auf die Spalten über den Index zugreifen:
    string val = csv.GetValueByIndex(0, 0, "N/A"); // should be EURUSD
    Print("Row0 Col0: ", val);
    
    Bestätigen Sie, dass die Ausgabe Ihren Erwartungen entspricht.

  3. Fehlende Spalten oder Zeilen: Versuchen Sie, einen Spaltennamen anzufordern, der nicht existiert, oder eine Zeile außerhalb der geladenen Daten. Sie sollten die von Ihnen angegebenen Standardfehlerwerte erhalten. Zum Beispiel:
    string nonExistent = csv.GetValueByName(0, "NonExistentColumn", "MISSING");
    Print("NonExistent: ", nonExistent);
    
    Dies sollte MISSING ausdrucken und nicht abstürzen.

  4. Größere Dateien: Wenn Sie eine Datei mit mehr Zeilen haben, laden Sie diese und bestätigen Sie die Zeilenzahl. Prüfen Sie, ob der Speicherverbrauch und die Leistung angemessen bleiben. Dieser Schritt hilft sicherzustellen, dass der Ansatz für Ihr Szenario robust genug ist. 

Berücksichtigen Sie auch Zeichenkodierungen und ungewöhnliche Symbole. Die meisten CSV-Dateien sind einfache ASCII- oder UTF-8-Dateien, mit denen MQL5 gut zurechtkommt. Wenn Sie jemals seltsame Zeichen erhalten, kann es helfen, die Datei zunächst in eine freundlichere Kodierung zu konvertieren. Ähnlich verhält es sich, wenn Ihre CSV-Datei nachstehende Leerzeichen oder ungerade Interpunktion enthält: Das Trimmen von Feldern nach der Aufteilung sorgt für saubere Daten. Das Testen dieser „weniger schönen“ Szenarien stellt nun sicher, dass Ihr EA nicht an einem leicht abweichenden Dateiformat oder einer unerwarteten Glyphe scheitert, wenn er in der Praxis läuft.

Verwendungsszenarien:

  • Externe Parameter:
    Angenommen, Sie haben eine CSV-Datei mit Strategieparametern. Jede Zeile kann ein Symbol und einige Schwellenwerte enthalten. Anstatt diese Werte in Ihrem EA fest zu kodieren, können Sie sie beim Start laden, über die Zeilen iterieren und sie dynamisch anwenden. Das Ändern von Parametern ist so einfach wie das Bearbeiten der CSV-Datei, eine Neukompilierung ist nicht erforderlich.

  • Verwaltung der Beobachtungsliste:
    Sie können eine Liste der zu handelnden Symbole in einer CSV-Datei speichern. Ihr EA kann diese Liste zur Laufzeit lesen, sodass Sie schnell Instrumente hinzufügen oder entfernen können, ohne den Code zu verändern. Eine CSV könnte zum Beispiel so aussehen:

    Symbol
    EURUSD
    GBPUSD
    XAUUSD
    
    Das Lesen dieser Datei und die Iteration über die Zeilen in Ihrem EA ermöglicht es Ihnen, die gehandelten Symbole im laufenden Betrieb anzupassen.

  • Integration mit anderen Tools: Wenn Sie ein Python-Skript oder ein anderes Tool haben, das CSV-Analysen erzeugt – wie z. B. nutzerdefinierte Signale oder Prognosen – können Sie die Daten in CSV exportieren und Ihren EA sie in MQL5 importieren lassen. Dies überbrückt die Kluft zwischen verschiedenen Programmierökosystemen.

Schlussfolgerung

Wir haben nun die Grundlagen der MQL5-Datei-Operationen erforscht und gelernt, wie man Textdateien sicher Zeile für Zeile liest, CSV-Zeilen in Felder zerlegt und sie für den einfachen Abruf über Spaltennamen oder Indizes speichert. Mit der Präsentation des vollständigen Codes für einen einfachen CSV-Reader haben wir einen Baustein bereitgestellt, der Ihre automatisierten Handelsstrategien verbessern kann.

Dieser CSV-Reader ist nicht nur ein Codeschnipsel, sondern ein praktisches Dienstprogramm, das Sie an Ihre Bedürfnisse anpassen können. Sie benötigen ein anderes Trennzeichen? Ändern Sie _separator. Keine Kopfzeile in Ihrer Datei? Setzen Sie _hasHeader auf false und verlassen Sie sich auf Indizes. Der Ansatz ist flexibel und transparent und ermöglicht Ihnen eine saubere Integration externer Daten. Wenn Sie komplexere Handelsideen entwickeln, können Sie diesen CSV-Reader weiter ausbauen, indem Sie eine robustere Fehlerbehandlung hinzufügen, verschiedene Kodierungen unterstützen oder sogar in CSV-Dateien zurückschreiben. Für den Moment sollte diese Grundlage die meisten grundlegenden Szenarien abdecken.

Denken Sie daran, dass zuverlässige Daten der Schlüssel zum Aufbau einer soliden Handelslogik sind. Mit der Möglichkeit, externe Daten aus CSV-Dateien zu importieren, können Sie ein breiteres Spektrum an Markteinblicken, Konfigurationen und Parametersätzen nutzen, die alle dynamisch durch einfache Textdateien statt durch fest kodierte Werte gesteuert werden, und wenn Ihre Anforderungen komplexer werden – wie z. B. die Handhabung mehrerer Trennzeichen, das Ignorieren bestimmter Zeilen oder die Unterstützung von Feldern mit Anführungszeichen – müssen Sie nur den Code anpassen. Das ist das Schöne an einem eigenen CSV-Reader: Es ist eine solide Grundlage, die Sie mit der Entwicklung Ihrer Strategie und Ihrer Datenquellen verfeinern können. Mit der Zeit können Sie sogar ein kleines Daten-Toolkit aufbauen, mit dem Sie Ihren EA jederzeit mit neuen Erkenntnissen füttern können, ohne die Kernlogik von Grund auf neu schreiben zu müssen.

Viel Spaß beim Codieren und beim Handeln!

Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/16614

Beigefügte Dateien |
Automatisieren von Handelsstrategien in MQL5 (Teil 2): Das Breakout System Kumo mit Ichimoku und dem Awesome Oscillator Automatisieren von Handelsstrategien in MQL5 (Teil 2): Das Breakout System Kumo mit Ichimoku und dem Awesome Oscillator
In diesem Artikel erstellen wir einen Expert Advisor (EA), der die Kumo Breakout-Strategie unter Verwendung des Indikators Ichimoku Kinko Hyo und des Awesome Oscillators automatisiert. Wir gehen durch den Prozess der Initialisierung von Indikator-Handles, der Erkennung von Ausbruchsbedingungen und der Codierung von automatischen Handelsein- und -ausgängen. Zusätzlich implementieren wir Trailing-Stops und die Positionsmanagement-Logik, um die Leistung des EA und seine Anpassungsfähigkeit an die Marktbedingungen zu verbessern.
Entwicklung eines Toolkit zur Analyse von Preisaktionen (Teil 4): Der Analytik Forecaster EA Entwicklung eines Toolkit zur Analyse von Preisaktionen (Teil 4): Der Analytik Forecaster EA
Wir gehen über die einfache Darstellung von analysierten Metriken in Charts hinaus und bieten eine breitere Perspektive, die auch die Integration von Telegram umfasst. Mit dieser Erweiterung können wichtige Ergebnisse über die Telegram-App direkt auf Ihr mobiles Gerät geliefert werden. Begleiten Sie uns in diesem Artikel auf dieser gemeinsamen Reise.
Ensemble-Methoden zur Verbesserung numerischer Vorhersagen in MQL5 Ensemble-Methoden zur Verbesserung numerischer Vorhersagen in MQL5
In diesem Artikel stellen wir die Implementierung mehrerer Ensemble-Lernmethoden in MQL5 vor und untersuchen ihre Wirksamkeit in verschiedenen Szenarien.
Handelseinblicke über das Volumen: Trendbestätigung Handelseinblicke über das Volumen: Trendbestätigung
Die Enhanced Trend Confirmation Technique kombiniert Preisaktionen, Volumenanalysen und maschinelles Lernen, um echte Marktbewegungen zu identifizieren. Für die Handelsvalidierung sind sowohl Preisausbrüche als auch Volumensprünge (50 % über dem Durchschnitt) erforderlich, während ein neuronales LSTM-Netzwerk für zusätzliche Bestätigung sorgt. Das System verwendet eine ATR-basierte Positionsgröße und ein dynamisches Risikomanagement, wodurch es an verschiedene Marktbedingungen angepasst werden kann und gleichzeitig falsche Signale herausfiltert.