English Deutsch
preview
MQL5における取引戦略の自動化(第47回):ヘッジ機能を備えたNick Rypock Trailing Reverse (NRTR)

MQL5における取引戦略の自動化(第47回):ヘッジ機能を備えたNick Rypock Trailing Reverse (NRTR)

MetaTrader 5トレーディング |
40 3
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の記事(第46回)では、MetaQuotes Language 5 (MQL5)を使用して、流動性スイープ取引システムを開発しました。このシステムでは流動性ゾーンを特定し、Break of Structure (BoS)を検出し、カスタマイズ可能なリスクパラメータと視覚的インジケータを用いて取引を実行します。第47回では、Nick Rypock Trailing Reverse (NRTR)システムを構築します。このシステムは、チャネルベースの反転シグナルを利用してトレンドフォロー型のエントリーを実行し、さらにヘッジ機能、動的トレーリングストップ、および各種リスク管理機能を統合します。本記事では以下のトピックを扱います。

  1. Nick Rypock Trailing Reverse (NRTR)戦略とその構成要素の考察
  2. MQL5での実装
  3. バックテスト
  4. 結論

記事を読み終える頃には、ヘッジ機能を備えたNRTRベースのトレンド転換売買システムを実装したMQL5プログラムが完成し、自由に拡張およびカスタマイズできる状態になります。それでは始めましょう。


Nick Rypock Trailing Reverse (NRTR)戦略とその構成要素の考察

Nick Rypock Trailing Reverse (NRTR)戦略は、NRTRチャネル内における動的なサポートレベルの形成を追跡することで、潜在的なトレンド反転を検出します。NRTRチャネルは、ATRの値にユーザー指定の係数を乗じて算出された上下の境界線によって構成され、市場ボラティリティに応じて動的に変化します。エントリーシグナルは、新たなサポートレベルが出現した際に生成されます。買いサポート(ロングチャネル)の開始時には買いポジションを、売りサポート(ショートチャネル)の開始時には売りポジションを建てることで、新たに形成されるトレンドを追従します。ヘッジ機能を有効にした場合は、買いポジションと売りポジションを同時に保有できます。リスク管理機能としては、口座残高、エクイティ、または余剰証拠金に応じてロットサイズを自動計算し、1回の取引あたりのリスクを制限します。また、固定値またはATRベースのストップロス/テイクプロフィット設定により、市場ボラティリティに応じた保護機能を提供します。

本システムでは、MQL5で入手可能な無料のNRTRチャネルインジケータを利用し、サポートレベルの切り替わりを検出して売買シグナルを生成します。さらに、ポジション数制限およびヘッジ設定を考慮したエントリーを管理をし、リスクパラメータに基づいてロットサイズやストップ水準を動的に算出します。また、トレーリングストップ機能および仮想決済機能を実装することで、保有ポジションを継続的に監視します。その結果、トレンド転換を捉える能力と厳格なリスク管理を両立した反転売買システムを実現します。要するに、本フレームワークはNick Rypock Trailing Reverse (NRTR)の考え方を中心としたボラティリティ適応型の取引手法を提供し、柔軟なリスク管理機能によって一貫した運用を可能にします。以下は、この戦略の構成を示した図です。

NRTRフレームワーク


MQL5での実装

MQL5でプログラムを作成するには、まずMetaEditorを開き、ナビゲーターで[Experts]フォルダを探します。[新規]タブをクリックして指示に従い、ファイルを作成します。ファイルが作成されたら、コーディング環境で、まずプログラム全体で使用する入力パラメータグローバル変数をいくつか宣言する必要があります。

//+------------------------------------------------------------------+
//|                     NRTR - Nick Rypock Trailing Reverse - EA.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"

#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\SymbolInfo.mqh>

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ENUM_RISK_BASE {
   RISK_BASE_EQUITY    = 1, // Equity
   RISK_BASE_BALANCE   = 2, // Balance
   RISK_BASE_FREEMARGIN = 3 // Free Margin
};

enum ENUM_RISK_DEFAULT_SIZE {
   RISK_DEFAULT_FIXED = 1, // Fixed
   RISK_DEFAULT_AUTO  = 2  // Auto
};

enum ENUM_MODE_SL {
   SL_FIXED = 0, // Fixed
   SL_AUTO  = 1  // Auto
};

enum ENUM_MODE_TP {
   TP_FIXED = 0, // Fixed
   TP_AUTO  = 1  // Auto
};

enum ENUM_TRAILING_TYPE {
   TRAILING_NONE          = 0, // None
   TRAILING_POINTS        = 1, // Points
   TRAILING_SUPPORT_LEVELS = 2 // Support Levels
};

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "Risk Management Settings"
input ENUM_RISK_DEFAULT_SIZE RiskDefaultSize = RISK_DEFAULT_FIXED; // Default Risk Size
input double DefaultLotSize                  = 0.01;               // Default Lot Size
input ENUM_RISK_BASE RiskBase                = RISK_BASE_BALANCE;  // Risk Base
input int MaxRiskPerTrade                    = 5;                  // Max Risk Per Trade (%)
double MinLotSize                            = 0.01;               // Min Lot Size
double MaxLotSize                            = 100;                // Max Lot Size
input int MaxPositions                       = 1;                  // Max Positions
input bool HedgeMode                         = true;               // Hedge Mode
bool CloseOnReversalSignal                   = false;              // Close On Reversal Signal

input group "Stop-Loss and Take-Profit Settings"
input int DefaultStopLossPoints              = 300;                // Default Stop Loss (Points)
input int DefaultTakeProfitPoints            = 300;                // Default Take Profit (Points)
input bool CloseOnStopLossHit                = false;              // Close On Stop Loss Hit
input bool CloseOnTakeProfitHit              = false;              // Close On Take Profit Hit

input group "Additional Settings"
input int MagicNumber                        = 1234567890;         // Magic Number

input group "Trailing Stop Loss"
input ENUM_TRAILING_TYPE TrailingType        = TRAILING_POINTS;    // Trailing Type
input ushort TrailingFrequencySeconds        = 10;                 // Trailing Frequency (Seconds)
input ushort SignalCheckFrequencySeconds     = 10;                 // Signal Check Frequency (Seconds)
input ushort TrailingStopPoints              = 120;                // Trailing Stop (Points)
input ushort TrailingStepPoints              = 100;                // Trailing Step (Points)
input ushort BreakEvenPoints                 = 10;                 // Break Even (Points)
input ushort BreakEvenTriggerPoints          = 30;                 // Break Even Trigger (Points)

input group "NRTR Channel Settings"
input int NRTR_ATR_Period                    = 40;                 // NRTR ATR Period
input double NRTR_Multiplier                 = 2.0;                // NRTR Multiplier
input bool NRTR_Show_Price_Label             = true;               // Show Price Label

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
double CurrentStopLossPoints = DefaultStopLossPoints;              //--- Initialize current SL points
double CurrentTakeProfitPoints = DefaultTakeProfitPoints;          //--- Initialize current TP points
double CurrentTrailingStopPoints = TrailingStopPoints;             //--- Initialize current trailing stop points
double CurrentTrailingStepPoints = TrailingStepPoints;             //--- Initialize current trailing step points
bool PrintLog = false;                                             //--- Set print log flag
datetime LastTrailingTime = 0;                                     //--- Initialize last trailing time
ENUM_MODE_SL StopLossMode = SL_FIXED;                              //--- Set SL mode
ENUM_MODE_TP TakeProfitMode = TP_FIXED;                            //--- Set TP mode

double AtrMultiplierForStopLoss = 2;                               //--- Set ATR multiplier for SL
double AtrMultiplierForTakeProfit = 3;                             //--- Set ATR multiplier for TP

CTrade TradeObject;                                                //--- Declare trade object
CSymbolInfo SymbolInfo;                                            //--- Declare symbol info
int TrendIndicatorHandle = -1;                                     //--- Initialize trend handle
double CurrentTrendValue, PreviousTrendValue;                      //--- Declare trend values

double CurrentTrendDirection, PreviousTrendDirection;              //--- Declare trend directions
double CurrentUpSupport, PreviousUpSupport;                        //--- Declare up supports
double CurrentDownSupport, PreviousDownSupport;                    //--- Declare down supports
double CurrentNrtrAtr, PreviousNrtrAtr;                            //--- Declare NRTR ATR values

