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

MQL5入門(第40回):MQL5におけるファイル処理入門(II)

MetaTrader 5統計と分析 |
32 0
ALGOYIN LTD
Israel Pelumi Abioye

はじめに

連載「MQL5入門」の第40回へようこそ。前回の記事では、FileSelectDialogとFileOpenを使用してファイルを作成・開く方法を学び、MQL5におけるファイル操作の基礎を構築しました。また、シンプルな取引ジャーナルの構造を作成し、重要な口座情報を安全かつ整理された方法でファイルへ記録しました。 この第2部では、その基盤をさらに発展させ、実際の取引履歴をジャーナルファイルへエクスポートすることに焦点を当てます。指定した期間内の口座履歴へアクセスする方法、そしてチケット番号、銘柄、注文タイプ、ロットサイズ、エントリー時刻・決済時刻、価格、利益、取引結果などの重要な取引情報を抽出する方法を学びます。これらの記録は、構造化され読みやすい形式でジャーナルへ書き込まれます。

この記事も、連載内の他の記事と同様に、初心者を対象に書かれています。各操作の理由を細かく分析し、各ステップを詳細に説明します。取引ジャーナルのスクリプトを拡張することで、同じプロジェクトベースの手法を引き続き使用します。この記事を読み終える頃には、実際のMQL5アプリケーションにおいて重要となる、取引履歴の読み取り方法とファイルへ永続的に保存する方法を実践的に理解できるようになります。これは、ログ記録、分析、レポート作成、そして戦略評価において非常に重要な要素です。


取引ジャーナルの列ヘッダーを作成する

前回は、MQL5のFileWrite関数を使用してファイルへデータを書き込む方法を学びました。これを基礎として、プログラム内のデータをファイルへ保存する仕組みを構築しました。この章では、その理解をさらに発展させ、取引ジャーナルのヘッダー行を作成します。ヘッダー行とは、列名を定義するファイルの最初の行です。これらのタイトルは、各情報項目のラベルとして機能し、後から取引データを読み取り、分析する際に役立ちます。

今回作成する取引ジャーナルのヘッダーはTradeID、Symbol、OrderType、LotSize、OpenTime、OpenPrice、StopLoss、TakeProfit、CloseTime、ClosePrice、Profit in dollars、Resultです。これらのヘッダーは、それぞれ取引に関する重要な情報を含んでいます。OrderTypeは買いまたは売りの種類を示し、TradeIDはチケット番号を保存します。Symbolは取引対象の通貨ペアや金融商品の識別に使用されます。OpenTimeとOpenPriceは、取引が開始された時間と価格を示し、LotSizeは取引量を表示します。CloseTimeとClosePriceは取引が決済された時間と価格を示します。一方、StopLossとTakeProfitは、その取引に設定された保護レベルを表します。Profit in dollarsとResultは、取引結果の概要を提供します。

このヘッダー行を最初に書き込むことは非常に重要です。なぜなら、その後に追加されるすべての取引データが適切な列の下に整理されることを保証するからです。これにより、整理された、見やすくプロフェッショナルな形式の取引ジャーナルが作成されます。後から取引記録を追加する際も、各エントリーは対応するヘッダーの下に正確に配置されます。前回の記事で説明したMQL5関数を使用し、以下のステップでこのヘッダー行をファイルへ挿入する方法を解説します。その結果、取引ジャーナルは簡単に読み取れる形式になり、時間の経過とともに追加データを容易に拡張できるようになります。

例:
input datetime start_time = D'2026.01.01 00:00:00'; // Show history from this date
input datetime end_time = D'2026.01.31 00:00:00';   // To this date
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   string arrays_filename[];
   string default_filename = "Trading_Journal.csv";

   int result = FileSelectDialog("Select a File", NULL,"CSV files|*.csv", FSD_WRITE_FILE,arrays_filename,default_filename);

   string filename;
   if(result == 0)
     {

      Print("No file selected");

     }

   else
      if(result > 0)
        {

         Print("The trading journal will be saved in ",arrays_filename[0]);

         filename = arrays_filename[0];

        }

      else
        {

         Print(GetLastError());

        }

   int handle = FileOpen(filename, FILE_WRITE|FILE_CSV|FILE_SHARE_READ|FILE_ANSI, ',');


   if(handle == INVALID_HANDLE)
     {

      Print("Error opening file for writing. Error code: ", + GetLastError());

     }

   else
      if(handle != INVALID_HANDLE)
        {

         Print("File Sucessfully Opened");

         string account_name = AccountInfoString(ACCOUNT_NAME);
         double account_balance = AccountInfoDouble(ACCOUNT_BALANCE);
         long account_login = (long)AccountInfoInteger(ACCOUNT_LOGIN);

         FileWrite(handle, "Account Name: " + account_name);
         FileWrite(handle, "Account Balnce: " + DoubleToString(account_balance));
         FileWrite(handle, "Account Login: " + IntegerToString(account_login));
         FileWrite(handle, "Start Time: ", start_time);
         FileWrite(handle, "End Time: ", end_time);
         FileWrite(handle, "Last Update: ", TimeCurrent());

         FileWrite(handle, "\nTradeID","Symbol","OrderType", "LotSize", "OpenTime","OpenPrice","StopLoss", "TakeProfit", "CloseTime", "ClosePrice"
                   ,"Profit($)","Result");

         FileClose(handle);

        }
  }

