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

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

MetaTrader 5インディケータ |
21 2
ALGOYIN LTD
Israel Pelumi Abioye

はじめに

連載「MQL5入門」の第42回へようこそ。多くのトレーダーは、決済済み取引の損益(P/L)データを含むCSVファイルとして取引履歴を保存しています。しかし、その分析は表形式のままおこなわれることがほとんどです。Excelで開く場合でも、生のCSVファイルを直接閲覧する場合でも、データは静的で断片的な情報にとどまります。個々の取引結果は確認できますが、残高推移、ドローダウンの変化、あるいは各取引が残高推移に与えた影響を、MetaTrader 5上で直感的に把握することはできません。情報自体は存在していても、分析をおこなう取引環境に視覚的に統合されていないのです。

MetaTrader 5のストラテジーテスターには、残高曲線や各種パフォーマンス統計を表示する機能があります。しかし、これらの結果はテスト対象となるエキスパートアドバイザー(EA)の実行結果のみに基づいています。そのため、特に手動取引を含む実口座で実行された実際の取引を反映することはできません。前回の記事では、MetaTrader 5から完全な取引履歴をCSVファイルとしてエクスポートする方法と、そのCSVファイルをMQL5で読み込む方法について解説しました。また、Profit($)列や決済済み取引数などの主要な項目を抽出し、動的配列へ格納して、後続の処理で利用できるようデータを整理しました。この基盤が整ったことで、今度は生の取引履歴データをMetaTrader 5上で活用できる実用的な分析ツールへと発展させることができます。

この記事を読み終える頃には、MetaTrader 5用の実用的なMQL5インジケータが完成しています。このインジケータは、CSV形式の取引履歴ファイル(前回の記事で作成したサンプルファイルを使用し、本記事にも添付します)を読み込みます。また、そのCSVファイルをMetaTrader 5から参照できるように配置する場所についても説明します。インジケータはCSVからProfit($)の値を抽出し、それらを基に累積残高曲線を計算します。算出された曲線はサブウィンドウに描画され、Y軸は累積残高の最小値と最大値に応じて自動的にスケーリングされます。

さらに、データとの位置関係を分かりやすくするために、水平軸と垂直軸を描画します。また、入力パラメーターにより、各取引の利益・損失を数値ラベルとしてチャート上に表示するかどうかを切り替えることができます。OnInitおよびOnTimerイベントハンドラを利用することで、CSVファイルの内容を定期的に監視し、新しい取引が追加された場合にのみ累積残高曲線を更新・再描画する仕組みを実装します。これにより、静的なCSVデータを、口座パフォーマンスを視覚的に分かりやすく、検証もしやすい形で確認できる、実用的な分析ツールへと変換できます。

 

プロジェクトの概要

実際に実装へ取り掛かる前に、これから作成するものの目的と、扱うデータの種類を正確に理解することが重要です。このプロジェクトの目的は、CSVファイルに保存された取引データを使用して、残高曲線を可視化することです。これを適切に実現するためには、まずプロジェクト全体の概要と、CSVファイルの構造を理解する必要があります(サンプルファイルは本記事に添付されています)。

残高曲線の理解

インジケータの実装に入る前に、残高曲線とは何か、そしてそれが取引においてなぜ重要なのかを理解することが重要です。残高曲線とは、取引口座の成長または減少を時間の経過とともにグラフで表したものです。これは、決済済み取引の利益と損失の累積結果をプロットすることで作成されます。個々の取引結果を個別に分析するのではなく、それらを積み重ねることで、口座全体のパフォーマンスの推移を示します。簡単に言えば、それぞれの取引で発生した利益または損失を、前回までの累積値に順番に加算していきます。利益が出た取引では曲線は上昇し、損失で終わった取引では曲線は下降します。このように各取引結果を継続的に累積することで、口座残高の推移を表す曲線が作成されます。

残高曲線は、トレーダーにとって重要な疑問に答えるための指標です。戦略は一貫して利益を上げているのか。長期間にわたるドローダウンは存在するのか。パフォーマンスは安定しているのか、それとも大きく変動しているのか。一般的に、滑らかに右肩上がりとなる曲線は安定した成長を示します。一方で、大きく急激に下落する曲線は、不安定なパフォーマンスやリスク管理上の問題を示している可能性があります。また、残高とエクイティ(有効証拠金)の違いを理解することも重要です。残高は決済済み取引のみを反映した口座残高を指します。一方、エクイティには未決済ポジションの含み損益も含まれます。本記事で作成する残高曲線は、決済済み取引のみを含む履歴ファイルからデータを抽出するため、実現済みP/Lのみを基に作成されます。

ファイルに保存されたP/Lの履歴から残高を可視化することで、生の数値データを分かりやすいパフォーマンスの推移として表現できます。数字が並んだ表を一行ずつ確認する代わりに、口座が時間とともに成長しているのか、減少しているのか、それとも停滞しているのかを一目で把握できます。このインジケータの目的は単に統計情報を表示することではなく、口座パフォーマンスを分析しやすい形で意味のある可視化をおこなうことにあるため、この考え方を理解しておくことが重要です。

図1:Profit($)列

このプロジェクトでは、チャート上にデータをどのように表示するかを選択できます。残高曲線だけでなく、各取引の利益・損失金額もチャート上に直接表示できます。これにより、それぞれの決済済み取引が口座全体のパフォーマンスにどのような影響を与えたのかを簡単に確認できます。

たとえば、曲線上の各ポイントで実際の利益または損失額を確認したい場合があります。これは、戦略の挙動を分析したり、ドローダウンや回復局面を詳しく調査したりする際に非常に役立ちます。

図2:残高曲線

一方で、よりシンプルで見やすい表示を好む場合もあります。その場合は、各取引のP/Lを表示せず、残高曲線のみを表示することもできます。この表示方法では余分な情報がなくなるため、全体的な増減傾向に集中しやすくなります。

図3:残高曲線インジケータ

残高曲線の解釈

インジケータウィンドウに残高曲線を表示した後は、その曲線が取引口座について何を示しているのかを理解することが重要です。曲線上の各ポイントはCSVファイル内の1件の決済済み取引に対応しており、その時点までの累積口座残高を表しています。曲線が上昇するか下降するかは、その取引の利益または損失によって決まります。利益で終わった取引では曲線は上昇し、損失で終わった取引では曲線は下降します。 曲線の動きや傾きからは、パフォーマンスの傾向を直感的に把握できます。継続的に右肩上がりの曲線は、一連の利益取引と安定した成長を示しています。一方で、下向きの部分は損失を表しています。また、上昇や下降の傾きが急であるほど、利益やドローダウンの規模が大きいことを意味します。

ドローダウンは、それ以前の高値からその後の安値までの垂直方向の差として表されます。最大ドローダウンとは、取引期間全体を通して、高値から底値までの最も大きな下落幅を指します。これは口座が回復するまでに経験した最大の損失を示しており、取引戦略のリスクを評価するうえで重要な指標です。不利な相場環境において口座がどの程度減少する可能性があるのかを把握することができます。また、曲線上の各点は個々の取引と対応しているため、どの取引が最大ドローダウンの原因となったのかも確認できます。

図4:ドローダウン

