English Русский Deutsch 日本語
preview
在MQL5中实现基于经济日历新闻事件的突破型智能交易系统(EA)

在MQL5中实现基于经济日历新闻事件的突破型智能交易系统(EA)

MetaTrader 5示例 |
64 5
Zhuo Kai Chen
Zhuo Kai Chen

概述

重大经济数据发布前后市场波动率通常显著上升,为突破交易策略提供了理想的环境。在本文中,我们将阐述在MQL5中基于经济日历的突破策略的实现过程。我们将全面覆盖从创建用于解析和存储日历数据的类,到利用这些数据开发符合实际的回测系统,最终实现实盘交易执行代码的完整流程。


契机

尽管MQL5社区提供了大量关于在回测中使用MetaTrader 5日历的文章和代码库,但这些资源对想开发简单突破策略的新手而言往往过于复杂。本文旨在简化用MQL5新闻日历创建策略的流程,并为交易者提供一份全面指南。

构建“日历新闻突破”交易策略的动契机在于:利用可预期的定时新闻事件(如经济数据、财报或地缘公告)所引发的显著波动和价格变动。通过提前布局,交易者可在新闻后价格明确突破既定支撑或阻力时捕捉机会。该策略力求在新闻发布前后的高流动性与动量中获利,同时以严格的风险管理应对不确定性。最终形成利用关键日历事件市场反应的体系化方法。


日历新闻回测

MQL5提供了操作经纪商新闻日历的默认函数。然而,这些数据在策略测试器中无法直接获取。因此,我们可以基于勒内·巴尔克(Rene Balke)的思路创建一个日历历史数据头include文件,用于处理新闻历史数据并将其存储为二进制文件,以供后续使用。

  • CCalendarEntry类表示单个经济日历事件,包含国家、事件详情及相关数值(如预测值、实际值、先前值等)。
  • Compare()方法按时间与重要性比较两个日历事件,并返回一个值用以指示哪个事件被认为优先级更高。 
  • ToString() 方法将事件数据转为可读字符串,包含重要性及其他相关属性。

//+------------------------------------------------------------------+
//| A class to represent a single economic calendar event            |
//+------------------------------------------------------------------+
class CCalendarEntry :public CObject {
public:
   ulong country_id;
   string country_name;
   string country_code;
   string country_currency;
   string country_currency_symbol;
   string country_url_name;
   
   ulong event_id;
   ENUM_CALENDAR_EVENT_TYPE event_type;
   ENUM_CALENDAR_EVENT_SECTOR event_sector;
   ENUM_CALENDAR_EVENT_FREQUENCY event_frequency;
   ENUM_CALENDAR_EVENT_TIMEMODE event_time_mode;
   ENUM_CALENDAR_EVENT_UNIT event_unit;
   ENUM_CALENDAR_EVENT_IMPORTANCE event_importance;
   ENUM_CALENDAR_EVENT_MULTIPLIER event_multiplier;
   uint event_digits;
   string event_source_url;
   string event_event_code;
   string event_name;
      
   ulong value_id;
   datetime value_time;
   datetime value_period;
   int value_revision;
   long value_actual_value;
   long value_prev_value;
   long value_revised_prev_value;
   long value_forecast_value;
   ENUM_CALENDAR_EVENT_IMPACT value_impact_type;
   
//+------------------------------------------------------------------+
//| Compare news importance function                                 |
//+------------------------------------------------------------------+
   int Compare(const CObject *node, const int mode = 0) const{
      CCalendarEntry* other = (CCalendarEntry*)node;
      if (value_time==other.value_time){
         return event_importance-other.event_importance;
      }
      return (int)(value_time -other.value_time);
   }
   
//+------------------------------------------------------------------+
//| Convert data to string function                                  |
//+------------------------------------------------------------------+
   string ToString(){
      string txt;
      string importance = "None";
      if(event_importance==CALENDAR_IMPORTANCE_HIGH)importance="High";
      else if(event_importance==CALENDAR_IMPORTANCE_MODERATE) importance = "Moderate";
      else if(event_importance==CALENDAR_IMPORTANCE_LOW)importance = "Low";
      StringConcatenate(txt,value_time,">",event_name,"(",country_code,"|",country_currency,")",importance);
      return txt;
     }
   
};

  • CCalendarHistory类用于管理一组CCalendarEntry类对象,通过继承CArrayObj实现数组式功能,并提供访问和操作日历事件数据的方法。 
  • 重新加载operator[]方法,以返回集合中指定索引位置的CCalendarEntry对象,从而支持通过数组下标方式访问日历条目。 
  • At()方法返回指向指定索引处CCalendarEntry对象的指针。该方法会在访问数组前验证索引的有效性。 
  • LoadCalendarEntriesFromFile() 方法从二进制文件加载日历条目,读取相关数据(如国家信息、事件详情),并填充到CCalendarEntry对象中。

