English Deutsch
preview
MQL5での取引戦略の自動化(第2回):一目均衡表とオーサムオシレーターを備えた雲抜けシステム

MQL5での取引戦略の自動化(第2回):一目均衡表とオーサムオシレーターを備えた雲抜けシステム

MetaTrader 5トレーディング | 15 4月 2025, 07:35
63 0
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の記事(連載第1回)では、Profitunityシステム(ビル・ウィリアムズ著『Trading Chaos』)の自動化方法を紹介しました。 本記事(第2回)では、「雲抜け戦略」をMetaQuotes Language 5 (MQL5)を用いて完全に機能するEAへと構築する方法を解説します。雲抜け戦略は、一目均衡表を用いて、市場の反転やトレンドの継続を判断するための手法です。価格が「雲(Kumo)」と呼ばれる領域、すなわち先行スパンAと先行スパンBの間に形成される動的な支持/抵抗ゾーンを上抜けまたは下抜けることで、トレンドの転換シグナルを捉えます。加えて、オーサムオシレーターをトレンド確認用のフィルターとして活用することで、誤ったシグナルの除外とエントリー/エグジット精度の向上を図ります。この戦略は、強いモメンタムに基づく値動きから利益を狙うトレーダーに広く活用されています。

本記事では、戦略ロジックのコーディングから取引管理、トレーリングストップによるリスク制御の強化までの一連のプロセスを紹介します。記事を読み終える頃には、この戦略を自動化し、MQL5のストラテジーテスターを使ってそのパフォーマンスを検証・最適化するための方法が明確に理解できるはずです。理解を深めやすくするために、以下のセクションに分けて構成しています。

  1. 雲抜け戦略の概要
  2. MQL5による雲抜け戦略の実装
  3. 戦略のテストと最適化
  4. 結論


雲抜け戦略の概要

雲抜け戦略は、雲の範囲を価格が突破した際のトレンド発生を狙う、トレンドフォロー型の手法です。「雲」とは、一目均衡表において先行スパンAと先行スパンBの間に描かれる網掛け領域で、動的な支持および抵抗として機能します。価格が雲を上抜けた場合は、上昇トレンドの兆候と見なされ、逆に下抜けた場合は下降トレンドの可能性を示唆します。なお、この記事で使用する一目均衡表のパラメータ設定は、転換線=8、基準線=29、先行スパンB=34です。以下は設定を示します。

一目均衡表設定

誤シグナルを排除するために、本戦略ではオーサムオシレーターも組み合わせて使用し、取引エントリーの追加確認をおこないます。オーサムオシレーターは、中央値に基づき、34期間と5期間の単純移動平均の差を算出することで、モメンタムの変化を捉えるインジケーターです。オシレーターがマイナス領域からプラス領域へクロスした場合は買いシグナル、プラス領域からマイナス領域へクロスした場合は売りシグナルとして認識されます。このように、雲のブレイクアウトとオーサムオシレーターによるモメンタム確認を組み合わせることで、誤シグナルの発生を抑え、取引の成功確率を高めることを目指しています。

両者を組み合わせると、チャート上では以下のように表示されます。

エントリー条件

ポジションのクローズには、モメンタムの変化を捉えるロジックを使用します。オシレーターがプラス領域からマイナス領域へクロスした場合は、強気モメンタムの減退を示しており、既存の買いポジションを決済します。反対に、マイナス領域からプラス領域へクロスした場合は、既存の売りポジションを決済します。以下の図は、その動作を示したものです。

終了条件

このアプローチは、モメンタムが強いトレンド相場において特に高い効果を発揮します。しかし、レンジ相場や価格の持ち合いが続く局面では、雲やオシレーター内での値動きが不安定なため、誤シグナルが発生しやすくなります。そのため、トレーリングストップのようなリスク管理手法や、追加のフィルタリングロジックを導入することで、ドローダウンのリスクを抑えることが可能です。こうした基本的な原則を正しく理解することは、本戦略を自動売買EAとして効果的に実装するうえで極めて重要です。


MQL5による雲抜け戦略の実装

雲抜け戦略の理論を理解したところで、次はこの理論を自動化し、MetaTrader 5用のMetaQuotesLanguage5(MQL5)を使って、EAを作成していきましょう。

EAを作成するには、MetaTrader 5端末で[ツール]タブをクリックし、[MetaQuotes言語エディタ]を選択するか、キーボードのF4を押します。または、ツールバーのIDE(統合開発環境)アイコンをクリックすることもできます。これにより、MetaQuotes言語エディタ環境が開き、取引ロボット、テクニカルインジケーター、スクリプト、関数のライブラリを作成できるようになります。MetaEditorを開いたら、ツールバーの[ファイル]タブで[新しいファイル]を選択するか、CTRL+Nキーを押して新規ドキュメントを開きます。または、[ツール]タブの新規アイコンをクリックすることもできます。MQLウィザードのポップアップが表示されます。

