MQL5で自己最適化エキスパートアドバイザーを構築する(第8回):複数戦略分析(2) - 加重投票方策
ここでは、マルチ戦略エキスパートアドバイザー(EA)の最終構成要素であるWilliams Percentage Reversal戦略を追加します。これまでの記事と同様に、まず手動で戦略をハードコーディングし、その実装をベンチマークとして、今回構築する取引用クラスの性能を比較しました。ただし、本連載を継続してお読みいただいている方々にとって冗長にならないよう、クラスの検証に用いたテスト結果の詳細は割愛します。ここでは、提示するクラスの整合性を十分に確認したことだけを付け加えておきます。
さて、複数の戦略を組み合わせたアンサンブルを構築する際、「選択したすべての戦略は本当に必要なのか?」「一部を削減したほうがパフォーマンスの向上につながるのではないか?」 「それをどのように検証できるのか?」といった疑問が自然に浮かびます。
幸いなことに、遺伝的アルゴリズム最適化ツールは、このような難しい問いに対しても、適切に問題を定式化すれば有効な答えを導き出すことができます。
そのために、本稿では各戦略が一票ずつ投票する「民主的」な仕組みを採用します。各戦略の投票の重みはチューニングパラメータとして設定し、遺伝的最適化ツールによって最適化をおこないます。ある戦略が全体のパフォーマンスに正の寄与をしていないと判断されれば、その投票重みはゼロに近づきます。逆に、有効な戦略の重みは高められます。
この仕組みを「重み付き投票方式」と呼びます。初期状態では、すべての戦略に一様に重みを割り当て、ベンチマークとなる性能水準を設定します。本記事の例では、各戦略の投票重みを0〜1の範囲内で初期値0.5に設定するところから開始します。
その後、遺伝的最適化ツールによってこれらの重みを調整し、収益性を最大化するとともに、3つの戦略すべてが実際に有用であるかを検証します。
この手順によって、非常に多様な構成パターンが得られることが分かりました。それぞれの構成では、戦略の設定内容に応じて各戦略の有効性が変わります。各構成においては、それぞれの戦略の重みが変動します。つまり、ある構成では1つの戦略だけが有効である一方で、別の構成では3つすべての戦略が正の寄与を示す場合もあります。
このことから、「3つすべての戦略は必要なのか?」という問いに対する答えは、非常に難しいものになります。今回の結果からは、その答えが最初にアプリケーションで採用された構成に大きく依存することが示唆されます。では、始めましょう。
MQL5の始め方
議論の終わりには、取引戦略の継承ツリーを図1のように可視化できるようになります。ここでは3つの個別取引戦略を扱います。
- 相対力指数モメンタム(RSI)戦略
- 移動平均クロスオーバー(MA)戦略
- ウィリアムズパーセントレンジ反転(WPR: Williams Percent Range Reversal)戦略
これらはいずれも、ロングまたはショートのエントリーをシグナルできるといった共通の機能を持っています。この3つの戦略はすべて共通の親クラスを継承しており、これによってクラス全体で統一された機能性を保つことができます。
本稿では、図1に示す3つの戦略のうち最後の戦略、ウィリアムズパーセントレンジ反転戦略の実装に焦点を当てます。その後、遺伝的最適化ツールが各戦略に割り当てられた重みを調整し、最も収益性の低い戦略がアプリケーション全体のパフォーマンスを損なわないようにします。

図1:取引戦略間で共有される継承ツリーの現時点の構造
さらに、図2では戦略の中核となる考え方を視覚的に示す補助図を掲載しています。図2の例では、全体の投票重みの合計が1になっていないことに注意してください。一般的にはそのような制約を設けることもありますが、今回はあえてそうしていません。この変種は将来的に検討する可能性がありますが、その場合は今回実装するものとは若干異なるアルゴリズムが必要になります。
現段階では、3つの戦略それぞれについて、遺伝的最適化ツールが0〜1の範囲内(両端を含む)で値を割り当てられるようにしています。遺伝的最適化ツールは、最も収益性の低い戦略を考慮しなくてよい場合、より簡単に収益性の高い組み合わせを生成できます。これが今回の議論の背景にある考え方です。つまり、遺伝的最適化ツールが戦略の他の重要なパラメータをチューニングしながら、取引戦略ツリーを剪定できることを示すのが目的です。

