English Deutsch
preview
MQL5でのファイル操作の習得:基本的なI/OからカスタムCSVリーダーの構築まで

MQL5でのファイル操作の習得:基本的なI/OからカスタムCSVリーダーの構築まで

MetaTrader 5インディケータ | 16 4月 2025, 14:24
50 0
Sahil Bagdi
Sahil Bagdi

はじめに

今日の自動取引の世界では、データがすべてです。戦略に必要なカスタムパラメータを読み込んだり、銘柄のウォッチリストを扱ったり、外部ソースからの過去データを統合したりする必要があるかもしれません。MetaTrader 5を使っているなら、MQL5でコードから直接ファイルを操作するのは比較的簡単です。

とはいえ、正直なところ、ドキュメントを読みながらファイル操作を学ぶのは最初は少しハードルが高く感じるかもしれません。そこで本記事では、ファイル操作の基本を、わかりやすく段階的に説明していきます。まずは、MQL5の「サンドボックス」機能がファイルをどのように保護しているか、テキストモードやバイナリモードでのファイルの開き方、そして安全に行を読み取り、分割する方法などを解説します。その後、それらの知識を活かして、シンプルなCSVリーダークラスを構築していきます。

CSVファイルを使うのは、CSVが非常に一般的で、シンプルかつ人間にも読みやすく、数多くのツールでサポートされているからです。CSVリーダーを使えば、外部パラメータや銘柄リスト、その他のカスタムデータをEAやスクリプトに直接取り込み、コードを毎回変更することなく戦略の挙動を調整することができます。

すべてのMQL5ファイル関数を細かく網羅するわけではありませんが、実際に必要となる重要な部分はしっかりカバーします。最終的には、CSVファイルをテキストモードで開く方法から、ファイルの末尾まで行を読み取る方法、各行を指定した区切り文字で分割する方法、フィールドを列名やインデックスで保存・取得する方法までを、わかりやすい例を通じて理解できるようになります。

次が、この記事の計画です。

  1. MQL5ファイル操作の基礎
  2. CSVリーダークラスの設計
  3. CSVリーダークラスの実装の仕上げ
  4. テストと使用シナリオ
  5. 結論


MQL5ファイル操作の基礎

CSVリーダーを実装する前に、MQL5のいくつかのコアファイル処理概念を詳しく見て、コードで説明してみましょう。サンドボックスの制限、ファイルのオープンモード、行ごとの読み取り、基本的なエラー処理の理解に重点を置きます。これらの基礎を実際に確認することで、後にCSVリーダーを構築・デバッグしやすくなります。

まず、サンドボックスによる制限付きファイルアクセスについて理解しましょう。MQL5では、「サンドボックス」と呼ばれる特定のディレクトリ内にファイル操作が制限されるセキュリティモデルが採用されています。通常は、<TerminalDataFolder>/MQL5/Filesにあるファイルのみが読み書き可能です。このディレクトリの外にあるファイルへアクセスしようとすると、FileOpen()関数は失敗します。

たとえば、MT5ターミナルの MQL5/Filesフォルダにdata.csvという名前のファイルを配置した場合、次のようにして開くことができます。

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.

これらのエラーコードが何を意味するのか疑問に思うかもしれません。たとえば、「_LastError = 5004」は、通常「ファイルが見つかりません」や「ファイルを開けません」といったエラーを示しており、たいていはファイル名の入力ミスや、ファイルが MQL5/Filesフォルダ内に存在しないことが原因です。別のエラーコードが表示された場合は、MQL5のドキュメントやコミュニティフォーラムで調べれば、メッセージの意味をすぐに確認できます。単なるパスの問題だったり、別のプログラムがファイルをロックしている場合もあります。外部データがEAにとって重要な要素である場合は、すばやく問題を特定・解決できるように、簡易的な再試行処理や、より詳細なエラーメッセージの出力を検討するとよいでしょう。

ファイルを開く際には、さまざまなオプションがあります。FileOpen()を呼び出すときに、ファイルへのアクセス方法を制御するフラグを指定します。代表的なフラグには次のようなものがあります。

  • FILE_READ:ファイルを読み取り専用で開く
  • FILE_WRITE:ファイルを書き込み用で開く
  • FILE_BIN:バイナリモード(テキスト処理なし)
  • FILE_TXT:テキストモード(行末や文字コードの変換を処理)
  • FILE_CSV:ファイルをCSVとして扱う特殊なテキストモード