出力:

図1:ジャーナルヘッダー

解説:

このコード行では、FileWrite関数を使用して取引ジャーナルのヘッダー行を書き込んでいます。最初の引数であるHandleは、MQL5にどのファイルへ書き込むかを指示します。ハンドルが有効でなければ、プログラムはどのファイルへアクセスすべきかわからないため、必ず事前に開かれた、または作成されたファイルである必要があります。 行の最初の文字列はTradeIDで、その後の\nは改行文字を表します。これにより、ヘッダーとその前にある内容が同じ行に表示されることを防ぎます。代わりに、新しい行へ移動することで、口座情報と取引記録の部分が視覚的に分離されます。

残りの値は、取引ジャーナルの列タイトルです。具体的には、Symbol、OrderType、LotSize、OpenTime、OpenPrice、StopLoss、TakeProfit、CloseTime、ClosePrice、Profit($)、そしてResultです。これらは、それぞれの取引について記録される情報を表しています。これらすべての値をFileWriteに渡すと、プログラムは関数内で指定された順番通りに各値を書き込みます。この行は取引データ部分の最初の行となり、後続するすべての取引に構造を与えます。 簡単に言えば、この1行によってファイル内にスプレッドシートの最初の行が作成されます。プログラムやファイルを読む人は、それぞれの列見出しの下にどのようなデータが表示されるのかを理解できます。このヘッダーがなければ、ファイルには日付や数値などの生データだけが含まれることになり、読み取りや理解が難しくなります。

比喩的な説明

ファイルは、きれいに整理されたノートに例えることができます。後に書かれる内容が明確な意味を持つように、ページの上部には列見出しを書きます。例えば、生徒の成績表であれば、最初の行には「生徒名」「理科」「数学」「英語」などの項目が入ります。同様に、FileWriteを使用するこの行では、ファイルの先頭にある列ヘッダーを書き込んでいます。 同様に、FileWrite を使用している行は、ファイルの先頭の列ヘッダーを書き込んでいます。最初の値であるTradeIDによって、最初のラベルTradeIDが書き込まれ、新しい行が開始されます。残りの列を識別するために、Symbol、OrderType、LotSize、OpenTime、OpenPrice、StopLoss、TakeProfit、CloseTime、ClosePrice、Profit($)、そしてResultなどの追加の値が入力されます。

このヘッダー行によって、取引ログは整理され、読みやすくなります。後続するすべての取引は、適切な列に表示されます。このヘッダーがなければ、ファイルには未処理の日付や数値だけが含まれることになり、理解することが困難になります。最初にヘッダーを書き込むことで、ジャーナルは明確で整理され、プロフェッショナルな形式になります。

 

取引数を確認するための履歴データの反復処理

複数の取引を扱う場合、特定の期間内の取引総数は固定されていません。処理を開始する前に全取引数を把握するためには、過去の口座データをループ処理することが重要です。合計数を把握することで、各取引を安全かつ効率的に処理するループを作成できます。これにより、アルゴリズムはすべての約定を分析し、関連するデータを抽出し、対象となる期間や条件に適合するかどうかを判断できます。 これは、過去の取引が記録されたノートを確認する作業に似ています。最初は全体の記録数がわかりませんが、1行ずつ確認することで、必要な正確な記録を見つけ出すことができます。これにより、プログラム上で何も取りこぼすことなく、すべての取引が考慮されることが保証されます。また、決まった数の取引があると仮定して存在しない取引へアクセスしようとすることで発生するエラーの可能性も排除できます。

信頼性の高い取引ジャーナルを構築するための最初のステップは、取引数を把握することです。合計取引数が確定すると、各取引を個別に処理するループを構築できます。このループでは、チケット番号、銘柄、注文タイプ、ロットサイズ、エントリー・決済時刻、価格、利益、結果などの情報を収集します。この方法により、指定された期間内の関連するすべての取引が正確かつ包括的に取引ログへ含まれることが保証されます。

例:
FileWrite(handle, "Account Name: " + account_name);
FileWrite(handle, "Account Balnce: " + DoubleToString(account_balance));
FileWrite(handle, "Account Login: " + IntegerToString(account_login));
FileWrite(handle, "Start Time: ", start_time);
FileWrite(handle, "End Time: ", end_time);
FileWrite(handle, "Last Update: ", TimeCurrent());


FileWrite(handle, "\nTradeID","Symbol","OrderType", "LotSize", "OpenTime","OpenPrice","StopLoss", "TakeProfit", "CloseTime", "ClosePrice"
          ,"Profit($)","Result");

bool success = HistorySelect(start_time, end_time);

