English Русский
preview
MQL5入門(第24回):チャートオブジェクトで取引するEAの構築

MQL5入門(第24回):チャートオブジェクトで取引するEAの構築

MetaTrader 5トレーディング |
28 0
Israel Pelumi Abioye
Israel Pelumi Abioye

はじめに

連載「MQL5入門」の第24回へようこそ。本記事では、手動でのチャート分析と自動取引の実行を組み合わせることで、ひとつ大きなステップを踏み出します。特に今回は、矩形やトレンドライン、サポートおよびレジスタンスラインなどのチャートオブジェクトを基に取引をおこなうEAを構築します。

この手法により、自動取引と裁量取引の間のギャップを埋めることができます。チャート上に要素を描くだけで、事前に決められたテクニカルパラメータに頼らず、視覚的にEAを誘導することが可能になります。描いたオブジェクトを認識した後は、EAが価格変動を監視し、価格がそれらのラインや領域に接触したタイミングで自動的に取引を開始します。

本記事では、チャートオブジェクトをプログラムで検出し、その座標を使ってエントリーや決済を計算する方法も解説します。さらに、エキスパートアドバイザー(EA)を動的に反応させる設定もおこない、チャート上の要素を変更した際に即座に適応するようにします。

 

EAの仕組み

チャートオブジェクトを使って戦略的なゾーンでのトレンド反転を認識するEAを作ります。サポート&レジスタンスEAを構築し、ユーザーはチャート上に矩形オブジェクトを使って、反転が起こり得る場所を手動でマークします。EAはその領域を監視し、価格がそのゾーンに到達するのを待ちます。価格より上に矩形を配置した場合、それはレジスタンスゾーンを示します。

EAは、価格がそのゾーンに到達した後、反転の可能性が形成されていることを確認するために、弱気のChoCh(Change of Character、キャラクターの変化)が発生するのを待ちます。価格が矩形内で反応し、上抜けしなかった場合、かつ弱気のChoChが識別された場合、EAは自動的に売り取引を開始します。

市場が高値更新と安値切り上げ(強気構造)を形成する状態から高値切り下げと安値更新(弱気構造)を形成する状態からに移行したとき、それを弱気のChoChと呼びます。EAは以下の4つの一連の重要な値動きを検出してこれを判断します。

  • 安値
  • 高値
  • 安値切り上げ
  • 高値更新
  • 前の高めの安値を下回るブレイク

強気の構造が破られ、ネガティブな変化が起きたと確認できたら、EAは売り注文を実行します。このとき、価格がレジスタンス矩形の内側または近くにある場合、高い確率で反転が起きると判断します。

図1:レジスタンス

サポートゾーンも同じロジックです。サポートを示すために、ユーザーは価格より下に矩形を描き、強気の反転の可能性を示します。EAは、価格がそのゾーンに到達した際に強気の変化を待ってから取引をおこないます。強気のChoChは次の4つの一連のスイングで識別します。

  • 高値
  • 安値
  • 高値切り下げ
  • 安値更新
  • 前回の高値を上回るブレイク

市場構造が弱気構造から強気構造に変化した場合、最後のより低い高値の上抜けで確認できます。これは強気のの反転の可能性が高いことを示し、EAはサポート矩形内または近くで発生した場合、自動で買い注文を実行します。

図2:サポート


チャートオブジェクトの識別

EAがどの矩形をサポートやレジスタンスとして使うかを決定するため、チャートオブジェクトの識別はこのプロジェクトで非常に重要な部分です。EAは自分で作成したチャートオブジェクトは認識できますが、ユーザーが手動で描いたオブジェクトはそのままでは認識できません。EAは、チャート上に存在する多数のオブジェクト、矩形、トレンドライン、テキストラベル、フォームなどの中から、正確にどれを使うかを判断できる必要があります。

コード内でチャートオブジェクトを作成する場合は、ObjectCreate()関数を使用します。この関数を使うことで、EAはチャート上にさまざまなオブジェクトを描画できます。ただし、作成後はいつでもオブジェクトのプロパティにアクセスしたり変更したりできることに注意が必要です。プロパティには色、スタイル、表示/非表示、時間や価格のレベルなどが含まれます。

ObjectCreate()の詳細な動作については、本連載第9回で解説しています。そこではコード上でチャートオブジェクトを作成する方法をカバーしました。

最も重要なのは、MQL5ではオブジェクトの「名前」がそのオブジェクトに関するすべてを表すことです。オブジェクトがユーザーによって手動で作成されたものであろうと、EAによって自動で作成されたものであろうと、名前さえ分かればそのオブジェクトの情報を取得したり変更したりできます。たとえば、レジスタンスとして使う矩形オブジェクトの上限と下限価格や時間の境界を確認するのは、名前さえ知っていれば簡単です。

そのため、オブジェクト名はEAのロジックとユーザーのチャートをつなぐ「リンク」として機能します。描かれたゾーンの正確な座標をEAが読み取ることで、価格がその領域に入ったかどうかを判定する自動化が可能になります。

