
在MQL5中实现基于经济日历新闻事件的突破型智能交易系统(EA)
概述
重大经济数据发布前后市场波动率通常显著上升,为突破交易策略提供了理想的环境。在本文中,我们将阐述在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拖拽至图表上:
确保将saveFile设置为 "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
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。



你好,非常感谢!我对输入多种货币有点困惑。我试过
"美元"; "英镑"
"USD"; "GBP.
"美元""英镑";
只有最后一个没有出错,但我不确定它是否正确。也许它只接收美元。您能提供建议吗?
你好,非常感谢!我对输入多种货币有点困惑。我试过
"美元";"英镑
"USD"; "GBP.
"美元" 英镑
只有最后一个没有出错,但我不确定它是否正确。也许它只接收美元。能给点建议吗?
您好,如果您查看初始化函数中的代码,它会分割冒号并将不同货币存储到 curr 对象属性中。虽然不需要添加引号,但您的第一种方法应该可行。存储过程会将所有事件存储到二进制文件中,而不管其属性如何。只有在交易逻辑中,我们才会对属性进行过滤。这是我刚才运行的结果:
这种实现方式似乎没有考虑经纪商服务器上的时区切换(DST),因此在回溯测试 和优化过程中产生了不准确的结果。
谢谢你提醒我!我在文章中忘了考虑这一点,因为我使用了一个没有 DST 的经纪商进行演示。
https://www.mql5.com/zh/book/advanced/calendar
从这个来源我们可以知道,日历数据是由 MQL5 提供的,它会自动调整为经纪商当前的 Timetradeserver() 时区,这意味着对于有 DST 的经纪商,需要调整我的代码并将其考虑在内。
从这一来源我们可以知道,日历数据由 MQL5 方面提供,它会自动调整为经纪商当前的 Timetradeserver() 时区,这意味着对于使用 DST 的经纪商,需要调整我的代码并将其考虑在内。
由于书中公布的实现方法有点过时,实际(更新)情况可在博客、代码库(指标)和代码库(脚本)中找到。