ウィザードが表示されたら、[EA(テンプレート)]を選択し、[次へ]をクリックします。EAの一般プロパティで、[名前]フィールドにEAのファイル名を入力します。フォルダが存在しない場合にフォルダを指定または作成するには、EA名の前にバックスラッシュを使用することに注意してください。例えば、ここではデフォルトで「Experts\」となっています。つまり、私たちのEAはExpertsフォルダに作成され、そこで見つけることができます。他のフィールドはごく簡単ですが、ウィザードの一番下にあるリンクから、正確な手順を知ることができます。

新しいEA名

希望するEAファイル名を入力した後、[次へ]をクリックし、[完了]をクリックします。ここまでくれば、あとはコードを書いて戦略をプログラムするだけです。

まず、EAに関するメタデータを定義することから始めます。これには、EA名、著作権情報、MetaQuotesWebサイトへのリンクが含まれます。EAのバージョンも指定し、1.00とします。

//+------------------------------------------------------------------+
//|                                          1. Kumo Breakout EA.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

これにより、プログラムをロードするときにシステムメタデータが表示されます。次に、プログラム内で使用するグローバル変数をいくつか追加します。まず、ソースコードの先頭で#includeを使用して、取引インスタンスをインクルードします。これにより、CTradeクラスにアクセスできるようになります。これを使用して、取引オブジェクトを作成します。これは取引を開始するために必要なので、非常に重要です。

#include <Trade/Trade.mqh>
CTrade obj_Trade;

プリプロセッサは、#include<Trade/Trade.mqh>行をファイルTrade.mqhの内容に置き換えます。角括弧は、Trade.mqhファイルが標準ディレクトリ(通常、terminal_installation_directory\MQL5\Include)から取得されることを示します。カレントディレクトリは検索に含まれません。この行はプログラム中のどこにでも配置できますが、通常は、より良いコード構造と参照を容易にするために、すべてのインクルージョンはソースコードの先頭に置かれます。CTradeクラスのobj_Tradeオブジェクトを宣言すると、MQL5開発者のおかげで、そのクラスに含まれるメソッドに簡単にアクセスできるようになります。

CTRADEクラス

その後、取引システムで使用するいくつかの重要なインジケーターハンドルを宣言する必要があります。

int handle_Kumo = INVALID_HANDLE;                   //--- Initialize the Kumo indicator handle to an invalid state
int handle_AO = INVALID_HANDLE;                     //--- Initialize the Awesome Oscillator handle to an invalid state

ここでは、それぞれ雲(一目均衡表)とオーサムオシレーター(AO)のハンドルを格納するために使用する2つの整数変数「handle_Kumo」と「handle_AO」を宣言します。両方の変数を、無効または初期化されていないハンドルを表すMQL5の定義済み定数であるINVALID_HANDLE値で初期化します。これは、インジケーターを作成すると、インジケーターと対話できるようにするハンドルがシステムから返されるため重要です。ハンドルが「INVALID_HANDLE」の場合、インジケーターの作成に失敗したか、正しく初期化されていません。最初にハンドルをINVALID_HANDLEに設定することで、後で初期化の問題をチェックし、エラーを適切に処理できるようになります。

次に、取得した値を格納する配列を初期化する必要があります。

double senkouSpan_A[];                              //--- Array to store Senkou Span A values
double senkouSpan_B[];                              //--- Array to store Senkou Span B values

double awesome_Oscillator[];                        //--- Array to store Awesome Oscillator values

再びグローバルスコープで、3つの配列「senkouSpan_A」、「senkouSpan_B」、「awesome_Oscillator」を宣言します。これらは、それぞれ先行スパンA、先行スパンB、オーサムオシレーターの値を格納するために使用されます。いずれもdouble型で定義しており、インジケーターの計算結果として得られる浮動小数点の値を格納するのに適しています。senkouSpan_AおよびsenkouSpan_B配列には、一目均衡表インジケーターの各スパンの値を保持し、awesome_Oscillator配列には、Awesome Oscillatorによって算出された値を格納します。これらの配列を事前に宣言しておくことで、インジケーターの値を後から簡単に参照・活用できるようになります。

以上が、グローバルスコープで必要となるすべての変数です。これで、初期化処理を担当するOnInitイベントハンドラ内で、各インジケーターのハンドルを初期化する準備が整いました。

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

   return(INIT_SUCCEEDED);                          //--- Return successful initialization
}

これは、何らかの理由でインジケーターが初期化されるたびに呼び出されるイベントハンドラです。この中で、各インジケーターハンドルの初期化をおこないます。最初に、雲のハンドルを初期化します。

//--- Initialize the Ichimoku Kumo indicator
handle_Kumo = iIchimoku(_Symbol,_Period,8,29,34);

if (handle_Kumo == INVALID_HANDLE){              //--- Check if Kumo indicator initialization failed
   Print("ERROR: UNABLE TO INITIALIZE THE KUMO INDICATOR HANDLE. REVERTING NOW!"); //--- Log error
   return (INIT_FAILED);                         //--- Return initialization failure
}

ここでは、iIchimoku関数を呼び出してhandle_Kumoを初期化します。この関数は、現在の銘柄(_Symbol)および期間(_Period)に対して、Ichimoku Kumoインジケーターのインスタンスを作成します。使用するパラメータは、転換線=8、基準線=29、先行スパンB=34と、先に示した設定値に基づいています。