まず、取引操作、ポジション情報、および銘柄情報を扱うためのクラスを利用できるようにするため、「#include <Trade\Trade.mqh>」、「#include <Trade\PositionInfo.mqh>」、「#include <Trade\SymbolInfo.mqh>」といった重要なMQL5ライブラリをインクルードします。次に、ユーザー設定を体系的に管理するために、いくつかの列挙型を定義します。ENUM_RISK_BASE列挙型では、リスク計算の基準として「エクイティ」「残高」「余剰証拠金」を選択できるようにします。また、ENUM_RISK_DEFAULT_SIZEを作成し、ロットサイズを「固定」または「自動」で設定できるようにします。ストップロスおよびテイクプロフィットについては、ENUM_MODE_SLとENUM_MODE_TPを定義し、それぞれ「固定モード」または「自動モード」を選択できるようにします。さらに、トレーリングストップの種類を指定するためにENUM_TRAILING_TYPEを用意し、「なし」「ポイントベース」「サポートレベル連動」の各方式を選択できるようにします。

続いて、ユーザー入力項目をグループ化し、設定の使いやすさを向上させます。リスク管理セクションでは、デフォルトのロットサイズ計算方式を指定するRiskDefaultSize、固定ロット値を設定するDefaultLotSize、リスク計算の基準を選択するRiskBase、1回の取引あたりの最大リスク率を設定するMaxRiskPerTradeを用意します。また、最小ロットサイズと最大ロットサイズはグローバル変数として定義し、同時保有可能なポジション数を制限するMaxPositions、ヘッジを許可するHedgeMode、および反対シグナル発生時にポジションを決済するCloseOnReversalSignalを追加します。ストップロスおよびテイクプロフィットの設定では、ポイント単位で指定するDefaultStopLossPointsとDefaultTakeProfitPointsを用意し、それぞれのレベル到達時に自動決済するかどうかを設定するブール型オプションも追加します。

追加設定セクションでは、取引を識別するための一意の番号としてMagicNumberを設定します。また、トレーリングストップ関連のパラメータとして、TrailingType、トレーリング更新間隔(秒)、シグナル確認間隔(秒)、TrailingStopPoints、TrailingStepPoints、BreakEvenPoints、BreakEvenTriggerPointsを用意します。NRTRチャネル設定グループでは、インジケータの動作をプログラムから直接カスタマイズできるように、NRTR_ATR_Period、NRTR_Multiplier、NRTR_Show_Price_Labelを設定項目として追加します。

最後に、主要な値を管理するためのグローバル変数を宣言します。これには、現在使用するストップロス値やトレーリングストップ値、ログ出力フラグ、最後にトレーリング処理を実行した時刻、ストップロスおよびテイクプロフィットのモード、動的レベル計算用のATR倍率などが含まれます。また、CTradeおよびCSymbolInfoクラスのインスタンスも生成します。さらに、トレンドインジケータのハンドルは初期値として-1に設定し、現在および直前のトレンド値、方向、サポートレベル、NRTRのATR値を保持するためのdouble型変数を宣言します。次のステップでは、インジケータの初期化処理を実装します。コード全体の可読性、保守性、および拡張性を高めるため、必要に応じて関数を作成し、機能ごとに処理を分割したモジュール構造で実装を進めていきます。

//+------------------------------------------------------------------+
//| Initialize expert                                                |
//+------------------------------------------------------------------+
int OnInit() {
   if (!PreInitChecks()) return INIT_FAILED;                     //--- Perform pre-init checks
   SymbolInfo.Name(Symbol());                                    //--- Set symbol name
   if (!InitIndicatorHandles()) return INIT_FAILED;              //--- Initialize indicator handles
   InitTradeObject();                                            //--- Initialize trade object
   return INIT_SUCCEEDED;                                        //--- Return success
}

//+------------------------------------------------------------------+
//| Perform pre-init checks                                          |
//+------------------------------------------------------------------+
bool PreInitChecks() {
   if (MaxLotSize < MinLotSize) {                                //--- Check lot sizes
      Print("MaxLotSize cannot be less than MinLotSize");        //--- Print error
      return false;                                              //--- Return failure
   }
   return true;                                                  //--- Return success
}

//+------------------------------------------------------------------+
//| Initialize indicator handles                                     |
//+------------------------------------------------------------------+
bool InitIndicatorHandles() {
   TrendIndicatorHandle = iCustom(_Symbol, _Period, "Free Indicators\\NRTR Channel", NRTR_ATR_Period, NRTR_Multiplier, NRTR_Show_Price_Label); //--- Create NRTR handle
   if (TrendIndicatorHandle == INVALID_HANDLE) {                 //--- Check handle
      PrintFormat("Error creating NRTR handle - %d", GetLastError()); //--- Print error
      return false;                                              //--- Return failure
   }
   return true;                                                  //--- Return success
}

//+------------------------------------------------------------------+
//| Initialize trade object                                          |
//+------------------------------------------------------------------+
void InitTradeObject() {
   TradeObject.SetExpertMagicNumber(MagicNumber);                //--- Set magic number
}

OnInitイベントハンドラでは、まずPreInitChecksを呼び出して初期条件を検証します。このチェックに失敗した場合はINIT_FAILEDを返し、不正な設定のままプログラムが続行されないようにします。次に、現在の銘柄を使用してSymbolInfoオブジェクトに銘柄名を設定します。その後、InitIndicatorHandlesを呼び出してNRTRチャネルインジケータを読み込み、エラーが発生した場合はINIT_FAILEDを返します。続いて、InitTradeObjectを呼び出して取引オブジェクトを初期化し、最後にINIT_SUCCEEDEDを返して初期化が正常に完了したことを示します。

使用した関数について説明します。PreInitChecks関数は、MaxLotSizeがMinLotSize未満でないことを確認します。この条件に違反している場合はエラーメッセージを出力してfalseを返し、問題がなければtrueを返して初期化処理を続行できるようにします。InitIndicatorHandles内では、銘柄、時間足、インジケータパス、およびNRTR_ATR_Period、NRTR_Multiplier、NRTR_Show_Price_Labelなどのパラメータを指定して、iCustomを使用してTrendIndicatorHandleを作成します。ハンドルがINVALID_HANDLEの場合は、GetLastErrorを使用してエラー内容を出力し、falseを返します。それ以外の場合はtrueを返します。InitTradeObject関数では、プログラム固有の取引を識別するために、TradeObjectのマジックナンバーをMagicNumberに設定します。シグナルの主な生成元であるインジケータが正常に読み込まれていることを確認するため、プログラムを実行して検証することが非常に重要です。

プログラムへの初期インジケータの読み込み

今回の場合、インジケータは正常に読み込まれています。これでインジケータバッファの読み取りとシグナル生成ロジックの定義を進めることができます。まずはデータ取得ロジックから始めます。