//Total Deals
int totalDeal = 0;
if(success)
  {
   for(int i = 0; i < HistoryDealsTotal(); i++)
     {
      ulong ticket = HistoryDealGetTicket(i);
      if(HistoryDealGetInteger(ticket, DEAL_ENTRY) == DEAL_ENTRY_IN)
        {
         totalDeal++;

        }

     }
  }

FileClose(handle);

解説:

プログラムはまず、指定した期間内におこなわれた履歴約定を選択することから開始します。HistorySelect()関数は、開始時刻と終了時刻を受け取り、その期間内におこなわれた口座履歴の約定だけを選択します。選択が成功したかどうかは、successという名前のbool型変数に保存されます。結果がtrueであれば処理可能な約定が存在することを意味し、falseであれば、その期間に取引が存在しないか、何らかのエラーが発生したことを意味します。これにより、プログラムは有効な履歴データのみを対象に処理を実行することが保証されます。

次に、プログラムは取引総数を記録するための変数を初期化します。この変数は最初に0へ設定されます。その後、if文を使用して履歴の選択が成功したかどうかを確認します。選択が成功していれば、プログラムはすべての約定をループ処理します。成功していなければ、この処理はスキップされます。これにより、存在しない約定へアクセスしようとすることを防げます。 次の段階では、選択された履歴データ内のすべての約定を順番に処理します。ループはインデックス0から開始し、HistoryDealsTotal()が返す約定数に到達するまで繰り返します。各反復では1件の約定を処理するため、プログラムは各約定を個別に扱うことができます。この処理により、指定した期間内の約定を漏れなく処理できます。

ループ内では、HistoryDealGetTicket(i)を使用して現在の約定の一意なチケット番号を取得します。MetaTrader 5では、すべての約定に識別子としてチケット番号が割り当てられています。このチケット番号は、ファイル管理システムで特定の記録を探すための管理番号のような役割を果たし、プログラムが対象となる約定を正確に識別し、その情報を取得できるようにします。 続いて、HistoryDealGetInteger(ticket, DEAL_ENTRY)を使用して、その約定の種類を判定します。DEAL_ENTRY_IN定数は、ポジションを新規に開始した約定であることを示します。この判定は非常に重要です。口座履歴には決済約定やその他の修正に関する約定も含まれる可能性がありますが、それらを取引数として数えたくないためです。プログラムは約定種別をDEAL_ENTRY_INと比較し、本当にポジションを開始した約定だけを集計対象にします。

最後に、この条件を満たした場合、プログラムは総取引数を1増やします。ループ終了時には、totalDeal変数に指定期間内のエントリー約定数が保存されています。この値は、その後の処理、例えば取引を取引ジャーナルへ記録したり、各取引の詳細情報を取得するために再度ループ処理をおこなったりする際の基礎となります。このように最初に取引数を数えることで、プログラムは履歴データ処理の正確性を確保し、エラーを防止できます。

比喩的な説明

取引履歴を、口座でおこなわれたすべての取引が保管されている、大きな書類保管室だと考えてみてください。それぞれの箱が1件の取引を表しています。プログラムが最初におこなうのは、調べる箱を選ぶことです。HistorySelect()は助手のような役割を果たします。「1月から3月までの日付が付いた箱だけを持ってきてください」と助手へ依頼するようなものです。助手は、条件に合った箱を持ってくるか、何も見つからなければ空のまま戻ってきます。その結果はsuccess変数に保存されます。successがtrueであれば机の上には調査対象の箱があります。falseであれば調べるものが何もないため、プログラムは無駄に処理を進めません。

次に、プログラムはカウンターを用意します。これは、数え始める前にノートを開いて最初に「0」と書いておくようなものです。のカウンターは、有効な取引数を記録します。その後、プログラムは本当に助手が箱を持ってきたかどうかを確認してから数え始めます。箱がなければ数えるものがないため、無意味な処理やエラーを防ぐことができます。 箱があることを確認すると、プログラムは1箱ずつ順番に調べ始めます。最初の箱から最後の箱まで、1つずつ確認していきます。これは、机の前に立って書類を1枚ずつ確認し、何も見落とさないように調べる作業に似ています。ループの各反復は、1つの箱を取り上げて中身を確認する作業に相当します。

プログラムは、それぞれの箱に貼られたラベルを確認します。このラベルには約定チケット番号が記載されています。チケット番号は、フォルダーに書かれた管理番号やファイル番号のようなものです。各約定を一意に識別し、他の約定と混同することなく必要な情報を取得できます。 約定を特定した後、プログラムはその書類の種類を確認します。ある書類は新規取引の開始を示し、別の書類は決済や修正を示しています。DEAL_ENTRY_INの判定は、「これは取引を開始したことを示す書類ですか?」と確認しているのと同じです。「はい」と答える書類だけが集計対象になります。それ以外の書類は無視されます。

新規取引を表す書類が見つかるたびに、プログラムはカウンターを1つ増やします。すべての箱を確認し終えると、カウンターには指定した期間中に開始された取引数が記録されています。この最終的な数値は非常に重要です。なぜなら、この後に詳細情報を取得したり、取引ジャーナルへ記録したりする際に、プログラムが処理すべき取引数を把握できるからです。最初に取引数を数えることで、その後の処理を整理し、ミスを防ぐことができます。

 

