English Русский
preview
MQL5入門(第41回)MQL5におけるファイル処理入門(III)

MQL5入門(第41回)MQL5におけるファイル処理入門(III)

MetaTrader 5エキスパート |
17 0
ALGOYIN LTD
Israel Pelumi Abioye

はじめに

連載「MQL5入門」の第41回へようこそ。本記事では、MQL5におけるファイル操作の初心者向け解説をさらに進めていきます。前回までの記事では、ファイルの作成方法、構造化された取引データの書き込み方法、そしてエントリー情報や決済情報を読みやすい取引ジャーナルとして整理する方法について学びました。ここまでで、MQL5におけるファイルの基本的な仕組みや、取引データを安全に保存する方法について理解できているはずです。

前回の記事では、主にMetaTrader 5からファイルへデータを送信する、つまり書き込む処理に焦点を当てました。しかし、ファイル操作では書き込みだけが重要なのではありません。保存されたファイルの内容を読み込み、そのデータを統計分析や実際の取引に活用したい場合はどうすればよいでしょうか。このチュートリアルでは、MQL5でファイルから保存済みの取引情報を読み込み、体系的かつ実用的な形で利用する方法を解説します。これまでと同様に、プロジェクトベースのアプローチで、より分かりやすく実践的に学んでいきます。ファイル読み込みの機能だけを個別に学習するのではなく、トレーダーや開発者が実際の開発現場で頻繁に必要とする用途へ直接応用していきます。

今回取り組むプロジェクトの最終目標は、取引履歴を含むCSVファイルを読み込み、指定した期間内の損益データを利用して残高曲線を描画するインジケータを作成することです。この残高曲線によって、取引パフォーマンスに基づいた口座残高の推移を視覚的に確認できるようになります。

図1:エクイティカーブ

図3:エクイティカーブH

まずはMQL5におけるファイル読み込みの仕組みを理解することに集中します。いきなり完全なインジケータ開発へ進むと、初心者にとっては内容が複雑になりすぎる可能性があります。そのため、本記事ではファイルデータを効率的かつ正確に整理しながら読み込む方法に重点を置きます。たとえば、すべてのTradeID値を1つの配列に格納し、すべてのLotSize値を別の配列に格納する方法を学びます。その後、残りの列についても同じ処理を繰り返し、それぞれのデータ項目を専用の配列へ分割して管理します。

本連載を継続して読んでいる方は、第32回でで、APIの扱いとあわせてファイルから読み込んだデータをグループ化する方法について扱ったことを覚えているかもしれません。その時点では、主な目的がAPIの仕組みを理解することであり、ファイル操作そのものを深く掘り下げることではありませんでした。そのため、初心者向けの内容として、固定長配列やデータごとに個別の変数を使用する、より単純な方法を採用していました。今回の記事では、動的配列を使用した、より柔軟で拡張性の高い方法を採用します。これにより、必要な量のデータを自由に保存できるようになり、値ごとに新しい変数を作成する必要がなくなります。これは、実用的で効率的なMQL5ファイル処理を構築するための重要な基礎となります。


ファイル内のレコード総数を数える

プログラムがファイルからデータを読み込む前に、まず、読み取り可能なレコード数/要素数を把握する必要があります。この情報を取得することで、すべてのレコードへ安全かつ効率的にアクセスでき、適切なループ範囲を設定できます。この処理をおこなわない場合、存在しないデータを参照してしまい、不完全な結果や予期しないエラーが発生する可能性があります。そのため、ファイル読み込み処理では、レコード数のカウントが必ず最初におこなうべき重要なステップとなります。 

ここで、一般的な疑問が生まれます。ファイル内で何を数えているのでしょうか。文章全体でしょうか。単語でしょうか。それとも文字数でしょうか。人間が表形式のファイルを見る場合とは異なり、プログラムはCSVファイルを文章として認識しているわけではありません。プログラムはデータを個別の要素として処理します。このファイル形式では、改行が行(レコード)の区切りとして使用され、カンマで区切られた各値が個別のデータ要素として扱われます。CSVファイルをExcelで開くと、それぞれの値が正確に対応する列へ配置されているため、整然とした表形式のデータとして表示されます。

CSVファイルはExcelで開くと、構造化され整然とした状態で表示され、各データが それぞれの列に正確に配置されます。この視覚的な配置によって、ユーザーはデータ構造を簡単に理解できます。ただし、内部的にはあくまでシンプルなテキストファイルです。スプレッドシートではデータはセルの集合として表示されますが、MetaTraderではCSVファイルをテキストデータとして読み込み、CSV形式で定義された区切り文字によって各要素を分離して処理します。

図3:CSV

しかし、プログラム内部では、同じCSVデータは次のように認識されています。

Account Name: Abioye Israel Pelumi,
Account Balance: 7404.68000000,
Account Login: 31670702,
Start Time:, 2025.11.01 00:00:00,
End Time:, 2026.01.28 00:00:00,
Last Update:, 2026.02.09 18:23:33,
Total Trades:, 68,

