English Deutsch
preview
MQL5での取引戦略の自動化(第4回):Multi-Level Zone Recoveryシステムの構築

MQL5での取引戦略の自動化(第4回):Multi-Level Zone Recoveryシステムの構築

MetaTrader 5トレーディング |
202 0
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回(第3回)では、RSIに基づくシグナル生成と、Zone Recoveryメカニズムを統合した「Zone Recovery RSIシステム」について解説し、MetaQuotes Language 5 (MQL5)を用いて取引を管理し、不利な市場の動きに対して回復する方法を示しました。本記事(第4回)では、これまでの基礎を踏まえ、複数の独立したシグナルを同時に処理できる高度な取引管理手法「Multi-Level Zone Recoveryシステム」を導入します。

このシステムは、RSIを用いて取引シグナルを生成し、それぞれのシグナルを動的に配列構造へ組み込み、Zone Recoveryロジックとシームレスに統合します。これにより、複数の取引セットアップを効率的に管理できるリカバリーメカニズムを拡張し、ドローダウンの抑制と取引成果の改善を目指します。

本記事では、以下のトピックに分けて、戦略の設計からMQL5での実装、バックテストまでを段階的に解説します。

  1. 戦略の設計図
  2. MQL5での実装
  3. バックテスト
  4. 結論

この記事を通じて、柔軟で堅牢な取引管理を実現する「Multi-Level Zone Recoveryシステム」の構築と最適化について、実践的に理解できるようになります。


戦略の設計図

Multi-Level Zone Recoveryシステムでは、複数の取引シグナルを効率的に管理するために、整理された構造を採用します。その実現には、個別の取引バスケットを生成するための「設計図」として機能する構造体(struct)を定義します。RSIインジケーターによって生成される各取引シグナルは、それぞれ固有のバスケットと対応し、そのバスケットは配列の要素として動的に格納されます。たとえば、システムが「シグナル1」を生成した場合、「バスケット1」が作成されます。このバスケット1は、最初の取引情報を記録するだけでなく、そのシグナルに関連するすべてのリカバリーポジションを一括して管理します。同様に、「シグナル2」が生成されると「バスケット2」が作成され、バスケット2はシグナル2のパラメータに基づいて、対応するすべてのリカバリートレードを独立して追跡・実行します。以下に、バスケットおよびシグナルのプロパティを可視化したものを示します。

注文バスケットの可視化

各バスケットには、シグナル方向(買いまたは売り)、エントリー価格、リカバリーレベル、動的に計算されたロットサイズ、その他の取引固有のパラメータなどの重要なデータが含まれます。RSIによって新しいシグナルが識別されると、それらを配列に追加して、システムが複数のシグナルを同時に処理できるようにします。リカバリートレードはそれぞれのバスケット内で動的に計算され、実行されるため、各セットアップは独立して管理され、他のセットアップからの干渉を受けません。以下は、シグナルが個別に処理される例です。

別々のシグナルの例

このようにシステムを構築することで、高い拡張性と柔軟性を実現できます。各バスケットは自己完結型のユニットとして機能し、システムはあらゆるシグナルに対して市場の状況に応じた動的な対応が可能になります。この設計により、各シグナルとそれに対応するリカバリートレードが整理された形で管理されるため、複雑な取引構成の追跡と管理が容易になります。配列ベースのバスケットシステムは、効率性と明瞭性を維持しながら、多様な取引シナリオに柔軟に対応できる、堅牢で適応性に優れたMulti-Level Zone Recoveryシステムを構築するための基盤となります。では、さっそく始めましょう。


MQL5での実装

Zone Recovery取引戦略の理論を理解したところで、次はこの理論を自動化し、MetaTrader 5用のMetaQuotes Language 5 (MQL5)を使って、エキスパートアドバイザー(EA)を作成していきましょう。

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

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

新しいEA名

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

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

//+------------------------------------------------------------------+
//|                           1. Zone Recovery RSI EA Multi-Zone.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

これにより、プログラムをロードするときにシステムメタデータが表示されます。次に、次のようにユーザーインターフェイスに表示される入力変数を追加します。

sinput group "General EA Settings"
input double inputlot = 0.01;
input double inputzonesizepts = 200;
input double inputzonetragetpts = 400;
input double inputlotmultiplier = 2.0;
input double inputTrailingStopPts = 50; // Trailing stop distance in points
input double inputMinimumProfitPts = 50; // Minimum profit points before trailing stop starts
input bool inputTrailingStopEnabled = true; // Enable or disable trailing stop

「General EA Settings」カテゴリでは、入力パラメータのグループを定義し、ユーザーがEAを実行する前に、必要な基本設定を構成できるようにします。これらの入力はMQL5のinputデータ型で宣言されており、コードを変更することなくEAの入力設定パネルから直接調整可能です。各入力パラメータは、EAの動作やリスク管理を制御するためにそれぞれ特有の役割を持っています。

「inputlot」パラメータは、取引開始時の初期ロットサイズを定義します。デフォルト値は0.01で、取引量を正確に制御できます。「inputzonesizepts」パラメータは、リカバリーゾーンの幅をポイント単位で指定し、デフォルトは200に設定されています。この値はリカバリートレード間の距離を決定します。「inputzonetargetpts」パラメータ(デフォルト値400)は、目標利益の距離をポイント単位で設定し、EAが利益確定のタイミングを判断する指標となります。

リカバリートレードの処理には、デフォルト値2.0の「inputlotmultiplier」パラメータを使用します。これにより、EAは乗数に基づいてリカバリートレードのロットサイズを動的に増加させて計算できます。さらに、トレーリングストップ機能は3つのパラメータで制御されます。まず「inputTrailingStopPts」はトレーリングストップの距離をポイント単位で定義し、デフォルトで50に設定されています。これにより、市場が取引に有利な方向に動いた際にストップロスが調整されます。また、「inputMinimumProfitPts」パラメータも50に設定されており、トレーリングストップは取引が一定の利益に達した後にのみ有効になります。

最後に、bool型の「inputTrailingStopEnabled」パラメータによって、ユーザーはトレーリングストップ機能の有効・無効を自由に切り替えられます。この柔軟性により、EAは様々な取引戦略やリスク許容度、市場状況に応じて最適に対応可能で、効率的な取引およびリスク管理のためのカスタマイズ可能なフレームワークを提供します。次に、取引を開始するために、#includeを使っていくつかの追加ファイルをインクルードします。  これにより、CTradeクラスにアクセスできるようになります。これを使用して、取引オブジェクトを作成します。これは取引を開始するために必要なので、非常に重要です。

#include <Trade/Trade.mqh>
//--- Includes the MQL5 Trade library for handling trading operations.

プリプロセッサは「#include<Trade/Trade.mqh>」という行をTrade.mqhというファイルの内容に置き換えます。角括弧は、Trade.mqhファイルが標準ディレクトリ(通常、terminal_installation_directory\MQL5\Include)から取得されることを示します。カレントディレクトリは検索に含まれません。この行はプログラム中のどこにでも配置できますが、通常は、より良いコード構造と参照を容易にするために、すべてのインクルージョンはソースコードの先頭に置かれます。以下はナビゲータセクションからの特定のファイルです。

CTRADEクラス

その後、取引システムで使用するいくつかの重要なグローバル変数を宣言する必要があります。

//--- Global variables for RSI logic
int rsiPeriod = 14;                //--- The period used for calculating the RSI indicator.
int rsiHandle;                     //--- Handle for the RSI indicator, used to retrieve RSI values.
double rsiBuffer[];                //--- Array to store the RSI values retrieved from the indicator.
datetime lastBarTime = 0;          //--- Holds the time of the last processed bar to prevent duplicate signals.