取引履歴を使用してファイルのTrade ID列を埋める

指定した期間内の総取引数を取得する方法を理解したので、次のステップは、それらの約定を順番に処理し、それぞれのチケット番号を取得することです。これらのチケット番号は、各取引を一意に識別するIDとして、ファイルのTrade ID列へ書き込まれます。取引は履歴順に保存されているため、選択した期間内の最初の取引から開始し、最後の取引まで順番に処理を進めます。 この処理では、一度に1件ずつ取引チケットを取得し、ファイルへ書き込める状態にします。

これにより、すべての約定が正しく記録され、取引ジャーナル内の適切な行に対応付けられます。チケット番号を順番に取得することで、取引履歴とファイル内のデータとの明確で整理された対応関係を維持できます。また、この方法により、後から銘柄、注文タイプ、価格、利益などの追加情報を記録する際にも、ジャーナルの正確性と整理された構造を維持できます。

まず、各取引のチケット番号を保存するために、ulong型の動的配列を宣言します。MetaTrader 5では各取引が固有のチケット番号を持っているため、この配列を使用することで、ファイルへ書き込む前にすべてのTrade IDを整理して管理できるようになります。

例:
//Trade ID
ulong trade_id[];
ArrayResize(trade_id,totalDeal);
int j = 0;

if(success)
  {
   for(int i = 0; i < HistoryDealsTotal(); i++)
     {
      ulong ticket = HistoryDealGetTicket(i);


      if(HistoryDealGetInteger(ticket, DEAL_ENTRY) == DEAL_ENTRY_IN)
        {
         trade_id[j] = ticket;

         j++; // Increment the array index for the next valid ticket

        }
     }
  }

for(int i = 0; i < totalDeal; i++)
  {
   FileWrite(handle, trade_id[i]);
  }


FileClose(handle);

出力:

図2:取引ID

解説:

まず、取引チケット番号を保存するために、ulong trade_id[]という動的配列を宣言します。動的配列であるため、宣言時点では配列のサイズは決まっていません。これは、実行時まで取引数がわからない場合に便利です。ただし、この柔軟性がある一方で、値を格納する前には配列サイズを明示的に設定する必要があります。配列をリサイズせずに値を格納しようとすると、MetaTraderは「array out of range(配列の範囲外)」エラーを発生させます。これは、確保されていないメモリ領域へアクセスしようとするためです。

そのため、ArrayResize(trade_id, totalDeal);が必要になります。この関数は、事前に数えた取引総数に合わせて配列のサイズを変更し、すべての取引チケットを安全に保存できるようにします。簡単に言えば、この配列は空の棚のようなものです。配列を宣言することは棚を用意することに相当しますが、ArrayResize()によって棚の収納スペースの数を決定します。十分な収納スペースがない状態で物を置こうとすると、エラーが発生します。

プログラムが取引履歴を順番に処理し、有効なエントリー取引を見つけるたびに、その取引チケットを配列へ保存します。この処理は、DEAL_ENTRY_INを確認する条件の中でおこなわれます。これにより、新規ポジションを開始した取引だけが記録されます。ここで重要になるのがjという変数です。ループ変数iは履歴内のすべての約定を順番に処理しますが、それらすべてがエントリー取引とは限りません。jは、有効なエントリー取引が見つかった場合にのみ進む別のインデックスとして使用されます。これにより、チケット番号は配列内へ連続して保存され、途中に空きができたり、既存のデータが上書きされたりすることを防げます。 有効なチケットが追加されるたびに、jは1増加します。その結果、次の有効なチケットは次の空いている位置へ保存されます。もしjがなければ、プログラムはデータの位置をずらしてしまったり、以前に保存したチケットを上書きしたりして、不正確または欠落したジャーナルデータを作成してしまいます。

すべての取引チケットを配列へ保存した後、プログラムは取得済みの取引数を利用して配列全体をループ処理します。このループ内では、FileWrite(handle, trade_id[i]);を使用して、それぞれのチケット番号をファイルへ書き込みます。配列は適切なサイズへ変更され、正しい順序でデータが格納されているため、この処理によって各チケット番号は順番にファイルのTrade ID列へ書き込まれます。これにより、すべてのTrade IDが、整理された信頼性の高い形で取引ジャーナルへ記録されます。

比喩的な説明

の動的配列を、取引チケット番号を書き込むための空のノートだと考えてみてください。配列を宣言することは、何ページあるかまだ決めていない新しいノートを開くことに似ています。チケット番号を書き込むことは決まっていますが、何件の取引が見つかるかはまだわかりません。これが、動的配列の柔軟性です。 しかし、書き始める前には、ノートに何ページ必要かを決める必要があります。十分なページ数を用意せずに書き始めると、途中で書く場所がなくなり問題が発生します。そのため、配列のリサイズが必要になります。先ほど数えた取引総数を利用して配列をリサイズすることは、ノートに必要なページ数をあらかじめ用意することと同じです。これにより、用意したスペースを超えて書き込もうとして発生するエラーを防げます。