//+------------------------------------------------------------------+
//| A class to manage a collection of CCalendarEntry objects         |
//+------------------------------------------------------------------+
class CCalendarHistory :public CArrayObj{
public:
//overriding existing operators to better deal with calendar format data
   CCalendarEntry *operator[](const int index) const{return(CCalendarEntry*)At(index);}
   CCalendarEntry *At (const int index) const;
   bool LoadCalendarEntriesFromFile(string fileName);
   bool SaveCalendarValuesToFile(string filename);
   
};

CCalendarEntry *CCalendarHistory::At(const int index)const{
   if(index<0||index>=m_data_total)return(NULL);
   return (CCalendarEntry*)m_data[index];
   
}

//+------------------------------------------------------------------+
//| A function to load calendar events from your saved binary file   |
//+------------------------------------------------------------------+
bool CCalendarHistory::LoadCalendarEntriesFromFile(string fileName){
   CFileBin file;
   if(file.Open(fileName,FILE_READ|FILE_COMMON)>0){
      while(!file.IsEnding()){
         CCalendarEntry*entry = new CCalendarEntry();
         int len;
         file.ReadLong(entry.country_id);
         file.ReadInteger(len);
         file.ReadString(entry.country_name,len);
         file.ReadInteger(len);
         file.ReadString(entry.country_code,len);
         file.ReadInteger(len);
         file.ReadString(entry.country_currency,len);
         file.ReadInteger(len);
         file.ReadString(entry.country_currency_symbol,len);
         file.ReadInteger(len);
         file.ReadString(entry.country_url_name,len);
         
         file.ReadLong(entry.event_id);
         file.ReadEnum(entry.event_type);
         file.ReadEnum(entry.event_sector);
         file.ReadEnum(entry.event_frequency);
         file.ReadEnum(entry.event_time_mode);
         file.ReadEnum(entry.event_unit);
         file.ReadEnum(entry.event_importance);
         file.ReadEnum(entry.event_multiplier);
         file.ReadInteger(entry.event_digits);
         file.ReadInteger(len);
         file.ReadString(entry.event_source_url,len);
         file.ReadInteger(len);
         file.ReadString(entry.event_event_code,len);
         file.ReadInteger(len);
         file.ReadString(entry.event_name,len);
         
         file.ReadLong(entry.value_id);
         file.ReadLong(entry.value_time);
         file.ReadLong(entry.value_period);
         file.ReadInteger(entry.value_revision);
         file.ReadLong(entry.value_actual_value);
         file.ReadLong(entry.value_prev_value);
         file.ReadLong(entry.value_revised_prev_value);
         file.ReadLong(entry.value_forecast_value);
         file.ReadEnum(entry.value_impact_type);
         
         CArrayObj::Add(entry);        
      }
      Print(__FUNCTION__,">Loaded",CArrayObj::Total(),"Calendar Entries From",fileName,"...");
      CArray::Sort();
      file.Close();
      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| A function to save calendar values into a binary file            |
//+------------------------------------------------------------------+
bool CCalendarHistory::SaveCalendarValuesToFile(string fileName){
   CFileBin file;
   if(file.Open(fileName,FILE_WRITE|FILE_COMMON)>0){
     datetime chunk_end   = TimeTradeServer();
      // Let's do ~12 months (adjust as needed).
      int months_to_fetch  = 12*25;  
      
      while(months_to_fetch > 0)
      {
         // For each month, we go back ~30 days
         datetime chunk_start = chunk_end - 30*24*60*60;
         if(chunk_start < 1) // Just a safety check
            chunk_start = 1;
         MqlCalendarValue values[];
         if(CalendarValueHistory(values, chunk_start, chunk_end))
         {
            // Write to file
            for(uint i = 0; i < values.Size(); i++)
            {
               MqlCalendarEvent event;
               if(!CalendarEventById(values[i].event_id,event))
                  continue;  // skip if not found
               
               MqlCalendarCountry country;
               if(!CalendarCountryById(event.country_id,country))
                  continue;  // skip if not found
      
       file.WriteLong(country.id);
       file.WriteInteger(country.name.Length());
       file.WriteString(country.name,country.name.Length());
       file.WriteInteger(country.code.Length());
       file.WriteString(country.code,country.code.Length());
       file.WriteInteger(country.currency.Length());
       file.WriteString(country.currency,country.currency.Length());
       file.WriteInteger(country.currency_symbol.Length());
       file.WriteString(country.currency_symbol, country.currency_symbol.Length());
       file.WriteInteger(country.url_name.Length());
       file.WriteString(country.url_name,country.url_name.Length());
       
       file.WriteLong(event.id);
       file.WriteEnum(event.type);
       file.WriteEnum(event.sector);
       file.WriteEnum(event.frequency);
       file.WriteEnum(event.time_mode);
       file.WriteEnum(event.unit);
       file.WriteEnum(event.importance);
       file.WriteEnum(event.multiplier);
       file.WriteInteger(event.digits);
       file.WriteInteger(event.source_url.Length());
       file.WriteString(event.source_url,event.source_url.Length());
       file.WriteInteger(event.event_code.Length());
       file.WriteString(event.event_code,event.event_code.Length());
       file.WriteInteger(event.name.Length());
       file.WriteString(event.name,event.name.Length());
       
       file.WriteLong(values[i].id);
       file.WriteLong(values[i].time);
       file.WriteLong(values[i].period);
       file.WriteInteger(values[i].revision);
       file.WriteLong(values[i].actual_value);
       file.WriteLong(values[i].prev_value);
       file.WriteLong(values[i].revised_prev_value);
       file.WriteLong(values[i].forecast_value);
       file.WriteEnum(values[i].impact_type);
     }
            Print(__FUNCTION__, " >> chunk ", 
                  TimeToString(chunk_start), " - ", TimeToString(chunk_end), 
                  ": saved ", values.Size(), " events.");
         }
         
         // Move to the previous chunk
         chunk_end = chunk_start;
         months_to_fetch--;
         
         // short pause to avoid spamming server:
         Sleep(500);
      }
     
     file.Close();
     return true;
   }
   return false;
}

接下来,我们将构建一个用于获取回测结果的EA。

该EA将在5分钟时间框架下运行。针对每根已收盘的K线,系统会检查未来5分钟内是否存在高影响力新闻事件。若检测到此类事件,系统将在当前买入价(bid)的指定偏差范围内,分别设置买入止损(Buy Stop)和卖出止损(Sell Stop)订单,并可配置止损(Stop Loss)。此外,系统将在指定时间点自动平仓所有未平仓头寸。

开发这种新闻突破交易策略(即在重大新闻事件前于关键价位挂突破单)具有多方面的战略与战术意义:

  • 价格剧烈波动性:高影响力新闻通常引发显著的价格波动。在关键价位附近设置突破单,可帮助交易者在突破发生时立即入场,捕捉大幅价格走势。
  • 优化的风险回报比:如果交易者的止损在突破方向触发,那么快速剧烈的波动可以提供有利的盈亏比。
  • 新闻发布的可预测性:重大新闻的发布时间可提前获知,交易者可精准规划入场和出场时机,降低市场时机的不确定性。
  • 流动性预期激增: 新闻发布通常吸引更多市场参与者,当突破关键价位水平时,可以形成更可靠的突破走势。
  • 预先计划的执行:在新闻发布前将止损设置于关键技术价位,可避免市场剧烈波动时的情绪化决策,确保执行更加客观化。
  • 自动化执行潜力: 提前挂单可实现新闻发布时的自动化执行,无需人工干预即可快速响应市场变化。

通过整合这些逻辑,我们旨在构建一套系统化的方法,在利用高影响力新闻事件的可预测性与波动性的同时,保持严格的风险管理和清晰的执行规则。

该EA首先包含所有必要的辅助文件,并为相关类创建对象。同时声明后续将用到的全局变量。

#define FILE_NAME "CalendarHistory.bin"
#include <Trade/Trade.mqh>
#include <CalendarHistory.mqh>
#include <Arrays/ArrayString.mqh>
CCalendarHistory calendar;
CTrade trade;
CArrayString curr;

ulong poss, buypos = 0, sellpos=0;
input int Magic = 0;
int barsTotal = 0;
int currentIndex = 0;
datetime s_lastUpdate = 0;
input int closeTime = 18;
input int slp = 1000;
input int Deviation = 1000;
input string Currencies = "USD";
input ENUM_CALENDAR_EVENT_IMPORTANCE Importance = CALENDAR_IMPORTANCE_HIGH;
input bool saveFile = true;

OnInit()初始化函数负责完成以下操作:

  • 如果参数saveFile为 true,则将日历事件数据保存至名为 "CalendarHistory.bin" 的二进制文件中。
  • 随后,程序会尝试从该文件中加载日历事件数据。但是无法同时执行保存和加载操作,因为保存方法在完成写入后会自动关闭文件。
  • 将输入的字符串变量Currencies拆分为单个货币组成的数组,并对数组进行排序。因此,如果需要监控与美元(USD)和欧元(EUR)相关的新闻事件,只需在参数中输入 "USD;EUR"。
  • 程序会为CTrade交易对象分配一个唯一的Magic编号,用于标识由该EA发起的所有交易订单。

//+------------------------------------------------------------------+
//| Initializer function                                             |
//+------------------------------------------------------------------+
int OnInit() {
   if(saveFile==true)calendar.SaveCalendarValuesToFile(FILE_NAME);
   calendar.LoadCalendarEntriesFromFile(FILE_NAME);
   string arr[];
   StringSplit(Currencies,StringGetCharacter(";",0),arr);
   curr.AddArray(arr);
   curr.Sort();
   trade.SetExpertMagicNumber(Magic);  
   return(INIT_SUCCEEDED);
}

以下是执行交易任务所需的核心函数。

  • OnTradeTransaction:监控实时交易信号,当检测到带有指定Magic编号的买入/卖出订单成交时,将对应的订单号更新至buypos或sellpos变量中。
  • executeBuy:在指定价格水平挂出买入止损单(Buy Stop),并自动计算止损价位(Stop Loss),最终将生成的订单号记录至buypos变量。
  • executeSell:在指定价格水平挂出卖出止损单(Sell Stop),并自动计算止损价位(Stop Loss),最终将生成的订单号记录至sellpos变量。
  • IsCloseTime:检查当前服务器时间是否已超过预设的平仓截止时间。
//+------------------------------------------------------------------+
//| A function for handling trade transaction                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) {
    if (trans.type == TRADE_TRANSACTION_ORDER_ADD) {
        COrderInfo order;
        if (order.Select(trans.order)) {
            if (order.Magic() == Magic) {
                if (order.OrderType() == ORDER_TYPE_BUY) {
                    buypos = order.Ticket();
                } else if (order.OrderType() == ORDER_TYPE_SELL) {
                    sellpos = order.Ticket();
                }
            }
        }
    }
}

//+------------------------------------------------------------------+
//| Buy execution function                                           |
//+------------------------------------------------------------------+
void executeBuy(double price) {
       double sl = price- slp*_Point;
       sl = NormalizeDouble(sl, _Digits);
       double lots=0.1;
       trade.BuyStop(lots,price,_Symbol,sl,0,ORDER_TIME_DAY,1);
       buypos = trade.ResultOrder();
       }

//+------------------------------------------------------------------+
//| Sell execution function                                          |
//+------------------------------------------------------------------+
void executeSell(double price) {
       double sl = price + slp * _Point;
       sl = NormalizeDouble(sl, _Digits);
       double lots=0.1;
       trade.SellStop(lots,price,_Symbol,sl,0,ORDER_TIME_DAY,1);
       sellpos = trade.ResultOrder();
       }
       
//+------------------------------------------------------------------+
//| Exit time boolean function                                       |
//+------------------------------------------------------------------+
bool IsCloseTime(){
   datetime currentTime = TimeTradeServer();
   MqlDateTime timeStruct;
   TimeToStruct(currentTime,timeStruct);
   int currentHour =timeStruct.hour;
   return(currentHour>closeTime);
}

最后,我们只需在OnTick()函数中实现交易执行逻辑。

//+------------------------------------------------------------------+
//| OnTick function                                                  |
//+------------------------------------------------------------------+
void OnTick()
  {
    int bars = iBars(_Symbol,PERIOD_CURRENT);
  
    if (barsTotal!= bars){
      barsTotal = bars;
      double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   datetime now = TimeTradeServer();
   datetime horizon = now + 5*60; // 5 minutes from now

   while (currentIndex < calendar.Total())
   {   
         CCalendarEntry*entry=calendar.At(currentIndex);
         if (entry.value_time < now)
         {
            currentIndex++;
            continue;
         }
         // Now if the next event time is beyond horizon, break out
         if (entry.value_time > horizon) 
            break;
         
         // If it is within the next 5 minutes, check other conditions:
         if (entry.event_importance >= Importance && curr.SearchFirst(entry.country_currency) >= 0 && buypos == sellpos )
         {
             double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
             executeBuy(bid + Deviation*_Point);
             executeSell(bid - Deviation*_Point);
         }
         currentIndex++;
      }
    if(IsCloseTime()){
       for(int i = 0; i<PositionsTotal(); i++){
         poss = PositionGetTicket(i);
         if(PositionGetInteger(POSITION_MAGIC) == Magic) trade.PositionClose(poss);      
      }
    }
     if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
      buypos = 0;
      }
     if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
      sellpos = 0;
      }     

   }
}

