English Deutsch
preview
MQL5経済指標カレンダーを使った取引(第6回):ニュースイベント分析とカウントダウンタイマーによるトレードエントリーの自動化

MQL5経済指標カレンダーを使った取引(第6回):ニュースイベント分析とカウントダウンタイマーによるトレードエントリーの自動化

MetaTrader 5トレーディング | 23 6月 2025, 07:51
47 2
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

本記事では、MQL5経済指標カレンダーシリーズの次のステップとして、リアルタイムのニュース分析に基づいた自動トレードエントリーを実現します。前回の記事(第5回)でおこなったダッシュボード機能の強化を土台とし、今回は取引ロジックの統合に焦点を当てます。具体的には、ユーザーが指定したフィルターと時間オフセットを使ってニュースイベントを検出し、予想値と前回値を比較したうえで、買いまたは売りの注文を自動で発行するシステムを構築します。さらに、ニュース公開までの残り時間を表示し、取引後には自動的にリセットされる動的なカウントダウンタイマーを実装することで、常に変化する市場状況に対応可能な取引戦略を保ちます。この記事は次のトピックに沿って構成されています。

  1. 取引ロジックの要件を理解する
  2. MQL5での取引ロジックの実装
  3. カウントダウンタイマーの作成と管理
  4. 取引ロジックのテスト
  5. 結論

それでは早速、これらの構成要素がどのように連携し、精度と信頼性をもって取引エントリーを自動化するのかを詳しく見ていきましょう。


取引ロジックの要件を理解する

私たちの自動売買システムの最初のステップは、どの経済指標イベントが取引の候補として適しているかを特定することです。候補イベントは、その発表予定時刻に対して、ユーザー定義のオフセット入力によって決まる特定の時間ウィンドウ内に収まるものと定義します。次に、「ニュース公開前に取引する」などの取引モードを指定する入力項目を追加します。例えば「発表前に取引」モードでは、現在時刻がイベントの予定発表時刻からオフセット(例:5分)を引いた時刻と、発表時刻の間にある場合、そのイベントが有効と見なされます。つまり、実際の発表の5分前に取引するということです。

フィルタリングは、関係のあるニュースイベントのみを対象とするために極めて重要です。そこで本システムでは、複数のフィルターを使用します。通貨フィルターでは選択された通貨ペアに関連するイベントのみに絞り、重要度フィルターでは、選択したインパクトレベルのイベントのみに制限します。さらに、時間フィルターでは、全体的な時間範囲に収まるイベントのみを対象とします。これらはダッシュボードで指定されます。このような階層的なフィルタリングにより、ノイズを最小限に抑え、最も関連性の高いニュースイベントのみを処理できるようになります。

イベントがこれらのフィルタ条件を満たした場合、取引ロジックはニュースイベントの主要なデータポイント、つまり予想値と前回値を比較します。どちらの値も存在し、ゼロでない場合、予想値が前回値を上回っていれば買い注文を、下回っていれば売り注文を出します。いずれかの値が欠けているか、両者が同じである場合は、そのイベントはスキップされます。この意思決定プロセスにより、エキスパートアドバイザー(EA)は生のニュースデータを明確な取引シグナルに変換し、正確な自動取引エントリーを実現します。なお、この意思決定プロセスと取引方向はすべてユーザーによって設定可能ですが、本記事およびデモでは上記の設計図に基づいて進めていきます。

これらの処理を視覚的に確認するために、デバッグ出力を使用し、ダッシュボードのすぐ上のチャート上に、取引対象のニュースとその発表までの残り時間を表示するボタンやラベルを作成します。以下に、全体の設計図を示します。

設計図


MQL5での取引ロジックの実装

MQL5で取引ロジックを実装するには、トレーディングメソッドを含むファイルをインクルードし、システムのユーザー制御を可能にする入力項目と、プログラム全体で再利用するグローバル変数を定義する必要があります。これを実現するために、グローバルスコープでそれらを定義します。

#include <Trade\Trade.mqh> // Trading library for order execution
CTrade trade;             // Global trade object


//================== Trade Settings ==================//
// Trade mode options:
enum ETradeMode {
   TRADE_BEFORE,     // Trade before the news event occurs
   TRADE_AFTER,      // Trade after the news event occurs
   NO_TRADE,         // Do not trade
   PAUSE_TRADING     // Pause trading activity (no trades until resumed)
};
input ETradeMode tradeMode      = TRADE_BEFORE; // Choose the trade mode

// Trade offset inputs:
input int        tradeOffsetHours   = 12;         // Offset hours (e.g., 12 hours)
input int        tradeOffsetMinutes = 5;          // Offset minutes (e.g., 5 minutes before)
input int        tradeOffsetSeconds = 0;          // Offset seconds
input double     tradeLotSize       = 0.01;       // Lot size for the trade

//================== Global Trade Control ==================//
// Once a trade is executed for one news event, no further trades occur.
bool tradeExecuted = false;
// Store the traded event’s scheduled news time for the post–trade countdown.
datetime tradedNewsTime = 0;
// Global array to store event IDs that have already triggered a trade.
int triggeredNewsEvents[];