プログラムは取引履歴を調べる中で、多くの書類を見つけますが、そのすべてが必要なものではありません。ある書類は新規取引を示し、別の書類は決済や修正を示しています。プログラムが有効な新規取引を見つけるたびに、そのチケット番号をノートの次の空白ページへ書き込みます。ここでj変数はページしおりの役割を果たします。次に有効なチケットを書き込むべきページを記録しているのです。このしおりがなければ、プログラムは以前のページへ上書きしたり、途中に空白ページを作ったりしてしまい、ノートは整理されていない信頼性の低いものになってしまいます。

すべての有効なチケット番号を書き終えると、プログラムはノートを最初のページから最後のページまで順番に読み取ります。そして、それぞれのチケット番号を1行ずつファイルのTrade ID列へ書き込みます。ノートは最初から適切なページ数で用意され、正しい順番で記録されているため、ファイルには整理された完全なTrade ID一覧が作成されます。。その結果、取引ジャーナルは正確で読みやすくなり、次のステップでさらに詳しい取引情報を追加する準備が整います。

 

ファイルのSymbol列とOrder Type列を埋める

Trade IDの値を正しくファイルへ書き込んだら、次はSymbol列とOrder Type列を埋めます。これら2つの情報は、何を取引したのか、そしてどのような種類の取引だったのかを示すため、非常に重要です。Trade IDは各取引を一意に識別し、Symbolは取引対象の市場を示します。一方、Order Typeは、その取引が買い(Buy)なのか売り(Sell)なのかを示します。 これらの情報を取得するために、先ほど選択した同じ取引履歴データを使用します。各取引のチケット番号はすでに取得しているため、それを利用して、その取引に関する追加情報を取得できます。Symbolは取引レコードから直接取得されるため、その取引がEURUSD、GBPUSD、あるいは口座で利用可能なその他の金融商品でおこなわれたかを判別できます。

Order Typeは、取引の方向を示し、その取引が買いであったのか売りであったのかを表します。この区別は、取引履歴を分析する際に重要な情報となります。これにより、ロングポジションとショートポジションのパフォーマンスを比較したり、市場に対する自分の取引傾向を把握したり、取引戦略全体を改善したりすることが可能になります。

例:

ulong trade_id[];
ArrayResize(trade_id,totalDeal);

string symbol[];
string order_type[];
ArrayResize(symbol,totalDeal);
ArrayResize(order_type,totalDeal);
int j = 0;

if(success)
  {
   for(int i = 0; i < HistoryDealsTotal(); i++)
     {
      ulong ticket = HistoryDealGetTicket(i);


      if(HistoryDealGetInteger(ticket, DEAL_ENTRY) == DEAL_ENTRY_IN)
        {
         trade_id[j] = ticket;

         symbol[j] =  HistoryDealGetString(ticket, DEAL_SYMBOL);

         if(HistoryDealGetInteger(ticket,DEAL_TYPE) == DEAL_TYPE_BUY)
           {
            order_type[j] = "BUY";
           }

         if(HistoryDealGetInteger(ticket,DEAL_TYPE) == DEAL_TYPE_SELL)
           {
            order_type[j] = "SELL";
           }

         j++; // Increment the array index for the next valid ticket

        }
     }
  }

for(int i = 0; i < totalDeal; i++)
  {
   FileWrite(handle, trade_id[i],symbol[i],order_type[i]);
  }
FileClose(handle);

出力:

図3:銘柄と注文タイプ

解説:

この処理の最初では、銘柄名と注文タイプの情報を保存するために、2つの動的な文字列配列を宣言します。取引数は選択した履歴によって変化するため、これらの配列には最初から固定されたサイズはありません。総取引数が判明した時点で、その数に合わせて配列をリサイズします。ArrayResizeを使用して必要なメモリを確保することで、各取引の銘柄と注文タイプを安全に保存できるようになり、array out of range(配列の範囲外)エラーの発生を防ぐことができます。

配列の準備ができたら、プログラムは各取引の銘柄を取得します。この処理には、DEAL_SYMBOL属性を指定したHistoryDealGetStringを使用します。銘柄(EURUSDやGBPUSDなど)は文字列として保存されているため、文字列を取得する関数が必要になります。HistoryDealGetStringを使用することで、プログラムは取引レコードから読みやすい文字列を直接取得できます。取得した銘柄は、Trade IDやその他の取引情報との対応関係を維持するため、対応するインデックス位置のsymbol配列へ保存されます。

次に、プログラムは各取引の注文タイプを判定します。取引方向は、内部的には整数値として保存されています。DEAL_TYPEフィールドを指定したHistoryDealGetIntegerを使用することで、その約定が買いなのか売りなのかを判定できます。その後、この内部値は条件分岐によって人が読みやすい形式へ変換されます。取引タイプがDEAL_TYPE_BUYと一致した場合、プログラムはorder_type配列へ"BUY"という文字列を保存します。一方、DEAL_TYPE_SELLと一致した場合は、"SELL"という文字列を保存します。