MetaTrader内部では、区切り文字を使用してファイルを読み込み、1列ずつ、また1行ずつデータを確認します。各列の値は1つのデータ要素として扱われ、追加される各行は個別のレコードとして処理されます。この方法により、システムは文字数や単語数を数えているのではなく、行と列で構成された構造化されたデータ要素を数えていることが分かります。

ファイル内の要素数を数える場合、実際には利用可能なデータセルの数が対象になります。これには、ヘッダーセルや、口座情報などの取引データ以外の情報も含まれます。また、ファイル内で表示されるデータが存在しない空の行であっても、1つの要素として扱われます。そのため、ファイル読み込み時には、空行が存在する場合、その分だけ要素数が1つ増加します。

図4:要素のインデックス

この違いを理解することは非常に重要です。ユーザーが見ている整った行や列の形式は、プログラム側では単純に順番に並べられたデータ要素として扱われます。これらの要素を正確にカウントすることで、ファイルを安全にループ処理できるようになります。また、必要なサイズの動的配列を確保し、後の処理で使用する取引損益などの必要な情報だけを抽出できます。この段階で正確なデータ数を把握しておくことにより、後ほど残高曲線を生成する際のファイル処理において、配列範囲外アクセスエラーを防ぎ、正確な結果を得ることができます。

以前、FileOpenについて解説したことを覚えておいででしょうか。FileOpenは、あらゆるファイル処理の最初のステップとなる重要な関数です。この関数を呼び出すことで返されるファイルハンドルは、プログラムがファイルを管理するための固有の参照番号として使用されます。また、ファイルを読み込み用として開くのか、書き込み用として開くのか、あるいは別の目的で使用するのかを指定するために、適切なアクセスモードを同時に設定する必要があります。 

例:
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---

   string Filename = "Trading_Journal.csv";

   int  file_handle = FileOpen(Filename, FILE_READ | FILE_CSV | FILE_ANSI, ',');

   if(file_handle == INVALID_HANDLE)
     {
      Print("Failed to open file. Error: ", GetLastError());
     }

   else
      if(file_handle != INVALID_HANDLE)
        {


         FileClose(file_handle);
        }

  }

解説:

まず、Filenameという名前の文字列型変数を作成し、その値としてTrading_Journal.csvを設定しています。この変数の役割は、プログラムが参照するファイル名を保持することです。この方法を使用することでコードの構造が整理され、必要に応じてファイル名を簡単に変更できるようになります。ファイル名を定義した後、FileOpen関数を呼び出し、その戻り値をfile_handle変数へ格納します。返される整数値であるファイルハンドルは、プログラム内でファイルを識別するための固有の参照番号として機能します。プログラムは、ファイルからデータを読み込むたびに、このハンドルを使用して対象のファイルへアクセスします。 FileOpenでFILE_READフラグを指定することで、プログラムにファイルの内容を参照および読み込む権限を与えています。今回の処理では、ファイル内容を変更したり更新したりする必要はないため、FILE_WRITEフラグは指定していません。これにより、保存済みのデータを誤って変更してしまう可能性を減らしています。

さらに、MetaTraderに対して、このファイルがCSV形式の列データを含んでいることを通知しています。指定したカンマ区切り文字によって各値が分割され、FILE_ANSIオプションによって文字コード形式が設定されます。これらの設定を組み合わせることで、ファイルは意図した形式で正常に開かれ、正しく処理されます。 次に重要なのは、ファイルを開く処理が成功したかどうかを確認することです。ここでは、変数file_handleの値を確認し、INVALID_HANDLEと等しいかどうかを判定します。FileOpenが失敗した場合、通常のファイルハンドル値は返されません。その代わりに無効なハンドルが返され、ファイル読み込みの開始に失敗したことを示します。

ファイルを開けなかった場合、プログラムはエラーメッセージと関連するエラー識別番号を表示します。この識別番号は、アクセス権限の問題、ファイルが別の処理で使用中である場合、またはファイルが見つからない場合など、処理が失敗した理由を確認する手掛かりになります。 このような診断情報は非常に有用です。なぜなら、問題解決の方向性を示してくれるためです。ファイルが正常に開かれた場合、プログラムは安全に次の処理へ進むことができます。今回の目的は単にファイルへアクセスできることを確認することなので、最後にファイルを閉じます。ファイルを閉じることで、ファイルが開いたままになったり、他の処理で利用できなくなったりすることを防ぎ、システムリソースも解放できます。

比喩的な説明

Filename変数を宣言し、そこへTrading_Journal.csvを設定する処理は、図書館で読みたい本のタイトルを申込用紙へ記入する作業に似ています。本の名前をその都度口頭で伝えるのではなく、1か所に記録して管理している状態です。もし本のタイトルが変更された場合でも、その申込用紙だけを書き換えればよく、プログラム全体を修正する必要はありません。 次に、FileOpen関数を使用する処理は、図書館で本を借りる申請をすることに似ています。図書館員が本を見つけると、利用者には引換券が渡されます。この引換券がファイルハンドルに相当します。この番号によって、現在そのファイルへアクセスできるのが誰なのかをプログラム上で識別できます。ファイルからデータを読み込みたい場合は、必ずこの引換券(ファイルハンドル)を提示する必要があります。これがなければ、本(ファイル)へアクセスすることはできません。