グローバルスコープでは、#includeを使用して「Trade\Trade.mqh」ライブラリをインクルードし、注文実行を有効にします。また、取引を処理するためのグローバルなCTradeオブジェクトtradeを宣言します。さらに、列挙型を定義し、その中にTRADE_BEFORE、TRADE_AFTER、NO_TRADE、PAUSE_TRADINGのオプションを設けます。プログラムでは、この列挙型を使用して、ニュースイベントに対して取引をいつおこなうかを決定します。input変数tradeMode(デフォルトはTRADE_BEFORE)を通じてユーザーが選択できるようにします。加えて、取引のタイミングオフセットとロットサイズを指定するためのinput変数tradeOffsetHours、tradeOffsetMinutes、tradeOffsetSeconds、tradeLotSizeを設定します。取引制御と、同じニュースイベントに対して再度取引がおこなわれるのを防ぐために、tradeExecuted(ブール型)、tradedNewsTime(datetime型)、triggeredNewsEvents(int型配列)といったグローバル変数を用意します。その後、取引ロジックを関数内に組み込むことができます。

//--- Function to scan for news events and execute trades based on selected criteria
//--- It handles both pre-trade candidate selection and post-trade countdown updates
void CheckForNewsTrade() {
   //--- Log the call to CheckForNewsTrade with the current server time
   Print("CheckForNewsTrade called at: ", TimeToString(TimeTradeServer(), TIME_SECONDS));
   
   //--- If trading is disabled (either NO_TRADE or PAUSE_TRADING), remove countdown objects and exit
   if(tradeMode == NO_TRADE || tradeMode == PAUSE_TRADING) {
      //--- Check if a countdown object exists on the chart
      if(ObjectFind(0, "NewsCountdown") >= 0) {
         //--- Delete the countdown object from the chart
         ObjectDelete(0, "NewsCountdown");
         //--- Log that the trading is disabled and the countdown has been removed
         Print("Trading disabled. Countdown removed.");
      }
      //--- Exit the function since trading is not allowed
      return;
   }
   //--- Begin pre-trade candidate selection section
   
   //--- Define the lower bound of the event time range based on the user-defined start time offset
   datetime lowerBound = currentTime - PeriodSeconds(start_time);
   //--- Define the upper bound of the event time range based on the user-defined end time offset
   datetime upperBound = currentTime + PeriodSeconds(end_time);
   //--- Log the overall event time range for debugging purposes
   Print("Event time range: ", TimeToString(lowerBound, TIME_SECONDS), " to ", TimeToString(upperBound, TIME_SECONDS));
   
   //--- Retrieve historical calendar values (news events) within the defined time range
   MqlCalendarValue values[];
   int totalValues = CalendarValueHistory(values, lowerBound, upperBound, NULL, NULL);
   //--- Log the total number of events found in the specified time range
   Print("Total events found: ", totalValues);
   //--- If no events are found, delete any existing countdown and exit the function
   if(totalValues <= 0) {
      if(ObjectFind(0, "NewsCountdown") >= 0)
         ObjectDelete(0, "NewsCountdown");
      return;
   }
}

ここでは、ニュースイベントを検出し、選択された条件に基づいて取引を実行する関数CheckForNewsTradeを定義します。まず、Print関数でこの関数の呼び出しをログに記録し、TimeTradeServer関数を使って取得した現在のサーバー時間を表示します。次に、tradeMode変数がNO_TRADEまたはPAUSE_TRADINGになっているかを確認し、取引が無効になっている場合には、ObjectFind関数で「NewsCountdown」という名前のカウントダウンオブジェクトが存在するかを調べ、存在すればObjectDelete関数で削除し、関数から抜けます。

その後、関数は全体のイベント時間範囲を計算します。lowerBoundは、現在の時刻からstart_time入力値に対応する秒数(PeriodSeconds関数で変換)を引いた値に設定され、upperBoundは、現在の時刻にend_timeの秒数を加えた値になります。この全体の時間範囲はPrintでログに記録されます。最後に、関数はCalendarValueHistoryを呼び出して、定義された時間範囲内のすべてのニュースイベントを取得します。イベントが見つからない場合、既存のカウントダウンオブジェクトがあれば削除し、関数を終了します。これにより、システムはその後の候補イベントの選別と取引実行の準備が整います。

//--- Initialize candidate event variables for trade selection
datetime candidateEventTime = 0;
string candidateEventName = "";
string candidateTradeSide = "";
int candidateEventID = -1;