ここでは、EAの取引シグナルを生成するためのRSIインジケーターのロジックを管理するグローバル変数群を定義します。これらの変数は、RSIの計算・取得・処理を効率的におこなうよう設計されており、グローバル宣言することでEA全体でアクセス可能となり、一貫性のある効果的なシグナル生成を実現します。まず、RSIの計算に用いるルックバック期間を指定するために、int型変数「rsiPeriod」を14に設定します。この値は、EAがRSI値を算出する際に参照するバーの本数を表し、インジケーターの感度を調整する役割を持ちます。次に、int型変数「rsiHandle」を宣言します。これはiRSI関数を使ってRSIインジケーターを初期化した際に取得するハンドルを格納するためのもので、ターミナルのインジケーターバッファからRSIの値を直接取得する際に利用します。

RSI値を保存するために、double型の動的配列「rsiBuffer[]」を作成します。この配列には各バーごとに計算されたRSI値が格納され、市場の買われすぎ・売られすぎ状態を判定するために使用されます。さらに、datetime型の「lastBarTime」変数を定義し、最後に処理したバーの時間を保持します。この値を追跡することで、新しいバーが形成されたときにのみシグナル処理がおこなわれ、同一バーに対するシグナルの重複を防止します。以上を踏まえ、すべての生成されたシグナルに関連付ける共通の「バスケット」パラメータを構造体として定義します。構造体の基本的な構文は以下の通りです。

//--- Struct to track individual position recovery states
struct PositionRecovery {
        //--- Member 1
        //--- Member 2
        //--- Member 3

        //--- Method 1
        //...
};

関連するデータ変数をまとめて管理するために構造体を使います。以下はその一般的なプロトタイプです。「PositionRecovery」という名前の構造体を定義します。この構造体はEA内で各ポジションのリカバリー状態を整理・管理するための設計図の役割を果たします。この構造体はカスタムデータ型として機能し、関連する変数(メンバー)や関数(メソッド)をひとまとめに扱うことができます。

構文の説明

"struct" "PositionRecovery { ... };"

構造体「PositionRecovery」を宣言します。キーワードstructは構造体を定義するために使用され、中括弧{...}は構造体のメンバーとメソッドを囲みます。MQL5では定義の末尾のセミコロン(;)は必須です。

  • メンバー

メンバーとは、構造体内に定義された変数であり、PositionRecoveryの各インスタンス固有のデータを格納します。

//--- Member 1:取引の初期ロットサイズやエントリー価格などの変数のプレースホルダー。

//--- Member 2:リカバリーゾーンのサイズや現在の取引状態などのパラメータを表すことができます。

//--- Member 3:実行されたリカバリートレードの数やリカバリーの完了を示すフラグなどの追加データ。

これらのメンバーにより、個々のリカバリープロセスを追跡および管理するために必要なすべての情報をカプセル化できます。

  • メソッド

メソッドとは、構造体の内部で定義され、その構造体のメンバーを操作する関数です。

//--- Method 1:次のリカバリートレードのロットサイズを計算したり、リカバリーターゲットが達成されたかどうかを確認したりするメソッドのプレースホルダー。

データ(メンバー)とロジック(メソッド)を組み合わせることで、構造体はより汎用的かつ自己完結的になります。これを理解した上で、構造体のメンバー定義を始めましょう。

//--- Struct to track individual position recovery states
struct PositionRecovery {
   CTrade trade;                    //--- Object to handle trading operations.
   double initialLotSize;           //--- Initial lot size for this position.
   double currentLotSize;           //--- Current lot size in the recovery sequence.
   double zoneSize;                 //--- Distance in points defining the recovery zone size.
   double targetSize;               //--- Distance in points defining the profit target range.
   double multiplier;               //--- Lot size multiplier for recovery trades.
   string symbol;                   //--- Trading symbol.
   ENUM_ORDER_TYPE lastOrderType;   //--- Type of the last order (BUY or SELL).
   double lastOrderPrice;           //--- Price of the last executed order.
   double zoneHigh;                 //--- Upper boundary of the recovery zone.
   double zoneLow;                  //--- Lower boundary of the recovery zone.
   double zoneTargetHigh;           //--- Upper boundary of the target range.
   double zoneTargetLow;            //--- Lower boundary of the target range.
   bool isRecovery;                 //--- Whether the recovery is active.
   ulong tickets[];                 //--- Array to store tickets of positions associated with this recovery.
   double trailingStop;             //--- Trailing stop level
   double initialEntryPrice;        //--- Initial entry price for trailing stop calculation

   //---

};

ここでは、各ポジションのリカバリー状態を追跡するために必要なすべてのデータを整理・管理する構造体「PositionRecovery」を作成します。この構造体を用いることで、各リカバリープロセスを独立して管理でき、複数のシグナルを効果的に扱うことが可能になります。

まず、取引操作(注文の開始、変更、クローズなど)を実行するための「CTrade trade」オブジェクトを定義します。シーケンス内の最初の取引ロットサイズを保持する「initialLotSize」と、現在のリカバリープロセスでの最新取引ロットサイズを追跡する「currentLotSize」を用意します。リカバリー戦略の制御には、zoneSizeでリカバリーゾーンの距離(ポイント単位)を指定し、targetSizeで利益目標の範囲を定義します。

ロットサイズを動的に調整するためのmultiplierも含め、これにより後続のリカバリートレードのロットサイズを計算します。また、このリカバリーに対応する取引銘柄を特定する「symbol」を設定し、EAが正しい銘柄で取引をおこなうようにします。さらに、注文タイプ(買いまたは売り)を格納するENUM_ORDER_TYPE型の「lastOrderType」と、その注文価格を記録する「lastOrderPrice」を定義し、現在のリカバリーの状況を把握します。リカバリーゾーンの監視用に、上限と下限を示す「zoneHigh」「zoneLow」、利益目標範囲の上限と下限を示す「zoneTargetHigh」「zoneTargetLow」も用意します。

リカバリーがアクティブかどうかを判定するためのフラグ「isRecovery」は、必要に応じてtrueまたはfalseに設定します。リカバリーシーケンス内の全取引チケット番号を格納する配列「tickets[]」も含めており、個別の管理・追跡が可能です。最後に、トレーリングストップ距離を指定する「trailingStop」と、トレーリングストップ計算用に最初の取引のエントリー価格を記録する「initialEntryPrice」を設定し、リカバリー時の利益保護を動的におこないます。

メンバー変数の定義後は、生成される各インスタンス、つまり各バスケットごとにこれらを初期化する必要があります。これを効率よくおこなうための初期化メソッドを作成しましょう。

//--- Initialize position recovery
void Initialize(double lot, double zonePts, double targetPts, double lotMultiplier, string _symbol, ENUM_ORDER_TYPE type, double price) {
   initialLotSize = lot;             //--- Assign initial lot size.
   currentLotSize = lot;             //--- Set current lot size equal to initial lot size.
   zoneSize = zonePts * _Point;      //--- Calculate zone size in points.
   targetSize = targetPts * _Point;  //--- Calculate target size in points.
   multiplier = lotMultiplier;       //--- Assign lot size multiplier.
   symbol = _symbol;                 //--- Assign the trading symbol.
   lastOrderType = type;             //--- Set the type of the last order.
   lastOrderPrice = price;           //--- Record the price of the last executed order.
   isRecovery = false;               //--- Set recovery as inactive initially.
   ArrayResize(tickets, 0);          //--- Initialize the tickets array.
   trailingStop = 0;                 //--- Initialize trailing stop
   initialEntryPrice = price;        //--- Set initial entry price
   CalculateZones();                 //--- Calculate recovery and target zones.
}

