English Русский 中文 Español Deutsch Português
preview
初心者からプロまでMQL5をマスターする(第5回):基本的な制御フロー演算子

初心者からプロまでMQL5をマスターする(第5回):基本的な制御フロー演算子

MetaTrader 5 | 2 5月 2025, 08:00
73 0
Oleh Fedorov
Oleh Fedorov

はじめに

本連載の記事では、プログラムに意思決定をさせる方法に焦点を当てています。今回はその一環として、インサイドバーとアウトサイドバーを検出するインジケーターを開発します。

  • アウトサイドバーとは、前のローソク足の高値と安値の両方を超える値動きを見せたバーを指します。
  • インサイドバーとは、前のローソク足の高値と安値の範囲内で価格が収まり、終値を迎えたバーのことです。
  • 確定したローソク足にのみマーカーを表示します。形成中のローソク足にはマーカーを付けません。
  • マーカーは「実体の大きな」ローソク足に限定して適用します。アウトサイドバーの場合は現在のバーに、インサイドバーの場合はひとつ前のバーにマーカーを付けます。この選択は主観的なものなので、インジケーターを使いながら好みに応じてコードを調整しても構いません。
  • インサイドバーとアウトサイドバーの両方に対応する汎用インジケーターで、ユーザーが設定パラメータによって表示対象を選べるようにします。

この開発では、この記事で取り上げる制御フロー演算子を活用します。インジケーター自体は比較的シンプルなため、主に条件分岐とループを使用することになります。ただし、練習として以下のような応用にも取り組んでみるとよいでしょう。

  • 異なる種類のループを使用して実装してみる
  • 条件分岐の一部またはすべてをswitch-case文に置き換える

これまで作成してきたプログラムは、基本的に命令を上から順に実行するだけで、分岐や判断をおこなうことはありませんでした。この記事を通して、より柔軟で高度なロジックを持つプログラムを作れるようになるでしょう。

それではまず、制御フローの基本となるブール(論理)式について詳しく見ていきましょう。


論理式の詳細な概要

まず確認しておきましょう。ブール型(bool)には、trueまたはfalseの2つの値しか存在しません。

論理演算子が使用されるたびに、プログラムは現在のデータに対して「(条件)は本当ですか?(例:「(prev_calculated==rates_total)は本当ですか?」)本当の場合は現在の関数を終了します。そうでなければ、計算を続けます」のように問いかけることになります。

MQL5においては、数値の0はfalseを表し、その他の数値(正でも負でも)はtrueとして扱われます。

一方で、文字列型はbool型へ直接変換することはできません。これをおこなおうとすると、コンパイルエラーが発生します。ただし、文字列同士の比較は可能です。

論理値は、主に比較式の結果として得られます。最もシンプルな論理式は「比較文」です。MQL5で使用される比較演算子(標準的な数学記号と同様)は、より大きい(>)、より小さい(<)、等しい(==)、等しくない(!=)、以上(>=)、以下(<=)です。たとえば、式(a==b)は、両方の要素が等しい場合にのみtrueと評価されます。これは直感的に理解しやすいでしょう。

文字列同士を比較する場合、ASCIIコードに基づいて比較がおこなわれます。具体的には、文字コードの数値が大きい方が「大きい」とみなされます。たとえば、("A" > "1")はtrueですが、("Y" <  "A")はfalseです。複数文字から成る文字列の場合、左から順に1文字ずつ比較が進み、最初に異なる文字が見つかった時点で大小が決まります。また、空の文字列("")は、どんな非空文字列よりも小さいとみなされます。さらに、先頭の文字が同じ場合でも、短い方の文字列が小さいと判断されます。

MQL5では、次の3つの基本的な論理演算子が使用されます。

  • 論理否定または反転(論理NOT)は、感嘆符(!)で表されます。
    論理値をその反対の値に変換します。
  • 論理積(論理AND)は、2つのアンパサンド記号(&&)で表されます。
    この演算子を含む式は、式の両方の部分(左側と右側)がtrueの場合にのみtrueになります。
  • 論理和(論理OR)は2本の垂直線(||)で表されます。
    この演算子を含む式は、その部分の少なくとも1つがtrueの場合にtrueになります。
これらすべての論理演算子がどのように相互作用するかをよりよく理解するには、次の真理値表を参照してください。
表1:論理否定(!)
表2:論理積(&&)
表3:論理和(||)
not
and
or

理解を深めるために、以下の論理式それぞれの結果を考えてみましょう。コメントにヒントが書かれていますが、まずは見ずに自力で解いてみることをおすすめします。答え合わせの際にコメントを参考にしてください。

int a = 3;
int b = 5; 

bool e = ( a*b ) > ( a+b+b );          // true

Print ( (a>b) );                       // false
Print ( (a<b) || ((a/b) > (3/2)) );    // true
Print ( !a  );                         // false
Print ( ! (!e) );                      // true
Print ( (b>a) && ((a<=3) || (b<=3)) ); // true
Print ( "Trust" > "Training" );        // true
Print ( a==b );                        // false
Print ( b=a );                         // 3 (!!!) (therefore, for any logical operator this is always true)
//  In the last example, a very common and hard-to-detect mistake has been made. 
//    Instead of a comparison, an assignment was used, which results in a value of type int! 
//  Fortunately, the compiler will usually warn about this substitution.

