English
preview
MQL5でスマート取引マネージャーを構築する:損益分岐点、トレーリングストップ、部分決済を自動化する

MQL5でスマート取引マネージャーを構築する:損益分岐点、トレーリングストップ、部分決済を自動化する

MetaTrader 5エキスパートアドバイザー |
19 0
Chacha Ian Maroa
Chacha Ian Maroa

はじめに

取引管理は、取引において最も見落とされがちな要素のひとつです。多くのトレーダーは優れたエントリーを見つけることに注力しますが、利益が生まれるか損失になるかはエントリー後の取引管理によって決まることを忘れがちです。ストップロスの調整や部分利益の確定、トレーリングストップの管理には、ポジションを常に監視し、市場の動きに迅速に対応することが求められます。しかし、実際の相場環境においてこれを継続的に行うことは容易ではありません。

手動での取引管理はすぐにストレスになります。特に複数の通貨ペアや銘柄を同時に監視している場合、トレーダーはストップロスを移動するのをためらったり、遅れて調整したり、あるいは完全に忘れてしたりすることがあります。こうした小さなミスが、勝ちトレードを負けトレードに変えてしまい、感情的な判断や一貫性のない結果につながりかねません。

また、自らのルールに従って取引している規律あるトレーダーにとっても、手動での実行は負担です。チャートを絶えず監視し、レベルを計算し、価格変動に反応する作業は、集中力を低下させ、効率を下げてしまいます。これにより、取引の成功に必要な冷静な判断が奪われてしまいます。

本記事では、これらの問題を解決する自動化ソリューションを紹介します。ステップバイステップで、完全に機能する「取引マネージャー」エキスパートアドバイザー(EA)の作成方法を解説します。このEAは、損失を防ぐための損益分岐点(ブレークイーブン)へのストップロス移動、価格が有利に動いた際のトレーリングストップの設定、そして利益を段階的に確保するための部分決済という、取引管理の3つの重要な要素を自動化します。



スマート取引マネージャーEAの設計方法

「スマート取引マネージャー」(通称「AutoProtect」)は、MQL5環境内でEAとして設計されています。これは重要なポイントです。MQL5ではスクリプト、インジケーター、サービス、EAなど、複数のプログラムタイプをサポートしています。AutoProtectは、開いている取引を自動的に監視および管理することが目的としており、リアルタイムで継続的に稼働する必要があります。

AutoProtectには、損益分岐点機能、トレーリングストップ機能、部分利益決済機能の3つの主要な機能があります。これらの機能は連携し、取引を保護して、トレーダーの管理作業を自動化します。ユーザーはこれらの機能を個別に有効/無効にして、個々の取引スタイルに合わせて柔軟にEAを構成することができます。たとえば、損益分岐点機能だけを使用したい場合、他の2つの機能を無効にするだけで対応できます。

損益分岐点機能は、取引が定義された利益水準に達したときに、ストップロスを建値(オプションで設定できるバッファ距離を加えた価格)まで自動的に移動させます。これによりリスクを排除し、取引が有利に進んだ段階でポジションを保護できます。その後、トレーリングストップ機能が作動し、市場が利益方向に動き続ける際にストップロスを調整します。この機能により、利益を守りつつ、取引を自然に伸ばすことが可能です。最後に、部分利益決済機能は、一定の利益距離(ポイント単位)に達した際に取引の一部を決済し、段階的に利益を確保します。

AutoProtectは、アタッチされたチャート上の取引のみを管理します。この設計は意図的です。各金融商品には固有のポイント値があるため、単一設定で複数銘柄の取引を一括管理すると、一貫性が損なわれる可能性があります。1つの銘柄に限定して管理することで、精密かつ安定した挙動が保証されます。また、ユーザーは特定のマジックナンバーに基づいて取引を管理するようEAを設定できます。これは、AutoProtectが特定のEAによって開かれた取引のみを管理するようにしたい場合に有用です。

さらに重要なのは、AutoProtectは起動後に開かれた取引のみを監視および管理するということです。初期化時点で既に存在している古い取引やポジションには反応しません。この設計により、EAの監視下で作成された取引のみが影響を受け、意図しない動作を避けることができます。

EAには、設定の明確さと使いやすさのために論理的に整理された複数の入力パラメータが用意されています。

//+------------------------------------------------------------------+
//| User Input Variables                                             |
//+------------------------------------------------------------------+
input group "Information"
input bool  manageTradesByMagicNumber   = true;
input ulong tradeSelectionMagicNumber   = 254700680002;

input group "Break-Even"
input bool  enableBreakEven            = false;
input int   breakEvenTriggerPoints     = 50;
input int   breakEvenLockPoints        = 0;

input group "Trailing Stop"
input bool  enableTrailingStop         = false;
input int   trailingStartPoints        = 50;
input int   trailingStepPoints         = 20;
input int   trailingDistancePoints     = 100;

input group "Partial Close"
input bool   enablePartialClose        = false;
input int    partialCloseTriggerPoints = 100;
input double partialClosePercent       = 50.0;

各パラメータには特定の役割があります。たとえば、breakEvenTriggerPointsは損益分岐点の移動を開始するタイミングを定義し、breakEvenLockPointsはエントリー価格からさらに何ポイント上で利益を確定するかを決定します。トレーリングストップ設定では、trailingStartPointsがトレーリング開始の利益レベルを示し、trailingStepPointsがストップ移動の頻度を決定し、trailingDistancePointsが現在価格からどれだけ離してストップを維持するかを示します。部分利益決済設定では、partialCloseTriggerPointsとpartialClosePercentにより、価格が一定距離(ポイント単位)進んだ場合に取引のどの程度を決済するかが定義されます。

AutoProtectの設計ルールのひとつとして、損益分岐点機能とトレーリングストップ機能の両方が有効な場合、トレーリングストップは損益分岐点条件が満たされた直後に開始されるようになっています。これは初期化ロジックで以下のように反映されています。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){

   ...
      
   //--- If both BreakEven and Trailing are enabled,trailing should only start after break-even has triggered.
   if(enableBreakEven && enableTrailingStop){
      usefulTrailingStartPoints = breakEvenTriggerPoints;
   }
   
   ...
   
   return(INIT_SUCCEEDED);
}

内部的には、EAはモジュール化されたプログラミング手法を採用しており、コードは整理され、コメントが適切に記述されています。各関数は自己完結型のモジュールとして構築されており、この設計により可読性が高まり、デバッグが容易になり、将来的な機能拡張も既存機能に影響を与えずにおこなうことができます。



プロジェクトの設定

AutoProtect EAの開発を開始するには、まずMetaEditorを開き、新しいEAプロジェクトを作成し、ファイル名を「AutoProtect.mq5」とします。プロジェクトファイルが作成されたら、その内容を以下のボイラープレートコードに置き換えます。

//+------------------------------------------------------------------+
//|                                                  AutoProtect.mq5 |
//|          Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian |
//|                          https://www.mql5.com/ja/users/chachaian |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian"
#property link      "https://www.mql5.com/ja/users/chachaian"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| User Input Variables                                             |
//+------------------------------------------------------------------+
input group "Information"
input bool  manageTradesByMagicNumber   = true;
input ulong tradeSelectionMagicNumber   = 254700680002;

input group "Break-Even"
input bool  enableBreakEven            = false;
input int   breakEvenTriggerPoints     = 50;
input int   breakEvenLockPoints        = 0;