ここで問題になるのは、まだ分からないオブジェクト名をコードにどう組み込むかです。ユーザーがチャートにオブジェクトを描く前に、ソフトウェアはすでにコンパイルされ、稼働しています。では、EAはどうやって事前にどのオブジェクト名を使うか知ることができるでしょうか。

答えは、オブジェクトが描かれた後に、チャートから直接オブジェクト名を取得できる点にあります。この方法により、名前を事前にハードコーディングする必要なく、EAはユーザーが作成したオブジェクトを動的に検出できるのです。以下でその仕組みを解説します。

input string h_line = ""; // Horizontal Line

ulong chart_id = ChartID();
double line_price;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   line_price =  ObjectGetDouble(chart_id,h_line,OBJPROP_PRICE,0);

   Comment(line_price);
  }

説明

このアプローチの最初のステップは、ユーザーがチャート上に水平線を引くことを想定することです。そのために、h_lineという文字列型の入力パラメータを作りました。ユーザーはこの入力を使って、自分がチャート上に描いた水平線の正確な名前を手動で入力するかコピーできます。こうすることで、EAはどのオブジェクトを扱うべきかを正確に認識できます。

次に、ChartID()関数を使ってチャートIDを取得します。MetaTrader 5で複数のチャートを開いている場合でも、この方法でEAが正しいチャートとやり取りできるようになります。 入力で指定された名前のオブジェクトの価格レベルを取得するには、OnTick()関数内でObjectGetDouble()関数を使用します。この関数には、チャートID、オブジェクト名、アクセスしたいプロパティ(OBJPROP_PRICE)、およびインデックス値を渡します。水平線の場合、インデックスは0に設定します。

最後に、Comment()関数でオブジェクトの価格レベルをチャート上に直接表示させることで、EAが手動で作成されたオブジェクトを正しく特定し、データを取得できていることを確認します。この方法により、EAはユーザーが提供する任意のオブジェクト名に対して動的に動作できるようになり、使いやすく柔軟な設計になります。

出力

チャート上に描いたオブジェクトの名前を確認するには、オブジェクトを右クリックしてメニューから[Properties]を選びます。プロパティウィンドウに[Name]フィールドが表示されます。この名前を正確にコピーして、EAの入力設定に貼り付けます。必要に応じて、ResistanceZoneやSupportAreaのように名前をより覚えやすいものに変更することも可能です。

図4:オブジェクト名

次に、EAの入力設定に移動し、オブジェクト名を入力欄に貼り付けます。これは先ほど作成した、オブジェクト名を受け取るための入力パラメータと同じものです。

図5:名前入力

こうすることで、手動で作成したチャートオブジェクトをEAに接続できます。名前を入力すると、EAはその特定のオブジェクトを認識し、座標や価格レベルなどの情報を取得して、取引を実行するタイミングを判断するためにそのデータを活用できます。

オブジェクトを特定できるようになったら、次のステップはチャートオブジェクトからデータを取得することです。MQL5では、各チャートオブジェクトには固有のデータがありますが、アクセスできるデータの種類はオブジェクトのタイプによって異なります。 たとえば、水平線はチャート全体に固定レベルで表示されるため、価格情報しか持ちません。

図6:水平線

一方で、垂直線は特定の時間を示すため、時間情報しか持ちません。

図7:垂直線


レジスタンスオブジェクトのデータ取得

このプロジェクトでは、サポートとレジスタンスゾーンを示すために矩形オブジェクトを使うので、矩形は非常に重要です。矩形は時間情報と価格情報の両方を保持できます。各矩形は2つのポイントを持ち、それぞれに時間と価格の座標があります。

図8:アンカーポイント

EAはこれらの座標を取得することで、チャート上で矩形の正確な位置を把握できます。結果として、ユーザーが描いたサポートまたはレジスタンスエリアの時間範囲と価格範囲を特定でき、価格がそのゾーンに入ったときや、取引の可能性があるかどうかを判断するのに不可欠な情報を得られます。

矩形オブジェクトは、対角の2点によって定義され、それぞれに時間と価格の座標があります。矩形の描き方によって、これらのポイントは役割が入れ替わる場合があります。この柔軟性のため、EAは矩形からデータを取得するときに、片方のアンカーが必ずゾーンの上端や下端を表すと仮定してはいけません。サポートまたはレジスタンスエリアを正しく特定するためには、両方の座標を確認し、価格の低い方を下限、価格の高い方を上限として扱う必要があります。

input string reistance = ""; // Resistance Object Name

ulong chart_id = ChartID();

double res_anchor1_price;
double res_anchor2_price;

double res_max_price;
double res_min_price;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   res_anchor1_price = NormalizeDouble(ObjectGetDouble(chart_id,reistance,OBJPROP_PRICE,0),_Digits);
   res_anchor2_price = NormalizeDouble(ObjectGetDouble(chart_id,reistance,OBJPROP_PRICE,1),_Digits);

   res_max_price = NormalizeDouble(MathMax(res_anchor1_price,res_anchor2_price),_Digits);
   res_min_price = NormalizeDouble(MathMin(res_anchor1_price,res_anchor2_price),_Digits);

   Comment("Anchor 1 Price: ",res_anchor1_price,"\nAnchor 2 Price: ",res_anchor2_price,
           "\n\nMax Resistance Price: ",res_max_price,"\nMin Resistance Price: ",res_min_price);
  }