iIchimokuの呼び出し後、返されたハンドルはhandle_Kumoに格納されます。その後、このハンドルがINVALID_HANDLEと等しいかどうかをチェックします。もしハンドルが無効であれば、インジケーターの初期化に失敗したことを意味します。その場合は、Print関数を用いてエラーメッセージを出力し、初期化失敗を示すINIT_FAILEDを返します。同様に、オシレーターインジケーターを初期化します。

//--- Initialize the Awesome Oscillator
handle_AO = iAO(_Symbol,_Period);

if (handle_AO == INVALID_HANDLE){                //--- Check if AO indicator initialization failed
   Print("ERROR: UNABLE TO INITIALIZE THE AO INDICATOR HANDLE. REVERTING NOW!"); //--- Log error
   return (INIT_FAILED);                         //--- Return initialization failure
}

オシレーターを初期化するには、iAO関数を呼び出し、銘柄と期間のみをデフォルトパラメータとして渡します。次に、雲ハンドルと同じ形式を使用して、残りの初期化ロジックを続行します。初期化が完了したら、ストレージ配列を時系列として設定する作業に進むことができます。

ArraySetAsSeries(senkouSpan_A,true);             //--- Set Senkou Span A array as a time series
ArraySetAsSeries(senkouSpan_B,true);             //--- Set Senkou Span B array as a time series
ArraySetAsSeries(awesome_Oscillator,true);       //--- Set Awesome Oscillator array as a time series

ArraySetAsSeries関数を使用して、配列「senkouSpan_A」、「senkouSpan_B」、「awesome_Oscillator」の各配列を時系列配列として設定します。これにより、配列内の最新の値が先頭に格納され、過去のデータは配列の末尾に向かって並ぶようになります。これは、MQL5 において時系列データが一般的に「最新のデータが最初に配置される」形式で扱われるため、取引判断において最新データを素早く取得するのに非常に重要です。

この時系列動作を有効にするために、各配列に対して ArraySetAsSeriesを呼び出し、第2引数にtrueを渡します。これにより、多くの取引戦略で必要とされる「最新値への即時アクセス」が可能になります。すべての初期化が正常に完了したら、EAが準備完了であることを示すメッセージを操作ログに出力することができます。

Print("SUCCESS. ",__FILE__," HAS BEEN INITIALIZED."); //--- Log successful initialization

初期化が正常に完了した後、Print関数を使用して、その成功を示すメッセージをログに記録します。このメッセージには、文字列「SUCCESS.」に続いて、現在のソースコードファイル名を表す特別な定義済み変数__FILE__が含まれます。__FILE__ を使用することで、ログメッセージにファイル名を動的に挿入できるため、複数のファイルから構成される大規模なプロジェクトにおいて、初期化の流れをデバッグしたり追跡したりする際に非常に便利です。このメッセージはターミナルまたはログファイルに出力され、初期化が正常におこなわれたことを確認できます。このように、ログを通じて初期化のステータスに関する明確なフィードバックを得ることで、コード内の潜在的な問題を素早く特定しやすくなります。

完全な初期化コードスニペットは次のとおりです。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){
   //--- Initialize the Ichimoku Kumo indicator
   handle_Kumo = iIchimoku(_Symbol,_Period,8,29,34);
   
   if (handle_Kumo == INVALID_HANDLE){              //--- Check if Kumo indicator initialization failed
      Print("ERROR: UNABLE TO INITIALIZE THE KUMO INDICATOR HANDLE. REVERTING NOW!"); //--- Log error
      return (INIT_FAILED);                         //--- Return initialization failure
   }
   
   //--- Initialize the Awesome Oscillator
   handle_AO = iAO(_Symbol,_Period);
   
   if (handle_AO == INVALID_HANDLE){                //--- Check if AO indicator initialization failed
      Print("ERROR: UNABLE TO INITIALIZE THE AO INDICATOR HANDLE. REVERTING NOW!"); //--- Log error
      return (INIT_FAILED);                         //--- Return initialization failure
   }

   ArraySetAsSeries(senkouSpan_A,true);             //--- Set Senkou Span A array as a time series
   ArraySetAsSeries(senkouSpan_B,true);             //--- Set Senkou Span B array as a time series
   ArraySetAsSeries(awesome_Oscillator,true);       //--- Set Awesome Oscillator array as a time series
   
   Print("SUCCESS. ",__FILE__," HAS BEEN INITIALIZED."); //--- Log successful initialization
   
   //---
   return(INIT_SUCCEEDED);                          //--- Return successful initialization
}

出力は次のようになります。

初期化メッセージ

データ格納用の配列とハンドルを初期化したので、プログラムを終了する際にはそれらを保持しないようにします。そうしないと、不要なリソースを占有してしまうからです。この処理は、プログラムが終了するたびに呼び出されるOnDeinitイベントハンドラで行います。終了の理由に関わらず、このイベントハンドラが実行されます。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason){
   //--- Free memory allocated for Senkou Span A and B arrays
   ArrayFree(senkouSpan_A);
   ArrayFree(senkouSpan_B);
   
   //--- Free memory allocated for the Awesome Oscillator array
   ArrayFree(awesome_Oscillator);
}