ここでは、各ポジションリカバリーの状態を初期化し、必要なすべてのパラメータを設定するInitializeメソッドを定義します。このメソッドにより、各リカバリーインスタンスが正しく構成され、指定された入力値に基づいて動的に取引を管理する準備が整います。まず、リカバリーシーケンスにおける最初の取引サイズを指定するinitialLotSizeにlot値を代入し、同時にcurrentLotSizeも同じ値に設定します。最初の取引は初期ロットサイズと同一であるためです。続いて、「zonePts」と「targetPts」の入力値に「_Point」定数を掛けて、それぞれリカバリーゾーンの幅と利益目標の範囲をポイント単位で計算します。これにより、リカバリートレードの実行および目標達成に必要な距離の閾値が定義されます。

次に、lotMultiplierをmultiplier変数に設定し、以降のリカバリートレードにおけるロットサイズの増加率を決定します。取引銘柄が正確に指定されるよう「symbol」変数に銘柄を設定し、リカバリーインスタンスのすべての取引が正しい市場で実行されるようにします。また、直近の注文のタイプをlastOrderTypeに、実行価格をlastOrderPriceに設定することで、現在のリカバリープロセスの状態を追跡できるようにします。リカバリーが初期状態ではアクティブでないことを示すため、isRecoveryはfalseに初期化します。

ArrayResize関数を使ってtickets配列のサイズを0に設定し、既存のデータをクリアして、新たにこのリカバリーインスタンスに関連する取引のチケット番号を格納する準備をおこないます。さらに、柔軟性を確保するため、「trailingStop」を0に初期化し、「initialEntryPrice」には「price」を設定します。これにより、トレーリングストップの計算に必要な基準値が用意されます。最後に、CalculateZonesメソッドを呼び出し、リカバリーゾーンと利益目標範囲の上限および下限を計算します。この処理により、EAが取引を効率的に管理するために必要なすべての情報が揃います。このようにInitializeメソッドを用いることで、各リカバリープロセスに対して明確かつ完全な初期状態が確立され、効果的なトレード管理のための各種パラメータが正確に設定されます。次に、リカバリー範囲のレベルを計算するCalculateZones関数の定義へと進みます。

//--- Calculate dynamic zones and targets
void CalculateZones() {
   if (lastOrderType == ORDER_TYPE_BUY) { //--- If the last order was a BUY...
      zoneHigh = lastOrderPrice;         //--- Set upper boundary at the last order price.
      zoneLow = zoneHigh - zoneSize;     //--- Set lower boundary below the last order price.
      zoneTargetHigh = zoneHigh + targetSize; //--- Define target range above recovery zone.
      zoneTargetLow = zoneLow - targetSize;   //--- Define target range below recovery zone.
   } else if (lastOrderType == ORDER_TYPE_SELL) { //--- If the last order was a SELL...
      zoneLow = lastOrderPrice;                //--- Set lower boundary at the last order price.
      zoneHigh = zoneLow + zoneSize;           //--- Set upper boundary above the last order price.
      zoneTargetLow = zoneLow - targetSize;    //--- Define target range below recovery zone.
      zoneTargetHigh = zoneHigh + targetSize;  //--- Define target range above recovery zone.
   }
}

ここでは、CalculateZonesメソッドを定義し、最後に実行された注文の種類(買いまたは売り)とその価格に基づいて、リカバリーゾーンの上下限および利益目標の範囲を動的に算出します。このメソッドにより、各リカバリープロセスにおける重要な価格レベルが明確に定義され、システムは市場の変動に応じて適切な取引判断を下せるようになります。

まず、lastOrderTypeの値をチェックして、直近の注文が買いか売りかを判定します。lastOrderTypeがORDER_TYPE_BUYの場合、zoneHighにlastOrderPriceを割り当て、リカバリーゾーンの上限を買い注文のエントリー価格に設定します。次に、「zoneLow」は「zoneHigh」からzoneSize(ポイント単位に変換された値)を減算して計算し、リカバリーゾーンの下限を定めます。さらに、利益目標範囲を定義します。zoneTargetHighはzoneHighにtargetSizeを加算することで算出し、zoneTargetLowはzoneLowからtargetSizeを減算することで求めます。これにより、リカバリーゾーンと利益目標範囲が買い注文の価格を基準として適切に構成されます。

一方、lastOrderTypeがORDER_TYPE_SELLの場合、ロジックは逆になります。この場合は、zoneLowにlastOrderPriceを割り当て、リカバリーゾーンの下限を売り注文のエントリー価格に設定します。そして、「zoneHigh」は「zoneLow」にzoneSizeを加算して計算し、ゾーンの上限とします。利益目標範囲の設定では、zoneTargetLowはzoneLowからtargetSizeを減算して求め、zoneTargetHighはzoneHighにtargetSizeを加算して算出します。これらの値により、売り注文価格を基準としたリカバリーゾーンと利益目標範囲が正しく定義されます。これらのレベルの定義は、以下に示すようなイメージを表します。

システムレベルの可視化

ゾーンレベルを定義したら、ポジションを開く処理に進むことができます。この処理は、コード構造内で再利用しやすくするために、メソッドとしてカプセル化します。

//--- Open a trade with comments for position type
bool OpenTrade(ENUM_ORDER_TYPE type, string comment) {
   if (type == ORDER_TYPE_BUY) { //--- For a BUY order...
      if (trade.Buy(currentLotSize, symbol, 0, 0, 0, comment)) { //--- Attempt to place a BUY trade.
         lastOrderType = ORDER_TYPE_BUY;                        //--- Update the last order type.
         lastOrderPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Record the current price.
         ArrayResize(tickets, ArraySize(tickets) + 1);          //--- Resize the tickets array.
         tickets[ArraySize(tickets) - 1] = trade.ResultOrder(); //--- Store the new ticket.
         CalculateZones();                                      //--- Recalculate zones.
         isRecovery = false;                                    //--- Ensure recovery is inactive for initial trade.
         Print("Opened BUY Position, Ticket: ", tickets[ArraySize(tickets) - 1]);
         return true;                                           //--- Return success.
      }
   } else if (type == ORDER_TYPE_SELL) { //--- For a SELL order...
      if (trade.Sell(currentLotSize, symbol, 0, 0, 0, comment)) { //--- Attempt to place a SELL trade.
         lastOrderType = ORDER_TYPE_SELL;                        //--- Update the last order type.
         lastOrderPrice = SymbolInfoDouble(symbol, SYMBOL_BID);  //--- Record the current price.
         ArrayResize(tickets, ArraySize(tickets) + 1);           //--- Resize the tickets array.
         tickets[ArraySize(tickets) - 1] = trade.ResultOrder();  //--- Store the new ticket.
         CalculateZones();                                       //--- Recalculate zones.
         isRecovery = false;                                     //--- Ensure recovery is inactive for initial trade.
         Print("Opened SELL Position, Ticket: ", tickets[ArraySize(tickets) - 1]);
         return true;                                            //--- Return success.
      }
   }
   return false; //--- If the trade was not placed, return false.
}

このブール型のOpenTrade関数では、指定されたタイプ(買いまたは売り)の新しい取引を開始するロジックを処理し、回復システムに必要な更新を管理します。この関数は、取引が正しく実行され、関連するすべてのデータが更新されて、リカバリープロセスと同期が保たれるようにします。typeパラメータがORDER_TYPE_BUYの場合、trade.Buyメソッドを使用して買い取引を試みます。このメソッドはcurrentLotSize、symbol、commentパラメータを使用して取引を実行し、ストップロスおよびテイクプロフィットのレベルはゼロ(未指定)のままにします。これらのレベルはゾーンのターゲット範囲に応じて動的に設定されるためです。買い取引が正常に実行された場合、lastOrderTypeはORDER_TYPE_BUYに更新され、最後の取引価格であるlastOrderPriceは、SymbolInfoDouble関数にSYMBOL_BIDパラメータを指定して取得した現在の市場価格に設定されます。