//--- Loop through all retrieved events to evaluate each candidate for trading
for(int i = 0; i < totalValues; i++) {
   //--- Declare an event structure to hold event details
   MqlCalendarEvent event;
   //--- Attempt to populate the event structure by its ID; if it fails, skip to the next event
   if(!CalendarEventById(values[i].event_id, event))
      continue;
   
   //----- Apply Filters -----
   
   //--- If currency filtering is enabled, check if the event's currency matches the selected filters
   if(enableCurrencyFilter) {
      //--- Declare a country structure to hold country details
      MqlCalendarCountry country;
      //--- Populate the country structure based on the event's country ID
      CalendarCountryById(event.country_id, country);
      //--- Initialize a flag to determine if there is a matching currency
      bool currencyMatch = false;
      //--- Loop through each selected currency filter
      for(int k = 0; k < ArraySize(curr_filter_selected); k++) {
         //--- Check if the event's country currency matches the current filter selection
         if(country.currency == curr_filter_selected[k]) {
            //--- Set flag to true if a match is found and break out of the loop
            currencyMatch = true;
            break;
         }
      }
      //--- If no matching currency is found, log the skip and continue to the next event
      if(!currencyMatch) {
         Print("Event ", event.name, " skipped due to currency filter.");
         continue;
      }
   }
   
   //--- If importance filtering is enabled, check if the event's impact matches the selected filters
   if(enableImportanceFilter) {
      //--- Initialize a flag to determine if the event's impact matches any filter selection
      bool impactMatch = false;
      //--- Loop through each selected impact filter option
      for(int k = 0; k < ArraySize(imp_filter_selected); k++) {
         //--- Check if the event's importance matches the current filter selection
         if(event.importance == imp_filter_selected[k]) {
            //--- Set flag to true if a match is found and break out of the loop
            impactMatch = true;
            break;
         }
      }
      //--- If no matching impact is found, log the skip and continue to the next event
      if(!impactMatch) {
         Print("Event ", event.name, " skipped due to impact filter.");
         continue;
      }
   }
   
   //--- If time filtering is enabled and the event time exceeds the upper bound, skip the event
   if(enableTimeFilter && values[i].time > upperBound) {
      Print("Event ", event.name, " skipped due to time filter.");
      continue;
   }
   
   //--- Check if the event has already triggered a trade by comparing its ID to recorded events
   bool alreadyTriggered = false;
   //--- Loop through the list of already triggered news events
   for(int j = 0; j < ArraySize(triggeredNewsEvents); j++) {
      //--- If the event ID matches one that has been triggered, mark it and break out of the loop
      if(triggeredNewsEvents[j] == values[i].event_id) {
         alreadyTriggered = true;
         break;
      }
   }
   //--- If the event has already triggered a trade, log the skip and continue to the next event
   if(alreadyTriggered) {
      Print("Event ", event.name, " already triggered a trade. Skipping.");
      continue;
   }

ここでは、候補イベント変数を初期化します。datetime変数candidateEventTime、2つの文字列変数candidateEventNameとcandidateTradeSide、そして整数変数candidateEventID(初期値は-1)を用意します。次に、CalendarValueHistory関数で取得したMqlCalendarValue構造体の配列をループし、それぞれのイベントに対してCalendarEventById関数を使って、イベントの詳細を格納するMqlCalendarEvent 構造体を取得します。

その後、各種フィルターを適用します。通貨フィルターが有効な場合、CalendarCountryById 関数を使ってそのイベントに対応するMqlCalendarCountry構造体を取得し、そのcurrencyフィールドがcurr_filter_selected配列内のいずれかと一致するかを確認します。一致しなければ、ログに記録してこのイベントをスキップします。同様に、重要度フィルターが有効な場合は、imp_filter_selected配列をループして、イベントのimportanceが選択されたレベルのいずれかに合致するかを確認し、合致しなければログしてスキップします。

最後に、このイベントがすでに取引をトリガーしていないかを確認するため、イベントIDをtriggeredNewsEvents配列と比較します。すでに含まれている場合はログしてスキップします。このループにより、通貨、重要度、時間範囲、そして一意性のすべての条件を満たしたイベントのみが、取引実行候補として扱われることになります。すべての条件を通過し、有効なイベントが存在する場合、ユーザーによって許可された時間枠でさらにフィルタリング処理を進めることができます。

//--- For TRADE_BEFORE mode, check if the current time is within the valid window (event time minus offset to event time)
if(tradeMode == TRADE_BEFORE) {
   if(currentTime >= (values[i].time - offsetSeconds) && currentTime < values[i].time) {
      //--- Retrieve the forecast and previous values for the event
      MqlCalendarValue calValue;
      //--- If unable to retrieve calendar values, log the error and skip this event
      if(!CalendarValueById(values[i].id, calValue)) {
         Print("Error retrieving calendar value for event: ", event.name);
         continue;
      }
      //--- Get the forecast value from the calendar data
      double forecast = calValue.GetForecastValue();
      //--- Get the previous value from the calendar data
      double previous = calValue.GetPreviousValue();
      //--- If either forecast or previous is zero, log the skip and continue to the next event
      if(forecast == 0.0 || previous == 0.0) {
         Print("Skipping event ", event.name, " because forecast or previous value is empty.");
         continue;
      }
      //--- If forecast equals previous, log the skip and continue to the next event
      if(forecast == previous) {
         Print("Skipping event ", event.name, " because forecast equals previous.");
         continue;
      }
      //--- If this candidate event is earlier than any previously found candidate, record its details
      if(candidateEventTime == 0 || values[i].time < candidateEventTime) {
         candidateEventTime = values[i].time;
         candidateEventName = event.name;
         candidateEventID = (int)values[i].event_id;
         candidateTradeSide = (forecast > previous) ? "BUY" : "SELL";
         //--- Log the candidate event details including its time and trade side
         Print("Candidate event: ", event.name, " with event time: ", TimeToString(values[i].time, TIME_SECONDS), " Side: ", candidateTradeSide);
      }
   }
}

ここでは、TRADE_BEFOREモードで動作している場合に、候補ニュースイベントを評価します。TimeTradeServer関数で取得した現在の時刻が、ユーザー定義のオフセット(offsetSeconds)をイベントの予定時刻から引いた時刻から、イベントの正確な発表時刻までの有効な取引ウィンドウ内に収まっているかを確認します。その評価条件は以下のように定義されます。

//--- Get the current trading server time
datetime currentTime = TimeTradeServer();
//--- Calculate the offset in seconds based on trade offset hours, minutes, and seconds
int offsetSeconds = tradeOffsetHours * 3600 + tradeOffsetMinutes * 60 + tradeOffsetSeconds;

条件を満たす場合、CalendarValueById関数を使用してイベントの予想値と前回値を取得し、MqlCalendarValue構造に格納します。取得に失敗した場合はエラーメッセージをログに出力し、そのイベントをスキップします。次に、それぞれGetForecastValueおよびGetPreviousValueメソッドを使用して予想値と前回値を抽出します。どちらかの値がゼロであるか、両者が等しい場合には、メッセージをログに記録し、次のイベントに進み、意味のあるデータを持つイベントのみを処理するようにします。

イベントが条件を満たし、かつそれがこれまでに特定された候補イベントよりも早い時刻に発生する場合には、候補変数を更新します。candidateEventTimeにはイベント時刻を、candidateEventNameにはイベント名を、candidateEventIDにはイベントIDを格納し、candidateTradeSideには予想値が前回値より大きければ買い、小さければ売りを設定します。最後に、選択された候補イベントの詳細をログに出力し、取引実行に向けて最も早く発生する有効なイベントを追跡します。これで、取引実行のためのイベントを選択する準備が整います。

//--- If a candidate event has been selected and the trade mode is TRADE_BEFORE, attempt to execute the trade
if(tradeMode == TRADE_BEFORE && candidateEventTime > 0) {
   //--- Calculate the target time to start trading by subtracting the offset from the candidate event time
   datetime targetTime = candidateEventTime - offsetSeconds;
   //--- Log the candidate target time for debugging purposes
   Print("Candidate target time: ", TimeToString(targetTime, TIME_SECONDS));
   //--- Check if the current time falls within the trading window (target time to candidate event time)
   if(currentTime >= targetTime && currentTime < candidateEventTime) {
      //--- Loop through events again to get detailed information for the candidate event
      for(int i = 0; i < totalValues; i++) {
         //--- Identify the candidate event by matching its time
         if(values[i].time == candidateEventTime) {
            //--- Declare an event structure to store event details
            MqlCalendarEvent event;
   
         }
      }
   }
}

candidateEventTimeがゼロより大きいことを確認することで、候補イベントが選択されており、かつ取引モードがTRADE_BEFOREであるかを判定します。次に、ユーザーが定義したオフセット(offsetSeconds)を候補イベントの予定時刻から引くことでtargetTimeを算出し、デバッグ用にPrint関数を使ってこのターゲット時刻をログに出力します。その後、現在時刻が有効な取引ウィンドウ内、つまりtargetTimeと候補イベントの時刻の間にあるかどうかを判定します。条件を満たす場合は、イベントの配列をループし、候補イベントの時刻と一致するイベントを特定することで、さらに詳細を取得し取引を実行する処理へと進みます。

//--- Attempt to retrieve the event details; if it fails, skip to the next event
if(!CalendarEventById(values[i].event_id, event))
   continue;
//--- If the current time is past the event time, log the skip and continue
if(currentTime >= values[i].time) {
   Print("Skipping candidate ", event.name, " because current time is past event time.");
   continue;
}
//--- Retrieve detailed calendar values for the candidate event
MqlCalendarValue calValue;
//--- If retrieval fails, log the error and skip the candidate
if(!CalendarValueById(values[i].id, calValue)) {
   Print("Error retrieving calendar value for candidate event: ", event.name);
   continue;
}
//--- Get the forecast value for the candidate event
double forecast = calValue.GetForecastValue();
//--- Get the previous value for the candidate event
double previous = calValue.GetPreviousValue();
//--- If forecast or previous is zero, or if they are equal, log the skip and continue
if(forecast == 0.0 || previous == 0.0 || forecast == previous) {
   Print("Skipping candidate ", event.name, " due to invalid forecast/previous values.");
   continue;
}
//--- Construct a news information string for the candidate event
string newsInfo = "Trading on news: " + event.name +
                  " (Time: " + TimeToString(values[i].time, TIME_SECONDS)+")";
//--- Log the news trading information
Print(newsInfo);
//--- Create a label on the chart to display the news trading information
createLabel1("NewsTradeInfo", 355, 22, newsInfo, clrBlue, 11);

取引を開始する前に、CalendarEventById関数を使用して候補イベントの詳細情報を取得し、MqlCalendarEvent構造体に格納しようとします。この取得が失敗した場合は、直ちに次のイベントへスキップします。次に、現在のサーバー時刻(TimeTradeServerで取得)と候補イベントの予定時刻を比較し、現在時刻がすでにイベントの時刻を過ぎている場合は、その旨をログ出力し、処理をスキップします。

その後、CalendarValueByIdを用いてイベントの詳細なカレンダー値を取得し、MqlCalendarValue構造体に格納します。そして、GetForecastValueおよびGetPreviousValueメソッドを使って「予想値」と「前回値」を抽出します。これらの値のいずれかがゼロ、もしくは両者が等しい場合には、その理由をログに記録し、候補イベントをスキップします。最後に、ニュースの重要情報を含む文字列を構築してログ出力し、同時にcreateLabel1関数を使ってチャート上にこの情報を表示します。この関数のコードスニペットは以下の通りです。

//--- Function to create a label on the chart with specified properties
bool createLabel1(string objName, int x, int y, string text, color txtColor, int fontSize) {
   //--- Attempt to create the label object; if it fails, log the error and return false
   if(!ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0)) {
      //--- Print error message with the label name and the error code
      Print("Error creating label ", objName, " : ", GetLastError());
      //--- Return false to indicate label creation failure
      return false;
   }
   //--- Set the horizontal distance (X coordinate) for the label
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, x);
   //--- Set the vertical distance (Y coordinate) for the label
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, y);
   //--- Set the text that will appear on the label
   ObjectSetString(0, objName, OBJPROP_TEXT, text);
   //--- Set the color of the label's text
   ObjectSetInteger(0, objName, OBJPROP_COLOR, txtColor);
   //--- Set the font size for the label text
   ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize);
   //--- Set the font style to "Arial Bold" for the label text
   ObjectSetString(0, objName, OBJPROP_FONT, "Arial Bold");
   //--- Set the label's anchor corner to the top left of the chart
   ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   //--- Redraw the chart to reflect the new label
   ChartRedraw();
   //--- Return true indicating that the label was created successfully
   return true;
}