また、曲線を見ることで、連続した勝ちトレードと連続した負けトレードの最大回数も把握できます。連続損失とは、損失で終了した取引が連続する期間を指し、戦略に負荷がかかっている局面を特定するのに役立ちます。一方、連続利益とは、利益で終了した取引が続く期間を指し、高いパフォーマンスや勢いが続いている場面を示します。これらの両方を観察することで、取引手法の安定性や変動性についてより深く理解できます。 さらに、曲線上に表示されるP/Lの数値ラベルによって、この内容はさらに分かりやすくなります。各取引の実際のP/Lを表示することで、どの取引が累積残高に最も大きな影響を与えたのか、また特定の箇所で曲線が上昇または下降した理由を簡単に確認できます。

この曲線を利用することで、全体的な収益性を評価し、リスクの高い期間を特定し、取引戦略がどの程度口座残高の成長につながっているかを判断できます。最大ドローダウンや連続勝敗は、リスクと一貫性を評価するための具体的な指標となり、残高曲線はCSVデータを実際の取引実績を視覚的かつ実用的に表現したものへと変換します。 このことは、曲線上に数値のP/Lラベルを追加することでさらに明確になります。各取引の実際のP/Lを表示することで、どの取引が累積残高に最も大きな影響を与えたのか、また特定の箇所で曲線が上昇または下降した理由を容易に把握できます。 この曲線を利用することで、全体的な収益性を評価し、リスクの高い期間を特定し、取引機会をどの程度安定した口座成長につなげられているかを判断できます。残高曲線は、生のCSVデータを、口座パフォーマンスを明確かつ検証しやすい形で可視化できます。

ファイル構造

MQL5で外部データを扱う最初のステップは、ファイル構造を理解することです。CSVファイルには決められた数の列とヘッダーがあり、プログラムのロジックはその構造に完全に依存します。

本記事で使用するサンプルファイルには、12個の列と、それに対応する12個のヘッダーがあります。それぞれの列には、時刻、シンボル、注文種別、ロット数、エントリー時刻、利益、結果など、特定の取引情報が格納されています。

図5:ファイル構造

前回の記事では、CSVデータを配列へ読み込む際のインデックスの仕組みについて説明しました。各値は行内の位置に基づいて格納され、インデックスを使ってアクセスします。そのため、列の並び順は変更しない必要があります。

残高曲線を作成する目的において必要なのは、Profit($)列だけです。必要なデータはすべて、このヘッダーの下にある値から取得します。Profit値を対応するインデックス位置から抽出し、それらを累積加算することで残高曲線を計算できます。列の順序が変更されたり、Profit($)列が別の位置へ移動したりすると、インデックスは正しいデータを参照できなくなり、プログラムは意図したとおりに動作しなくなります。

MQL5プログラムからファイルへアクセスする方法

このセクションでは、必要なファイルを適切なフォルダへ配置する方法と、その重要性について説明します。本記事ではCSVファイルを扱います。説明のために、本記事に添付されているCSVファイルを使用するものとします。MQL5プログラムからこのファイルへアクセスできるようにするには、用途が異なる2種類のディレクトリのいずれかへ配置する必要があります。

1つ目は、ターミナル専用のMQL5\Filesフォルダです。このフォルダは現在使用しているMetaTrader 5ターミナル専用であり、プログラム固有のデータを保存する場所です。多くの環境では次のようなパスになります。

C:\Users\Dell\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Files

ここでD0E8209F77C8CF37AD8BF550E51FF075は、私のPCにインストールされているMetaTrader 5ターミナル固有の識別子です。このフォルダへ保存されたファイルは、その特定のターミナルからのみアクセスできます。そのため、他のターミナルとの競合を防ぎ、プログラムが常に目的のファイルを見つけられるようになります。プログラムが定期的にファイルへアクセスする必要がある場合は、この方法が最も安全です。

2つ目は、すべてのMetaTrader 5ターミナルで共有される共通フォルダです。通常は次の場所になります。

C:\Users\Dell\AppData\Roaming\MetaQuotes\Terminal\Common\Files

このフォルダに保存されたファイルは、同じコンピュータ上にインストールされているすべてのMetaTrader 5ターミナルからアクセスできます。複数のターミナル間でデータを共有する場合には便利ですが、複数のプログラムが同時に同じファイルを読み書きしようとすると問題が発生する可能性があります。単一のプログラムで作業する場合は、一般的にターミナル専用のMQL5\Filesフォルダを使用することが推奨されます。このフォルダへCSVファイルを配置することで、プログラムは常に確実にファイルへアクセスでき、ファイルも他のターミナルとは分離された状態で整理できます。

本記事では、追加のフラグや特別な設定をおこなうことなくCSVファイルへアクセスできるように、CSVファイルをターミナル専用のMQL5\Filesフォルダへ配置してください。これにより、セットアップがシンプルになり、サンプルを実行する際の不要な混乱も防げます。もちろん、実際には、複数のMetaTrader 5環境でファイルをどのように利用・管理したいかに応じて、共通フォルダとターミナル専用フォルダのどちらを使用するかを選択できます。

インジケータウィンドウへのオブジェクトの描画

まず、ループを使用してファイル内からProfit($)ヘッダーを検索します。前述のとおり、各取引レコードは12列で構成されているため、利益列の開始位置を特定した後は、インデックスを12ずつ増やしていくことで各利益値を確実に取得できます。さらに、別のループを使用して、ファイル内に保存されている「Total Trades:」の項目も検索します。これにより、有効な取引データのみを取得し、実行済み取引の総数を計算できます。Profit($)列に含まれるすべての利益値を取得した後、それらを格納する配列を作成します。続いて、それらの値を累積加算し、時間の経過に伴う残高推移を表す累積結果を計算します。この累積計算が残高曲線の基礎となります。

累積値の計算が完了すると、インジケータウィンドウ内での最小値と最大値を計算します。これにより、残高曲線全体がサブウィンドウ内に適切なスケールで表示されます。インジケータの初期化時には、OnInit()イベントハンドラからこのデータ処理ルーチンを呼び出してデータを読み込みます。また、OnTimer()イベントハンドラからも同じ処理を呼び出すことで、一定間隔ごとにインジケータを自動更新できるようになります。

最後に、OnCalculate()でトレンドラインやテキストラベルなどのグラフィカルオブジェクトを描画します。OnCalculate()はチャートのバー情報へアクセスできるため、オブジェクトを正しい位置へ配置するために必要です。また、パフォーマンス向上のため、現在の取引総数が前回記録した値と異なる場合にのみOnCalculate()が描画処理を実行する条件を追加します。これにより、不要な再描画を防ぎ、処理効率を向上させます。

 

MQL5でCSVファイルを開く

このセクションでは、MQL5でCSVファイルを開く方法について説明します。今回は、前回の記事でイベントハンドラ内から直接ファイルを開いていた方法とは異なり、より体系的で効率的な方法を採用します。ファイルをOnCalculate()内で開くのではなく、専用の関数で開きます。この設計により、ティックが発生するたびにファイルを開く必要がなくなり、パフォーマンスの低下や不要なリソース消費を防ぐことができます。

この関数は、CSVファイルを開く処理、ファイルハンドルの検証、そしてデータを処理できる状態に準備することを含め、ファイルに関するすべての処理を担当します。これらのロジックを1つの関数にまとめることで、コードの可読性が向上し、モジュール化が進み、保守もしやすくなります。この関数は、インジケータの初期化時にファイルを読み込むため、OnInit()イベントハンドラ内から呼び出されます。また、OnTimer()イベントハンドラからも呼び出されます。タイマーを利用することで、毎回のマーケットティックごとではなく、あらかじめ設定した一定間隔でファイルを再読み込みできるため、ファイル内容の変更を定期的に反映できます。