続いて、ArrayResize関数を使用してtickets配列のサイズを変更し、新しい取引を格納するためのスペースを確保します。成功した取引のチケット番号は、trade.ResultOrder()を使用して保存されます。これにより、このリカバリーインスタンスに関連するすべての取引が効率的に追跡・管理されます。次にCalculateZones関数を呼び出して、最新の取引に基づいてリカバリーゾーンおよびターゲットゾーンを再計算します。そして、これは最初の取引でありリカバリープロセスの一部ではないため、isRecoveryはfalseに設定されます。成功のメッセージがログに出力され、関数はtrueを返して、取引が正常に開始されたことを示します。

typeパラメータがORDER_TYPE_SELLの場合も、同様のロジックに従います。trade.Sellメソッドを呼び出して、指定されたパラメータで売り取引を実行します。成功した場合、lastOrderTypeはORDER_TYPE_SELLに設定され、lastOrderPriceには現在の市場価格が記録されます。買い注文同様、tickets配列をリサイズして新しいチケットを保存し、CalculateZonesでゾーンを再計算します。isRecoveryはfalseに設定され、成功メッセージが表示された後、関数はtrueを返します。

どちらの注文タイプにおいても、取引に失敗した場合は関数がfalseを返し、操作が失敗したことを示します。この構造により、取引は体系的に管理され、リカバリー関連のすべてのデータが正しく更新されるため、取引管理がシームレスにおこなえます。ポジションが開かれ、ゾーンのレベルが計算された後は、ティックごとにこれらのゾーンを監視し、いずれかのレベルに達したときにリカバリーポジションを開くことが可能です。

//--- Manage zone recovery
void ManageZones() {
   double currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Get the current price.
   if (lastOrderType == ORDER_TYPE_BUY && currentPrice <= zoneLow) { //--- If price drops below the recovery zone for a BUY...
      double previousLotSize = currentLotSize;                       //--- Store the current lot size temporarily.
      currentLotSize *= multiplier;                                 //--- Tentatively increase lot size.
      if (OpenTrade(ORDER_TYPE_SELL, "Recovery Position")) {        //--- Attempt to open a SELL recovery trade.
         isRecovery = true;                                         //--- Mark recovery as active if trade is successful.
      } else {
         currentLotSize = previousLotSize;                          //--- Revert the lot size if the trade fails.
      }
   } else if (lastOrderType == ORDER_TYPE_SELL && currentPrice >= zoneHigh) { //--- If price rises above the recovery zone for a SELL...
      double previousLotSize = currentLotSize;                       //--- Store the current lot size temporarily.
      currentLotSize *= multiplier;                                 //--- Tentatively increase lot size.
      if (OpenTrade(ORDER_TYPE_BUY, "Recovery Position")) {         //--- Attempt to open a BUY recovery trade.
         isRecovery = true;                                         //--- Mark recovery as active if trade is successful.
      } else {
         currentLotSize = previousLotSize;                          //--- Revert the lot size if the trade fails.
      }
   }
}

リカバリーゾーンの市場価格を監視し、価格が初期取引とは逆方向に動いた場合に対応するために、ManageZones関数を定義します。まず、SymbolInfoDouble関数を使用して現在の市場価格を取得し、最新のbid価格を取得します。その後、現在の価格がリカバリーゾーンの境界を外れたかどうかを確認します。買い注文の場合、ゾーンの下限であるzoneLow、売り注文の場合は上限であるzoneHighによってリカバリーゾーンが定義されます。

最後の注文が買い(lastOrderType == ORDER_TYPE_BUYで示される)であり、現在の価格がzoneLowを下回った場合、リカバリートレードのロットサイズを増やします。現在のロットサイズをpreviousLotSizeに保存し、currentLotSizeにmultiplierを掛けて増加させます。その後、OpenTrade関数を使用して売りのリカバリートレードを試みます。リカバリートレードが正常に実行された場合、isRecoveryをtrueに設定してリカバリーが有効であることを示します。取引が失敗した場合は、ロットサイズをpreviousLotSizeに保存されていた元の値に戻します。

同様に、最後の注文が売り(lastOrderType == ORDER_TYPE_SELL)で、価格がzoneHighを上回った場合にも同じロジックを適用して、買いのリカバリートレードを開始します。ロットサイズを増加させたうえで、買いエントリーを試みます。成功すればisRecoveryはtrueに設定されますが、失敗した場合はロットサイズを元に戻します。このようにして、システムはリカバリートレードを効果的に管理し、ポジションサイズを調整しながら、市場の動きに応じて適切な対処をおこないます。最後に、価格があらかじめ定められたターゲットレベルに到達した際にポジションをクローズする必要があるため、その処理を行う専用の関数が別途必要となります。

//--- Check and close trades at targets
void CheckCloseAtTargets() {
   double currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Get the current price.
   if (lastOrderType == ORDER_TYPE_BUY && currentPrice >= zoneTargetHigh) { //--- If price reaches the target for a BUY...
      ClosePositionsAtTarget();                               //--- Close positions that meet the target criteria.
   } else if (lastOrderType == ORDER_TYPE_SELL && currentPrice <= zoneTargetLow) { //--- If price reaches the target for a SELL...
      ClosePositionsAtTarget();                               //--- Close positions that meet the target criteria.
   }
}

ここでは、市場価格があらかじめ定義された目標レベルに到達したかどうかを確認し、該当するポジションをクローズするためのCheckCloseAtTargets関数(void型)を定義します。まず、SymbolInfoDouble (symbol, SYMBOL_BID)を使用して、現在の市場のBID価格を取得します。次に、この価格を、買い注文の場合はzoneTargetHigh、売り注文の場合はzoneTargetLowと比較します。

最後の取引が買い(lastOrderType == ORDER_TYPE_BUYで示される)であり、現在の価格がzoneTargetHigh以上に上昇した場合、ポジションは設定された利益目標に到達したと判断されます。この場合、ClosePositionsAtTarget関数を呼び出して、対象のポジションをすべてクローズします。同様に、最後の注文が売り(lastOrderType == ORDER_TYPE_SELL)であり、価格がzoneTargetLow以下に下落した場合にも、ClosePositionsAtTargetを呼び出してポジションをクローズします。このロジックにより、市場が定められた利益目標に達したときに取引が自動的に終了し、利益が確定され、リカバリープロセスが完了します。

なお、ポジションのクローズ処理は再利用可能なClosePositionsAtTarget関数を使用して実装されています。以下にその関数スニペットを示します。

//--- Close positions that have reached the target
void ClosePositionsAtTarget() {
   for (int i = ArraySize(tickets) - 1; i >= 0; i--) {              //--- Iterate through all tickets.
      ulong ticket = tickets[i];                                    //--- Get the position ticket.
      int retries = 10;                                             //--- Set retry count.
      while (retries > 0) {                                         //--- Retry until successful or retries exhausted.
         if (trade.PositionClose(ticket)) {                         //--- Attempt to close the position.
            Print("CLOSED # ", ticket, " Trailed and closed: ", (trailingStop != 0));
            ArrayRemove(tickets, i);                                //--- Remove the ticket from the array on success.
            retries = 0;                                            //--- Exit the loop on success.
         } else {
            retries--;                                              //--- Decrement retries on failure.
            Sleep(100);                                             //--- Wait before retrying.
         }
      }
   }
   if (ArraySize(tickets) == 0) {                                   //--- If all tickets are closed...
      Reset();                                                      //--- Reset recovery state after closing the target positions.
   }
}

ClosePositionsAtTarget関数では、tickets配列に保存されているすべてのオープンポジションを反復処理し、目標レベルに達したポジションをクローズしようとします。まず、tickets配列を逆順にループして、決済後にポジションを削除する際にポジションをスキップしないようにします。各チケットには、最初の試行でポジションのクローズに失敗した場合、システムが再試行するようにretriesを設定します。