出力

図9:レジスタンスの最大値と最小値

説明

オブジェクト名は入力パラメータとして用意されています。これにより、ユーザーはチャート上の矩形オブジェクトの名前を手動でコピーしてEAの入力設定に貼り付けることができます。その後、矩形の2つのアンカーの価格レベルは2つの変数に格納されます。アンカー1とアンカー2はそれぞれ矩形の2つの角を表し、各角には時間と価格の座標があります。EAは両方のアンカーの価格を取得するため、このプロジェクトでは価格だけを使ってレジスタンスゾーンを定義できます。

矩形の2つのアンカーの価格は、ObjectGetDouble関数を使ってEAが読み取ります。ポイント1はこの関数のインデックス値0で、ポイント2はインデックス値1で指定します。両方のアンカーにはそれぞれ時間と価格の座標がありますが、今回は価格レベルだけが必要なので、EAは価格データのみを取得します。

EAは取得した価格を比較して、どちらが高くどちらが低いかを判断します。レジスタンスゾーンの上限は最大価格、下限は最小価格で表されます。

このロジックにより、ユーザーが矩形を上から下に描いた場合でも下から上に描いた場合でも、EAは正しくゾーンの境界を読み取れます。この2つの境界は、プロジェクト中の価格の動きを追跡するために使用されます。売り注文を出す前に、EAは価格がレジスタンスゾーンに近づく際の市場の弱気の変化を待機します。

さらに、EAは2つのアンカーの時間値を比較することで、どちらが矩形の開始でどちらが終了かを判断できます。これにより、矩形の水平位置を正しく解釈し、現在の市場時間が描かれたゾーンの有効範囲内にある場合にのみ反応するようにできます。

input string reistance = ""; // Resistance Object Name

ulong chart_id = ChartID();

double res_anchor1_price;
double res_anchor2_price;

double res_max_price;
double res_min_price;

long res_anchor1_time;
long res_anchor2_time;

datetime res_start_time;
datetime res_end_time;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   res_anchor1_price = NormalizeDouble(ObjectGetDouble(chart_id,reistance,OBJPROP_PRICE,0),_Digits);
   res_anchor2_price = NormalizeDouble(ObjectGetDouble(chart_id,reistance,OBJPROP_PRICE,1),_Digits);

   res_max_price = NormalizeDouble(MathMax(res_anchor1_price,res_anchor2_price),_Digits);
   res_min_price = NormalizeDouble(MathMin(res_anchor1_price,res_anchor2_price),_Digits);

   res_anchor1_time =  ObjectGetInteger(chart_id,reistance,OBJPROP_TIME,0);
   res_anchor2_time = ObjectGetInteger(chart_id,reistance,OBJPROP_TIME,1);

   res_start_time = (datetime)MathMin(res_anchor1_time,res_anchor2_time);
   res_end_time = (datetime)MathMax(res_anchor1_time,res_anchor2_time);

   Comment("RESISTANCE START TIME: ",res_start_time,"\nRESISTANCE END TIME: ",res_end_time);
  }

出力

図10:レジスタンスの開始時間と終了時間

矩形の最初と2番目のポイント(インデックス0と1)を特定することで、この部分ではレジスタンス矩形の時間座標を取得します。その後、早い方を開始時刻、遅い方を終了時刻として設定し、どちらが先かを比較します。これにより、ユーザーが矩形をどのように描いた場合でも、EAはレジスタンスゾーンの時間範囲を正確に判定できます。

 

弱気のChoCh

次のステップは、レジスタンスゾーンからの反転の可能性を示す弱気のChoChを見つけることです。EAは価格の動きを追跡し、顕著な構造変化が発生しているかを確認します。具体的には、ユーザーが描いたレジスタンスゾーン内に市場の最高値が含まれているかを検証します。これにより、価格が実際にゾーンと接触しており、そのゾーンが有効であることを保証できます。弱気のChoChは、市場がそのレジスタンスゾーン内で最高値を形成し、その後、直前の安値切り上げを下抜けしたときに確認されます。これにより、そのゾーンからの下方向の反転の可能性が示されます。

input string reistance = ""; // Resistance Object Name
input ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT; // TIME-FRAME

ulong chart_id = ChartID();

double res_anchor1_price;
double res_anchor2_price;

double res_max_price;
double res_min_price;

long res_anchor1_time;
long res_anchor2_time;

datetime res_start_time;
datetime res_end_time;

int res_total_bars;
double res_close[];
double res_open[];
double res_high[];
double res_low[];
datetime res_time[];