FILE_READを指定することは、図書館員に対して「この本は館内で読むだけにしたい」と伝えることに相当します。内容を書き換えたり、メモを書き込んだりする許可は求めていません。そのため、FILE_WRITEは指定していません。今回はデータを変更するのではなく、単純に読み取るだけだからです。この設定によって、元のデータが意図せず変更されることを防止できます。 FILE_CSVフラグは、図書館員に「この本は行と列で整理された表形式になっています」と伝えるようなものです。FILE_ANSIフラグは、本に使用されている文字形式や表記方法を示します。また、カンマ区切りの指定は、「本の中の各情報項目は特定の記号によって区切られています」と伝えることに相当します。

そして、図書館員が本を見つけられたかどうかを必ず確認する必要があります。もし「その本は見つかりません」と言われた場合、何らかの問題があります。本自体が存在しない可能性もありますし、誰かが現在借りている可能性もあります。原因を確認することで、どのように問題を解決すればよいか判断できます。 図書館員から引換券を受け取ることができれば、すべて正常です。これで本を読むことができます。読み終わった後は、引換券を返却し、本を元の場所へ戻します。これがFileClose関数の役割です。FileCloseは、システムへ処理完了を通知すると同時に、使用していたリソースを解放し、ファイルがロックされた状態のまま残らないようにします。

これでファイルを開く処理が完了しました。次のステップでは、ファイル内に存在する要素の総数を取得します。

例:

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

   string Filename = "Trading_Journal.csv";

   int  file_handle = FileOpen(Filename, FILE_READ | FILE_CSV | FILE_ANSI, ',');

   if(file_handle == INVALID_HANDLE)
     {
      Print("Failed to open file. Error: ", GetLastError());
     }

   else
      if(file_handle != INVALID_HANDLE)
        {

         int total_elements_count = 0;
         while(!FileIsEnding(file_handle))
           {
            FileReadString(file_handle);
            total_elements_count++;
           }

         Print(total_elements_count);


         FileClose(file_handle);
        }
  }

解説:

最初のステップとして、total_elements_countという名前の変数を宣言し、その値を0に設定します。この変数は、ファイルから読み込んだ要素の総数を記録するために使用されます。プログラムはまだデータを読み込んでいないため、初期値として0を設定します。プログラムがファイルから要素を読み込むたびに、この変数の値は増加していきます。最終的に、読み込まれた要素の総数を示すカウンタとして機能します。 データが存在する間、ループ処理によってファイルの読み込みを繰り返します。ファイルにまだ読み込むデータが残っているかどうかを確認するために、ファイル参照位置を監視する関数を使用します。追加のデータが存在する場合、ループは継続されます。一方、ファイルの終端に到達すると、その関数によってループ終了の判断が行われます。

ループ内部では、プログラムがファイルから次の要素を読み込みます。この関数は、文字列形式でデータを取得するだけではなく、ファイルポインタを自動的に次の位置へ移動させます。CSV形式で整理されたファイルの場合、各呼び出しでは通常、指定された区切り文字によって分割された1つの要素が読み込まれます。この方法により、プログラムはファイル全体を一度に読み込もうとするのではなく、各データ要素を個別に処理できます。 各要素の読み込みが完了すると、プログラムはtotal_elements_countの値を1増加させます。この処理は、正常に読み込まれた要素数を追跡するために非常に重要です。新しい要素が読み込まれるたびにカウンタが増加し、現在までに取得した総数を正確に反映します。

ループが終了すると、プログラムはファイルの終端まで到達し、利用可能なすべての要素数をカウントした状態になります。最後のステップとして、total_elements_countの値を表示します。この出力によって、読み込み処理が正常に完了したことを確認でき、ファイル内に存在する要素の総数を把握できます。

比喩的な説明

図書館へ行き、特定の棚に並んでいる本の数を数えたいとします。あなたはノートを持っており、まずページの上部に「0」と書きます。この0は、現時点で数え終わった本の数を意味します。まだ1冊も数えていないため、0から始めるのは自然です。これは、total_elements_countという変数を宣言し、初期値を0に設定する処理と同じです。ここからカウントが始まります。

次に、棚の左端から右方向へ順番に確認していきます。次の本を確認する前に、必ず棚にまだ本が残っているかを確認します。本が存在する場合は、次の本へ進みます。棚が空になり、これ以上本がない場合は確認を停止します。これがFileIsEndingの役割です。つまり、棚の最後まで到達したかどうかを確認する処理に相当します。まだ本が残っている場合、関数は「まだ終端ではありません」と判断し、カウント処理を続行します。本がなくなった場合は「終端に到達しました」と判断し、ループを終了します。