標準のCSVを読み取る場合、FILE_READ|FILE_TXTが出発点として最適です。テキストモードでは、FileReadString()が改行で区切られた単位で読み取りを停止してくれるため、ファイルを行ごとに処理するのが簡単になります。

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");
  }

ファイルをテキストモードで開くと、行の読み取りは簡単になります。次の改行まで読み取るには、FileReadString()を使用します。ファイルが終了すると、FileIsEnding()はtrueを返します。次のループを考えてみましょう。

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

このスニペットでは、ファイルの終端に達するまで、行を繰り返し読み取ります。エラーが発生した場合は処理を停止します。空行は許容されているため、スキップしたい場合は「if(line == "") continue;」のようにチェックすれば十分です。この方法は、CSVの各行を処理する際に非常に役立ちます。

テキストファイルは、必ずしも一貫した形式になっているとは限らない点に注意してください。ほとんどのファイルでは行末に\nや\r\nが使われており、MQL5は通常それらを正しく処理します。ただし、特殊なソースから取得したファイルでは、行が正しく読み取れているかを確認する価値があります。FileReadString()が不自然な結果(行が結合されているなど)を返す場合は、そのファイルをテキストエディタで開いて、文字エンコードや改行スタイルを確認してください。また、非常に長い行にも注意が必要です。小規模なCSVではまれですが、可能性はあります。行の長さチェックやトリミング処理を加えることで、EAが予期しないフォーマットで躓くのを防げます。

CSVデータを処理するには、通常カンマやセミコロンなどの区切り文字に基づいて、各行をフィールドに分割します。MQL5のStringSplit()関数がその際に役立ちます。

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);
}

このコードは、解析された各フィールドを出力します。CSVを読み取る際には、分割したフィールドをメモリに保持し、後から列インデックスまたは列名でアクセスできるようにします。

StringSplit()は単純な区切り文字に対しては非常に有効ですが、CSV形式は一筋縄ではいかないこともあります。引用符で囲まれたフィールドや、エスケープされた区切り文字を含む場合もあり、ここではそれらの処理はおこなっていません。もしファイルがシンプルで、引用符や特別な記述がないのであれば、StringSplit()で十分です。フィールドの末尾に余分なスペースや不自然な句読点が含まれている場合には、分割後にStringTrim()を使用することを検討してください。こうしたちょっとしたチェックが、データソースに微細なフォーマットの癖があっても、EAの安定性を保つうえで役立ちます。

多くのCSVファイルには、列名を定義するヘッダー行が含まれています。今後作成するCSVリーダーにおいて_hasHeaderがtrueに設定されている場合、最初に読み込まれた行は分割され、列名をインデックスにマッピングするハッシュマップに格納されます。

以下は例です。

// 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");
ヘッダーが存在しない場合は、数値インデックスにのみ依存します。最初に読み取られる行はデータ行として扱われ、列はその位置で参照されます。


列名用のハッシュマップ(CHashMap)は、一見些細なことのようでいて、大きな違いを生む便利な仕組みです。これがないと、必要な列インデックスを取得するたびに、ヘッダーの各フィールドを毎回ループして探さなければなりません。ハッシュマップを使えば、TryGetValue() によって即座にインデックスを取得できます。列が見つからない場合は、エラー値を返すようにすれば、シンプルでエレガントに対応できます。また、同じ列名が複数回出現する可能性が心配な場合は、ヘッダーの読み取り時に簡単な重複チェックを加えて、発見時に警告を出力することもできます。こういった小さな工夫が、CSVが徐々に複雑化してもコードの堅牢性を保つのに役立ちます。

データの保存については、構造はシンプルに保ちます。分割された各行は、そのまま1行のデータとなります。1行のフィールド群にはCArrayStringを使用し、複数行をまとめて格納するためにCArrayObjを利用します。

#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);

後で値を取得するには、次のようにします。

// 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);

アクセスする前に、インデックスが有効であることを必ず確認する必要があります。 