//+------------------------------------------------------------------+
//| Fetch indicator data                                             |
//+------------------------------------------------------------------+
bool FetchIndicatorData() {
   double atrBuffer[];                                           //--- Declare ATR buffer as dynamic
   double trendBuffer[];                                         //--- Declare trend buffer as dynamic
   double upSupportBuffer[];                                     //--- Declare up support buffer as dynamic
   double downSupportBuffer[];                                   //--- Declare down support buffer as dynamic
   ArrayResize(atrBuffer, 2);                                    //--- Resize to 2
   ArrayResize(trendBuffer, 2);                                  //--- Resize to 2
   ArrayResize(upSupportBuffer, 2);                              //--- Resize to 2
   ArrayResize(downSupportBuffer, 2);                            //--- Resize to 2
   ArraySetAsSeries(atrBuffer, true);                            //--- Set as series
   ArraySetAsSeries(trendBuffer, true);                          //--- Set as series
   ArraySetAsSeries(upSupportBuffer, true);                      //--- Set as series
   ArraySetAsSeries(downSupportBuffer, true);                    //--- Set as series
   int copyCount;                                                //--- Declare copy count
   bool dataReady = false;                                       //--- Set data ready flag
   int maxAttempts = 5;                                          //--- Set max attempts
   int delayMs = 200;                                            //--- Set delay ms
   int attempt = 0;                                              //--- Initialize attempt
   while (!dataReady && attempt < maxAttempts) {                 //--- Loop until ready
      dataReady = true;                                          //--- Assume ready
      copyCount = CopyBuffer(TrendIndicatorHandle, 5, 1, 2, atrBuffer); //--- Copy ATR
      if (copyCount < 2 || atrBuffer[0] == EMPTY_VALUE) {        //--- Check ATR copy
         dataReady = false;                                      //--- Set not ready
      } else {                                                   //--- Set ATR values
         CurrentNrtrAtr = atrBuffer[0];                          //--- Set current ATR (recent)
         PreviousNrtrAtr = atrBuffer[1];                         //--- Set previous ATR (older)
      }
      copyCount = CopyBuffer(TrendIndicatorHandle, 4, 1, 2, trendBuffer); //--- Copy trend
      if (copyCount < 2) {                                       //--- Check trend copy
         dataReady = false;                                      //--- Set not ready
      } else {                                                   //--- Set trend directions
         CurrentTrendDirection = trendBuffer[0];                 //--- Set current direction (recent)
         PreviousTrendDirection = trendBuffer[1];                //--- Set previous direction (older)
      }
      copyCount = CopyBuffer(TrendIndicatorHandle, 1, 1, 2, upSupportBuffer); //--- Copy up support
      if (copyCount < 2) {                                       //--- Check up copy
         dataReady = false;                                      //--- Set not ready
      } else {                                                   //--- Set up supports
         CurrentUpSupport = upSupportBuffer[0];                  //--- Set current up (recent)
         PreviousUpSupport = upSupportBuffer[1];                 //--- Set previous up (older)
      }
      copyCount = CopyBuffer(TrendIndicatorHandle, 2, 1, 2, downSupportBuffer); //--- Copy down support
      if (copyCount < 2) {                                       //--- Check down copy
         dataReady = false;                                      //--- Set not ready
      } else {                                                   //--- Set down supports
         CurrentDownSupport = downSupportBuffer[0];              //--- Set current down (recent)
         PreviousDownSupport = downSupportBuffer[1];             //--- Set previous down (older)
      }
      if (dataReady) {                                           //--- Check ready
         CurrentTrendValue = (CurrentTrendDirection > 0) ? CurrentUpSupport : ((CurrentTrendDirection < 0) ? CurrentDownSupport : EMPTY_VALUE); //--- Set current trend value
         PreviousTrendValue = (PreviousTrendDirection > 0) ? PreviousUpSupport : ((PreviousTrendDirection < 0) ? PreviousDownSupport : EMPTY_VALUE); //--- Set previous trend value
         if (CurrentTrendValue == EMPTY_VALUE || PreviousTrendValue == EMPTY_VALUE) dataReady = false; //--- Check values
      }
      attempt++;                                                 //--- Increment attempt
      Sleep(delayMs);                                            //--- Delay
   }
   if (!dataReady) {                                             //--- Check final ready
      Print("Failed to fetch indicator data");                   //--- Print error
      return false;                                              //--- Return failure
   }
   return true;                                                  //--- Return success
}

FetchIndicatorData関数を定義し、NRTRチャネルインジケータのバッファから値を取得して保存します。まず、atrBuffer、trendBuffer、upSupportBuffer、downSupportBufferの動的配列を宣言します。各配列はArrayResizeを使用して2要素分のサイズに設定し、ArraySetAsSeriesを使用して時系列配列として設定することで、最新のデータがインデックス0に配置されるようにします。次に、コピー件数を格納する変数、データ準備完了フラグ、最大再試行回数を5回、待機時間を200ミリ秒、そして0から開始する試行回数カウンターを初期化します。データの準備が完了するか、再試行回数の上限に達するまで継続するwhileループ内で、まずデータが利用可能であると仮定します。その後、CopyBufferを使用して、インジケータの各バッファから直近2本分のバーのデータを取得します。ATR値にはバッファ5、トレンド方向にはバッファ4、上側サポートにはバッファ1、下側サポートにはバッファ2を使用します。

いずれかのコピー結果が2未満であった場合、または最新のATR値がEMPTY_VALUEであった場合は、データ未準備として扱います。それ以外の場合は、現在の値(最新、インデックス0)と前回の値(1本前、インデックス1)を、CurrentNrtrAtr、PreviousNrtrAtr、CurrentTrendDirection、PreviousTrendDirection、CurrentUpSupport、PreviousUpSupport、CurrentDownSupport、PreviousDownSupportといったグローバル変数に格納します。すべてのバッファのコピーが成功した場合は、トレンド方向が正であればCurrentTrendValueにCurrentUpSupportを、負であればCurrentDownSupportを、それ以外の場合はEMPTY_VALUEを設定します。同様に、PreviousTrendValueも設定します。いずれかのトレンド値がEMPTY_VALUEである場合は、データ未準備として再設定します。

その後、試行回数カウンターを増加させ、再試行前にSleepで待機します。再試行後もデータを取得できない場合は、エラーメッセージを出力してfalseを返します。データ取得に成功した場合は、trueを返して正常にデータを取得できたことを示します。データの取得後は、シグナル生成のための分析処理に進むことができます。まずは、いくつかのヘルパー関数を定義します。

