MQL5での取引戦略の自動化(第22回):Envelopes Trend取引のためのZone Recoveryシステムの作成
はじめに
前回の記事(第21回)では、MetaQuotes Language 5 (MQL5)でニューラルネットワークを活用した取引戦略に、適応学習率を組み合わせ、市場の動きの予測精度を向上させる方法を検討しました。第22回では、Envelopes Trend取引戦略と統合されたZone Recoveryシステムの作成に焦点を移します。RSIとエンベロープ指標を組み合わせることで、取引の自動化と損失管理を効率的におこなえるシステムを設計します。本記事では以下のトピックを扱います。
これにより、変動の激しい市場環境に対応可能な、堅牢で信頼性の高い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)が市場とスムーズに連携し、特に注文の発注処理を正確におこなうためのツールを提供するからです。ファイルを開く方法については以下を参照してください。

次に、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関数はSymbolInfoDoubleをSYMBOL_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関数を作成します。最初に、AccountInfoDoubleをACCOUNT_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
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
知っておくべきMQL5ウィザードのテクニック(第73回):一目均衡表とADX-Wilderのパターンの利用
共和分株式による統計的裁定取引(第1回):エングル=グレンジャーおよびジョハンセンの共和分検定
MQL5 Algo Forgeへの移行(第4回):バージョンとリリースの操作
MQL5で他の言語の実用的なモジュールを実装する(第1回):Pythonにヒントを得たSQLite3ライブラリの構築
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
本当にありがとうᙏ。
大歓迎です。ありがとうございます。