English Deutsch
preview
MQL5での取引戦略の自動化(第22回):Envelopes Trend取引のためのZone Recoveryシステムの作成

MQL5での取引戦略の自動化(第22回):Envelopes Trend取引のためのZone Recoveryシステムの作成

MetaTrader 5トレーディング |
234 2
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の記事(第21回)では、MetaQuotes Language 5 (MQL5)でニューラルネットワークを活用した取引戦略に、適応学習率を組み合わせ、市場の動きの予測精度を向上させる方法を検討しました。第22回では、Envelopes Trend取引戦略と統合されたZone Recoveryシステムの作成に焦点を移します。RSIとエンベロープ指標を組み合わせることで、取引の自動化と損失管理を効率的におこなえるシステムを設計します。本記事では以下のトピックを扱います。

  1. Zone Recovery Envelopes Trendアーキテクチャを理解する
  2. MQL5での実装
  3. バックテスト
  4. 結論

これにより、変動の激しい市場環境に対応可能な、堅牢で信頼性の高いMQL5自動取引システムを完成させ、実装とテストにすぐに移行できる状態にすることを目指します。では、さっそく取り組んでいきましょう。


Zone Recovery Envelopes Trendアーキテクチャを理解する

ゾーンリカバリー(zone recovery)は、潜在的な損失を利益に変えることを目的としたスマートな取引戦略です。市場が予想に反して動いた場合に追加の取引をおこなうことで、損失を取り戻すか、または損益をゼロに抑えることを狙います。たとえば、ある通貨ペアの価格が上昇すると予想して買いポジションを持ったものの、価格が下落した場合を考えます。このときゾーンリカバリーは、「ゾーン」と呼ばれる価格帯を設定し、そのゾーン内で逆方向の取引をおこなうことで、価格が反発した際に損失を回復します。本記事では、この概念をMetaQuotes Language 5 (MQL5)で自動化し、低リスクで利益最大化を目指すFX取引システムの開発を目指します。

この戦略を効果的に機能させるために、2つのテクニカル指標を組み合わせて最適なエントリーポイントを見極めます。1つ目は、市場の勢いを評価するもので、方向性が強い場合にのみ取引をおこなうように設計されており、弱い動きやノイズによる誤シグナルを避けることができます。2つ目であるEnvelopesは、市場の平均価格を中心にチャネルを描き、価格が上限または下限に達したときに反転の可能性を示します。この2つのインジケーターを組み合わせることで、トレンドの中で価格が反転しやすい高確率の取引機会を見極めることができます。

ここからは、これまでの要素をどのように組み合わせるかです。まず、インジケーターが反転を示したタイミングで取引を開始します。たとえば、価格がエンベロープチャネルの端に達し、十分な勢いを伴っている場合です。市場が予想とは逆方向に動いた場合には、設定した価格ゾーン内で逆方向の取引をおこない、損失を回復するゾーンリカバリーを発動します。この際、取引量はリスクと回復のバランスを慎重に設定します。また、過剰取引を避けるために取引回数に上限を設け、システムの規律を保ちます。この仕組みにより、トレンドのチャンスを追いつつ、安全ネットを確保でき、荒れた市場でも穏やかな市場でも柔軟に対応可能な取引システムとなります。これから、この計画を実際に形にしてテストしていきます。以下に実装計画を示します。

戦略計画


MQL5での実装

MQL5でプログラムを作成するには、まずMetaEditorを開き、ナビゲータに移動して、インジケーターフォルダを見つけ、[新規]タブをクリックして、表示される手順に従ってファイルを作成します。作成が完了したら、コーディング環境で、プログラムの主要な値を簡単に制御するのに役立ついくつかの入力変数を宣言することから始めます。

//+------------------------------------------------------------------+
//|                 Envelopes Trend Bounce with Zone Recovery EA.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property strict

#include <Trade/Trade.mqh>                                             //--- Include trade library

enum TradingLotSizeOptions { FIXED_LOTSIZE, UNFIXED_LOTSIZE };         //--- Define lot size options

input group "======= EA GENERAL SETTINGS ======="
input TradingLotSizeOptions lotOption = UNFIXED_LOTSIZE;               // Lot Size Option
input double initialLotSize = 0.01;                                    // Initial Lot Size
input double riskPercentage = 1.0;                                     // Risk Percentage (%)
input int    riskPoints = 300;                                         // Risk Points
input int    magicNumber = 123456789;                                  // Magic Number
input int    maxOrders = 1;                                            // Maximum Initial Positions
input double zoneTargetPoints = 600;                                   // Zone Target Points
input double zoneSizePoints = 300;                                     // Zone Size Points
input bool   restrictMaxOrders = true;                                 // Apply Maximum Orders Restriction

ここでは、MQL5におけるEnvelopes Trend取引のためのZone Recoveryシステムの基盤を構築し、必要なコンポーネントやユーザー設定可能なオプションを整えます。まず、「<Trade/Trade.mqh>」ライブラリをインクルードします。このライブラリはCTradeクラスを提供しており、ポジションの新規注文や決済など、取引操作を実行するために必要な機能を備えています。このライブラリを組み込むことは非常に重要です。なぜなら、エキスパートアドバイザー(EA)が市場とスムーズに連携し、特に注文の発注処理を正確におこなうためのツールを提供するからです。ファイルを開く方法については以下を参照してください。

MQL5取引操作ファイル

次に、TradingLotSizeOptionsという列挙型を定義し、2つの値「FIXED_LOTSIZE」と「UNFIXED_LOTSIZE」を設定します。これにより、ユーザーは固定ロットサイズで取引するか、リスクパラメータに応じて動的にロットサイズを調整するかを選択でき、さまざまな取引スタイルに対応した柔軟なロット管理が可能になります。続いて、「EA GENERAL SETTINGS」グループ内で入力パラメータを設定します。これらはMetaTrader 5プラットフォーム上でユーザーが調整可能です。

ここで設定するlotOptionはデフォルトでUNFIXED_LOTSIZEに設定されており、取引が固定ロットでおこなわれるかリスクベースでおこなわれるかを決定します。initialLotSizeは固定取引時のロットサイズを0.01に設定し、動的ロットサイズの場合はriskPercentageとriskPointsにより、口座残高の1%とストップロス距離300ポイントを基準にロットを計算します。これらの設定により、取引ごとのリスク量を制御し、EAの動作がユーザーのリスク許容度に沿うように調整されます。

さらに、EAの取引を識別するために一意のマジックナンバーとして「123456789」を割り当てます。これにより、同一口座内の他の取引と区別することができます。maxOrders (1)とrestrictMaxOrders (true)の入力値は、初期ポジションの数を制限し、EAが一度に過剰な取引を開かないようにします。最後にzoneTargetPointsを600、zoneSizePointsを300に設定し、利益目標とリカバリーゾーンの大きさをポイント単位で定義することで、ゾーンリカバリー戦略の範囲を明確にしています。コンパイルすると、次のような出力が得られます。

読み込まれた入力

入力が読み込まれたら、システム全体のコアロジックの宣言を開始できます。オブジェクト指向プログラミング(OOP) アプローチを適用するため、まず使用するいくつかの構造体とクラスを宣言します。

class MarketZoneTrader {
private:
   //--- Trade State Definition
   enum TradeState { INACTIVE, RUNNING, TERMINATING };                 //--- Define trade lifecycle states

   //--- Data Structures
   struct TradeMetrics {
      bool   operationSuccess;                                         //--- Track operation success
      double totalVolume;                                              //--- Sum closed trade volumes
      double netProfitLoss;                                            //--- Accumulate profit/loss
   };

   struct ZoneBoundaries {
      double zoneHigh;                                                 //--- Upper recovery zone boundary
      double zoneLow;                                                  //--- Lower recovery zone boundary
      double zoneTargetHigh;                                           //--- Upper profit target
      double zoneTargetLow;                                            //--- Lower profit target
   };