//+------------------------------------------------------------------+
//| Count open positions                                             |
//+------------------------------------------------------------------+
int CountOpenPositions() {
   int count = 0;                                                //--- Initialize count
   int totalPositions = PositionsTotal();                        //--- Get total positions
   for (int i = 0; i < totalPositions; i++) {                    //--- Loop positions
      if (PositionGetSymbol(i) != Symbol()) continue;            //--- Skip wrong symbol
      if (MagicNumber != 0 && PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue; //--- Skip wrong magic
      count++;                                                   //--- Increment count
   }
   return count;                                                 //--- Return count
}

//+------------------------------------------------------------------+
//| Count open positions by type                                     |
//+------------------------------------------------------------------+
int CountOpenPositionsByType(ENUM_POSITION_TYPE positionType) {
   int count = 0;                                                //--- Initialize count
   int totalPositions = PositionsTotal();                        //--- Get total positions
   for (int i = 0; i < totalPositions; i++) {                    //--- Loop positions
      if (PositionGetSymbol(i) != Symbol()) continue;            //--- Skip wrong symbol
      if (MagicNumber != 0 && PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue; //--- Skip wrong magic
      if (PositionGetInteger(POSITION_TYPE) == positionType) count++; //--- Increment if match
   }
   return count;                                                 //--- Return count
}

//+------------------------------------------------------------------+
//| Open buy position                                                |
//+------------------------------------------------------------------+
bool OpenBuyPosition(string positionType) {
   double askPrice = SymbolInfoDouble(Symbol(), SYMBOL_ASK);               //--- Get ask price
   double stopLossPrice = CalculateStopLoss(ORDER_TYPE_BUY, askPrice);     //--- Calculate SL
   double takeProfitPrice = CalculateTakeProfit(ORDER_TYPE_BUY, askPrice); //--- Calculate TP
   double positionSize = CalculatePositionSize(stopLossPrice, askPrice);   //--- Calculate size
   string orderComment = positionType + " Buy";                            //--- Set comment
   if (!TradeObject.Buy(positionSize, Symbol(), 0, stopLossPrice, takeProfitPrice, orderComment)) { //--- Open buy
      PrintFormat("Failed to open BUY: %d", TradeObject.ResultRetcode());  //--- Print error
      return false;                                                        //--- Return failure
   }
   PrintFormat("%s Buy Position Opened Successfully", positionType);       //--- Print success
   return true;                                                            //--- Return success
}

//+------------------------------------------------------------------+
//| Open sell position                                               |
//+------------------------------------------------------------------+
bool OpenSellPosition(string positionType) {
   double bidPrice = SymbolInfoDouble(Symbol(), SYMBOL_BID);               //--- Get bid price
   double stopLossPrice = CalculateStopLoss(ORDER_TYPE_SELL, bidPrice);    //--- Calculate SL
   double takeProfitPrice = CalculateTakeProfit(ORDER_TYPE_SELL, bidPrice); //--- Calculate TP
   double positionSize = CalculatePositionSize(stopLossPrice, bidPrice);   //--- Calculate size
   string orderComment = positionType + " Sell";                           //--- Set comment
   if (!TradeObject.Sell(positionSize, Symbol(), 0, stopLossPrice, takeProfitPrice, orderComment)) { //--- Open sell
      PrintFormat("Failed to open SELL: %d", TradeObject.ResultRetcode()); //--- Print error
      return false;                                                        //--- Return failure
   }
   PrintFormat("%s Sell Position Opened Successfully", positionType);      //--- Print success
   return true;                                                            //--- Return success
}

//+------------------------------------------------------------------+
//| Close all buy positions                                          |
//+------------------------------------------------------------------+
void CloseAllBuyPositions() {
   int total = PositionsTotal();                                           //--- Get total positions
   for (int i = total - 1; i >= 0; i--) {                                  //--- Loop backward
      if (PositionGetSymbol(i) != Symbol()) continue;                      //--- Skip wrong symbol
      if (PositionGetInteger(POSITION_TYPE) != POSITION_TYPE_BUY) continue; //--- Skip non-buy
      if (PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue;     //--- Skip wrong magic
      TradeObject.PositionClose(PositionGetInteger(POSITION_TICKET));      //--- Close position
   }
   Print("All Buy Positions Closed");                                      //--- Print closed
}

//+------------------------------------------------------------------+
//| Close all sell positions                                         |
//+------------------------------------------------------------------+
void CloseAllSellPositions() {
   int total = PositionsTotal();                                           //--- Get total positions
   for (int i = total - 1; i >= 0; i--) {                                  //--- Loop backward
      if (PositionGetSymbol(i) != Symbol()) continue;                      //--- Skip wrong symbol
      if (PositionGetInteger(POSITION_TYPE) != POSITION_TYPE_SELL) continue; //--- Skip non-sell
      if (PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue;     //--- Skip wrong magic
      TradeObject.PositionClose(PositionGetInteger(POSITION_TICKET));      //--- Close position
   }
   Print("All Sell Positions Closed");                                     //--- Print closed
}

//+------------------------------------------------------------------+
//| Close all positions                                              |
//+------------------------------------------------------------------+
void CloseAllPositions() {
   int total = PositionsTotal();                                           //--- Get total positions
   for (int i = total - 1; i >= 0; i--) {                                  //--- Loop backward
      if (PositionGetSymbol(i) != Symbol()) continue;                      //--- Skip wrong symbol
      if (PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue;     //--- Skip wrong magic
      TradeObject.PositionClose(PositionGetInteger(POSITION_TICKET));      //--- Close position
   }
   Print("All Positions Closed");                                          //--- Print closed
}

//+------------------------------------------------------------------+
//| Calculate stop loss                                              |
//+------------------------------------------------------------------+
double CalculateStopLoss(ENUM_ORDER_TYPE orderType, double entryPrice) {
   double stopLossPrice = 0;                                               //--- Initialize SL price
   if (StopLossMode == SL_FIXED) {                                         //--- Check fixed mode
      if (DefaultStopLossPoints == 0) return 0;                            //--- Return zero if none
      double pointValue = SymbolInfoDouble(Symbol(), SYMBOL_POINT);        //--- Get point value
      if (orderType == ORDER_TYPE_BUY) stopLossPrice = entryPrice - DefaultStopLossPoints * pointValue; //--- Set buy SL
      if (orderType == ORDER_TYPE_SELL) stopLossPrice = entryPrice + DefaultStopLossPoints * pointValue; //--- Set sell SL
   } else {                                                                //--- Handle auto mode
      if (orderType == ORDER_TYPE_BUY) stopLossPrice = entryPrice - PreviousNrtrAtr * AtrMultiplierForStopLoss; //--- Set buy SL
      if (orderType == ORDER_TYPE_SELL) stopLossPrice = entryPrice + PreviousNrtrAtr * AtrMultiplierForStopLoss; //--- Set sell SL
   }
   return NormalizeDouble(stopLossPrice, _Digits);                         //--- Normalize SL
}

//+------------------------------------------------------------------+
//| Calculate take profit                                            |
//+------------------------------------------------------------------+
double CalculateTakeProfit(ENUM_ORDER_TYPE orderType, double entryPrice) {
   double takeProfitPrice = 0;                                             //--- Initialize TP price
   if (TakeProfitMode == TP_FIXED) {                                       //--- Check fixed mode
      if (DefaultTakeProfitPoints == 0) return 0;                          //--- Return zero if none
      double pointValue = SymbolInfoDouble(Symbol(), SYMBOL_POINT);        //--- Get point value
      if (orderType == ORDER_TYPE_BUY) takeProfitPrice = entryPrice + DefaultTakeProfitPoints * pointValue; //--- Set buy TP
      if (orderType == ORDER_TYPE_SELL) takeProfitPrice = entryPrice - DefaultTakeProfitPoints * pointValue; //--- Set sell TP
   } else {                                                                //--- Handle auto mode
      if (orderType == ORDER_TYPE_BUY) takeProfitPrice = entryPrice + PreviousNrtrAtr * AtrMultiplierForTakeProfit; //--- Set buy TP
      if (orderType == ORDER_TYPE_SELL) takeProfitPrice = entryPrice - PreviousNrtrAtr * AtrMultiplierForTakeProfit; //--- Set sell TP
   }
   return NormalizeDouble(takeProfitPrice, _Digits);                       //--- Normalize TP
}

//+------------------------------------------------------------------+
//| Calculate position size                                          |
//+------------------------------------------------------------------+
double CalculatePositionSize(double stopLossPrice, double entryPrice) {
   double size = DefaultLotSize;                                          //--- Set default size
   if (RiskDefaultSize == RISK_DEFAULT_AUTO) {                            //--- Check auto risk
      if (stopLossPrice == 0) stopLossPrice = 200;                        //--- Set default SL if zero
      double riskBaseAmount = 0;                                          //--- Initialize base amount
      double tickValue = SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_VALUE); //--- Get tick value
      if (RiskBase == RISK_BASE_BALANCE) riskBaseAmount = AccountInfoDouble(ACCOUNT_BALANCE); //--- Set balance base
      else if (RiskBase == RISK_BASE_EQUITY) riskBaseAmount = AccountInfoDouble(ACCOUNT_EQUITY); //--- Set equity base
      else if (RiskBase == RISK_BASE_FREEMARGIN) riskBaseAmount = AccountInfoDouble(ACCOUNT_MARGIN_FREE); //--- Set free margin base
      double stopLossDistancePoints = MathAbs(entryPrice - stopLossPrice) / SymbolInfoDouble(Symbol(), SYMBOL_POINT); //--- Compute distance
      size = (riskBaseAmount * MaxRiskPerTrade / 100) / (stopLossDistancePoints * tickValue); //--- Compute size
   }
   double lotStep = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_STEP);       //--- Get lot step
   double maxLot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MAX);         //--- Get max lot
   double minLot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN);         //--- Get min lot
   size = MathFloor(size / lotStep) * lotStep;                            //--- Step size
   if (size > MaxLotSize) size = MaxLotSize;                              //--- Clamp max size
   if (size > maxLot) size = maxLot;                                      //--- Clamp symbol max
   if (size < MinLotSize || size < minLot) size = 0;                      //--- Clamp min size
   return size;                                                           //--- Return size
}

ヘルパー関数として、CountOpenPositions関数を作成し、現在の銘柄およびマジックナンバーに一致するすべてのオープンポジションをカウントします。関数内ではカウンターを初期化し、PositionsTotalで総ポジション数を取得し、0からその合計までループします。PositionGetSymbolを使用して銘柄が一致しないものをスキップし、PositionGetIntegerPOSITION_MAGICを取得してMagicNumberが設定されている場合のみフィルタリングし、有効なものをカウントして最後に返します。同様に、CountOpenPositionsByType関数では、特定のポジションタイプ(買いまたは売りなど)のポジション数をカウントします。同じループ構造を使用しますが、PositionGetIntegerでPOSITION_TYPEを取得し、入力されたpositionTypeと一致するかを確認する条件を追加します。

取引のオープン処理として、OpenBuyPosition関数を定義します。この関数はpositionType文字列をコメントとして受け取ります。SymbolInfoDoubleでSYMBOL_ASKを取得してアスク価格を取得し、専用関数でストップロスとテイクプロフィットを計算し、ロットサイズを決定します。コメントは「positionType + ' Buy'」として作成します。TradeObject.Buyで買い注文を実行し、失敗した場合はretcodeを出力してエラーを表示し、成功時は結果を出力して結果を返します。OpenSellPositionも同様で、SYMBOL_BIDを使用してビッド価格を取得し、ストップを計算し、TradeObject.Sellを実行し、コメントは'Sell'とします。

決済処理として、CloseAllBuyPositionsは「PositionsTotal - 1」から0へ逆方向にループし、安全に削除します。銘柄が一致しないもの、POSITION_TYPE_BUYでないもの、またマジックが一致しないものをスキップし、有効なもののみPOSITION_TICKETを使用してTradeObject.PositionCloseで決済し、結果を出力します。CloseAllSellPositionsも同様に売りポジションに対してPOSITION_TYPE_SELLを確認します。CloseAllPositionsはタイプフィルタなしで、銘柄とマジックに一致するすべてのポジションを決済し、結果を出力します。

CalculateStopLossでは、注文タイプとエントリー価格に基づいてストップロス価格を計算します。StopLossModeが固定モードの場合、ポイントが0であれば0を返し、それ以外ではポイント値を取得し、買いの場合は価格からポイント分を引き、売りの場合は加算します。自動モードではPreviousNrtrAtrとAtrMultiplierForStopLossを使用して調整し、Digitsに基づいて正規化します。CalculateTakeProfitも同様に処理し、TakeProfitModeとAtrMultiplierForTakeProfitを使用して自動調整します。CalculatePositionSizeでは、まずデフォルトロットを設定しますが、自動リスクの場合はストップロスが0であればデフォルト距離を設定します。次にリスク基準額を決定し、ティックバリューを取得します。距離をポイントで計算し、ロットサイズを「risk amount / (distance × tick value)」として算出します。その後、MathFloorでロットステップに合わせて調整し、最小ロットと最大ロット(銘柄制限を含む)の間に収めて最終サイズを返します。これらの関数を使用することで、シグナルのチェックおよびポジションのオープン処理をスムーズに実行できるようになります。

//+------------------------------------------------------------------+
//| Check for entry signals                                          |
//+------------------------------------------------------------------+
void CheckForEntrySignals() {
   bool buySignal = (CurrentUpSupport != EMPTY_VALUE) && (PreviousUpSupport == EMPTY_VALUE);      //--- Check buy signal (start of long support)
   bool sellSignal = (CurrentDownSupport != EMPTY_VALUE) && (PreviousDownSupport == EMPTY_VALUE); //--- Check sell signal (start of short support)
   if (buySignal) {                                                                               //--- Handle buy signal
      string currUpStr = DoubleToString(CurrentUpSupport, _Digits);
      string prevUpStr = (PreviousUpSupport == EMPTY_VALUE) ? "EMPTY" : DoubleToString(PreviousUpSupport, _Digits);
      PrintFormat("Buy Signal Detected: Start of Long Support (Buffer 1 Recent: %s, Older: %s)", currUpStr, prevUpStr); //--- Print buy signal
      if ((CountOpenPositionsByType(POSITION_TYPE_BUY) == 0 && HedgeMode) || CountOpenPositions() < MaxPositions) {     //--- Check open buys
         OpenBuyPosition("Initial Signal");                                                       //--- Open buy
      } else {                                                                                    //--- Handle rejected
         Print("Buy Trade Rejected: Maximum positions reached or hedge mode restricts");          //--- Print rejected
      }
   }
   if (sellSignal) {                                                                              //--- Handle sell signal
      string currDownStr = DoubleToString(CurrentDownSupport, _Digits);
      string prevDownStr = (PreviousDownSupport == EMPTY_VALUE) ? "EMPTY" : DoubleToString(PreviousDownSupport, _Digits);
      PrintFormat("Sell Signal Detected: Start of Short Support (Buffer 2 Recent: %s, Older: %s)", currDownStr, prevDownStr); //--- Print sell signal
      if ((CountOpenPositionsByType(POSITION_TYPE_SELL) == 0 && HedgeMode) || CountOpenPositions() < MaxPositions) {          //--- Check open sells
         OpenSellPosition("Initial Signal");                                                      //--- Open sell
      } else {                                                                                    //--- Handle rejected
         Print("Sell Trade Rejected: Maximum positions reached or hedge mode restricts");         //--- Print rejected
      }
   }
}

ここではCheckForEntrySignals関数を実装し、NRTRチャネルの遷移に基づいてエントリーシグナルを検出し、取引を実行します。関数内では、まず買いシグナルをCurrentUpSupportがEMPTY_VALUEではなく、かつPreviousUpSupportがEMPTY_VALUEである場合に成立と定義します。これは買いサポートレベルの開始を示します。同様に、売りシグナルはCurrentDownSupportが有効であり、かつPreviousDownSupportがEMPTY_VALUEである場合に発生し、売りサポートの開始を示します。

買いシグナルが検出された場合、サポート値を文字列に変換し(EMPTY_VALUEの場合は前回値をEMPTYとして扱う)、バッファ1からの検出であることを示す詳細メッセージをフォーマットして出力します。その後、条件を確認します。ヘッジが有効であり、かつ買いポジションが存在しない場合、または総ポジション数がMaxPositions未満の場合にOpenBuyPositionをInitial Signalとして呼び出します。条件を満たさない場合は拒否メッセージを出力します。売りシグナルの場合も同様の処理をおこないます。バッファ2からの検出として詳細をフォーマットして出力し、ヘッジ条件により既存の売りポジションがない場合、またはポジション数が上限未満の場合にOpenSellPositionを呼び出します。条件を満たさない場合は拒否メッセージを出力します。この関数をティックイベントハンドラー内で呼び出すことで、シグナル検出と取引実行の結果を得ることができます。

//+------------------------------------------------------------------+
//| Handle tick event                                                |
//+------------------------------------------------------------------+
void OnTick() {
   ProcessEachTick();                                            //--- Process tick
}

//+------------------------------------------------------------------+
//| Process each tick                                                |
//+------------------------------------------------------------------+
void ProcessEachTick() {
   if (!FetchIndicatorData()) return;                            //--- Fetch indicator data

   static datetime lastBarTime = WRONG_VALUE;                    //--- Initialize last bar time
   datetime currentBarTime = iTime(Symbol(), Period(), 0);       //--- Get current bar time
   static int newBarTicks = 0;                                   //--- Initialize new bar ticks
   if (currentBarTime == lastBarTime) {                          //--- Check same bar
      newBarTicks++;                                             //--- Increment ticks
      if (newBarTicks > 1) return;                               //--- Skip if more than 1
   } else {                                                      //--- New bar
      newBarTicks = 0;                                           //--- Reset ticks
      lastBarTime = currentBarTime;                              //--- Update last time
   }
   CheckForEntrySignals();                                       //--- Check entry signals
}

OnTickイベントハンドラーでは、各価格更新時の処理をProcessEachTickの呼び出しに集約して扱います。ProcessEachTick内では、まずFetchIndicatorDataを使用して最新のインジケータデータを取得し、失敗した場合は早期に処理を終了して、必ず有効な情報に基づいて判断が下されるようにします。次に、新しいバーのみを効率的に処理するため、WRONG_VALUEで初期化された静的変数lastBarTimeを使用し、iTimeを用いて銘柄と時間足における現在のバーの開始時刻を取得します。また、同一バー内のティック数を管理するために、静的変数newBarTicksを0で初期化します。

現在のバー時間がlastBarTimeと一致する場合、すなわち同一バーである場合はnewBarTicksをインクリメントし、1を超える場合はそれ以上の処理をおこなわずに終了し、同一バー内での過剰なチェックを抑制します。一方、新しいバーの場合はnewBarTicksを0にリセットし、lastBarTimeを更新した上で処理を継続します。その後、CheckForEntrySignalsを呼び出し、更新されたデータに基づいてエントリーシグナルの評価および取引の実行をおこないます。コンパイルすると、次の結果が得られます。

シグナル生成と取引

画像から、シグナル生成に基づいて取引が実行されていることが確認できます。現在残っているのはエグジット戦略の処理であり、反対シグナルでの決済および仮想決済の処理を実装する段階です。

//+------------------------------------------------------------------+
//| Check for exit signals                                           |
//+------------------------------------------------------------------+
void CheckForExitSignals() {
   bool exitBuySignal = false;                                   //--- Initialize buy exit
   bool exitSellSignal = false;                                  //--- Initialize sell exit
   if (CloseOnReversalSignal) {                                  //--- Check reversal close
      exitBuySignal = (CurrentDownSupport != EMPTY_VALUE) && (PreviousDownSupport == EMPTY_VALUE); //--- Set buy exit (start of short)
      exitSellSignal = (CurrentUpSupport != EMPTY_VALUE) && (PreviousUpSupport == EMPTY_VALUE); //--- Set sell exit (start of long)
   }
   if (exitBuySignal) {                                          //--- Handle buy exit
      string currDownStr = DoubleToString(CurrentDownSupport, _Digits);
      string prevDownStr = (PreviousDownSupport == EMPTY_VALUE) ? "EMPTY" : DoubleToString(PreviousDownSupport, _Digits);
      PrintFormat("Exit Buy Signal Detected (Reversal): Start of Short Support (Buffer 2 Recent: %s, Older: %s)", currDownStr, prevDownStr); //--- Print buy exit
      CloseAllBuyPositions();                                    //--- Close buys
   }
   if (exitSellSignal) {                                         //--- Handle sell exit
      string currUpStr = DoubleToString(CurrentUpSupport, _Digits);
      string prevUpStr = (PreviousUpSupport == EMPTY_VALUE) ? "EMPTY" : DoubleToString(PreviousUpSupport, _Digits);
      PrintFormat("Exit Sell Signal Detected (Reversal): Start of Long Support (Buffer 1 Recent: %s, Older: %s)", currUpStr, prevUpStr); //--- Print sell exit
      CloseAllSellPositions();                                   //--- Close sells
   }
}

//+------------------------------------------------------------------+
//| Close on virtual take profit                                     |
//+------------------------------------------------------------------+
void CloseOnVirtualTakeProfit(ulong ticket) {
   if (!PositionSelectByTicket(ticket)) return;                  //--- Select position
   double entryPrice = PositionGetDouble(POSITION_PRICE_OPEN);   //--- Get entry price
   double virtualTakeProfit = 0;                                 //--- Initialize virtual TP
   double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);          //--- Get ask
   double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);          //--- Get bid
   if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy
      virtualTakeProfit = entryPrice + DefaultTakeProfitPoints * SymbolInfoDouble(Symbol(), SYMBOL_POINT); //--- Set buy TP
      if (virtualTakeProfit <= bid && CloseOnTakeProfitHit) {    //--- Check hit
         TradeObject.PositionClose(ticket);                      //--- Close position
         PrintFormat("Position Closed on Virtual TP: Ticket %llu, Price Hit %.5f", ticket, virtualTakeProfit); //--- Print closed
      }
   } else {                                                      //--- Handle sell
      virtualTakeProfit = entryPrice - DefaultTakeProfitPoints * SymbolInfoDouble(Symbol(), SYMBOL_POINT); //--- Set sell TP
      if (virtualTakeProfit >= ask && CloseOnTakeProfitHit) {    //--- Check hit
         TradeObject.PositionClose(ticket);                      //--- Close position
         PrintFormat("Position Closed on Virtual TP: Ticket %llu, Price Hit %.5f", ticket, virtualTakeProfit); //--- Print closed
      }
   }
}