次に、本を1冊取り出す前には、必ずそこに本が存在することを確認します。これは、ファイルを要素単位で読み込む処理と同じです。1回の読み込みで1つの情報だけを取得し、その後ファイルポインタが自動的に次の位置へ進みます。CSV形式のような列構造を持つファイルでは、各項目を順番に処理できます。 本を1冊確認するたびに、ノートに記録した合計数を1増やします。カウントは0から始まり、1、2、3というように増えていきます。これは、新しい要素を1つ処理するたびにカウンタを増加させる処理と同じです。

最終的に棚の最後まで到達します。もう数える本は残っていません。そこでノートを確認すると、最後に記録された数字が棚にある本の正確な冊数を示します。同様に、プログラムではループ終了後にtotal_elements_countの値を表示します。この値によって、ファイル内に存在する要素の総数が確認でき、カウント処理が正常に完了したことを判断できます。

 

ファイル内のすべての要素を動的配列へ格納する

ファイル内の要素数を取得した後、次のステップでは、各要素を順番に読み込み、1つの動的配列へ格納します。プログラムでは、ファイル内での位置に基づいて各要素にインデックスを割り当てるため、保存されたデータへ迅速かつ一貫してアクセスできるようになります。 動的配列は、必要に応じて格納できる要素数を柔軟に変更できるため、サイズが異なるファイルを扱う場合に特に有効です。要素が配列へ格納された後は、それぞれの要素をインデックスで参照することで、取引ジャーナル内のTradeID、LotSize、またはその他の列データなど、特定のデータを簡単に取得できます。

例:

if(file_handle == INVALID_HANDLE)
  {
   Print("Failed to open file. Error: ", GetLastError());
  }

else
   if(file_handle != INVALID_HANDLE)
     {

      int total_elements_count = 0;
      while(!FileIsEnding(file_handle))
        {
         FileReadString(file_handle);
         total_elements_count++;
        }

      // Print(total_elements_count);

      FileSeek(file_handle, 0, SEEK_SET);
      string TotalElements[];
      ArrayResize(TotalElements,total_elements_count);

      for(int i = 0; i < total_elements_count; i++)
        {

         TotalElements[i] = FileReadString(file_handle);

        }

      Print(TotalElements[1]);

      FileClose(file_handle);
     }

解説:

まず、すべての要素を読み込めるように、ファイルを先頭位置へ戻します。この手順を省略すると、カウント処理の後でファイルポインタが末尾に残ったままになるため、何も読み込まれません。最初からすべての内容を確認するためには、テープを再生する前に巻き戻すようなものです。 その後、プログラムはforループを使用して各要素を順番に処理します。ループは、最初の要素である0から開始し、測定された要素総数に到達するまで継続します。この方法により、ファイル内のすべての構成要素が個別かつ体系的に処理されます。

各ループの繰り返しでは、プログラムは次に利用可能なデータを読み込み、現在のインデックス位置にある配列へ挿入します。次のループサイクルでは、処理は自動的に次の要素へ進みます。要素を保存するために動的配列を使用することで、後から任意のデータへ体系的かつ効率的にアクセスできます。 各ループの繰り返しごとに、プログラムはファイルから1つの要素を取得し、適切なインデックス位置の配列へ追加します。各要素は、見出し、テキスト、数値のいずれであっても、個別に処理されます。後続の処理で同じ要素を繰り返し読み込まないように、読み込み処理は各段階の後に自動的に次の要素へ移動します。

プログラムは、読み込んだ各要素に動的配列内の固有の位置を割り当てることで、データを整理します。これにより、後からインデックスを使用して任意の要素へアクセスできます。例えば、最初の要素はインデックス0に保存され、2番目の要素はインデックス1に保存されます。 動的配列を使用することは実用的です。なぜなら、ファイルにいくつの要素が含まれるかを事前に推測する必要がないからです。配列のサイズをすべての要素が収まるように変更した後、すべてのデータ項目を安全に保存できます。これにより、すべてのデータが保存され、後の分析、分類、処理のためにアクセス可能になります。 最後に、要素が正しく保存されていることを確認するために、配列の2番目の要素を表示しました。この簡単な確認手順により、インデックス処理が正常に動作していること、そして配列にファイル内容が期待どおり格納されていることを確認できます。

比喩的な説明

まず、FileSeekを使用してファイルポインタを先頭へ移動します。これは、VHSやカセットテープをもう一度再生する前に巻き戻す作業に似ています。すべての項目を数え終えた後、ポインタは末尾にあるため、この処理をおこなわなければ何も読み込まれません。FileSeekによって、ファイルの最初の要素から読み込みを開始できるようになります。 次に、プログラムはforループを使用して各要素を処理します。これは、ベルトコンベア上で箱が1つずつ移動している状態を考えると分かりやすくなります。ループは最初の箱から始まり、最後の箱に到達するまで順番に進みます。それぞれの箱がファイル内の1つの要素を表しています。要素を1つも見逃さないように、プログラムは各箱を個別に処理します。