ファイルや列が存在しない可能性を常に念頭に置いて処理をおこないます。たとえば、FileOpen() が INVALID_HANDLEを返した場合は、エラーメッセージをログに記録して処理を中断します。要求された列名が存在しない場合には、デフォルト値を返します。最終的に作成するCSVリーダークラスでは、こうしたチェックをクラス内部にカプセル化し、メインのEAコードをすっきりと保てるようにします。

これまでに扱ってきた、サンドボックスのルール、ファイルのオープン、行の読み取り、フィールドの分割、そして結果の保存といった基本的な要素がすべて揃いました。次のセクションでは、これらの概念をもとにCSVリーダークラスを段階的に設計・実装していきます。今の段階で明快さとエラー処理にしっかり取り組んでおくことで、後の実装はよりスムーズかつ信頼性の高いものになるはずです。


CSVリーダークラスの設計

基礎を復習したところで、CSVリーダークラスの構造を概観し、主要な部分の実装に取りかかりましょう。CSimpleCSVReaderのような名前のクラスを作成し、以下の機能を持たせます。

  1. 指定されたCSVファイルをテキスト読み取りモードで開く
  2. 要求があれば、最初の行をヘッダーとして扱い、列名を保存し、列名からインデックスへのマップを構築する
  3. 続くすべての行をメモリに読み込み、各行を文字列の配列(各列ごとに1つ)に分割する
  4. 列のインデックスまたは名前を使ってデータを検索するメソッドを提供する
  5. 欠損しているデータがある場合には、デフォルト値やエラー値を返す

これらを段階的に実装していきます。まずは、内部で使用するデータ構造について考えてみましょう。

  • ヘッダーが存在する場合に、列名→インデックスのマッピングを格納するCHashMap<string,uint>
  • 各行を表すCArrayStringのポインタを格納する動的配列CArrayObj
  • _hasHeader、_filename、_separator、およびおそらく_rowCountと_colCountなどの保持されるプロパティ

CArrayObjやCArrayStringを使うのは単に便利なだけではなく、低レベルの配列リサイズ処理に伴う面倒を避けるためでもあります。ネイティブ配列は強力ですが、複雑なデータセットでは扱いづらくなることがあります。一方で、CArrayStringを使えばフィールドの追加は簡単で、CArrayObjによって増えていく行のリストも手軽に管理できます。さらに、列名のためのハッシュマップを使えば、ヘッダー行を毎回スキャンする必要がなくなります。この設計はシンプルでありながらスケーラブルなので、CSVの規模が大きくなったり、データ要件が変化した場合でも対応しやすくなります。

クラス全体を実装する前に、まずはファイルを開いて行を読み取るためのビルディングブロック的なコードスニペットをいくつか書いてみましょう。これらのコードは、後にクラス内で統合して使用する予定です。まずはファイルを開いてみましょう。

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.

このスニペットは、MQL5/Filesディレクトリ内のdata.csvを開こうとします。失敗した場合は、エラーメッセージを出力して処理を終了します。_LastError変数は、ファイルを開けなかった理由を確認する手がかりとなります。たとえば、5004はCANNOT_OPEN_FILEを意味します。次に、ファイルが終わるまでファイルを読み取ってみます。

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
}

ここでは、FileIsEnding()がtrueを返すまでループします。各イテレーションで1行をlineに読み込みます。もし空の行があり、かつエラーが発生した場合は、処理を停止します。ファイルの終わりであれば、自然にループを終了します。完全に空の行は空の文字列として処理されるので、CSVフォーマットによってはそのシナリオを適切に処理することを検討してください。

ここで、CSVがセミコロン(;)を区切り文字として使用しているものとします。次のようにできます。

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()は見つかった部分の数を返します。この呼び出しの後、fieldsには「;」で区切られた各トークンが含まれます。lineが「EURUSD;1.2345;10000」の場合、fields[0]は「EURUSD」、fields[1]は「1.2345」、fields[2]は「10000」になります。

_hasHeaderがtrueの場合、最初に読み取る行は特別です。これを分割し、列名をCHashMapに保存します。以下は例です。

#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);