   struct TradeConfig {
      string         marketSymbol;                                     //--- Trading symbol
      double         openPrice;                                        //--- Position entry price
      double         initialVolume;                                    //--- Initial trade volume
      long           tradeIdentifier;                                  //--- Magic number
      string         tradeLabel;                                       //--- Trade comment
      ulong          activeTickets[];                                  //--- Active position tickets
      ENUM_ORDER_TYPE direction;                                       //--- Trade direction
      double         zoneProfitSpan;                                   //--- Profit target range
      double         zoneRecoverySpan;                                 //--- Recovery zone range
      double         accumulatedBuyVolume;                             //--- Total buy volume
      double         accumulatedSellVolume;                            //--- Total sell volume
      TradeState     currentState;                                     //--- Current trade state
   };

   struct LossTracker {
      double tradeLossTracker;                                         //--- Track cumulative profit/loss
   };
};

ここでは、MQL5におけるEnvelopes Trend取引のシステムの中核構造を、MarketZoneTraderクラスを実装することで定義します。特にprivateセクションに焦点を当て、取引状態の定義やデータ構造を整えます。このロジックにより、取引管理、リカバリーゾーンの追跡、パフォーマンス監視など、重要な要素を整理することが可能になります。まず、MarketZoneTraderクラスを定義します。このクラスはEAの骨格として機能し、取引戦略のロジックをカプセル化します。

privateセクションでは、まずTradeStateという列挙型を導入し、INACTIVE、RUNNING、TERMINATINGの3つの状態を設定します。これにより、EAがアイドル状態にあるのか、取引を積極的に管理しているのか、あるいはポジションを終了しているのかを追跡でき、取引プロセスの管理が容易になります。これは、リカバリー取引の発動やポジションの最終決済などのアクションを調整する上で非常に重要です。

次に、取引の主要なパフォーマンスデータを保持するTradeMetrics構造体を作成します。この構造体には、取引操作(ポジション決済など)が成功したかどうかを追跡するoperationSuccess、決済済み取引の合計ボリュームを管理するtotalVolume、およびそれらの取引から得られた損益を累積するnetProfitLossが含まれます。これにより、リカバリーや決済時の取引結果を評価し、パフォーマンスを明確に把握できます。

さらに、ZoneBoundaries構造体を定義し、ゾーンリカバリー戦略で使用する価格レベルを格納します。変数「zoneHigh」と「zoneLow」は損失を軽減するために逆方向の取引をおこなうリカバリーゾーンの上限と下限を示し、「zoneTargetHigh」と「zoneTargetLow」はゾーンを超えた利益目標を設定し、利益確定のタイミングを示します。これらの境界は戦略に不可欠であり、リカバリーの発動やポジションのクローズのタイミングを指示する役割を持ちます。視覚的なイメージを示すと、構造体の必要性が理解しやすくなります。

ゾーンサンプル

次に、TradeConfig構造体では取引設定を格納します。marketSymbolには通貨ペアを、openPriceにはエントリー価格を、initialVolumeには取引サイズを設定します。tradeIdentifierには一意のマジックナンバーを保持し、tradeLabelは取引を識別するコメントを付与します。activeTickets配列はポジションのチケットを追跡し、directionは買いか売りかを指定します。また、zoneProfitSpanとzoneRecoverySpanにより、利益目標とリカバリーゾーンの大きさを価格単位で定義し、accumulatedBuyVolumeとaccumulatedSellVolumeで各取引タイプの累積ボリュームを監視します。currentState変数はTradeState列挙型を用いて取引状態を追跡し、全体を統合します。

最後に、LossTracker構造体を追加し、tradeLossTracker変数で取引全体の累積損益を監視します。これにより、リカバリーの影響を定量的に評価し、損失が大きくなりすぎた場合に戦略を調整することが可能になります。さらに、取引に関するその他の重要度は低いが必要な情報を格納するためのメンバー変数も定義していきます。

//--- Member Variables
TradeConfig           m_tradeConfig;                                //--- Store trade configuration
ZoneBoundaries        m_zoneBounds;                                 //--- Store zone boundaries
LossTracker           m_lossTracker;                                //--- Track profit/loss
string                m_lastError;                                  //--- Store error message
int                   m_errorStatus;                                //--- Store error code
CTrade                m_tradeExecutor;                              //--- Manage trade execution
int                   m_handleRsi;                                  //--- RSI indicator handle
int                   m_handleEnvUpper;                             //--- Upper Envelopes handle
int                   m_handleEnvLower;                             //--- Lower Envelopes handle
double                m_rsiBuffer[];                                //--- RSI data buffer
double                m_envUpperBandBuffer[];                       //--- Upper Envelopes buffer
double                m_envLowerBandBuffer[];                       //--- Lower Envelopes buffer
TradingLotSizeOptions m_lotOption;                                  //--- Lot size option
double                m_initialLotSize;                             //--- Fixed lot size
double                m_riskPercentage;                             //--- Risk percentage
int                   m_riskPoints;                                 //--- Risk points
int                   m_maxOrders;                                  //--- Maximum positions
bool                  m_restrictMaxOrders;                          //--- Position restriction flag
double                m_zoneTargetPoints;                           //--- Profit target points
double                m_zoneSizePoints;                             //--- Recovery zone points

MarketZoneTraderクラスのprivateセクションでは、取引設定、リカバリーゾーン、指標データを管理するための主要なメンバー変数を定義します。m_tradeConfig(TradeConfig構造体)には、銘柄や取引方向などの取引情報を格納し、m_zoneBounds(ZoneBoundaries構造体)にはリカバリーゾーンや利益目標の価格を保持します。m_lossTracker(LossTracker構造体)は取引の損益を追跡します。エラー処理のためにm_lastError(文字列)やm_errorStatus(整数)で問題を記録し、m_tradeExecutor(CTradeクラス)が実際の取引操作を担当します。

インジケーターに関しては、m_handleRsi、m_handleEnvUpper、m_handleEnvLowerでRSIおよびエンベロープのデータにアクセスし、m_rsiBuffer、m_envUpperBandBuffer、m_envLowerBandBufferの配列にそれぞれの値を格納します。入力設定はm_lotOption(TradingLotSizeOptions)、m_initialLotSize、m_riskPercentage、m_riskPoints、m_maxOrders、m_restrictMaxOrders、m_zoneTargetPoints、およびm_zoneSizePointsに保存し、ロットサイズ、ポジション上限、ゾーンサイズを制御いたします。これらの変数は取引およびインジケーター管理の基盤を形成し、今後の取引ロジックに備える役割を果たします。次に、プログラム内で頻繁に使用するヘルパー関数を定義する必要があります。

//--- Error Handling
void logError(string message, int code) {
   //--- Error Logging Start
   m_lastError = message;                                           //--- Store error message
   m_errorStatus = code;                                            //--- Store error code
   Print("Error: ", message);                                       //--- Log error to Experts tab
   //--- Error Logging End
}

//--- Market Data Access
double getMarketVolumeStep() {
   //--- Volume Step Retrieval Start
   return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_VOLUME_STEP); //--- Retrieve broker's volume step
   //--- Volume Step Retrieval End
}

double getMarketAsk() {
   //--- Ask Price Retrieval Start
   return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_ASK); //--- Retrieve ask price
   //--- Ask Price Retrieval End
}

double getMarketBid() {
   //--- Bid Price Retrieval Start
   return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_BID); //--- Retrieve bid price
   //--- Bid Price Retrieval End
}

ここでは、エラー処理と市場データアクセスのための重要なユーティリティ関数を追加します。logError関数はmessageをm_lastErrorに、codeをm_errorStatusに格納し、Printを通じてExpertsタブにメッセージを出力してデバッグをおこないます。getMarketVolumeStep関数はSymbolInfoDoubleSYMBOL_VOLUME_STEPとともに使用し、m_tradeConfig.marketSymbolに対してブローカーのボリューム刻み値を取得して、有効な取引サイズを確保します。getMarketAsk関数およびgetMarketBid関数は、SymbolInfoDoubleをSYMBOL_ASKおよびSYMBOL_BIDとともに使用し、正確な取引価格を取得します。

ここから、取引操作を実行する主要な関数を定義していきます。まずは初期化、取引チケットの保存による追跡および監視、そして取引のクローズをおこなうための、比較的単純なロジックから開始します。