各ループサイクルの最後では、次のファイル要素が読み込まれ、適切なインデックス位置の配列へ追加されます。これは、ベルトコンベアから商品を取り出し、番号が付けられた容器へ入れる作業に似ています。最初の商品は番号0の容器へ入れられ、次の商品は次の番号が付いた容器へ配置されます。ベルトコンベアが自動的に前へ進むことで、次の商品が次のサイクルの準備状態になります。 プログラムは、動的配列へ要素を保存することで、それぞれの要素に決められた場所を割り当てます。これは、倉庫内のすべての棚に番号を付け、後から簡単に商品を探せるようにすることに似ています。動的配列を利用することで、事前に要素数を推測する必要がなくなります。これは、保管する商品数に応じて大きくできる倉庫を持つことに似ています。

 

ファイルデータを列ごとに動的配列へグループ化する

ファイルから関連するすべての要素を1つの動的配列へ読み込んだ後、このセクションでは次のステップへ進みます。すべての項目を1つの配列に保持することは有用ですが、TradeID、LotSize、OpenPriceなど、特定の種類のデータを処理または分析する場合には、あまり実用的ではありません。データを扱いやすくするために、同じ列ヘッダーに属するすべての要素を、それぞれ個別の動的配列へ整理します。例えば、1つの配列にはすべてのTradeIDデータを格納し、別の配列にはすべてのLotSize値を格納します。この方法を使用することで、それぞれの種類のデータへ素早くアクセスし、個別に確認や処理をおこなうことができます。

これは、ファイルキャビネットを整理する作業に似ています。最初は、すべての書類が1つの大きな山に積まれている状態かもしれません。その場合、必要な書類を見つけることは困難です。請求書、領収書、レポートなど、それぞれのカテゴリーごとに専用のフォルダを作成すれば、どの書類でも簡単に見つけて処理できます。ファイルデータについても同様に、列ごとに専用の配列へグループ化することで、それぞれの情報種類に対応した独自のメモリ領域を確保できます。これにより、プログラムはより整理され、効率的になり、追加の処理や分析へ対応できる状態になります。

まず、すべてのTradeID値を1つのグループへまとめます。この配列は最初の列専用配列となり、各要素はファイル内の1つのTradeIDに対応します。TradeIDを独自の動的配列へ分離することで、各取引の固有識別子へ迅速にアクセスするための体系的な方法を構築できます。最初に確認すべきことは、ファイルに含まれる総取引数です。この確認が完了した後、その値が配列内のどのインデックスに存在するかを特定する必要があります。

図5:総取引数

このステップは非常に重要です。なぜなら、総取引数は動的配列へ保存されるTradeID値の数に直接影響するためです。この数を理解することで、配列を適切に確保でき、範囲外のTradeID値を格納してしまうようなミスを防ぐことができます。

例:

FileSeek(file_handle, 0, SEEK_SET);
string TotalElements[];
ArrayResize(TotalElements,total_elements_count);

for(int i = 0; i < total_elements_count; i++)
  {

   TotalElements[i] = FileReadString(file_handle);

  }

int total_deals = (int)StringToInteger(TotalElements[10]);

解説:

前のセクションでは、ファイル内のすべての要素をTotalElementsという名前の動的な文字列配列へ保存しました。配列のインデックスは0から始まるため、総取引数は11番目の要素、つまりインデックス10の位置に存在します。この値を文字列から整数へ変換した後、変数total_dealsへ格納します。この数値は、取引総数を表しており、TradeID配列を適切なサイズに設定するため、また取引を処理するループで使用するために必要になります。

比喩的な説明

動的配列TotalElementsは、ファイル情報の一部をそれぞれ格納している番号付きの郵便受けが並んでいる状態として考えることができます。インデックスは0から始まるため、総取引数は11番目の郵便受けに存在し、それはインデックス10に対応します。郵便受けの中にある数字は、現在はテキスト形式で書かれた紙のような状態です。そのため、まだ計算に使用することはできません。ここでStringToIntegerを使用して、そのテキストを利用可能な数値へ「変換」します。これは、郵便受けから紙を取り出し、それを数えたり計算に使用したりできる実際のコインへ変える作業に似ています。整数へ変換して取引数を取得できれば、動的配列内のTradeID領域を確保する際に、容量不足を心配することなく準備できます。

次のステップでは、TotalElements配列内でTradeID値が開始されるインデックスを特定します。これは重要な処理です。なぜなら、後続するすべてのチケット番号を正しく取得するためには、元のTradeIDが存在する正確な位置を把握する必要があるからです。この開始位置を特定した後、総取引数分のすべてのチケットをulong型の1つの動的配列へ保存できます。開始インデックスからループ処理をおこない、この配列へ値を格納することで、ファイル内のすべてのTradeIDが整理された状態で記録されます。これにより、後続の処理で利用したり、必要に応じてファイルへ書き戻したりするための準備が整います。