例1:論理演算子の使用例

式の中に複数の演算子があり、かつ括弧が使われていない場合、基本的に左から右へ順に評価されます(ただし代入演算は例外です)。演算の実行順序は次のルールに従います。

  1. まず、すべての算術演算子が実行されます。
    • *、/、%
    • +、-
  2. 次に比較演算子(==、!=、>、<、>=、<=)が評価されます。
  3. その後、残りの論理演算子が、以下の順番で実行されます。
    • 論理否定(!)
    • 論理積(&&)
    • 論理和(||)
  4. 最後に、代入演算子(=)が実行されます。

コード中で実行順序が不明確に感じる場合は、括弧を使用することをおすすめします(例1参照)。括弧を使うことで、括弧内の処理が最優先されるため、実行順序を明確にできるだけでなく、インデントを整えることでコードの可読性も向上します。


if文

if文は、条件文、分岐文、決定構造とも呼ばれ、プログラムが選択をおこなうのに役立ちます。

その構文は簡単です。

if (condition) action_if_condition_is_true;
else action_in_all_other_cases; // The "else" branch is optional

例2:条件文テンプレート

なぜ「分岐」と呼ばれるのでしょうか。視覚化すると、プログラムフローを分割する木の枝のような構造になります。

分岐演算子

図1:分岐演算子

想像力を働かせると、プログラムの実行におけるメインの「幹」が、どのようにして枝分かれしていくかが見えてきます。昔のフローチャートでは、アルゴリズム全体がまるで茂みのように広がる形をしていることも珍しくありません。例えば、本を読もうとしたとき、デスクランプが点灯しなかったとしましょう。まずはスイッチを入れてみます。もしかすると、それだけで点灯するかもしれません。もし点灯しなければ、電球がソケットに正しく取り付けられているかを確認します。誰かが取り外している可能性もあります。電球が付いているにもかかわらず点灯しない場合は、電球が切れていないかをチェックしましょう。電球が切れていたら交換します。それでも点かない場合は、別のランプを使うことを考えます。この一連の論理的な手順は、決定木として表現することができます。

枝のイラスト

図2:枝のイラスト

このフローチャートでは、下部の緑色の[Start]ポイントから実行が始まります。私の考えでは、各選択ポイントでどのように分岐が発生するかが、とても分かりやすく示されています。青で示された各決定ポイントは、それぞれ分岐するパスを表しています。

MQL5言語における「action_if_condition_is_true」と「action_in_all_other_cases」は単一の文で記述することもできますが、必要に応じて複合文にすることも可能です。

詳しく説明しましょう。MQL5では、アクションを中括弧{}で囲むことができますし、囲まずに記述することもできます。中括弧は独立した「単位」として機能し、コードブロックを区切る役割を持ちます。ブロック内で宣言された変数は、ブロックの外からアクセスすることができません。状況によっては、中括弧の使用が必須になることもありますが、多くの場合は省略可能です。中括弧で囲まれた内容は、プログラム全体からひとつのまとまりとして認識されます。たとえば、ブロック内で宣言された変数は、そのブロックの外側からは見えません。ここで言う「複合文」とは、中括弧{}で囲まれたこうした「コードブロック」のことを指します。中には複数のアクションを記述できますが、if文のような制御構文では、最初に現れる閉じ中括弧(})までをひとつのまとまりとして処理します。中括弧を省略した場合、if文の直後に続く最初の1文だけが条件に従って実行され、それ以降の文は条件に関係なく実行されてしまいます。

一般的に、ifやelseに従属するコマンド群は文の本体と呼ばれます。

例2のテンプレートでは、if文の本体が「action_if_condition_is_true」、else文の本体が「action_in_all_other_cases」となっています。

ここで、実際の例を紹介しましょう。以下のコードは、例16に基づき、インジケーター内で新しいローソク足の出現をチェックするものです。ロジックは、前回計算したローソク足の数(prev_calculated)と、現在のローソク足の総数(rates_total)を比較します。

if (prev_calculated == rates_total) // _If_ they are equal, 
    
  {                                 //    _then_ do nothing, wait for a new candlestick
    Comment("Nothing to do");       // Display a relevant message in a comment
    return(rates_total);            // Since nothing needs to be done, exit the function 
                                    //   and inform the terminal that all bars have been calculated (return rates_total)
  }

// A new candle has arrived. Execute necessary actions
Comment("");                        // Clear comments
Print("I can start to do anything");   // Log a message


// Since the required actions will be executed 
//   only if prev_calculated is not equal to rates_total, 
//   else branch is not needed in this example.
// We just perform the actions.

例3:prev_calculatedとrates_totalを使用して新しいローソク足を待機する

ここでは、if文の中にComment()とreturnの2つの処理が含まれています。必要なすべての処理はメインの実行ブロック内でおこなわれるため、else文は省略されています。

中括弧の中には、必要なだけ多くの処理を記述することができます。しかし、中括弧を取り除いてしまうと、条件付きで実行されるのは、「Comment("Nothing to do")」文だけになります。その場合、returnは無条件で実行されてしまい、「作業準備完了」に関するメッセージが表示されなくなります(この違いを確認するには、1分足チャートで試してみることをおすすめします)。