この関数のロジックは新しいものではなく、すでにダッシュボード作成時に説明済みであるため、改めて詳しく解説する必要はありません。したがって、ここでは取得した値に基づいて取引を実行する処理へと進みます。

//--- Initialize a flag to store the result of the trade execution
bool tradeResult = false;
//--- If the candidate trade side is BUY, attempt to execute a buy order
if(candidateTradeSide == "BUY") {
   tradeResult = trade.Buy(tradeLotSize, _Symbol, 0, 0, 0, event.name);
} 
//--- Otherwise, if the candidate trade side is SELL, attempt to execute a sell order
else if(candidateTradeSide == "SELL") {
   tradeResult = trade.Sell(tradeLotSize, _Symbol, 0, 0, 0, event.name);
}
//--- If the trade was executed successfully, update the triggered events and trade flags
if(tradeResult) {
   Print("Trade executed for candidate event: ", event.name, " Side: ", candidateTradeSide);
   int size = ArraySize(triggeredNewsEvents);
   ArrayResize(triggeredNewsEvents, size + 1);
   triggeredNewsEvents[size] = (int)values[i].event_id;
   tradeExecuted = true;
   tradedNewsTime = values[i].time;
} else {
   //--- If trade execution failed, log the error message with the error code
   Print("Trade execution failed for candidate event: ", event.name, " Error: ", GetLastError());
}
//--- Break out of the loop after processing the candidate event
break;