input group "Trailing Stop"
input bool  enableTrailingStop         = false;
input int   trailingStartPoints        = 50;
input int   trailingStepPoints         = 20;
input int   trailingDistancePoints     = 100;

input group "Partial Close"
input bool   enablePartialClose        = false;
input int    partialCloseTriggerPoints = 100;
input double partialClosePercent       = 50.0;

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CTrade Trade;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){
   // ---
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason){
   //---
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){
   //---
}

//+------------------------------------------------------------------+
//| Trade Transaction Events handler                                 |
//+------------------------------------------------------------------+
void OnTradeTransaction(
   const MqlTradeTransaction &trans,
   const MqlTradeRequest     &request,
   const MqlTradeResult      &result){
      // ----
}
//+------------------------------------------------------------------+

この構造は、MQL5でEAに必要な基本的なイベントハンドラ(初期化、初期化解除、ティック処理、取引トランザクション追跡)をすべて備えています。

取引のストップ変更やポジション決済などの自動化を有効にするために、まずTrade.mqh標準ライブラリをインクルードします。このライブラリはCTradeクラスを提供しており、PositionModifyやPositionClosePartialといった組み込みメソッドを使用して、取引管理を簡単におこなうことができます。

次に、グローバルにCTrade Tradeのインスタンスを作成します。このオブジェクトはEA全体で使用され、すべての取引操作を実行するための中心的な役割を果たします。

グローバルに定義することで、EA内のどの関数からでもTradeオブジェクトにアクセスできるようになります。これにより、ストップロスの調整、損益分岐点ロジックの適用、トレーリング処理など、あらゆる取引操作を効率的かつ一貫した方法で実行することが可能になります。

取引選択ロジック

AutoProtect EAは、起動したチャート上で開かれた取引のみを管理します。この設計は、一貫した動作を保証するために重要です。というのも、異なる銘柄ではポイント値がそれぞれ異なるため、すべての銘柄に対して同じ設定で管理しようとすると、計算誤差や不整合が生じる可能性があるからです。チャートの銘柄に管理を限定することで、精度が保たれ、異なる市場間での衝突を避けることができます。

さらに柔軟性を持たせるために、EAでは特定のマジックナンバーによる取引フィルタリングも可能です。有効化すると、指定したマジックナンバーに一致する取引のみが管理対象となります。この機能は、同じ銘柄上で複数のEAを同時に稼働させている場合に、特定の戦略によって開かれた取引のみをAutoProtectに管理させたいときに特に有用です。

もう1つの重要な設計上のポイントは、AutoProtectが起動後に新規に開かれたポジションにのみ反応するということです。EAがチャートにアタッチされる前に存在していた既存の取引を管理しようとはしません。この設計により、不整合を避け、管理対象の各取引が生成された時点から正確に登録および追跡されることが保証されます。

この仕組みを実現するために、MqlTradeInfoというカスタム構造体を定義します。この構造体には、チケット番号、ポジションタイプ、建値、ストップレベル、トレーリング関連のパラメータなど、取引管理に必要な情報が格納されます。そして、EAが管理するすべてのポジションを追跡するために、これらの構造体の配列をグローバルに保持します。

...

//+------------------------------------------------------------------+
//| User Input Variables                                             |
//+------------------------------------------------------------------+
input group "Information"
input bool  manageTradesByMagicNumber   = true;
input ulong tradeSelectionMagicNumber   = 254700680002;

...

//+------------------------------------------------------------------+
//| Data Structures                                                  |
//+------------------------------------------------------------------+
struct MqlTradeInfo{
   ulong ticket;
   ENUM_POSITION_TYPE positionType;
   double openPrice;
   double originalStopLevel;
   double currentStopLevel;
   double nextStopLevel;
   double originalTargetLevel;
   double nextTrailTriggerPrice;
   bool   isMovedToBreakEven;
   bool   isPartialProfitsSecured;
};

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CTrade Trade;
MqlTradeInfo tradeInfo[];


//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){

   //---
   return(INIT_SUCCEEDED);
}

...

次に、OnTradeTransactionイベントハンドラは、新規取引が開かれたタイミングを検出するために使用されます。新しいポジションが作成されるたびに、EAは取引履歴を通じてその取引を確認し、tradeInfo配列に詳細情報を記録します。以降、トレーリングストップ機能が処理するのは、この配列に登録された取引のみとなります。

...

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){
   //---
}

//+------------------------------------------------------------------+
//| Trade Transaction Events handler                                 |
//+------------------------------------------------------------------+
void OnTradeTransaction(
   const MqlTradeTransaction &trans,
   const MqlTradeRequest     &request,
   const MqlTradeResult      &result){
   
   // When a new position is opened
   if(trans.type   != TRADE_TRANSACTION_DEAL_ADD){
      return;
   }
   
   if(trans.symbol != _Symbol){
      return;
   }
   
   if(trans.deal   == 0){
      return;
   }
   
   bool selected = false;

   HistorySelect(TimeCurrent() - 60, TimeCurrent());
   for(int i = 0; i < 6; i++){
      if(HistoryDealSelect(trans.deal)){
         selected = true;
         break;
      }
      Sleep(5);
      HistorySelect(TimeCurrent() - 60, TimeCurrent());
   }
   
   long entry = -1;
   if(selected){
      entry = (long)HistoryDealGetInteger(trans.deal, DEAL_ENTRY);
   }
   
   if(selected && entry == DEAL_ENTRY_IN){
   
      ulong  positionTicket      = trans.position;
      double openPrice           = 0.000000;
      double originalStopLevel   = 0.000000;
      double originalTargetLevel = 0.000000;
      ulong  magicNumber         = 0;
      
      if(PositionSelectByTicket(positionTicket)){
         openPrice           = PositionGetDouble(POSITION_PRICE_OPEN);
         originalStopLevel   = PositionGetDouble(POSITION_SL);
         originalTargetLevel = PositionGetDouble(POSITION_TP);
         magicNumber         = (ulong)PositionGetInteger(POSITION_MAGIC);
      }
      
      if(manageTradesByMagicNumber){
         if(magicNumber != tradeSelectionMagicNumber){
            return;
         }
      }
   
      if(trans.deal_type == DEAL_TYPE_BUY){
         Print("NEW LONG opened (confirmed via history): deal=", trans.deal, " pos=", trans.position);
         ArrayResize(tradeInfo, ArraySize(tradeInfo) + 1);
         tradeInfo[ArraySize(tradeInfo) - 1].ticket                  = positionTicket;
         tradeInfo[ArraySize(tradeInfo) - 1].positionType            = POSITION_TYPE_BUY;
         tradeInfo[ArraySize(tradeInfo) - 1].openPrice               = openPrice;
         tradeInfo[ArraySize(tradeInfo) - 1].originalStopLevel       = originalStopLevel;
         tradeInfo[ArraySize(tradeInfo) - 1].currentStopLevel        = originalStopLevel;
         tradeInfo[ArraySize(tradeInfo) - 1].nextStopLevel           = originalStopLevel + trailingStepPoints * pointValue;
         tradeInfo[ArraySize(tradeInfo) - 1].originalTargetLevel     = originalTargetLevel;
         tradeInfo[ArraySize(tradeInfo) - 1].nextTrailTriggerPrice   = openPrice + usefulTrailingStartPoints  * pointValue;
         tradeInfo[ArraySize(tradeInfo) - 1].isMovedToBreakEven      = false;
         tradeInfo[ArraySize(tradeInfo) - 1].isPartialProfitsSecured = false;
      }
      
      if(trans.deal_type == DEAL_TYPE_SELL){
         Print("NEW SHORT opened (confirmed via history): deal=", trans.deal, " pos=", trans.position);
         tradeInfo[ArraySize(tradeInfo) - 1].ticket                  = positionTicket;
         tradeInfo[ArraySize(tradeInfo) - 1].positionType            = POSITION_TYPE_SELL;
         tradeInfo[ArraySize(tradeInfo) - 1].openPrice               = openPrice;
         tradeInfo[ArraySize(tradeInfo) - 1].originalStopLevel       = originalStopLevel;
         tradeInfo[ArraySize(tradeInfo) - 1].currentStopLevel        = originalStopLevel;
         tradeInfo[ArraySize(tradeInfo) - 1].nextStopLevel           = originalStopLevel - trailingStepPoints * pointValue;
         tradeInfo[ArraySize(tradeInfo) - 1].originalTargetLevel     = originalTargetLevel;
         tradeInfo[ArraySize(tradeInfo) - 1].nextTrailTriggerPrice   = openPrice - usefulTrailingStartPoints  * pointValue;
         tradeInfo[ArraySize(tradeInfo) - 1].isMovedToBreakEven      = false;
         tradeInfo[ArraySize(tradeInfo) - 1].isPartialProfitsSecured = false;
      }
   }
   
   if(!selected){
   
      if(trans.deal_type == DEAL_TYPE_BUY  && trans.position != 0){
         Print("Probable NEW LONG (fallback): deal=", trans.deal, " pos=", trans.position);
      }
      
      if(trans.deal_type == DEAL_TYPE_SELL && trans.position != 0){
         Print("Probable NEW SHORT (fallback): deal=", trans.deal, " pos=", trans.position);
      }
      
   }
   
}
//+------------------------------------------------------------------+