ベストプラクティスの推奨事項

if文では、たとえ1文しか含まれていなくても必ず中括弧{}を使うようにしましょう。

このルールは、たとえばコードベースで他のプログラマーのコードを読むなどして十分に経験を積むまでは、守り続けることをおすすめします。中括弧を使用すると、アプリケーションのデバッグ作業がより視覚的になり、見つけにくいエラーに何時間も悩まされるリスクを減らせます。さらに、最初は1つの処理しかなくても、中括弧を使っておけば、後から追加で処理を挿入するのも簡単になります。たとえば、後になってデバッグ情報やアラートの出力を追加したくなった場合にも、スムーズに対応できるでしょう。


三項演算子

変数に2つの可能な値のうちどちらかを代入するために、条件文を使うこともあります。以下は例です。

int a = 9;
int b = 45;
string message;

if( (b%a) == 0 ) // (1)
  {
    message = "b divides by a without remainder"; // (2)
  }
else 
  {
    message = "b is NOT divisible by a"; // (3)
  }

Print (message);

例4:三項演算子への変換条件

この場合、三項演算子(3つの部分から成る)を使用すると表記を若干短縮できます。

int a = 9;
int b = 45;
string message;

message = ( (b%a) == 0 ) /* (1) */ ? 
          "b divides by a without remainder" /* (2) */ : 
          "b is NOT divisible by a" /* (3) */ ;
  
Print (message);

例5:三項演算子

この演算子は、if文と同じ流れに従い、条件 →(疑問符?)→ 条件がtrueの場合の値 → (コロン :、elseの代わり) → 条件がfalseの場合の値となります。このような三項演算子は、タスクに適している場合に使用することができます。

三項演算子と従来のif文との主な違いは、三項演算子は左辺の変数の型に一致する値を返さなければならないという点です。そのため、三項演算子は式の中でしか使用できません。一方、従来のif文は本体内で複数の処理や式を記述できますが、直接値を返すことはできません。


switch-case文

2つ以上のオプションから選択する必要があるシナリオがあります。複数のネストされたIF文を使用することもできますが、コードの可読性が大幅に低下する可能性があります。このような場合、switch文(セレクターとも呼ばれます)が推奨される解決策です。構文テンプレートは次のとおりです。

switch (integer_variable) 
  {
    case value_1:
      operation_list_1;
    case value_2
      operation_list_2;
    // …
    default:
      default_operation_list;
  }

例6:switch-case文の構造

動作の仕組みはこうです。integer_variableの値が指定されたいずれかの値(value_1、value_2など)と一致する場合、該当するcaseから処理が開始され、その後は順に実行が継続されます。通常は、1つのブロックだけを実行すれば十分なため、各caseブロックの末尾にはbreak文を追加して、switch文の処理を即座に終了させるのが一般的です。どのcaseにも一致しない場合は、defaultセクションが実行されます。defaultセクションは、常にswitch文の最後に配置する必要があります。

MQL5においては、switch-case構文は、EAでの取引エラーの処理や、キーボード操作・マウス操作といったユーザー入力の処理など、さまざまな場面で広く使用されています。以下に、取引エラーを処理する関数の典型的な例を示します。

void PrintErrorDescription()
{

  int lastError = GetLastError();

// If (lastError == 0), there are no errors…
  if(lastError == 0)
    {
      return;  // …no need to load cpu with unnecessary computations.
    }

// If there are errors, output an explanatory message to the log.        
  switch(lastError)
    {
      case ERR_INTERNAL_ERROR:         
        Print("Unexpected internal error"); // You can select any appropriate action here
        break;                
      case ERR_WRONG_INTERNAL_PARAMETER:
        Print("Wrong parameter in the inner call of the client terminal function");
        break;
      case ERR_INVALID_PARAMETER:
        Print("Wrong parameter when calling the system function");
        break;
      case ERR_NOT_ENOUGH_MEMORY:
        Print("Not enough memory to perform the system function");
        break;

      default: 
        Print("I don't know anything about this error");
    } 
}

例7:標準的なエラー処理プロセス


前提条件付きループ(while)

ループとは、あるコードブロックを繰り返し実行できるようにする制御構造の一つです。

プログラミングでは、同じ操作を複数回行う必要がある場面が頻繁にありますが、その際に扱うデータは少しずつ異なることがほとんどです。たとえば、インジケーターの動作を過去のデータ上で視覚化するために、配列(インジケーターバッファなど)のすべての要素を順に処理する場合があります。あるいは、複雑なEAのパラメータをファイルから1行ずつ読み取るといったケースもあります。その他にもさまざまな活用例があります。

MQL5では、用途に応じて使い分けられる3種類のループ構文が用意されています。これらのループは相互に置き換えることが可能であり、基本的にはどの構文を使っても同じ機能を実現できます。ただし、適切なループ構文を選ぶことで、コードの可読性や実行効率を向上させることができます。

その中でも最初に紹介するのが、whileループ(前提条件付きループ)です。この名称は、ループ本体の処理を実行する前に条件判定が行われるという特徴に由来します。視覚的に表現すると、次のようになります。

Whileループ図

図3:whileループ図