例:
else
   if(file_handle != INVALID_HANDLE)
     {

      int total_elements_count = 0;
      while(!FileIsEnding(file_handle))
        {
         FileReadString(file_handle);
         total_elements_count++;
        }

      // Print(total_elements_count);

      FileSeek(file_handle, 0, SEEK_SET);
      string TotalElements[];
      ArrayResize(TotalElements,total_elements_count);

      for(int i = 0; i < total_elements_count; i++)
        {

         TotalElements[i] = FileReadString(file_handle);

        }

      int total_deals = (int)StringToInteger(TotalElements[10]);

      ulong TradeID[];
      ArrayResize(TradeID,total_deals);

      int id_count = 0;

      for(int i = 24; i < total_elements_count; i += 12)
        {

         TradeID[id_count] = (ulong)StringToInteger(TotalElements[i]);
         id_count++;
        }

      ArrayPrint(TradeID);

      FileClose(file_handle);
     }

解説:

まず、TradeIDという名前のulong型の動的配列を宣言します。MQL5では、各取引に割り当てられるチケット番号は非常に大きな数値になる可能性があるため、符号なしlong整数として保存されます。ulongを使用することで、配列はあらゆる可能性のあるチケット番号を安全に保存でき、オーバーフローや負の値が発生する可能性を排除できます。事前にいくつのチケット番号が存在するか分からないため、動的配列を使用します。 配列を宣言した後、total_dealsに保存されている取引総数に基づいて配列サイズを変更します。これにより、配列が保持できる数を超えて要素を追加しようとした場合に発生するエラーを防止できます。id_count変数は、TradeID配列内で次のチケット番号を保存する位置を追跡するために使用され、初期値は0です。チケットが追加されるたびに、このカウンタは増加し、次に利用可能なインデックスを示します。

ループ処理では、インデックス24からTotalElements配列内のチケット番号を取得します。この配置は、CSVファイル形式の最初の24要素がヘッダー情報や口座関連データで構成されていることに基づいています。最初のTradeIDはファイル内の25番目の要素に存在します。配列のインデックスは0から始まるため、これは配列内ではインデックス24に対応します。 ファイル内では、各取引は12個の異なる要素に分割されています。そのため、ループは毎回12ずつ進み、次の取引の開始位置へ正しく移動します。この処理により、読み込み位置がファイル構造と一致した状態で維持されます。

図6:動的配列

ループ内部では、プログラムは現在のインデックス位置にあるTotalElementsの要素を取得し、id_countで指定された位置のTradeID配列へ保存します。保存が完了するとid_countは1増加し、次のチケットが配列内の次の位置へ格納されるようになります。この処理は、すべての取引に対応するすべてのチケットが保存されるまで繰り返されます。

比喩的な説明

TradeID配列は、それぞれ1つのチケット番号を受け取る準備ができた空の郵便受けが並んでいる状態に例えることができます。口座内の各取引には、固有の鍵やIDカードのようなチケット番号が存在します。これらの番号は非常に大きな値になる可能性があります。ulongを使用することは、すべての鍵を安全に収納できる十分な大きさの郵便受けを選ぶことに似ています。これにより、容量不足によって壊れたり、あふれたりすることを防ぎます。事前に取引数が分からないため、必要な数だけ正確に収容できるよう、郵便受けの列を動的に拡張できるようにします。 鍵を郵便受けへ入れる前に、取引総数に合わせて列の大きさを変更します。これは、すべての鍵に対して正確な数の収納場所を準備するために、郵便受けの数を測定することに似ています。

もし収納場所の数を間違えると、一部の鍵が床へ落ちてしまう可能性があります。プログラムでは、これは範囲外エラーが発生することに相当します。id_count変数はカウンタとして機能し、次の鍵をどの郵便受けへ入れるべきかを示します。鍵を1つ入れると、カウンタは次の郵便受けへ進みます。 

次に、取引情報が行ごとに整理された長い表を想像してください。各行には12個の区画があり、その最初の区画にTradeIDが配置されています。最初の24個の区画はヘッダー情報と口座情報で占められているため、ループは表の25番目の位置にある最初のTradeIDから開始します。次の行にあるTradeIDへ移動するために、表の上を12個の区画ずつ進みます。各TradeIDを見つけるたびに取得し、TradeID配列内の次の空いている場所へ保存します。最終的に、すべてのチケット番号は適切な郵便受けへ配置されます。これで、特定の場所を検索することで、任意のチケット番号をすぐに見つけられるようになります。

すべてのTradeID値を動的配列へ正常にまとめた後、残りの列についても同じ処理を進めることができます。CSVファイル内のSymbol、OrderType、LotSize、OpenTime、OpenPrice、StopLoss、TakeProfit、CloseTime、ClosePrice、Profit、Resultなどの列も取得し、それぞれ別の動的配列へ保存できます。列ごとにすべての要素をまとめることで、取引順序が維持され、後から特定のデータを簡単に取得できるようになります。

