English Русский Deutsch 日本語
preview
使用MQL5经济日历进行交易(第六部分):利用新闻事件分析和倒计时器实现交易入场自动化

使用MQL5经济日历进行交易(第六部分):利用新闻事件分析和倒计时器实现交易入场自动化

MetaTrader 5交易 |
356 3
Allan Munene Mutiiria
Allan Munene Mutiiria

概述

在本文中,我们针对MQL5经济日历系列推进一步,基于实时新闻分析实现交易入场自动化。在前一篇仪表盘功能增强(第五部分)的基础上,我们现在集成了一种交易逻辑,该逻辑使用用户自定义的筛选条件和时差偏移量来扫描新闻事件,对比预测值和先前值,并根据市场预期自动执行买入或卖出订单。我们还实现了动态倒计时器,用于显示距离新闻发布剩余的时间,并在订单执行后重置系统,确保我们的交易策略能够及时响应不断变化的市场状况。本文将通过以下主题展开论述:

  1. 理解交易逻辑需求
  2. 在MQL5中实现交易逻辑
  3. 创建并管理倒计时器
  4. 测试交易逻辑
  5. 结论

让我们深入探究,看看这些组件如何协同作用,以精准且可靠的方式实现自从化入场交易。


理解交易逻辑需求

对于我们的自动化交易系统而言,第一步是确定哪些新闻事件适合作为交易候选对象。我们将把符合以下条件的新闻事件定义为候选事件:该事件处于相对于其预定发布时间的特定时间窗口内,这个时间窗口由用户自定义的偏移量输入值确定。接下来,我们会设置交易模式输入项,例如在新闻发布前进行交易。以“新闻发布前交易”模式为例,只有在当前时间处于事件预定发布时间减去偏移量(例如5分钟)与事件实际发布时间之间时,该事件才符合条件。也就是说,我们会在实际发布前5分钟进行交易。

筛选环节至关重要,这样能确保我们只关注相关新闻。因此,我们的系统将采用多种筛选条件——货币筛选条件:用于聚焦选定的货币对;影响程度筛选条件:用于将事件限定在所选重要级别范围内;时间筛选条件:用于将事件限制在预设的总体时间范围内。用户可从仪表盘上进行选择。这种分层筛选方式有助于减少干扰信息,确保只处理最相关的新闻事件。

一旦某个事件通过了筛选条件,交易逻辑就会对比该新闻事件的关键数据点,具体来说,就是预测值与先前值。如果这两个值都存在且不为0,并且预测值高于先前值,系统将开立买入订单;如果预测值低于先前值,系统将开立卖出订单。如果任一值缺失或两者相等,则跳过该事件。这一决策过程将使得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”库,以实现订单执行功能,并声明一个名为“trade”的全局“CTrade”对象,用于处理交易。我们定义了一个枚举类型“ETradeMode”,其选项包括“TRADE_BEFORE”(新闻发布前交易)、“TRADE_AFTER”(新闻发布后交易)、“NO_TRADE”(不交易)和“PAUSE_TRADING”(暂停交易),并使用输入变量“tradeMode”(默认值为“TRADE_BEFORE”,程序将采用此默认值)来确定相对于新闻事件应在何时开立交易。此外,我们设置了“input”类型的变量“tradeOffsetHours”(交易偏移小时数)、“tradeOffsetMinutes”(交易偏移分钟数)、“tradeOffsetSeconds”(交易偏移秒数)和“tradeLotSize”(交易手数),用于指定时间偏移量和交易规模。同时,全局变量“tradeExecuted”(布尔型)、“tradedNewsTime”( 日期时间型)和数组“triggeredNewsEvents”(整型数组)有助于我们管理交易控制,并防止对同一新闻事件进行重复交易。随后,我们可以将交易逻辑整合到一个函数中。

//--- 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;
   }

这里,我们使用一个日期时间型变量(“candidateEventTime”,候选事件时间)、两个“string”类型变量(“candidateEventName”,候选事件名称;“candidateTradeSide”,候选交易方向)以及一个初始值设为 -1的“整型”变量(“candidateEventID”,候选事件ID)来初始化候选事件变量。然后,我们通过循环遍历由CalendarValueHistory函数检索到的每个事件(这些事件存储在由MqlCalendarValue结构体组成的数组中),并使用CalendarEventById函数将事件的详细信息填充到一个MqlCalendarEvent结构体中。

接下来,我们应用筛选条件:如果启用了货币筛选,我们会通过CalendarCountryById获取事件对应的MqlCalendarCountry结构体,并检查其“currency”字段是否与“curr_filter_selected”数组中的任一选项匹配;如果不匹配,我们会记录一条提示信息并跳过该事件。同样地,如果启用了重要性筛选,我们会遍历“imp_filter_selected”数组,以确保事件的重要性等级与所选等级之一匹配,如果不匹配,则记录信息并跳过。

最后,我们通过将事件ID与“triggeredNewsEvents”(已触发新闻事件)数组中存储的ID进行比较,来检查该事件是否已经触发过交易;如果已触发,我们会记录信息并跳过。该循环确保只有满足所有条件——货币、影响程度、时间范围以及唯一性——的事件才会被视为交易执行的候选事件。如果所有条件均通过且存在符合条件的事件,就可以根据用户允许的时间范围对事件进行进一步地筛选。

//--- 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;
   
         }
      }
   }
}