この演算子の構文テンプレートは非常にシンプルです。

while (condition)
    action_if_condition_is_true;

例8:whileループテンプレート

ここで、「action_if_condition_is_true」は、単一の文(単文または複合文)です。

プログラムがwhile文に遭遇すると、まず条件式が評価されます。条件がtrueの場合、ループ本体の処理が実行され、その後再び条件が評価されます。このプロセスを、条件がfalseになるまで繰り返します。条件がループの前にチェックされるため、一度もループ本体が実行されないこともあり得ます

while文は、繰り返しの回数が事前に分からないようなケースでよく使用されます。たとえば、テキストファイルを1行ずつ読み込む処理では、ファイルの長さを事前に知ることはできません。プログラムは ファイルの終端マーカー(EOF)に到達するまで読み込みを続け、到達した時点で処理を終了します。このような「終わりが不定の処理」は、プログラミングにおいて非常によく見られる状況です。

いずれの場合も、ループ本体の中で条件に影響を与える何らかの変化が発生する必要があります。もし条件に関係する変数などが一切変化しなければ、無限ループになります。そうなると、ウィンドウを閉じることでしか停止できないこともあります。最悪のケースでは、プロセッサが無限ループでロックされ、キーボード入力やマウス操作さえも受け付けなくなる恐れがあります。その場合、システムを強制再起動しない限りプログラムを終了できなくなる可能性もあります。したがって、必ずプログラムを安全に終了させる手段を残しておくことが重要です。たとえば、「&& !IsStopped()IsStopped())」のような条件を加えることで、ターミナルが終了処理に入った際にループが自動的に停止するようにできます。

等差数列の知識がないと仮定して、whileループを使って1〜5の合計を求める簡単なタスクを実装してみましょう。

whileループを使用すると、この問題は次のように解決されます。

//--- Variables declaration
int a  = 1;  // initialize (!) the variable parameter used in the condition
int sum = 0; // result
string message = "The sum of "+ (string) a; // show message

//--- Perform main operations
while (a <= 5)  // While a is less than 5
  {
    sum += a; // Add the value to the sum
    if ( a != 1) // The first value is already added at the time of initialization
    {
      message += " + " + string (a); // Further operations
    }
    
    a++; // Increase parameter (very important!)
  }

//-- After the loop is completed, output a message
message += " is " + (string) sum;  // message text
Comment (message); // show the comment

例9:whileループの使用

反復回数は少ないですが、この例はwhileループの仕組みを示しています。


事後条件付きループ(do…while)

do…whileループ本体のすべてのアクションが処理された後に条件チェックを使用します。このループは、グラフィカルに次のように表すことができます。

Do...whileループ図

図4:Do-whileループ図

構文テンプレートは次のとおりです。

do
  actions_if_condition_is_true;
while (condition);

例10:do-while文のテンプレート

whileとは異なり、このループ文ではループ本体の処理が少なくとも一度は実行されます。その後、先頭に戻るかどうかを判断するために条件がチェックされ、条件が満たされていれば、再度ループ本体の処理が実行されます。

このループの使用例としては、たとえば気配値表示ウィンドウで取引銘柄を確認するようなケースが考えられます。アルゴリズムの流れとしては次のようになります。

  • リストの最初の銘柄を取得する(その銘柄が確実に存在していると仮定) 
  • 必要な処理を実行する(例:既存の注文を確認する) 
  • リスト内に他の銘柄が存在するかを確認する
  • 別の銘柄が見つかれば、リストが終わるまで処理を継続する

この形式のループで1から5までの数値を合計する場合の処理は、前のwhileループとほとんど同じです。変更されるのは、ループの記述部分の2行だけです。

//--- Variables declaration
int a  = 1;  // initialize (!) the variable parameter used in the condition
int sum = 0; // result
string message = "The sum of "+ (string) a; // show message

//--- Perform main operations
do // Execute
  {
    sum += a;    // Add value to the sum
    if ( a != 1) // The first value is already added at the time of initialization
    {
      message += " + " + string (a); // Further operations
    }
    
    a++;         // Increase parameter (very important!)
  }
while (a <= 5)  // While a is less than 5

//-- After the loop is completed, output a message
message += " is " + (string) sum;  // message text
Comment (message);                 // show the comment

例11:do-whileループの使用


Forループ

forループは、反復処理する要素の数が簡単に計算できる状況で、さまざまな種類のシーケンスを繰り返すために使用される、最も一般的なループ形式です。構文テンプレートは次のとおりです。

for (initialize_counter ; conditions ; change_counter)
  action_if_condition_is_true;

例12:Forループテンプレート

これまでの他のループ形式とは異なり、forループでは「どのパラメータ(このテンプレートではカウンタと呼んでいます)がどのように変化するのか」を明確に確認できます。そのため、ループの本体部分では、メインの処理(たとえば配列の要素を順に処理するなど)に集中できます。

本質的にはこのループもwhileループと同じく、ループ本体の処理が実行される前に条件がチェックされます。以下に、forループを使用して1から5までの数を合計する方法を示します。

//--- Variables declaration
int a;   // do NOT initialize the variable parameter, only describe 
         //   (this is optional as you can describe it in the loop header)