这样确保了我们仅在新K线收盘时检查交易条件。若当前K线与上次保存的K线相同,则说明尚未形成新K线,此时将直接退出,不执行后续的交易逻辑。

    int bars = iBars(_Symbol,PERIOD_CURRENT);
  
    if (barsTotal!= bars){
      barsTotal = bars;

该循环逻辑通过从数据起始点开始处理,确保了回测过程的高效性。仅在当前时间晚于指定事件时间时,程序会递增全局索引变量,从而避免每次重新从数据开头遍历。这种方法显著减少了回测期间的计算量和内存占用,大幅加速了回测进程,尤其在长时间周期的测试中可节省大量时间。

while (currentIndex < calendar.Total())
   {   
         CCalendarEntry*entry=calendar.At(currentIndex);
         if (entry.value_time < now)
         {
            currentIndex++;
            continue;
         }
         // Now if the next event time is beyond horizon, break out
         if (entry.value_time > horizon) 
            break;
         
         // If it is within the next 5 minutes, check other conditions:
         if (entry.event_importance >= Importance && curr.SearchFirst(entry.country_currency) >= 0 && buypos == sellpos )
         {
             double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
             executeBuy(bid + Deviation*_Point);
             executeSell(bid - Deviation*_Point);
         }
         currentIndex++;
      }  

此部分用于检查当前时间是否已超过预设的收盘时间。如果条件满足,系统将遍历投资组合,查找带有该EA的Magic编号的未平仓头寸,并执行平仓操作。平仓完成后,对应持仓订单号将被重置为0。

    if(IsCloseTime()){
       for(int i = 0; i<PositionsTotal(); i++){
         poss = PositionGetTicket(i);
         if(PositionGetInteger(POSITION_MAGIC) == Magic) trade.PositionClose(poss);      
      }
    }
     if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
      buypos = 0;
      }
     if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
      sellpos = 0;
      }     