OnDeinitOnDeinit関数内では、初期化プロセス中に割り当てられたメモリを解放するためのクリーンアップタスクを実行します。具体的には、ArrayFree関数を使用して、配列「senkouSpan_A」、「senkouSpan_B」、「awesome_Oscillator」のメモリの割り当てを解除します。これらの配列は、以前はIchimoku Kumoインジケーターとオーサムオシレーターの値を格納するために使用されていましたが、現在は不要になったため、解放リークを防ぐためにメモリを解放します。これにより、EAがアクティブでなくなった後もプログラムがシステムリソースを効率的に管理し、不要なメモリの使用を回避できるようになります。

残っているのは、インジケーターの値を取得して分析し、取引の決定を下す取引ロジックの処理だけです。これは、新しいティックがあるか、単に価格が変化するたびに呼び出されるOnTickイベントハンドラで処理します。最初におこなう必要があるステップは、インジケーターハンドルからデータポイントを取得し、さらに分析するために保存することです。

//--- Copy data for Senkou Span A from the Kumo indicator
if (CopyBuffer(handle_Kumo,SENKOUSPANA_LINE,0,2,senkouSpan_A) < 2){
   Print("ERROR: UNABLE TO COPY REQUESTED DATA FROM SENKOUSPAN A LINE. REVERTING NOW!"); //--- Log error
   return;                                        //--- Exit if data copy fails
}
//--- Copy data for Senkou Span B from the Kumo indicator
if (CopyBuffer(handle_Kumo,SENKOUSPANB_LINE,0,2,senkouSpan_B) < 2){
   Print("ERROR: UNABLE TO COPY REQUESTED DATA FROM SENKOUSPAN B LINE. REVERTING NOW!"); //--- Log error
   return;                                        //--- Exit if data copy fails
}

ここでは、CopyBuffer関数を使用して、雲(Ichimoku)インジケーターの先行スパンAと先行スパンBのデータを、それぞれsenkouSpan_AとsenkouSpan_B配列にコピーします。CopyBuffer関数の最初の引数はインジケーターハンドル「handle_Kumo」で、これは初期化された雲インジケーターを指します。2番目の引数はコピーするデータラインを指定します。これは、先行スパンAの場合はSENKOUSPANA_LINE、先行スパンBの場合はSENKOUSPANB_LINEです。3番目の引数はコピーを開始するインデックスで、最も新しいデータから開始するために0に設定されます。4番目の引数はコピーするデータポイントの数を指定し、この場合は2です。最後の引数はデータを格納する配列で、ここではsenkouSpan_AまたはsenkouSpan_Bです。

CopyBufferを呼び出した後、関数が2未満の値を返すかどうかを確認します。これは、要求されたデータが正常にコピーされなかったことを示します。この場合、Print関数を使用してエラーメッセージをログに記録し、どの先行スパンラインからデータをコピーできなかったのかを指定し、次にreturnで関数を終了します。これにより、データコピーが失敗した場合に、問題をログに記録し、関数のさらなる実行を停止することでエラーを適切に処理します。

同じロジックを使用してオシレーターの値を取得します。

//--- Copy data from the Awesome Oscillator
if (CopyBuffer(handle_AO,0,0,3,awesome_Oscillator) < 3){
   Print("ERROR: UNABLE TO COPY REQUESTED DATA FROM AWESOME OSCILLATOR. REVERTING NOW!"); //--- Log error
   return;                                        //--- Exit if data copy fails
}

CopyBuffer関数を使用して、オーサムオシレーター(AO)からawesome_Oscillator配列にデータをコピーします。CopyBuffer関数の最初の引数には、初期化したオーサムオシレーターのインジケーターハンドル「handle_AO」を渡します。次の引数はコピーするデータラインまたはバッファインデックスを指定します。この場合、オーサムオシレーターは1つのデータバッファのみを持っているため、インデックスは0です。3番目の引数はデータの取得開始位置で、最新のデータをコピーするために0に設定します。4番目の引数にはコピーするデータポイント数を指定し、ここでは最新の3つの値をコピーするので、数値は3に設定します。最後に、コピーされたデータを格納する配列「awesome_Oscillator」を指定します。取得したデータの量が要求した数よりも少ない場合は、エラーメッセージをログに記録し、関数を終了します。

必要なデータがすべて揃ったら、次のステップとして処理を進めます。最初におこなうべきことは、ティックごとにではなく、新しい完全なバーが生成された時点でデータを1回だけ分析するためのロジックを定義することです。そのロジックを関数に組み込みます。

//+------------------------------------------------------------------+
//|   IS NEW BAR FUNCTION                                            |
//+------------------------------------------------------------------+
bool isNewBar(){ 
   static int prevBars = 0;                         //--- Store previous bar count
   int currBars = iBars(_Symbol,_Period);           //--- Get current bar count for the symbol and period
   if (prevBars == currBars) return (false);        //--- If bars haven't changed, return false
   prevBars = currBars;                             //--- Update previous bar count
   return (true);                                   //--- Return true if new bar is detected
}