int sum = 0;                    // result
string message = "The sum of "; // a shorter message for the user, 
                                //   as the value is not yet known

//--- Perform main operations
for (a=1; a<=5; a++)// For (each `a` from 1 to 5) /loop header/
  {
    sum += a;    // Add `a` to the sum
    if ( a != 1) // the first value does not contain "+" 
    {
      message += " + " // add "+" before all sequence members starting from the second
    } 
   message += string (a); // Add sequence elements to the message
    
            // Changes to the counter are described not here, but in the loop header as the last parameter.
  }

//-- After the loop is completed, output a message
message += " is " + (string) sum;  // message text
Comment (message);                 // show the comment

例13:forループの使用

この例では、前の例と比べてさらに多くの変更点が見られます(黄色で強調表示されています)。ここで改めて強調したいのは、ループカウンタ(変更可能なパラメータ)に関するすべての処理が、ループヘッダーにまとめられているという点です。つまり、次のステップがヘッダー内で完結しています:変数の初期化(a = 1)、条件のチェック(a <= 5。この条件がtrueであれば、ループ本体が実行されます)、そしてインクリメント(a++)と条件の再評価です。各反復の終了時にカウンタが更新され、それに応じて条件が再びチェックされるという流れになります。


break文とcontinue文

場合によっては、ループ内のすべての処理を最後まで実行する必要がないことがあります。

たとえば、配列の中から特定の値を探す場合、目的の値が途中で見つかれば、それ以降の要素を調べる必要はありません。このようなときにループ自体を完全に終了させるには、break文を使います。breakは、ループの実行を即座に終了し、その後の処理に進ませる制御文です。以下にその例を示します。

string message = "";

for (int i=0; i<=3; i++)
  {
    if ( i == 2 )
      {
        break;
      }
    
    message += " " + IntegerToString( i );
  }

Print(message); // Result: 0 1 (no further execution)

例14:break文の使用

特定の条件下では、ループ本体を実行する必要がないが、ループ自体は終了せず、単に特定の反復処理をスキップしたい場合もあります。例えば、例14では、数字2を出力から除外し、それ以外の数字はそのまま印刷したい場合、breakをcontinueに置き換えることで、コードを少し変更する必要があります。continue文は、即座に次の反復処理にスキップします。

string message = "";

for (int i=0; i<=3; i++)
  {
    if ( i == 2 )
      {
        continue;
      }
    
    message += " " + IntegerToString( i );
  }

Print(message); // Output: 0 1 3 (the number 2 is skipped)

例15:continue文の使用

break文はループ内またはcase文内で使用できますが、continue文はループ内でのみ使用できます。

これで文については終了です。次に、これらの演算子がどのように実際のプログラム作成に役立つかを見てみましょう。


MQL5ウィザード(インジケーターモード)について

最初の記事のウィザードを使用してインジケーターを作成する方法はすでにご存じかもしれませんが、今回は異なる視点でウィザードの手順を簡単に再確認したいと思います。

ウィザードの最初のウィンドウには、最初の記事と比べて新しい内容はないため、スキップします。2番目のウィンドウも新しいものではありませんが、グローバル設定が入力パラメータを通じて管理されることに慣れている今、より理解しやすくなっています。時には、これらのパラメータをウィザードで直接設定する方が直感的に感じられることもあります。注意点として、パラメータ名は任意ですが、コード内で簡単に区別できるようにプレフィックスとして「inp_」を追加することをお勧めします。

入力パラメータの追加

図5:ウィザードを使用してプログラムパラメータを追加する

次の重要なステップは、OnCalculateメソッドの形式を選択することです(図6)。OnCalculateは、MQL5言語のあらゆるインジケーターのメインメソッドで、端末がこのインジケーターを計算するたびに呼び出されます。この関数は、ウィザードでの選択に応じて、異なる入力パラメータのセットを受け取ることができます。

OnCalculateメソッドフォームの選択

図6:OnCalculateメソッドフォームの選択

この場合の関数は、自動生成された配列(始値、高値、安値、終値、出来高、時間)を受け取るため、ほとんどの状況では上側のオプションを選択するのが適しています。ユーザーは必要に応じて、これらの配列を自由に処理できます。

しかしながら、特定のインジケーターを計算する際に、どの価格系列(曲線)を使用するかをユーザー自身が選べるようにしたい特別なケースも存在します。たとえば、移動平均は高値、安値、あるいは他のインジケーターの出力に基づく場合があります。このようなケースでは、下側のオプションを選択する必要があります。このオプションを選ぶと、曲線データを含む配列がOnCalculate関数に渡されるようになり、コンパイル後のインジケーターでは、入力パラメータウィンドウに曲線選択用の特別なタブが表示されます。図7には、上側オプションと下側オプションを選択した際の、既製インジケーターの開始ウィンドウが示されています。

異なるOnCalculateフォームによるインジケーター開始ウィンドウの比較

図7:異なるOnCalculateフォームによるインジケーター開始ウィンドウの比較