//--- Trade Initialization
bool configureTrade(ulong ticket) {
   //--- Trade Configuration Start
   if (!PositionSelectByTicket(ticket)) {                               //--- Select position by ticket
      logError("Failed to select ticket " + IntegerToString(ticket), INIT_FAILED); //--- Log selection failure
      return false;                                                     //--- Return failure
   }
   m_tradeConfig.marketSymbol = PositionGetString(POSITION_SYMBOL);     //--- Set symbol
   m_tradeConfig.tradeLabel = __FILE__;                                 //--- Set trade comment
   m_tradeConfig.tradeIdentifier = PositionGetInteger(POSITION_MAGIC);  //--- Set magic number
   m_tradeConfig.direction = (ENUM_ORDER_TYPE)PositionGetInteger(POSITION_TYPE);   //--- Set direction
   m_tradeConfig.openPrice = PositionGetDouble(POSITION_PRICE_OPEN);    //--- Set entry price
   m_tradeConfig.initialVolume = PositionGetDouble(POSITION_VOLUME);    //--- Set initial volume
   m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier); //--- Set magic number for executor
   return true;                                                         //--- Return success
   //--- Trade Configuration End
}

//--- Trade Ticket Management
void storeTradeTicket(ulong ticket) {
   //--- Ticket Storage Start
   int ticketCount = ArraySize(m_tradeConfig.activeTickets);        //--- Get ticket count
   ArrayResize(m_tradeConfig.activeTickets, ticketCount + 1);       //--- Resize ticket array
   m_tradeConfig.activeTickets[ticketCount] = ticket;               //--- Store ticket
   //--- Ticket Storage End
}

//--- Trade Execution
ulong openMarketTrade(ENUM_ORDER_TYPE tradeDirection, double tradeVolume, double price) {
   //--- Trade Opening Start
   ulong ticket = 0;                                                //--- Initialize ticket
   if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, tradeDirection, tradeVolume, price, 0, 0, m_tradeConfig.tradeLabel)) { //--- Open position
      ticket = m_tradeExecutor.ResultOrder();                       //--- Get ticket
   } else {
      Print("Failed to open trade: Direction=", EnumToString(tradeDirection), ", Volume=", tradeVolume); //--- Log failure
   }
   return ticket;                                                   //--- Return ticket
   //--- Trade Opening End
}

//--- Trade Closure
void closeActiveTrades(TradeMetrics &metrics) {
   //--- Trade Closure Start
   for (int i = ArraySize(m_tradeConfig.activeTickets) - 1; i >= 0; i--) {    //--- Iterate tickets in reverse
      if (m_tradeConfig.activeTickets[i] > 0) {                               //--- Check valid ticket
         if (m_tradeExecutor.PositionClose(m_tradeConfig.activeTickets[i])) { //--- Close position
            m_tradeConfig.activeTickets[i] = 0;                               //--- Clear ticket
            metrics.totalVolume += m_tradeExecutor.ResultVolume();            //--- Accumulate volume
            if ((ENUM_ORDER_TYPE)PositionGetInteger(POSITION_TYPE) == ORDER_TYPE_BUY) { //--- Check buy position
               metrics.netProfitLoss += m_tradeExecutor.ResultVolume() * (m_tradeExecutor.ResultPrice() - PositionGetDouble(POSITION_PRICE_OPEN)); //--- Calculate buy profit
            } else {                                                          //--- Handle sell position
               metrics.netProfitLoss += m_tradeExecutor.ResultVolume() * (PositionGetDouble(POSITION_PRICE_OPEN) - m_tradeExecutor.ResultPrice()); //--- Calculate sell profit
            }
         } else {
            metrics.operationSuccess = false;                                  //--- Mark failure
            Print("Failed to close ticket: ", m_tradeConfig.activeTickets[i]); //--- Log failure
         }
      }
   }
   //--- Trade Closure End
}

//--- Bar Detection
bool isNewBar() {
   //--- New Bar Detection Start
   static datetime previousTime = 0;                                      //--- Store previous bar time
   datetime currentTime = iTime(m_tradeConfig.marketSymbol, Period(), 0); //--- Get current bar time
   bool result = (currentTime != previousTime);                           //--- Check for new bar
   previousTime = currentTime;                                            //--- Update previous time
   return result;                                                         //--- Return new bar status
   //--- New Bar Detection End
}

ここでは、プログラムの中核となるロジックに踏み込み、取引の設定、ポジションの追跡、注文の実行、取引のクローズ、そしてアクションのタイミング制御をおこなう関数を作成します。最初に、指定されたticketの取引を準備するためのconfigureTrade関数を作成します。まず、PositionSelectByTicket関数を使用してポジションの選択を試みます。これが失敗した場合は、logError関数を使って問題を記録し、falseを返して処理を終了します。成功した場合は、PositionGetString関数を使ってmarketSymbolを取得し、tradeLabelに__FILE__を設定します。さらに、PositionGetIntegerからtradeIdentifierとdirectionを取得し、後者をENUM_ORDER_TYPEにキャストします。その後、PositionGetDoubleでopenPriceとinitialVolumeを設定し、SetExpertMagicNumberを使用してm_tradeExecutorにタグを付け、取引の準備を整えます。

次に、storeTradeTicket関数を作成して、ポジションを整理します。ArraySize関数でm_tradeConfig.activeTicketsのサイズを確認し、ArrayResize関数で配列を1つ拡張し、新しいticketを格納します。これにより、アクティブな取引を常に把握できるようにします。続いて、openMarketTrade関数を作成し、市場での取引を実行します。m_tradeExecutor.PositionOpenを呼び出し、tradeDirection、tradeVolume、price、m_tradeConfigの詳細を渡します。成功した場合はResultOrderでticketを割り当て、失敗した場合はPrintを使用してエラーを記録し、取引実行の安定性を確保します。

その後、closeActiveTrades関数を作成し、ポジションをクローズします。m_tradeConfig.activeTicketsを逆順にループし、各有効なticketをm_tradeExecutor.PositionCloseでクローズします。成功した場合はticketをクリアし、ResultVolumeをmetrics.totalVolumeに加算し、PositionGetIntegerおよびPositionGetDoubleを使用して取引方向を確認しながらmetrics.netProfitLossを算出します。何らかの失敗が発生した場合は、metrics.operationSuccessをfalseに設定し、Printでログを残して全ての結果を追跡します。

最後に、isNewBar関数を追加して、1本のバーにつき1回だけ取引を行えるようにします。iTime関数を使用してm_tradeConfig.marketSymbolの現在のバー時間を取得し、previousTimeと比較します。異なる場合はpreviousTimeを更新し、新しいバーが生成されたことを検知して取引シグナルを確認できるようにします。この後は、取引量を計算する関数およびオープン条件を判定する関数を作成する必要があります。

//--- Lot Size Calculation
double calculateLotSize(double riskPercent, int riskPips) {
   //--- Lot Size Calculation Start
   double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * riskPercent / 100;                //--- Calculate risk amount
   double tickSize = SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_TRADE_TICK_SIZE);   //--- Get tick size
   double tickValue = SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_TRADE_TICK_VALUE); //--- Get tick value
   if (tickSize == 0 || tickValue == 0) {                           //--- Validate tick data
      Print("Invalid tick size or value");                          //--- Log invalid data
      return -1;                                                    //--- Return invalid lot
   }
   double lotValue = (riskPips * _Point) / tickSize * tickValue;    //--- Calculate lot value
   if (lotValue == 0) {                                             //--- Validate lot value
      Print("Invalid lot value");                                   //--- Log invalid lot
      return -1;                                                    //--- Return invalid lot
   }
   return NormalizeDouble(riskMoney / lotValue, 2);                 //--- Return normalized lot size
   //--- Lot Size Calculation End
}

