
MQL5で自己最適化エキスパートアドバイザーを構築する(第3回):ダイナミックトレンドフォローと平均回帰戦略
移動平均に基づくアルゴリズム取引戦略は、長期的なトレンドに取引アプリケーションを連動させるという点で、他の多くの戦略と一線を画します。しかし、市場が明確なトレンドを持たずレンジ相場にある場合には、かつて有効だったトレンドフォロー型の戦略が、かえって損失を生む結果となることもあります。市場がトレンド状態とレンジ状態をどのように切り替えるのかを理解することは、移動平均に基づく戦略をより効果的に活用するうえで極めて重要です。
多くのトレーダーは、当日の取引に適した戦略を選択する前に、市場が「現在はレンジ相場かトレンド相場か」をまず判断しようとします。しかし、市場がこれら2つの状態をどのように切り替えるのかに焦点を当てた文献は、ほとんど存在しません。市場を単なる二択の静的な状態と捉えるのではなく、常に両者の間を揺れ動く動的な環境として認識することが、本記事の基本的な視点です。 そして本稿の目的は、トレンドからレンジ、あるいはその逆への転換を自動で検出できる、ひとつの汎用的な動的取引戦略を構築することにあります。
この新たな動的戦略は、これまでのように「市場の状態ごとに個別の戦略を用意する」という従来の発想に代わるものです。 本戦略では、ローリングベースでチャネルを計算します。根底にある考え方は、トレンドとレンジを分ける「境界」が存在するという仮定に基づいており、価格がこの境界に対してどの位置にあるかに注目することで、より精度の高い取引判断が可能になります。チャネルの上限と下限は、移動平均の値にATRの倍数を加算(上限)・減算(下限)することで算出されます。この手法の詳細については、記事の後半で詳しく解説します。
読者の皆さんには、これらのチャネルがMetaTrader 5に標準搭載されているテクニカルインジケーターを用いて、日々動的に計算されている点を理解していただきたいと思います。当初の戦略は、価格が100期間の移動平均を上回って終値を付けた場合にロングエントリー、それ以外はショートエントリーをおこなうという、シンプルなトレンドフォロー戦略でした。ポジションは、固定のストップロスおよびテイクプロフィットで管理されました。この戦略をEURUSDのM1データに対して4年間バックテストしたところ、勝率は52%にとどまりました。これに対し、提案されたチャネルベースの動的ルールでは、同期間・同条件下で勝率が86%にまで改善されました。なお、この改善はカーブフィッティングやAI技術を用いることなく実現されたものです。
この結果は、チャネルの境界線をより正確に見積もる技術を習得する努力が、十分に報われる価値があることを示しています。市場を厳密に分類しようとするのではなく、市場の自然なリズムに合わせて取引をおこなうことで、私たちのアルゴリズムは驚くべき勝率の向上を達成することができました。さらに、今回の戦略はコーディングの構造もシンプルに設計されているため、読者は自らの市場観を加えて、提供されたテンプレートを容易にカスタマイズ・拡張できるはずです。
取引戦略の概要
記事の冒頭でも述べたように、移動平均に基づく取引戦略は、長期的な市場のトレンドに沿ったポジションを取りやすいため、多くのトレーダーに支持されています。以下の図1は、この原則が実際に機能している顕著な例を示したものです。このスクリーンショットは、EUR/USDのデイリーチャートから取得したもので、2016年11月下旬から2018年4月まで続いた力強い上昇トレンドを示しています。いかなる観点から見ても印象的な上昇相場であり、移動平均線もまた、価格が長期間にわたり上昇傾向にあったことを裏付けています。
一般的に言えば、価格が移動平均線を上回って終値を付けたタイミングでロングポジションを取り、逆に移動平均線を下回って終値を付けたタイミングではショートポジションを取る、という戦略が考えられます。
図1:移動平均で定義されたトレンドフォローシステムの実際の例
図1で示した例は、トレンドが発生している市場において移動平均戦略が持つ最大の強みを表しています。しかし、図2に示すような明確な長期トレンドが存在しない市場環境では、こうした単純なトレンドフォロー戦略は効果を発揮しません。
図2:トレンドフォローシステムを使用した場合の長期にわたる不採算期間の例
この記事で提案されている戦略は、追加のインジケーターや、「いつ戦略を切り替えるか」「どの戦略に切り替えるか」といった複雑な判断ロジックを必要とせず、トレンド相場・レンジ相場のいずれにも非常にうまく対応できます。それでは、シンプルな移動平均取引戦略を、動的かつ自動で調整可能なものに進化させるために必要な要素を学んでいきましょう。
MQL5入門
私たちの取引システムは、さまざまな要素で構成されています。
要素 | 意図した目的 |
---|---|
システム定数 | エンドユーザーから隠された定数であり、バックテスト間でシステムの動作を一貫させるためのものです。これにより、バイアスや取引ロジックを壊すような意図しない変更を防ぎます。 |
ライブラリ | 本アプリケーションでは、ポジションの発注および管理をおこなうために、tradeライブラリのみをインポートしています。。 |
グローバル変数 | インジケーターの値や価格水準など、システム内のさまざまな要素間で共有されるデータを格納するためのものです。 |
システムイベントハンドラ | OnTick()のような関数は、システムによって呼び出されるイベントハンドラであり、各処理を整理された形で実行するのに役立ちます。 |
カスタム関数 | 移動平均によって定義されたトレンドに従うために必要な処理を実現するため、特定の目的に合わせて作成された関数です。 |
まず最初に、システム定数を定義します。これらの値は、今後構築していく取引アプリケーションの各バージョンにおいても変更しないことに注意してください。
//+------------------------------------------------------------------+ //| Dynamic Moving Average Strategy.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/ja/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/ja/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define MA_PRICE PRICE_CLOSE //Moving average applied price #define MA_MODE MODE_EMA //Moving average type #define MA_SHIFT 0 //Moving average shift #define ATR_PERIOD 14 //Period for our ATR #define TF_1 PERIOD_D1 //Hihger order timeframe #define TRADING_MA_PERIOD 100 //Moving average period #define SL_WIDTH 1.5 //How wide should the stop loss be? #define CURRENT_VOL 0.1 //Trading volume
次に、MQL5 APIで最もよく使用されるライブラリのひとつであるTradeライブラリをインポートします。このライブラリは、ポジションを簡単に管理するために不可欠です。
//+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade;
また、市場価格とテクニカルインジケーターの値を保存するためのグローバル変数も作成する必要があります。
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int ma_handler,trading_ma_handler,atr_handler; double ma[],atr[],trading_ma[]; double ask,bid;
MQL5アプリケーションの本体は、イベントハンドラによって構成されています。これらのハンドラは、端末上で新しいイベント(たとえば、新しい価格の提示やアプリケーションのチャートからの削除など)が発生したときに呼び出されます。イベントの一部はエンドユーザーによってトリガーされ、もう一部は取引サーバーによってトリガーされます。エンドユーザーが取引アプリケーションを起動すると、OnInit()ハンドラが実行され、グローバル変数を初期化するために設計された専用の初期化関数を呼び出します。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- setup(); //--- return(INIT_SUCCEEDED); }
エンドユーザーがチャートから取引アプリケーションを削除すると、OnDeinit()ハンドラが呼び出され、消費しなくなったシステムメモリリソースを解放します。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- IndicatorRelease(atr_handler); IndicatorRelease(ma_handler); IndicatorRelease(trading_ma_handler); }
OnTick()ハンドラは、エンドユーザーではなく取引サーバーによってトリガーされます。価格情報が更新されるたびに呼び出されます。この中では、テクニカルインジケーターを更新し、新しい価格情報を保存するための専用関数を呼び出します。その後、未決済ポジションが存在しない場合には、新たなポジションをエントリーするかどうかを判断します。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- update(); if(PositionsTotal() == 0) find_setup(); } //+------------------------------------------------------------------+
トレンドフォロー戦略に基づくポジションエントリーのルールは非常にシンプルです。価格が100期間移動平均線を上回っている場合は、固定のストップロスを設定してロングポジションをエントリーします。逆に、価格が移動平均線を下回っている場合は、ショートポジションをエントリーします。
//+------------------------------------------------------------------+ //| Find setup | //+------------------------------------------------------------------+ void find_setup(void) { //Buy on rallies if(iClose(Symbol(),PERIOD_CURRENT,0) > trading_ma[0]) Trade.Buy(CURRENT_VOL,Symbol(),ask,(bid - (atr[0] * SL_WIDTH)),(bid + (atr[0] * SL_WIDTH)),""); if(iClose(Symbol(),PERIOD_CURRENT,0) < trading_ma[0]) Trade.Sell(CURRENT_VOL,Symbol(),bid,(ask + (atr[0] * SL_WIDTH)),(ask - (atr[0] * SL_WIDTH)),""); }
以下は、テクニカルインジケーターを設定するためにOnInit()ハンドラによって呼び出される関数です。
//+------------------------------------------------------------------+ //| Setup our global variables | //+------------------------------------------------------------------+ void setup(void) { atr_handler = iATR(Symbol(),TF_1,ATR_PERIOD); trading_ma_handler = iMA(Symbol(),PERIOD_CURRENT,TRADING_MA_PERIOD,MA_SHIFT,MA_MODE,MA_PRICE); }
Update()関数はOnTick()ハンドラによって呼び出され、グローバル変数を更新します。
//+------------------------------------------------------------------+ //| Update | //+------------------------------------------------------------------+ void update(void) { static datetime time_stamp; datetime current_time = iTime(Symbol(),PERIOD_CURRENT,0); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); if(time_stamp != current_time) { time_stamp = current_time; CopyBuffer(atr_handler,0,0,1,atr); CopyBuffer(trading_ma_handler,0,0,1,trading_ma); } } //+------------------------------------------------------------------+
現在のバージョンの取引アプリケーションの状態は、次のようになっています。
//+------------------------------------------------------------------+ //| Dynamic Moving Average Strategy.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/ja/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/ja/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define MA_PRICE PRICE_CLOSE //Moving average applied price #define MA_MODE MODE_EMA //Moving average type #define MA_SHIFT 0 //Moving average shift #define ATR_PERIOD 14 //Period for our ATR #define TF_1 PERIOD_D1 //Hihger order timeframe #define TRADING_MA_PERIOD 100 //Moving average period #define SL_WIDTH 1.5 //How wide should the stop loss be? #define CURRENT_VOL 0.1 //Trading volume //+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade; //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int ma_handler,trading_ma_handler,atr_handler; double ma[],atr[],trading_ma[]; double ask,bid; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- setup(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- IndicatorRelease(atr_handler); IndicatorRelease(ma_handler); IndicatorRelease(trading_ma_handler); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- update(); if(PositionsTotal() == 0) find_setup(); } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Find setup | //+------------------------------------------------------------------+ void find_setup(void) { //Buy on rallies if(iClose(Symbol(),PERIOD_CURRENT,0) > trading_ma[0]) Trade.Buy(CURRENT_VOL,Symbol(),ask,(bid - (atr[0] * SL_WIDTH)),(bid + (atr[0] * SL_WIDTH)),""); if(iClose(Symbol(),PERIOD_CURRENT,0) < trading_ma[0]) Trade.Sell(CURRENT_VOL,Symbol(),bid,(ask + (atr[0] * SL_WIDTH)),(ask - (atr[0] * SL_WIDTH))); } //+------------------------------------------------------------------+ //| Setup our global variables | //+------------------------------------------------------------------+ void setup(void) { atr_handler = iATR(Symbol(),TF_1,ATR_PERIOD); trading_ma_handler = iMA(Symbol(),PERIOD_CURRENT,TRADING_MA_PERIOD,MA_SHIFT,MA_MODE,MA_PRICE); } //+------------------------------------------------------------------+ //| Update | //+------------------------------------------------------------------+ void update(void) { static datetime time_stamp; datetime current_time = iTime(Symbol(),PERIOD_CURRENT,0); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); if(time_stamp != current_time) { time_stamp = current_time; CopyBuffer(atr_handler,0,0,1,atr); CopyBuffer(trading_ma_handler,0,0,1,trading_ma); } } //+------------------------------------------------------------------+
ベンチマークの確立
2020年1月1日水曜日から2025年1月6日月曜日までのM1履歴データを使用して、トレンドフォローアプリケーションのパフォーマンスを観察してみましょう。バックテストにはEURUSDペアを使用します。戦略のパラメータは調整していないため、バックテストの[Forward]設定は[No]に設定されています。
図3:バックテストの日付
さらに、テスターで実際の取引状況を最適にエミュレートするために、実際のティックに基づいてモデリングを設定します。また、ランダム遅延を使用することで、システムがストレスのかかる状況でどのように動作するかを把握できます。リアルタイムで取引する際の遅延は変動する可能性があるため、システムが本番環境で動作する際の条件にできるだけ近づけたいと考えています。
図4:バックテストの条件
これは、5年間のバックテストを通じて私たちの取引戦略が得たエクイティカーブです。口座の最大残高と最終残高の差に注意してください。結果から、私たちの戦略が安定しておらず、利益を得るのと同じくらい簡単に損失を出す傾向があることが明確に示されています。この戦略による連勝は短期間で終わり、その後は勝ち期間と同じくらい長く続いたドローダウン期間が続きました。これらの長期的なドローダウン期間は、市場が長期的なトレンドに従わない期間と関連している可能性が高く、私たちの単純な取引戦略では、このような市場状況に適切に対応できません。
図5:戦略の初期バージョンから得られたエクイティカーブ
バックテストの詳細な結果を確認すると、この戦略は5年間にわたって利益を上げていたことがわかります。これは戦略を改善しようという意欲を高める要因となりますが、勝ちトレードと負けトレードの比率はほぼ50/50であり、これは望ましくありません。負けトレードを除外して、勝ちトレードの割合を高める必要があります。平均連勝回数は2回、平均連敗回数も2回です。これにより、私たちのシステムが資本を失うリスクと同じくらい利益を上げる可能性があるという私たちの指摘が裏付けられます。このようなシステムは、監視なしで取引をおこなうことは信頼できません。
図6:取引バックテストの詳細な要約
提案されたソリューションの概要
この最初のバックテストを通じて、提案されたシステムについて詳しく議論できるようになりました。私たちのシステムは、市場がトレンドに従っている場合には利益を上げますが、市場にトレンドがない場合には損失を出すことが分かっています。それでは、市場がトレンドに従っているかどうかを独自に判断できるシステムをどのように設計すればよいのでしょうか。
市場がこれらの2つのモード間をどのように切り替えるかを知ることで、システムは移動平均インジケーターをより効果的に活用できるようになります。私たちのシステムは、市場がレンジ相場にあると判断した場合、移動平均が示すトレンドに逆らって取引をおこないます。それ以外の場合、つまりトレンド相場の場合には、移動平均が示すトレンドに沿って取引をおこないます。
言い換えれば、アルゴリズムが取引対象の銘柄がレンジ相場にある可能性が高いと判断した場合、価格レベルが移動平均を上回って終値を付けたとしても、買うのではなく売ります。しかし、アルゴリズムが銘柄がトレンド相場にあることを検出した場合、価格レベルが移動平均を上回って終値を付けると購入します。本質的に、同じ市場イベントであっても、市場がどのモードにあるかに応じて解釈と処理が異なります。
したがって、残る唯一の疑問は「市場の現在のモードをどのように特定するか」です。私たちが提案する戦略は、価格レベルを4つの異なるゾーンに分けることです。この戦略の基本的な考え方は直感的です。
- トレンドモード:市場が実際にトレンドを形成するのは、ゾーン1またはゾーン4の場合のみです。
- レンジモード:市場が実際にレンジ相場にあるのは、ゾーン2またはゾーン3の場合のみです。
図7:ゾーンベースの取引システムの簡単な図解例
まず、ゾーン1から順に各ゾーンを見ていきましょう。価格レベルがゾーン1にある場合、これは強気の市場感情と見なされ、価格レベルが100期間移動平均を上回って終値を付けた時には、いつでも購入の機会を探します。テイクプロフィットとストップロスの幅は、最初のバックテストから維持されます。注意点として、私たちはゾーン1にいる間、強気トレンドに従ってロングの機会のみを探し、ゾーン1においてはショートポジションは取らないことにします。
図8:ゾーン1の重要性を説明する
価格レベルがゾーン1からゾーン2に下がると、市場感情が変わります。価格レベルがゾーン2に留まる限り、真のトレンドは見られないと考えています。むしろ、ゾーン2では価格レベルがゾーン2とゾーン3を分ける中間帯域付近で振動する傾向があると予想します。したがって、ゾーン2にいる間は売却の機会のみを探し、価格レベルが中間バンドに戻ると考えます。
ゾーン2にいる間に価格レベルが100期間移動平均を上回った場合は売り、ストップロスは初期の取引戦略に基づいて設定を維持します。しかし、テイクプロフィットの位置は変更する必要があります。テイクプロフィットは、ゾーン2とゾーン3を分ける中間バンドに設定します。これは、ゾーン2内にいる限り、価格レベルがその中間バンドで安定する傾向があると予測するためです。なお、ゾーン2にいる間はロングポジションは取らず、各ゾーンでは1種類のポジションのみを取ることが許可されます。
図9:定義した4つの市場ゾーンを通過する際に、取引戦略がどのように進化するかを理解する
この時点で、読者の頭の中にパターンが形成され、残りのルールが直感的に理解できるようになったことを願っています。ここで、楽しいクイズをして、お互いの認識が一致しているか確認しましょう。価格レベルがゾーン3に下がった場合、どんなポジションを取るべきだと思いますか。心の中で「ロングポジション」と言っていただけたら嬉しいです。
では、テイクプロフィットはどこに設定するのでしょうか。ゾーン2またはゾーン3にいる場合、テイクプロフィットはゾーン2とゾーン3を分ける中間バンドに設定されることを、読者は直感的に理解していると思います。
つまり、ゾーン3にいるときは、価格レベルが100期間移動平均を下回って終値した場合にのみロングポジションを取ります。テイクプロフィットは中間バンドに設定し、ストップロスは元の戦略と同じサイズに保ちます。ゾーン3にいる間はショートポジションは取らないことになります。
図10:ゾーン2とゾーン3は平均回帰ゾーンであると考えられている
最後に、価格レベルがゾーン4にある場合は、ショートポジションを取る機会のみを探します。価格レベルが100期間移動平均を下回るたびにショートポジションを取ります。テイクプロフィットとストップロスの幅は、ベンチマーク戦略で使用された幅と同じに設定します。価格レベルがゾーン4に留まる限り、ロングポジションは取らないことになります。
図11:ゾーン4では弱気トレンドが形成されると考えられるため、ロングポジションは取らない
元のトレンドフォロー戦略を改良して、4ゾーン戦略を実装します。
変更の提案 | 意図した目的 |
---|---|
新しいシステム変数 | 作成するチャネルを制御するには、新しいシステム変数が必要になります。 |
ユーザー入力の作成 | ユーザーはチャネルのすべての側面を制御することはできませんが、チャネルの幅やチャネルの計算に使用されるバーの数などの一部の側面は、エンドユーザーによって制御されます。 |
新しいグローバル変数 | アプリケーションの取引ロジックを改善するために、チャネルに関連する新しいグローバル変数が作成されます。 |
カスタム関数の変更 | 取引アプリケーションに新しい関数を作成し、既存の関数の一部を変更して、概説した取引ロジックを実装します。 |
チャネルの計算に関する詳細については、今後説明していきます。
MQL5でのソリューションの実装
まず、チャネルを計算するために必要なシステム定数を定義します。チャネルの計算には、意図した時間枠よりも長い時間枠に移動平均インジケーターを適用する必要があります。例えば、ここではM1で取引をおこないたいため、日足時間枠に20期間の移動平均を適用し、それを使ってチャネルを計算します。取引するのと同じ時間枠でチャネルを計算してしまうと、チャネルの動きが大きすぎて、安定した取引戦略を構築するのが難しくなる可能性があります。
#define MA_PERIOD 20 //Moving Average period for calculating the mid point of our range zone
次に、チャネルを追跡するグローバル変数を定義する必要があります。特に、ゾーン2(上限)とゾーン3(下限)の境界がどこにあるかを把握することを目指します。さらに、ゾーン2とゾーン3の境界(中間境界)がどこにあるかを正確に特定したいと考えています。ゾーン1は上限から定義され、上限制限はありません。同様に、ゾーン4はゾーン3の終了点から始まり、下限はありません。
double range_zone_mid,range_zone_ub,range_zone_lb; int active_zone;
ここでは、エンドユーザーが変更できるチャネルのパラメータを定義します。例えば、履歴データからの計算期間や、ユーザーが希望するチャネルの幅などです。チャネル幅は口座のリスク量に関連しているため、ユーザーがチャネル幅を制御できるようにすることが重要です。このデモンストレーションでは、チャネル幅を2ATRの値に設定することにします。ほとんどの市場は1日に1ATR程度動く傾向があり、チャネルが過度に動いてしまうことを避けたいことを思い出してください。
//+------------------------------------------------------------------+ //| User inputs | //+------------------------------------------------------------------+ input group "Technical Analysis" input double atr_multiple = 2 ; //ATR Multiple input int bars_used = 30; //How Many Bars should we use to calculate the channel?
現在どのゾーンにいるのかを判断する関数が必要です。ゾーンを判断するロジックについては、すでに詳しく説明しました。
//+------------------------------------------------------------------+ //| Get our current active zone | //+------------------------------------------------------------------+ void get_active_zone(void) { if(iClose(Symbol(),PERIOD_CURRENT,0) > range_zone_ub) { active_zone = 1; return; } if((iClose(Symbol(),PERIOD_CURRENT,0) < range_zone_ub) && (iClose(Symbol(),PERIOD_CURRENT,0) > range_zone_mid)) { active_zone = 2; return; } if((iClose(Symbol(),PERIOD_CURRENT,0) < range_zone_mid) && (iClose(Symbol(),PERIOD_CURRENT,0) > range_zone_lb)) { active_zone = 3; return; } if(iClose(Symbol(),PERIOD_CURRENT,0) < range_zone_lb) { active_zone = 4; return; } } //+------------------------------------------------------------------+
setup関数も更新する必要があります。変更されていない部分は無視しましょう。新しく追加するテクニカルインジケーターは1つだけです。最初に適用したテクニカルインジケーターは、M1時間枠の100期間移動平均でした。新しいインジケーターは、日次時間枠に適用される20期間移動平均です。
//+------------------------------------------------------------------+ //| Setup our global variables | //+------------------------------------------------------------------+ void setup(void) { //We have omitted parts of the code that have not changed ma_handler = iMA(Symbol(),TF_1,MA_PERIOD,MA_SHIFT,MA_MODE,MA_PRICE); }
次に、update関数を調整する必要があります。変更されていない部分は意図的に省略し、新しい部分に焦点を当てます。まず、ゼロで初期化したベクトルを作成します。次に、この新しいベクトルを使用して、前の手順で日次時間枠に適用した新しい移動平均から、ユーザーが計算に使用するバーの数をコピーします。
その後、20期間の日次移動平均の平均値を取得します。これが、ゾーン2とゾーン3を分ける中間バンドになります。ゾーン2とゾーン3の限界は、計算した20期間の移動平均を基に、ATRの倍数を中間点に加算(ゾーン2)または減算(ゾーン3)することによって求められます。
//+------------------------------------------------------------------+ //| Update | //+------------------------------------------------------------------+ void update(void) { static datetime time_stamp; datetime current_time = iTime(Symbol(),PERIOD_CURRENT,0); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); if(time_stamp != current_time) { //Omitted parts of the function that remained unchanged vector ma_average = vector::Zeros(1); ma_average.CopyIndicatorBuffer(ma_handler,0,1,bars_used); range_zone_mid = ma_average.Mean(); range_zone_ub = (range_zone_mid + (atr[0] * atr_multiple)); range_zone_lb = (range_zone_mid - (atr[0] * atr_multiple)); get_active_zone(); Comment("Zone: ",active_zone); ObjectDelete(0,"RANGE HIGH"); ObjectDelete(0,"RANGE LOW"); ObjectDelete(0,"RANGE MID"); ObjectCreate(0,"RANGE MID",OBJ_HLINE,0,0,range_zone_mid); ObjectCreate(0,"RANGE LOW",OBJ_HLINE,0,0,range_zone_lb); ObjectCreate(0,"RANGE HIGH",OBJ_HLINE,0,0,range_zone_ub); } }
最後におこなう変更は、設定を見つけるメソッドに適用されます。ポジション配置の背後にある取引ロジックについては既に広範に説明されているため、このコードセグメントは読者にとって理解しやすいはずです。主な考えを要約すると、ゾーン1とゾーン4ではトレンドに従います。つまり、価格がゾーン1の100期間移動平均を上回って終値した場合、買います。それ以外の場合、ゾーン2または3にいるときはトレンドに逆らいます。つまり、価格レベルがゾーン2の100期間移動平均を上回って終値した場合、ゾーン1のように買うのではなく、売ります。
//+------------------------------------------------------------------+ //| Find setup | //+------------------------------------------------------------------+ void find_setup(void) { // Follow the trend if(active_zone == 1) { //Buy on rallies if(iClose(Symbol(),PERIOD_CURRENT,0) > trading_ma[0]) Trade.Buy(CURRENT_VOL,Symbol(),ask,(bid - (atr[0] * 1.5)),(bid + (atr[0] * SL_WIDTH)),""); } // Go against the trend if(active_zone == 2) { //Sell on rallies if(iClose(Symbol(),PERIOD_CURRENT,0) > trading_ma[0]) Trade.Sell(CURRENT_VOL,Symbol(),bid,(ask + (atr[0] * 1.5)),range_zone_mid); } // Go against the trend if(active_zone == 3) { //Buy the dip if(iClose(Symbol(),PERIOD_CURRENT,0) < trading_ma[0]) Trade.Buy(CURRENT_VOL,Symbol(),ask,(bid - (atr[0] * 1.5)),range_zone_mid,""); } // Follow the trend if(active_zone == 4) { //Sell the dip if(iClose(Symbol(),PERIOD_CURRENT,0) < trading_ma[0]) Trade.Sell(CURRENT_VOL,Symbol(),bid,(ask + (atr[0] * atr_multiple)),(ask - (atr[0] * SL_WIDTH))); } }
すべてをまとめると、これが私たちの取引戦略の改訂版です。
//+------------------------------------------------------------------+ //| Dynamic Moving Average Strategy.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/ja/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/ja/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define MA_PRICE PRICE_CLOSE //Moving average shift #define MA_MODE MODE_EMA //Moving average shift #define MA_SHIFT 0 //Moving average shift #define ATR_PERIOD 14 //Period for our ATR #define TF_1 PERIOD_D1 //Hihger order timeframe #define MA_PERIOD 20 //Moving Average period for calculating the mid point of our range zone #define TRADING_MA_PERIOD 100 //Moving average period #define SL_WIDTH 1.5 //How wide should the stop loss be? #define CURRENT_VOL 0.1 //Trading volume //+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade; //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int ma_handler,trading_ma_handler,atr_handler; double ma[],atr[],trading_ma[]; double range_zone_mid,range_zone_ub,range_zone_lb; double ask,bid; int active_zone; //+------------------------------------------------------------------+ //| User inputs | //+------------------------------------------------------------------+ input group "Technical Analysis" input double atr_multiple = 1; //ATR Multiple input int bars_used = 30; //How Many Bars should we use to calculate the channel? //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- setup(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- IndicatorRelease(ma_handler); IndicatorRelease(atr_handler); IndicatorRelease(trading_ma_handler); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- update(); if(PositionsTotal() == 0) find_setup(); } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Find setup | //+------------------------------------------------------------------+ void find_setup(void) { // Follow the trend if(active_zone == 1) { //Buy on rallies if(iClose(Symbol(),PERIOD_CURRENT,0) > trading_ma[0]) Trade.Buy(CURRENT_VOL,Symbol(),ask,(bid - (atr[0] * 1.5)),(bid + (atr[0] * SL_WIDTH)),""); } // Go against the trend if(active_zone == 2) { //Sell on rallies if(iClose(Symbol(),PERIOD_CURRENT,0) > trading_ma[0]) Trade.Sell(CURRENT_VOL,Symbol(),bid,(ask + (atr[0] * 1.5)),range_zone_mid); } // Go against the trend if(active_zone == 3) { //Buy the dip if(iClose(Symbol(),PERIOD_CURRENT,0) < trading_ma[0]) Trade.Buy(CURRENT_VOL,Symbol(),ask,(bid - (atr[0] * 1.5)),range_zone_mid,""); } // Follow the trend if(active_zone == 4) { //Sell the dip if(iClose(Symbol(),PERIOD_CURRENT,0) < trading_ma[0]) Trade.Sell(CURRENT_VOL,Symbol(),bid,(ask + (atr[0] * atr_multiple)),(ask - (atr[0] * SL_WIDTH))); } } //+------------------------------------------------------------------+ //| Setup our global variables | //+------------------------------------------------------------------+ void setup(void) { atr_handler = iATR(Symbol(),TF_1,ATR_PERIOD); trading_ma_handler = iMA(Symbol(),PERIOD_CURRENT,TRADING_MA_PERIOD,MA_SHIFT,MA_MODE,MA_PRICE); ma_handler = iMA(Symbol(),TF_1,MA_PERIOD,MA_SHIFT,MA_MODE,MA_PRICE); } //+------------------------------------------------------------------+ //| Update | //+------------------------------------------------------------------+ void update(void) { static datetime time_stamp; datetime current_time = iTime(Symbol(),PERIOD_CURRENT,0); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); if(time_stamp != current_time) { time_stamp = current_time; CopyBuffer(ma_handler,0,0,1,ma); CopyBuffer(atr_handler,0,0,1,atr); CopyBuffer(trading_ma_handler,0,0,1,trading_ma); vector ma_average = vector::Zeros(1); ma_average.CopyIndicatorBuffer(ma_handler,0,1,bars_used); range_zone_mid = ma_average.Mean(); range_zone_ub = (range_zone_mid + (atr[0] * atr_multiple)); range_zone_lb = (range_zone_mid - (atr[0] * atr_multiple)); get_active_zone(); Comment("Zone: ",active_zone); ObjectDelete(0,"RANGE HIGH"); ObjectDelete(0,"RANGE LOW"); ObjectDelete(0,"RANGE MID"); ObjectCreate(0,"RANGE MID",OBJ_HLINE,0,0,range_zone_mid); ObjectCreate(0,"RANGE LOW",OBJ_HLINE,0,0,range_zone_lb); ObjectCreate(0,"RANGE HIGH",OBJ_HLINE,0,0,range_zone_ub); } } //+------------------------------------------------------------------+ //| Get our current active zone | //+------------------------------------------------------------------+ void get_active_zone(void) { if(iClose(Symbol(),PERIOD_CURRENT,0) > range_zone_ub) { active_zone = 1; return; } if((iClose(Symbol(),PERIOD_CURRENT,0) < range_zone_ub) && (iClose(Symbol(),PERIOD_CURRENT,0) > range_zone_mid)) { active_zone = 2; return; } if((iClose(Symbol(),PERIOD_CURRENT,0) < range_zone_mid) && (iClose(Symbol(),PERIOD_CURRENT,0) > range_zone_lb)) { active_zone = 3; return; } if(iClose(Symbol(),PERIOD_CURRENT,0) < range_zone_lb) { active_zone = 4; return; } } //+------------------------------------------------------------------+
これらの変更によって、希望どおりの制御が可能になるかどうか確認してみましょう。バックテストを実行する期間を固定します。
図12:バックテストの期間は、最初に使用した期間と一致する
同様に、バックテストの条件を前回のテストと一致するように修正します。
図13:両方の戦略が同じ条件でテストされていることを確認する
新しい戦略には2つの入力があります。1番目はチャネルの幅を制御し、2番目はチャネルの計算に使用されるバーの数を制御します。
図14:エンドユーザーが調整できるコントロール
改訂された戦略によって作成された新しいエクイティカーブには、どの戦略にもあるように損失期間がありますが、注目すべきはその回復速度の速さです。以前の取引戦略のように停滞することはありません。取引で損失を出しても、すぐに市場に再調整されます。これは、損失期間の後に利益を出す取引期間が続くことからも明らかです。
図15:新しい取引戦略によって生成されたエクイティカーブ
新しい取引戦略の詳細な結果を分析すると、勝ちトレードの割合が52%から86%に急増し、負けトレードが47%から13%に減少したことがわかります。平均利益は平均損失よりも小さいですが、ストップロスとテイクプロフィットは固定されていることを考慮してください。この問題は、損失が出ている場合にストップロスを固定し、利益が出ている場合にはストップロスをトレールさせることで解決できます。さらに、平均連勝数は2回から9回に増加し、一方で平均連敗数は2回から1回に減少しました。当初の戦略では300回の取引がおこなわれましたが、新しい戦略では合計1301回の取引がおこなわれました。つまり、新しい戦略は、より多くの取引をおこない、より頻繁に勝つことができるのです。
図16:新しい戦略のパフォーマンスの詳細な概要
結論
本日の議論では、単純なトレンドフォロー戦略から始め、MetaTrader 5端末で利用可能な履歴データを活用して、より情報に基づいた意思決定を行う方法を説明しました。理論的には、この戦略はペンと紙で実装できますが、幸いなことに、MQL5 APIはこのプロセス全体を効率化してくれます。ベクトル関数を用いて統計的な指標を迅速に計算することから、最適な取引実行に至るまで、MetaTrader 5アプリケーションがバックグラウンドで多くの手間を引き受けてくれます。今後の記事では、利益の出る取引の割合をさらに向上させ、損失の出る取引の規模をよりコントロールする方法について検討していきます。
添付ファイル | 詳細 |
---|---|
Benchmark Moving Average Strategy | トレンドフォロー戦略の初期実装 |
Dynamic Moving Average Strategy | 新たに提案されたダイナミック戦略 |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/16856




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