double high;
datetime high_time;
double low;
datetime low_time;
double higher_low;
datetime higher_low_time;
double higher_high;
datetime higher_high_time;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   ArraySetAsSeries(res_close,true);
   ArraySetAsSeries(res_open,true);
   ArraySetAsSeries(res_high,true);
   ArraySetAsSeries(res_low,true);
   ArraySetAsSeries(res_time,true);

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   Comment("");
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   res_anchor1_price = NormalizeDouble(ObjectGetDouble(chart_id,reistance,OBJPROP_PRICE,0),_Digits);
   res_anchor2_price = NormalizeDouble(ObjectGetDouble(chart_id,reistance,OBJPROP_PRICE,1),_Digits);

   res_max_price = NormalizeDouble(MathMax(res_anchor1_price,res_anchor2_price),_Digits);
   res_min_price = NormalizeDouble(MathMin(res_anchor1_price,res_anchor2_price),_Digits);

   res_anchor1_time =  ObjectGetInteger(chart_id,reistance,OBJPROP_TIME,0);
   res_anchor2_time = ObjectGetInteger(chart_id,reistance,OBJPROP_TIME,1);

   res_start_time = (datetime)MathMin(res_anchor1_time,res_anchor2_time);
   res_end_time = (datetime)MathMax(res_anchor1_time,res_anchor2_time);

   res_total_bars = Bars(_Symbol,timeframe,TimeCurrent(),res_start_time);

   CopyOpen(_Symbol, timeframe, TimeCurrent(), res_start_time, res_open);
   CopyClose(_Symbol, timeframe, TimeCurrent(), res_start_time, res_close);
   CopyLow(_Symbol, timeframe, TimeCurrent(), res_start_time,  res_low);
   CopyHigh(_Symbol, timeframe, TimeCurrent(), res_start_time, res_high);
   CopyTime(_Symbol, timeframe, TimeCurrent(), res_start_time, res_time);

   for(int i = 4; i < res_total_bars-3; i++)
     {
      if(IsSwingHigh(res_high, i, 3))
        {
         higher_high = res_high[i];
         higher_high_time = res_time[i];

         for(int j = i; j < res_total_bars-3; j++)
           {
            if(IsSwingLow(res_low,j,3))
              {
               higher_low = res_low[j];
               higher_low_time = res_time[j];

               for(int k = j; k < res_total_bars-3; k++)
                 {
                  if(IsSwingHigh(res_high, k, 3))
                    {
                     high = res_high[k];
                     high_time = res_time[k];

                     for(int l = k; l < res_total_bars-3; l++)
                       {
                        if(IsSwingLow(res_low,l,3))
                          {
                           //   ObjectCreate(chart_id,"kk",OBJ_VLINE,0,res_time[l],0);
                           ObjectDelete(chart_id,"kk");

                           low = res_low[l];
                           low_time = res_time[l];

                           for(int m = i; m > 0; m--)
                             {
                              if(res_close[m] < higher_low && res_open[m] > higher_low)
                                {
                                 if(higher_low < higher_high && high > higher_low && high < higher_high && low < high && low < higher_low)
                                   {
                                    ObjectCreate(chart_id,"HHHL",OBJ_TREND,0,higher_high_time,higher_high,higher_low_time,higher_low);
                                    ObjectSetInteger(chart_id,"HHHL",OBJPROP_COLOR,clrRed);
                                    ObjectSetInteger(chart_id,"HHHL",OBJPROP_WIDTH,2);

                                    ObjectCreate(chart_id,"HLH",OBJ_TREND,0,higher_low_time,higher_low,high_time,high);
                                    ObjectSetInteger(chart_id,"HLH",OBJPROP_COLOR,clrRed);
                                    ObjectSetInteger(chart_id,"HLH",OBJPROP_WIDTH,2);

                                    ObjectCreate(chart_id,"HL",OBJ_TREND,0,high_time,high,low_time,low);
                                    ObjectSetInteger(chart_id,"HL",OBJPROP_COLOR,clrRed);
                                    ObjectSetInteger(chart_id,"HL",OBJPROP_WIDTH,2);

                                    ObjectCreate(chart_id,"Cross Line",OBJ_TREND,0,higher_low_time,higher_low,res_time[m],higher_low);
                                    ObjectSetInteger(chart_id,"Cross Line",OBJPROP_COLOR,clrRed);
                                    ObjectSetInteger(chart_id,"Cross Line",OBJPROP_WIDTH,2);
                                   }

                                 break;
                                }
                             }

                           break;
                          }
                       }

                     break;
                    }
                 }

               break;
              }
           }

         break;
        }
     }
  }

//+------------------------------------------------------------------+
//| FUNCTION FOR SWING LOW                                           |
//+------------------------------------------------------------------+
bool IsSwingLow(const double &low_price[], int index, int lookback)
  {
   for(int i = 1; i <= lookback; i++)
     {
      if(low_price[index] > low_price[index - i] || low_price[index] > low_price[index + i])
         return false;
     }
   return true;
  }

//+------------------------------------------------------------------+
//| FUNCTION FOR SWING HIGH                                          |
//+------------------------------------------------------------------+
bool IsSwingHigh(const double &high_price[], int index, int lookback)
  {
   for(int i = 1; i <= lookback; i++)
     {
      if(high_price[index] < high_price[index - i] || high_price[index] < high_price[index + i])
         return false; // If the current high is not the highest, return false.
     }
   return true;
  }