//--- Order Execution
int openOrder(ENUM_ORDER_TYPE orderType, double stopLoss, double takeProfit) {
   //--- Order Opening Start
   int ticket;                                                      //--- Initialize ticket
   double openPrice;                                                //--- Initialize open price
   
   if (orderType == ORDER_TYPE_BUY) {                               //--- Check buy order
      openPrice = NormalizeDouble(getMarketAsk(), Digits());        //--- Set buy price
   } else if (orderType == ORDER_TYPE_SELL) {                       //--- Check sell order
      openPrice = NormalizeDouble(getMarketBid(), Digits());        //--- Set sell price
   } else {
      Print("Invalid order type");                                  //--- Log invalid type
      return -1;                                                    //--- Return invalid ticket
   }
   
   double lotSize = 0;                                              //--- Initialize lot size
   
   if (m_lotOption == FIXED_LOTSIZE) {                              //--- Check fixed lot
      lotSize = m_initialLotSize;                                   //--- Use fixed lot size
   } else if (m_lotOption == UNFIXED_LOTSIZE) {                     //--- Check dynamic lot
      lotSize = calculateLotSize(m_riskPercentage, m_riskPoints);   //--- Calculate risk-based lot
   }
   
   if (lotSize <= 0) {                                              //--- Validate lot size
      Print("Invalid lot size: ", lotSize);                         //--- Log invalid lot
      return -1;                                                    //--- Return invalid ticket
   }
   
   if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, orderType, lotSize, openPrice, 0, 0, __FILE__)) { //--- Open position
      ticket = (int)m_tradeExecutor.ResultOrder();                  //--- Get ticket
      Print("New trade opened: Ticket=", ticket, ", Type=", EnumToString(orderType), ", Volume=", lotSize); //--- Log success
   } else {
      ticket = -1;                                                  //--- Set invalid ticket
      Print("Failed to open order: Type=", EnumToString(orderType), ", Volume=", lotSize); //--- Log failure
   }
   
   return ticket;                                                   //--- Return ticket
   //--- Order Opening End
}

まず、リスクパラメータに基づいて取引サイズを算出するために、calculateLotSize関数を作成します。最初に、AccountInfoDoubleACCOUNT_BALANCEとともに使用し、口座残高に対してriskPercentの割合を掛けてriskMoneyを計算します。次に、SymbolInfoDoubleをSYMBOL_TRADE_TICK_SIZEおよびSYMBOL_TRADE_TICK_VALUEとともに使用して、m_tradeConfig.marketSymbolのtickSizeおよびtickValueを取得します。いずれかがゼロの場合は、Printでエラーを記録し、無効な計算を避けるために-1を返します。続いて、riskPips、_Point、tickSize、およびtickValueを用いてlotValueを算出します。これがゼロの場合も同様にエラーを記録して-1を返します。最後に、NormalizeDoubleを使用して小数点以下2桁に丸めたロットサイズを返し、ブローカー仕様に適合させます。

次に、取引をおこなうためのopenOrder関数を作成します。まず、ticketとopenPriceを初期化し、orderTypeを確認します。ORDER_TYPE_BUYの場合はgetMarketAskを使用し、Digitsを指定してNormalizeDoubleでopenPriceを設定します。ORDER_TYPE_SELLの場合はgetMarketBidを使用します。orderTypeが無効な場合はPrintでエラーを記録し、-1を返します。続いて、m_lotOptionに基づいてlotSizeを決定します。FIXED_LOTSIZEの場合はm_initialLotSizeを使用し、UNFIXED_LOTSIZEの場合はm_riskPercentageおよびm_riskPointsを引数としてcalculateLotSizeを呼び出します。lotSizeが無効な場合はPrintでエラーを記録し、-1を返します。その後、m_tradeExecutor.PositionOpenを使用して、m_tradeConfig.marketSymbol、orderType、lotSize、openPrice、およびFILEをコメントとして指定し、ポジションをオープンします。成功した場合はResultOrderでticketを設定し、Printでログを残します。失敗した場合はticketを-1に設定し、同様にPrintでエラーを出力します。最後にticketの値を返します。

これらの準備が整ったら、システムの各種値を初期化する必要があります。専用の初期化関数を定義する方法もありますが、シンプルさを保つためにコンストラクタを使用します。コンストラクタはプログラム全体からアクセスできるよう、publicアクセス修飾子で定義することを推奨します。また、ここでデストラクタも定義し、リソース解放や終了処理を適切におこなえるようにします。

public:
   //--- Constructor
   MarketZoneTrader(TradingLotSizeOptions lotOpt, double initLot, double riskPct, int riskPts, int maxOrds, bool restrictOrds, double targetPts, double sizePts) {
      //--- Constructor Start
      m_tradeConfig.currentState = INACTIVE;                           //--- Set initial state
      ArrayResize(m_tradeConfig.activeTickets, 0);                     //--- Initialize ticket array
      m_tradeConfig.zoneProfitSpan = targetPts * _Point;               //--- Set profit target
      m_tradeConfig.zoneRecoverySpan = sizePts * _Point;               //--- Set recovery zone
      m_lossTracker.tradeLossTracker = 0.0;                            //--- Initialize loss tracker
      m_lotOption = lotOpt;                                            //--- Set lot size option
      m_initialLotSize = initLot;                                      //--- Set initial lot
      m_riskPercentage = riskPct;                                      //--- Set risk percentage
      m_riskPoints = riskPts;                                          //--- Set risk points
      m_maxOrders = maxOrds;                                           //--- Set max positions
      m_restrictMaxOrders = restrictOrds;                              //--- Set restriction flag
      m_zoneTargetPoints = targetPts;                                  //--- Set target points
      m_zoneSizePoints = sizePts;                                      //--- Set zone points
      m_tradeConfig.marketSymbol = _Symbol;                            //--- Set symbol
      m_tradeConfig.tradeIdentifier = magicNumber;                     //--- Set magic number
      //--- Constructor End
   }

   //--- Destructor
   ~MarketZoneTrader() {
      //--- Destructor Start
      cleanup();                                                       //--- Release resources
      //--- Destructor End
   }

次に、MarketZoneTraderクラスのpublicセクション内でコンストラクタとデストラクタを定義します。まず、MarketZoneTraderコンストラクタを定義し、引数としてlotOpt、initLot、riskPct、riskPts、maxOrds、restrictOrds、targetPts、sizePtsを受け取ります。最初に、m_tradeConfig.currentStateをINACTIVEに設定し、現在アクティブな取引が存在しないことを示します。次に、 ArrayResizeを使用してm_tradeConfig.activeTickets配列をサイズ0にリセットし、新しいチケットを受け入れる準備を整えます。さらに、targetPtsおよびsizePtsに_Pointを掛けてm_tradeConfig.zoneProfitSpanおよびm_tradeConfig.zoneRecoverySpanを算出し、利益目標およびリカバリーゾーンの価格単位を設定します。m_lossTracker.tradeLossTrackerは0.0に初期化し、損益の追跡をゼロから開始します。

続いて、入力パラメータをメンバー変数に代入します。m_lotOptionにはlotOpt、m_initialLotSizeにはinitLot、m_riskPercentageにはriskPct、m_riskPointsにはriskPts、m_maxOrdersにはmaxOrds、m_restrictMaxOrdersにはrestrictOrds、m_zoneTargetPointsにはtargetPts、m_zoneSizePointsにはsizePtsをそれぞれ設定します。また、m_tradeConfig.marketSymbolには_Symbolを代入し、現在のチャートシンボルで取引をおこなうようにします。m_tradeConfig.tradeIdentifierにはmagicNumberを設定し、取引識別用の一意の番号として使用します。これにより、EAがユーザー設定を正確に反映し、取引準備が整います。

次に、~MarketZoneTraderデストラクタを定義し、リソースのクリーンアップをおこないます。cleanup関数を呼び出して、インジケータハンドルなどの確保済みリソースを解放し、EAがメモリリークを起こすことなく正常に終了できるようにします。なお、コンストラクタとデストラクタは同じクラス名を使用しますが、デストラクタは名前の前にチルダ(~)が付く点のみが異なります。以下が、不要になった際にクラスを破棄するための関数定義部分です。

//--- Cleanup
void cleanup() {
   //--- Cleanup Start
   IndicatorRelease(m_handleRsi);                                   //--- Release RSI handle
   ArrayFree(m_rsiBuffer);                                          //--- Free RSI buffer
   IndicatorRelease(m_handleEnvUpper);                              //--- Release upper Envelopes handle
   ArrayFree(m_envUpperBandBuffer);                                 //--- Free upper Envelopes buffer
   IndicatorRelease(m_handleEnvLower);                              //--- Release lower Envelopes handle
   ArrayFree(m_envLowerBandBuffer);                                 //--- Free lower Envelopes buffer
   //--- Cleanup End
}

インジケーターハンドルを解放するためにはIndicatorRelease関数を、配列のメモリを解放するためにはArrayFree関数を使用します。インジケータを扱うことになるため、プログラム起動時に呼び出す初期化関数を定義します。