まず、取引結果を格納するためのブール型フラグ「tradeResult」を初期化します。次にcandidateTradeSideを確認し、BUYであれば指定されたtradeLotSizeと銘柄(_Symbol)を引数にして、trade.Buy関数を呼び出します。この際、イベント名をコメントとして付け、識別しやすくしています。同様に、SELLであればtrade.Sellを呼び出します。取引の実行が成功した場合(つまりtradeResultがtrueの場合)、実行内容をログに記録し、triggeredNewsEvents配列をArrayResize関数でサイズ変更してイベントIDを追加、tradeExecutedをtrueに設定し、イベントの予定時刻をtradedNewsTimeに保存します。失敗した場合は、GetLastErrorを用いてエラーメッセージをログに記録し、以降の候補イベント処理を防ぐためにループを抜けます。以下はイベント範囲で開始された取引の例です。

取引コメント

取引を開始した後は、イベントのカウントダウンロジックを初期化するだけで、これは次のセクションで扱います。


カウントダウンタイマーの作成と管理

カウントダウンタイマーを作成・管理するために、時間を表示するボタンを作成する補助関数と、必要に応じてラベルを更新する関数が必要になります。

//--- Function to create a button on the chart with specified properties
bool createButton1(string objName, int x, int y, int width, int height, 
                   string text, color txtColor, int fontSize, color bgColor, color borderColor) {
   //--- Attempt to create the button object; if it fails, log the error and return false
   if(!ObjectCreate(0, objName, OBJ_BUTTON, 0, 0, 0)) {
      //--- Print error message with the button name and the error code
      Print("Error creating button ", objName, " : ", GetLastError());
      //--- Return false to indicate button creation failure
      return false;
   }
   //--- Set the horizontal distance (X coordinate) for the button
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, x);
   //--- Set the vertical distance (Y coordinate) for the button
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, y);
   //--- Set the width of the button
   ObjectSetInteger(0, objName, OBJPROP_XSIZE, width);
   //--- Set the height of the button
   ObjectSetInteger(0, objName, OBJPROP_YSIZE, height);
   //--- Set the text that will appear on the button
   ObjectSetString(0, objName, OBJPROP_TEXT, text);
   //--- Set the color of the button's text
   ObjectSetInteger(0, objName, OBJPROP_COLOR, txtColor);
   //--- Set the font size for the button text
   ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize);
   //--- Set the font style to "Arial Bold" for the button text
   ObjectSetString(0, objName, OBJPROP_FONT, "Arial Bold");
   //--- Set the background color of the button
   ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, bgColor);
   //--- Set the border color of the button
   ObjectSetInteger(0, objName, OBJPROP_BORDER_COLOR, borderColor);
   //--- Set the button's anchor corner to the top left of the chart
   ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   //--- Enable the background display for the button
   ObjectSetInteger(0, objName, OBJPROP_BACK, true);
   //--- Redraw the chart to reflect the new button
   ChartRedraw();
   //--- Return true indicating that the button was created successfully
   return true;
}
//--- Function to update the text of an existing label
bool updateLabel1(string objName, string text) {
   //--- Check if the label exists on the chart; if not, log the error and return false
   if(ObjectFind(0, objName) < 0) {
      //--- Print error message indicating that the label was not found
      Print("updateLabel1: Object ", objName, " not found.");
      //--- Return false because the label does not exist
      return false;
   }
   //--- Update the label's text property with the new text
   ObjectSetString(0, objName, OBJPROP_TEXT, text);
   //--- Redraw the chart to update the label display
   ChartRedraw();
   //--- Return true indicating that the label was updated successfully
   return true;
}

//--- Function to update the text of an existing label
bool updateLabel1(string objName, string text) {
   //--- Check if the label exists on the chart; if not, log the error and return false
   if(ObjectFind(0, objName) < 0) {
      //--- Print error message indicating that the label was not found
      Print("updateLabel1: Object ", objName, " not found.");
      //--- Return false because the label does not exist
      return false;
   }
   //--- Update the label's text property with the new text
   ObjectSetString(0, objName, OBJPROP_TEXT, text);
   //--- Redraw the chart to update the label display
   ChartRedraw();
   //--- Return true indicating that the label was updated successfully
   return true;
}