一方、OnCalculate()イベントハンドラは、グラフィカルオブジェクトの描画のみを担当します。さらに効率を高めるための条件を追加し、前回計算した取引総数と現在の取引総数が異なる場合にのみ、OnCalculate()が描画処理を実行するようにします。これにより、新しい取引データが検出された場合にのみインジケータが更新され、不要な再計算やオブジェクトの再描画を防ぐことができます。

#property indicator_separate_window

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {

//--- create timer
   EventSetTimer(60);

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {

//--- destroy timer
   EventKillTimer();
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int32_t &spread[])
  {
//---
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {

  }
//+-----------------------------------------------------------------+
//| Balance curve function                                           |
//+------------------------------------------------------------------+
void BalanceCurve(string Filename)
  {

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

        }
  }

解説

最初の重要な設定はIndicator_separate_windowです。この設定は、MetaTraderに対して、インジケータをメインチャートとは別のウィンドウへ表示するよう指示します。今回扱うのは価格データではなく口座のパフォーマンスデータであるため、可視化結果とローソク足チャートを分けて表示する方が適しています。残高曲線は、市場価格の値動きではなく、時間の経過に伴う累積P/Lを表します。 そのため、別ウィンドウに表示することで、チャートを見やすく保ち、パフォーマンス分析も理解しやすくなります。この設定を使用しない場合は、残高曲線が価格チャート上へ直接表示されることになり、価格データと口座パフォーマンスが重なってしまうため、両方の情報を読み取りにくくなる可能性があります。別ウィンドウを使用することで、視認性とレイアウトの整理が向上します。

「void BalanceCurve(string Filename)」という関数宣言では、戻り値を持たないBalanceCurve関数を定義しています。voidキーワードは、この関数が内部で処理を実行するものの、呼び出し元へ値を返さないことを意味します。この関数は、CSVファイル名を表す「string Filename」という1つの引数を受け取ります。ファイル名をコード内へ固定するのではなく引数として渡すことで、関数の柔軟性と再利用性が向上します。つまり、コアとなる処理を変更することなく、異なるCSVファイルに対して同じ関数を利用できます。

関数内では、渡されたファイル名を使用して、CSV形式・ANSIエンコーディング・読み取り専用モードでファイルを開こうとし、その結果をfile_handleという変数へ保存します。MQL5では、ファイルを正常に開くことができると、システムからファイルハンドルと呼ばれる整数値が返されます。このファイルハンドルは、そのファイルを識別するための一意の識別子として機能し、プログラムはこのハンドルを利用してファイルを読み込んだり、その他のファイル操作を実行したりできます。何らかの理由でファイルを開けなかった場合は、無効なハンドルが返され、処理が失敗したことを示します。

最初の引数であるFilenameは、プログラムへどのファイルを開くかを指定します。MQL5はデフォルトで、ターミナル専用のMQL5\Filesフォルダ内からファイルを検索します。このフォルダは現在実行中のMetaTrader 5ターミナルに属しており、そのディレクトリ内へ保存されたファイルは追加設定なしで直接アクセスできます。インジケータ専用のデータを扱う場合、この場所を使用することが推奨されています。

2番目の引数には、ファイルアクセスフラグを指定します。これらのフラグは、ファイルをどのように開き、どのように処理するかを定義します。FILE_READフラグは、ファイルを読み取り専用モードで開くことを指定します。本記事の目的はCSVファイルからデータを取得することであり、内容を書き換えることではないため、このフラグは必須です。FILE_CSVフラグは、ファイルをCSV形式として扱うようMQL5へ指示します。このフラグを指定すると、指定された区切り文字に従ってデータが自動的に各フィールドへ分割されます。

そのため、それぞれの項目を個別に読み取ることが容易になります。FILE_ANSIフラグは、ファイルの文字コードを指定します。このフラグは、ファイルがANSIエンコーディングで保存されていることをMQL5へ伝えます。ファイルがUnicode形式で保存されている場合は、代わりにFILE_UNICODEフラグを使用する必要があります。文字を正しく読み取るためには、指定するエンコーディングが実際のファイル形式と一致している必要があります。

最後の引数であるカンマ文字は、CSVファイルの区切り文字を指定します。通常のCSVファイルでは各値はカンマで区切られているため、この引数によってMQL5は各行をどの位置で区切るかを判断します。もしセミコロンなど別の区切り文字が使用されている場合は、その文字を指定する必要があります。 また、ファイルがターミナル専用のMQL5\Filesフォルダではなく、共通ターミナルフォルダに保存されている場合は、FileOpen関数へFILE_COMMONフラグを追加する必要があります。このフラグを指定すると、MQL5はTerminal\Common\Filesフォルダからファイルを検索します。このフォルダは、同じコンピューターへインストールされているすべてのMetaTrader 5ターミナルから共有されるため、複数のターミナル間で同じファイルを利用する場合に便利です。

ファイルを開こうとした直後に、関数は返されたハンドルがINVALID_HANDLEと一致するかどうかを確認します。一致した場合は、ファイルが存在しない、あるいは保存場所が誤っているなどの問題を特定しやすくするため、GetLastError()から取得したエラーコードとともにエラーメッセージを出力します。一方、正常にファイルを開くことができた場合は、FileCloseを使用して適切にファイルを閉じます。ファイルを正しく閉じることで、システムリソースが解放されるだけでなく、将来的なファイルアクセスの競合も防ぐことができます。実際の実装では、通常はファイルを閉じる前にファイル読み込み処理が実行されます。

 

ファイル要素を動的配列に格納する

ファイルを開いた後は、その内容をすべて動的配列へ保存する必要があります。これを安全におこなうためには、まずファイル内に存在する要素数を把握する必要があります。動的配列は使用する前にサイズを調整する必要があり、格納する要素数と同じ大きさにしておかなければなりません。配列のサイズが不足している状態でそれ以上の要素へアクセスしようとすると、配列範囲外アクセス(Out-of-range)エラーが発生します。

要素数は、ファイルを先頭から順番に読み込み、それぞれの要素を処理するたびにカウンタを1つずつ増やすことで数えます。このカウンタが、ファイル内に存在するレコード総数を表します。その後、ファイルポインタを先頭へ戻し、再び最初から内容を読み込めるようにします。総要素数が分かったら、その数に合わせて動的配列のサイズを変更します。そして、ファイルを再度先頭から読み込みながら、それぞれのレコードを順番に配列へ格納していきます。

//+------------------------------------------------------------------+
//| Balance curve function                                           |
//+------------------------------------------------------------------+
void BalanceCurve(string Filename)
  {
   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)
        {
         //counting total elements
         int   total_elements_count = 0;
         while(!FileIsEnding(file_handle))
           {
            FileReadString(file_handle);
            total_elements_count++;
           }

         FileSeek(file_handle, 0, SEEK_SET);

         //storing in array
         string TotalElements[];
         ArrayResize(TotalElements,total_elements_count);

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

            TotalElements[i] = FileReadString(file_handle);

           }

         FileClose(file_handle);
        }
  }