//--- Getters
TradeState getCurrentState() {
   //--- Get Current State Start
   return m_tradeConfig.currentState;                               //--- Return trade state
   //--- Get Current State End
}

double getZoneTargetHigh() {
   //--- Get Target High Start
   return m_zoneBounds.zoneTargetHigh;                              //--- Return profit target high
   //--- Get Target High End
}

double getZoneTargetLow() {
   //--- Get Target Low Start
   return m_zoneBounds.zoneTargetLow;                               //--- Return profit target low
   //--- Get Target Low End
}

double getZoneHigh() {
   //--- Get Zone High Start
   return m_zoneBounds.zoneHigh;                                    //--- Return recovery zone high
   //--- Get Zone High End
}

double getZoneLow() {
   //--- Get Zone Low Start
   return m_zoneBounds.zoneLow;                                     //--- Return recovery zone low
   //--- Get Zone Low End
}

//--- Initialization
int initialize() {
   //--- Initialization Start
   m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier); //--- Set magic number
   int totalPositions = PositionsTotal();                               //--- Get total positions
   
   for (int i = 0; i < totalPositions; i++) {                           //--- Iterate positions
      ulong ticket = PositionGetTicket(i);                              //--- Get ticket
      if (PositionSelectByTicket(ticket)) {                             //--- Select position
         if (PositionGetString(POSITION_SYMBOL) == m_tradeConfig.marketSymbol && PositionGetInteger(POSITION_MAGIC) == m_tradeConfig.tradeIdentifier) { //--- Check symbol and magic
            if (activateTrade(ticket)) {                                //--- Activate position
               Print("Existing position activated: Ticket=", ticket);   //--- Log activation
            } else {
               Print("Failed to activate existing position: Ticket=", ticket); //--- Log failure
            }
         }
      }
   }
   
   m_handleRsi = iRSI(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 8, PRICE_CLOSE); //--- Initialize RSI
   if (m_handleRsi == INVALID_HANDLE) {                             //--- Check RSI
      Print("Failed to initialize RSI indicator");                  //--- Log failure
      return INIT_FAILED;                                           //--- Return failure
   }
   
   m_handleEnvUpper = iEnvelopes(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 150, 0, MODE_SMA, PRICE_CLOSE, 0.1); //--- Initialize upper Envelopes
   if (m_handleEnvUpper == INVALID_HANDLE) {                        //--- Check upper Envelopes
      Print("Failed to initialize upper Envelopes indicator");      //--- Log failure
      return INIT_FAILED;                                           //--- Return failure
   }
   
   m_handleEnvLower = iEnvelopes(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 95, 0, MODE_SMA, PRICE_CLOSE, 1.4); //--- Initialize lower Envelopes
   if (m_handleEnvLower == INVALID_HANDLE) {                        //--- Check lower Envelopes
      Print("Failed to initialize lower Envelopes indicator");      //--- Log failure
      return INIT_FAILED;                                           //--- Return failure
   }
   
   ArraySetAsSeries(m_rsiBuffer, true);                             //--- Set RSI buffer
   ArraySetAsSeries(m_envUpperBandBuffer, true);                    //--- Set upper Envelopes buffer
   ArraySetAsSeries(m_envLowerBandBuffer, true);                    //--- Set lower Envelopes buffer
   
   Print("EA initialized successfully");                            //--- Log success
   return INIT_SUCCEEDED;                                           //--- Return success
   //--- Initialization End
}

ここでは、主要な取引データへアクセスするためのシンプルなゲッター関数を作成します。まず、getCurrentState関数を定義し、m_tradeConfig.currentStateを返します。これにより、システムがINACTIVE、RUNNING、またはTERMINATINGのどの状態にあるかを確認できます。次に、getZoneTargetHighおよびgetZoneTargetLowを定義し、それぞれm_zoneBounds.zoneTargetHighおよびm_zoneBounds.zoneTargetLowを取得して、取引の利益目標価格を取得できるようにします。その後、getZoneHighおよびgetZoneLowを定義し、m_zoneBounds.zoneHighおよびm_zoneBounds.zoneLowを取得して、リカバリーゾーンの上下限を参照できるようにします。

続いて、エキスパートアドバイザー(EA)を初期化するinitialize関数を作成します。最初に、SetExpertMagicNumberを使用してm_tradeConfig.tradeIdentifierをm_tradeExecutorに割り当て、取引識別用のタグを設定します。次に、PositionsTotalで既存のポジションを確認し、ループを通じて各ticketをPositionGetTicketで取得します。PositionSelectByTicketが成功し、そのポジションがPositionGetStringおよびPositionGetIntegerを介してm_tradeConfig.marketSymbolおよびm_tradeConfig.tradeIdentifierと一致する場合、activateTradeを呼び出してそのポジションを管理します。成功または失敗はPrintでログに出力します。

次に、インジケーターを設定します。まず、iRSI関数を使用してm_tradeConfig.marketSymbolに対するRSIハンドルを作成し、期間8、現在の時間足、PRICE_CLOSEを指定します。m_handleRsiがINVALID_HANDLEの場合はPrintでエラーを記録し、INIT_FAILEDを返します。続いて、Envelopesインジケーターを初期化します。m_handleEnvUpperにはiEnvelopes関数を使用し、期間150、単純移動平均、偏差0.1、PRICE_CLOSEを設定します。m_handleEnvLowerには期間95、偏差1.4を指定して作成します。いずれかのハンドルがINVALID_HANDLEの場合は、エラーをログに記録し、INIT_FAILEDを返します。最後に、m_rsiBuffer、m_envUpperBandBuffer、m_envLowerBandBufferをArraySetAsSeriesで時系列配列として設定し、Printで初期化成功をログに出力してINIT_SUCCEEDEDを返します。これで、この関数をOnInitイベントハンドラから呼び出せるようになりますが、その前にクラスのインスタンスを生成する必要があります。

//--- Global Instance
MarketZoneTrader *trader = NULL;                                        //--- Declare trader instance

ここでは、MarketZoneTraderクラスへのポインタを宣言し、システム全体で使用するグローバルインスタンスを設定します。MarketZoneTrader型のポインタ変数traderを作成し、初期値としてNULLを代入します。これにより、取引の初期化、注文の実行、リカバリーゾーンの処理など、EA全体で取引操作を統一的に管理できる単一のグローバルインスタンスが確保されます。初期状態をNULLにしておくことで、EAの完全な初期化が完了する前にインスタンスへ誤ってアクセスすることを防ぎ、適切なタイミングでtraderを生成できるように準備します。これで、関数を呼び出す準備が整いました。

int OnInit() {
   //--- EA Initialization Start
   trader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, maxOrders, restrictMaxOrders, zoneTargetPoints, zoneSizePoints); //--- Create trader instance
   return trader.initialize();                                           //--- Initialize EA
   //--- EA Initialization End
}

OnInitイベントハンドラ内では、まずMarketZoneTraderクラスの新しいインスタンスを作成し、グローバルポインタtraderに代入します。ユーザーが設定した入力パラメータであるlotOption、initialLotSize、riskPercentage、riskPoints、maxOrders、restrictMaxOrders、zoneTargetPoints、zoneSizePointsをコンストラクタに渡し、希望する設定内容で取引システムを構成します。次に、traderのinitialize関数を呼び出してEAを初期化します。この処理には、取引タグの設定、既存ポジションの確認、およびインジケータの初期化が含まれます。その戻り値を返すことで、セットアップが正常に完了したかどうかをシステムに通知します。この関数によって、EAは指定された構成で取引を開始するための完全な準備が整います。コンパイルすると以下の出力が得られます。

初期化イメージ

画像から、プログラムが正常に初期化されたことがわかります。ただし、プログラムを削除しようとすると問題が発生します。以下をご覧ください。

オブジェクトメモリリーク

画像から、削除されていないオブジェクトが存在し、メモリリークが発生していることが分かります。これを解決するためには、オブジェクトのクリーンアップをおこなう必要があります。そのために、以下のロジックを使用します。

void OnDeinit(const int reason) {
   //--- EA Deinitialization Start
   if (trader != NULL) {                                                 //--- Check trader existence
      delete trader;                                                     //--- Delete trader
      trader = NULL;                                                     //--- Clear pointer
      Print("EA deinitialized");                                         //--- Log deinitialization
   }
   //--- EA Deinitialization End
}