ここでは、タイマーボタンを作成するための補助関数と、ラベルを更新するための更新関数を用意します。これらの関数のロジックは以前に本連載で既に詳しく説明しているため、説明は省略し、そのまま実装に進みます。

//--- Begin handling the post-trade countdown scenario
if(tradeExecuted) {
   //--- If the current time is before the traded news time, display the countdown until news release
   if(currentTime < tradedNewsTime) {
      //--- Calculate the remaining seconds until the traded news time
      int remainingSeconds = (int)(tradedNewsTime - currentTime);
      //--- Calculate hours from the remaining seconds
      int hrs = remainingSeconds / 3600;
      //--- Calculate minutes from the remaining seconds
      int mins = (remainingSeconds % 3600) / 60;
      //--- Calculate seconds remainder
      int secs = remainingSeconds % 60;
      //--- Construct the countdown text string
      string countdownText = "News in: " + IntegerToString(hrs) + "h " +
                             IntegerToString(mins) + "m " +
                             IntegerToString(secs) + "s";
      //--- If the countdown object does not exist, create it with a blue background
      if(ObjectFind(0, "NewsCountdown") < 0) {
         createButton1("NewsCountdown", 50, 17, 300, 30, countdownText, clrWhite, 12, clrBlue, clrBlack);
         //--- Log that the post-trade countdown was created
         Print("Post-trade countdown created: ", countdownText);
      } else {
         //--- If the countdown object exists, update its text
         updateLabel1("NewsCountdown", countdownText);
         //--- Log that the post-trade countdown was updated
         Print("Post-trade countdown updated: ", countdownText);
      }
   } else {
      //--- If current time is past the traded news time, calculate elapsed time since trade
      int elapsed = (int)(currentTime - tradedNewsTime);
      //--- If less than 15 seconds have elapsed, show a reset countdown
      if(elapsed < 15) {
         //--- Calculate the remaining delay for reset
         int remainingDelay = 15 - elapsed;
         //--- Construct the reset countdown text
         string countdownText = "News Released, resetting in: " + IntegerToString(remainingDelay) + "s";
         //--- If the countdown object does not exist, create it with a red background
         if(ObjectFind(0, "NewsCountdown") < 0) {
            createButton1("NewsCountdown", 50, 17, 300, 30, countdownText, clrWhite, 12, clrRed, clrBlack);
            //--- Set the background color property explicitly to red
            ObjectSetInteger(0,"NewsCountdown",OBJPROP_BGCOLOR,clrRed);
            //--- Log that the post-trade reset countdown was created
            Print("Post-trade reset countdown created: ", countdownText);
         } else {
            //--- If the countdown object exists, update its text and background color
            updateLabel1("NewsCountdown", countdownText);
            ObjectSetInteger(0,"NewsCountdown",OBJPROP_BGCOLOR,clrRed);
            //--- Log that the post-trade reset countdown was updated
            Print("Post-trade reset countdown updated: ", countdownText);
         }
      } else {
         //--- If 15 seconds have elapsed since traded news time, log the reset action
         Print("News Released. Resetting trade status after 15 seconds.");
         
         //--- If the countdown object exists, delete it from the chart
         if(ObjectFind(0, "NewsCountdown") >= 0)
            ObjectDelete(0, "NewsCountdown");
         //--- Reset the tradeExecuted flag to allow new trades
         tradeExecuted = false;
      }
   }
   //--- Exit the function as post-trade processing is complete
   return;
}

ここでは、取引後のカウントダウン処理を詳しく扱います。取引が実行されたら、まずTimeTradeServer関数で現在のサーバー時刻を取得し、候補イベントの予定リリース時刻を保持するtradedNewsTimeと比較します。現在時刻がtradedNewsTimeより前であれば、残り秒数を計算し、それを時間・分・秒に変換して、IntegerToString関数を使って「News in: __h __m __s」という形式のカウントダウン文字列を作成します。

次にObjectFindでNewsCountdownオブジェクトの有無を確認し、存在しなければカスタム関数「createButton1」を使って座標X=50、Y=17、幅300、高さ30、青色背景で作成し、存在すればupdateLabel1関数で更新します。ただし、現在時刻がtradedNewsTimeを過ぎている場合は経過時間を計算し、その経過が15秒未満であればカウントダウンオブジェクトに「News Released, resetting in: XXs」というリセットメッセージを表示し、ObjectSetInteger関数で背景色を赤に設定します。

15秒のリセット期間が過ぎたらNewsCountdownオブジェクトを削除し、tradeExecutedフラグをリセットして新たな取引を許可します。これにより、ニュースのタイミング変化に動的に対応しつつ、取引実行を制御します。また、取引済みで発表前の状態であればカウントダウンを表示する必要があります。次のロジックを通じてそれを実現します。

if(currentTime >= targetTime && currentTime < candidateEventTime) {

        //---

}
else {
   //--- If current time is before the candidate event window, show a pre-trade countdown
   int remainingSeconds = (int)(candidateEventTime - currentTime);
   int hrs = remainingSeconds / 3600;
   int mins = (remainingSeconds % 3600) / 60;
   int secs = remainingSeconds % 60;
   //--- Construct the pre-trade countdown text
   string countdownText = "News in: " + IntegerToString(hrs) + "h " +
                          IntegerToString(mins) + "m " +
                          IntegerToString(secs) + "s";
   //--- If the countdown object does not exist, create it with specified dimensions and blue background
   if(ObjectFind(0, "NewsCountdown") < 0) {
      createButton1("NewsCountdown", 50, 17, 300, 30, countdownText, clrWhite, 12, clrBlue, clrBlack);
      //--- Log that the pre-trade countdown was created
      Print("Pre-trade countdown created: ", countdownText);
   } else {
      //--- If the countdown object exists, update its text
      updateLabel1("NewsCountdown", countdownText);
      //--- Log that the pre-trade countdown was updated
      Print("Pre-trade countdown updated: ", countdownText);
   }
}