解説

ファイル内の要素数をすべて数え終えたら、すべての要素を動的配列へ格納する準備が整います。まず、TotalElementsという動的文字列配列を宣言します。動的配列は、格納する要素数に合わせてサイズを自由に変更できるため、必要な数だけ正確にメモリを確保できます。 総要素数を算出した後、その数に合わせて配列のサイズを変更します。これにより、ファイル内のすべての要素に対して、それぞれ専用のメモリ領域が確保されます。また、配列のサイズが保存する要素数よりも小さい場合には、配列範囲外アクセスエラーが発生する可能性があります。配列サイズを事前に適切な大きさへ変更することで、この問題を防ぐことができます。

配列のサイズを正しく確保した後は、ファイルを順番に読み込みながら配列へデータを格納します。この処理は、最初のインデックスから開始し、すべての要素を処理し終えるまで繰り返すループによって実行されます。各ループでは、ファイルから次の要素を読み込み、それを対応する配列のインデックスに割り当てます。ファイルポインタは読み込みのたびに自動的に次の位置へ進むため、データは順序どおり連続して配列へ格納されます。

ループが終了するまでに、ファイル内のすべての要素は順番どおり配列へ整理されます。このような構造にしておくことで、後から任意の要素をインデックスを使って簡単に参照できます。また、データを列ごとに整理したり、残高曲線を作成する際の累積利益計算のような、より高度な処理をおこなったりするための基盤にもなります。 まず、ファイル内の要素数を数えるために、カウンタ変数を宣言し、初期値を0に設定します。まだ何も数えていないため、0から開始するのが適切です。その後、ファイルの終端へ到達するまでループを実行します。この条件によって、利用可能なデータがなくなるまでプログラムは読み込みを継続します。

ループ内では、プログラムはファイルを1要素ずつ読み込みます。値を正常に取得するたびに、要素を1つ数えたことを示すため、カウンタを1つ増やします。また、ファイルポインタは読み込みのたびに自動的に次の位置へ進むため、ファイルは順番どおり処理されます。ループが終了した時点で、カウンターにはファイル内に存在するすべての要素数が格納されています。この時点では、ファイル全体を読み終えているため、ファイルポインタはファイルの末尾を指しています。その後もファイルを処理する必要がある場合は、ファイルポインタを先頭へ戻さなければなりません。これにより、後続の処理ではデータをファイルの先頭から再び読み込むことができ、すでに終端へ到達していて読み取るデータが存在しない状態を避けることができます。

                          

CSVファイル内のProfit($)列を特定する

このセクションでは、ファイル内で特定の情報や要素が存在するインデックスを見つける方法について説明します。前回の記事では、CSVファイルではインデックスによって各要素の位置を把握できることを説明しました。サンプルファイルを見ると、Profit($)ヘッダーはインデックス22にあります。しかし、この説明ではそのインデックスをコード内へ固定することはしません。この方法を採用することで、同じ仕組みを他のCSVファイルにも適用できるようになります。つまり、固定された位置に依存するのではなく、必要な列をコードが動的に検索して特定できるようになります。

図6:利益列

#property indicator_separate_window


string File_name = "Trading_Journal.csv";
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {

   BalanceCurve(File_name);

//--- create timer
   EventSetTimer(60);

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {

//--- destroy timer
   EventKillTimer();
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int32_t &spread[])
  {
//---

   return(rates_total);
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {

  }
//+-----------------------------------------------------------------+
//| Balance curve function                                           |
//+------------------------------------------------------------------+
void BalanceCurve(string Filename)
  {

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

         //counting total elements
         int   total_elements_count = 0;
         while(!FileIsEnding(file_handle))
           {
            FileReadString(file_handle);
            total_elements_count++;
           }

         FileSeek(file_handle, 0, SEEK_SET);

         //storing in array
         string TotalElements[];
         ArrayResize(TotalElements,total_elements_count);

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

            TotalElements[i] = FileReadString(file_handle);

           }

         //Serching Profit($) header
         int profit_header_index;

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

            if(TotalElements[i] == "Profit($)")
              {

               profit_header_index = i;

               break;
              }
           }

         Comment(profit_header_index);

         FileClose(file_handle);

        }
  }

解説

プログラムの最初の処理では、CSVファイル内のProfit($)列のインデックスを見つけます。このインデックスは変数へ保存され、後続の処理で利益データを読み始める位置として使用されます。コードはファイル内の各要素を先頭から順番に確認し、「Profit($)」ヘッダーを見つけると、その時点で検索を直ちに終了し、その位置を記録します。正しい列が見つかったことを確認するために、プログラムは見つかったインデックスを一時的に表示します。これにより、期待どおりの列が特定されたことを確認できます。その後、CSVファイルが選択され、その内容が読み込まれて配列へ整理されます。これで、残高曲線を描画するための利益データを利用できる状態になります。


CSVファイル内の「Total Trades」数を取得する

「Profit($)」ヘッダーを検索したのと同じように、プログラムはファイル内から「Total Trades:」も検索できます。取引総数は、この文字列の右側にある値として保存されています。数値そのものは取引数によって変化しますが、「Total Trades:」という文字列は常に同じであるため、数値を探すのではなく、この文字列を検索する方が確実です。プログラムは「Total Trades:」の位置を見つけた後、その位置から1つ先へ進むだけで実際の総取引数を取得できます。この方法により、現在の取引数がどのような値であっても、常に正しい総取引数を取得できます。

図7:総取引数

//+------------------------------------------------------------------+
//| Balance curve function                                           |
//+------------------------------------------------------------------+
void BalanceCurve(string Filename, int &total_deals)
  {
   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)
        {

         //counting total elements
         int   total_elements_count = 0;
         while(!FileIsEnding(file_handle))
           {
            FileReadString(file_handle);
            total_elements_count++;
           }

         FileSeek(file_handle, 0, SEEK_SET);

         //storing in array
         string TotalElements[];
         ArrayResize(TotalElements,total_elements_count);

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

            TotalElements[i] = FileReadString(file_handle);

           }

         //Serching Profit($) header
         int profit_header_index;

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

            if(TotalElements[i] == "Profit($)")
              {

               profit_header_index = i;

               break;
              }
           }

         //Comment(profit_header_index);

         //Getting total deals
         int total_deal_index;

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

            if(TotalElements[i] == "Total Trades:")
              {

               total_deal_index = i + 1;

               break;
              }
           }

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

         FileClose(file_handle);
        }
  }
string File_name = "Trading_Journal.csv";
int Total_deals;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {

   BalanceCurve(File_name,Total_deals);
   Comment("Total Deals: ", Total_deals);

//--- create timer
   EventSetTimer(60);

//---
   return(INIT_SUCCEEDED);
  }

解説

BalanceCurve関数の目的は、CSVファイルを処理し、総取引数を監視することです。この関数は、総取引数を参照渡しで受け取ります。参照を使用することで、従来のreturn文を使用しなくても、関数内で変更した総取引数の値が関数の外側にある変数へ自動的に反映されます。その結果、この関数は実質的に値を「返す」ことができます。