この条件分岐は非常に重要です。プラットフォームが返す値は、画面表示を目的としたものではなく、プログラム内部で使用する定数だからです。もしこの変換をおこなわなければ、ファイルにはDEAL_TYPE_BUYやDEAL_TYPE_SELLのような内部定数、あるいはそれに対応する数値が出力される可能性があり、取引ジャーナルとしては読みづらくなってしまいます。これらの内部定数を単純なBUYやSELLという文字列へ変換することで、ファイルの可読性が向上し、より整理されたプロフェッショナルな取引ジャーナルとなり、分析もしやすくなります。


取引のエントリー情報をデータへ記録する

Trade ID、Symbol、Order Typeを書き込んだ後は、各取引のエントリー情報をファイルへ記録します。これには、Lot Size、Open Time、Open Price、Stop Loss、Take Profitなどの情報が含まれます。これらの情報を取得することは非常に重要です。なぜなら、各取引がどのように実行されたのかを包括的に把握できるからです。具体的には、ポジションサイズ、エントリーした時刻、価格、そしてリスク管理の設定内容を確認できます。 これらの情報は、配列に保存されている各取引チケットを利用して、口座履歴から直接取得できます。Lot Sizeは取引数量を示し、Open TimeとOpen Priceは取引が開始された時刻と価格を表します。Stop LossとTake Profitは、リスク管理および想定利益を管理するために設定された価格レベルを示します。これらの値を記録することで、取引ジャーナルには各取引に関する重要なエントリー情報が漏れなく保存されます。

例:

ulong trade_id[];
ArrayResize(trade_id,totalDeal);

string symbol[];
string order_type[];
ArrayResize(symbol,totalDeal);
ArrayResize(order_type,totalDeal);

double lot_size[];
datetime open_time[];
double open_price[];
double stop_l[];
double take_p[];

ArrayResize(lot_size,totalDeal);
ArrayResize(open_time,totalDeal);
ArrayResize(open_price,totalDeal);
ArrayResize(stop_l,totalDeal);
ArrayResize(take_p,totalDeal);

int j = 0;

if(success)
  {
   for(int i = 0; i < HistoryDealsTotal(); i++)
     {
      ulong ticket = HistoryDealGetTicket(i);


      if(HistoryDealGetInteger(ticket, DEAL_ENTRY) == DEAL_ENTRY_IN)
        {
         trade_id[j] = ticket;

         symbol[j] =  HistoryDealGetString(ticket, DEAL_SYMBOL);

         if(HistoryDealGetInteger(ticket,DEAL_TYPE) == DEAL_TYPE_BUY)
           {
            order_type[j] = "BUY";
           }

         if(HistoryDealGetInteger(ticket,DEAL_TYPE) == DEAL_TYPE_SELL)
           {
            order_type[j] = "SELL";
           }

         lot_size[j] = HistoryDealGetDouble(ticket,DEAL_VOLUME);
         open_time[j] = (datetime)HistoryDealGetInteger(ticket,DEAL_TIME);
         open_price[j]  = HistoryDealGetDouble(ticket,DEAL_PRICE);
         stop_l[j] = HistoryDealGetDouble(ticket,DEAL_SL);
         take_p[j] = HistoryDealGetDouble(ticket,DEAL_TP);

         j++; // Increment the array index for the next valid ticket

        }
     }
  }

for(int i = 0; i < totalDeal; i++)
  {
   FileWrite(handle, trade_id[i],symbol[i],order_type[i],lot_size[i],open_time[i],open_price[i],stop_l[i],take_p[i]);
  }

FileClose(handle);

出力:

図4:取引入力データ

解説:

Lot Size、Open Time、Open Price、Stop Loss、Take Profitを整理して保存するために、それぞれ専用の動的配列を作成します。各配列は、保存するデータの種類に対応したデータ型で宣言します。価格やロット数は小数値を正確に扱う必要があるためdouble型で宣言し、Open Timeは時刻データを正しく表現するためdatetime型を使用します。これらの配列を動的配列として宣言することで、事前に正確な取引数がわからなくても、指定した期間内の任意の数の取引を処理できるようになります。

動的配列であっても、使用する前にはメモリ領域を確保する必要があります。そのため、それぞれの配列に対してArrayResizeを使用し、サイズを先ほど数えた取引数であるtotalDealに設定します。配列をリサイズすることで、各取引の値を保存するために十分なメモリが確保されます。この処理をおこなわなければ、プログラムは割り当てられていないメモリへ書き込もうとするため、取引情報を保存する際にarray out of range(配列の範囲外)エラーが発生します。例えば、配列を宣言することは空の箱を用意することに似ています。そして、配列をリサイズすることで、実際に必要な箱の数をプログラムへ伝えることになります。 配列の準備が完了すると、プログラムは有効な各取引チケットに対応する取引情報を取得します。