例:
//symbol
string symbol[];
ArrayResize(symbol,total_deals);

int sym_count = 0;

for(int i = 25; i < total_elements_count; i += 12)
  {

   symbol[sym_count] = TotalElements[i];
   sym_count++;
  }

//OrderType

string OrderType[];
ArrayResize(OrderType,total_deals);

int order_type_count = 0;

for(int i = 26; i < total_elements_count; i += 12)
  {

   OrderType[order_type_count] = TotalElements[i];
   order_type_count++;
  }

//LotSize
double LotSize[];
ArrayResize(LotSize,total_deals);

int lot_count = 0;

for(int i = 27; i < total_elements_count; i += 12)
  {

   LotSize[lot_count] = StringToDouble(TotalElements[i]);
   lot_count++;
  }

//OpenTime
datetime OpenTime[];
ArrayResize(OpenTime,total_deals);

int open_time_count = 0;

for(int i = 28; i < total_elements_count; i += 12)
  {

   OpenTime[open_time_count] = StringToTime(TotalElements[i]);
   open_time_count++;
  }

//OpenPrice
double OpenPrice[];
ArrayResize(OpenPrice,total_deals);

int open_price_count = 0;

for(int i = 29; i < total_elements_count; i += 12)
  {

   OpenPrice[open_price_count] = StringToDouble(TotalElements[i]);
   open_price_count++;
  }

//StopLoss
double StopLoss[];
ArrayResize(StopLoss,total_deals);

int StopLoss_count = 0;

for(int i = 30; i < total_elements_count; i += 12)
  {

   StopLoss[StopLoss_count] = StringToDouble(TotalElements[i]);
   StopLoss_count++;
  }

// TakeProfit
double TakeProfit[];
ArrayResize(TakeProfit,total_deals);

int TakeProfit_count = 0;

for(int i = 31; i < total_elements_count; i += 12)
  {

   TakeProfit[TakeProfit_count] = StringToDouble(TotalElements[i]);
   TakeProfit_count++;
  }

//CloseTime
datetime CloseTime[];
ArrayResize(CloseTime,total_deals);

int close_time_count = 0;

for(int i = 32; i < total_elements_count; i += 12)
  {

   CloseTime[close_time_count] = StringToTime(TotalElements[i]);
   close_time_count++;
  }

//ClosePrice
double ClosePrice[];
ArrayResize(ClosePrice,total_deals);

int close_price_count = 0;

for(int i = 33; i < total_elements_count; i += 12)
  {

   ClosePrice[close_price_count] = StringToDouble(TotalElements[i]);
   close_price_count++;

  }

//Profit
double Profit[];
ArrayResize(Profit,total_deals);

int Profit_count = 0;

for(int i = 34; i < total_elements_count; i += 12)
  {

   Profit[Profit_count] = StringToDouble(TotalElements[i]);
   Profit_count++;

  }

//Result
string Result[];
ArrayResize(Result,total_deals);

int Result_count = 0;

for(int i = 35; i < total_elements_count; i += 12)
  {

   Result[Result_count] = TotalElements[i];
   Result_count++;

  }

ArrayPrint(Result);

解説:

最初に確認する列はSymbolです。すべての取引の銘柄を保存するために、まずsymbolという名前の動的な文字列配列を宣言します。現在、全体の取引数が分かっているため、配列サイズをtotal_dealsへ変更します。これにより、すべての取引銘柄を安全に格納できます。また、0で初期化されたカウンタsym_countも作成します。このカウンタは、次の銘柄を格納する配列内の現在のインデックス位置を追跡します。 Symbolはインデックス24の取引識別子の直後に存在するため、ループはTotalElements配列のインデックス25から開始します。各取引は12個の要素で構成されているため、ループは12ずつ進みます。各繰り返しの最後で、現在の要素がsymbol配列へコピーされます。保存後、sym_count変数が増加し、配列内の次に利用可能な位置を示します。

次に、OrderType列を処理します。別の動的な文字列配列を宣言し、OrderTypeのサイズをtotal_dealsへ変更します。ここでもカウンタとしてorder_type_countを使用します。CSVファイルでは、OrderTypeはSymbolの直後に存在するため、forループはインデックス26から開始します。銘柄の場合と同様に、ループは各繰り返しで12ずつ増加し、次の取引のOrderTypeへ移動します。各値はOrderType配列へ保存され、カウンタが増加します。これにより、取引タイプが正しい順序で記録されます。 次に、ロットサイズは小数値であるため、LotSize用にdouble型の動的配列を宣言します。lot_countカウンタを使用し、配列サイズを変更します。forループはインデックス27から開始し、再び12ずつ増加します。各要素は、配列へ保存される前にStringToDoubleを使用して文字列からdouble型へ変換されます。カウンタは増加し、次のロットサイズが次の位置へ保存されるようになります。