指定された銘柄と期間のチャートに新しいバーが表示されたかどうかを検出するためのブール関数「isNewBar」を定義します。この関数内で、前回のチェックからのバーの数を保存するstatic変数「prevBars」を宣言します。staticキーワードにより、この変数は関数呼び出し間で値を保持することが保証されます。

次に、iBars関数を使用して、指定された銘柄(_Symbol)と期間(_Period)におけるチャート上の現在のバー数を取得します。その結果はcurrBars変数に格納されます。バーの数が変わっていない場合(つまり、prevBarsとcurrBarsが同じ場合)、新しいバーが表示されていないことを示すfalseを返します。逆に、バーの数が変わっていた場合は、現在のバー数でprevBarsを更新し、新しいバーが検出されたことを示すtrueを返します。この関数を用いることで、ティックイベントハンドラ内で呼び出し、新しいバーの有無を分析することができます。

//--- Check if a new bar has formed
if (isNewBar()){
   //--- Determine if the AO has crossed above or below zero
   bool isAO_Above = awesome_Oscillator[1] > 0 && awesome_Oscillator[2] < 0;
   bool isAO_Below = awesome_Oscillator[1] < 0 && awesome_Oscillator[2] > 0;

//---
}

ここでは、isNewBar関数を呼び出して、新しいバーが形成されたかどうかを確認します。新しいバーが検出された場合(つまり、isNewBarがtrueを返す場合)、オーサムオシレーター(AO)の動作の判定に進みます。

2つのブール変数「isAO_Above」と「isAO_Below」を定義します。awesome_Oscillator[1](前回のAOの値)が0より大きく、その前の値(awesome_Oscillator[2])が0より小さい場合、変数「isAO_Above」はtrueに設定されます。この条件は、AOが0を超えたことを確認し、潜在的な強気シグナルを示します。同様に、前のAO値(awesome_Oscillator[1])が0未満で、その前の値(awesome_Oscillator[2])が0より大きい場合、isAO_Belowはtrueに設定され、AOが0を下回ったことを示し、弱気の動きの兆候となります。その後、同じ方法を使用して他のロジックを設定できます。

//--- Determine if the Kumo is bullish or bearish
bool isKumo_Above = senkouSpan_A[1] > senkouSpan_B[1];
bool isKumo_Below = senkouSpan_A[1] < senkouSpan_B[1];
      
//--- Determine buy and sell signals based on conditions
bool isBuy_Signal = isAO_Above && isKumo_Below && getClosePrice(1) > senkouSpan_A[1] && getClosePrice(1) > senkouSpan_B[1];
bool isSell_Signal = isAO_Below && isKumo_Above && getClosePrice(1) < senkouSpan_A[1] && getClosePrice(1) < senkouSpan_B[1];

ここでは、強気または弱気の雲(Ichimoku)セットアップの条件を決定します。まず、2つのブール変数「isKumo_Above」と「isKumo_Below」を定義します。isKumo_Aboveは、前回の先行スパンA(senkouSpan_A[1])が前回の先行スパンB(senkouSpan_B[1])より大きい場合にtrueに設定され、強気の雲(強気市場感情)を示します。一方、isKumo_Belowは、先行スパンAが先行スパンBより小さい場合にtrueに設定され、弱気の雲(弱気市場感情)を示します。

次に、潜在的な買いシグナルと売りシグナルの条件を定義します。買いシグナル(isBuy_Signal)は、オーサムオシレーターがゼロを上回り(isAO_Above)、雲が弱気(isKumo_Below)、および前のバーの終値が先行スパンAと先行スパンBの両方を上回っている場合に設定されます。これは、弱気の雲にもかかわらず、価格が上昇する可能性があることを示唆しています。オーサムオシレーターがゼロを下回り(isAO_Below)、雲が強気(isKumo_Above)、および前のバーの終値が先行スパンAと先行スパンBの両方を下回っている場合、売りシグナル(isSell_Signal)はtrueに設定されます。これは、強気の雲にもかかわらず、価格が下がる可能性があることを示しています。

終値を取得するために新しい関数を使用していることにお気づきかもしれません。必要となるすべての関数のロジックは次のとおりです。

//+------------------------------------------------------------------+
//|        FUNCTION TO GET CLOSE PRICES                              |
//+------------------------------------------------------------------+
double getClosePrice(int bar_index){
   return (iClose(_Symbol, _Period, bar_index));    //--- Retrieve the close price of the specified bar
}

//+------------------------------------------------------------------+
//|        FUNCTION TO GET ASK PRICES                                |
//+------------------------------------------------------------------+
double getAsk(){
   return (NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits)); //--- Get and normalize the Ask price
}

//+------------------------------------------------------------------+
//|        FUNCTION TO GET BID PRICES                                |
//+------------------------------------------------------------------+
double getBid(){
   return (NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits)); //--- Get and normalize the Bid price
}