现在编译程序并打开MetaTrader 5交易终端。打开任意一个图表窗口,按如下方式将此EA拖拽至图表上:

拖拽EA

确保将saveFile设置为 "true"。

设置为true

您的经纪商提供的新闻事件数据在所有图表中均保持一致,因此不会影响交易品种的选择。该EA在附加到图表后会立即初始化,这意味着文件将在此时完成保存。等待数秒后,您即可以移除该EA。文件将被保存至您计算机的 公共文件路径,并且您可以前往该路径验证二进制文件是否已成功保存。

现在,您可以在策略测试器中测试该策略。

参数

关于回测参数的重要说明:

  • saveFile变量设为 "false",以避免二进制数据文件在初始化阶段被自动关闭。

  • 合理设置偏差值(Deviation)和止损(Stop Loss)。若偏差范围过大,将无法有效捕捉新闻事件引发的价格波动;而偏差或止损设置过小,则可能在新闻数据剧烈波动时导致严重滑点。

  • 合理选择平仓时间。建议选择接近市场收盘或交易日结束的时间点平仓,以便完整捕捉新闻事件驱动的行情走势。具体平仓时间需根据您经纪商的服务器时间进行相应调整。

以下是我对SPIUSDc品种在2019年1月1日 - 2024年12月1日期间、5分钟时间框架下的回测结果。