列名のハッシュマップは、小さな詳細ですが、大きな効果をもたらします。これがないと、インデックスが必要なたびに列ヘッダーをループすることになります。ハッシュマップを使用すれば、TryGetValue() をすばやく呼び出すことでインデックスを得ることができ、列が見つからない場合はデフォルト値を返すことができます。重複した列名や奇妙な列名があれば、事前に検出できます。この方法により、検索が高速になり、コードもスッキリとしたものになります。CSVのサイズが2倍になったとしても、列インデックスの取得は簡単です。

列を各列名をそのインデックスにマップします。後で特定の列名のインデックスが必要になった場合は、次のようにします。

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

各データ行はCArrayStringオブジェクトに格納します。これらの行へのポインタの動的配列を保持します。たとえば次のようになります。

#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);

後で値を取得するには、次のようにします。

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

もちろん、範囲外エラーを避けるために、常に境界をチェックする必要があります。

名前またはインデックスで列にアクセスしてみましょう。CSVにヘッダーがある場合は、列マップを使用して名前で列インデックスを見つけることができます。

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);
  }

この疑似コードは、2つのアクセサ関数を実装する方法を示しています。GetValueByNameはハッシュマップを使用して列名をインデックスに変換し、その後GetValueByIndexを呼び出します。GetValueByIndexは境界をチェックし、必要に応じて値やエラーのデフォルト値を返します。

次に、コンストラクタとデストラクタについてです。これらはすべてクラス内にまとめることができます。コンストラクタは内部変数を初期化するだけでよく、デストラクタはメモリの解放を担当します。以下は例です。

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();
                     }
  };

このクラスのスケッチは、可能な構造を示しています。Load()はまだ実装していませんが、すぐに実装する予定です。メモリを解放するためにClear()メソッドを保持する方法に注目してください。deleterow;を呼び出した後、ポインタの配列をリセットするためにRows.Clear()も実行する必要があります。

それでは、Loadメソッドを実装してみましょう。Load()はファイルを開き、最初の行(おそらくヘッダー)を読み取り、残りの行をすべて読み込んで解析します。

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;
  }

Load()関数は以下をおこないます。

  • 古いデータをクリアする
  • ファイルを開く
  • _hasHeaderがtrueの場合、最初の行をヘッダーとして読み取り、Columnsに入力する
  • 次に、ファイルの最後まで行を読み取り、フィールドに分割する
  • 各行に対して、CArrayStringを作成し、それを入力して、Rowsに追加する
  • 読み取られたデータ行の数を返す

すべてをまとめると、ロジックの大部分が概説されました。次のセクションでは、コードを改良して完成させ、不足しているアクセサメソッドを追加し、最終的な完全なコードを示します。また、取得した行数を確認する方法、存在する列、値を安全に取得する方法などの使用例も紹介します。

これらのコードスニペットを確認することで、ロジックの各部分がどのように組み合わされているかがわかります。最終的なCSVリーダークラスは自己完結型で、簡単に統合できます。インスタンスを作成し、呼び出し「Load("myfile.csv")」を試用し、GetValueByName()またはGetValueByIndex()を使用して必要な情報を取得するだけです。

次のセクションでは、クラスの実装を完了し、コピーして適応できる最終的なコードスニペットを示します。その後、いくつかの使用例と結論を述べて締めくくります。


CSVリーダークラスの実装の仕上げ

前のセクションでは、CSVリーダーの構造を説明し、コードの各部分を詳しく解説しました。ここでは、それらをすべて統合して、一貫した実装にまとめます。その後、簡単に使用方法を紹介します。最終的な記事の構成では、コード全体を一度に示すので、明確な参照として役立ててください。

これまでに説明したヘルパー関数(ファイルの読み込み、ヘッダーの解析、行の保存、アクセサメソッド)を1つのMQL5クラスに統合します。そして、このクラスをEAやスクリプトでどのように使用するかを示す短いコード例もご紹介します。このクラスでは以下のことがおこなえます。

  • MQL5/FilesディレクトリからCSVを読み取る
  • _hasHeaderがtrueの場合、最初の行から列名を抽出する
  • 後続の行から、CArrayStringに格納されるデータの行を形成する
  • 列名(ヘッダーが存在する場合)または列インデックスで値を取得する