参照変数は、OnCalculate関数がチャート上へオブジェクトを描画するタイミングを制御するために使用されます。具体的には、総取引数が変化した場合にのみ、チャート上のオブジェクトを描画します。現在の取引数を参照変数へ保持しておくことで、前回の計算以降に新しい取引が追加されたのか、あるいは削除されたのかをプログラムが判定できます。これにより、不要な描画処理を防ぎ、パフォーマンスを向上させることができます。 関数内では、まずCSVファイルに含まれる総取引数を計算します。続いて、ファイル内の要素を格納した配列を検索し、「Total Trades:」というラベルを探します。このラベルが見つかると、その直後の要素に実際の総取引数が格納されていることが分かります。目的のラベルを見つけた時点で検索を終了するため、不要な処理をおこなわずに済みます。

その後、文字列として取得した取引数を整数へ変換し、その値を参照変数へ格納します。参照渡しであるため、この値は関数の外側でも自動的に利用でき、インジケータの他の処理から必要に応じて参照できます。 最後に、CSVファイルと総取引数の変数を引数としてBalanceCurveを呼び出すことで、外部変数には現在の総取引数が更新されます。確認のために、プログラムはこの値をチャート上へ表示することもできます。この方法を採用することで、インジケータは常に最新の取引数を把握できるようになり、OnCalculateは取引数が変化した場合にのみ描画処理を実行するため、精度と処理効率の両方を向上させることができます。

 

累積残高推移の計算

このセクションでは、forループを使用して残高推移を段階的に計算します。目的は、各取引のP/Lを累積した数値へ変換し、時間の経過に伴って口座パフォーマンスがどのように変化したかを表すことです。まず、損益推移の意味を明確にしておきます。損益推移とは、各取引結果を順番に加算していくことで得られる損益推移のことです。各取引を個別に評価するのではなく、新しい利益または損失を、それまでの累積値へ継続して加算していきます。利益で終了した取引では累積値は増加し、損失で終了した取引では累積値は減少します。この累積値が、残高曲線の基礎となります。

次に、Profit($)列に含まれるP/Lの値だけを1つの動的配列へ格納します。これにより、累積計算に必要な値だけを取り出して処理できます。この配列の各要素は、それぞれ1件の決済済み取引の結果を表します。

P/Lの値をすべて1つの配列へ格納した後、forループを使用して累積推移を計算します。ループを1回実行するごとに、現在の取引の利益または損失を累積値を保持する変数へ加算します。更新された累積値は、同時に別の動的配列へ保存されます。この2つ目の配列には、各インデックスがそれぞれの取引終了時点における口座パフォーマンスを表す、損益推移全体が格納されます。この配列を使用して、最終的にチャート上へ残高曲線を描画します。計算済みの累積値と元の利益データを別々の配列として保持することで、データ構造を整理できるだけでなく、描画処理も分かりやすく管理できます。

//+------------------------------------------------------------------+
//| Balance curve function                                           |
//+------------------------------------------------------------------+
void BalanceCurve(string Filename, int &total_deals, double &cumulative[])
  {

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

         //counting total elements
         int   total_elements_count = 0;
         while(!FileIsEnding(file_handle))
           {
            FileReadString(file_handle);
            total_elements_count++;
           }

         FileSeek(file_handle, 0, SEEK_SET);

         //storing in array
         string TotalElements[];
         ArrayResize(TotalElements,total_elements_count);

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

            TotalElements[i] = FileReadString(file_handle);

           }

         //Serching Profit($) header
         int profit_header_index;

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

            if(TotalElements[i] == "Profit($)")
              {

               profit_header_index = i;

               break;
              }
           }

         //Comment(profit_header_index);

         //Getting total deals
         int total_deal_index;

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

            if(TotalElements[i] == "Total Trades:")
              {

               total_deal_index = i + 1;

               break;
              }
           }

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

         // saving all profit/loss in one dynamic array
         double pro_loss[];
         ArrayResize(pro_loss,total_deals);
         int profit_count = 0;

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

            pro_loss[profit_count] = StringToDouble(TotalElements[i]);
            profit_count++;
           }

         Print("Profit/Loss:");
         ArrayPrint(pro_loss);

         //saving all progit progression in one dynamic array
         ArrayResize(cumulative,total_deals);
         double running_total = 0.0;

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

            running_total += pro_loss[i];
            cumulative[i] = running_total;

           }

         FileClose(file_handle);

        }
  }
double balance_progression[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {

   BalanceCurve(File_name,Total_deals,balance_progression);
   Print("\nBalance Progression: ");
   ArrayPrint(balance_progression);

//--- create timer
   EventSetTimer(60);

//---
   return(INIT_SUCCEEDED);
  }

解説

参照渡しによって累積配列を関数へ渡しているため、この場合、BalanceCurve関数内で加えらrれた変更は、関数の外側で定義されている元のbalance_progression[]配列へ即座に反映されます。配列を参照渡しすることは非常に重要です。これにより、データの検証や残高曲線の描画など、インジケータ内の他の処理から計算済みの累積損益推移へ直接アクセスできるようになります。参照渡しを使用しない場合、関数内で変更されるのはローカルコピーだけとなり、その結果は関数の外側から利用できません。

関数はまず、各P/L値を個別の配列へ格納することから開始します。各値はCSVファイルから取得され、ヘッダ情報は除外されます。この配列は総取引数に合わせてサイズが変更されます。各テキスト値を計算に使用できる数値形式へ変換した後、プログラムはその順序を維持したまま配列へ保存します。処理が完了すると、すべての取引結果が順番どおり配列へ格納され、デバッグや検証のためにログへ出力することもできます。

個々の取引結果を整理した後、累積残高推移を計算します。これは、時間の経過に伴う口座パフォーマンスの累積値です。まず累積値を保持する変数を初期化し、累積配列のサイズを総取引数に合わせて変更します。その後、利益・損失の配列を順番に処理しながら、各取引結果を累積値へ加算し、その更新後の累積値を対応する位置の累積配列へ保存します。この処理によって、個々の取引結果の並びが、各インデックスにその取引終了時点の口座残高を表す累積系列へと変換されます。

インジケータの先頭で宣言されている動的配列balance_progression[]には、この累積残高推移が格納されます。OnInitがBalanceCurveを呼び出す際には、CSVファイル名、総取引数を格納する変数、そして配列をすべて参照渡しで渡します。これにより、関数は累積値を直接balance_progression[]配列へ書き込むことができ、プログラム内の他の処理から累積データへ即座にアクセスできます。 関数の処理が完了した後は、計算結果が正しいことを確認するため、累積推移をログへ出力することもできます。この方法では、生データと累積データを分離したまま明確なデータ構造を維持できるため、計算処理と表示処理を効率的に結び付けることができます。最終的に、この累積配列を基にしてインジケータウィンドウへ残高曲線を描画することで、時間の経過に伴う口座パフォーマンスを視覚的に確認できます。

出力:

図8:残高推移


インジケータウィンドウの表示範囲を定義する

MQL5でサブウィンドウ型のインジケータを扱う場合は、インジケータウィンドウの表示範囲(境界)を定義することが重要です。残高曲線のようなデータを描画する場合、インジケータは表示すべき値のスケールを自動的には認識できません。チャートが最小値と最大値を認識していないと、データが過度に圧縮されたり引き伸ばされたりし、結果を視覚的に把握しにくくなる場合があります。表示範囲を定義することで、描画されるラインがインジケータウィンドウ内へ適切に収まり、データの変化を正確に表現できるようになります。

表示範囲を決定する最初の手順は、データが取り得る値の範囲を求めることです。本記事では、累積残高推移を格納した配列を対象とします。この配列の最大値がインジケータウィンドウの上限となり、最小値が下限となります。これにより、残高曲線全体が表示され、ピークとボトムも正しく表現されます。求めた最小値と最大値は、そのままインジケータのプロパティへ設定できます。これにより、サブウィンドウはY軸を適切なスケールで表示し、残高曲線が利用可能な表示領域を効果的に使用するようになります。このようにインジケータウィンドウの表示範囲を定義することで、歪みのない見やすいチャートを表示でき、トレーダーは時間の経過に伴うパフォーマンスの推移を容易に把握できるようになります。

         //saving all progit progression in one dynamic array
         ArrayResize(cumulative,total_deals);
         double running_total = 0.0;

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

            running_total += pro_loss[i];
            cumulative[i] = running_total;
           }

         //MAX AND MIN Indicator Window
         double max_cumulative  = cumulative[ArrayMaximum(cumulative,0,WHOLE_ARRAY)];
         double min_cumulative  = cumulative[ArrayMinimum(cumulative,0,WHOLE_ARRAY)];

         IndicatorSetDouble(INDICATOR_MAXIMUM, max_cumulative);  // maximum value
         IndicatorSetDouble(INDICATOR_MINIMUM,min_cumulative);  // minimum value

         FileClose(file_handle);

        }
  }