ここでは、異なる種類の価格データを取得するための3つの関数を定義します。

  • getClosePrice関数:指定したバーの終値を取得する関数です。引数「bar_index」は、終値を取得したいバーのインデックスを指定します。この関数では、組み込み関数iCloseを使用し、銘柄(_Symbol)、期間(_Period)、バーインデックスを渡して、対象バーの終値を取得します。取得した終値はdouble型で返されます。
  • getAsk関数:指定した銘柄の現在のAsk(売値)を取得します。SymbolInfoDouble関数にSYMBOL_ASKを指定してAsk価格を取得し、NormalizeDouble関数を使って、小数点以下の桁数が銘柄の_Digitsに基づいて正しく丸められるようにします。この関数は正規化されたAsk価格をdouble型で返します。
  • getBid関数:指定した銘柄の現在のBid(買値)を取得します。処理の流れはgetAsk関数と同様で、SymbolInfoDoubleSYMBOL_BIDを指定してBid価格を取得し、NormalizeDoubleで小数点以下の桁数を調整します。最終的に、正規化されたBid価格をdouble型で返します。

これらの関数を活用することで、取引に必要な価格データを簡単かつ正確に取得・処理できるようになります。その後、算出された取引シグナルに基づいて、該当する売買ポジションを発注することができます。

if (isBuy_Signal){                            //--- If buy signal is generated
   Print("BUY SIGNAL GENERATED @ ",iTime(_Symbol,_Period,1),", PRICE: ",getAsk()); //--- Log buy signal
   obj_Trade.Buy(0.01,_Symbol,getAsk());      //--- Execute a buy trade
}
else if (isSell_Signal){                      //--- If sell signal is generated
   Print("SELL SIGNAL GENERATED @ ",iTime(_Symbol,_Period,1),", PRICE: ",getBid()); //--- Log sell signal
   obj_Trade.Sell(0.01,_Symbol,getBid());     //--- Execute a sell trade
}

買いまたは売りのシグナルが発生したかどうかを確認し、該当する取引を実行します。まず、isBuy_Signalがtrueの場合、つまり買いシグナルが検出された場合には、Print関数を使ってイベントをログに記録します。このログには、iTime関数で取得した前のバーのタイムスタンプと、getAsk関数から取得した現在のAsk価格を含めます。これにより、シグナルが発生した時点の価格情報とその記録が残ります。その後、obj_Trade.Buy(0.01, _Symbol, getAsk())を呼び出して、現在のAsk価格で0.01ロットの買い注文を発注します。

同様に、isSell_Signalがtrueの場合は、売りシグナルが発生したことを意味します。これもPrint関数を使って、前のバーのタイムスタンプとgetBid関数による現在のBid価格を含むログを出力します。その後、obj_Trade.Sell(0.01, _Symbol, getBid()) を使って、現在のBid価格で0.01ロットの売り注文を実行します。このように、売買シグナルの条件が満たされたときに確実に取引をおこない、その内容を記録することで、戦略の透明性と追跡性が確保されます。

最後に、勢いの変化を確認し、必要に応じてポジションをクローズする処理をおこないます。以下がそのロジックです。

if (isAO_Above || isAO_Below){                //--- If AO crossover occurs
   if (PositionsTotal() > 0){                 //--- If there are open positions
      for (int i=PositionsTotal()-1; i>=0; i--){ //--- Loop through open positions
         ulong posTicket = PositionGetTicket(i); //--- Get the position ticket
         if (posTicket > 0){                  //--- If ticket is valid
            if (PositionSelectByTicket(posTicket)){ //--- Select position by ticket
               ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); //--- Get position type
               if (posType == POSITION_TYPE_BUY){ //--- If position is a buy
                  if (isAO_Below){            //--- If AO indicates bearish crossover
                     Print("CLOSING THE BUY POSITION WITH #",posTicket); //--- Log position closure
                     obj_Trade.PositionClose(posTicket); //--- Close the buy position
                  }
               }
               else if (posType == POSITION_TYPE_SELL){ //--- If position is a sell
                  if (isAO_Above){            //--- If AO indicates bullish crossover
                     Print("CLOSING THE SELL POSITION WITH #",posTicket); //--- Log position closure
                     obj_Trade.PositionClose(posTicket); //--- Close the sell position
                  }
               }
            }
         }
      }
   }
}

ここでは、オーサムオシレーター(AO)がゼロラインを上抜けまたは下抜けしたかどうかをチェックし、それに応じて保有中のポジションを管理します。「isAO_Above」または「isAO_Below」のいずれかがtrueの場合、つまりAOのクロスオーバーが発生したことを示している場合には、まずPositionsTotal関数を呼び出して、保有中のポジションが存在するかを確認します。ポジション数が1つ以上(PositionsTotal() > 0)の場合は、最新のポジション(PositionsTotal() - 1)から順に逆方向にループ処理をおこないます。

ループ内では、まずPositionGetTicket関数を使用してポジションのチケット番号を取得します。チケット番号が有効(0より大きい)であれば、PositionSelectByTicket関数を使ってそのポジションを選択します。続いてPositionGetInteger関数でポジションのタイプ(買いか売りか)を取得します。ポジションが買い(POSITION_TYPE_BUY)である場合、isAO_Belowがtrueであれば、それは弱気なクロスオーバーが発生したことを示しています。その場合、Print関数で買いポジションのクローズをログに記録し、「obj_Trade.PositionClose(posTicket)」でそのポジションをクローズします。