设置

净值曲线:

结果

重要成果:

  • 盈利系数:1.26
  • 夏普比率:2.66
  • 交易数量:1604

最后,建议读者在条件允许时使用真实tick数据进行回测。在压力测试中,选择高延迟环境以模拟重大新闻事件期间的滑点和高点差影响。最后但同样重要的是,在实盘模拟环境中交易,以验证回测结果的可信度。不同经纪商的滑点差异显著——某些经纪商滑点严重,而另一些则几乎无滑点。对于依赖高波动性的策略,务必采取额外措施确保其在实盘交易中持续盈利。


实盘交易实现指南

实盘交易需使用独立的EA代码。我们可以继续利用MQL5的财经日历功能,沿用与此前相同的逻辑。唯一区别在于:需创建一个函数,每小时更新一次即将发生的新闻事件,并将其存储在自定义的日历对象数组中。

该代码通过以下步骤更新日历历史数据:如果自上次更新已超过1小时,则从财经日历API中获取新事件数据;将每个事件的详细信息(包括国家、数值、预测值等)处理并存储至新建的CCalendarEntry对象中。

//+------------------------------------------------------------------+
//| Update upcoming news events                                      |
//+------------------------------------------------------------------+
void UpdateCalendarHistory(CCalendarHistory &history)
{
   //upcoming event in the next hour
   datetime fromTime = TimeTradeServer()+3600;
   // For example, if it's been > 1hr since last update:
   if(fromTime - s_lastUpdate > 3600)
   {
      // Determine the time range to fetch new events
      // For instance, from s_lastUpdate to 'now'
      MqlCalendarValue values[];
      if(CalendarValueHistory(values, s_lastUpdate, fromTime))
      {
         for(uint i = 0; i < values.Size(); i++)
         {
            MqlCalendarEvent event;
            if(!CalendarEventById(values[i].event_id,event)) 
               continue;           
            MqlCalendarCountry country;
            if(!CalendarCountryById(event.country_id, country))
               continue;          
            // Create a new CCalendarEntry and fill from 'values[i]', 'event', 'country'
            CCalendarEntry *entry = new CCalendarEntry();
            entry.country_id = country.id;
            entry.value_time               = values[i].time;
            entry.value_period             = values[i].period;
            entry.value_revision           = values[i].revision;
            entry.value_actual_value       = values[i].actual_value;
            entry.value_prev_value         = values[i].prev_value;
            entry.value_revised_prev_value = values[i].revised_prev_value;
            entry.value_forecast_value     = values[i].forecast_value;
            entry.value_impact_type        = values[i].impact_type;
            // event data
            entry.event_id             = event.id;
            entry.event_type           = event.type;
            entry.event_sector         = event.sector;
            entry.event_frequency      = event.frequency;
            entry.event_time_mode      = event.time_mode;
            entry.event_unit           = event.unit;
            entry.event_importance     = event.importance;
            entry.event_multiplier     = event.multiplier;
            entry.event_digits         = event.digits;
            entry.event_source_url     = event.source_url;
            entry.event_event_code     = event.event_code;
            entry.event_name           = event.name;
            // country data
            entry.country_name         = country.name;
            entry.country_code         = country.code;
            entry.country_currency     = country.currency;
            entry.country_currency_symbol = country.currency_symbol;
            entry.country_url_name     = country.url_name;
            // Add to your in-memory calendar
            history.Add(entry);
         }
      }
      // Sort to keep chronological order
      history.Sort();     
      // Mark the last update time
      s_lastUpdate = fromTime;
   }
}