そして、ウィザードで注目していただきたい最後のポイントです。インジケーター作成時のダイアログボックスの最終ステップでは、インジケーターをどのように表示するかを正確に選択できます(図8参照)。インジケーターを表示するには、「バッファ」と呼ばれる、描画用データを格納する特別な配列を追加する必要があります。表示スタイルには、単純なライン(たとえば移動平均)から、多色のヒストグラムやローソク足まで、さまざまな種類があります。さらに高度なカスタマイズを行えば、独自のグラフィックを描画することも可能ですが、それはウィザードの機能範囲を超えた作業になります。

インジケーターレンダリングパラメータ

図8:インジケーターのレンダリングパラメータ

いずれにしても、インジケーターに何かを描画させ、かつその動作結果を他のプログラム(たとえばEA)から簡単に取得できるようにするには、最低でも1つのバッファを追加する必要があります。

バッファとは、各ローソク足に対してインジケーターが計算したデータを格納する数値の配列です。これらのデータは、必ずしも描画目的で使用されるとは限りません。バッファには、色指定情報や、他のバッファの計算に必要な中間データが格納される場合もあります。

バッファを手動で追加することも可能ですが、多少の(とはいえ単純な)設定作業が必要になります。初心者の方には、バッファの作成にはウィザードの利用を強くお勧めします。

通常、ラインを描画するには、計算対象のカーブごとに少なくとも1つのバッファが必要です。矢印を描画する場合、通常は方向ごとに1つずつ、合計で最低2つのバッファが必要です。ただし、矢印が単に1本の値のみを示し、方向を表示しないのであれば、1つのバッファだけでも問題ありません。より複雑な表示をおこなう場合は、さらにバッファが必要になることもあります(これについては今後の記事で解説します)。現時点では、ここまでの情報で十分です。

では、インジケーターを作成してみましょう。

  • 名前:InsideOutsideBar 
  • 1つのパラメータ:inp_barsTypeSelector、型int、デフォルト値0、 
  • ウィザードの3画面目では、「配列のリストを受け取る」形式(上部の形式)を選択
  • Plotsセクションでは、UpとDownという名前の2つの描画バッファを追加し、描画タイプはArrowを指定します。


ウィザードによって生成されたインジケーターコード

前のセクションのすべてが正しく実行されていれば、次のコードが得られるはずです。