各ポジションについて、trade.PositionClose(ticket)関数を使用してポジションをクローズしようとします。ポジションが正常にクローズされた場合、チケットがクローズされたことと、トレーリングストップが適用されたかどうかを確認するために「trailingStop != 0」を使用して、トレーリングストップであったかどうかを示すメッセージを出力します。ポジションがクローズされたら、ArrayRemove関数を使用してtickets配列からチケットを削除し、retriesを0に設定して再試行ループを終了します。ポジションのクローズに失敗した場合は、retriesカウンタを減算し、Sleep関数を使用してしばらく待機してから、関数に過負荷がかからないようにしながら、再度ポジションのクローズを試行します。

すべてのポジションをクローズしようとした後、ArraySize関数を使用してtickets配列が空かどうかを確認します。すべてのポジションがクローズされた場合、Reset関数を呼び出してリカバリー状態をリセットし、残っているリカバリー関連データをすべてクリアして、将来の取引の準備をします。それだけのことです。しかし、市場が目標レベルに達するかどうかはほぼ確実ではないため、レベルに達するまで待つのではなく、最小利益に達したポジションを追跡することでシステムを改善できます。このロジックはメソッド内に再度存在します。

//--- Apply trailing stop logic to initial positions
void ApplyTrailingStop() {
   if (inputTrailingStopEnabled && ArraySize(tickets) == 1) { // Ensure trailing stop is enabled and there is only one position (initial position)
      ulong ticket = tickets[0]; // Get the ticket of the initial position
      double entryPrice = GetPositionEntryPrice(ticket); // Get the entry price of the position by ticket
      double currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); // Get the current price
      double newTrailingStop;

      if (lastOrderType == ORDER_TYPE_BUY) {
         if (currentPrice > entryPrice + (inputMinimumProfitPts + inputTrailingStopPts) * _Point) {
            newTrailingStop = currentPrice - inputTrailingStopPts * _Point; // Calculate new trailing stop for BUY
            if (newTrailingStop > trailingStop) {
               trailingStop = newTrailingStop; // Update trailing stop if the new one is higher
               Print("Trailing BUY Position, Ticket: ", ticket, " New Trailing Stop: ", trailingStop);
            }
         }

         if (trailingStop != 0 && currentPrice <= trailingStop) {
            Print("Trailed and closing BUY Position, Ticket: ", ticket);
            ClosePositionsAtTarget(); // Close position if the price falls below the trailing stop
         }
      } else if (lastOrderType == ORDER_TYPE_SELL) {
         if (currentPrice < entryPrice - (inputMinimumProfitPts + inputTrailingStopPts) * _Point) {
            newTrailingStop = currentPrice + inputTrailingStopPts * _Point; // Calculate new trailing stop for SELL
            if (newTrailingStop < trailingStop) {
               trailingStop = newTrailingStop; // Update trailing stop if the new one is lower
               Print("Trailing SELL Position, Ticket: ", ticket, " New Trailing Stop: ", trailingStop);
            }
         }

         if (trailingStop != 0 && currentPrice >= trailingStop) {
            Print("Trailed and closing SELL Position, Ticket: ", ticket);
            ClosePositionsAtTarget(); // Close position if the price rises above the trailing stop
         }
      }
   }
}

定義されたApplyTrailingStopメソッド(void型)では、トレーリングストップが有効かどうか、そしてアクティブなポジションが1つだけであるかどうかに基づいて、初期ポジションに対するトレーリングストップのロジックを実装します。まず、inputTrailingStopEnabledを用いてトレーリングストップ機能が有効であること、そして「ArraySize(tickets) == 1」によってポジションが1件のみであることを確認します。次に、初期ポジションのチケットを取得し、それを使用してGetPositionEntryPrice関数からエントリー価格を取得します。また、SymbolInfoDouble関数を使用して現在の市場価格も取得します。

買いポジションの場合は、現在の価格がエントリー価格より一定以上(inputMinimumProfitPts + inputTrailingStopPtsで定義された最小利益とトレーリングストップ距離の合計)上昇しているかを確認し、それに応じて新しいトレーリングストップを設定します。計算されたトレーリングストップが現在のtrailingStopより高ければ、値を更新し、新しいトレーリングストップレベルをログに出力します。現在価格がトレーリングストップの水準まで下落した場合は、ClosePositionsAtTarget関数を呼び出してポジションをクローズします。

売りポジションの場合も同様ですが、処理は逆になります。現在の価格がエントリー価格を一定以上下回っているかを確認し、必要に応じてトレーリングストップを引き下げます。新しく計算されたトレーリングストップが現在のtrailingStopより低ければ、その値に更新し、更新内容をログに出力します。価格がトレーリングストップの水準まで上昇した場合は、ポジションをクローズします。この関数は、トレーリングストップを市場の動きに応じて動的に適用し、ポジションを大きな損失から守りつつ、利益を確保するためのものです。価格が有利に動けばトレーリングストップが調整され、価格が反転してその水準に達した場合はポジションがクローズされます。

なお、エントリー価格の取得にはカスタム関数を使用しています。以下にそのロジックを示します。

//--- Get the entry price of a position by ticket
double GetPositionEntryPrice(ulong ticket) {
   if (PositionSelectByTicket(ticket)) {
      return PositionGetDouble(POSITION_PRICE_OPEN);
   } else {
      Print("Failed to select position by ticket: ", ticket);
      return 0.0;
   }
}

ここではGetPositionEntryPrice関数を定義し、指定されたチケット番号を使用してポジションのエントリー価格を取得します。まず、PositionSelectByTicket関数を用いて、指定されたチケットに対応するポジションを選択しようとします。ポジションの選択に成功した場合は、PositionGetDouble(POSITION_PRICE_OPEN)を呼び出して、そのポジションがオープンされた際の価格(エントリー価格)を取得します。一方、ポジションの選択に失敗した場合(たとえば、チケットが無効であるか、すでにポジションが存在しない場合など)は、失敗を示すエラーメッセージを出力し、エントリー価格が取得できなかったことを示す値として0.0を返します。

ポジションを開いて閉じた後、クリーンアップ方法としてシステムをリセットし、関連する取引バスケットを削除する必要があります。ここでは、Reset関数でクリーンアップロジックを処理する方法を説明します。

//--- Reset recovery state
void Reset() {
   currentLotSize = inputlot; //--- Reset lot size to initial value.
   lastOrderType = -1;              //--- Clear the last order type.
   lastOrderPrice = 0.0;            //--- Reset the last order price.
   isRecovery = false;              //--- Mark recovery as inactive.
   ArrayResize(tickets, 0);         //--- Clear the tickets array.
   trailingStop = 0;                //--- Reset trailing stop
   initialEntryPrice = 0.0;         //--- Reset initial entry price
   Print("Strategy BASKET reset after closing trades.");
}

Reset関数では、新しい取引サイクルに備えてリカバリー状態をリセットします。まず、ロットサイズをユーザー定義の初期値であるinputlotに戻すことで、currentLotSizeを初期状態にリセットします。次に、lastOrderTypeを-1(アクティブな注文が存在しないことを示す)に設定し、lastOrderPriceを0.0にリセットすることで、前回の注文情報をクリアします。

続いて、isRecoveryをfalseに設定してリカバリーモードを無効化し、リセット中にリカバリーロジックが適用されないようにします。また、arrayresize関数を使用してtickets配列をクリアし、前回のリカバリーで使用されたすべてのポジションチケットを削除します。さらに、trailingStopを0に、initialEntryPriceを0.0にリセットして、過去の取引に関するトレーリングストップおよびエントリー価格の情報も消去します。最後に、「Strategy BASKET reset after closing trades」というメッセージを出力して、トレード終了後の戦略のリセットとリカバリー状態の初期化が完了したことを通知します。この関数により、システムはクリーンな状態となり、次の取引サイクルへの準備が整います。