現在時刻が候補イベントのt路五匹ウィンドウ内にない場合、つまりtargetTime(候補イベントの予定時刻からオフセットを引いた時間)以上でもなく、かつ候補イベントの予定時刻未満でもない場合、現在時刻はまだ取引ウィンドウ開始前と判断します。そのため、候補イベントの予定時刻から現在時刻を引いて残り時間を計算し、それを時間・分・秒に変換します。

IntegerToStringを使い、「News in: __h __m __s」という形式のカウントダウンテキストを作成します。続いてObjectFind関数でNewsCountdownオブジェクトの有無を確認し、存在しなければcreateButton1関数で座標X=50、Y=17、幅300、高さ30、青背景のボタンを作成し、事前取引のカウントダウンが作成された旨をログに記録します。すでに存在する場合はupdateLabel1関数でテキストを更新し、更新ログを残します。最後に、解析後に取引対象のイベントが選択されなかった場合は、作成済みのオブジェクトを削除します。

//--- If no candidate event is selected, delete any existing countdown and trade info objects
if(ObjectFind(0, "NewsCountdown") >= 0) {
   ObjectDelete(0, "NewsCountdown");
   ObjectDelete(0, "NewsTradeInfo");
   //--- Log that the pre-trade countdown was deleted
   Print("Pre-trade countdown deleted.");
}

候補イベントが選択されなかった場合、つまり取引実行の条件を満たすイベントがない場合は、ObjectFind関数を使ってNewsCountdownオブジェクトの存在を確認します。存在する場合は、ObjectDelete関数を呼び出してNewsCountdownオブジェクトとNewsTradeInfoオブジェクトの両方をチャートから削除し、古いカウントダウンや取引情報が表示されたままにならないようにします。

ただし、ユーザーがプログラムを明示的に終了する可能性もあるため、その際にもチャートをきれいに保つ必要があります。そこで、チャートのクリーンアップを簡単におこなうための関数を定義しておくことができます。

//--- Function to delete trade-related objects from the chart and redraw the chart
void deleteTradeObjects(){
   //--- Delete the countdown object from the chart
   ObjectDelete(0, "NewsCountdown");
   //--- Delete the news trade information label from the chart
   ObjectDelete(0, "NewsTradeInfo");
   //--- Redraw the chart to reflect the deletion of objects
   ChartRedraw();
}

関数を定義した後は、OnDeinitイベントハンドラ内でその関数を呼び出します。ここでは既存のダッシュボードも破棄し、完全なクリーンアップをおこないます。以下の黄色で強調した部分をご覧ください。

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

   destroy_Dashboard();
   deleteTradeObjects();
}

一つ残っていることは、ユーザーがダッシュボードをクリックした際にフィルター情報の更新を追跡し、常に最新の情報を保持することです。そのためには、OnChartEventイベントハンドラでイベントを監視する必要があります。それを簡単に実装するための関数を作成します。

//--- Function to log active filter selections in the Experts log
void UpdateFilterInfo() {
   //--- Initialize the filter information string with the prefix "Filters: "
   string filterInfo = "Filters: ";
   //--- Check if the currency filter is enabled
   if(enableCurrencyFilter) {
      //--- Append the currency filter label to the string
      filterInfo += "Currency: ";
      //--- Loop through each selected currency filter option
      for(int i = 0; i < ArraySize(curr_filter_selected); i++) {
         //--- Append the current currency filter value
         filterInfo += curr_filter_selected[i];
         //--- If not the last element, add a comma separator
         if(i < ArraySize(curr_filter_selected) - 1)
            filterInfo += ",";
      }
      //--- Append a semicolon to separate this filter's information
      filterInfo += "; ";
   } else {
      //--- Indicate that the currency filter is turned off
      filterInfo += "Currency: Off; ";
   }
   //--- Check if the importance filter is enabled
   if(enableImportanceFilter) {
      //--- Append the impact filter label to the string
      filterInfo += "Impact: ";
      //--- Loop through each selected impact filter option
      for(int i = 0; i < ArraySize(imp_filter_selected); i++) {
         //--- Append the string representation of the current importance filter value
         filterInfo += EnumToString(imp_filter_selected[i]);
         //--- If not the last element, add a comma separator
         if(i < ArraySize(imp_filter_selected) - 1)
            filterInfo += ",";
      }
      //--- Append a semicolon to separate this filter's information
      filterInfo += "; ";
   } else {
      //--- Indicate that the impact filter is turned off
      filterInfo += "Impact: Off; ";
   }
   //--- Check if the time filter is enabled
   if(enableTimeFilter) {
      //--- Append the time filter information with the upper limit
      filterInfo += "Time: Up to " + EnumToString(end_time);
   } else {
      //--- Indicate that the time filter is turned off
      filterInfo += "Time: Off";
   }
   //--- Print the complete filter information to the Experts log
   Print("Filter Info: ", filterInfo);
}

UpdateFilterInfoというvoid関数を作成します。まず、文字列変数を「Filters:」で初期化し、通貨フィルターが有効かどうかをチェックします。有効であれば「Currency:」を追加し、curr_filter_selected配列をArraySizeでループして各通貨をカンマ区切りで追加し、最後にセミコロンを付けます。無効の場合は「Currency: Off;」と記載します。次に、影響度フィルターについて同様の処理をおこない、有効なら「Impact:」を追加してimp_filter_selectedをループし、EnumToStringで影響度レベルを文字列に変換して連結し、無効なら「Impact: Off;」とします。