クリーンアップをおこなうために、OnDeinitイベントハンドラ内ではまずtraderポインタがNULLでないかを確認し、MarketZoneTraderインスタンスが存在することを確認します。存在する場合は、delete演算子を使用してtraderに割り当てられたメモリを解放し、メモリリークを防ぎます。その後、traderをNULLに設定し、解放済みメモリへの誤アクセスを防止します。最後に、Print関数でEAの初期化解除完了をログに出力します。この処理により、EAはリソースを適切に解放して安全に終了できるようになります。これで、次に進み、シグナル評価やオープン済み取引の管理を行うメインロジックを定義する準備が整いました。そのためにユーティリティ関数を用意する必要があります。

//--- Position Management
bool activateTrade(ulong ticket) {
   //--- Position Activation Start
   m_tradeConfig.currentState = INACTIVE;                           //--- Set state to inactive
   ArrayResize(m_tradeConfig.activeTickets, 0);                     //--- Clear tickets
   m_lossTracker.tradeLossTracker = 0.0;                            //--- Reset loss tracker
   if (!configureTrade(ticket)) {                                    //--- Configure trade
      return false;                                                 //--- Return failure
   }
   storeTradeTicket(ticket);                                        //--- Store ticket
   if (m_tradeConfig.direction == ORDER_TYPE_BUY) {                 //--- Handle buy position
      m_zoneBounds.zoneHigh = m_tradeConfig.openPrice;              //--- Set zone high
      m_zoneBounds.zoneLow = m_zoneBounds.zoneHigh - m_tradeConfig.zoneRecoverySpan; //--- Set zone low
      m_tradeConfig.accumulatedBuyVolume = m_tradeConfig.initialVolume; //--- Set buy volume
      m_tradeConfig.accumulatedSellVolume = 0.0;                    //--- Reset sell volume
   } else {                                                         //--- Handle sell position
      m_zoneBounds.zoneLow = m_tradeConfig.openPrice;               //--- Set zone low
      m_zoneBounds.zoneHigh = m_zoneBounds.zoneLow + m_tradeConfig.zoneRecoverySpan; //--- Set zone high
      m_tradeConfig.accumulatedSellVolume = m_tradeConfig.initialVolume; //--- Set sell volume
      m_tradeConfig.accumulatedBuyVolume = 0.0;                     //--- Reset buy volume
   }
   m_zoneBounds.zoneTargetHigh = m_zoneBounds.zoneHigh + m_tradeConfig.zoneProfitSpan; //--- Set target high
   m_zoneBounds.zoneTargetLow = m_zoneBounds.zoneLow - m_tradeConfig.zoneProfitSpan; //--- Set target low
   m_tradeConfig.currentState = RUNNING;                            //--- Set state to running
   return true;                                                     //--- Return success
   //--- Position Activation End
}

//--- Tick Processing
void processTick() {
   //--- Tick Processing Start
   double askPrice = NormalizeDouble(getMarketAsk(), Digits());     //--- Get ask price
   double bidPrice = NormalizeDouble(getMarketBid(), Digits());     //--- Get bid price
   
   if (!isNewBar()) return;                                         //--- Exit if not new bar
   
   if (!CopyBuffer(m_handleRsi, 0, 0, 3, m_rsiBuffer)) {            //--- Load RSI data
      Print("Error loading RSI data. Reverting.");                  //--- Log RSI failure
      return;                                                       //--- Exit
   }
   
   if (!CopyBuffer(m_handleEnvUpper, 0, 0, 3, m_envUpperBandBuffer)) { //--- Load upper Envelopes
      Print("Error loading upper envelopes data. Reverting.");         //--- Log failure
      return;                                                          //--- Exit
   }
   
   if (!CopyBuffer(m_handleEnvLower, 1, 0, 3, m_envLowerBandBuffer)) { //--- Load lower Envelopes
      Print("Error loading lower envelopes data. Reverting.");         //--- Log failure
      return;                                                          //--- Exit
   }
   
   int ticket = 0;                                                     //--- Initialize ticket
   
   const int rsiOverbought = 70;                                       //--- Set RSI overbought level
   const int rsiOversold = 30;                                         //--- Set RSI oversold level
   
   if (m_rsiBuffer[1] < rsiOversold && m_rsiBuffer[2] > rsiOversold && m_rsiBuffer[0] < rsiOversold) { //--- Check buy signal
      if (askPrice > m_envUpperBandBuffer[0]) {                        //--- Confirm price above upper Envelopes
         if (!m_restrictMaxOrders || PositionsTotal() < m_maxOrders) { //--- Check position limit
            ticket = openOrder(ORDER_TYPE_BUY, 0, 0);                  //--- Open buy order
         }
      }
   } else if (m_rsiBuffer[1] > rsiOverbought && m_rsiBuffer[2] < rsiOverbought && m_rsiBuffer[0] > rsiOverbought) { //--- Check sell signal
      if (bidPrice < m_envLowerBandBuffer[0]) {                        //--- Confirm price below lower Envelopes
         if (!m_restrictMaxOrders || PositionsTotal() < m_maxOrders) { //--- Check position limit
            ticket = openOrder(ORDER_TYPE_SELL, 0, 0);                 //--- Open sell order
         }
      }
   }
   
   if (ticket > 0) {                                                //--- Check if trade opened
      if (activateTrade(ticket)) {                                  //--- Activate position
         Print("New position activated: Ticket=", ticket);          //--- Log activation
      } else {
         Print("Failed to activate new position: Ticket=", ticket); //--- Log failure
      }
   }
   //--- Tick Processing End
}

ここでは、MarketZoneTraderクラス内でポジション管理およびマーケットティック処理をおこなうactivateTrade関数とprocessTick関数を実装して、プログラム開発を進めます。まず、activateTrade関数では、指定されたticketの取引を有効化します。最初に、m_tradeConfig.currentStateをINACTIVEに設定し、ArrayResizeを使用してm_tradeConfig.activeTicketsをクリアし、チケットリストをリセットします。m_lossTracker.tradeLossTrackerを0.0にリセットした後、configureTradeにticketを渡して取引設定を行います。失敗した場合はfalseを返します。次に、storeTradeTicketでticketを保存します。買い取引の場合(m_tradeConfig.directionがORDER_TYPE_BUY)、m_zoneBounds.zoneHighをm_tradeConfig.openPriceに設定し、m_zoneBounds.zoneLowをm_tradeConfig.zoneRecoverySpanを引いて計算します。また、m_tradeConfig.accumulatedBuyVolumeをm_tradeConfig.initialVolumeに更新し、m_tradeConfig.accumulatedSellVolumeをリセットします。

売り取引の場合は、m_zoneBounds.zoneLowをm_tradeConfig.openPriceに設定し、m_tradeConfig.zoneRecoverySpanを加えてm_zoneBounds.zoneHighを算出し、ボリュームも同様に調整します。その後、m_zoneBounds.zoneTargetHighおよびm_zoneBounds.zoneTargetLowをm_tradeConfig.zoneProfitSpanで設定し、m_tradeConfig.currentStateをRUNNINGに変更してtrueを返します。

次に、processTick関数ではマーケットティックの処理をおこないます。まず、getMarketAskおよびgetMarketBidを使用してaskPriceとbidPriceを取得し、NormalizeDoubleとDigitsで正規化します。isNewBarがfalseを返した場合はリソース節約のため処理を終了します。次に、CopyBufferでインジケーターデータを読み込みます。m_handleRsiからm_rsiBuffer、m_handleEnvUpperからm_envUpperBandBuffer、m_handleEnvLowerからm_envLowerBandBufferへコピーし、失敗した場合はPrintでエラーを記録して処理を終了します。取引シグナルの判定では、rsiOverboughtを70、rsiOversoldを30に設定します。

m_rsiBufferが売られすぎを示し、かつaskPriceがm_envUpperBandBufferを上回る場合は、m_restrictMaxOrdersがfalse、またはPositionsTotalがm_maxOrders未満であればopenOrderで買い注文を発行します。逆に買われすぎの条件でbidPriceがm_envLowerBandBufferを下回る場合は売り注文を発行します。有効なticketが返された場合はactivateTradeを呼び出し、結果を操作ログにログ出力します。この関数をOnTickイベントハンドラで呼び出すことで、シグナル評価およびポジションの開始処理を実行できるようになります。