//+------------------------------------------------------------------+
//|                                             InsideOutsideBar.mq5 |
//|                                       Oleg Fedorov (aka certain) |
//|                                   mailto:coder.fedorov@gmail.com |
//+------------------------------------------------------------------+
#property copyright "Oleg Fedorov (aka certain)"
#property link      "mailto:coder.fedorov@gmail.com"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots   2
//--- plot Up
#property indicator_label1  "Up"
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrMediumPurple
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- plot Down
#property indicator_label2  "Down"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrMediumPurple
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1
//--- input parameters
input int      inp_barsTypeSelector=0;
//--- indicator buffers
double         UpBuffer[];
double         DownBuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,UpBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,DownBuffer,INDICATOR_DATA);
//--- setting a code from the Wingdings charset as the property of PLOT_ARROW
   PlotIndexSetInteger(0,PLOT_ARROW,159);
   PlotIndexSetInteger(1,PLOT_ARROW,159);
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int 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 int &spread[])
  {
//---
   
//--- return value of prev_calculated for the next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

例16:MQL5ウィザードによって生成されたインジケーターコード

ここでの最初のブロックは、通常どおり、パラメータを含むヘッダーです。次に著者を説明する標準パラメータが続きます。これらの後には、インジケーターがチャートウィンドウに配置されることを示す行が続きます。このブロックの最後の2行は、インジケーターに計算用の2つのバッファとレンダリング用の2つのバッファがあることをコンパイラに伝えるメッセージを表しています(ご存じのとおり、この場合これらは同じバッファですが、一般的なケースではバッファの数は異なる場合があります)。

#property indicator_buffers 2
#property indicator_plots   2

例17:計算とレンダリングのためのインジケーターバッファの説明

次のパラメータブロックでは、各バッファによって作成されるプロットの外観を指定します。

//--- plot Up
#property indicator_label1  "Up"             // Display name of the buffers
#property indicator_type1   DRAW_ARROW       // Drawing type - arrow
#property indicator_color1  clrMediumPurple  // Arrow color
#property indicator_style1  STYLE_SOLID      // Line style - solid
#property indicator_width1  1                // Line width (arrow size)
//--- plot Down
#property indicator_label2  "Down"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrMediumPurple
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1

例18:レンダリングパラメータ

次のコードブロックは、インジケーターの入力パラメーターを記述します。この場合、パラメータはinp_barsTypeSelectorの1つだけです。

//--- input parameters
input int      inp_barsTypeSelector=0;

例19:インジケーターの入力パラメータ

次に、レンダリングバッファの変数を作成するコードが続きます。このバッファはdouble型の要素を持つ動的配列を使用することに注意してください。実際、これは価格レベルの配列であり、それに基づいてインジケーターカーブが構築されます。

//--- indicator buffers
double         UpBuffer[];
double         DownBuffer[];

例20:描画バッファの変数

続いて、OnInitとOnCalculateの2つの関数の説明に入ります。

OnInitは、スクリプトとまったく同じ関数です。インジケーターの起動直後、他の関数よりも先に1回だけ実行されます。この例では、ウィザードによってSetIndexBuffer関数が2回呼び出されており、これは配列とインジケーターの描画バッファを関連付けるために使用されます。さらに、PlotIndexSetInteger関数を使って、矢印にアイコンが割り当てられています。OnInit関数は引数を取らず、初期化が成功したかどうかのステータスを返します(この場合は常に「成功」)。

次に OnCalculate ですが、これは前述の通り、毎ティックごと、あるいは他のプログラムがこのインジケーターを呼び出すたびに実行される関数です。現在は空の状態です。この関数の中で、インジケーターの主な処理を記述していきます。

この関数は、以下のような複数の引数を取ります。

  • rates_total:チャート上のローソク足の総数
  • prev_calculated:前回の計算で処理されたバーの数。最初の実行時にはこの値は0ですが、ウィザードが作成した関数の実装では、関数の最後で今回のrates_totalを返しており、次回実行時にその値がprev_calculatedとして渡されます。この仕組みにより、新しいローソク足を検出する基本的なアルゴリズムが可能になります(rates_total == prev_calculatedなら新しいバーは生成されていないため計算不要、そうでなければ処理を実行)。
  • 複数の配列:価格、時間、出来高、スプレッドなどのデータ配列。これらはカスタムインジケーターを作成する際に必要となる情報です。配列は参照渡しで渡されます(値渡しはできないため)が、const修飾子がついており、これによって配列の内容が関数内で変更されないことが保証されます。

それでは、インジケーターのプログラミングを始めましょう。


インジケーターのプログラミング

まず最初に行いたいのは、インジケーターのパラメータを少し調整することです。フォーメーションの種類は、「インサイドバー」か「アウトサイドバー」の2つしかありません。そこで、プリプロセッサディレクティブの直後に、グローバルな列挙型を定義しましょう。

enum BarsTypeSelector
 {
  Inside  = 0, // Inside bar
  Outside = 1  // Outside bar
 };

例21:計算タイプの列挙

入力パラメータのデフォルトの型と値を変更してみましょう。

//--- input parameters
input BarsTypeSelector      inp_barsTypeSelector=Inside;

例22:入力パラメータのデフォルトの型と値を変更する

次に、インジケーターのカスタマイズの可能性を広げていきましょう。アイコンの見た目や、バーからの表示上のインデント(距離)を変更できる機能を追加します。そのために、入力パラメータのセクションに、手動で(パラメータの追加が非常に簡単だということを覚えておくためにも)もう2つの変数を追加しましょう。

//--- input parameters
input BarsTypeSelector      inp_barsTypeSelector=Inside;
input int                   inp_arrowCode=159; 
input int                   inp_arrowShift=5;

例23:外観設定の追加変数

入力パラメータ自体は、それだけではインジケーターの動作に何の影響も与えないことを思い出してください。それらをどこかで使用する必要があります。この変更を適用するために、OnInit関数を修正しましょう。

int OnInit()
 {
//--- indicator buffers mapping
  SetIndexBuffer(0,UpBuffer,INDICATOR_DATA);
  SetIndexBuffer(1,DownBuffer,INDICATOR_DATA);
//--- setting a code from the Wingdings charset as the property of PLOT_ARROW
  PlotIndexSetInteger(0,PLOT_ARROW,inp_arrowCode);
  PlotIndexSetInteger(1,PLOT_ARROW,inp_arrowCode);
  
  PlotIndexSetInteger(0, PLOT_ARROW_SHIFT, inp_arrowShift); 
  PlotIndexSetInteger(1, PLOT_ARROW_SHIFT, -inp_arrowShift);

//---
  return(INIT_SUCCEEDED);
 }

例24:OnInit関数の変更

それでは、OnCalculate関数内での主な処理のプログラミングに移りましょう。コードを少しでも簡潔にするために、関数の定義部分は省略し、実際に有用な処理をおこなうコードの記述から始めます。

/********************************************************************************************
 *                                                                                          *
 * Attention! All arrays in the code are NOT series, so we have larger numbers on the right. *
 *                                                                                          *
 ********************************************************************************************/

//--- Description of variables
  int i,     // Loop counter
      start; // Initial bar for historical data

  const int barsInPattern = 2; // For proper preparation we need to know,
                               //   how many bars will be involved in one check
                               //   I've added a constant to avoid the presence of 
                               //   "magic numbers" that come from nowhere,
                               //   we could use the #define directive instead of the constant

//--- Check the boundary conditions and set the origin
  if(rates_total < barsInPattern)      // If there are not enough bars to work
    return(0);                         // Do nothing

  if(prev_calculated < barsInPattern+1)// If nothing has been calculated yet
   {
    start = barsInPattern;     // Set the minimum possible number of bars to start searching 
   }
  else
   {
    start = rates_total — barsInPattern; // If the indicator has been running for some time,
                                         //   Just count the last two bars
   }

//--- To avoid strange artifacts on the last candlestick, initialize the last elements of the arrays 
//      with EMPTY_VALUE
  UpBuffer[rates_total-1] = EMPTY_VALUE;
  DownBuffer[rates_total-1] = EMPTY_VALUE;

//---
  for(i = start; i<rates_total-1; i++) // Start counting from the starting position 
                                       //   and continue until there are no more closed barsя
                                       // (If we needed to include the last - unclosed - bar, 
                                       //   we would set the condition i<=rates_total-1)
   {
    // First, let's clear both indicator buffers (initialize to an empty value) 
    UpBuffer[i] = EMPTY_VALUE;
    DownBuffer[i] = EMPTY_VALUE;
    
    if(inp_barsTypeSelector==Inside) // If the user wants to display inside bars
     {
      // Check if the current bar is inside
      if(high[i] <= high[i-1] && low[i] >= low[i-1])
       {
        // And if yes, we mark the previous (larger) candlestick
        UpBuffer[i-1] = high[i-1];
        DownBuffer[i-1] = low[i-1];
       }
     }
    else // If outside bars are needed
     {
      // Check if the current bar is outside
      if(high[i] >= high[i-1] && low[i] <= low[i-1])
       {
        // Mark the current candlestick if necessary
        UpBuffer[i] = high[i];
        DownBuffer[i] = low[i];
       }
     }
   }

//--- return value of prev_calculated for the next call
  return(rates_total);

例25:OnCalculate関数本体

コードにはすでに十分なコメントが付いていると思われますので、これ以上の詳しい解説は不要かと思います。もしそうでなければ、コメント欄でお気軽にご質問ください。このコードは、OnCalculate 関数の中括弧{}の間に挿入し、既存の内容はすべて削除してください(例には最終行のreturn演算子も含まれています)。インジケーターの完全な動作コードは、この記事に添付されています。

図9は、インジケーターの動作を視覚的に示しています。左側には、インサイドバーに先行するバーがピンク色で表示されており、右側にはアウトサイドバーがマークされています。

InsideOutsideBarインジケーターの仕組み

図9:InsideOutsideBarインジケーターの仕組み。左側はインサイドバー、右側はアウトサイドバー


結論

この記事で紹介した各種演算子をマスターすれば、MQL5で書かれたほとんどのプログラムを理解できるようになり、自分自身でどんなに複雑なアルゴリズムでも記述できるようになります。基本的に、プログラミングとは算術や代入といった単純な操作、条件による分岐(ifやswitch)、必要な回数だけ処理を繰り返すループに集約されます。関数はこうした操作を効率的に整理する手段を提供し、オブジェクトは関数や外部データを使いやすくまとめるための枠組みとなります。

もはやあなたは初心者ではありません。ただし、ここからMQL5のプロフェッショナルへと進むには、まだ多くの学びが必要です。たとえば、既存のインジケーターを新たな開発で再利用する方法や、実際に自動売買を行うEAを作成する方法などがあります。また、MQL5プラットフォームには、EA向けのグラフィカルインターフェース、「平均足」や「練行足チャート」といった特殊なインジケーター、便利なサービス、自動売買向けの収益性の高い戦略など、習得すべき豊富な機能があります。したがって、この連載は今後も続きます。次回の記事では、EAをどのように作成するかについて、できる限り詳しく解説します。その後、MQL5におけるOOP(オブジェクト指向プログラミング)の考え方を私なりの視点で紹介します。これで「基礎編」は一区切りとなり、その後はプラットフォームの応用機能や、端末に標準で付属しているIncludeフォルダ内のコードライブラリについて深掘りしていく予定です。

以下は、本連載の過去の記事のリストです。

MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/15499

添付されたファイル |
タブーサーチ(TS) タブーサーチ(TS)
この記事では、最初期かつ最も広く知られているメタヒューリスティック手法の一つであるタブーサーチアルゴリズムについて解説します。初期解の選択や近傍解の探索から始め、特にタブーリストの活用に焦点を当てながら、アルゴリズムの動作を詳しく見ていきます。本記事では、タブーサーチの主要な特徴と要素について取り上げます。
MQL5でパラボリックSARと単純移動平均(SMA)を使用した高速取引戦略アルゴリズムを実装する MQL5でパラボリックSARと単純移動平均(SMA)を使用した高速取引戦略アルゴリズムを実装する
この記事では、パラボリックSARと単純移動平均(SMA)インジケーターを活用し、応答性の高い取引戦略を構築する高速取引型エキスパートアドバイザー(EA)をMQL5で開発します。インジケーターの使用方法、シグナルの生成、テストおよび最適化プロセスなど、戦略の実装について詳しく解説します。
USDとEURの指数チャート—MetaTrader 5サービスの例 USDとEURの指数チャート—MetaTrader 5サービスの例
MetaTrader 5サービスを例に、米ドル指数(USDX)およびユーロ指数(EURX)チャートの作成と更新について考察します。サービス起動時には、必要な合成銘柄が存在するかを確認し、未作成であれば新規作成します。その後、それを気配値表示ウィンドウに追加します。続いて、合成銘柄の1分足およびティック履歴を作成し、最後にその銘柄のチャートを表示します。
多通貨エキスパートアドバイザーの開発(第17回):実際の取引に向けたさらなる準備 多通貨エキスパートアドバイザーの開発(第17回):実際の取引に向けたさらなる準備
現在、EAはデータベースを利用して、取引戦略の各インスタンスの初期化文字列を取得しています。しかし、データベースは非常に大容量であり、実際のEAの動作には不要な情報も多数含まれています。そこで、データベースへの接続を必須とせずにEAを機能させる方法を考えてみましょう。