出力

図11:ChoCh

説明

ユーザーはまず、EAが監視するチャートの時間足をtimeframe入力パラメータで選択できます。任意のサポートされている期間に変更可能ですが、デフォルトでは現在のチャートウィンドウの時間足が使用されます。これは重要です。なぜなら、この時間足がその後の市場構造の識別、タイムスタンプ、スイング検出、バーのカウントなど、すべての処理に使われるためです。どの時間足を選ぶかは設計上の判断であり、ノイズと反応速度のバランスを考慮する必要があります。

初期化処理で、履歴データを格納する配列は時系列として設定されます。時系列として設定されると、古いバーほどインデックスが大きくなり、最新のバーはインデックス0に格納されます。この順序のおかげで、最新のスイングを確認する際に配列を新しいバーから古いバーに向かって順番に処理するのが簡単になります。また、インデックス0が最新の確定バーであることを前提にループ処理を組むことができ、MQL5のリアルタイムシリーズデータの扱いと整合します。

次に、EAは矩形の開始位置から現在時刻までのバー数を計算します。このバー数に応じてコピーするデータの範囲が決まります。バー数がわかったら、EAはその範囲の各バーについて始値、高値、安値、終値、時間の配列を複製します。この複製された配列がEAの分析対象データセットになります。データを矩形の時間範囲に限定することで、EAはユーザーが描いたゾーンと実際に価格がタッチした部分だけを考慮します。

EAはスイングハイとスイングローを検出する小さなヘルパー関数も使用します。各関数では、候補となるローソク足の左右にある一定本数のローソク足と比較をおこないます。スイングローの関数は、候補の安値が左右両方の指定されたローソク足よりも低い場合にのみtrueを返します。一方、スイングハイの関数は、候補の高値が左右両方のローソク足よりも高い場合にtrueを返します。このように両側にルックバックウィンドウを設けることで、検出されるスイングがランダムなノイズではなく、局所的に意味のあるピボットであることを保証し、単発のティックによる誤検知を防ぎます。

すべてを組み合わせると、処理は次のように進みます。EAはまず、複製された価格配列をチェックしてスイング高値の候補を探します。1つ見つかると、その後に続くスイングローを探し、これを安値切り上げの候補とします。次に、さらにスイングハイを探し、その後に続くスイングローを探します。これら4つのスイングポイントが想定されるパターンとして揃った後、EAは明確なブレイクを探します。具体的には、直前に特定した安値切り上げを下抜けて終値が確定するローソク足があるかを確認します。このブレイクが、市場のキャラクターが変化した証拠として扱われます。

その後、EAは各スイング間の関係性を確認します。たとえば、安値切り上げが実際に高値更新よりも下に位置しているか、また後続の高値や安値が想定される市場構造に沿った配置になっているかを検証します。

弱気のChoChの正確性を確認するために、次に2つの重要なチェックをおこなう必要があります。まず、矩形の開始時刻以降に、どのバーもレジスタンスの最大価格を上抜けしていないかを判定します。もし上抜けが発生していれば、そのレジスタンスはもはや有効ではありません。 そのため、矩形の開始位置から現在のバーまでをループ処理し、すべての高値を収集したうえで、その中で最も高い高値がどれかを判断します。 

int max_high_index;
double max_high;
if(higher_low < higher_high && high > higher_low && high < higher_high && low < high && low < higher_low)
  {

   ObjectCreate(chart_id,"HHHL",OBJ_TREND,0,higher_high_time,higher_high,higher_low_time,higher_low);
   ObjectSetInteger(chart_id,"HHHL",OBJPROP_COLOR,clrRed);
   ObjectSetInteger(chart_id,"HHHL",OBJPROP_WIDTH,2);

   ObjectCreate(chart_id,"HLH",OBJ_TREND,0,higher_low_time,higher_low,high_time,high);
   ObjectSetInteger(chart_id,"HLH",OBJPROP_COLOR,clrRed);
   ObjectSetInteger(chart_id,"HLH",OBJPROP_WIDTH,2);

   ObjectCreate(chart_id,"HL",OBJ_TREND,0,high_time,high,low_time,low);
   ObjectSetInteger(chart_id,"HL",OBJPROP_COLOR,clrRed);
   ObjectSetInteger(chart_id,"HL",OBJPROP_WIDTH,2);

   ObjectCreate(chart_id,"Cross Line",OBJ_TREND,0,higher_low_time,higher_low,res_time[m],higher_low);
   ObjectSetInteger(chart_id,"Cross Line",OBJPROP_COLOR,clrRed);
   ObjectSetInteger(chart_id,"Cross Line",OBJPROP_WIDTH,2);

   max_high_index = ArrayMaximum(res_high,0,res_total_bars);
   max_high = res_high[max_high_index];
   Comment(max_high);
  }

出力

図12:最大高

説明

選択された範囲内での最高価格と、その配列内での位置は2つの変数によって管理されます。一方の変数には、そのインデックスに対応する実際の価格が保持され、もう一方には最高値を記録したインデックス(位置)が保存されます。プログラムはまず配列内で最も高い価格を持つインデックスを特定し、その後、そのインデックスに対応する価格値を取得します。