其余代码部分与回测EA几乎完全一致。您只需按如下方式将代码整合到实盘执行EA中,即可完成部署。

#include <Trade/Trade.mqh>
#include <CalendarHistory.mqh>
#include <Arrays/ArrayString.mqh>
CCalendarHistory calendar;
CArrayString curr;
CTrade trade;

ulong poss, buypos = 0, sellpos=0;
input int Magic = 0;
int barsTotal = 0;
datetime s_lastUpdate = 0;
input int closeTime = 18;
input int slp = 1000;
input int Deviation = 1000;
input string Currencies = "USD";
input ENUM_CALENDAR_EVENT_IMPORTANCE Importance = CALENDAR_IMPORTANCE_HIGH;

//+------------------------------------------------------------------+
//| Initializer function                                             |
//+------------------------------------------------------------------+
int OnInit() {
   trade.SetExpertMagicNumber(Magic);  
   string arr[];
   StringSplit(Currencies,StringGetCharacter(";",0),arr);
   curr.AddArray(arr);
   curr.Sort();
   return(INIT_SUCCEEDED);
}

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

//+------------------------------------------------------------------+
//| OnTick function                                                  |
//+------------------------------------------------------------------+
void OnTick()
  {
    int bars = iBars(_Symbol,PERIOD_CURRENT);
  
    if (barsTotal!= bars){
      barsTotal = bars;
      UpdateCalendarHistory(calendar);
      double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   datetime now = TimeTradeServer();
   datetime horizon = now + 5*60; // 5 minutes from now

   // Loop over all loaded events
   for(int i = 0; i < calendar.Total(); i++)
   {
      CCalendarEntry *entry = calendar.At(i);

      // If event time is between 'now' and 'now+5min'
      if(entry.value_time > now && entry.value_time <= horizon&&buypos==sellpos&&entry.event_importance>=Importance&&curr.SearchFirst(entry.country_currency)>=0)
      {
        executeBuy(bid+Deviation*_Point);
        executeSell(bid-Deviation*_Point);
        }
     }
    if(IsCloseTime()){
       for(int i = 0; i<PositionsTotal(); i++){
         poss = PositionGetTicket(i);
         if(PositionGetInteger(POSITION_MAGIC) == Magic) trade.PositionClose(poss);      
      }
    }
     if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
      buypos = 0;
      }
     if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
      sellpos = 0;
      }     
   }
}

//+------------------------------------------------------------------+
//| A function for handling trade transaction                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) {
    if (trans.type == TRADE_TRANSACTION_ORDER_ADD) {
        COrderInfo order;
        if (order.Select(trans.order)) {
            if (order.Magic() == Magic) {
                if (order.OrderType() == ORDER_TYPE_BUY) {
                    buypos = order.Ticket();
                } else if (order.OrderType() == ORDER_TYPE_SELL) {
                    sellpos = order.Ticket();
                }
            }
        }
    }
}