最後に時間フィルターについて、「Time: Up to」とend_time入力の文字列表現(こちらもEnumToString使用)を追加し、無効なら「Time: Off」とします。すべてを連結した文字列をPrint関数で[エクスパート]ログに出力し、現在有効なフィルターの状態をリアルタイムで確認できるようにします。この関数はOnChartEventイベントハンドラおよびOnTickで呼び出します。

//+------------------------------------------------------------------+
//|    OnChartEvent handler function                                 |
//+------------------------------------------------------------------+
void  OnChartEvent(
   const int       id,       // event ID  
   const long&     lparam,   // long type event parameter 
   const double&   dparam,   // double type event parameter 
   const string&   sparam    // string type event parameter 
){
      
   if (id == CHARTEVENT_OBJECT_CLICK){ //--- Check if the event is a click on an object
      UpdateFilterInfo();
      CheckForNewsTrade();
   }
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){
//---
UpdateFilterInfo();
CheckForNewsTrade();
   if (isDashboardUpdate){
      update_dashboard_values(curr_filter_selected,imp_filter_selected);
   }
   
}

プログラムを実行すると、次の結果が得られます。

最終結果イメージ

画像から、ユーザーが選択した設定に基づいて取引を開始でき、ニュースが取引されるとカウントダウンタイマーとラベルを作成して情報を表示・更新し、その更新内容をログに記録していることがわかります。これにより、目的を達成しています。あとはロジックのテストをおこなうだけであり、その内容は次のセクションで扱います。


取引ロジックのテスト

バックテストに関しては、ライブ取引ニュースイベントを待ち、テストの結果は以下のビデオに示すとおりでした。



結論

今回、ユーザー定義のフィルター、正確な時間オフセット、そして動的なカウントダウンタイマーを活用して、MQL5経済指標カレンダーシステムに自動売買エントリー機能を無事統合しました。システムはニュースイベントを検出し、予想値と前回値を比較、明確なカレンダーのシグナルに基づいて自動的に買いまたは売り注文を実行します。

しかし、実際の取引環境に対応するためにはさらなる改良が必要です。特にリスク管理の強化やフィルター条件の微調整に注力し、最適なパフォーマンスを追求するために、継続的な開発とテストを推奨します。取引をお楽しみください。


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

添付されたファイル |
最後のコメント | ディスカッションに移動 (2)
Nikolay Moskalev
Nikolay Moskalev | 3 3月 2025 において 16:06

こんにちは!お疲れ様でした。

2Kモニターでのテーブル表示に問題があります。

Allan Munene Mutiiria
Allan Munene Mutiiria | 3 3月 2025 において 20:11
Nikolay Moskalev #:

こんにちは!お疲れ様でした。

2Kモニターでのテーブル表示に問題があります。

こんにちは。ようこそ。それは、すべてが完全に収まるようにフォントサイズを変更する必要があります。

プライスアクション分析ツールキットの開発(第15回):クォーターズ理論の紹介(I) - Quarters Drawerスクリプト プライスアクション分析ツールキットの開発(第15回):クォーターズ理論の紹介(I) - Quarters Drawerスクリプト
サポートとレジスタンスのポイントは、トレンドの反転や継続の可能性を示す重要なレベルです。これらのレベルを見つけるのは難しいこともありますが、一度特定できれば、市場をより的確に捉える準備が整います。さらなるサポートとして、本記事で紹介されているQuarters Drawerツールをぜひご活用ください。このツールは、主要およびマイナーなサポート・レジスタンスレベルの特定に役立ちます。
知っておくべきMQL5ウィザードのテクニック(第55回):PER付きSAC 知っておくべきMQL5ウィザードのテクニック(第55回):PER付きSAC
強化学習において、リプレイバッファは特にDQNやSACのようなオフポリシーアルゴリズムにおいて重要な役割を果たします。これにより、メモリバッファのサンプリング処理が注目されます。たとえばSACのデフォルト設定では、このバッファからランダムにサンプルを取得しますが、Prioritized Experience Replay (PER)を用いることで、TDスコア(時間差分誤差)に基づいてサンプリングを調整することができます。本稿では、強化学習の意義を改めて確認し、いつものように交差検証ではなく、この仮説だけを検証する、ウィザードで組み立てたエキスパートアドバイザー(EA)を用いて考察します。
MQL5での取引戦略の自動化(第10回):トレンドフラットモメンタム戦略の開発 MQL5での取引戦略の自動化(第10回):トレンドフラットモメンタム戦略の開発
この記事では、「トレンドフラットモメンタム(Trend Flat Momentum)戦略」のためのエキスパートアドバイザー(EA)をMQL5で開発します。移動平均線のクロスオーバーに、RSI(相対力指数)とCCI(商品チャネル指数)といったモメンタム系のフィルターを組み合わせて、トレードシグナルを生成します。また、バックテストの方法や、実運用でのパフォーマンス向上のための改善案についても取り上げます。
MQL5での取引戦略の自動化(第9回):アジアブレイクアウト戦略のためのエキスパートアドバイザーの構築 MQL5での取引戦略の自動化(第9回):アジアブレイクアウト戦略のためのエキスパートアドバイザーの構築
この記事では、アジアブレイクアウト戦略のためのエキスパートアドバイザー(EA)をMQL5で構築します。セッション中の高値と安値を計算し、移動平均によるトレンドフィルタリングをおこないます。また、動的なオブジェクトスタイリング、ユーザー定義の時間入力、堅牢なリスク管理も実装します。最後に、プログラムの精度を高めるためのバックテストおよび最適化手法を紹介します。