構造体のプロパティを定義したことで、次にシグナルを生成し、それらを定義済みの構造体に追加できるようになります。ただし、多くの動的シグナルを管理する必要があるため、生成されるシグナルごとにサブバスケットを定義するバスケット全体とただし、多数の動的なシグナルを管理する必要があるため、各シグナルごとにサブバスケットを定義し、それらをまとめて扱える「バスケット」として機能する配列構造を定義する必要があります。以下がその実装例です。

//--- Dynamic list to track multiple positions
PositionRecovery recoveryArray[]; //--- Dynamic array for recovery instances.

ここでは、複数のリカバリーポジションを追跡・管理するための動的配列recoveryArrayを宣言します。この配列はPositionRecovery構造体をベースとしており、複数のトレードに対する個別のリカバリー状態をそれぞれ独立して保持できます。配列の各要素は、ロットサイズ、ゾーンの境界、関連するポジションチケットなどの情報を含んだ個別のリカバリーセットアップを表します。

この配列は動的であるため、ArrayResizeなどの関数を使用して実行時にサイズの拡張・縮小が可能です。これにより、新たな取引シグナルに対して新しいリカバリーインスタンスを動的に追加したり、完了済みのリカバリーを削除したりすることができ、効率的なメモリ管理と柔軟な取引対応が可能になります。このように、各取引のリカバリーロジックが自身の「バスケット」内で独立して機能できるようになるため、複数ポジションを同時に管理する上で、このアプローチは極めて重要です。

配列を定義した後は、シグナル生成ロジックを開始できます。プログラムが初期化されるたびに呼び出されるOnInitイベントハンドラ内で、インジケーターハンドルを初期化する必要があります。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   rsiHandle = iRSI(_Symbol, PERIOD_CURRENT, rsiPeriod, PRICE_CLOSE); //--- Create RSI indicator handle.
   if (rsiHandle == INVALID_HANDLE) {                                 //--- Check if handle creation failed.
      Print("Failed to create RSI handle. Error: ", GetLastError());  //--- Print error message.
      return(INIT_FAILED);                                            //--- Return initialization failure.
   }
   ArraySetAsSeries(rsiBuffer, true); //--- Set RSI buffer as a time series.
   Print("Multi-Zone Recovery Strategy initialized."); //--- Log initialization success.
   return(INIT_SUCCEEDED); //--- Return initialization success.
}

OnInitイベントハンドラ関数では、EAが機能するために必要な重要なコンポーネントを初期化します。まず、現在の銘柄と時間枠に対してRSIを計算するiRSI関数を使用して、RSIインジケーターハンドルを作成します。このハンドルにより、EAはRSIの値に動的にアクセスできるようになります。ハンドルの作成に失敗した場合、INVALID_HANDLEが返されます。この場合、Print関数を使用して詳細なエラーメッセージを記録し、INIT_FAILEDを返して初期化プロセスを終了します。次に、ArraySetAsSeriesを使用してrsiBuffer配列を時系列として構成し、データが新しい順に整理されて正確に処理されるようにします。初期化が成功したら、戦略の準備が整ったことを示す確認メッセージを出力し、EAの動作準備が完了したことを示すためにINIT_SUCCEEDEDを返します。そして、OnDeinitイベントハンドラでは、リソースを節約するためにこのハンドルを破棄します。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   if (rsiHandle != INVALID_HANDLE)             //--- Check if RSI handle is valid.
      IndicatorRelease(rsiHandle);              //--- Release the RSI handle.
   Print("Multi-Zone Recovery Strategy deinitialized."); //--- Log deinitialization.
}

ここでは、EAが削除または非アクティブ化されたときに、EAのクリーンアップとリソース管理を処理します。まず、RSIインジケーターの「rsiHandle」が「INVALID_HANDLE」と等しくないことを確認して、有効かどうかを確認します。ハンドルが有効な場合は、IndicatorRelease関数を使用してハンドルを解放し、リソースを解放してメモリリークを回避します。最後に、Printを使用して、Multi-Zone Recovery戦略が正常に初期化解除されたことを示すメッセージをログに記録します。この機能により、リソースやプロセスが残らず、プログラムがクリーンかつ秩序正しくシャットダウンされます。

その後、OnTickイベントハンドラに進むことができます。このハンドラは、先ほど定義した構造を利用して、メインシステムのすべてのロジックを処理します。まず、インジケーターのデータを取得して、さらに分析できるようにする必要があります。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   if (CopyBuffer(rsiHandle, 0, 1, 2, rsiBuffer) <= 0) { //--- Copy the RSI buffer values.
      Print("Failed to copy RSI buffer. Error: ", GetLastError()); //--- Print error on failure.
      return;                                                     //--- Exit on failure.
   }

   //---

}

ここで、OnTick関数では、取引銘柄の価格更新を表す新しいティックごとに実行されるロジックを処理します。最初のステップでは、CopyBuffer関数を使用して、RSIインジケーターの値をrsiBuffer配列にコピーします。RSIインジケーターを識別するためにrsiHandleを指定し、バッファインデックスを0に設定し、最新のバーから始まる2つの値を要求します。操作が失敗した場合(つまり、返された値が0以下の場合)、Printを使用してエラーメッセージを出力し、ユーザーに問題を通知し、GetLastError関数から取得したエラーの詳細を含めます。エラーをログに記録した後、すぐにreturnを使用して関数を終了します。これにより、RSIデータの取得が失敗した場合にロジックの残りの部分が実行されず、EAの整合性と安定性が維持されます。

データを正常に取得できたら、それを次のようにシグナル生成ロジックに使用できます。

datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0); //--- Get the time of the current bar.
if (currentBarTime != lastBarTime) {                         //--- Check if a new bar has formed.
   lastBarTime = currentBarTime;                             //--- Update the last processed bar time.
   if (rsiBuffer[1] > 30 && rsiBuffer[0] <= 30) {            //--- Check for oversold RSI crossing up.
      Print("BUY SIGNAL");
      PositionRecovery newRecovery;                          //--- Create a new recovery instance.
      newRecovery.Initialize(inputlot, inputzonesizepts, inputzonetragetpts, inputlotmultiplier, _Symbol, ORDER_TYPE_BUY, SymbolInfoDouble(_Symbol, SYMBOL_BID)); //--- Initialize the recovery.
      newRecovery.OpenTrade(ORDER_TYPE_BUY, "Initial Position"); //--- Open an initial BUY position.
      ArrayResize(recoveryArray, ArraySize(recoveryArray) + 1); //--- Resize the recovery array.
      recoveryArray[ArraySize(recoveryArray) - 1] = newRecovery; //--- Add the new recovery to the array.
   } else if (rsiBuffer[1] < 70 && rsiBuffer[0] >= 70) {      //--- Check for overbought RSI crossing down.
      Print("SELL SIGNAL");
      PositionRecovery newRecovery;                          //--- Create a new recovery instance.
      newRecovery.Initialize(inputlot, inputzonesizepts, inputzonetragetpts, inputlotmultiplier, _Symbol, ORDER_TYPE_SELL, SymbolInfoDouble(_Symbol, SYMBOL_BID)); //--- Initialize the recovery.
      newRecovery.OpenTrade(ORDER_TYPE_SELL, "Initial Position"); //--- Open an initial SELL position.
      ArrayResize(recoveryArray, ArraySize(recoveryArray) + 1); //--- Resize the recovery array.
      recoveryArray[ArraySize(recoveryArray) - 1] = newRecovery; //--- Add the new recovery to the array.
   }
}