また、エラーチェックやデフォルト値の設定も含まれています。それでは、コード全体を以下に紹介します。このコードは例示用であり、実際の環境に合わせて若干の調整が必要になる場合があります。HashMap.mqh、ArrayString.mqh、ArrayObj.mqh の各ファイルが、標準のMQL5インクルードディレクトリに存在することを前提としています。

以下がCSVリーダーの完全なコードです。

//+------------------------------------------------------------------+
//|  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);
  }

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

Load()を詳しく見てみましょう。このメソッドはまず古いデータをクリアし、ファイルを開こうとします。_hasHeaderがtrueに設定されている場合は、最初の1行をヘッダーとして読み取り、列名を分割して保存します。その後はファイルを1行ずつループしながら読み取っていきます。空行はスキップし、有効な行は区切り文字で分割してフィールドの配列に変換します。各フィールドのセットはCArrayStringオブジェクトとして作成され、Rowsに追加されていきます。処理が終わる頃には、読み取った行数が正確に把握でき、Columnsマップも列名による検索にすぐ使える状態になっています。このシンプルで分かりやすい処理の流れにより、翌日CSVの行数が増えたりフォーマットが多少変わったりしても、EAが柔軟に対応できるようになります。

GetValueByName()メソッドとGetValueByIndex()メソッドに関しては、これらのアクセサメソッドは、CSVデータにアクセスするための主要なインターフェイスです。どちらも常に境界チェックをおこなっているため、安全に使えます。存在しない行や列を指定した場合でも、クラッシュすることなく無害なデフォルト値を返します。ヘッダーが存在しない状態でGetValueByName()を呼び出すと、適切にエラー値を返してくれます。このように、たとえCSVの内容に欠落があったり、_hasHeader の設定が誤っていたとしても、EA全体が止まることはありません。必要であれば、列名の不一致などをログ出力するためにPrint()を挿入することもできますが、それは任意です。重要なのは、これらのメソッドによってコード全体の処理が安定し、エラーの少ないスムーズなワークフローが実現できるという点です。

以下のparams.csvに対しては、次のようになります。

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

出力

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

名前ではなくインデックスでアクセスしたい場合は次のようにします。

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

これは、GBPUSDのMaxLotに対応する0.10を出力します。

ヘッダーが存在しない場合、つまり_hasHeaderがfalseに設定されている場合は、列マップの作成はスキップされます。次に、データにアクセスするにはGetValueByIndex()を使用する必要があります。たとえば、CSVにヘッダーがなく、各行に次の3つのフィールドがある場合、

  • 列0:銘柄
  • 列1:価格
  • 列2:コメント

銘柄を取得するには、csv.GetValueByIndex(rowNum,0)を直接呼び出すことができます。

エラー処理はどうでしょうか。存在しない列や行など、何かが欠けている場合、コードはデフォルト値を返します。ファイルを開くことができない場合には、エラーが出力されます。実際には、より堅牢なログ出力が必要になる場合があります。たとえば、外部データに大きく依存している場合は、「rows=csv.Load("file.csv")」を確認し、rows==0の場合は適切に処理することを検討してください。EAの初期化を中止するか、内部のデフォルト値にフォールバックするという対応も考えられます。

不正なCSVや異常な文字エンコーディングに対する厳密なエラー処理は実装していません。より複雑なケースに対応するには、追加のチェックが必要です。ColumnCount()が0の場合は、警告をログに記録することができます。必要な列が存在しない場合には、[エキスパート]タブにメッセージを出力するようにします。

パフォーマンスについて見てみましょう。小規模から中規模のCSVファイルであれば、このアプローチでまったく問題ありません。非常に大きなファイルを扱う必要がある場合には、より効率的なデータ構造やストリーミング方式を検討するのがよいでしょう。ただし、数百行から数千行程度のデータを読み込むといった一般的なEAの用途では、十分なパフォーマンスが得られます。

これでCSVリーダーが完成しました。次の(最後の)セクションでは、テストについて簡単に説明し、いくつかの使用例を紹介し、最後にまとめをおこないます。MQL5のEAやスクリプトとスムーズに統合できる、すぐに使えるCSVリーダークラスが完成します。


テストと使用シナリオ