void OnTick() {
   //--- Tick Handling Start
   if (trader != NULL) {                                                 //--- Check trader existence
      trader.processTick();                                              //--- Process tick
   }
   //--- Tick Handling End
}

OnTickイベントハンドラでは、まずMarketZoneTraderクラスのインスタンスであるtraderポインタがNULLでないかを確認し、取引システムが初期化されていることを確認します。存在する場合は、traderのprocessTick関数を呼び出し、各マーケットティックを処理します。この処理では、ポジションの評価、インジケーターシグナルの確認、および必要に応じた取引の実行がおこなわれます。コンパイルすると、次の結果が得られます。

初期位置

画像から、シグナルを検出し評価したうえで、買いポジションを開始したことが確認できます。次におこなうべきは、ポジションを管理することです。これについては、処理をモジュール化するために専用の関数内で管理します。

//--- Market Tick Evaluation
void evaluateMarketTick() {
   //--- Tick Evaluation Start
   if (m_tradeConfig.currentState == INACTIVE) return;              //--- Exit if inactive
   if (m_tradeConfig.currentState == TERMINATING) {                 //--- Check terminating state
      finalizePosition();                                           //--- Finalize position
      return;                                                       //--- Exit
   }
}

ここでは、MarketZoneTraderクラス内にevaluateMarketTick関数を実装し、アクティブな取引に対するマーケット状況を評価します。まず、m_tradeConfig.currentStateがINACTIVEかどうかを確認し、取引がアクティブでない場合は不要な処理を避けるために即座に終了します。次に、m_tradeConfig.currentStateがTERMINATINGかどうかを確認します。TERMINATINGの場合は、finalizePosition関数を呼び出してすべてのオープンポジションをクローズし、取引サイクルを完了させた後に終了します。取引を終了する関数は次のとおりです。

//--- Position Finalization
bool finalizePosition() {
   //--- Position Finalization Start
   m_tradeConfig.currentState = TERMINATING;                        //--- Set terminating state
   TradeMetrics metrics = {true, 0.0, 0.0};                         //--- Initialize metrics
   closeActiveTrades(metrics);                                       //--- Close all trades
   if (metrics.operationSuccess) {                                  //--- Check success
      ArrayResize(m_tradeConfig.activeTickets, 0);                  //--- Clear tickets
      m_tradeConfig.currentState = INACTIVE;                        //--- Set inactive state
      Print("Position closed successfully");                        //--- Log success
   } else {
      Print("Failed to close position");                            //--- Log failure
   }
   return metrics.operationSuccess;                                 //--- Return status
   //--- Position Finalization End
}

まず、m_tradeConfig.currentStateをTERMINATINGに設定し、取引サイクルが終了することを示します。これにより、取引をクローズする途中で管理サイクルが実行されることを防ぎます。次に、TradeMetrics 構造体のmetricsを初期化します。operationSuccessをtrue、totalVolumeを0.0、netProfitLossを0.0に設定し、クローズ処理の結果を追跡できるようにします。その後、closeActiveTradesにmetricsを渡して、m_tradeConfig.activeTicketsに登録されているすべてのポジションをクローズします。metrics.operationSuccessがtrueのままであれば、ArrayResizeを使用してm_tradeConfig.activeTicketsをクリアし、チケットリストをリセットします。また、m_tradeConfig.currentStateをINACTIVEに設定してシステムをアイドル状態にし、Printで成功をログ出力します。

クローズに失敗した場合は、Printで失敗を記録します。最後に、metrics.operationSuccessを返し、処理が正常に完了したかどうかを示します。ここで取引がクローズされていない場合は、ポジションクローズ処理中ではないため、次に価格がリカバリーゾーンやターゲットレベルに到達したかどうかを評価する処理に進むことができます。まずは買いポジションの評価から開始します。