ここでは、新しいバーを検出し、インジケーターが特定のレベルを超えたことに基づいて取引シグナルを生成することに焦点を当てます。まず、「currentBarTime」変数に保存されているiTime関数を使用して現在のバーの時間を取得します。次に、「currentBarTime」と「lastBarTime」を比較して、新しいバーが形成されたかどうかを確認します。2つの値が異なる場合は、新しいバーが形成されたことを示しているので、同じバーが複数回処理されるのを防ぐために、lastBarTimeをcurrentBarTimeの値に更新します。

次に、RSIベースのシグナルの条件を評価します。rsiBuffer[1](前のバー)のRSI値が30より大きく、rsiBuffer[0](現在のバー)の現在の値が30以下である場合、上向きのクロスを伴う売られ過ぎ状態を示します。この場合、「BUY SIGNAL」メッセージを出力し、新しいPositionRecoveryインスタンス「newRecovery」を開始します。次に、「newrecovery」のinitializeメソッドを呼び出して、「inputlot」、「inputzonesizepts」、「inputzonetragetpts」、「inputlotmultiplier」、銘柄、注文タイプ(order_type_buy)、およびsymbolinfodouble関数からの現在のbid価格などのリカバリーパラメータを設定します。初期化に続いて、OpenTradeメソッドを使用して、最初の買いポジションを開き、「ORDER_TYPE_BUY」と説明的なコメントを渡します。

同様に、rsiBuffer[1]のRSI値が70未満で、rsiBuffer[0]の現在の値が70以上の場合は、下向きにクロスする買われすぎの状態を示します。このシナリオでは、「SELL SIGNAL」メッセージを出力し、新しいPositionRecoveryインスタンスを作成します。同じパラメータで初期化し、注文タイプをORDER_TYPE_SELLに設定した後、OpenTradeメソッドを使用して初期の売りポジションを開きます。

最後に、買いシグナルと売りシグナルの両方について、初期化されたPositionRecoveryインスタンスをrecoveryArrayに追加します。配列はarrayresize関数を使用してサイズ変更され、新しいインスタンスが配列の最後の位置に割り当てられ、各リカバリーが個別に追跡されるようになります。このロジックは、初期ポジションと条件を使用してポジションバスケットを初期化する役割を担います。ポジション管理を処理するには、メインバスケット内のバスケットをループし、ティックごとにメイン構造と同じように管理ロジックを適用する必要があります。以下がそのロジックです。

for (int i = 0; i < ArraySize(recoveryArray); i++) { //--- Iterate through all recovery instances.
   recoveryArray[i].ManageZones();                 //--- Manage zones for each recovery instance.
   recoveryArray[i].CheckCloseAtTargets();         //--- Check and close positions at targets.
   recoveryArray[i].ApplyTrailingStop();           //--- Apply trailing stop logic to initial positions.
}

位置管理を独立して処理するために、forループを使用して、recoveryarrayに保存されているすべてのリカバリーインスタンスを反復処理します。このループにより、各リカバリーインスタンスが個別に管理され、システムが複数のリカバリーシナリオを独立して制御できるようになります。ループはインデックス「i」が0に設定された状態で開始され、ArraySize関数によって決定されたrecoveryArray内のすべての要素が処理されるまで続行されます。

ループ内では、各リカバリーインスタンスで3つの重要なメソッドが呼び出されます。まず、ドット演算子を使用してmanagezonesメソッドが呼び出され、定義されたリカバリーゾーンに対する価格の動きを監視します。価格がゾーンの境界を抜けると、このメソッドは指定された乗数に応じてロットサイズを動的に調整し、リカバリーポジションを開こうとするアクションを実行します。

次に、CheckCloseAtTargetsメソッドが実行され、価格がリカバリーインスタンスのターゲットレベルに達したかどうかが評価されます。目標条件が満たされた場合、このメソッドは関連するすべてのポジションをクローズし、リカバリーインスタンスをリセットして、利益が確保され、インスタンスが新しいサイクルの準備が整っていることを確認します。

最後に、ApplyTrailingStopメソッドが適用され、リカバリーインスタンスの初期位置にトレーリングストップロジックが強制されます。この方法では、価格が有利に動くとトレーリングストップレベルが動的に調整され、利益が確定します。価格が反転してトレーリングストップに達した場合、この方法によりポジションが確実にクローズされ、潜在的な損失から保護されます。

このように各リカバリーインスタンスを処理することにより、システムは複数の独立したポジションを効果的に管理し、すべてのリカバリーシナリオが動的に、事前定義された戦略に沿って処理されることを保証します。プログラムが正しく動作していることを確認するためにプログラムを実行すると、次のような結果が得られます。

戦略リセットの例

画像から、システムの主な目的の1つであるリカバリーインスタンスを閉じた後に戦略がリセットされることがわかります。ただし、クロージャは他の実行中のインスタンスに干渉しないため、インスタンスは配列内の他のインスタンスとは独立して処理されます。これを確認するために、取引タブに移動すると、アクティブなインスタンスがあることがわかります。

トレードインスタンス

画像から、まだリカバリーインスタンスが存在し、そのうち2つがすでにリカバリーモードになっていることがわかります。これらは、右側のセクションに追加されたコメントによって完全に区別され、初期位置かリカバリー位置かが示されます。これにより、目的が正常に達成されたことが証明され、残っているのはプログラムをバックテストしてそのパフォーマンスを分析することです。これについては次のセクションで説明します。


バックテスト

プログラムのパフォーマンスおよび堅牢性を評価するためには、まず過去の市場環境をシミュレートする必要があります。これにより、プログラムがリカバリーシナリオにどの程度対応できるか、価格変動にどのように適応するか、そして取引管理をどれだけ適切におこなえるかを判断できます。バックテストは、戦略の収益性、ドローダウンのレベル、リスク管理の有効性など、重要な指標を提供します。プログラムは、過去の価格データをティック単位で処理し、実際の市場環境を再現します。そして、ユーザーの好みに合わせたしきい値(例:30で売られ過ぎ、70で買われ過ぎ)に基づいて売買シグナルを生成します。シグナルが発生すると、EAは新たなリカバリーインスタンスを初期化し、最初の取引を実行し、指定されたリカバリーゾーン内での価格の動きを追跡します。 

私たちは、市場のさまざまな状況下で、ロットサイズを乗数で調整し、必要に応じてヘッジポジションを展開する動的なリカバリーメカニズムを厳密にテストしました。プログラムは、各シグナルに対するリカバリーーシナリオを個別に評価し、それぞれの取引が独立して管理されるようrecoveryArrayによって反映されます。これにより、複数のリカバリーインスタンスが同時にアクティブであっても、戦略全体の整合性と柔軟性が維持されます。本検証では、以下の設定を用いて、過去5か月間にわたるデータを使用してプログラムをテストしました。

バックテスト設定

完了すると、次の結果が得られます。

以下は、ストラテジーテスターのグラフです。

グラフ

以下は、ストラテジーテスターのレポートです。

レポート

上記の画像からわかるように、グラフは全体的に滑らかですが、残高と有効証拠金の間に相関がある場合には多少の凸凹が見られます。これは、各インスタンスでプログラムが実行するリカバリーレベルの数や、生成されるシグナルの数が増加し続けたことによるものです。したがって、トレーリングストップ機能を有効にすることで、リカバリーポジションの数を制限することが可能です。以下がその結果です。

REPORT_TRAILING有効

画像から分かるように、トレーリングストップ機能を有効にすることで、取引回数が減少し、勝率が向上しています。さらに、ポジション数を制限するために、トレード回数制限ロジックを導入することができます。これは、すでに複数のポジションがオープンしている場合には、新たに生成されたシグナルに基づいて追加の注文をおこなわないようにする仕組みです。これを実現するために、以下のような追加の入力変数を定義します。

input bool inputEnablePositionsRestriction = true; // Enable Maximum positions restriction
input int inputMaximumPositions = 11; // Maximum number of positions