この情報を使って、レジスタンスゾーンの有効性を2つの条件で検証できます。1つ目の条件は、最高値がレジスタンスの最大価格よりも低い場合、つまり、どのローソク足もレジスタンスゾーンを上抜けしていないことです。2つ目の条件は、キャラクター変化時の最高値が、レジスタンスの最小価格より高く、かつ最大価格より低いことです。これは、市場が実際にレジスタンスゾーンに到達して反応していることを示しており、そこから弱気のChoChが起こる可能性のある正当な反応ポイントであることを意味します。

if(higher_low < higher_high && high > higher_low && high < higher_high && low < high && low < higher_low)
  {

   ObjectCreate(chart_id,"HHHL",OBJ_TREND,0,higher_high_time,higher_high,higher_low_time,higher_low);
   ObjectSetInteger(chart_id,"HHHL",OBJPROP_COLOR,clrRed);
   ObjectSetInteger(chart_id,"HHHL",OBJPROP_WIDTH,2);

   ObjectCreate(chart_id,"HLH",OBJ_TREND,0,higher_low_time,higher_low,high_time,high);
   ObjectSetInteger(chart_id,"HLH",OBJPROP_COLOR,clrRed);
   ObjectSetInteger(chart_id,"HLH",OBJPROP_WIDTH,2);

   ObjectCreate(chart_id,"HL",OBJ_TREND,0,high_time,high,low_time,low);
   ObjectSetInteger(chart_id,"HL",OBJPROP_COLOR,clrRed);
   ObjectSetInteger(chart_id,"HL",OBJPROP_WIDTH,2);

   ObjectCreate(chart_id,"Cross Line",OBJ_TREND,0,higher_low_time,higher_low,res_time[m],higher_low);
   ObjectSetInteger(chart_id,"Cross Line",OBJPROP_COLOR,clrRed);
   ObjectSetInteger(chart_id,"Cross Line",OBJPROP_WIDTH,2);

   max_high_index = ArrayMaximum(res_high,0,res_total_bars);
   max_high = res_high[max_high_index];

   if(max_high < res_max_price && higher_high > res_min_price && higher_high < res_max_price)
     {

     }
  }


取引の実行

高値更新がレジスタンスゾーン内にあること、そしてすべての前提条件が満たされていることを確認した後、次におこなうのが取引の実行です。これは、正当な弱気のChoChが発生し、市場がレジスタンスエリアを意識して反応したことを示しています。戦略のルールに基づき、EAは売りポジションを建てる、またはあらかじめ定義された他の取引アクションを実行できます。テイクプロフィット(TP)はユーザーが指定したリスクリワード比に基づいて計算され、ストップロス(SL)は高値更新ではなく、ChoChが発生した時点の高値に設定されます。

#include <Trade/Trade.mqh>
CTrade trade;
int MagicNumber = 533915;  // Unique Number
double lot_size = 0.2; // Lot Size
double ask_price;
double take_profit;
datetime lastTradeBarTime = 0;
if(max_high < res_max_price && higher_high > res_min_price && higher_high < res_max_price)
  {

   if(res_time[1] == res_time[m] && currentBarTime != lastTradeBarTime)
     {
      take_profit = MathAbs(ask_price - ((high - ask_price) * RRR));

      trade.Sell(lot_size,_Symbol,ask_price,high,take_profit);
      lastTradeBarTime = currentBarTime;
     }

  }

出力

図13:取引執行

説明

すべての取引条件が満たされた後、この部分のソフトウェアは取引の計画と実行を担当します。まず、売買処理をおこなうために必要な機能を提供するTradeライブラリをインクルードします。その後、EAがポジションの新規作成、変更、決済をおこなえるように、取引クラスのインスタンスを生成します。このEAがおこなう各取引には固有のマジックナンバーが割り当てられ、他のEAや手動ポジションの取引と干渉しないように管理されます。1回の注文で取引されるボリュームは、ロットサイズによって決定されます。

同一のローソク足内で複数の取引が実行されるのを防ぐために、プログラムでは売り注文を実行できる現在価格であるAsk価格、TPレベル、そして直前に実行された取引の時間を保存する変数も定義します。その後、現在のバーの時間を記録し、対象銘柄の最新のAsk価格を取得します。

取引を実行する前に、EAは2つの点を確認します。1つ目は、その時間がレジスタンスゾーンの正確な検証ポイントに対応しているかどうかです。2つ目は、現在のバーの時間が前回取引を行ったローソク足の時間と異なっているかどうかで、これにより同一ローソク足での重複エントリーを防ぎます。これらの条件が満たされた場合、EAはユーザーが指定したリスクリワードレシオ(RRR)に基づいてTPを計算し、売りを実行します。TPは計算された比率を用いて設定され、SLはChoChが発生した時点の高値に設定されます。

 

サポートゾーンにおける強気のChoCh