CSVリーダーの実装が完了したら、すべてが意図したとおりに動作することを確認するのが賢明です。テストは簡単です。小さなCSVファイルを作成し、それをMQL5/Filesに配置し、それをロードしていくつかの結果を出力するEAを作成します。次に、[エキスパート]タブをチェックして、値が正しいかどうかを確認します。以下にテストの提案をいくつか示します。

  1. 基本テスト(ヘッダーあり) :次のようなtest.csvを作成します。

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

    以下をロードします。

    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"));
    

    出力を確認します。「Rows loaded:2」「EURUSD Spread:1」「USDJPY Comment:Another Major」と表示されたら成功です。

    CSVが完全に均一でない場合はどうなるでしょうか。たとえば、ある行の列数が期待より少ないといったケースです。現在の実装では、一貫性を強制していません。行にフィールドが欠けている場合、その列を取得しようとするとデフォルト値が返されます。これは部分的なデータでも処理できる場合には有用ですが、厳密なフォーマットが必要な場合には、Load()の後に列数を検証することを検討してください。非常に大きなファイルでも、この方法は問題なく機能しますが、何万行ものデータを扱うようになると、パフォーマンスの最適化や部分的な読み込みの検討が必要になるかもしれません。とはいえ、日常的な用途(小規模から中規模のCSVファイル)であれば、この構成で十分対応可能です。

  2. テスト(ヘッダーなし):「csv.SetHasHeader(false);」を設定し、ヘッダーのないファイルを使用する場合

    EURUSD;1;Major Pair
    USDJPY;2;Another Major
    
    インデックスによって列にアクセスする必要があります。
    string val = csv.GetValueByIndex(0, 0, "N/A"); // should be EURUSD
    Print("Row0 Col0: ", val);
    
    出力が期待どおりであることを確認します。

  3. 列または行の欠落:存在しない列名や、読み込まれたデータの範囲を超えた行を指定してみてください。設定されたデフォルトのエラー値が返されるはずです。以下は例です。
    string nonExistent = csv.GetValueByName(0, "NonExistentColumn", "MISSING");
    Print("NonExistent: ", nonExistent);
    
    これにより、クラッシュするのではなく、「MISSING」が出力されるはずです。

  4. 大きなファイル :行数の多いファイルがある場合は、それを読み込んで行数を確認します。メモリ使用量やパフォーマンスが妥当な範囲に収まっているかも確認します。この手順によって、このアプローチがあなたの用途に対して十分に堅牢であることを確認できます。 

文字エンコードや特殊な記号についても考慮します。ほとんどのCSVはプレーンASCIIまたはUTF-8であり、MQL5はこれらを適切に処理します。もし奇妙な文字が表示された場合、まずファイルをより適切なエンコードに変換することで解決することがあります。同様に、CSVに末尾の空白や奇妙な句読点が含まれている場合、分割後にフィールドをトリミングすることで、よりクリーンなデータを得ることができます。これらの「完璧ではない」シナリオを今のうちにテストしておくことで、EAが実際に稼働する際、わずかに異なるファイル形式や予期しない文字で処理が止まることを防げます。

使用シナリオ

  • 外部パラメータ
    戦略パラメータを含むCSVがあるとします。各行は、銘柄やいくつかの閾値を定義しているかもしれません。これらの値をEAにハードコーディングする代わりに、起動時にCSVファイルを読み込み、行ごとに処理して動的に適用することができます。パラメータの変更はCSVを編集するだけで簡単におこなえ、再コンパイルする必要はありません。

  • ウォッチリスト管理
    取引対象の銘柄リストをCSVファイルに保存することもできます。EAは実行時にこのリストを読み取ることができるため、コードを変更することなく、銘柄を簡単に追加または削除できます。例えば、CSVには次のような内容が含まれているかもしれません。

    Symbol
    EURUSD
    GBPUSD
    XAUUSD
    
    このファイルを読み込み、EA内で行を順に処理することで、取引対象銘柄をリアルタイムで変更することができます。

  • 他のツールとの統合 :Pythonスクリプトや他のツールでカスタムシグナルや予測をCSV形式で生成している場合、そのデータをCSVにエクスポートし、EAでMQL5にインポートすることができます。これにより、異なるプログラミングエコシステム間のデータ連携が可能になります。