我们首先检查是否已选定候选事件且交易模式为“TRADE_BEFORE”,具体通过验证“candidateEventTime”是否大于0来实现。接着,我们从候选事件的预定时间中减去用户自定义的偏移量(“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"方法提取"预测值"和"先前值;如果任一值为0或两者相等,我们将记录原因并跳过该候选事件。最后,我们构建一个包含关键新闻信息的字符串并记录日志,同时使用"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”(候选交易方向)——如果是“买入”,则调用“trade.Buy”函数,传入指定的“tradeLotSize”(交易手数)、交易品种(_Symbol),并将事件名称作为备注(以确保唯一性并便于识别);如果“candidateTradeSide”是“卖出”,则同样调用“trade.Sell”函数。如果交易成功执行(即“tradeResult”为true),我们记录执行详情,使用ArrayResize函数调整“triggeredNewsEvents”数组的大小,并追加事件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秒,则在倒计时对象中显示重置消息——"新闻已发布,XX秒后重置"("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);
   }
}

如果当前时间未处于候选事件的交易窗口内——即当前时间既不大于或等于“targetTime”(计算方式为候选事件预定时间减去偏移量),也未小于候选事件预定时间,则我们假定当前时间仍处于交易窗口开启前。此时,通过将当前时间从候选事件预定时间中减去,计算出距离候选事件发生的剩余时间,并将该时间差转换为小时、分钟和秒。

我们使用IntegerToString函数,构建格式为“新闻倒计时:__小时 __分钟 __秒”的倒计时文本字符串。随后,我们使用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" 的无返回值函数。首先,我们初始化一个字符串,前缀为 "Filters: "。接着检查货币过滤器是否启用——如果启用,则追加"Currency: ",并使用"ArraySize"遍历"curr_filter_selected"数组,将每个选中的货币(用逗号分隔)添加到字符串中,最后以分号结尾;如果未启用,则简单记录为"Currency: Off; "。接下来,我们对影响级别过滤器执行类似操作:如果启用,则追加"Impact: ",并遍历"imp_filter_selected"数组,使用EnumToString将每个选中的影响级别转换为字符串后追加,如果未启用,记录为"Impact: Off "。

我们创建一个名为 "UpdateFilterInfo" 的无返回值函数(void function)。将所有片段拼接完成后,我们使用Print函数将完整的筛选信息输出至Experts日志,从而为故障排查和验证提供清晰、实时的筛选器状态快照。随后,我们在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

附加的文件 |
最近评论 | 前往讨论 (3)
Nikolay Moskalev
Nikolay Moskalev | 3 3月 2025 在 16:06

你好!感谢你们所做的工作。

我在 2K 显示器上的表格显示有问题。

Allan Munene Mutiiria
Allan Munene Mutiiria | 3 3月 2025 在 20:11
Nikolay Moskalev #:

你好!感谢您所做的工作。

我在 2K 显示器上的表格显示有问题。

您好。欢迎光临。这需要您修改字体大小,以便一切都能完美匹配。

Hosna karkooti
Hosna karkooti | 28 6月 2025 在 08:50
你好,日安。
关于您的 EA,我有几个问题。希望得到您的指导:

1.您建议使用该 EA 交易哪些货币对?


2.您建议用什么方法关闭交易?(止损、基于时间还是其他方法?)


3.安全使用该 EA 所需的最低账户余额 是多少?


4.如果我有一个 200 美元的账户,您建议使用什么设置文件或设置?



非常感谢您的支持。🌹

价格行为分析工具包开发(第十五部分):引入四分位理论(1)——四分位绘图脚本 价格行为分析工具包开发(第十五部分):引入四分位理论(1)——四分位绘图脚本
支撑位与阻力位是预示潜在趋势反转和延续的关键价位。尽管识别这些价位颇具挑战性,但一旦精准定位,您便能从容应对市场波动。如需进一步辅助,请参阅本文介绍的四分位绘图工具,该工具可帮助您识别主要及次要支撑位与阻力位。
市场模拟(第一部分):跨期订单(一) 市场模拟(第一部分):跨期订单(一)
今天我们将开始第二阶段,研究市场回放/模拟系统。首先,我们将展示跨期订单的可能解决方案。我会向你展示解决方案,但它还不是最终的。这将是我们在不久的将来需要解决的一个问题的可能解决方案。
MQL5自动化交易策略(第九部分):构建亚洲盘突破策略的智能交易系统(EA) MQL5自动化交易策略(第九部分):构建亚洲盘突破策略的智能交易系统(EA)
在本文中,我们将在MQL5中开发一款适用于亚洲盘突破策略的智能交易系统(EA),用来计算亚洲时段的高低价以及使用移动平均线(MA)进行趋势过滤。同时实现动态对象样式、用户自定义时间输入和完善的风险管理。最后演示回测与优化技术,进一步打磨策略表现。
MQL5交易策略自动化(第八部分):构建基于蝴蝶谐波形态的智能交易系统(EA) MQL5交易策略自动化(第八部分):构建基于蝴蝶谐波形态的智能交易系统(EA)
在本文中,我们将构建一个MQL5智能交易系统(EA),用于检测蝴蝶谐波形态。我们会识别关键转折点,并验证斐波那契(Fibonacci)水平以确认该形态。之后,我们会在图表上可视化该形态,并在得到确认时自动执行交易。