//+------------------------------------------------------------------+
//| Buy execution function                                           |
//+------------------------------------------------------------------+
void executeBuy(double price) {
       double sl = price- slp*_Point;
       sl = NormalizeDouble(sl, _Digits);
       double lots=0.1;
       trade.BuyStop(lots,price,_Symbol,sl,0,ORDER_TIME_DAY,1);
       buypos = trade.ResultOrder();
       }

//+------------------------------------------------------------------+
//| Sell execution function                                          |
//+------------------------------------------------------------------+
void executeSell(double price) {
       double sl = price + slp * _Point;
       sl = NormalizeDouble(sl, _Digits);
       double lots=0.1;
       trade.SellStop(lots,price,_Symbol,sl,0,ORDER_TIME_DAY,1);
       sellpos = trade.ResultOrder();
       }

//+------------------------------------------------------------------+
//| Update upcoming news events                                      |
//+------------------------------------------------------------------+
void UpdateCalendarHistory(CCalendarHistory &history)
{
   //upcoming event in the next hour
   datetime fromTime = TimeTradeServer()+3600;
   // For example, if it's been > 1hr since last update:
   if(fromTime - s_lastUpdate > 3600)
   {
      // Determine the time range to fetch new events
      // For instance, from s_lastUpdate to 'now'
      MqlCalendarValue values[];
      if(CalendarValueHistory(values, s_lastUpdate, fromTime))
      {
         for(uint i = 0; i < values.Size(); i++)
         {
            MqlCalendarEvent event;
            if(!CalendarEventById(values[i].event_id,event)) 
               continue;           
            MqlCalendarCountry country;
            if(!CalendarCountryById(event.country_id, country))
               continue;          
            // Create a new CCalendarEntry and fill from 'values[i]', 'event', 'country'
            CCalendarEntry *entry = new CCalendarEntry();
            entry.country_id = country.id;
            entry.value_time               = values[i].time;
            entry.value_period             = values[i].period;
            entry.value_revision           = values[i].revision;
            entry.value_actual_value       = values[i].actual_value;
            entry.value_prev_value         = values[i].prev_value;
            entry.value_revised_prev_value = values[i].revised_prev_value;
            entry.value_forecast_value     = values[i].forecast_value;
            entry.value_impact_type        = values[i].impact_type;
            // event data
            entry.event_id             = event.id;
            entry.event_type           = event.type;
            entry.event_sector         = event.sector;
            entry.event_frequency      = event.frequency;
            entry.event_time_mode      = event.time_mode;
            entry.event_unit           = event.unit;
            entry.event_importance     = event.importance;
            entry.event_multiplier     = event.multiplier;
            entry.event_digits         = event.digits;
            entry.event_source_url     = event.source_url;
            entry.event_event_code     = event.event_code;
            entry.event_name           = event.name;
            // country data
            entry.country_name         = country.name;
            entry.country_code         = country.code;
            entry.country_currency     = country.currency;
            entry.country_currency_symbol = country.currency_symbol;
            entry.country_url_name     = country.url_name;
            // Add to your in-memory calendar
            history.Add(entry);
         }
      }
      // Sort to keep chronological order
      history.Sort();     
      // Mark the last update time
      s_lastUpdate = fromTime;
   }
}

//+------------------------------------------------------------------+
//| Exit time boolean function                                       |
//+------------------------------------------------------------------+
bool IsCloseTime(){
   datetime currentTime = TimeTradeServer();
   MqlDateTime timeStruct;
   TimeToStruct(currentTime,timeStruct);
   int currentHour =timeStruct.hour;
   return(currentHour>closeTime);
}

在未来的策略开发中,您可以基于本文阐述的基础框架,深入探索以新闻事件为核心的交易策略。例如:

关键价位水平突破交易
摒弃固定点差依赖,转而聚焦重大新闻引发的价格突破关键支撑/阻力位。例如,当重要经济数据或企业公告导致价格突破时,您可以顺势入场。这就需要提前标注新闻事件时间表与关键价位。

新闻反向交易
假设市场对新闻的初始反应过度,该策略逆市场反应交易。新闻发布后价格剧烈波动时,您可以等待市场修正后反向入场。

新闻事件过滤
如果策略适合低波动市场,您可以主动规避高影响力新闻时段。通过提前查看新闻日历,您可以将交易系统设置为:在事件来临前平仓或暂停新交易,待消息落地、待市场恢复平稳后再恢复交易,从而确保处于更稳定的市场环境。