レジスタンスゾーンで弱気のChoChを特定する際に使用したロジックの大部分は、ここでも同様に適用されます。そのため、このセクションでは詳細な説明は省略します。ただし、誤解や不具合を防ぐために、いくつかの重要なポイントには触れておく必要があります。そこで本章では、要点に絞り、主に基本的な違いに焦点を当てて説明します。

input string support = ""; // Support Object Name
double sup_anchor1_price;
double sup_anchor2_price;

double sup_max_price;
double sup_min_price;

long sup_anchor1_time;
long sup_anchor2_time;

datetime sup_start_time;
datetime sup_end_time;

int sup_total_bars;
double sup_close[];
double sup_open[];
double sup_high[];
double sup_low[];
datetime sup_time[];

double lower_high;
datetime lower_high_time;
double lower_low;
datetime lower_low_time;

int min_low_index;
double min_low;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   ArraySetAsSeries(res_close,true);
   ArraySetAsSeries(res_open,true);
   ArraySetAsSeries(res_high,true);
   ArraySetAsSeries(res_low,true);
   ArraySetAsSeries(res_time,true);

   ArraySetAsSeries(sup_close,true);
   ArraySetAsSeries(sup_open,true);
   ArraySetAsSeries(sup_high,true);
   ArraySetAsSeries(sup_low,true);
   ArraySetAsSeries(sup_time,true);

//---
   return(INIT_SUCCEEDED);
  }
sup_anchor1_price = NormalizeDouble(ObjectGetDouble(chart_id,support,OBJPROP_PRICE,0),_Digits);
sup_anchor2_price = NormalizeDouble(ObjectGetDouble(chart_id,support,OBJPROP_PRICE,1),_Digits);

sup_max_price = NormalizeDouble(MathMax(sup_anchor1_price,sup_anchor2_price),_Digits);
sup_min_price = NormalizeDouble(MathMin(sup_anchor1_price,sup_anchor2_price),_Digits);

sup_anchor1_time =  ObjectGetInteger(chart_id,support,OBJPROP_TIME,0);
sup_anchor2_time = ObjectGetInteger(chart_id,support,OBJPROP_TIME,1);

sup_start_time = (datetime)MathMin(sup_anchor1_time,sup_anchor2_time);
sup_end_time = (datetime)MathMax(sup_anchor1_time,sup_anchor2_time);

sup_total_bars = Bars(_Symbol,timeframe,TimeCurrent(),sup_start_time);

CopyOpen(_Symbol, timeframe, TimeCurrent(), sup_start_time, sup_open);
CopyClose(_Symbol, timeframe, TimeCurrent(), sup_start_time, sup_close);
CopyLow(_Symbol, timeframe, TimeCurrent(), sup_start_time,  sup_low);
CopyHigh(_Symbol, timeframe, TimeCurrent(), sup_start_time, sup_high);
CopyTime(_Symbol, timeframe, TimeCurrent(), sup_start_time, sup_time);

for(int i = 4; i < sup_total_bars-3; i++)
  {
   if(IsSwingLow(sup_low, i, 3))
     {
      lower_low = sup_low[i];
      lower_low_time = sup_time[i];

      for(int j = i; j < sup_total_bars-3; j++)
        {
         if(IsSwingHigh(sup_high,j,3))
           {
            lower_high = sup_high[j];
            lower_high_time = sup_time[j];

            for(int k = j; k < sup_total_bars-3; k++)
              {
               if(IsSwingLow(sup_low, k, 3))
                 {
                  low = sup_low[k];
                  low_time = sup_time[k];

                  for(int l = k; l < sup_total_bars-3; l++)
                    {
                     if(IsSwingHigh(sup_high,l,3))
                       {
                        high = sup_high[l];
                        high_time = sup_time[l];

                        for(int m = i; m > 0; m--)
                          {
                           if(sup_close[m] > lower_high && sup_open[m] < lower_high)
                             {
                              if(lower_high > lower_low && low < lower_high && low > lower_low && high > low && high > lower_high)
                                {

                                 ObjectCreate(chart_id,"LLLH",OBJ_TREND,0,lower_low_time,lower_low,lower_high_time,lower_high);
                                 ObjectSetInteger(chart_id,"LLLH",OBJPROP_COLOR,clrRed);
                                 ObjectSetInteger(chart_id,"LLLH",OBJPROP_WIDTH,2);

                                 ObjectCreate(chart_id,"LHL",OBJ_TREND,0,lower_high_time,lower_high,low_time,low);
                                 ObjectSetInteger(chart_id,"LHL",OBJPROP_COLOR,clrRed);
                                 ObjectSetInteger(chart_id,"LHL",OBJPROP_WIDTH,2);

                                 ObjectCreate(chart_id,"LH",OBJ_TREND,0,low_time,low,high_time,high);
                                 ObjectSetInteger(chart_id,"LH",OBJPROP_COLOR,clrRed);
                                 ObjectSetInteger(chart_id,"LH",OBJPROP_WIDTH,2);

                                 ObjectCreate(chart_id,"S Cross Line",OBJ_TREND,0,lower_high_time,lower_high,sup_time[m],lower_high);
                                 ObjectSetInteger(chart_id,"S Cross Line",OBJPROP_COLOR,clrRed);
                                 ObjectSetInteger(chart_id,"S Cross Line",OBJPROP_WIDTH,2);

                                 min_low_index = ArrayMinimum(sup_low,0,sup_total_bars);
                                 min_low = sup_low[min_low_index];

                                 if(min_low > sup_min_price && lower_low < sup_max_price && lower_low > sup_min_price)
                                   {
                                    if(sup_time[1] == sup_time[m] && currentBarTime != lastTradeBarTime)
                                      {
                                       take_profit = MathAbs(ask_price + ((ask_price - low) * RRR));

                                       trade.Buy(lot_size,_Symbol,ask_price,low,take_profit);
                                       lastTradeBarTime = currentBarTime;

                                      }
                                   }
                                }
                              break;
                             }
                          }
                        break;
                       }
                    }
                  break;
                 }
              }
            break;
           }
        }
      break;
     }
  }