ロットサイズはHistoryDealGetDouble(ticket, DEAL_VOLUME)を使用して取得し、lot_size配列へ保存します。Open TimeはHistoryDealGetInteger(ticket, DEAL_TIME)で取得し、datetime型へ変換した後、open_time配列へ保存します。Open PriceはHistoryDealGetDouble(ticket, DEAL_PRICE)で取得します。Stop LossはHistoryDealGetDouble(ticket, DEAL_SL)を使用して取得し、Take ProfitはHistoryDealGetDouble(ticket, DEAL_TP)で取得します。これらの値はすべて、対応する配列の現在のインデックスjへ保存されます。これにより、各取引のエントリー情報が対応する取引チケットと正しく対応付けられます。 

これらの配列を使用することで、プログラムは数値データをファイルへ書き込む前に、一度メモリ上へ保存できます。最初にデータを収集しておくことで、後から各取引を順番に処理し、適切な項目へ情報を書き込むことができます。この方法により、ファイルの構造を整理したまま、すべての取引についてエントリー情報を漏れなく、正しい順序で記録できます。

 

ファイルへ取引の決済情報を書き込む

取引のエントリー情報を記録した後は、決済情報を追加するのが自然な流れです。各取引記録を完全なものにするため、Close Time、Close Price、Profit、Resultを記録します。これらの情報は、取引の最終結果を示し、パフォーマンスを分析するために必要な情報を提供します。エントリー情報と決済情報の両方を記録することで、トレーダーは取引の一貫性を評価し、自身の戦略の長所と短所を把握し、口座残高の推移を正確に追跡できるようになります。

プログラムは、配列に保存されている取引チケットを利用して、口座履歴から決済情報を直接取得できます。Close Timeは取引が決済された正確な時刻を示し、Close Priceはポジションが決済された価格を記録します。Profitはその取引による損益を表し、Result列には、その取引が利益となったのか、損失となったのか、あるいは損益ゼロで終了したのかが表示されます。これらの値を取得して記録することで、各取引にはエントリー情報と決済情報の両方が含まれるようになり、取引ジャーナルは分析に適した完全な記録となります。

例:
datetime close_time[];
double close_price[];
double profit[];
string result[];

ArrayResize(close_time,totalDeal);
ArrayResize(close_price,totalDeal);
ArrayResize(profit,totalDeal);
ArrayResize(result,totalDeal);

int j = 0;
int h = 0;

if(success)
  {
   for(int i = 0; i < HistoryDealsTotal(); i++)
     {
      ulong ticket = HistoryDealGetTicket(i);

      if(HistoryDealGetInteger(ticket, DEAL_ENTRY) == DEAL_ENTRY_IN)
        {
         trade_id[j] = ticket;

         symbol[j] =  HistoryDealGetString(ticket, DEAL_SYMBOL);

         if(HistoryDealGetInteger(ticket,DEAL_TYPE) == DEAL_TYPE_BUY)
           {
            order_type[j] = "BUY";
           }

         if(HistoryDealGetInteger(ticket,DEAL_TYPE) == DEAL_TYPE_SELL)
           {
            order_type[j] = "SELL";
           }

         lot_size[j] = HistoryDealGetDouble(ticket,DEAL_VOLUME);
         open_time[j] = (datetime)HistoryDealGetInteger(ticket,DEAL_TIME);
         open_price[j]  = HistoryDealGetDouble(ticket,DEAL_PRICE);
         stop_l[j] = HistoryDealGetDouble(ticket,DEAL_SL);
         take_p[j] = HistoryDealGetDouble(ticket,DEAL_TP);

         j++; // Increment the array index for the next valid ticket

        }

      if(HistoryDealGetInteger(ticket, DEAL_ENTRY) == DEAL_ENTRY_OUT)
        {
         trade_id[h] = ticket;
         close_time[h] = (datetime)HistoryDealGetInteger(ticket,DEAL_TIME);
         close_price[h]  = HistoryDealGetDouble(ticket,DEAL_PRICE);
         profit[h] = HistoryDealGetDouble(ticket,DEAL_PROFIT);

         if(profit[h] > 0)
           {

            result[h] = "WIN";

           }
         else
            if(profit[h] < 0)
              {

               result[h] = "LOSS";

              }
            else
              {

               result[h] = "Break Even";

              }

         h++;
        }
     }
  }

for(int i = 0; i < totalDeal; i++)
  {
   FileWrite(handle, trade_id[i],symbol[i],order_type[i],lot_size[i],open_time[i],open_price[i],stop_l[i],take_p[i],close_time[i],close_price[i],profit[i]
             ,result[i]);
  }
FileClose(handle);

出力:

図5:取引終了データ

解説:

まず、記録する4つの決済情報、すなわちResult、Profit、Close Time、Close Priceを保存するための動的配列を作成します。Close Timeは取引が決済された正確な時刻を記録するため、datetime型で保存します。ProfitとClose Priceは数値であるためdouble型を使用し、Resultは"WIN"、"LOSS"、"Break Even"といった文字列を保存できるようにstring型で宣言します。 取引数は事前にわからないため、これらの配列はすべて動的配列として宣言します。