新闻剥头皮交易
捕捉新闻引发的短期价格波动,通过高频进出获利。该策略以快进快出为核心:持仓时间超短、极窄止损、快速止盈。特别适用于因高波动事件而引发的快速价格摆动行情。

经济日历驱动交易
该策略围绕经济日历中的既定事件展开,例如利率决议、GDP公布或就业报告。通过研究市场对同类新闻的历史反应,并结合当前预期,您可以预判潜在价格走势并提前布局。

这些策略都依赖于提前收集并分析相关的新闻数据,让您能够充分利用重要事件引发的市场波动。



结论

在本文中,我们首先创建了一个辅助头(include)文件,用于解析、格式化并存储经济日历中的新闻事件数据。随后,我们开发了一款回测型智能交易系统(EA),用于获取经纪商提供的新闻事件数据,实现策略逻辑,并基于这些数据验证策略盈利能力。该策略在5年的tick数据、超过1,600个样本的测试中展现出良好的盈利能力。最后,我们公开了用于实盘交易的EA代码,并展望了未来的发展方向,鼓励读者基于本文提出的框架进一步开发更多策略。


文件表

文件名 文件使用
CalendarHistory.mqh 处理日历新闻事件数据的辅助include文件。
News Breakout Backtest.mq5 用于存储新闻事件数据和执行回测的EA。
News Breakout.mq5 实盘交易EA。

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16752

最近评论 | 前往讨论 (5)
hrawoodward
hrawoodward | 16 5月 2025 在 06:47

你好,非常感谢!我对输入多种货币有点困惑。我试过

"美元"; "英镑"

"USD"; "GBP.

"美元""英镑";

只有最后一个没有出错,但我不确定它是否正确。也许它只接收美元。您能提供建议吗?

Zhuo Kai Chen
Zhuo Kai Chen | 16 5月 2025 在 10:28
hrawoodward #:

你好,非常感谢!我对输入多种货币有点困惑。我试过

"美元";"英镑

"USD"; "GBP.

"美元" 英镑

只有最后一个没有出错,但我不确定它是否正确。也许它只接收美元。能给点建议吗?

您好,如果您查看初始化函数中的代码,它会分割冒号并将不同货币存储到 curr 对象属性中。虽然不需要添加引号,但您的第一种方法应该可行。存储过程会将所有事件存储到二进制文件中,而不管其属性如何。只有在交易逻辑中,我们才会对属性进行过滤。这是我刚才运行的结果:

设置

结果

Stanislav Korotky
Stanislav Korotky | 16 5月 2025 在 16:22
这种实现方式似乎没有考虑经纪商服务器上的时区切换(DST),因此在回溯测试 和优化过程中产生了不准确的结果。
Zhuo Kai Chen
Zhuo Kai Chen | 17 5月 2025 在 03:17
Stanislav Korotky #:
这种实现方式似乎没有考虑经纪商服务器上的时区切换(DST),因此在回溯测试 和优化过程中产生了不准确的结果。

谢谢你提醒我!我在文章中忘了考虑这一点,因为我使用了一个没有 DST 的经纪商进行演示。

https://www.mql5.com/zh/book/advanced/calendar

从这个来源我们可以知道,日历数据是由 MQL5 提供的,它会自动调整为经纪商当前的 Timetradeserver() 时区,这意味着对于有 DST 的经纪商,需要调整我的代码并将其考虑在内。

Stanislav Korotky
Stanislav Korotky | 17 5月 2025 在 13:05
Zhuo Kai Chen #:

从这一来源我们可以知道,日历数据由 MQL5 方面提供,它会自动调整为经纪商当前的 Timetradeserver() 时区,这意味着对于使用 DST 的经纪商,需要调整我的代码并将其考虑在内。

由于书中公布的实现方法有点过时,实际(更新)情况可在博客代码库(指标)和代码库(脚本)中找到。

交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
日志记录精通指南(第三部分):探索日志处理器(Handlers)实现方案 日志记录精通指南(第三部分):探索日志处理器(Handlers)实现方案
在本文中,我们将探索日志库中"处理器"(handlers)的概念,理解其工作原理,并创建三种基础实现:控制台、数据库和文件。我们将覆盖从处理器的基本结构到实际测试,为后续文章中的完整功能实现奠定基础。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
迁移至 MQL5 Algo Forge(第 3 部分):在您自己的项目中使用外部仓库 迁移至 MQL5 Algo Forge(第 3 部分):在您自己的项目中使用外部仓库
让我们探索如何开始将 MQL5 Algo Forge 存储中任何仓库的外部代码集成到您自己的项目中。在本文中,我们最后转向这个有前景但更复杂的任务:如何在 MQL5 Algo Forge 中实际连接和使用来自第三方仓库的库。