解説

この部分では、累積残高推移の最大値と最小値を取得し、インジケータウィンドウの表示範囲を設定します。これは非常に重要な処理です。なぜなら、残高曲線を正しく表示するためには、専用のインジケータウィンドウがデータの範囲を把握する必要があるためです。累積配列内の最大値は曲線の上限を示し、最小値は曲線の下限を示します。この2つの値によって、残高曲線を描画するために使用される垂直方向の表示範囲が決定されます。

その後、計算された最大値と最小値は、実際に設定へ適用する前に正確性を確認するため、Comment関数を使用して一時的にチャート上へ表示されます。インジケーターの実際の表示範囲を設定するために、IndicatorSetDouble関数を使用して最大値と最小値を割り当てます。これにより、MetaTraderはY軸のスケールを自動的に調整し、残高曲線全体がインジケータウィンドウ内へ適切に収まるようになります。この表示範囲を設定することで、インジケータは口座パフォーマンスの高値と安値を正確に表現できるようになります。その結果、トレンドの確認、パターンの認識、取引結果の評価が容易になります。

出力:

図9:ウィンドウ境界


水平軸と垂直軸の描画

インジケータの残高曲線を描画する前に、X軸とY軸を適切に設計する必要があります。この処理では、CSVファイル内の総取引数と、通常のローソク足チャートに存在するバー数の両方を考慮する必要があります。Y軸は残高曲線上の利益水準を示し、X軸は時間の経過に伴う取引の進行状況を表します。

まず、CSVファイル内の最初の取引をチャート上で表現するために、どこまで過去へ戻る必要があるかを計算します。そのため、現在のローソク足から逆方向へバーを数え、ファイル内の最初の取引に対応するバーを探します。このバーが、X軸とY軸が交差する位置、つまりグラフの原点になります。そこから、X軸は最初の取引に対応するバーから現在のバーまで水平方向へ描画され、Y軸は利益の増加を表すために垂直方向へ延びます。 実際の多くの取引環境では、取引間の時間間隔が非常に長くなる場合があります。場合によっては、1回の取引から次の取引まで2か月以上空くこともあります。そのため、CSVファイル内に保存されている実際の取引時間をそのまま使用するのではなく、チャート上のローソク足バー数を基準として使用します。取引時間だけを基準にした場合、描画された曲線が不適切に圧縮されたり、逆に引き伸ばされたりする可能性があります。

さらに、チャート上のバーを使用するもう1つの理由は、インジケータウィンドウが常にメインチャートウィンドウと互換性を維持できるためです。これは、両方のウィンドウが同時にスクロールする場合でも、インジケーターウィンドウがメインチャートの最後のバーより先へスクロールできないことを意味します。必要なのは、チャート上のバー数とCSVファイル内の総取引数を対応させることだけです。 また、実際の取引時間ではなくローソク足バーを使用することで、インジケーターはすべての時間足で適切に動作します。もしX軸を取引時間に基づいて作成した場合、より高い時間足へ切り替えた際に残高曲線が縮小して表示され、読み取りや解釈が難しくなる可能性があります。ローソク足バー自体を使用することで、チャートの時間足が変化しても残高曲線は正しい比率と間隔を維持できます。

#property indicator_separate_window

string File_name = "Trading_Journal.csv";
int Total_deals;
double balance_progression[];