同様に、ポジションが売り(POSITION_TYPE_SELL)である場合、「isAO_Above」がtrueであれば、強気なクロスオーバーが発生したことになります。この場合も、売りポジションのクローズをログに記録し、「obj_Trade.PositionClose(posTicket)」でクローズします。このようにして、AOのクロスオーバーに応じて市場のモメンタムの変化を検出し、適切に保有ポジションをクローズすることで、リスク管理と戦略の整合性を保ちます。プログラムを実行すると、次のような出力が得られます。

以下は、売りポジションの確認です。

売りポジション確認

以下は、市場の勢いの変化による売りポジションの終了確認です。

売りポジション終了

上記の説明と実行結果から、私たちが目指していた目的が達成されたことは確かです。これで、プログラムのテストおよび最適化に進む準備が整いました。この内容は、次のセクションで取り扱います。


戦略のテストと最適化

このセクションでは、戦略を実際にテストし、さまざまな市場状況下でより効果的に機能するように最適化をおこないます。今回の変更点はリスク管理の部分で、市場のモメンタムが明確に転換するのを待つのではなく、すでに含み益が出ている場合にその利益を確保できるよう、トレーリングストップを導入することにあります。これを効率よく管理するために、トレーリングストップのロジックを動的に処理する関数を構築します。

//+------------------------------------------------------------------+
//|        FUNCTION TO APPLY TRAILING STOP                           |
//+------------------------------------------------------------------+
void applyTrailingSTOP(double slPoints, CTrade &trade_object,int magicNo=0){
   double buySL = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID)-slPoints,_Digits); //--- Calculate SL for buy positions
   double sellSL = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK)+slPoints,_Digits); //--- Calculate SL for sell positions

   for (int i = PositionsTotal() - 1; i >= 0; i--){ //--- Iterate through all open positions
      ulong ticket = PositionGetTicket(i);          //--- Get position ticket
      if (ticket > 0){                              //--- If ticket is valid
         if (PositionGetString(POSITION_SYMBOL) == _Symbol &&
            (magicNo == 0 || PositionGetInteger(POSITION_MAGIC) == magicNo)){ //--- Check symbol and magic number
            if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY &&
               buySL > PositionGetDouble(POSITION_PRICE_OPEN) &&
               (buySL > PositionGetDouble(POSITION_SL) ||
               PositionGetDouble(POSITION_SL) == 0)){ //--- Modify SL for buy position if conditions are met
               trade_object.PositionModify(ticket,buySL,PositionGetDouble(POSITION_TP));
            }
            else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL &&
               sellSL < PositionGetDouble(POSITION_PRICE_OPEN) &&
               (sellSL < PositionGetDouble(POSITION_SL) ||
               PositionGetDouble(POSITION_SL) == 0)){ //--- Modify SL for sell position if conditions are met
               trade_object.PositionModify(ticket,sellSL,PositionGetDouble(POSITION_TP));
            }
         }
      }
   }
}

ここでは、ポジションにトレーリングストップを適用する関数「applyTrailingSTOP」を実装します。この関数は3つのパラメータを取ります。slPointsはストップロスに設定するポイント数を表し、trade_objectはポジションを変更するために使用される取引オブジェクトへの参照、そして任意のmagicNoは特定のポジションをマジックナンバーで識別するために使用されます。まず、買いポジションと売りポジションそれぞれに対してストップロス(SL)レベルを計算します。買いポジションの場合、ストップロスはBid価格から指定されたslPointsを引いた値に設定され、売りポジションの場合はAsk価格に指定されたslPointsを加えた値に設定されます。どちらのSL値も、銘柄の小数点精度(_Digits)に合わせるために、NormalizeDouble関数で正規化されます。

次に、PositionsTotal関数を使用して、最新のポジションから最も古いポジションまで反復しながらすべてのポジションをループします。各ポジションについて、PositionGetTicket関数でポジションチケットを取得し、それが有効であることを確認します。そのうえで、ポジションの銘柄が現在の銘柄(_Symbol)と一致しているかどうか、またポジションのマジックナンバーが指定された「magicNo」と一致しているかどうかを確認します(magicNoが0に設定されている場合は、すべてのポジションが対象)。

ポジションが買い(POSITION_TYPE_BUY)の場合は、計算された買いストップロス(buySL)がポジションの建値(POSITION_PRICE_OPEN)を上回っており、かつ現在のストップロス(POSITION_SL)よりも高いか、もしくはストップロスが未設定(POSITION_SL == 0)である場合に、「trade_object.PositionModify(ticket, buySL, PositionGetDouble(POSITION_TP))」を呼び出してストップロスを更新します。テイクプロフィット(POSITION_TP)はそのまま維持されます。

ポジションが売りポジション(POSITION_TYPE_SELL)の場合も同様に、計算された売りストップロス(sellSL)が建値を下回っており、かつ現在のストップロスよりも低いか、または未設定である場合に、「trade_object.PositionModify(ticket, sellSL, PositionGetDouble(POSITION_TP))」を使用してポジションのストップロスを更新します。これらの条件が満たされた場合、「trade_object.PositionModify(ticket,sellSL,PositionGetDouble(POSITION_TP))」を使用してポジションのストップロスを更新します。