結論

ここまで、MQL5ファイル操作の基本を学びました。テキストファイルを1行ずつ安全に読み込み、CSV行をフィールドに解析して、それらを列名やインデックスで簡単に取得できるように保存する方法を紹介しました。シンプルなCSVリーダーの完全なコードを提供することで、自動取引戦略を強化するための基盤を提供しました。

このCSVリーダークラスは単なるコードスニペットではなく、ニーズに合わせて調整できる実用的なユーティリティです。異なる区切り文字を使用したい場合は、_separatorを変更するだけです。ファイルにヘッダーがない場合は、_hasHeaderをfalseに設定してインデックスに依存できます。このアプローチは柔軟で透明性が高く、外部データをきれいに統合することができます。さらに複雑な取引アイデアを開発していく中で、このCSVリーダーを拡張して、より堅牢なエラー処理を追加したり、さまざまなエンコードをサポートしたり、CSVファイルへの書き戻しを実装したりすることも可能です。現時点では、この基盤がほとんどの基本的なシナリオに対応できるはずです。

信頼できるデータは、堅実な取引ロジックを構築するための鍵となります。CSVファイルから外部データをインポートすることで、より多くの市場の洞察、設定、パラメータセットを活用することができ、これらはすべてハードコーディングされた値ではなく、シンプルなテキストファイルで動的に制御できます。もしニーズがさらに複雑になり、複数の区切り文字を扱ったり、特定の行を無視したり、引用符で囲まれたフィールドをサポートしたりする必要が出てきた場合も、コードを微調整するだけで対応できます。これが、自分自身のCSVリーダーを持つことの利点です。戦略やデータソースの進化に合わせて改良し続けることができる強力な基盤となります。時間が経つにつれて、これを中心にミニデータツールキットを構築し、コアロジックを最初から書き直すことなく、EAに新たな洞察を常に提供できるようになります。

楽しいコーディングと取引を!

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/16614

添付されたファイル |
出来高ベースの取引システムを構築し最適化する方法(チャイキンマネーフロー:CMF) 出来高ベースの取引システムを構築し最適化する方法(チャイキンマネーフロー:CMF)
この記事では、出来高ベースの指標であるチャイキンマネーフロー(CMF)の構築方法、計算方法、使用方法を説明した上で、その概要を説明します。カスタムインジケーターの構築方法を理解します。使用できるいくつかの簡単な戦略を共有し、それらをテストしてどれが優れているかを理解します。
ニュース取引が簡単に(第6回):取引の実施(III) ニュース取引が簡単に(第6回):取引の実施(III)
この記事では、IDに基づいて個々のニュースイベントをフィルターする関数を実装します。さらに、以前のSQLクエリを改善し、追加情報が提供されたり、クエリの実行時間が短縮されるようになります。さらに、これまでの記事で作成したコードを機能的なものにします。
スイングエントリーモニタリングEAの開発 スイングエントリーモニタリングEAの開発
年末が近づくと、多くの長期トレーダーは市場の過去を振り返り、その動きや傾向を分析して、将来の動向を予測しようとします。この記事では、MQL5を用いて長期エントリーの監視をおこなうエキスパートアドバイザー(EA)の開発について解説します。手動取引や自動監視システムの不在によって、長期的な取引チャンスを逃してしまうという課題に取り組むことが本稿の目的です。今回は、特に取引量の多い通貨ペアの一つを例に挙げ、効果的な戦略を立案しながらソリューションを構築していきます。
独自のLLMをEAに統合する(第5部):LLMを使った取引戦略の開発とテスト(III) - アダプタチューニング 独自のLLMをEAに統合する(第5部):LLMを使った取引戦略の開発とテスト(III) - アダプタチューニング
今日の人工知能の急速な発展に伴い、言語モデル(LLM)は人工知能の重要な部分となっています。私たちは、強力なLLMをアルゴリズム取引に統合する方法を考える必要があります。ほとんどの人にとって、これらの強力なモデルをニーズに応じてファインチューニングし、ローカルに展開して、アルゴリズム取引に適用することは困難です。本連載では、この目標を達成するために段階的なアプローチをとっていきます。