
MQL5での取引戦略の自動化(第2回):一目均衡表とオーサムオシレーターを備えた雲抜けシステム
はじめに
前回の記事(連載第1回)では、Profitunityシステム(ビル・ウィリアムズ著『Trading Chaos』)の自動化方法を紹介しました。 本記事(第2回)では、「雲抜け戦略」をMetaQuotes Language 5 (MQL5)を用いて完全に機能するEAへと構築する方法を解説します。雲抜け戦略は、一目均衡表を用いて、市場の反転やトレンドの継続を判断するための手法です。価格が「雲(Kumo)」と呼ばれる領域、すなわち先行スパンAと先行スパンBの間に形成される動的な支持/抵抗ゾーンを上抜けまたは下抜けることで、トレンドの転換シグナルを捉えます。加えて、オーサムオシレーターをトレンド確認用のフィルターとして活用することで、誤ったシグナルの除外とエントリー/エグジット精度の向上を図ります。この戦略は、強いモメンタムに基づく値動きから利益を狙うトレーダーに広く活用されています。
本記事では、戦略ロジックのコーディングから取引管理、トレーリングストップによるリスク制御の強化までの一連のプロセスを紹介します。記事を読み終える頃には、この戦略を自動化し、MQL5のストラテジーテスターを使ってそのパフォーマンスを検証・最適化するための方法が明確に理解できるはずです。理解を深めやすくするために、以下のセクションに分けて構成しています。
- 雲抜け戦略の概要
- MQL5による雲抜け戦略の実装
- 戦略のテストと最適化
- 結論
雲抜け戦略の概要
雲抜け戦略は、雲の範囲を価格が突破した際のトレンド発生を狙う、トレンドフォロー型の手法です。「雲」とは、一目均衡表において先行スパン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名、著作権情報、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開発者のおかげで、そのクラスに含まれるメソッドに簡単にアクセスできるようになります。
その後、取引システムで使用するいくつかの重要なインジケーターハンドルを宣言する必要があります。
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関数と同様で、SymbolInfoDoubleにSYMBOL_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に設定されており、これは、マジックナンバーに関係なく、すべてのオープンポジションにトレーリングストップが適用されることを意味します。
トレーリングストップを適用すると、次の出力が得られます。
可視化から、市場の勢いの変化を待つのではなく、市場が私たちの方向に進むたびにストップロスレベルを移動することで、利益を確定し、利益を最大化できることがわかります。最終的なストラテジーテスターの結果は次のとおりです。
テスターグラフ
結論
本記事では、雲抜けシステムを用いてMQL5のEAを構築する方法を解説しました。一目均衡表の雲インジケーターとAwesome Oscillator (AO)を組み合わせることで、市場のモメンタム変化やブレイクアウトシグナルを検出するフレームワークを構築しました。主な手順としては、インジケーターハンドルの設定、主要値の抽出、トレーリングストップおよびポジション管理による自動売買の実装が含まれており、戦略ベースの堅牢な取引ロジックを備えたEAを実現しました。
免責条項:本記事は、インジケーターに基づいた取引シグナルを活用したMQL5 EAの開発を目的とした教育用ガイドです。雲抜けシステムは人気のある手法ではありますが、すべての市場環境で常に有効とは限りません。取引には常に金融リスクが伴い、過去のパフォーマンスが将来の結果を保証するものではありません。実運用を行う前に、十分な検証と適切なリスク管理をおこなうことが不可欠です。
このガイドを通じて、MQL5における開発スキルを向上させ、より高度な取引システムの構築に役立てていただけます。本記事で紹介したインジケーターの統合、シグナルロジック、取引自動化の概念は、他の戦略にも応用可能であり、アルゴリズム取引におけるさらなる探求と創造を促進します。コーディングの成功と、取引における成果をお祈りします。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/16657





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索