OpenTime列には日時情報が保存されているため、datetime型の配列とopen_time_countというカウンタを定義します。ループはインデックス28から開始し、12ずつ増加しながら、各要素をStringToTimeで変換します。これにより、配列には各取引の開始時間がdatetime値として正しく保存されます。OpenPriceについては、double配列を宣言してサイズを変更し、open_price_countを使用します。ループはインデックス29から開始し、12ずつ増加します。各文字列要素をdouble型へ変換した後、配列へ格納します。カウンタによって、次の始値が次の位置へ保存されることが保証されます。

TakeProfitとStopLoss列も同じパターンになります。それぞれにカウンタ、double配列、そしてループ処理があります。StopLossはインデックス30、TakeProfitはインデックス31からループを開始します。カウンタが位置を管理するため、ファイル内の各文字列要素はdouble型へ変換され、対応する配列へ保存されます。次に、OpenTimeと同様の処理となるCloseTimeを扱います。close_time_countというカウンタを初期化し、datetime配列を作成してサイズを変更します。ループはインデックス32から開始し、12ずつ増加します。各要素をStringToTimeで変換し、その結果を保存します。

続いて、ClosePrice、Profit、Resultについても、再び配列とカウンタを使用します。ClosePriceとProfitはdouble型であるため、保存前にファイル要素をdouble型へ変換します。Resultは文字列配列になります。それぞれのループは、次の取引に対応する同じ列へ移動するため、開始インデックスは順番に33、34、35となり、毎回12ずつ増加します。各配列のカウンタによって、すべての要素が正しい位置へ挿入されることが保証されます。 処理が完了すると、各列のデータはそれぞれ別の動的配列へ保存されます。各配列では、同じインデックスが同一の取引に対応します。また、すべての配列には同じ数の要素(total_deals)が含まれます。この整理された構造により、取引情報へ構造的かつ信頼性の高い方法でアクセスし、確認し、変更できるようになります。


結論

CSVファイルの読み込み方法と、その内容を動的配列へ整理する方法を学ぶことで、MQL5におけるファイル処理への理解を大きく深めることができました。まずファイル内の要素総数を確認し、その後すべてのデータを1つの配列へまとめ、さらに各列を個別の動的配列へ分割しました。この方法により、各取引へ迅速にアクセスして確認できるだけでなく、統計分析やバランスカーブの作成など、より複雑な処理へ進むための基礎も構築できます。このようにファイルデータを構造化することで、取引履歴をプログラム上で管理するための堅牢な仕組みを作成できました。次回は、これらの配列を使用して取引パフォーマンスを可視化する処理へ進む準備が整います。

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

添付されたファイル |
MQL5でカスタムインジケータを作成する(第7回):セッション分析のためのハイブリッドTime Price Opportunity (TPO)マーケットプロファイル MQL5でカスタムインジケータを作成する(第7回):セッション分析のためのハイブリッドTime Price Opportunity (TPO)マーケットプロファイル
ハイブリッドTime Price Opportunity (TPO)マーケットプロファイル用のカスタムインジケータをMQL5で開発します。このインジケータは、タイムゾーン調整に対応し、イントラデイ、日足、週足、月足、固定期間など、複数のセッション時間足をサポートします。このインジケータでは、価格をグリッド化し、各セッションの高値、安値、始値、終値などのデータを追跡します。また、TPOカウントに基づいて、ポイントオブコントロール(PoC)やバリューエリア(VA)などの主要要素を計算します。さらに、TPO文字、単独プリント、VA、PoC、終値マーカーの色をカスタマイズ可能にし、チャート上にプロファイルを視覚的に描画することで、詳細なセッション分析を可能にします。
MQL5取引ツール(第19回):チャート描画用のインタラクティブなツールパレットの構築 MQL5取引ツール(第19回):チャート描画用のインタラクティブなツールパレットの構築
ドラッグによる移動やリサイズが可能なパネル、テーマ切り替え機能を備えた、チャート描画用のインタラクティブなツールパレットをMQL5で構築します。また、十字カーソル、トレンドライン、ライン、矩形、フィボナッチ、テキスト、矢印などのツール用ボタンを追加し、マウスイベントを処理してツールの有効化や操作ガイドを実現します。このシステムは、カスタマイズ可能なUIを通じてリアルタイムなチャート操作をサポートし、トレード分析をより効率的におこなえるようにします。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
MQL5入門(第40回):MQL5におけるファイル処理入門(II) MQL5入門(第40回):MQL5におけるファイル処理入門(II)
MQL5で、指定した期間の口座履歴を読み取り、構造化されたレコードをファイルに書き込むことで、CSV形式の取引ジャーナルを作成します。約定数の取得、チケット番号の取得、銘柄や注文タイプの判別、そしてエントリー(ロット、時間、価格、SL/TP)および決済(時間、価格、利益、結果)データの取得方法を、動的配列を使用して解説します。その結果、分析やレポート作成に適した、整理された永続的なログを作成できます。