図2:遺伝的最適化ツールによって各戦略に割り当てられた重みの可視化
戦略の実装における最初のステップは依存関係の読み込みです。最初の依存関係は、以前にWPR戦略でおこなったように、あらかじめ作成しておいたシングルバッファ版のWPRインジケータークラスをロードすることです。その後、別の記事で開発した親戦略クラスを読み込みます。
//+------------------------------------------------------------------+ //| WPRReversal.mqh | //| 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" //+------------------------------------------------------------------+ //| Dependencies | //+------------------------------------------------------------------+ #include <VolatilityDoctor\Indicators\WPR.mqh> #include <VolatilityDoctor\Strategies\Parent\Strategy.mqh>
依存関係を読み込んだ後は、クラスのメンバー定義に進みます。最初のメンバーは、使用するWPRインジケーターを参照するものです。これはprivateメンバーであり、本クラス内で唯一のprivateメンバーとなります。残りのメンバーはすべてpublicメンバーです。具体的には、コンストラクタ、デストラクタ、そして親クラスから継承した仮想メソッドを含みます。
class WPRReversal : public Strategy { private: //--- The instance of the RSI used in this strategy WPR *my_wpr; public: //--- Class constructor WPRReversal(string user_symbol,ENUM_TIMEFRAMES user_timeframe,int user_period); //--- Class destructor ~WPRReversal(); //--- Class overrides virtual bool Update(void); virtual bool BuySignal(void); virtual bool SellSignal(void); };
まずはUpdateメソッドをオーバーライドします。Updateメソッドは単にset_indicator_valuesメソッドを呼び出します。このset_indicator_valuesは、すべてのインジケータークラスに共通して存在する関数です。この関数はターミナルから取得したWPRの値をインジケーターバッファに格納します。また、呼び出し元へ制御を戻す前に、データ数がゼロでないことを確認する安全チェックをおこないます。
//+------------------------------------------------------------------+ //| Our strategy update method | //+------------------------------------------------------------------+ bool WPRReversal::Update(void) { //--- Set the indicator value my_wpr.SetIndicatorValues(Strategy::GetIndicatorBufferSize(),true); //--- Check readings are valid if(my_wpr.GetCurrentReading() != 0) return(true); //--- Something went wrong return(false); }
ここで、買いエントリーおよび売りエントリーを判定するための2つのメソッドを定義します。これらのメソッドは、それぞれの条件が満たされている場合にtrueを返す仕組みになっています。
//+------------------------------------------------------------------+ //| Check for our buy signal | //+------------------------------------------------------------------+ bool WPRReversal::BuySignal(void) { //--- Buy signals when the RSI is above 50 return(my_wpr.GetCurrentReading()>50); } //+------------------------------------------------------------------+ //| Check for our sell signal | //+------------------------------------------------------------------+ bool WPRReversal::SellSignal(void) { //--- Sell signals when the RSI is below 50 return(my_wpr.GetCurrentReading()<50); }
最後に、パラメータ付きコンストラクタを定義します。このコンストラクタは、WPRインジケーターを初期化する際に使用する銘柄、時間足、および期間を受け取ります。また、デストラクタでは、WPRクラスのインスタンスに対して作成したポインタを削除します。
//+------------------------------------------------------------------+ //| Our class constructor | //+------------------------------------------------------------------+ WPRReversal::WPRReversal(string user_symbol,ENUM_TIMEFRAMES user_timeframe,int user_period) { my_wpr = new WPR(user_symbol,user_timeframe,user_period); Print("WPRReversal Strategy Loaded."); } //+------------------------------------------------------------------+ //| Our class destructor | //+------------------------------------------------------------------+ WPRReversal::~WPRReversal() { delete my_wpr; } //+------------------------------------------------------------------+
EAの構築
ここから、現在の設定で使用するEAの定義を開始します。EAの最初の部分として、システム定数を設定します。これらの定数は、テストの再現性を確保するために固定しておきます。移動平均のシフトや使用する移動平均の種類などの単純なパラメータは固定する必要があります。これらは、シフトをゼロにするなど、覚えやすい値に設定します。
//+------------------------------------------------------------------+ //| MSA Test 1.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 | //+------------------------------------------------------------------+ //--- Fix any parameters that can afford to remain fixed #define MA_SHIFT 0 #define MA_TYPE MODE_EMA #define RSI_PRICE PRICE_CLOSE
さらに、特定のユーザー入力を受け入れる必要があります。例え話を思い出してください。私たちは、これらの入力提案を遺伝的最適化ツールから受け入れることを意図しています。最初の3つの入力グループは、読者にとって非常に馴染みのあるものです。これは、単に私たちがテクニカル指標に使用する期間を示しています。
本議論で特に注目する入力グループは、最後の「グローバル戦略パラメータ」グループです。ここに、本日設定するウェイトが保存されます。保持期間や戦略の時間軸といった設定は、既存の読者には既に馴染みのある内容です。しかし、新しい読者に向けて説明すると、保持期間とはポジションが成熟し、クローズすべきかどうかを判断するまでの期間を指します。もちろん、この保持期間は戦略の時間軸に依存します。たとえば、M10の時間軸で保持期間を5に設定した場合、ポジションはクローズするまで50分間保有することになります。
//+------------------------------------------------------------------+ //| User Inputs | //+------------------------------------------------------------------+ input group "Moving Average Strategy Parameters" input int MA_PERIOD = 10;//Moving Average Period input group "RSI Strategy Parameters" input int RSI_PERIOD = 15;//RSI Period input group "WPR Strategy Parameters" input int WPR_PERIOD = 30;//WPR Period input group "Global Strategy Parameters" input ENUM_TIMEFRAMES STRATEGY_TIME_FRAME = PERIOD_D1;//Strategy Timeframe input int HOLDING_PERIOD = 5;//Position Maturity Period input double weight_1 = 0.5;//Strategy 1 vote weight input double weight_2 = 0.5;//Strategy 2 vote weight input double weight_3 = 0.5;//Strategy 3 vote weight
次に、私たちの取引アプリケーションが必要とする依存関係について説明します。最初の依存関係は取引ライブラリで、これは基本的な依存関係となります。取引ライブラリは、ポジション管理をおこなう際に役立ちます。そこから、TimeInfoやTradeInfoといったカスタムで作成された依存関係があります。これらはそれぞれ、市場情報に基づいていつ行動できるかを知るため、最低取引レベルや売値、最小取引可能額にアクセスするために使用されます。
残りの3つの依存関係は、これまでのシリーズで一緒に構築してきた戦略クラスから来ています。既にご存知の方も多いでしょう。もし新しい読者であれば、少なくとも最後の依存関係は認識できるはずです。なぜなら、それが本日一緒に構築した内容だからです。
//+------------------------------------------------------------------+ //| Dependencies | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> #include <VolatilityDoctor\Time\Time.mqh> #include <VolatilityDoctor\Trade\TradeInfo.mqh> #include <VolatilityDoctor\Strategies\OpenCloseMACrossover.mqh> #include <VolatilityDoctor\Strategies\RSIMidPoint.mqh> #include <VolatilityDoctor\Strategies\WPRReversal.mqh>
また、カスタムオブジェクトのハンドラーや、ポジションの成熟までの時間を把握するためのタイマーなどの、いくつかのグローバル変数も必要となります。
//+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ //--- Custom Types CTrade Trade; Time *TradeTime; TradeInfo *TradeInformation; RSIMidPoint *RSIMid; OpenCloseMACrossover *MACross; WPRReversal *WPRR; //--- System Types int position_timer;
アプリケーションが最初に初期化されるときに、戦略やTradeInfoクラスなどのカスタム定義クラスの新しいインスタンスを作成します。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Create dynamic instances of our custom types TradeTime = new Time(Symbol(),STRATEGY_TIME_FRAME); TradeInformation = new TradeInfo(Symbol(),STRATEGY_TIME_FRAME); MACross = new OpenCloseMACrossover(Symbol(),STRATEGY_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE); RSIMid = new RSIMidPoint(Symbol(),STRATEGY_TIME_FRAME,RSI_PERIOD,RSI_PRICE); WPRR = new WPRReversal(Symbol(),STRATEGY_TIME_FRAME,WPR_PERIOD); //--- Everything was fine return(INIT_SUCCEEDED); } //--- End of OnInit Scope
アプリケーションが使用されなくなったら、メモリがリークされないようにこれらのカスタム定義オブジェクトを削除します。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Delete the dynamic objects delete TradeTime; delete TradeInformation; delete MACross; delete RSIMid; } //--- End of Deinit Scope
新しい価格データが受信されるたびに、まず新しいローソク足が形成されたかどうかを確認します。もし形成されていれば、その後、戦略内のパラメータや指標の値を更新します。最後に、ポジションが開かれていない場合は、ポジションタイマーをリセットし、シグナル条件を確認します。逆に、すでにポジションが開かれている場合は、ポジションをクローズする準備を進めながら、成熟までの時間を追跡します。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Check if a new daily candle has formed if(TradeTime.NewCandle()) { //--- Update strategy Update(); //--- If we have no open positions if(PositionsTotal() == 0) { //--- Reset the position timer position_timer = 0; //--- Check for a trading signal CheckSignal(); } //--- Otherwise else { //--- The position has reached maturity if(position_timer == HOLDING_PERIOD) Trade.PositionClose(Symbol()); //--- Otherwise keep holding else position_timer++; } } } //--- End of OnTick Scope
Updateメソッドは、各戦略に関連付けられた更新関数を呼び出すだけで実装されます。
//+------------------------------------------------------------------+ //| Update our technical indicators | //+------------------------------------------------------------------+ void Update(void) { //--- Update the strategy RSIMid.Update(); MACross.Update(); WPRR.Update(); } //--- End of Update Scope
CheckSignalメソッドの設定方法は興味深いものです。具体的には、まず総投票数をゼロに初期化することから始めます。プロセスの終了時に総投票数が正であれば買いに入り、逆に総投票数が負であれば売りに入ります。次に、各戦略がどのシグナルを生成しているかを確認します。戦略がロングシグナルを出している場合、その戦略のウェイトを総投票数に加算します。ショートシグナルを出している場合は、その戦略のウェイトを総投票数から減算します。各戦略には一度だけ投票の機会が与えられます。最後に、上述のルールに従って総投票数を評価します。
//+------------------------------------------------------------------+ //| Check for a trading signal using our cross-over strategy | //+------------------------------------------------------------------+ void CheckSignal(void) { double vote = 0; if(MACross.BuySignal()) vote += weight_1; else if(MACross.SellSignal()) vote -= weight_1; if(RSIMid.BuySignal()) vote += weight_2; else if(RSIMid.SellSignal()) vote -= weight_2; if(WPRR.BuySignal()) vote += weight_3; else if(WPRR.SellSignal()) vote -= weight_3; //--- Long positions when the close moving average is above the open if(vote > 0) { Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,""); return; } //--- Otherwise short else if(vote < 0) { Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,""); return; } } //--- End of CheckSignal Scope
どのアプリケーションでもそうですが、最後には作成したすべてのシステム定数を統合する必要があります。
//+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef MA_SHIFT #undef RSI_PRICE #undef MA_TYPE //+------------------------------------------------------------------+
さて、これでトレーニング戦略のテストおよび最適化を開始する準備ができました。まず、先ほど一緒に構築したEAを選択します。次に、アプリケーションをテストする銘柄を指定します。これまでの説明でも触れた通り、日足でEURUSDを使用しています。テスト期間はカスタムの間隔を用いて選択し、2023年2月から2025年5月までの期間でテストを実施しています。

図3:遺伝的最適化で使用する設定および日付
フォワードテストでは、データの半分を使用します。つまり、前半はバックテストに使用し、後半をフォワードテストに用います。フォワードテストは、どの戦略が安定しており、どの戦略がバックテストに対して過剰適合している可能性があるかを確認することを目的としています。市場イベントのより現実的なシミュレーションをおこなうため、常にランダムな遅延を加え、モデリングは実際のティックデータに基づいておこないます。

図4:遺伝的最適化に対して、どのパラメータをどの間隔で探索するかを指示
最後に最適化では、高速な遺伝的アルゴリズムを選択します。戦略内の総パラメータ数は、制御および制限することに努めてきた特定の次元です。しかし、見ての通り、戦略を最適化するために必要な総ステップ数は、控えめな設定であっても非常に大きくなっています。わずか一段階で驚くほど大きくなったのです。したがって、一部の作業をMQL5クラウドにオフロードする必要があると判断しました。追従するには、まずMQL5アカウントにログインし、残高があることが必要です。

図5:MetaTrader 5ターミナルからMQL5ユーザーアカウントにログインする
最適化手順を開始したら、マシンで使用可能なコア数を右クリックし、[MQL5クラウドネットワークを使用する]を選択します。これはすべて、ストラテジーテスターの[エージェント]タブでおこないます。

図6:MQL5クラウドを有効にしてバックテストを加速する
MQL5クラウドを有効化すると、マシン上で実行される一部のタスクがクラウドにオフロードされます。これにより最適化手順が加速され、ネットワークが安全かつ信頼できる場合、より早く結果を得られることが期待されます。

図7:最寄りのデータセンターに接続する
最適化結果は、遺伝的最適化ツールがアクセスできる過去データに対して行われたテストを示します。これにより、戦略のパフォーマンスを評価し、パラメータを調整して性能を改善することが可能です。ただし、遺伝的最適化ツールはフォワードテストの結果にはアクセスできません。フォワードテストは、選択した戦略設定がサンプル外でどのように機能するかを反映しています。
バックテスト結果を見ると、利益水準はこれまでの取引アプリケーションのバージョンで達成されたものと一致しています。上位の戦略を確認すると、各サブ戦略に割り当てられた重みは0.4から0.8の範囲内に収まっており、上位戦略の重みは互いに非常に近いことがわかります。これは、バックテストで最もパフォーマンスが良かった構成が、すべての戦略を用いていたことを示唆しています。

図8:遺伝的最適化プロセスのバックテスト結果
しかし、フォワードテストを見ると、上位の戦略は主に2つの戦略のみを利用していたことがわかります。実際、上位戦略では戦略3の重みが最小限で、ゼロに設定されているケースもあります。
残念なことに、フォワードテストで上位に入った戦略のうち、バックテストでも利益を上げた戦略はごくわずかです。しかし、両方のテストで良好な成績を収めた戦略を見ると、戦略3の重みは依然として小さく、最も安定した構成でも同様でした。
したがって、フォワードテストの上位戦略において戦略3は有意義な貢献をしていないため、削除を検討する考えが浮かびます。ただし、この結論は最も利益が大きかった構成に基づくものであり、この方法で結論を導くことは、データへの過剰適合を招く可能性があるため必ずしも推奨されません。
それでも、両方のテストで利益を上げたすべての戦略を確認すると、多くの場合、3つの戦略の重みは比較的近い値で設定されています。唯一の例外は、戦略3の重みが最も小さいにもかかわらず、最良のパフォーマンスを示したケースです。このような不確実性の中で意思決定をおこなうことは困難ですが、これこそが、私たちに課せられた挑戦の本質です。
したがって、可能な限り最良のパフォーマンスに沿った行動を信じ、戦略3はそれほど重要ではないと結論づけ、最初の2つの戦略のみを使用して続行します。

図9:遺伝的最適化プロセスのフォワードテスト結果から、戦略3は成功に必須ではない可能性が示唆される
結論
これまでの議論からもわかるように、複数の戦略を組み合わせてアンサンブルアプリケーションに使用する最適な戦略数を決定することは、非常に難しい課題になり得ます。最初から、1つの戦略が必要なのか、5つなのか、あるいは10の異なる戦略が必要になるのかを知ることはできません。
しかし、重要なポイントは、遺伝的最適化ツールを使用することで、このような難しい問題に容易に取り組むことができるということです。また、遺伝的最適化ツールは、開発者がアルゴリズムアプリケーションを構築する過程で直面する質問に答えるために利用されることのある、人気のあるChatGPTやその他のLLMよりもはるかに強力なツールである点も注目に値します。
以前、姉妹連載の記事「AIの限界を克服する」においても触れた通り、ドメインに特化したアルゴリズムは、汎用アルゴリズムよりも本質的に価値があります。ChatGPTやその他のLLMは汎用アルゴリズムである一方、MetaTrader 5に組み込まれた遺伝的最適化ツールはドメイン特化型のアルゴリズムであり、同じ質問をChatGPTに投げるよりもはるかに優れています。
さらに、MQL5クラウドを活用してバックテストおよび最適化プロセスを高速化する方法も示しました。本記事は、MQL5クラウドを使用しなければ、期日までに完成させることは困難だった可能性があります。使用開始も簡単で、料金も非常に手頃です。
実際、24時間体制で複数の冗長構成を備えたデータセンターを通じてクラウドコンピューティングが提供されます。ネットワーク障害による接続の切断が発生した場合でも、ほぼ常時接続が維持されます。総じて、MQL5クラウドと遺伝的最適化ツールは、現代のアルゴリズム開発者にとって欠かせないツールです。
この演習は、今後取引戦略の統計モデルを開発する際の意思決定プロセスを導く助けとなります。
続編の記事では、最初の2つの戦略だけで、戦略1が最も利益を上げる場合と戦略2が最も利益を上げる場合という二値分類タスクを開発するのに十分であることがわかるでしょう。その後、開発した統計モデリングアプリケーションのパフォーマンスを、これまで一緒に構築してきた初期戦略と比較して評価します。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/18770
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
初心者からエキスパートへ:MQL5を使ったアニメーションニュース見出し(V) - イベントリマインダーシステム
MQL5における特異スペクトル解析
プライスアクション分析ツールキットの開発(第31回):Python Candlestick Recognitionエンジン(I) - 手動検出
MQL5での取引戦略の自動化(第23回):トレーリングとバスケットロジックによるゾーンリカバリ
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索