//+------------------------------------------------------------------+
//| Close on virtual stop loss                                       |
//+------------------------------------------------------------------+
void CloseOnVirtualStopLoss(ulong ticket) {
   if (!PositionSelectByTicket(ticket)) return;                  //--- Select position
   double entryPrice = PositionGetDouble(POSITION_PRICE_OPEN);   //--- Get entry price
   double virtualStopLoss = 0;                                   //--- Initialize virtual SL
   double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);          //--- Get ask
   double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);          //--- Get bid
   if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy
      virtualStopLoss = entryPrice - DefaultStopLossPoints * SymbolInfoDouble(Symbol(), SYMBOL_POINT); //--- Set buy SL
      if (virtualStopLoss >= ask && CloseOnStopLossHit) {        //--- Check hit
         TradeObject.PositionClose(ticket);                      //--- Close position
         PrintFormat("Position Closed on Virtual SL: Ticket %llu, Price Hit %.5f", ticket, virtualStopLoss); //--- Print closed
      }
   } else {                                                      //--- Handle sell
      virtualStopLoss = entryPrice + DefaultStopLossPoints * SymbolInfoDouble(Symbol(), SYMBOL_POINT); //--- Set sell SL
      if (virtualStopLoss <= bid && CloseOnStopLossHit) {        //--- Check hit
         TradeObject.PositionClose(ticket);                      //--- Close position
         PrintFormat("Position Closed on Virtual SL: Ticket %llu, Price Hit %.5f", ticket, virtualStopLoss); //--- Print closed
      }
   }
}