int last_total_deals = 0;
string y = "Y Axis";
string x = "X Axis";
double max_cumulative;
double min_cumulative;
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {

   BalanceCurve(File_name,Total_deals,balance_progression);
// Print("\nBalance Progression: ");
// ArrayPrint(balance_progression);

//--- create timer
   EventSetTimer(60);

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {

//--- destroy timer
   EventKillTimer();
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int32_t &spread[])
  {
//---

   if(rates_total >= Total_deals && Total_deals != last_total_deals)
     {

      //MAX AND MIN Indicator Window
      max_cumulative  = balance_progression[ArrayMaximum(balance_progression,0,WHOLE_ARRAY)];
      min_cumulative  = balance_progression[ArrayMinimum(balance_progression,0,WHOLE_ARRAY)];

      // Creating y and x axis
      ObjectCreate(0,y,OBJ_TREND,1,time[rates_total - Total_deals],0,time[rates_total - Total_deals],max_cumulative);
      ObjectCreate(0,x,OBJ_TREND,1,time[rates_total - Total_deals],0,time[rates_total - 1],0);

      IndicatorSetDouble(INDICATOR_MAXIMUM, max_cumulative);  // maximum value
      IndicatorSetDouble(INDICATOR_MINIMUM,min_cumulative);  // minimum value

      last_total_deals = Total_deals;
     }

   return(rates_total);
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {

   BalanceCurve(File_name,Total_deals,balance_progression);

  }

解説

BalanceCurve関数をCSVファイル、総取引数を格納する変数、そして累積残高配列とともに実行すると、プログラムは2つの重要な情報を取得します。1つ目は総取引数です。これは取引を監視する変数へ保存されます。2つ目は累積損益推移です。これは各取引結果を加算した累積値として計算され、実行中の合計値を保持する配列へ保存されます。残高曲線をインジケータウィンドウへ描画するためには、この累積配列が必要になります。この配列には、各取引後の口座の累積損益推移が格納されているためです。

コードをOnInitハンドラ内で実行することで、インジケータはチャートへ適用された直後から有効になります。これにより、初期設定が正しく確立され、総取引数や累積利益などの初期データが正しく取得されます。次に、EventSetTimerを使用してタイマーを設定し、OnTimerが60秒ごとに実行されるようにします。OnTimer内のBalanceCurveは、総取引数と累積推移を更新します。OnInitとOnTimerの両方からこの関数を呼び出すことで、インジケータは初期化時に正しくセットアップされ、その後も設定された間隔で自動的に更新されます。 また、EventKillTimerを使用することで、インジケータが削除された場合やターミナルが終了した場合にタイマーを停止できます。これにより、不要な更新処理を防ぎ、システムリソースを節約できます。

グローバル領域にある変数は、最後に確認された総取引数を保持します。OnCalculateハンドラ内部には条件を設定し、すべての取引を表示するために十分なバー数が存在し、かつ前回の計算時と比較して総取引数が変化している場合にのみチャート描画処理が実行されるようにします。更新後には、最新の総取引数を監視する変数を更新します。これにより、必要な場合にのみインジケータが再計算されるようになり、不要な処理を排除して効率を向上させることができます。

インジケータの軸値はチャート描画に必要となるため、最大値と最小値はOnCalculate内で設定します。プログラムは累積配列内から最大値と最小値を検索し、Y軸の上限と下限を決定します。X軸は最初の取引に対応するバーからチャート上の最後のバーまで水平方向へ広がります。一方、Y軸は最初の取引に対応するバーを基準として開始され、累積値の最大値まで垂直方向へ伸びます。これらの制約を利用することで、残高曲線はインジケータウィンドウ内へ適切に収まり、軸と正しく位置合わせされた状態で表示されます。

この設定により、残高曲線はチャート上で正確に表示され、ローソク足と整列し、新しい取引が追加された場合にのみ更新されるようになります。また、タイムスタンプではなくバー位置を基準として描画するため、不規則な取引間隔にも対応でき、すべての時間足で安定して動作します。時間を基準とした方式では、時間足を変更した際に曲線が縮小または拡大してしまう可能性があります。一方、バーを基準にすることで、時間足が変化しても残高曲線は適切な比率と位置関係を維持できます。

出力:

図10:X軸とY軸


インジケータチャートへ残高曲線を描画する

準備処理の最終段階は、インジケーターチャート上へ残高曲線を描画することです。ここまでで、チャートを構成するX軸とY軸の設定、インジケータウィンドウの最大値と最小値の決定、CSVファイルからのP/Lデータの取得、累積残高推移の計算が完成しています。これで、過去の取引結果を基に口座残高が時間とともにどのように変化したかを示す残高曲線自体を表示する準備が整いました。残高曲線を描画するために、累積利益配列内の各値を順番に処理します。各数値はチャート上の1つのバーへ対応付けられ、その取引時点における口座残高を表します。X軸上の位置は、CSVファイルに記録された総取引数を使用し、現在のローソク足から逆方向へバーを数えることで計算されます。

これにより、最新の取引はチャート上の現在のバーに対応し、ファイル内の最初の取引は残高曲線の開始地点に対応します。その地点におけるY軸の値は、その時点の累積利益によって決定されます。つまり、口座残高の状態に応じて垂直方向の位置が決まります。プログラムは、各ポイントのX座標とY座標を基に、それぞれの点を順番につなぎ合わせることで、連続した残高曲線を作成します。

//+------------------------------------------------------------------+
//| Balance curve function                                           |
//+------------------------------------------------------------------+
void BalanceCurve(string Filename, int &total_deals, double &cumulative[], double &pro_loss[])
  {
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {

   BalanceCurve(File_name,Total_deals,balance_progression,profit_loss);
// Print("\nBalance Progression: ");
// ArrayPrint(balance_progression);

//--- create timer
   EventSetTimer(60);

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {

//--- destroy timer
   EventKillTimer();
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int32_t &spread[])
  {
//---

   if(rates_total >= Total_deals && Total_deals != last_total_deals)
     {
     
    IndicatorSetString(INDICATOR_SHORTNAME, "Balance Curve");
    int indicator_window = ChartWindowFind(ChartID(),"Balance Curve");
    
   // Comment(indicator_window);
    
      ObjectsDeleteAll(ChartID(),indicator_window,OBJ_TREND);

      //MAX AND MIN Indicator Window
      max_cumulative  = balance_progression[ArrayMaximum(balance_progression,0,WHOLE_ARRAY)];
      min_cumulative  = balance_progression[ArrayMinimum(balance_progression,0,WHOLE_ARRAY)];

      // Creating y and x axis
      ObjectCreate(0,y,OBJ_TREND,indicator_window,time[rates_total - Total_deals],0,time[rates_total - Total_deals],max_cumulative);
      ObjectCreate(0,x,OBJ_TREND,indicator_window,time[rates_total - Total_deals],0,time[rates_total - 1],0);

      IndicatorSetDouble(INDICATOR_MAXIMUM, max_cumulative);  // maximum value
      IndicatorSetDouble(INDICATOR_MINIMUM,min_cumulative);  // minimum value

      int c = 0;

      for(int i = rates_total - Total_deals; i <= rates_total - 2; i++)
        {

         chart_lines = StringFormat("Lines %d", c);

         if(ObjectFind(0, chart_lines) == -1)
           {

            ObjectCreate(0,chart_lines,OBJ_TREND,indicator_window,time[i],balance_progression[c],time[i+1],balance_progression[c+1]);

           }
         else
           {
            ObjectMove(0, chart_lines, 0, time[i], balance_progression[c]);
            ObjectMove(0, chart_lines, 1, time[i+1], balance_progression[c+1]);
           }

         p_l = StringFormat("Profit-Loss %d", c);
         if(show_pro == true)
           {
            if(ObjectFind(0, p_l) == -1)
              {
               ObjectCreate(0,p_l,OBJ_TEXT,1,time[i],balance_progression[c]);
              }
            else
              {
               ObjectMove(0, p_l, 0, time[i], balance_progression[c]);

              }

           }
         else
           {
            ObjectDelete(0,p_l);
           }

         if(profit_loss[c] > 0)
           {

            d_sign = "$";

           }

         if(profit_loss[c] <= 0)
           {

            d_sign = "-$";

           }

         ObjectSetString(0,p_l,OBJPROP_TEXT, d_sign + DoubleToString(MathAbs(profit_loss[c]),2));

         c++;

        }

      last_total_deals = Total_deals;
     }

   return(rates_total);
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {

   BalanceCurve(File_name,Total_deals,balance_progression,profit_loss);
  }

解説

BalanceCurve関数を改善するために、P/L配列を参照渡しで受け取る追加パラメータを導入しました。参照渡しを使用することで、関数内でローカルコピーを作成するのではなく、外部で作成された元の配列へ直接接続できます。これにより、計算結果をプログラム全体で利用できるようになり、関数はCSVファイルのProfit($)列から抽出した正確なP/L値を配列へ格納できます。以前は、この関数は累積残高推移を生成するだけでした。しかし今回の変更により、各取引の個別結果にもアクセスできるようになります。この対応を行うため、グローバル領域で定義されているP/L配列をOnInitとOnTimer内のBalanceCurve関数呼び出しへ追加しました。これにより、累積値だけでなく、個々の取引結果もプログラム全体で利用できるようになります。

ループ変数はチャート上のバーを表し、累積残高配列内のインデックスを追跡するためのカウンタも初期化されます。これら2つのインデックスは同時に進行しますが、それぞれ異なる役割を持っています。一方はファイル内の取引番号を追跡し、もう一方はチャート上のローソク足位置を追跡します。

以前の実装では、インジケータが常に2番目のチャートウィンドウへ配置されることを前提とし、トレンドラインなどのグラフィカルオブジェクトを作成する際に固定値であるサブウィンドウ番号1を使用していました。この方法は、その前提条件が維持されている場合にのみ有効です。しかし、インジケータを別の場所へ移動した場合や、他のインジケータを追加した場合には、正しく動作しなくなる可能性があります。さらに、その固定されたウィンドウ番号を使用してオブジェクトを削除すると、取引数が変化した際に、そのサブウィンドウ内に存在するすべてのトレンドラインが削除される可能性があります。これは、他のオブジェクトへ意図しない影響を与える危険があります。

そこで今回は、より動的で信頼性の高い方法として、IndicatorSetString()関数を使用してインジケータの短縮名を設定し、ChartWindowFind()関数を使用して実行時に実際のサブウィンドウ番号を取得します。取得した値はindicator_windowのような変数へ保存し、オブジェクトの作成や削除を行う際に一貫して使用できます。これにより、チャート構成に関係なく、オブジェクト管理は常に正しいウィンドウ内で実行されるようになります。また、インジケータが常にウィンドウ1に存在するという前提も不要になります。

チャートの表示範囲内へすべての取引を配置するため、描画ループでは最初の取引に対応するバーから最後から2番目のバーまで、連続するバーのペアを順番につなぎます。残高曲線の各区間には、取引インデックスを使用して固有の名前を設定します。まずObjectFindを使用して、対象となるトレンドラインオブジェクトがすでに存在するかどうかを確認します。存在しない場合は、ObjectCreateを使用して2つの連続する累積残高ポイント間を結ぶラインを作成します。一方、すでにオブジェクトが存在する場合は、ObjectMoveを使用してラインの位置を更新します。これにより、処理負荷を軽減しながら、新しいデータの追加やチャート更新時にも残高曲線を動的に更新できます。

同様に、各取引のP/Lを表示するテキストオブジェクトにも、それぞれ固有の名前を設定します。これらの数値ラベルを表示するかどうかは、表示制御用のshow_pro変数によって決定されます。有効になっている場合、プログラムは対象オブジェクトが存在するかを確認し、存在する場合は現在の位置へ移動します。存在しない場合は、適切なバー位置と累積残高レベルへOBJ_TEXTオブジェクトを新規作成します。show_proが無効の場合は、ObjectDeleteを使用してテキストオブジェクトを削除します。これにより、残高曲線自体を維持しながら、チャートを整理された状態に保つことができます。

各P/L額は、適切な符号を判断し、数値部分から負号を除去するためにMathAbsを使用して整形されます。その後、整形されたテキストはObjectSetStringによってオブジェクトへ設定されます。これにより、利益の場合は「$25.50」、損失の場合は「-$12.30」のような、分かりやすい形式で表示できます。最後に、次のループ処理で次の取引を処理できるよう、取引インデックスのカウンタを1つ増加させます。このロジックにより、インジケータは残高曲線の各点へ正確に対応したP/Lラベルを、必要に応じて動的に表示または非表示にできるようになります。

出力:

図11:残高曲線


結論

この記事を通して、MQL5でCSV形式の取引履歴ファイルを読み込み、Profit($)の値や総取引数などの重要なデータを抽出する方法を学びました。また、必要なファイルを適切なフォルダへ配置する方法についても学びました。ここまでで、累積残高推移を計算し、それを利用してインジケータウィンドウの最大値と最小値を決定する方法を理解できたはずです。さらに、連続した残高曲線の描画、水平軸と垂直軸の作成および位置合わせ、そして必要に応じて各取引のP/Lラベルを表示する方法についても学びました。加えて、新しい取引が追加された場合にのみ曲線を再描画する仕組みや、OnInitとOnTimerイベントハンドラを利用してインジケータを動的に更新する方法についても説明しました。総合的に見ると、MetaTrader 5を使用して、生のCSV取引データを意味のある視覚的なチャートへ変換する方法を学びました。このチャートを利用することで、プログラム内部で口座パフォーマンス、ドローダウン、個々の取引が全体へ与えた影響を分析できます。

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

添付されたファイル |
最後のコメント | ディスカッションに移動 (2)
Oluwatosin Mary Babalola
Oluwatosin Mary Babalola | 1 3月 2026 において 07:40
わあ、あなたは本当に素晴らしい先生ですね。これからもずっとあなたの記事を読み続けます
ALGOYIN LTD
Israel Pelumi Abioye | 1 3月 2026 において 07:43
Oluwatosin Mary Babalola #:
わあ、あなたは本当に素晴らしい先生ですね。これからもずっとあなたの記事を読み続けます
オルーワトシンさん、こんにちは。

温かいお言葉をいただき、ありがとうございます。
MQL5でのスイング極値とプルバック(第2回):EAによる戦略の自動化 MQL5でのスイング極値とプルバック(第2回):EAによる戦略の自動化
下位足の市場構造を基盤とし、そのシグナル運用を上位足で統合・調整する本インジケーターは、価格が統計的に反転しやすくなるスイングの極値を検出します。価格の行き過ぎ(オーバーエクステンション)とプルバックゾーンを視覚化し、平均回帰の動きを早い段階で把握できるようにします。
ラリー・ウィリアムズの『市場の秘密』(第12回):相場環境に基づくスマッシュデー反転トレード ラリー・ウィリアムズの『市場の秘密』(第12回):相場環境に基づくスマッシュデー反転トレード
体系化されたコンテキストの中で、ラリー・ウィリアムズのスマッシュデー反転パターンをMQL5で自動化する方法を解説します。限定された有効期間内でセットアップを検証し、Supertrendを用いた曜日フィルタを備え、エントリーはレベル突破時またはバー確定時のいずれにも対応するエキスパートアドバイザー(EA)を実装します。エントリー方式としては、レベル突破時の即時エントリー、またはローソク足確定後のエントリーに対応しています。また、同時保有ポジション数を1つに制限し、リスクベースまたは固定ロットによるポジションサイジングをサポートします。さらに、段階的な開発手順、バックテスト方法、再現可能な設定も併せて提示します。
初心者からエキスパートへ: トレンドフィルタによる流動性戦略の拡張 初心者からエキスパートへ: トレンドフィルタによる流動性戦略の拡張
流動性ベースの戦略にシンプルなトレンド条件を追加します。具体的には、EMA(50)の方向に沿って流動性ゾーンを取引するというフィルタを導入します。記事では、不要なセットアップを除外するためのルールを説明し、再利用可能なTrendFilter.mqhクラスと、MQL5でのEA統合方法を紹介します。また、フィルタなしの基本戦略と、フィルタを適用した戦略のテスト結果を比較します。読者は、明確な方向性の判断基準、逆トレンド局面での過剰取引の抑制方法、そしてすぐに利用できるソースファイルを得ることができます。
Neuro-Structural Trading Engine (NSTE)(第I回):プロップファーム対応マルチ口座システムの構築方法 Neuro-Structural Trading Engine (NSTE)(第I回):プロップファーム対応マルチ口座システムの構築方法
プロップファームの制約を遵守しながら、MetaTrader 5上で暗号資産CFDを運用するマルチ口座アルゴリズム取引システムアーキテクチャを提示します。システムは、「固定ドルリスク」「1口座につき1スクリプト」「設定の一元管理」という3つの基本原則を定義した上で、PythonとMQL5の役割分担、60秒ごとの処理ループ、JSONベースのシグナル連携について詳しく解説します。さらに、ロットサイズの実践的な計算方法、安全性を確保するためのチェック機構、ポジション管理パターンについても紹介し、信頼性の高い運用を実現するための構成を示します。