これらの入力変数には、制限オプションを有効または無効にするためのフラグと、制限が有効な場合にシステムで許可される最大ポジション数が含まれます。この制限ロジックは、シグナルが確認された際のOnTickイベントハンドラ内に組み込まれ、取引に対する追加の制約として機能します。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   if (CopyBuffer(rsiHandle, 0, 1, 2, rsiBuffer) <= 0) { //--- Copy the RSI buffer values.
      Print("Failed to copy RSI buffer. Error: ", GetLastError()); //--- Print error on failure.
      return;                                                     //--- Exit on failure.
   }

   datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0); //--- Get the time of the current bar.
   if (currentBarTime != lastBarTime) {                         //--- Check if a new bar has formed.
      lastBarTime = currentBarTime;                             //--- Update the last processed bar time.
      if (rsiBuffer[1] > 30 && rsiBuffer[0] <= 30) {            //--- Check for oversold RSI crossing up.
         Print("BUY SIGNAL");
         if (inputEnablePositionsRestriction == false || inputMaximumPositions > PositionsTotal()){
            PositionRecovery newRecovery;                          //--- Create a new recovery instance.
            newRecovery.Initialize(inputlot, inputzonesizepts, inputzonetragetpts, inputlotmultiplier, _Symbol, ORDER_TYPE_BUY, SymbolInfoDouble(_Symbol, SYMBOL_BID)); //--- Initialize the recovery.
            newRecovery.OpenTrade(ORDER_TYPE_BUY, "Initial Position"); //--- Open an initial BUY position.
            ArrayResize(recoveryArray, ArraySize(recoveryArray) + 1); //--- Resize the recovery array.
            recoveryArray[ArraySize(recoveryArray) - 1] = newRecovery; //--- Add the new recovery to the array.
         }
         else {
            Print("FAILED: Maximum positions threshold hit!");
         }
      } else if (rsiBuffer[1] < 70 && rsiBuffer[0] >= 70) {      //--- Check for overbought RSI crossing down.
         Print("SELL SIGNAL");
         if (inputEnablePositionsRestriction == false || inputMaximumPositions > PositionsTotal()){
            PositionRecovery newRecovery;                          //--- Create a new recovery instance.
            newRecovery.Initialize(inputlot, inputzonesizepts, inputzonetragetpts, inputlotmultiplier, _Symbol, ORDER_TYPE_SELL, SymbolInfoDouble(_Symbol, SYMBOL_BID)); //--- Initialize the recovery.
            newRecovery.OpenTrade(ORDER_TYPE_SELL, "Initial Position"); //--- Open an initial SELL position.
            ArrayResize(recoveryArray, ArraySize(recoveryArray) + 1); //--- Resize the recovery array.
            recoveryArray[ArraySize(recoveryArray) - 1] = newRecovery; //--- Add the new recovery to the array.
         }
         else {
            Print("FAILED: Maximum positions threshold hit!");
         }
      }
   }

   for (int i = 0; i < ArraySize(recoveryArray); i++) { //--- Iterate through all recovery instances.
      recoveryArray[i].ManageZones();                 //--- Manage zones for each recovery instance.
      recoveryArray[i].CheckCloseAtTargets();         //--- Check and close positions at targets.
      recoveryArray[i].ApplyTrailingStop();           //--- Apply trailing stop logic to initial positions.
   }
}

ここでは、ポジション制限を管理し、EAが任意の時点で開くことができる取引数を制御するメカニズムを実装しています。ロジックは、ポジション制限が無効である(inputEnablePositionsRestriction == false)か、または現在のポジション数(PositionsTotal)がユーザー定義の最大数(inputMaximumPositions)を下回っているかを判定することから始まります。いずれかの条件が満たされていれば、EAは新しい取引を開始し、ユーザーの「無制限」または「制限付き」取引の設定に沿って動作します。

一方、どちらの条件も満たされない場合(すなわち、ポジション制限が有効であり、許可された最大ポジション数にすでに達している場合)、EAは新たな取引を開始しません。その代わりに、ターミナルに「FAILED:Maximum positions threshold hit!」という失敗メッセージを記録します。このメッセージは、追加の取引が実行されなかった理由をユーザーに伝える情報フィードバックとして機能します。変更箇所は視認性を高めるために淡い黄色で強調表示しています。テストの結果、以下のような成果が得られました。

REPORT_MAXIMUM ORDERS RESTRICTION

画像から分かるように、取引回数はさらに減少し、勝率も一段と向上しています。これにより、Multi-zone Recoveryシステムの構築という当初の目的を達成できたことが確認されました。さらに、Graphic Interchange Format (GIF)形式の可視化では、次のようなシミュレーションが表示されており、この成果を明確に示しています。

マルチゾーンGIF


結論

本記事では、Multi-Level Zone Recovery戦略に基づく堅牢なMQL5 EAの構築プロセスについて解説しました。自動シグナル検出、動的なリカバリー管理、トレーリングストップなどの利益確保メカニズムといったコアコンセプトを活用することで、複数の独立したリカバリーインスタンスを柔軟に処理できるシステムを実現しました。本実装の主要な構成要素には、取引シグナルの生成、ポジション制限ロジック、そしてリカバリーおよびエグジット戦略の効率的な管理が含まれます。

免責条項本記事は、MQL5プログラミングに関する教育目的の資料です。紹介したMulti-Level Zone Recoveryシステムは、取引管理のための構造化された枠組みを提供しますが、市場の動きは本質的に不確実であり、常にリスクが伴います。過去の実績が将来の成功を保証するものではありません。実際の取引環境で戦略を運用する前には、徹底的なバックテストと適切なリスク管理が必要不可欠です。

このガイドで紹介した手法を活用することで、アルゴリズム取引に関する理解をさらに深め、より高度な取引システムの開発にも応用できるでしょう。コーディングをお楽しみください。そして、あなたの取引トが成功しますように。

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

金融モデリングにおける合成データのための敵対的生成ネットワーク(GAN)(第2回):テスト用の合成シンボルの作成 金融モデリングにおける合成データのための敵対的生成ネットワーク(GAN)(第2回):テスト用の合成シンボルの作成
この記事では、敵対的生成ネットワーク(GAN)を使用して合成シンボルを作成し、EURUSDなどの実際の市場商品の挙動を模倣した現実的な金融データを生成します。GANモデルは、過去の市場データからパターンやボラティリティを学習し、同様の特性を持つ合成価格データを生成します。
独自のLLMをEAに統合する(第5部):LLMによる取引戦略の開発とテスト(IV) - 取引戦略のテスト 独自のLLMをEAに統合する(第5部):LLMによる取引戦略の開発とテスト(IV) - 取引戦略のテスト
今日の人工知能の急速な発展に伴い、言語モデル(LLM)は人工知能の重要な部分となっています。私たちは、強力なLLMをアルゴリズム取引に統合する方法を考える必要があります。ほとんどの人にとって、これらの強力なモデルをニーズに応じてファインチューニングし、ローカルに展開して、アルゴリズム取引に適用することは困難です。本連載では、この目標を達成するために段階的なアプローチをとっていきます。
プライスアクション分析ツールキットの開発(第10回):External Flow (II) VWAP プライスアクション分析ツールキットの開発(第10回):External Flow (II) VWAP
私たちの総合ガイドで、VWAPの力を完全にマスターしましょう。MQL5とPythonを活用して、VWAP分析を取引戦略に統合する方法を学びます。市場に対する洞察を最大限に活かし、より良い取引判断を下せるようになりましょう。
アンサンブル学習におけるゲーティングメカニズム アンサンブル学習におけるゲーティングメカニズム
この記事では、アンサンブルモデルの検討をさらに進め、「ゲート」という概念に注目し、モデル出力を組み合わせることで予測精度や汎化性能の向上にどのように役立つかを解説します。