このコードブロックにより、AutoProtectは、銘柄および(設定されている場合は)マジックナンバーのフィルタ条件に合致する新規ポジションを自動的に登録することが保証されます。ここから先は、登録された取引が正式に追跡され、EAのトレーリングストップモジュールによって安全に変更できるようになります。

この段階でEAをコンパイルすると、いくつかのコンパイル時エラーが発生することに気付くでしょう。これは、プログラム内でまだ宣言されていないグローバル変数を参照しているためです。これらのエラーを解消するには、使用する前に変数を定義する必要があります。

次に、EAのさまざまな部分で使用される重要な取引データを格納するグローバル変数をいくつか追加します。これには、freezeLevelPoints、askPrice、bidPrice、pointValueが含まれます。

...

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CTrade Trade;
MqlTradeInfo tradeInfo[];
double closePriceMinutesData [];
int usefulTrailingStartPoints = trailingStartPoints;
long freezeLevelPoints;
double askPrice;
double bidPrice;
double pointValue;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){
   // ---
   return(INIT_SUCCEEDED);
}

...

freezeLevelPoints変数は、ブローカーのフリーズレベルをポイント単位で保持します。この値は、現在の市場価格からストップレベル(ストップロスおよびテイクプロフィット)を変更できる最小距離を定義します。この値はプログラム実行中に変化しないため、OnInit関数内で安全に初期化することが可能です。

...

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){

   //--- Initialize global variables
   freezeLevelPoints  = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);
   
   return(INIT_SUCCEEDED);
}

...

askPrice変数とbidPrice変数は、それぞれ現在のAskとBidの値を表します。これらの値はティックごとに変化するため、OnTick関数内で更新する必要があります。

...

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){

   //--- Scope variables
   askPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   bidPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   
}

...

pointValue変数は、現在の銘柄における最小価格変動単位を表します。この値は一定で変化しないため、freezeLevelPointsと同様に、OnInit関数内で初期化することが可能です。

...

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
int OnInit(){

   //--- Initialize global variables
   freezeLevelPoints  = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL);
   pointValue         = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   
   return(INIT_SUCCEEDED);
}

...

次に進む前に、EAの動作において重要な役割を果たすグローバル変数をさらに2つ定義する必要があります。これらは以下の通りです。

double closePriceMinutesData[];
int usefulTrailingStartPoints = trailingStartPoints;

closePriceMinutesData配列は、1分足(M1)の直近の終値を格納するために使用されます。このデータは後に、価格の上抜け(クロスオーバー)や下抜け(クロスアンダー)などの条件を検出する際に役立ちます。これらの条件は、後続で解説する取引管理機能の一部で重要になります。

usefulTrailingStartPoints変数は、損益分岐点機能とトレーリングストップ機能の動作を調整するために使用されます。通常、この変数はtrailingStartPointsと同じ値を取ります。これは、トレーリングストップが利益方向に何ポイント進んだ時点で開始されるかを定義する値です。しかし、損益分岐点機能とトレーリングストップ機能の両方が同時に有効な場合は、論理的にトレーリングストップは損益分岐点が発動した後に開始されるべきです。この動作を自動的に管理するため、以下のコードをOnInit関数内に配置します。

...

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){

   ...
   
   //--- If both BreakEven and Trailing are enabled,trailing should only start after break-even has triggered.
   if(enableBreakEven && enableTrailingStop){
      usefulTrailingStartPoints = breakEvenTriggerPoints;
   }
   
   // Set arrays as series
   ArraySetAsSeries(closePriceMinutesData, true);
   
   return(INIT_SUCCEEDED);
}

...

ArraySetAsSeries関数は、closePriceMinutesData配列の最新データが常にインデックス0に格納されることを保証します。これにより、直近の価格を簡単に参照できるようになります。

その後、OnTick関数内で、CopyClose関数を使用してM1(1分足)時間軸の直近7本の終値を取得し、配列に読み込みます。

...

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){

   ...
   bidPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   
   // Get some minutes data
   if(CopyClose(_Symbol, PERIOD_M1, 0, 7, closePriceMinutesData) == -1){
      Print("Error while copying minutes datas ", GetLastError());
      return;
   }
   
}

...

この配列をティックごとに更新することで、EAは直近の市場データを小さなローリングウィンドウとして保持します。このデータは、上抜けを用いた判定ロジックが必要な場合に参照することができます。



損益分岐機能

損益分岐点ロジックを実装する前に、コードをより整理され、保守しやすくするための補助関数をいくつか定義しておくことが重要です。これらの小さなユーティリティ関数は、メインのロジックが依存する特定のタスクを実行します。これらの関数は、既存のコード構造のすぐ下に、ユーティリティ関数専用のセクションとして配置します。

...

//--- UTILITY FUNCTIONS
//+------------------------------------------------------------------+
//| Checks if position modification is allowed                       |                               
//+------------------------------------------------------------------+
bool IsTradeModificationAllowed(long freezeLevelPts, ENUM_ORDER_TYPE action, double askPr, double bidPr, double stopLossLvl, double takeProfitLvl){
   double freezeDistance = freezeLevelPts * pointValue;
   
   if(freezeLevelPts == 0){
      return true;
   }
   
   if(action == ORDER_TYPE_BUY) {
      double distanceFromSpotPriceToTP = takeProfitLvl - bidPr;
      double distanceFromSpotPriceToSL = bidPr - stopLossLvl;
      
      if(distanceFromSpotPriceToTP > freezeDistance && distanceFromSpotPriceToSL > freezeDistance){
         return true;
      }
   }
      
   if(action == ORDER_TYPE_SELL){
      double distanceFromSpotPriceToTP = askPr - takeProfitLvl;
      double distanceFromSpotPriceToSL = stopLossLvl - askPr;
      
      if(distanceFromSpotPriceToTP > freezeDistance && distanceFromSpotPriceToSL > freezeDistance){
         return true;
      }    
   }
   return false;
}