関数を定義したら、実行できるようにtick関数でそれを呼び出すだけです。次のようにそれぞれのパラメータを渡します。

if (PositionsTotal() > 0){                       //--- If there are open positions
   applyTrailingSTOP(3000*_Point,obj_Trade,0);  //--- Apply a trailing stop
}

ポジションが存在する場合は、applyTrailingSTOP関数を呼び出して、これらのポジションにトレーリングストップを適用します。この関数は3つの引数で呼び出されます。

  • トレーリングストップポイント:ストップロス距離は「3000*_Point」として計算されます。ここで、_Pointは現在の銘柄の最小の価格変動を表します。これは、ストップロスが現在の市場価格から3000ポイント離れたところに設定されていることを意味します。
  • 取引オブジェクト:ポジションのストップロスとテイクプロフィットのレベルを変更するために使用される取引オブジェクトのインスタンスである「obj_Trade」を渡します。
  • マジックナンバー:3番目の引数は0に設定されており、これは、マジックナンバーに関係なく、すべてのオープンポジションにトレーリングストップが適用されることを意味します。

トレーリングストップを適用すると、次の出力が得られます。

トレーリングストップGIF

可視化から、市場の勢いの変化を待つのではなく、市場が私たちの方向に進むたびにストップロスレベルを移動することで、利益を確定し、利益を最大化できることがわかります。最終的なストラテジーテスターの結果は次のとおりです。

テスターグラフ

テスターグラフ

テスターの結果

テスターの結果


結論

本記事では、雲抜けシステムを用いてMQL5のEAを構築する方法を解説しました。一目均衡表の雲インジケーターとAwesome Oscillator (AO)を組み合わせることで、市場のモメンタム変化やブレイクアウトシグナルを検出するフレームワークを構築しました。主な手順としては、インジケーターハンドルの設定、主要値の抽出、トレーリングストップおよびポジション管理による自動売買の実装が含まれており、戦略ベースの堅牢な取引ロジックを備えたEAを実現しました。

免責条項:本記事は、インジケーターに基づいた取引シグナルを活用したMQL5 EAの開発を目的とした教育用ガイドです。雲抜けシステムは人気のある手法ではありますが、すべての市場環境で常に有効とは限りません。取引には常に金融リスクが伴い、過去のパフォーマンスが将来の結果を保証するものではありません。実運用を行う前に、十分な検証と適切なリスク管理をおこなうことが不可欠です。

このガイドを通じて、MQL5における開発スキルを向上させ、より高度な取引システムの構築に役立てていただけます。本記事で紹介したインジケーターの統合、シグナルロジック、取引自動化の概念は、他の戦略にも応用可能であり、アルゴリズム取引におけるさらなる探求と創造を促進します。コーディングの成功と、取引における成果をお祈りします。

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

添付されたファイル |
独自のLLMをEAに統合する(第5部):LLMを使った取引戦略の開発とテスト(III) - アダプタチューニング 独自のLLMをEAに統合する(第5部):LLMを使った取引戦略の開発とテスト(III) - アダプタチューニング
今日の人工知能の急速な発展に伴い、言語モデル(LLM)は人工知能の重要な部分となっています。私たちは、強力なLLMをアルゴリズム取引に統合する方法を考える必要があります。ほとんどの人にとって、これらの強力なモデルをニーズに応じてファインチューニングし、ローカルに展開して、アルゴリズム取引に適用することは困難です。本連載では、この目標を達成するために段階的なアプローチをとっていきます。
MQL5で自己最適化エキスパートアドバイザーを構築する(第2回):USDJPYスキャルピング戦略 MQL5で自己最適化エキスパートアドバイザーを構築する(第2回):USDJPYスキャルピング戦略
今日は私たちと一緒にUSDJPYペアを中心とした取引戦略の構築に挑戦するしましょう。日足のローソク足パターンは、潜在的により強い動きがあるため、日足パターンで形成されるローソク足パターンを取引します。私たちの当初の戦略は利益を生み、これにより獲得した資本を保護するために、戦略を継続的に改良し、安全性をさらに高める努力を続けることができました。
ニュース取引が簡単に(第6回):取引の実施(III) ニュース取引が簡単に(第6回):取引の実施(III)
この記事では、IDに基づいて個々のニュースイベントをフィルターする関数を実装します。さらに、以前のSQLクエリを改善し、追加情報が提供されたり、クエリの実行時間が短縮されるようになります。さらに、これまでの記事で作成したコードを機能的なものにします。
プライスアクション分析ツールキットの開発(第5回):Volatility Navigator EA プライスアクション分析ツールキットの開発(第5回):Volatility Navigator EA
市場の方向性を判断するのは簡単ですが、いつエントリーするかを知るのは難しい場合があります。連載「プライスアクション分析ツールキットの開発」の一環として、エントリーポイント、テイクプロフィットレベル、ストップロスの配置を提供する別のツールを紹介できることを嬉しく思います。これを実現するために、MQL5プログラミング言語を利用しました。この記事では、各ステップについて詳しく見ていきましょう。