次に、それぞれの配列に対してArrayResizeを使用し、先ほど数えた取引数であるtotalDealをサイズとしてメモリを確保します。これにより、各取引の決済情報を安全に保存できる十分な領域が確保されます。配列をリサイズすることで、配列の範囲外へ書き込もうとして発生するエラーを防ぐこともできます。これは、各取引の決済情報を保存するためのラベル付きの箱を必要な数だけあらかじめ用意しておくようなものです。

その後、変数hを0で初期化します。この変数は、決済情報を配列へ保存する際のインデックスとして使用されます。決済を表す取引の情報を現在のh番目の位置へ保存した後、hを1増やして次の保存位置へ進みます。これは非常に重要です。履歴配列内のすべての約定が決済取引であるとは限らないからです。すでにエントリー情報として処理済みのDEAL_ENTRY_INも含まれています。もし専用のインデックスhを使用しなければ、決済情報の配列に空きができたり、以前に保存したデータを上書きしてしまったりする可能性があります。 

プログラムは、HistoryDealGetInteger(ticket, DEAL_ENTRY)を使用して各約定の種類を確認し、DEAL_ENTRY_OUTとして記録されている約定だけを処理します。DEAL_ENTRY_INで示される新規エントリー取引は、すでにエントリー情報の配列へ保存されています。このようにエントリー取引と決済取引を区別することで、取引記録の正確性と整理された構造が維持され、この段階では決済情報だけが処理されます。

各決済取引について、プログラムはHistoryDealGetInteger(ticket, DEAL_TIME)を使用して決済時刻を取得し、datetime型へ変換します。決済価格はHistoryDealGetDouble(ticket, DEAL_PRICE)で取得し、利益はHistoryDealGetDouble(ticket, DEAL_PROFIT)で取得します。これらの値は、それぞれ対応する配列のh番目の位置へ保存されます。 最後に、プログラムは取引結果を判定します。利益が0より大きければ、Resultには"WIN"を記録します。利益が0より小さければ、"LOSS"を記録します。利益がちょうど0であれば、"Break Even"を記録します。この処理により、数値として保存されている損益を、人が一目で理解できる取引結果へ変換できます。そのため、取引ジャーナルを素早く確認したり分析したりすることが容易になります。 

この処理が完了すると、決済情報の配列には、すべての取引の決済情報が正しい順序で保存され、対応する取引と正しく関連付けられています。このような段階的な方法により、取引ジャーナル内のすべての取引には必要なエントリー情報と決済情報の両方が揃い、整理された分かりやすい取引記録としてファイルへ書き込む準備が整います。

 

結論

本記事では、Trade ID、Symbol、Order Type、Lot Size、Open Time、Open Price、Stop Loss、Take Profit、Close Time、Close Price、Profit、Resultを含む、取引のエントリー情報と決済情報の両方を記録することで、MQL5で取引データファイルを扱う方法について学びました。また、動的配列を使用してデータを保存する方法、口座履歴から取引情報を取得する方法、そして正確性と整理された構造を維持しながら、それらを順番にファイルへ書き込む方法についても解説しました。これらの手順を実践することで、MQL5におけるファイル管理の基礎を構築し、後から取引内容を確認・分析できるよう、重要な取引データを整理して保存する、見やすく使いやすい取引ジャーナルを作成できました。

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

添付されたファイル |
EAのサンプル EAのサンプル
一般的なMACDを使ったEAを例として、MQL4開発の原則を紹介します。
Market Memory Zonesインジケーターの自動化:価格が戻りやすい領域 Market Memory Zonesインジケーターの自動化:価格が戻りやすい領域
Market Memory Zonesをチャート上の概念から、完全なMQL5エキスパートアドバイザー(EA)へ発展させます。ディスプレイスメントゾーン、市場構造転換 (CHoCH)ゾーン、流動性スイープゾーンの検出をATRおよびローソク足構造フィルタによって自動化し、下位足での確認を適用するとともに、リスクベースのポジションサイズ計算、動的なストップロス(SL)、そして市場構造に基づくテイクプロフィット(TP)を実装します。また、検出、エントリー、トレード管理、可視化のコード構成に加え、バックテスト結果も簡潔に確認します。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
MQL5経済指標カレンダーを用いたニュースフィルタリング(第1回):MQL5におけるニュース前・ニュース後ウィンドウの実装 MQL5経済指標カレンダーを用いたニュースフィルタリング(第1回):MQL5におけるニュース前・ニュース後ウィンドウの実装
本システムでは、Webリクエストや外部DLLを使用せず、MQL5のみでカレンダー連動型ニュースフィルタを構築します。第1回は、経済イベントの読み込みとキャッシュ処理、通貨を基準とした銘柄へのイベント紐付け、重要度レベルによるフィルタリング、ニュース前後の監視時間(ウィンドウ)の設定、そしてニュース発生中の新規トレード制限について解説します。また、必要に応じてニュース前に保有ポジションを決済する機能も実装します。この結果、不要な取引停止を減らし、ボラティリティが高まる局面でのエントリーを保護する、設定可能でプロップファーム向けにも適したリスク管理機能を実現できます。