説明

このプログラム部分で使用されるロジックは、レジスタンスゾーンの場合とほぼ同じです。主な違いは、価格の動きと市場構造の方向にあります。サポートゾーンでは強気のChoChを期待するため、市場は上方向に動き始め、下側のエリアを否定する形になる必要があります。

コード上の重要なポイントとして、特にサポートゾーン用のローソク足データを保持するために、別の配列セットを宣言しています。これには、ローソク足データ用の配列が含まれます。サポート用の矩形オブジェクトの開始時刻は、レジスタンス用の矩形の開始時刻と異なることが多いため、新しい配列を使用します。配列を分けることで、分析対象のデータがチャート上で選択されたサポートゾーンと正確に一致するようになります。その結果、EAは各ゾーンを独立して解析でき、2つのゾーン間でのデータの混在や混乱を防げます。

注意

本記事で紹介している戦略は、あくまでプロジェクトベースであり、実践的な例を通してMQL5を学ぶことを目的としています。実際の取引で利益が出ることを保証する手法ではありません。


結論

この記事では、手動のチャート分析と自動売買を組み合わせ、サポートやレジスタンスゾーンといったチャートオブジェクトに反応するEAを作成しました。自動売買と裁量取引のギャップを埋めるために、チャートデータを取得して解釈し、それをもとに取引を自動実行する方法を学びました。 

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

添付されたファイル |
知っておくべきMQL5ウィザードのテクニック(第84回):ストキャスティクスとFrAMAのパターンの使用 - 結論 知っておくべきMQL5ウィザードのテクニック(第84回):ストキャスティクスとFrAMAのパターンの使用 - 結論
ストキャスティクスとフラクタル適応型移動平均(FrAMA: Fractal Adaptive Moving Average)は、互いに補完し合う特性を持っており、MQL5のエキスパートアドバイザー(EA)で使えるインジケーターペアの1つです。この組合せについては前回の記事で紹介しましたが、今回はその締めくくりとして、残る5つのシグナルパターンを検討していきます。これらの検証にあたっては、これまでと同様にMQL5ウィザードを用いて構築およびテストをおこないます。
MQL5における二変量コピュラ(第1回):依存関係モデリングのための正規コピュラおよびtコピュラの実装 MQL5における二変量コピュラ(第1回):依存関係モデリングのための正規コピュラおよびtコピュラの実装
本記事は、MQL5における二変量コピュラ(Bivariate Copula)の実装を紹介する連載の第1回です。本記事では、正規コピュラおよびtコピュラ(スチューデントtコピュラ)の実装コードを取り上げます。また、統計的コピュラの基礎概念や関連トピックについても解説します。本記事で紹介するコードは、Hudson and Thamesが提供するArbitragelab Pythonパッケージを参考にしています。
MQL5で自己最適化エキスパートアドバイザーを構築する(第15回):線形系同定 MQL5で自己最適化エキスパートアドバイザーを構築する(第15回):線形系同定
取引戦略の改善は困難な課題です。その大きな理由の一つは、戦略がどこで、なぜ誤作動しているのかを私たち自身が十分に理解できていない点にあります。本記事では、制御理論の一分野である線形系同定を紹介します。線形帰還系(フィードバックシステム)は、データから学習することでシステムの誤差を特定し、その挙動を意図した結果へと導くことができます。これらの手法は、必ずしも完全に解釈可能な説明を与えるものではありませんが、制御系が存在しない状態と比べれば、はるかに有用です。本記事では、線形系同定がどのようにアルゴリズムトレーダーを支援し、取引アプリケーションを制御下に保つことができるのかを探っていきます。
MQL5入門(第23回):オープニングレンジブレイクアウト戦略の自動化 MQL5入門(第23回):オープニングレンジブレイクアウト戦略の自動化
この記事では、MQL5でオープニングレンジブレイクアウト(ORB)エキスパートアドバイザー(EA)を作成する方法を解説します。EAが市場の初期レンジからのブレイクアウトをどのように検知し、それに応じてポジションを建てるかを説明します。また、建てるポジションの数を制御したり、指定した時間で自動的に取引を停止する方法についても学べます。