
MQL5で自己最適化エキスパートアドバイザーを構築する(第9回):二重移動平均クロスオーバー
本連載では、従来の移動平均クロスオーバー戦略におけるラグを削減するためのさまざまな視点を検討してきました。
初期の試みでは、統計モデリングツールを用いて、移動平均のクロスオーバーを事前に予測する方法を試しました。この方向で一定の進展があり、適切な市場環境下では、価格を直接予測するよりも移動平均クロスオーバーを予測する方が正確であることが分かりました。さらにラグを減らす別の方法も見つかりました。このアプローチでは、2本の移動平均の期間を共通の値に固定し、クロスオーバーは、一方の移動平均を始値に、もう一方を終値に適用して生成します。この代替システムは有効であり、期間を固定しつつ、2つのインジケーターに異なる価格を適用するだけで、追加のモデリングツールを使用せずにラグをさらに低減できました。
今回のディスカッションでは、これまで検討していなかった新しいアプローチを紹介します。人生や数学の多くの問題と同様、問題解決には複数の方法があり、それぞれに利点と欠点があります。複数の選択肢を比較することで、システムのラグをどの程度制御できるかを理解することを目指します。
ここでは、私が「二重移動平均クロスオーバー戦略」と呼ぶ手法を試みます。図1に示すように、従来の移動平均クロスオーバー戦略は通常、単一の時間足で異なる期間の2本の移動平均を使用して実行されます。以前の議論では両方の移動平均が同一期間でしたが、今回は少し元のクラシカルアプローチに戻し、2本の移動平均に異なる期間を使用できるようにします。
この従来型の問題点は、エントリーシグナルの確認が遅れることです。つまり、値動きがすでに始まった後にシグナルが出るため、エントリーが遅れたり、チャンスを逃したりする可能性があります。
図1:日足での移動平均クロスオーバー戦略の可視化
私たちの提案は完全に新しいものではありません。実際、裁量トレーダーは長年、同様のロジックを用いてきました。基本的な考え方は、上位時間足(例:図1のような日足)でクロスオーバーパターンを観察することです。ただし、この段階で直ちに行動するのではなく、上位時間足でクロスオーバーを確認した後、下位時間足(例:M30、図2)に降りて、対応するクロスオーバーパターンを探します。
人間のトレーダーはしばしば「上位時間足に沿って取引せよ」と言います。従来のアルゴリズムトレードでは、同一時間足で戦略を適用していました。しかし今回は、戦略を2段階で適用します。上位時間足はその日の方向性バイアスを提供し、下位時間足でそのバイアスに沿ったエントリーシグナルを探します。これが二重クロスオーバー戦略の本質です。上位時間足でバイアスを決定し、下位時間足でそのバイアスに沿った取引機会を探すことで、上位時間足のシグナルによるラグを軽減することを目指します。
私たちの理解が一致したところで、この戦略が有効かどうかを判断するための実装を開始できます。コードに取り掛かる前に、このアプローチにはいくつかの可動部分があり、慎重に検討する必要があることは明らかです。まず重要な問いは、エントリー条件をどのように定義するかです。たとえば、上位時間足で強気のクロスオーバーが発生した場合、下位時間足では2つの選択肢があります。
- 逆張りエントリー:下位時間足で弱気のクロスオーバーが発生するのを待ち、それに反して取引をおこないます。これは、最終的に下位時間足が日中の終わりまでに上位時間足の強気バイアスに再調整されると考える戦略です。
- トレンドフォロー(順張り)エントリー:単純に下位時間足で強気のクロスオーバーが発生するのを待ち、上位時間足のバイアスと同じ方向に取引をおこないます。
これら2つの選択肢は、取引戦略におけるエントリーの理念が異なることを示しています。ポジションの決済はさらに複雑で、さまざまなバリエーションがあり、それぞれにトレードオフがあります。たとえば、下位時間足が上位時間足のバイアスと一致しなくなった時点でポジションをクローズする方法があります。あるいは、上位時間足のバイアスが変化した時点までポジションを保持する方法もあります。つまり、日足チャートで強気のシグナルからスタートした場合、上位時間足が弱気に切り替わるまでポジションを保持することになります。
ご覧の通り、この手法を使った取引には、エントリーおよびエグジットの方法が非常に多様です。最適な組み合わせを理論だけで決めるのではなく、遺伝的アルゴリズムを使用して検証することが重要です。これにより、市場データから最も収益性の高い選択肢を特定することが可能になります。
図2:下位時間足(M30)における移動平均クロスオーバー戦略の可視化
最初のステップとして、アプリケーション開発段階で固定すべきいくつかのシステム定数を定義します。簡便さのために、時間足は固定します:日足を上位時間足の代用とし、M15を下位時間足、H4をストップロス計算に使用する時間足とします。
これらのシステム定数は、後に遺伝的アルゴリズムで最適化可能なチューニングパラメータに変換することも検討できます。最適なエントリーを実現するためです。しかし、まずは開発を開始するために、これらの値は固定したままとします。//+------------------------------------------------------------------+ //| Double Crossover.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" //+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ //--- System time frames #define TF_1 PERIOD_D1 #define TF_2 PERIOD_M15 #define TF_3 PERIOD_H4 #define LOT_MULTILPLE 1
また、戦略が動作するさまざまなモードを表現するためのカスタム列挙型を定義する必要があります。たとえば、アプリケーションは順張りモードまたは平均回帰モードで動作させることができ、専用の列挙型によってユーザーがこれらのモードを切り替えられるようにします。同様に、ポジションの決済条件を下位時間足で評価するか、上位時間足で評価するかを指定するための列挙型も定義します。
//+------------------------------------------------------------------+ //| Custom enumerations | //+------------------------------------------------------------------+ //--- What trading style should we follow when opening our positions, trend following or mean reverting? enum STRATEGY_MODES { TREND = 0, //Trend Following Mode MEAN_REVERTING = 1 //Mean Reverting Mode }; //--- Which time frame should we consult, when determining if we should close our position? enum CLOSING_TIME_FRAME { HIGHER_TIME_CLOSE = 0, //Close on higher time frames LOWER_TIME_CLOSE = 1 //Close on lower time frames };
入力パラメータは比較的シンプルです。まず、上位時間足の期間値を1つ指定し、次に第1移動平均期間と第2移動平均期間の間のギャップを定義します。この設計により、遺伝的アルゴリズムは必ず1以上のギャップを選択するようになり、期間間にゼロでない差を持たせることが保証されます。
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "Technical Indicators" input int ma_1_period = 10; //Higher Time Frame Period input int ma_1_gap = 20; //Higher Time Frame Period Gap input int ma_2_period = 10; //Lower Time Frame Period input int ma_2_gap = 20; //Lower Time Frame Period Gap input group "Strategy Settings" input STRATEGY_MODES strategy_mode = 0; //Strategy Operation Mode input CLOSING_TIME_FRAME closing_tf = 0; //Strategy Closing Timeframe
アプリケーションでは、テクニカル指標や現在の市場価格を管理するために使用するインスタンスなど、比較的少数のグローバル変数のみが必要です。
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int ma_c_1_handle,ma_c_2_handle,ma_c_3_handle,ma_c_4_handle; double ma_c_1[],ma_c_2[],ma_c_3[],ma_c_4[]; double volume_min; double bid,ask; int state;
この演習で必要となる外部依存は、ポジションの読み込みやクローズに使用するTradeライブラリのみです。
//+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade;
初期化時には、ユーザーから渡された設定を用いてテクニカル指標を構成します。また、システムの状態を「-1」にリセットし、ポジションがオープンされていないことを示すとともに、市場で許容される最小取引量を記録します。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- volume_min = SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN); ma_c_2_handle = iMA(Symbol(),TF_1,ma_1_period,0,MODE_SMA,PRICE_CLOSE); ma_c_1_handle = iMA(Symbol(),TF_1,(ma_1_period + ma_1_gap),0,MODE_SMA,PRICE_CLOSE); ma_c_4_handle = iMA(Symbol(),TF_2,ma_2_period,0,MODE_SMA,PRICE_CLOSE); ma_c_3_handle = iMA(Symbol(),TF_2,(ma_2_period + ma_2_gap),0,MODE_SMA,PRICE_CLOSE); state = -1; //--- return(INIT_SUCCEEDED); }
アプリケーションが使用されなくなった場合は、テクニカル指標のメモリを解放します。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- IndicatorRelease(ma_c_1_handle); IndicatorRelease(ma_c_2_handle); IndicatorRelease(ma_c_3_handle); IndicatorRelease(ma_c_4_handle); }
新しい価格データを受信した際には、下位時間足(この場合はM15)で新しいローソク足が形成されたかを確認します。新しいローソク足が検出された場合は、内部のシステム状態を更新します。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- datetime current_time = iTime(Symbol(),TF_2,0); static datetime time_stamp; if(time_stamp != current_time) { time_stamp = current_time; update(); } } //+------------------------------------------------------------------+
最も重要な要素のひとつは、取引のセットアップを特定するために使用される関数です。この関数は「パディング」と呼ばれるパラメータを受け取ります。パディングはポジションのストップロス幅を表し、親関数によって過去の値動きのレンジから自然に計算されます。安全策として、セットアップ関数はまず、過剰取引を防ぐために現在のオープンポジションがゼロであるかを確認します。
上位時間足で強気のクロスオーバーが発生し、戦略が順張りモードで動作している場合、セットアップ関数は下位時間足で同方向の強気クロスオーバーを探します。一方、平均回帰モードで動作している場合は、逆のクロスオーバー(すなわち弱気)を探し、それに対して逆張りをおこないます。このロジックは弱気のエントリーでも同様で、上位時間足で弱気のクロスオーバーが発生した場合、選択されたモードに応じて下位時間足で対応するシグナルを探します。
//+------------------------------------------------------------------+ //| Find a trading signal | //+------------------------------------------------------------------+ void find_setup(double padding) { if(PositionsTotal() == 0) { //--- Reset the system state state = -1; //--- Bullish on the higher time frame if(ma_c_1[0] > ma_c_2[0]) { //--- Trend following mode if((ma_c_3[0] > ma_c_4[0]) && (strategy_mode == 0)) { Trade.Buy(volume_min,Symbol(),ask,(bid - padding),0,""); state = 1; } //--- Mean reverting mode if((ma_c_3[0] < ma_c_4[0]) && (strategy_mode == 1)) { Trade.Buy(volume_min,Symbol(),ask,(bid - padding),0,""); state = 1; } } //--- Bearish on the higher time frame if(ma_c_1[0] < ma_c_2[0]) { //--- Trend following mode if((ma_c_3[0] < ma_c_4[0]) && (strategy_mode == 0)) { Trade.Sell(volume_min,Symbol(),bid,(ask + padding),0,""); state = 0; } //--- Mean reverting mode if((ma_c_3[0] > ma_c_4[0]) && (strategy_mode == 1)) { Trade.Sell(volume_min,Symbol(),bid,(ask + padding),0,""); state = 0; } } } }
ポジションをオープンした後は、別のポジション管理関数を呼び出します。この関数も、取引を下位時間足または上位時間足に基づいてクローズするかどうかによって、2つの異なるモードで動作します。ポジションがオープンされるとシステム状態が更新され、この状態によって取引方向が決まります。たとえば、状態がゼロの場合は売りポジションをオープンしたことを示します。もし第1移動平均線が第2移動平均線を上回っており(日足での強気の勢いを示す場合)、かつ上位時間足に基づいて決済を管理している場合は、そのポジションをクローズします。一方、下位時間足に基づいて管理している場合は、そちらのクロスオーバーが発生するのを待ちます。 どちらのエグジット条件がより効果的かはすぐには明らかではないため、両方のオプションをテストする必要があります。
//+------------------------------------------------------------------+ //| Manage our open positions | //+------------------------------------------------------------------+ void manage_setup(void) { if(closing_tf == 0) { if((state ==0) && (ma_c_1[0] > ma_c_2[0])) Trade.PositionClose(Symbol()); if((state ==1) && (ma_c_1[0] < ma_c_2[0])) Trade.PositionClose(Symbol()); } else if(closing_tf == 1) { if((state ==0) && (ma_c_3[0] > ma_c_4[0])) Trade.PositionClose(Symbol()); if((state ==1) && (ma_c_3[0] < ma_c_4[0])) Trade.PositionClose(Symbol()); } }
最後に、現在の買値および売値など、主要なシステム変数を更新する関数が必要です。また、第三の時間足であるリスク時間足における過去10本の高値と安値も追跡します。この例ではリスク時間足はH4で、M15と日足の間に位置しており、市場リスクを測定したり、エントリーからあまりにも近すぎず、また遠すぎないストップロスを設定するために適した候補となります。
//+------------------------------------------------------------------+ //| Update our technical indicators and positions | //+------------------------------------------------------------------+ void update(void) { //Update technical indicators and market readings CopyBuffer(ma_c_2_handle,0,0,1,ma_c_2); CopyBuffer(ma_c_1_handle,0,0,1,ma_c_1); CopyBuffer(ma_c_4_handle,0,0,1,ma_c_4); CopyBuffer(ma_c_3_handle,0,0,1,ma_c_3); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); vector high = vector::Zeros(10); vector low = vector::Zeros(10); low.CopyRates(Symbol(),TF_3,COPY_RATES_LOW,0,10); high.CopyRates(Symbol(),TF_3,COPY_RATES_HIGH,0,10); vector var = high - low; double padding = var.Mean(); //Find an open position if(PositionsTotal() == 0) find_setup(padding); //Manage our open positions else if(PositionsTotal() > 0) manage_setup(); } //+------------------------------------------------------------------+
バックテストを開始するためには、まず私たちが一緒に作成したエキスパートアドバイザー「Double Crossover.ex5」を選択する必要があります。次に、通貨ペアとしてEURUSDを選び、1分足の時間足を指定します。バックテスト期間は2020年1月から今年までの5年間とします。その後、結果をできるだけ現実的にするため、手元のデータの半分を用いてフォワードテストをおこないます。
図3:取引アプリケーションとトレーニングの日付を選択する
実際の市場環境をシミュレートするために、遅延はランダムに設定し、モデリングモードにはリアルティックティックを使用します。なお、最適化手順には高速な遺伝的アルゴリズムを使用することを思い出してください。
図4:市場シミュレーション条件を選択する
次に、先に説明した通り、調整が必要な戦略の入力パラメータを選択します。これには、上位および下位時間足の移動平均クロスオーバー期間とギャップが含まれます。加えて、戦略の2つの運用モード、順張りモードと平均回帰モードを有効にします。最後に、戦略は上位時間足または下位時間足のどちらに基づいてポジションをクローズするかを設定可能です。これらすべての設定が、遺伝的アルゴリズムによって最適なパラメータを探索する対象となります。
図5:遺伝的アルゴリズムで調整する戦略パラメータを選択する
バックテストの結果は概ね好ましいものです。最適化結果を見ると、ほとんどの戦略は利益を上げており、特に平均回帰モードでの成績が良好でした。しかし、フォワードテストの結果を考慮すると、バックテストでは利益を上げていた平均回帰モードの戦略よりも、順張りモードで利益を上げた戦略の方が多いことが分かります。
図6:初期テストからのバックテスト結果
さらに懸念されるのは、フォワードテストの結果を詳しく見ると、バックテストで利益を上げた戦略がフォワードテストと一致しないことです。つまり、フォワードテストでうまく機能した戦略のほとんどが、バックテストでは利益を上げていません。
図7:初回テストのフォワードテスト結果。ほとんどの戦略は両方のテストで安定していない
私は手作業でフォワードテストの結果をフィルタリングし、バックテストとフォワードテストの両方で利益を上げる戦略設定を見つける必要がありました。これが安定性の目安であり、戦略はバックテストとフォワードテストの両方で利益を上げる必要があります。両方の条件を満たす設定がわずかしか存在しなかったため、追加の改良を試みる動機となりました。
図8:遺伝的アルゴリズムで生成された戦略のうち、両方のテストで利益を上げたものはわずかしかない。これは良好な指標ではない
さらなる改善
戦略を改善する有力な方策のひとつは、遺伝的アルゴリズムにストップロスやリスクパラメータを計算する際の時間足を制御させることです。さらに、ストップロス計算に使用する過去バーの本数も、アルゴリズムに決定させることにします。最初の試行では、H4のバー10本で十分と仮定していました。しかし今回、アルゴリズムにこの設定を調整させ、性能が向上するかどうかをテストします。//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "Money Management Settings" input ENUM_TIMEFRAMES TF_3 = PERIOD_H4; //Risk Time Frame input int HISTORICAL_BARS = 10; //Historical bars for risk calculation
コードにも変更を加える必要があります。前回のバージョンでは、updateメソッドが各ポジションのパディング計算を担当していました。しかし今回の新しいバージョンでは、別の関数がパディングを担当します。これは、ストップロスをトレーリングさせ、利益が出ているポジションに追従させたいからです。
//+------------------------------------------------------------------+ //| Get the stop loss size to use | //+------------------------------------------------------------------+ double get_padding(void) { vector high = vector::Zeros(10); vector low = vector::Zeros(10); low.CopyRates(Symbol(),TF_3,COPY_RATES_LOW,0,HISTORICAL_BARS); high.CopyRates(Symbol(),TF_3,COPY_RATES_HIGH,0,HISTORICAL_BARS); vector var = high - low; double padding = var.Mean(); return(padding); }
updateメソッドもそれに応じて変更されます。パディングはget_paddingメソッドを使って計算されます。updateメソッドの当初ポジションを特定していた部分は、現在はセットアップを見つけるための関数と、オープンポジションを管理する別のメソッドを呼び出すようになります。
//+------------------------------------------------------------------+ //| Update our technical indicators and positions | //+------------------------------------------------------------------+ void update(void) { //Update technical indicators and market readings CopyBuffer(ma_c_2_handle,0,0,1,ma_c_2); CopyBuffer(ma_c_1_handle,0,0,1,ma_c_1); CopyBuffer(ma_c_4_handle,0,0,1,ma_c_4); CopyBuffer(ma_c_3_handle,0,0,1,ma_c_3); bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); double padding = get_padding(); //Find an open position if(PositionsTotal() == 0) find_setup(padding); //Manage our open positions else if(PositionsTotal() > 0) manage_setup(); } //+------------------------------------------------------------------+
ポジションを管理するメソッドは、常に新たに提案されたストップロス値が現在の値よりも有利かどうかを確認します。有利であればストップロスを更新し、そうでなければ現在の値を維持します。
//+------------------------------------------------------------------+ //| Manage our open positions | //+------------------------------------------------------------------+ void manage_setup(void) { //Does the position exist? if(PositionSelect(Symbol())) { //Get the current stop loss double current_sl = PositionGetDouble(POSITION_SL); double padding = get_padding(); double new_sl; //Sell position if((state == 0)) { new_sl = (ask + padding); if(new_sl < current_sl) Trade.PositionModify(Symbol(),new_sl,0); } //Buy position if((state == 1)) { new_sl = (bid - padding); if(new_sl > current_sl) Trade.PositionModify(Symbol(),new_sl,0); } if(closing_tf == 0) { if((state ==0) && (ma_c_1[0] > ma_c_2[0])) Trade.PositionClose(Symbol()); if((state ==1) && (ma_c_1[0] < ma_c_2[0])) Trade.PositionClose(Symbol()); } else if(closing_tf == 1) { if((state ==0) && (ma_c_3[0] > ma_c_4[0])) Trade.PositionClose(Symbol()); if((state ==1) && (ma_c_3[0] < ma_c_4[0])) Trade.PositionClose(Symbol()); } } }
テストをおこなうにあたり、バックテストとフォワードテストの両方で利益を上げた1つの設定を選択し、その他の設定は固定したまま、遺伝的アルゴリズムにリスクパラメータの最適化を任せました。
図9:初期結果の改善を試みる。遺伝的アルゴリズムにリスク設定の調整を任せたにもかかわらず、新しい結果もまだバックテストとフォワードテストの両方で利益を上げることはできなかった
残念ながら、同じ問題に再び直面しました。リスクパラメータの最適化をおこなった際、アプリケーションはフォワードテストでは利益を上げたものの、バックテストでは利益を上げることができませんでした。
図10:新しい結果もまだ両方のテストで利益を上げられていない
結論
この演習から多くのことを学びました。二重移動平均クロスオーバー戦略を用いることで、戦略の遅延の量をある程度制御できることが確認できましたが、その変更の影響は必ずしもすぐには明確になりません。すべてのパラメータを同時に最適化して再実行することも検討すべきかもしれません。1つのパラメータだけを調整し、他を固定する方法は最適なアプローチでない可能性があります。全パラメータを同時に探索することで、より安定した結果が得られるかもしれません。
次回の議論では、フル最適化スイープを再実行した後、最も利益を上げた設定に基づいて統計モデルを構築します。これにより、さらにラグを低減できる可能性があります。しかし現時点では、順調なスタートを切ったと言えます。最適化は保証を提供するものではなく、AIは努力する開発者に代わるものではありません。遺伝的アルゴリズムがバックテストとフォワードテストの両方で利益を上げる戦略群を提供できるまで、最適化手順を繰り返す必要があります。さもなければ、最適化は時期尚早と言えます。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/18793
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。





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