//+------------------------------------------------------------------+
//| To detect a crossover                                            |                               
//+------------------------------------------------------------------+
bool IsCrossOver(const double price, const double &closePriceMinsData[]){
   if(closePriceMinsData[1] <= price && closePriceMinsData[0] > price){
      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| To detect a crossunder                                           |                               
//+------------------------------------------------------------------+
bool IsCrossUnder(const double price, const double &closePriceMinsData[]){
   if(closePriceMinsData[1] >= price && closePriceMinsData[0] < price){
      return true;
   }
   return false;
}

最初の関数であるIsTradeModificationAllowedは、ブローカーのフリーズレベルに基づき、ポジションを変更できるかどうかを確認します。フリーズレベルとは、注文やストップレベルが現在の市場価格にどれだけ近づけるかを定義する制限です。制限範囲内で価格に近すぎる場合、ブローカーは変更を拒否します。この関数では、現在価格とストップロスおよびテイクプロフィットレベルとの距離を計算し、フリーズ距離よりも大きい場合にのみ更新を許可します。

次に、2つのつのシンプルなヘルパー関数IsCrossOverとIsCrossUnderを定義します。これらは、直近の1分足の終値が指定された価格レベルを上抜けまたは下抜けしたかを検出します。これにより、ストップロスを変更する前に、価格が損益分岐点トリガーレベルを超えたことを確認できます。これらの関数を別に分けることで、プログラムをモジュール化し、可読性を高め、将来的な拡張も容易になります。たとえば、同じ上抜け判定ロジックをトレーリングストップや部分決済にも再利用することが可能です。

ヘルパー関数を定義した後、ManageBreakEven関数を作成します。この関数は、価格が一定の利益距離に達したときに、取引のストップロスを自動的に損益分岐点まで移動する役割を担います。

...

//--- UTILITY FUNCTIONS

...

//+------------------------------------------------------------------+
//| To mange the trade break even functionality                      |                               
//+------------------------------------------------------------------+
void ManageBreakEven   (){
   int totalPositions = PositionsTotal();
   for(int i = totalPositions - 1; i >= 0; i--){
      ulong ticket = PositionGetTicket(i);
      if(ticket != 0){
         // Get some useful position properties
         ENUM_POSITION_TYPE positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
         double openPrice                = PositionGetDouble(POSITION_PRICE_OPEN);
         double currentPrice             = PositionGetDouble(POSITION_PRICE_CURRENT);
         string symbol                   = PositionGetString(POSITION_SYMBOL);
         double stopLevel                = PositionGetDouble(POSITION_SL);
         double takeProfitLevel          = PositionGetDouble(POSITION_TP);
         ulong magicNumber               = PositionGetInteger(POSITION_MAGIC);
         
         if(symbol == _Symbol){
         
            if(!manageTradesByMagicNumber){

               if(positionType == POSITION_TYPE_BUY ){                 
                  for(int j = ArraySize(tradeInfo) - 1; j >= 0; j--){
                     if(tradeInfo[j].ticket == ticket && tradeInfo[j].isMovedToBreakEven == false){
                        if(IsCrossOver(openPrice + breakEvenTriggerPoints * pointValue, closePriceMinutesData)){
                           // Move SL to breakeven + breakEvenLockPoints
                           double newStopLevel = openPrice + (breakEvenLockPoints * pointValue);
                           if(IsTradeModificationAllowed(freezeLevelPoints, ORDER_TYPE_BUY, askPrice, bidPrice, stopLevel, takeProfitLevel)){
                              if(!Trade.PositionModify(ticket, newStopLevel, takeProfitLevel)){
                                 Print("Error while moving Stop Loss to breakeven! ", GetLastError());
                                 Print(Trade.ResultComment());
                              }
                              tradeInfo[j].isMovedToBreakEven = true;
                           }
                        }
                     }
                  }
               }
       
               if(positionType == POSITION_TYPE_SELL){
                  for(int j = ArraySize(tradeInfo) - 1; i >= 0; i--){
                     if(tradeInfo[j].ticket == ticket && tradeInfo[j].isMovedToBreakEven == false){
                        if(IsCrossUnder(openPrice - breakEvenTriggerPoints * pointValue, closePriceMinutesData)){
                           // Move SL to breakeven + breakEvenLockPoints
                           double newStopLevel = openPrice - (breakEvenLockPoints * pointValue);
                           if(IsTradeModificationAllowed(freezeLevelPoints, ORDER_TYPE_BUY, askPrice, bidPrice, stopLevel, takeProfitLevel)){
                              if(!Trade.PositionModify(ticket, newStopLevel, takeProfitLevel)){
                                 Print("Error while moving Stop Loss to breakeven! ", GetLastError());
                                 Print(Trade.ResultComment());
                              }
                              tradeInfo[j].isMovedToBreakEven = true;
                           }
                        }
                     }
                  }
               }

            }
            
            if(manageTradesByMagicNumber){
               if(magicNumber == tradeSelectionMagicNumber){
                  if(positionType == POSITION_TYPE_BUY ){                 
                     for(int j = ArraySize(tradeInfo) - 1; j >= 0; j--){
                        if(tradeInfo[j].ticket == ticket && tradeInfo[j].isMovedToBreakEven == false){
                           if(IsCrossOver(openPrice + breakEvenTriggerPoints * pointValue, closePriceMinutesData)){
                              // Move SL to breakeven + breakEvenLockPoints
                              double newStopLevel = openPrice + (breakEvenLockPoints * pointValue);
                              if(IsTradeModificationAllowed(freezeLevelPoints, ORDER_TYPE_BUY, askPrice, bidPrice, stopLevel, takeProfitLevel)){
                                 if(!Trade.PositionModify(ticket, newStopLevel, takeProfitLevel)){
                                    Print("Error while moving Stop Loss to breakeven! ", GetLastError());
                                    Print(Trade.ResultComment());
                                 }
                                 tradeInfo[j].isMovedToBreakEven = true;
                              }
                           }
                        }
                     }
                  }
          
                  if(positionType == POSITION_TYPE_SELL){
                     for(int j = ArraySize(tradeInfo) - 1; i >= 0; i--){
                        if(tradeInfo[j].ticket == ticket && tradeInfo[j].isMovedToBreakEven == false){
                           if(IsCrossUnder(openPrice - breakEvenTriggerPoints * pointValue, closePriceMinutesData)){
                              // Move SL to breakeven + breakEvenLockPoints
                              double newStopLevel = openPrice - (breakEvenLockPoints * pointValue);
                              if(IsTradeModificationAllowed(freezeLevelPoints, ORDER_TYPE_BUY, askPrice, bidPrice, stopLevel, takeProfitLevel)){
                                 if(!Trade.PositionModify(ticket, newStopLevel, takeProfitLevel)){
                                    Print("Error while moving Stop Loss to breakeven! ", GetLastError());
                                    Print(Trade.ResultComment());
                                 }
                                 tradeInfo[j].isMovedToBreakEven = true;
                              }
                           }
                        }
                     }
                  }
               }
            }
            
         }else{
            continue;
         }
      }else{
         Print("Error while getting a position ticket!", GetLastError());
         continue;
      }
   }
}

関数内では、EAは口座のすべてのポジションをループ処理します。各取引に対して、まず銘柄がEAが稼働しているチャートの銘柄と一致するかを確認します。ユーザーがマジックナンバーによる取引フィルタリングを有効にしている場合は、指定されたマジックナンバーと一致する取引のみが管理対象であることをさらに確認します。

買いポジションの場合、EAは市場価格が損益分岐点トリガーレベルを上抜けるのを待ちます。この条件が満たされると、関数は新しいストップロスレベルを計算します。具体的には、建値にユーザー定義のロックポイントを加えた値です。このロックポイントは、損益分岐点をわずかに上回る小さなバッファを表します。その後、IsTradeModificationAllowed関数を呼び出し、ブローカーが変更を許可していることを確認してからストップロスを更新します。

売りポジションの場合も同様のロジックが適用されますが、方向は逆になります。価格が損益分岐点トリガーレベルを下抜けると、EAはストップロスを建値からバッファポイント分だけ下に移動します。

この設計により、損益分岐点機能はEA起動後に開かれたアクティブな取引のみに反応します。過去の取引や関連のない取引には影響を与えません。変更が失敗した場合は、EAが明確なエラーメッセージを表示し、デバッグを助けます。

要するに、この関数の目的は、取引が定義されたポイント距離だけ有利に動いた場合に、それ以上損失に転じないようにトレーダーの資本を自動的に保護することです。この小さいながらも重要な機能は、AutoProtect スマート取引マネージャーEAの基盤のひとつとなっています。



トレーリングストップ機能

トレーリングストップ機能は、EAがストップロスレベルを自動的に移動させ、価格がさらに利益方向に進むにつれて段階的に利益を確保できるようにします。これにより、トレーダーは常にポジションを監視することなく、利益を確実にロックインすることが可能になります。トレーリング処理は徐々におこなわれるため、有利な取引が不必要に早く決済されることを防ぎつつ、市場が反転した場合には利益を保護することができます。次に、ManageBreakEven関数のすぐ下に、トレーリングストップ用の関数ManageTrailingStopを定義します。

...

//+------------------------------------------------------------------+
//| To mange the trade break even functionality                      |                               
//+------------------------------------------------------------------+
void ManageBreakEven   (){
   ...
}

//+------------------------------------------------------------------+
//| To manage the trade trailing stop functionality                  |                               
//+------------------------------------------------------------------+
void ManageTrailingStop(){
   int totalPositions = PositionsTotal();
   // Loop through all open positions
   for(int i = totalPositions - 1; i >= 0; i--){
      ulong ticket = PositionGetTicket(i);
      if(ticket != 0){
         // Get some useful position properties
         ENUM_POSITION_TYPE positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
         double openPrice                = PositionGetDouble (POSITION_PRICE_OPEN);
         double currentPrice             = PositionGetDouble (POSITION_PRICE_CURRENT);
         string symbol                   = PositionGetString (POSITION_SYMBOL);
         double stopLevel                = PositionGetDouble (POSITION_SL);
         double takeProfitLevel          = PositionGetDouble (POSITION_TP);
         ulong magicNumber               = PositionGetInteger(POSITION_MAGIC);
         // Skip positions not matching this financial security
         if(symbol == _Symbol){            
            if(!manageTradesByMagicNumber){

               if(positionType == POSITION_TYPE_BUY ){
                  
                  // 07-10-2025
                  for(int i = ArraySize(tradeInfo) - 1; i >= 0; i--){
                     if(tradeInfo[i].ticket == ticket){
                        if(IsCrossOver(tradeInfo[i].nextTrailTriggerPrice, closePriceMinutesData)){
                           // Physically trail SL only when next SL level is above current SL Level
                           if(tradeInfo[i].nextStopLevel > tradeInfo[i].currentStopLevel){
                              if(IsTradeModificationAllowed(freezeLevelPoints, ORDER_TYPE_BUY, askPrice, bidPrice, stopLevel, takeProfitLevel)){
                                 if(!Trade.PositionModify(ticket, tradeInfo[i].nextStopLevel, takeProfitLevel)){
                                    Print("Error while trailing Stop Loss! ", GetLastError());
                                    Print(Trade.ResultComment());
                                    Print(Trade.ResultRetcode());
                                 }
                              }
                           }
                           tradeInfo[i].currentStopLevel = tradeInfo[i].currentStopLevel + trailingStepPoints * pointValue;
                           tradeInfo[i].nextStopLevel    = tradeInfo[i].nextStopLevel    + trailingStepPoints * pointValue;                          
                        }
                     }
                  }
               }
               
               if(positionType == POSITION_TYPE_SELL){
                  // 07-10-2025
                  for(int i = ArraySize(tradeInfo) - 1; i >= 0; i--){
                     if(tradeInfo[i].ticket == ticket){
                        if(IsCrossUnder(tradeInfo[i].nextTrailTriggerPrice, closePriceMinutesData)){
                           // Physically trail SL only when next SL level is above current SL Level
                           if(tradeInfo[i].nextStopLevel < tradeInfo[i].currentStopLevel){
                              if(IsTradeModificationAllowed(freezeLevelPoints, ORDER_TYPE_SELL, askPrice, bidPrice, stopLevel, takeProfitLevel)){
                                 if(!Trade.PositionModify(ticket, tradeInfo[i].nextStopLevel, takeProfitLevel)){
                                    Print("Error while trailing Stop Loss! ", GetLastError());
                                    Print(Trade.ResultComment());
                                    Print(Trade.ResultRetcode());
                                 }
                              }
                           }
                           tradeInfo[i].currentStopLevel = tradeInfo[i].currentStopLevel - trailingStepPoints * pointValue;
                           tradeInfo[i].nextStopLevel    = tradeInfo[i].nextStopLevel    - trailingStepPoints * pointValue;                          
                        }
                     }
                  }
               }
            }
            
            if( manageTradesByMagicNumber){
               if(magicNumber == tradeSelectionMagicNumber){
 
                  if(positionType == POSITION_TYPE_BUY ){
                     // 07-10-2025
                     for(int i = ArraySize(tradeInfo) - 1; i >= 0; i--){
                        if(tradeInfo[i].ticket == ticket){
                           if(IsCrossOver(tradeInfo[i].nextTrailTriggerPrice, closePriceMinutesData)){
                              // Physically trail SL only when next SL level is above current SL Level
                              if(tradeInfo[i].nextStopLevel > tradeInfo[i].currentStopLevel){
                                 if(IsTradeModificationAllowed(freezeLevelPoints, ORDER_TYPE_BUY, askPrice, bidPrice, stopLevel, takeProfitLevel)){
                                    if(!Trade.PositionModify(ticket, tradeInfo[i].nextStopLevel, takeProfitLevel)){
                                       Print("Error while trailing Stop Loss! ", GetLastError());
                                       Print(Trade.ResultComment());
                                       Print(Trade.ResultRetcode());
                                    }
                                 }
                              }
                              tradeInfo[i].currentStopLevel = tradeInfo[i].currentStopLevel + trailingStepPoints * pointValue;
                              tradeInfo[i].nextStopLevel    = tradeInfo[i].nextStopLevel    + trailingStepPoints * pointValue;                          
                           }
                        }
                     }
                  }
                  if(positionType == POSITION_TYPE_SELL){
                     // 07-10-2025
                     for(int i = ArraySize(tradeInfo) - 1; i >= 0; i--){
                        if(tradeInfo[i].ticket == ticket){
                           if(IsCrossUnder(tradeInfo[i].nextTrailTriggerPrice, closePriceMinutesData)){
                              // Physically trail SL only when next SL level is above current SL Level
                              if(tradeInfo[i].nextStopLevel < tradeInfo[i].currentStopLevel){
                                 if(IsTradeModificationAllowed(freezeLevelPoints, ORDER_TYPE_SELL, askPrice, bidPrice, stopLevel, takeProfitLevel)){
                                    if(!Trade.PositionModify(ticket, tradeInfo[i].nextStopLevel, takeProfitLevel)){
                                       Print("Error while trailing Stop Loss! ", GetLastError());
                                       Print(Trade.ResultComment());
                                       Print(Trade.ResultRetcode());
                                    }
                                 }
                              }
                              tradeInfo[i].currentStopLevel = tradeInfo[i].currentStopLevel - trailingStepPoints * pointValue;
                              tradeInfo[i].nextStopLevel    = tradeInfo[i].nextStopLevel    - trailingStepPoints * pointValue;                          
                           }
                        }
                     }
                  }
               }else{
                  continue;
               }
            }
            
         }else{
            continue;
         }
      }else{
         Print("Error while getting a position ticket! ", GetLastError());
         continue;
      }
   }
}

ManageTrailingStop関数は、すべてのポジションを順番にチェックし、トレーリングの条件を満たすポジションがあるかどうかを確認します。各ポジションに対して、まずその取引がEAが稼働しているチャートの銘柄と一致するか、またユーザー設定に応じて特定のマジックナンバーで管理すべき取引かどうかを判定します。これにより、関連する取引のみが変更対象となります。

関数内では、IsCrossOverおよびIsCrossUnderヘルパー関数を使用して、直近の市場価格を比較します。これらのチェックにより、ストップロスの調整をトリガーする特定の価格レベルを価格がクロスしたかどうかを確認します。上抜けまたは下抜けが発生した場合、EAは新しいストップロスレベルを計算し、ポジションの方向に応じて事前定義されたステップサイズ(trailingStepPoints)分だけ移動させます。

取引を変更する前に、関数はIsTradeModificationAllowedを使用して、安全に変更できるかを確認します。これにより、価格がフリーズレベルに近すぎる場合にブローカーエラーが発生するのを防ぎます。確認後、EAはTrade.PositionModifyメソッドを用いてストップロスを更新し、処理中に発生したエラーをログに出力します。

トレーリング条件が満たされるたびに、EAはtradeInfo構造体内に保存されている取引データを更新します。currentStopLevelとnextStopLevelの両方を増加させることで、将来のトレーリング処理がスムーズに継続されるようにします。この関数は買いポジションと売りポジションの両方に対応しており、ロジックは方向のみ逆になります。

この機能により、トレーダーはストップロスを手動でトレールする必要がなくなります。EAが自動的にストップを調整し、有利な取引を維持しつつ、リスクの露出を最小化します。



部分利益確定機能

ManageTrailingStop関数のすぐ下に、ManagePartialClose関数を追加します。

...

//+------------------------------------------------------------------+
//| To manage the trade trailing stop functionality                  |                               
//+------------------------------------------------------------------+
void ManageTrailingStop(){
   ...
}

//+------------------------------------------------------------------+
//| To manage the trade partial close functionality                  |                               
//+------------------------------------------------------------------+
void ManagePartialClose(){
   int totalPositions = PositionsTotal();
      
   for(int i = totalPositions - 1; i >= 0; i--){
      ulong ticket = PositionGetTicket(i);
      if(ticket != 0){
         // Get some useful position properties
         ENUM_POSITION_TYPE positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
         double openPrice                = PositionGetDouble (POSITION_PRICE_OPEN);
         double currentPrice             = PositionGetDouble (POSITION_PRICE_CURRENT);
         string symbol                   = PositionGetString (POSITION_SYMBOL);
         ulong magicNumber               = PositionGetInteger(POSITION_MAGIC);
         double positionVolume           = PositionGetDouble (POSITION_VOLUME);
         if(symbol == _Symbol){
         
            double volumeToDecrease = NormalizeDouble((partialClosePercent / 100.0) * positionVolume, 2);
            
            if(!manageTradesByMagicNumber){           
               if(positionType == POSITION_TYPE_BUY ){               
                  for(int j = ArraySize(tradeInfo) - 1; j >= 0; j--){
                     if(tradeInfo[j].ticket == ticket && tradeInfo[j].isPartialProfitsSecured == false){
                        if(IsCrossOver(openPrice + partialCloseTriggerPoints * pointValue, closePriceMinutesData)){
                           if(!Trade.PositionClosePartial(ticket, volumeToDecrease)){
                              Print("Error closing partial volume! ", GetLastError());
                              Print(Trade.ResultComment());
                           }
                           tradeInfo[j].isPartialProfitsSecured = true;
                        }
                     }
                  }
               }
               
               if(positionType == POSITION_TYPE_SELL){               
                  for(int j = ArraySize(tradeInfo) - 1; j >= 0; j--){
                     if(tradeInfo[j].ticket == ticket && tradeInfo[j].isPartialProfitsSecured == false){
                        if(IsCrossOver(openPrice - partialCloseTriggerPoints * pointValue, closePriceMinutesData)){
                           if(!Trade.PositionClosePartial(ticket, volumeToDecrease)){
                              Print("Error closing partial volume! ", GetLastError());
                              Print(Trade.ResultComment());
                           }
                           tradeInfo[j].isPartialProfitsSecured = true;
                        }
                     }
                  }
               }
               
            }
            
            if( manageTradesByMagicNumber){
               if(magicNumber == tradeSelectionMagicNumber){
               
                  if(positionType == POSITION_TYPE_BUY ){               
                     for(int j = ArraySize(tradeInfo) - 1; j >= 0; j--){
                        if(tradeInfo[j].ticket == ticket && tradeInfo[j].isPartialProfitsSecured == false){
                           if(IsCrossOver(openPrice + partialCloseTriggerPoints * pointValue, closePriceMinutesData)){
                              if(!Trade.PositionClosePartial(ticket, volumeToDecrease)){
                                 Print("Error closing partial volume! ", GetLastError());
                                 Print(Trade.ResultComment());
                              }
                              tradeInfo[j].isPartialProfitsSecured = true;
                           }
                        }
                     }
                  }
                  
                  if(positionType == POSITION_TYPE_SELL){               
                     for(int j = ArraySize(tradeInfo) - 1; j >= 0; j--){
                        if(tradeInfo[j].ticket == ticket && tradeInfo[j].isPartialProfitsSecured == false){
                           if(IsCrossOver(openPrice - partialCloseTriggerPoints * pointValue, closePriceMinutesData)){
                              if(!Trade.PositionClosePartial(ticket, volumeToDecrease)){
                                 Print("Error closing partial volume! ", GetLastError());
                                 Print(Trade.ResultComment());
                              }
                              tradeInfo[j].isPartialProfitsSecured = true;
                           }
                        }
                     }
                  }
               
               }
            }
            
         }
         
      }else{
         Print("Error while getting a position ticket!", GetLastError());
         continue;
      }
   }
}

この関数は、取引が定義された利益レベルに達した際に、ポジションの一部を部分決済する処理を担当します。これにより、EAは利益の一部を確保しつつ、残りのポジションを市場で継続して運用することができるようになります。

関数はまず、すべてのポジションをループ処理することから始まります。各ポジションに対して、ポジションタイプ(買いまたは売り)、建値、現在価格、銘柄、マジックナンバー、取引量などの有用な情報を取得します。

次に、ポジションが現在のチャート銘柄に属しているかを確認します。マジックナンバーによる取引管理が有効な場合は、指定されたマジックナンバーと一致するかどうかも確認してから処理を進めます。

関数は、部分決済する取引量を、現在の取引量に部分決済パーセンテージを適用して計算します。価格が指定されたトリガー距離(ポイント単位)だけ有利に動いた時点で、EAはCTrade.PositionClosePartialメソッドを使用して計算された部分の取引を決済します。

買いポジションの場合、部分決済は現在価格が建値から定義された閾値分上昇したときに発動します。売りポジションの場合は、現在価格が建値から同じ距離分下落したときに発動します。

部分決済処理中にエラーが発生した場合は、エラーメッセージと失敗理由が操作ログに出力され、デバッグを助けます。



テストと検証

Trade Manager EAの開発が完了したら、次のステップはその機能をテストすることです。このプロセスにより、各機能が実際の取引環境下で期待通りに動作するかを確認できます。損益分岐点、トレーリングストップ、部分決済などの各機能は、個別にテストして正しく動作することを確認する必要があります。

これを実現するために、起動されると自動的に取引を開くシンプルなテスト用EAを使用します。その後、機能を1つずつ有効化し、取引に対する影響を観察します。スクリーンショットやメモを記録することで、EA内のすべての機能が正しく動作していることを明確かつ信頼性の高い方法で検証できます。

保護機能を効果的にテストするために、AutoProtect.mq5という新しいEAファイルを作成します。このファイルは起動時に自動的に取引を開き、各機能が制御された環境下でどのように動作するかを観察できるようにします。以下に、テスト用EAで使用する追加の関数とコードの説明を示します。

1. アクティブポジションの確認

    まず、既存のユーティリティ関数のすぐ下に、IsThereAnActiveBuyPositionとIsThereAnActiveSellPositionの2つの関数を定義します。

    ...
    
    //+------------------------------------------------------------------+
    //| To manage the trade partial close functionality                  |                               
    //+------------------------------------------------------------------+
    void ManagePartialClose(){
       ...
    }
    
    //+------------------------------------------------------------------+
    //| To check if there is an active buy position opened by this EA    |                               
    //+------------------------------------------------------------------+
    bool IsThereAnActiveBuyPosition(ulong magicNm){
       for(int i = PositionsTotal() - 1; i >= 0; i--){
          ulong ticket = PositionGetTicket(i);
          if(ticket == 0){
             Print("Error while fetching position ticket ", _LastError);
             continue;
          }else{
             if(PositionGetInteger(POSITION_MAGIC) == magicNm && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){
                return true;
             }
          }
       }
       return false;
    }
    
    //+------------------------------------------------------------------+
    //| To check if there is an active sell position opened by this EA   |                               
    //+------------------------------------------------------------------+
    bool IsThereAnActiveSellPosition(ulong mgcNumber){
       for(int i = PositionsTotal() - 1; i >= 0; i--){
          ulong ticket = PositionGetTicket(i);
          if(ticket == 0){
             Print("Error while fetching position ticket ", _LastError);
             continue;
          }else{
             if(PositionGetInteger(POSITION_MAGIC) == mgcNumber && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){
                return true;
             }
          }
       }
       return false;
    }
    
    ...

    これらの関数は、それぞれ買いポジションまたは売りポジションが現在アクティブかどうかを確認するために使用されます。各関数は、PositionsTotalおよびPositionGetTicketを使用して口座のすべてのポジションをループ処理します。その後、マジックナンバーおよびポジションタイプを比較して、対象のポジションが自分のEAによって開かれたものかどうかを判定します。該当するポジションが見つかった場合、関数はtrueを返し、見つからなければfalseを返します。これにより、EAが同じ種類の取引を不必要に複数開くことを防ぐことができます。

    2. 取引の実行を一度だけ許可する

    次に、新しいグローバル変数を導入します。

    bool isNewTradeAllowed;

    この変数は、新しい取引を開くことができるかどうかを制御するフラグとして使用されます。OnInit関数内では、次のように初期化します。

    isNewTradeAllowed  = true;

    これは、EA起動時に1回だけ取引を開くことを許可することを意味します。最初の取引が開かれた後、このフラグはfalseに設定され、追加の取引が自動的に開かれることを防ぎます。

    3. テスト取引を自動的に開く

    OnTick関数内で、EA起動直後に買いポジションを自動的に建てるロジックを追加します。

    ...
    
    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick(){
    
       ...
       
       //--- Get some minutes data
       if(CopyClose(_Symbol, PERIOD_M1, 0, 7, closePriceMinutesData) == -1){
          Print("Error while copying minutes datas ", GetLastError());
          return;
       }
       
       //--- Open a long position immediately after launch
       if(isNewTradeAllowed){
          if(!IsThereAnActiveBuyPosition(tradeSelectionMagicNumber) && !IsThereAnActiveSellPosition(tradeSelectionMagicNumber)){
             Trade.Buy(1.0, _Symbol, askPrice, askPrice - 400 * pointValue, askPrice + 800 * pointValue);
             isNewTradeAllowed = false;
          }
       }
    }
    
    ...

    ここでは、EAが指定されたマジックナンバーに対して既存のポジションがないかを確認します。ポジションが見つからない場合、あらかじめ定義されたストップロスおよびテイクプロフィットレベルを使用して買いポジションを建てます。取引が開かれた後、isNewTradeAllowedはfalseに設定され、これ以上の取引が自動的に開かれないようにします。

    4. 保護機能の呼び出し

    AutoProtect.mq5とAutoProtectTest.mq5はどちらも、以下に示すように主要な保護機能を呼び出します。

    ...
    
    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick(){
    
       ...
       
       //--- Open a long position immediately after launch
       if(isNewTradeAllowed){
          ...
       }
       
       if(enableBreakEven){
          ManageBreakEven();
       }
       
       if(enableTrailingStop){
          ManageTrailingStop();
       }
       
       if(enablePartialClose){
          ManagePartialClose();
       }
       
    }
    
    ...

    各条件は、特定の機能が有効かどうかを確認します。有効であれば、対応する関数が実行されます。この方法により、EAの設定で入力パラメータを切り替えるだけで、機能を1つずつテストすることが可能になります。

    5. チャート表示の設定

    テスト環境をより見やすくするために、ConfigureChartAppearanceというヘルパー関数を定義します。

    ...
    
    //+------------------------------------------------------------------+
    //| To check if there is an active sell position opened by this EA   |                               
    //+------------------------------------------------------------------+
    bool IsThereAnActiveSellPosition(ulong mgcNumber){
       ...
    }
    
    //+------------------------------------------------------------------+
    //| This function configures the chart's appearance                  |                               
    //+------------------------------------------------------------------+
    bool ConfigureChartAppearance(){
       if(!ChartSetInteger(0, CHART_COLOR_BACKGROUND, clrWhiteSmoke)){
          Print("Error while setting chart background, ", GetLastError());
          return false;
       }
       if(!ChartSetInteger(0, CHART_SHOW_GRID, false)){
          Print("Error while setting chart grid, ", GetLastError());
          return false;
       }
       if(!ChartSetInteger(0, CHART_MODE, CHART_CANDLES)){
          Print("Error while setting chart mode, ", GetLastError());
          return false;
       }
       if(!ChartSetInteger(0, CHART_COLOR_CANDLE_BULL, clrGreen)){
          Print("Error while setting bullish candles color, ", GetLastError());
          return false;
       }   
       if(!ChartSetInteger(0, CHART_COLOR_CANDLE_BEAR, clrDarkRed)){
          Print("Error while setting bearish candles color, ", GetLastError());
          return false;
       }
       if(!ChartSetInteger(0, CHART_COLOR_CHART_UP, clrGreen)){
          Print("Error while setting bearish candles color, ", GetLastError());
          return false;
       }
       if(!ChartSetInteger(0, CHART_COLOR_CHART_DOWN, clrDarkRed)){
          Print("Error while setting bearish candles color, ", GetLastError());
          return false;
       }
       if(!ChartSetInteger(0, CHART_COLOR_BID, clrDarkRed)){
          Print("Error while setting chart bid line, ", GetLastError());
          return false;
       }
       if(!ChartSetInteger(0, CHART_COLOR_ASK, clrGreen)){
          Print("Error while setting chart ask line, ", GetLastError());
          return false;
       }
       if(!ChartSetInteger(0, CHART_SHOW_ONE_CLICK, true)){
          Print("Error while setting one click buttons, ", GetLastError());
          return false;
       }
       if(!ChartSetInteger(0, CHART_COLOR_STOP_LEVEL, clrDarkBlue)){
          Print("Error while setting stop levels, ", GetLastError());
          return false;
       }
       if(!ChartSetInteger(0, CHART_COLOR_FOREGROUND, clrBlack)){
          Print("Error while setting chart foreground, ", GetLastError());
          return false;
       }
       
       return true;
    }
    
    //+------------------------------------------------------------------+

    この関数は、テスト中にチャートをより見やすくするためにチャートの表示をカスタマイズします。背景色、ローソク足の色、グリッド線、ストップレベルラインなどの要素を調整します。

    例は以下の通りです。

    • 背景色はclrWhiteSmokeに設定されます。
    • 陽線は緑色、陰線は濃い赤色に設定されます。
    • BidラインとAskラインも識別しやすいように色付けされます。

    各設定は、ChartSetInteger MQL5関数を使用して適用されます。エラーが発生した場合は、関数内でエラーメッセージを表示し、falseを返します。この関数はOnInit内で呼び出されます。

    ...
    
    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit(){
       
       if(!ConfigureChartAppearance()){
          Print("Error while configuring the chart's appearance, ", GetLastError());
          return(INIT_FAILED);
       }
       
       ...
       
    }
    
    ...
    

    これにより、EAが稼働を開始する前にチャートが正しく設定されることが保証されます。設定に失敗した場合は、EAは初期化を停止し、視覚的な混乱を防ぎます。

    この準備により、各機能をテストするためのクリーンで自動化された環境が整います。テストは再現可能で制御されており、特に損益分岐点、トレーリングストップ、部分決済の動作を確認する際に観察しやすくなります。

    このプロセスを開始するにあたり、まず、損益分岐点機能をテストします。取引が利益方向に動いた際に、ストップロスがリスクフリーの水準に自動的に調整されることを確認するためです。

    トレーリングストップおよび部分決済機能は無効にし、損益分岐点機能のみを有効にします。次に、入力パラメータbreakEvenTriggerPointsを200に設定し、EAをEURUSDチャート上で起動して、その動作を観察します。

    損益分岐点の入力パラメータ

    EAを起動するとすぐに、新しいロングポジションが建てられ、ストップロスは建値から400ポイント下に、テイクプロフィットは建値から800ポイント上に設定されていることが確認できます。

    ポジションオープン

    ポジションが200ポイント利益方向に動くと、ストップロスが自動的に損益分岐点水準に調整されていることが確認できます。

    損益分岐点に到達する

    次に、トレーリングストップ機能をテストします。他のすべての機能は無効にし、入力パラメータでトレーリングストップ機能のみを有効にします。その後、EAをチャート上で起動し、価格がさらに利益方向に動くにつれてストップロスがどのように調整されるかを観察します。

    トレーリングストップのテスト

    trailingStartPointsを50、trailingStepPointsを20に設定します。次に、EAをEURUSDチャート上で起動し、その動作の様子を観察します。起動直後に、EAがロングポジションを建て、ストップロスは建値から400ポイント下、テイクプロフィットは建値から800ポイント上に設定されていることが確認できます。

    トレーリングストップ機能の初回起動

    この場合、トレーリングストップは1.05945から1.05985に移動されました。

    トレーリングストップ機能のテスト手順1

    次に、ストップレベルは1.05985から1.06025に調整されました。

    トレーリングストップ機能のテスト手順2

    ここでもトレーリングストップは1.0625から1.06165に調整されました。

    トレーリングストップ機能のテスト手順3

    これにより、トレーリングストップ機能が期待通りに動作していることが明確に確認できます。最後に、部分決済機能をテストします。他のすべての機能は無効にし、部分決済機能のみを有効にします。PartialTriggerPointsを200、PartialClosePercentを50.0に設定します。これは、価格が200ポイント利益方向に動いた際に、ポジションの建玉量の50%が自動的に決済されることを意味します。 

    部分決済設定

    それでは、EAを再びEURUSDチャート上で起動し、その動作の様子を観察します。再び、EAが起動直後にロングポジションを建てることが確認できます。このポジションのストップロスは建値から400ポイント下に、テイクプロフィットは建値から800ポイント上に設定されています。また、ポジションが1標準ロットの数量で開かれていることにも注意してください。

    部分決済

    取引が200ポイント利益方向に動くと、建玉量の50%が決済されることが確認できます。これにより、部分決済機能が期待通りに動作していることが確認され、テストおよび検証フェーズが無事完了したことを示します。



    結論

    ほとんどのトレーダーは、まず取引を手動で管理することから始めます。チャートを見ながら、直感でストップロスを調整し、利益を確定するのです。しかし、この方法では感情的な判断ミスや利益の取り逃しが発生しやすくなります。

    本記事では、これらの作業を自動化するスマート取引マネージャーEAを構築しました。このEAは、ストップロスを損益分岐点まで移動させ、利益をトレーリングし、ポジションを部分決済します。各機能はリアルタイム環境でテストされ、信頼性が確認されています。

    自動化は単なる利便性をもたらすだけでなく、規律と一貫性も提供します。AutoProtect EAを使用することで、トレーダーは資金を保護し、利益を確保し、ポジションを効率的に管理することができます。これにより、取引は感情的な反応ではなく、構造化されたルールに基づいた過程になります。

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

    添付されたファイル |
    AutoProtect.mq5 (30.1 KB)
    AutoProtectTest.mq5 (34.25 KB)
    EAのサンプル EAのサンプル
    一般的なMACDを使ったEAを例として、MQL4開発の原則を紹介します。
    定量的トレンド分析:Pythonで統計情報を収集する 定量的トレンド分析:Pythonで統計情報を収集する
    外国為替市場における定量的トレンド分析とは何でしょうか。本記事では、EURUSD通貨ペアにおけるトレンド、その大きさ、分布に関する統計を収集します。利益を生む取引用エキスパートアドバイザー(EA)の開発に、定量的トレンド分析がどのように役立つかも示します。
    エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
    この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
    初級から中級まで:構造体(VI) 初級から中級まで:構造体(VI)
    本記事では、共通の構造化されたコード基盤をどのように実装していくかについて解説します。目的は、プログラミングの作業負担を軽減し、使用しているプログラミング言語(ここではMQL5)そのものが持つ潜在能力を最大限に活用することです。