double currentPrice;                                             //--- Initialize price
if (m_tradeConfig.direction == ORDER_TYPE_BUY) {                 //--- Handle buy position
   currentPrice = getMarketBid();                                //--- Get bid price
   if (currentPrice > m_zoneBounds.zoneTargetHigh) {             //--- Check profit target
      Print("Closing position: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh); //--- Log closure
      finalizePosition();                                        //--- Close position
      return;                                                    //--- Exit
   } else if (currentPrice < m_zoneBounds.zoneLow) {             //--- Check recovery trigger
      Print("Triggering recovery trade: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow); //--- Log recovery
      triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice);       //--- Open sell recovery
   }
}

次に、MarketZoneTraderクラスのevaluateMarketTick関数内で、買いポジションを管理するロジックを実装します。まず、現在の市場価格を格納するためにcurrentPriceを宣言します。m_tradeConfig.directionがORDER_TYPE_BUYの場合、currentPriceをgetMarketBid関数で取得します。これは、買いポジションをクローズする際に使用できる価格です。次に、currentPriceがm_zoneBounds.zoneTargetHighを上回るかを確認します。上回っていれば、Printでクローズ処理をログに記録し、入札価格とターゲットを表示した後、finalizePositionを呼び出して取引をクローズし、returnで処理を終了します。

一方、currentPriceがm_zoneBounds.zoneLowを下回った場合は、Printでリカバリートリガーをログに出力し、triggerRecoveryTradeをORDER_TYPE_SELLおよびcurrentPriceを引数として呼び出し、損失を軽減するために売り取引を発動します。このロジックにより、利益の出ている買いポジションは適切にクローズし、損失の出ているポジションにはリカバリー取引を迅速に開始できるようになります。リカバリー取引を開始する関数のロジックは次のとおりです。

//--- Recovery Trade Handling
void triggerRecoveryTrade(ENUM_ORDER_TYPE tradeDirection, double price) {
   //--- Recovery Trade Start
   TradeMetrics metrics = {true, 0.0, 0.0};                         //--- Initialize metrics
   closeActiveTrades(metrics);                                      //--- Close existing trades
   for (int i = 0; i < 10 && !metrics.operationSuccess; i++) {      //--- Retry closure
      Sleep(1000);                                                  //--- Wait 1 second
      metrics.operationSuccess = true;                              //--- Reset success flag
      closeActiveTrades(metrics);                                   //--- Retry closure
   }
   m_lossTracker.tradeLossTracker += metrics.netProfitLoss;         //--- Update loss tracker
   if (m_lossTracker.tradeLossTracker > 0 && metrics.operationSuccess) { //--- Check positive profit
      Print("Closing position due to positive profit: ", m_lossTracker.tradeLossTracker); //--- Log closure
      finalizePosition();                                           //--- Close position
      m_lossTracker.tradeLossTracker = 0.0;                         //--- Reset loss tracker
      return;                                                       //--- Exit
   }
   double tradeSize = determineRecoverySize(tradeDirection);        //--- Calculate trade size
   ulong ticket = openMarketTrade(tradeDirection, tradeSize, price); //--- Open recovery trade
   if (ticket > 0) {                                                //--- Check if trade opened
      storeTradeTicket(ticket);                                     //--- Store ticket
      m_tradeConfig.direction = tradeDirection;                     //--- Update direction
      if (tradeDirection == ORDER_TYPE_BUY) m_tradeConfig.accumulatedBuyVolume += tradeSize; //--- Update buy volume
      else m_tradeConfig.accumulatedSellVolume += tradeSize;        //--- Update sell volume
      Print("Recovery trade opened: Ticket=", ticket, ", Direction=", EnumToString(tradeDirection), ", Volume=", tradeSize); //--- Log recovery trade
   }
   //--- Recovery Trade End
}

//--- Recovery Size Calculation
double determineRecoverySize(ENUM_ORDER_TYPE tradeDirection) {
   //--- Recovery Size Calculation Start
   double tradeSize = -m_lossTracker.tradeLossTracker / m_tradeConfig.zoneProfitSpan; //--- Calculate lot size
   tradeSize = MathCeil(tradeSize / getMarketVolumeStep()) * getMarketVolumeStep(); //--- Round to volume step
   return tradeSize;                                                //--- Return trade size
   //--- Recovery Size Calculation End
}

マーケットがリカバリー取引をトリガーする必要がある場合に対応するため、まずtriggerRecoveryTrade関数から開始し、ポジションが不利に動いた際のリカバリー取引を処理します。まずTradeMetrics構造体 をmetricsという名前で初期化し、operationSuccessをtrue、totalVolumeを0.0、netProfitLossを0.0に設定します。その後closeActiveTradesをmetricsとともに呼び出し、既存のポジションをクローズします。metrics.operationSuccessがfalseの場合は、最大10回まで再試行し、各試行前にSleepで1秒待機し、operationSuccessをリセットします。

次にm_lossTracker.tradeLossTrackerにmetrics.netProfitLossを加算して更新します。m_lossTracker.tradeLossTrackerが正の値で、かつmetrics.operationSuccessがtrueの場合は、Printでクローズ情報をログに記録し、finalizePositionを呼び出し、m_lossTracker.tradeLossTrackerを0.0にリセットした上でreturnで処理を終了します。それ以外の場合は、determineRecoverySizeを使用してリカバリー取引のtradeSizeを計算し、その後openMarketTradeでtradeDirection、tradeSize、priceを指定して新規取引をオープンします。

返却されたticketが有効であれば、storeTradeTicketで保存し、m_tradeConfig.directionを更新します。またtradeDirectionに応じてm_tradeConfig.accumulatedBuyVolumeもしくはm_tradeConfig.accumulatedSellVolumeを調整し、EnumToStringを用いてPrintで取引内容をログに記録します。次にリカバリー取引のロットサイズを計算するdetermineRecoverySize関数を作成します。ここでは損失をカバーするために負の値のm_lossTracker.tradeLossTrackerをm_tradeConfig.zoneProfitSpanで割りtradeSizeを算出します。その後MathCeilとgetMarketVolumeStepを用いてブローカーの最小取引単位に合わせてtradeSizeを切り上げ、結果を返します。これによりリカバリー取引の処理が完了し、売りゾーンの処理ロジックに進むことができます。売りゾーンのロジックは買いの逆の処理となるためここでは詳細には触れません。最終的な完全な関数は次のようになります。

//--- Market Tick Evaluation
void evaluateMarketTick() {
   //--- Tick Evaluation Start
   if (m_tradeConfig.currentState == INACTIVE) return;              //--- Exit if inactive
   if (m_tradeConfig.currentState == TERMINATING) {                 //--- Check terminating state
      finalizePosition();                                           //--- Finalize position
      return;                                                       //--- Exit
   }
   double currentPrice;                                             //--- Initialize price
   if (m_tradeConfig.direction == ORDER_TYPE_BUY) {                 //--- Handle buy position
      currentPrice = getMarketBid();                                //--- Get bid price
      if (currentPrice > m_zoneBounds.zoneTargetHigh) {             //--- Check profit target
         Print("Closing position: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh); //--- Log closure
         finalizePosition();                                        //--- Close position
         return;                                                    //--- Exit
      } else if (currentPrice < m_zoneBounds.zoneLow) {             //--- Check recovery trigger
         Print("Triggering recovery trade: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow); //--- Log recovery
         triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice);       //--- Open sell recovery
      }
   } else if (m_tradeConfig.direction == ORDER_TYPE_SELL) {         //--- Handle sell position
      currentPrice = getMarketAsk();                                //--- Get ask price
      if (currentPrice < m_zoneBounds.zoneTargetLow) {              //--- Check profit target
         Print("Closing position: Ask=", currentPrice, " < TargetLow=", m_zoneBounds.zoneTargetLow); //--- Log closure
         finalizePosition();                                        //--- Close position
         return;                                                    //--- Exit
      } else if (currentPrice > m_zoneBounds.zoneHigh) {            //--- Check recovery trigger
         Print("Triggering recovery trade: Ask=", currentPrice, " > ZoneHigh=", m_zoneBounds.zoneHigh); //--- Log recovery
         triggerRecoveryTrade(ORDER_TYPE_BUY, currentPrice);        //--- Open buy recovery
      }
   }
   //--- Tick Evaluation End
}

この関数は、リカバリーのためのすべての指示を処理するようになりました。コンパイルすると、次の結果が得られます。

最終結果

画像から、トレンドバウンスシグナルによってトリガーされたポジションを正常に処理できていることが確認できます。残っている作業は、このプログラムのバックテストをおこなうことです。バックテストについては次のセクションで扱います。


バックテスト

徹底的なバックテストの結果、次の結果が得られました。

バックテストグラフ

グラフ

バックテストレポート

レポート


結論

結論として、今回私たちはEnvelopes Trend取引のためのZone Recoveryシステムを実装した堅牢なMQL5プログラムを構築しました。このプログラムはRSIとEnvelopesインジケーターを組み合わせて取引機会を特定し、構造化されたリカバリーゾーンを通じて損失を管理します。またオブジェクト指向プログラミング(OOP)アプローチを採用しています。MarketZoneTraderクラスのようなコンポーネント、TradeConfigやZoneBoundariesといった構造体、processTickやtriggerRecoveryTradeといった関数を用いることで、柔軟なシステムを作成しました。このシステムはzoneTargetPointsやriskPercentageなどのパラメータを調整することで、さまざまな市場条件に適応させることが可能です。

免責条項:本記事は教育目的のみを意図したものです。取引は大きな財務リスクを伴い、市場のボラティリティにより損失が発生する可能性があります。プログラムを実際の市場で運用する前に、十分なバックテストと慎重なリスク管理が不可欠です。

この記事で構築した基盤をもとに、ゾーンリカバリーシステムをさらに改良したり、そのロジックを応用して新たな取引戦略を開発することが可能です。アルゴリズムトレーディングの進歩に役立ててください。取引をお楽しみください。

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

最後のコメント | ディスカッションに移動 (2)
Sabrina Hellal
Sabrina Hellal | 9 7月 2025 において 13:44
本当にありがとう🙏。
Allan Munene Mutiiria
Allan Munene Mutiiria | 9 7月 2025 において 16:46
Sabrina Hellal #:
本当にありがとうᙏ。

大歓迎です。ありがとうございます。

知っておくべきMQL5ウィザードのテクニック(第73回):一目均衡表とADX-Wilderのパターンの利用 知っておくべきMQL5ウィザードのテクニック(第73回):一目均衡表とADX-Wilderのパターンの利用
一目均衡表とADX-Wilderオシレーターは、MQL5のエキスパートアドバイザー(EA)内で補完的に使用できる組み合わせです。一目均衡表は多機能な指標ですが、本記事では主にサポート・レジスタンス(S/R)レベルを定義する目的で使用します。一方、ADXはトレンドの判定に使用します。通常通り、MQL5ウィザードを用いて構築し、両者が持つ潜在能力をテストします。
共和分株式による統計的裁定取引(第1回):エングル=グレンジャーおよびジョハンセンの共和分検定 共和分株式による統計的裁定取引(第1回):エングル=グレンジャーおよびジョハンセンの共和分検定
本記事は、トレーダー向けに、最も一般的な共和分検定を優しく紹介し、その結果の理解方法を簡単に解説することを目的としています。エングル=グレンジャーおよびジョハンセンの共和分検定は、長期的なダイナミクスを共有する統計的に有意な資産のペアやグループを特定するのに有効です。特にジョハンセン検定は、3つ以上の資産を含むポートフォリオに対して有用で、複数の共和分ベクトルの強さを一度に評価できます。
MQL5 Algo Forgeへの移行(第4回):バージョンとリリースの操作 MQL5 Algo Forgeへの移行(第4回):バージョンとリリースの操作
SimpleCandlesプロジェクトおよびAdwizardプロジェクトの開発を継続しつつ、MQL5 Algo Forgeのバージョン管理システムおよびリポジトリのより詳細な活用方法についても説明していきます。
MQL5で他の言語の実用的なモジュールを実装する(第1回):Pythonにヒントを得たSQLite3ライブラリの構築 MQL5で他の言語の実用的なモジュールを実装する(第1回):Pythonにヒントを得たSQLite3ライブラリの構築
Pythonのsqlite3モジュールは、SQLiteデータベースを扱うためのシンプルで高速かつ便利な方法を提供しています。本記事では、MQL5に組み込まれているデータベース操作用の関数群を活用し、Pythonのsqlite3モジュールと同様の操作感でSQLite3データベースを扱える独自モジュールを構築します。