//+------------------------------------------------------------------+
//| Handle virtual closures                                          |
//+------------------------------------------------------------------+
void HandleVirtualClosures() {
   int total = PositionsTotal();                                 //--- Get total positions
   for (int i = total - 1; i >= 0; i--) {                        //--- Loop backward
      if (PositionGetSymbol(i) == "") continue;                  //--- Skip invalid
      ulong ticket = PositionGetInteger(POSITION_TICKET);        //--- Get ticket
      if (PositionGetString(POSITION_SYMBOL) != Symbol() || PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue; //--- Skip wrong symbol/magic
      if (CloseOnStopLossHit) CloseOnVirtualStopLoss(ticket);    //--- Check virtual SL
      if (CloseOnTakeProfitHit) CloseOnVirtualTakeProfit(ticket); //--- Check virtual TP
   }
}

まず、CheckForExitSignals関数を開発し、既存ポジションを決済する必要がある条件を監視します。この処理では、まず買いと売りのエグジット用フラグをfalseで初期化します。CloseOnReversalSignalが有効な場合、売りサポートの開始(現在のダウンサポートが有効で、前回がEMPTY_VALUE)を検出したときにexitBuySignalをtrueに設定します。同様に、買いサポートの開始を検出した場合にはexitSellSignalを設定します。エグジットの買いシグナルが発生した場合、ダウンサポートの値を文字列として整形し(EMPTY_VALUEは「EMPTY」として扱う)、バッファ2からの詳細メッセージを出力し、すべての買いポジションを決済するためにCloseAllBuyPositionsを呼び出します。同様に、売りのエグジットシグナルが発生した場合は、バッファ1の情報を出力し、CloseAllSellPositionsを呼び出して売りポジションをすべて決済します。

次に、CloseOnVirtualTakeProfit関数では、指定されたチケットのポジションに対して仮想テイクプロフィットを管理します。まずPositionSelectByTicketでポジションを選択し、失敗した場合は処理を終了します。その後エントリー価格を取得します。買いポジションの場合は、DefaultTakeProfitPointsにポイント値を掛けたものをエントリー価格に加算してvirtualTakeProfitを計算します。売りポジションの場合は減算します。その後、AskとBid価格を取得し、買いの場合はTP <= Bid、売りの場合はTP >= Askを満たしたときに、かつCloseOnTakeProfitHitが有効であればTradeObject.PositionCloseを実行してポジションを決済し、チケット番号とヒット価格を出力します。

同様に、CloseOnVirtualStopLoss関数では仮想ストップロスを処理します。ポジション選択後にエントリー価格を取得し、買いの場合はポイント分を引き、売りの場合は加算してvirtualStopLossを計算します。AskとBidを用いて、買いではSL >= Ask、売りではSL <= Bidの条件を満たし、かつCloseOnStopLossHitが有効な場合にポジションを決済し、詳細を出力します。HandleVirtualClosures関数では、すべてのポジションを安全に処理するために「PositionsTotal - 1」から0へ逆方向にループします。各ポジションについて、まず銘柄が無効であればスキップします。その後チケットを取得し、銘柄またはマジックナンバーが一致しない場合は処理を継続します。CloseOnStopLossHitが有効であればCloseOnVirtualStopLossを呼び出し、CloseOnTakeProfitHitが有効であればCloseOnVirtualTakeProfitを呼び出して、すべてのオープンポジションに対して仮想決済処理を適用します。さらに、トレーリングタイプが有効な場合には、ポイントベースまたはサポートレベルベースのいずれかに応じてトレーリング処理を実装する必要があります。

//+------------------------------------------------------------------+
//| Adjust to break even                                             |
//+------------------------------------------------------------------+
bool AdjustToBreakEven(ulong ticket) {
   if (TrailingType == TRAILING_NONE) return false;              //--- Check trailing type
   if (!PositionSelectByTicket(ticket)) return false;            //--- Select position
   MqlTradeRequest request = {};                                 //--- Declare request
   MqlTradeResult result = {};                                   //--- Declare result
   request.action = TRADE_ACTION_SLTP;                           //--- Set action SLTP
   request.position = ticket;                                    //--- Set position ticket
   long positionType = PositionGetInteger(POSITION_TYPE);        //--- Get type
   double entryPrice = PositionGetDouble(POSITION_PRICE_OPEN);   //--- Get entry price
   double currentStopLoss = PositionGetDouble(POSITION_SL);      //--- Get current SL
   double currentTakeProfit = PositionGetDouble(POSITION_TP);    //--- Get current TP
   string symbol = PositionGetString(POSITION_SYMBOL);           //--- Get symbol
   double point = SymbolInfoDouble(symbol, SYMBOL_POINT);        //--- Get point
   double currentPrice = (positionType == POSITION_TYPE_BUY) ? SymbolInfoDouble(symbol, SYMBOL_BID) : SymbolInfoDouble(symbol, SYMBOL_ASK); //--- Get current price
   double currentProfitPoints = (positionType == POSITION_TYPE_BUY) ? (currentPrice - entryPrice) / point : (entryPrice - currentPrice) / point; //--- Compute profit points
   double newStopLoss = 0;                                       //--- Initialize new SL
   if (currentProfitPoints <= 0) return false;                   //--- Skip if no profit

   switch (TrailingType) {                                       //--- Switch trailing type
   case TRAILING_POINTS: {                                       //--- Handle points
      if (currentProfitPoints >= BreakEvenTriggerPoints) {       //--- Check BE trigger
         double bePoints = BreakEvenPoints;                      //--- Set BE points
         newStopLoss = (positionType == POSITION_TYPE_BUY) ? NormalizeDouble(entryPrice + (bePoints * point), _Digits) : NormalizeDouble(entryPrice - (bePoints * point), _Digits); //--- Compute new SL
         if ((positionType == POSITION_TYPE_BUY && newStopLoss > currentStopLoss) || (positionType == POSITION_TYPE_SELL && newStopLoss < currentStopLoss)) { //--- Check improvement
            if (NormalizeDouble(newStopLoss, _Digits) != NormalizeDouble(currentStopLoss, _Digits)) { //--- Check different
               request.sl = newStopLoss;                            //--- Set SL
               request.tp = currentTakeProfit;                      //--- Set TP
               if (OrderSend(request, result)) {                    //--- Send order
                  PrintFormat("Breakeven SL Adjusted for %s Position: Ticket %llu, New SL = %.5f (Triggered at %.0f points profit)", (positionType == POSITION_TYPE_BUY) ? "Buy" : "Sell", ticket, newStopLoss, currentProfitPoints); //--- Print adjusted
                  return true;                                      //--- Return success
               }
            }
         }
      }
      if (currentProfitPoints >= CurrentTrailingStopPoints) {       //--- Check trailing trigger
         double trailPoints = currentProfitPoints - CurrentTrailingStepPoints; //--- Compute trail points
         newStopLoss = (positionType == POSITION_TYPE_BUY) ? NormalizeDouble(currentPrice - (CurrentTrailingStopPoints * point), _Digits) : NormalizeDouble(currentPrice + (CurrentTrailingStopPoints * point), _Digits); //--- Compute new SL
         if ((positionType == POSITION_TYPE_BUY && newStopLoss > currentStopLoss) || (positionType == POSITION_TYPE_SELL && newStopLoss < currentStopLoss)) { //--- Check improvement
            if (NormalizeDouble(newStopLoss, _Digits) != NormalizeDouble(currentStopLoss, _Digits)) { //--- Check different
               request.sl = newStopLoss;                            //--- Set SL
               request.tp = currentTakeProfit;                      //--- Set TP
               if (OrderSend(request, result)) {                    //--- Send order
                  PrintFormat("Trailing SL Adjusted for %s Position: Ticket %llu, New SL = %.5f (Current Profit = %.0f points)", (positionType == POSITION_TYPE_BUY) ? "Buy" : "Sell", ticket, newStopLoss, currentProfitPoints); //--- Print adjusted
                  return true;                                      //--- Return success
               }
            }
         }
      }
      break;
   }

   case TRAILING_SUPPORT_LEVELS: {                                 //--- Handle support levels
      double currentSupport = (positionType == POSITION_TYPE_BUY) ? CurrentUpSupport : CurrentDownSupport; //--- Get current support
      double previousSupport = (positionType == POSITION_TYPE_BUY) ? PreviousUpSupport : PreviousDownSupport; //--- Get previous support
      if (currentSupport == EMPTY_VALUE) return false;             //--- Skip if invalid support
      if (currentProfitPoints >= BreakEvenTriggerPoints) {         //--- Check BE trigger
         newStopLoss = currentSupport;                             //--- Set new SL to support
         bool isProfitable = (positionType == POSITION_TYPE_BUY && newStopLoss >= entryPrice) || (positionType == POSITION_TYPE_SELL && newStopLoss <= entryPrice); //--- Ensure beyond entry
         if (isProfitable && ((positionType == POSITION_TYPE_BUY && newStopLoss > currentStopLoss) || (positionType == POSITION_TYPE_SELL && newStopLoss < currentStopLoss))) { //--- Check improvement and profitable
            if (NormalizeDouble(newStopLoss, _Digits) != NormalizeDouble(currentStopLoss, _Digits)) { //--- Check different
               request.sl = newStopLoss;                            //--- Set SL
               request.tp = currentTakeProfit;                      //--- Set TP
               if (OrderSend(request, result)) {                    //--- Send order
                  PrintFormat("Breakeven SL Adjusted to Support Level for %s Position: Ticket %llu, New SL = %.5f (Triggered at %.0f points profit)", (positionType == POSITION_TYPE_BUY) ? "Buy" : "Sell", ticket, newStopLoss, currentProfitPoints); //--- Print adjusted
                  return true;                                      //--- Return success
               }
            }
         }
      }
      if ((positionType == POSITION_TYPE_BUY && currentSupport > previousSupport) || (positionType == POSITION_TYPE_SELL && currentSupport < previousSupport)) { //--- Check support moved
         newStopLoss = currentSupport;                              //--- Set new SL to support
         bool isProfitable = (positionType == POSITION_TYPE_BUY && newStopLoss >= entryPrice) || (positionType == POSITION_TYPE_SELL && newStopLoss <= entryPrice); //--- Ensure beyond entry
         if (isProfitable && ((positionType == POSITION_TYPE_BUY && newStopLoss > currentStopLoss) || (positionType == POSITION_TYPE_SELL && newStopLoss < currentStopLoss))) { //--- Check improvement and profitable
            if (NormalizeDouble(newStopLoss, _Digits) != NormalizeDouble(currentStopLoss, _Digits)) { //--- Check different
               request.sl = newStopLoss;                            //--- Set SL
               request.tp = currentTakeProfit;                      //--- Set TP
               if (OrderSend(request, result)) {                    //--- Send order
                  PrintFormat("SL Trailed to Support Level for %s Position: Ticket %llu, New SL = %.5f (Support Moved from %.5f to %.5f)", (positionType == POSITION_TYPE_BUY) ? "Buy" : "Sell", ticket, newStopLoss, previousSupport, currentSupport); //--- Print trailed
                  return true;                                      //--- Return success
               }
            }
         }
      }
      break;
   }

   default:
      break;
   }
   return false;                                                    //--- Return no adjustment
}

//+------------------------------------------------------------------+
//| Adjust all stop losses                                           |
//+------------------------------------------------------------------+
void AdjustAllStopLosses() {
   for (int i = PositionsTotal() - 1; i >= 0; i--) {                //--- Loop backward
      if (PositionGetSymbol(i) == "") continue;                     //--- Skip invalid
      ulong ticket = PositionGetInteger(POSITION_TICKET);           //--- Get ticket
      AdjustToBreakEven(ticket);                                    //--- Adjust to BE
   }
}

ここでは、AdjustToBreakEven関数を構築し、チケットで識別される特定のポジションに対してトレーリングおよびブレークイーブン調整を管理します。TrailingTypeがTRAILING_NONEの場合、またはPositionSelectByTicketでポジション選択に失敗した場合は、即座にfalseを返します。次に、MqlTradeRequestおよびMqlTradeResultの構造体を準備し、ストップロスとテイクプロフィットを変更するためにTRADE_ACTION_SLTPをrequest.actionに設定し、ポジションチケットを割り当てます。ポジションタイプ、エントリー価格、現在のストップロス、テイクプロフィット、銘柄、およびポイント値を取得します。現在価格は買いの場合はBid、売りの場合はAskを使用し、エントリーからの差分をポイント換算したcurrentProfitPointsを計算します。利益が発生していない場合は処理をスキップしfalseを返します。

TrailingTypeに対するswitch処理では、まずTRAILING_POINTSの場合を扱います。利益がBreakEvenTriggerPointsに到達しているかを確認し、到達している場合はブレークイーブン用の新しいストップロスを計算します。買いポジションでは「entry + BreakEvenPoints * point」、売りポジションでは「entry - BreakEvenPoints * point」とし、正規化します。現在のストップロスよりも有利であり(買いなら高い、売りなら低い)、かつ差分がある場合のみ、リクエストにストップロスとテイクプロフィットを設定し、OrderSendで送信します。成功した場合はログを出力し、trueを返します。同じくポイントモード内で、利益がCurrentTrailingStopPointsを超えている場合にはトレーリングストップを計算します。現在価格から「CurrentTrailingStopPoints * point」を減算または加算し、より有利であることと差分があることを確認した上で更新リクエストを送信し、成功時にログを出力してtrueを返します。

TRAILING_SUPPORT_LEVELSの場合、現在および前回のサポート値をポジションタイプに応じて取得し、現在値がEMPTYであればスキップします。利益がブレークイーブン条件に達している場合は、ストップロスを現在のサポートに設定します。このとき、エントリー価格以上であること(買いの場合)または以下であること(売りの場合)を確認し、改善されておりかつ差分がある場合のみ更新リクエストを送信し、成功時に詳細を出力します。サポートが有利方向に移動した場合(買いなら上昇、売りなら下降)にも同様に更新し、現在のサポートが利益圏内かつ改善されていることを確認したうえでストップロスを更新し、変更内容をログ出力してtrueを返します。どの条件にも該当しない場合はbreakし、調整されなかった場合はfalseを返します。AdjustAllStopLosses関数では、すべてのポジションを安全に処理するために「PositionsTotal- 1から0へ逆方向にループします。各ポジションについて無効な銘柄をスキップし、チケットを取得した後、各ポジションに対してAdjustToBreakEvenを呼び出し、すべてのオープントレードに対して調整処理を適用します。これにより、これらすべての関数を管理関数内で呼び出し、ティックイベントごとのポジション管理処理を完成させることができます。最終的な関数は以下のようになります。

//+------------------------------------------------------------------+
//| Process each tick                                                |
//+------------------------------------------------------------------+
void ProcessEachTick() {
   if (!FetchIndicatorData()) return;                            //--- Fetch indicator data
   int positionCount = CountOpenPositions();                     //--- Count positions
   if (positionCount > 0) {                                      //--- Check positions exist
      CheckForExitSignals();                                     //--- Check exit signals
   }
   static datetime lastBarTime = WRONG_VALUE;                    //--- Initialize last bar time
   datetime currentBarTime = iTime(Symbol(), Period(), 0);       //--- Get current bar time
   static int newBarTicks = 0;                                   //--- Initialize new bar ticks
   if (currentBarTime == lastBarTime) {                          //--- Check same bar
      newBarTicks++;                                             //--- Increment ticks
      if (newBarTicks > 1) return;                               //--- Skip if more than 1
   } else {                                                      //--- New bar
      newBarTicks = 0;                                           //--- Reset ticks
      lastBarTime = currentBarTime;                              //--- Update last time
   }
   CheckForEntrySignals();                                       //--- Check entry signals
   datetime currentTime = TimeCurrent();                         //--- Get current time
   if (currentTime - LastTrailingTime >= TrailingFrequencySeconds) { //--- Check trailing time
      AdjustAllStopLosses();                                     //--- Adjust SLs
      LastTrailingTime = currentTime;                            //--- Update trailing time
   }
   HandleVirtualClosures();                                      //--- Handle virtual closures
}

各関数を条件に応じて呼び出すことで、コンパイル時に以下の結果が得られます。

NRTRトレーリング

画像から、NRTRシグナルのセットアップを検出し、取引し、管理していることが確認できます。これにより、目的は達成されています。残っている作業は、このプログラムのバックテストをおこなうことです。バックテストについては次のセクションで扱います。


バックテスト

バックテストを実施した結果、以下の結果が得られました。

バックテストグラフ

グラフ

バックテストレポート

レポート


結論

MQL5でNick Rypock Trailing Reverse(NRTR)取引システムを構築しました。このシステムは、動的チャネルを通じて反転シグナルを検出し、買いと売りの両方に対応したヘッジ付きトレンドフォローエントリーをサポートし、ポジション制限によってリスクエクスポージャーを制御します。また、エクイティ、残高、余剰証拠金に基づく自動ロット計算や、固定またはATR調整型のストップロスおよびテイクプロフィットなどのリスク管理機能も追加しました。

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

このNRTRリバーサル戦略により、トレンド機会を捉えつつヘッジとトレーリング保護を備えた運用が可能になり、さらなる改善にも対応できる設計となっています。取引をお楽しみください。

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

最後のコメント | ディスカッションに移動 (3)
peterssozi2004
peterssozi2004 | 30 1月 2026 において 21:34
このロボットは手に入るか?
Jay4rty
Jay4rty | 31 1月 2026 において 06:50
私も知りたいのですが、入手可能ですか?
Hemant Yadav
Hemant Yadav | 31 1月 2026 において 08:09
こんにちは先生、しかし、このEAはm5 Xau / USDでバックテストすることはできませんか? plsはどのようにスモスをバックテスト することができます私を助ける。
ダイナミックマルチペアEAの形成(第6回):高頻度銘柄切り替えのための適応型スプレッド感度制御 ダイナミックマルチペアEAの形成(第6回):高頻度銘柄切り替えのための適応型スプレッド感度制御
本パートでは、マルチ銘柄におけるリアルタイムのスプレッド条件を継続的に監視し、評価するインテリジェントな実行レイヤーの設計に焦点を当てます。EAは、固定ルールではなくスプレッドの効率性に基づいて取引の有効と無効を切り替えることで、銘柄選択を動的に適応させます。このアプローチにより、高頻度で銘柄を切り替えるマルチペアシステムはコスト効率の高い銘柄を優先できるようになります。
MQL5でカスタムインジケータを作成する(第6回):平滑化、色相シフト、マルチタイムフレーム対応を備えたRSI計算の拡張 MQL5でカスタムインジケータを作成する(第6回):平滑化、色相シフト、マルチタイムフレーム対応を備えたRSI計算の拡張
MQL5で多用途なRSIインジケータを構築します。このインジケータは複数のバリエーション、データソース、平滑化手法をサポートし、より高度な分析を可能にします。さらに、視覚的な色表現のための色相シフト、買われすぎ・売られすぎゾーンの動的境界、トレンドアラート用の通知機能を追加します。また、補間を伴うマルチタイムフレーム対応も実装し、異なる時間足のRSI値を補間によって滑らかに対応付けるカスタマイズ可能なRSIツールを提供します。
初心者からエキスパートへ:サプライ&デマンドゾーンの統計的検証 初心者からエキスパートへ:サプライ&デマンドゾーンの統計的検証
サプライ&デマンド取引戦略の背後にある、見落とされがちな統計的基盤を明らかにします。MQL5とPythonをJupyter Notebookワークフローで連携させることで、マーケットに対する視覚的な仮定を定量的な洞察へと変換する体系的なデータ駆動型リサーチをおこないます。本記事では、データ収集からPythonによる統計分析、アルゴリズム設計、テスト、最終的な結論に至るまで、一連の研究プロセスを解説します。手法と結果の詳細については、本文をご参照ください。
MQL5取引ツール(第14回):アンチエイリアシングと角丸スクロールバーを備えたピクセルパーフェクトなスクロール対応テキストキャンバス MQL5取引ツール(第14回):アンチエイリアシングと角丸スクロールバーを備えたピクセルパーフェクトなスクロール対応テキストキャンバス
本記事では、MQL5のCCanvasベース価格ダッシュボードを拡張し、利用ガイドを表示するためのピクセルパーフェクトなスクロール可能テキストパネルを追加します。これにより、ネイティブのスクロール機能の制限を回避しつつ、カスタムアンチエイリアス処理と角丸デザインのスクロールバーを実現します。テキストパネルは、不透明度を設定可能なテーマ対応背景をサポートし、説明文や連絡先情報などのコンテンツを動的に改行表示できます。また、上下ボタン、スライダーのドラッグ操作、本文領域内でのマウスホイール操作によるインタラクティブなナビゲーションにも対応しています。