Desenvolvendo um Expert Advisor de Breakout Baseado em Eventos de Notícias do Calendário em MQL5
Introdução
A volatilidade tende a atingir picos em torno de eventos de notícias de alto impacto, criando oportunidades significativas de breakout. Neste artigo, iremos delinear o processo de implementação de uma estratégia de breakout baseada em calendário em MQL5.. Abordaremos tudo, desde a criação de uma classe para interpretar e armazenar dados do calendário, o desenvolvimento de backtests realistas utilizando esses dados e, por fim, a implementação do código de execução para negociação ao vivo.
Motivação
Embora a comunidade MQL5 ofereça inúmeros artigos e bases de código sobre o tratamento de calendários do MetaTrader 5 em backtesting, esses recursos podem ser excessivamente complexos para iniciantes que desejam desenvolver uma estratégia simples de breakout. Este artigo busca simplificar o processo de criação de uma estratégia utilizando o calendário de notícias no MQL5 e fornecer um guia abrangente para traders.
A motivação para criar uma estratégia de negociação de breakout baseada em notícias de calendário reside em aproveitar o momento previsível de eventos de notícias programados — como relatórios econômicos, divulgações de resultados ou anúncios geopolíticos — que frequentemente desencadeiam volatilidade significativa no mercado e movimentos de preço. Ao antecipar esses eventos, os traders buscam capitalizar oportunidades de breakout quando os preços se movem decisivamente além de níveis estabelecidos de suporte ou resistência após a divulgação da notícia. Essa estratégia procura maximizar os lucros provenientes do aumento de liquidez e momentum em torno das divulgações de notícias, ao mesmo tempo em que emprega uma gestão de risco disciplinada para navegar pela incerteza elevada. Em última análise, ela fornece uma abordagem estruturada para explorar os padrões e reações que normalmente ocorrem nos mercados em torno de eventos-chave do calendário.
Backtest de Notícias do Calendário
O MQL5 possui operações padrão para lidar com dados do Calendário de Notícias fornecidos pelos corretores. No entanto, esses dados não podem ser obtidos no testador de estratégias utilizando o código existente. Portanto, podemos criar um arquivo include de histórico de calendário adaptado de Rene Balke que processa os dados históricos de notícias e os armazena em um arquivo binário, que utilizaremos posteriormente.
- A classe CCalendarEntry representa um único evento do calendário econômico com várias propriedades relacionadas ao país, aos detalhes do evento e aos seus valores associados (por exemplo, valores previstos, reais, anteriores, etc.).
- O método Compare() compara dois eventos do calendário com base em seu tempo e importância, retornando um valor que indica qual evento é considerado maior.
- O método ToString() converte os dados do evento em um formato de string legível por humanos, incluindo a importância do evento e outras propriedades relevantes.
//+------------------------------------------------------------------+ //| 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; } };
- A classe CCalendarHistory gerencia uma coleção de objetos da classe CCalendarEnrtry, estendendo CArrayObj para funcionalidade semelhante a arrays, e fornece métodos para acessar e manipular os dados dos eventos do calendário.
- O método operator[] é sobrescrito para retornar um objeto CCalendarEnrtry em um índice específico da coleção, permitindo o acesso aos eventos do calendário no estilo de array.
- O método At() retorna um ponteiro para um CCalendarEnrtry em um índice especificado. Ele garante que o índice seja válido antes de acessar o array.
- O método LoadCalendarEntriesFromFile() carrega as entradas do calendário a partir de um arquivo binário, lendo os dados relevantes (por exemplo, informações do país, detalhes do evento) e populando objetos CCalendarEnrtry.
//+------------------------------------------------------------------+ //| 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; }
Em seguida, criaremos o expert advisor responsável por obter os resultados do backtest.
Para este expert advisor, iremos executá-lo em um timeframe de 5 minutos. Para cada barra fechada, verificamos se existe um evento de notícia de alto impacto nos próximos 5 minutos. Se existir, posicionamos ordens buy stop e sell stop dentro de um determinado desvio em relação ao preço bid atual, com um stop loss opcional. Além disso, sairemos de todas as posições abertas assim que um horário especificado for atingido.
Desenvolver uma estratégia de negociação de breakout de notícias — em que ordens de compra/venda stop são posicionadas em níveis-chave pouco antes de eventos de notícias de alto impacto — pode ser motivado por várias considerações estratégicas e táticas:
- Movimento Explosivo de Preço: Notícias de alto impacto frequentemente desencadeiam oscilações significativas de preço. Posicionar ordens stop próximas a níveis-chave pode ajudar os traders a entrar assim que um breakout ocorre, capturando grandes movimentos de preço.
- Relação Risco-Retorno Aprimorada: Movimentos rápidos e voláteis podem apresentar configurações favoráveis de risco-retorno se as ordens do trader forem acionadas na direção do breakout.
- Previsibilidade das Divulgações de Notícias: Como o momento das notícias de alto impacto é conhecido com antecedência, os traders podem planejar entradas e saídas com mais precisão, reduzindo a incerteza em torno do timing do mercado.
- Antecipação de Aumento de Liquidez: As divulgações de notícias frequentemente atraem maior participação do mercado, o que pode levar a breakouts mais confiáveis quando níveis-chave são rompidos.
- Execução Pré-Planejada: Definir ordens stop em níveis técnicos-chave antes da notícia ajuda a remover a tomada de decisão emocional no momento do choque de mercado, levando a uma execução mais disciplinada.
- Possibilidade de Automação: Posicionar ordens com antecedência pode permitir a execução automatizada assim que a notícia é divulgada, garantindo uma resposta rápida aos movimentos do mercado sem a necessidade de intervenção manual.
Ao combinar essas motivações, buscamos desenvolver uma abordagem sistemática que capitalize a previsibilidade e a volatilidade dos eventos de notícias de alto impacto, mantendo uma gestão de risco disciplinada e regras claras de execução.
O expert advisor começa incluindo os arquivos auxiliares necessários e criando objetos para as classes relacionadas. Ele também declara as variáveis globais relevantes que serão utilizadas posteriormente.
#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;
A função inicializadora OnInit() garante as seguintes ações:
- Se saveFile for verdadeiro, as entradas do calendário são salvas em um arquivo chamado "CalendarHistory.bin".
- Os eventos do calendário são então carregados a partir desse arquivo. Mas não é possível salvar e carregar ao mesmo tempo, pois o método de salvamento fecha o arquivo ao final.
- A variável de entrada string Currencies é dividida em um array de moedas individuais, e o array é ordenado. Assim, se você quiser eventos relacionados às moedas USD e EUR, basta inserir "USD";"EUR".
- O número Magic é atribuído ao objeto CTrade para identificar negociações iniciadas por este 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); }
Aqui estão as funções necessárias para realizar as tarefas de execução.
- OnTradeTransaction: Monitora transações de trade recebidas e atualiza buypos ou sellpos com o ticket da ordem quando uma ordem de compra ou venda com o número magic especificado é adicionada.
- executeBuy: Posiciona uma ordem buy stop no preço fornecido com um stop loss calculado e registra o ticket da ordem resultante em buypos.
- executeSell: Posiciona uma ordem sell stop no preço fornecido com um stop loss calculado e registra o ticket da ordem resultante em sellpos.
- IsCloseTime: Verifica o horário atual do servidor para determinar se ele já ultrapassou a hora de fechamento predefinida.
//+------------------------------------------------------------------+ //| 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); }
Por fim, apenas implementamos a lógica de execução na função 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; } } }
Isso garante que verifiquemos a condição apenas em novas barras fechadas. Se a barra atual for a mesma que a última barra salva, isso implica que ainda não é uma nova barra, portanto retornamos sem executar o restante da lógica de negociação.
int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars;
A lógica do loop while garante um backtesting eficiente ao iniciar desde o começo dos dados. Se o tempo atual for posterior a um determinado evento, incrementamos a variável global index, evitando a necessidade de percorrer o loop desde o início novamente. Isso ajuda a reduzir o tempo de computação e o uso de memória durante o backtesting, acelerando drasticamente o processo e economizando muito tempo — especialmente em períodos longos de teste.
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++; }
Esta parte verifica se a hora atual já ultrapassou o horário de fechamento. Se isso ocorrer, ela percorre o portfólio, verifica se existem posições abertas com o número magic do EA e as fecha. Uma vez fechadas, o ticket da posição é redefinido para zero.
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; }
Agora compile o programa e vá para o terminal MetaTrader 5. Abra qualquer gráfico e arraste este EA para o gráfico da seguinte forma:

Certifique-se de que a configuração saveFile esteja definida como "true".

Os dados de eventos de notícias fornecidos pelo seu corretor são os mesmos em todos os gráficos, portanto, os símbolos não importam. O EA será inicializado imediatamente ao ser anexado ao gráfico, o que significa que o arquivo será salvo nesse momento. Após alguns segundos, você pode remover o EA. O arquivo será salvo no caminho de arquivos comuns do seu computador, e você pode navegar até lá para verificar se o arquivo binário foi salvo.
Agora você pode testar a estratégia no testador de estratégias.

Notas importantes sobre os parâmetros do seu backtest:
-
Defina a variável saveFile como "false" para evitar que o arquivo de dados binários seja fechado durante a inicialização.
-
Selecione um desvio e um stop loss razoáveis. Um desvio muito amplo não conseguirá capturar a volatilidade do evento de notícias, enquanto um desvio ou stop loss muito estreito ficará suscetível a slippage durante picos de notícias.
-
Escolha um horário de fechamento de posição razoável. Recomendo selecionar uma hora próxima ao fechamento do mercado ou ao final do dia de negociação, para que seja possível capturar todo o movimento impulsionado pelas notícias. A hora de entrada correspondente dependerá do horário do servidor do seu corretor.
Aqui está o meu backtest no SPIUSDc de 2019.1.1 – 2024.12.1 no timeframe de 5 minutos.



Resultados principais:
- Fator de lucro: 1,26
- Índice Sharpe: 2,66
- Número de negociações: 1604
Por fim, sugiro que os leitores utilizem ticks reais, se disponíveis. Selecione o teste de estresse em alta latência para considerar slippage e spreads elevados durante eventos de notícias de alto impacto. Por último, mas não menos importante, opere em conta demo em condições ao vivo para validar que os resultados do backtest são confiáveis. Alguns corretores apresentam slippage significativo, enquanto outros podem ter slippage mínimo. Ao negociar uma estratégia que depende de alta volatilidade, sempre adote medidas adicionais para garantir que a estratégia seja lucrativa na negociação ao vivo.
Implementação de Negociação ao Vivo
Para a negociação ao vivo, utilizaremos um código de expert advisor separado. Agora podemos utilizar as operações de calendário do MQL5, implementando a mesma lógica de antes. A única diferença é que criamos uma função para atualizar os próximos eventos de notícias e armazená-los no array do objeto de calendário que criamos, atualizando-o a cada hora.
A lógica desse código atualiza um histórico de calendário buscando novos dados de eventos a partir de uma API de calendário se mais de uma hora tiver se passado desde a última atualização, processando e armazenando então os detalhes do evento, incluindo país, valor e dados de previsão, em um novo objeto CCalendarEntry para cada evento.
//+------------------------------------------------------------------+ //| 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; } }
O restante do código é praticamente o mesmo do EA de backtest. Você pode simplesmente integrar o código ao EA de execução desta forma, e pronto.
#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); }
Para o desenvolvimento futuro de estratégias, você pode construir sobre a base descrita neste artigo e explorar ideias focadas em negociação baseada em notícias. Por exemplo:
Negociando Breakouts em Níveis-Chave
Em vez de depender de desvios fixos, essa abordagem foca em movimentos de preço acionados por notícias significativas que rompem níveis-chave de suporte ou resistência. Por exemplo, quando grandes relatórios econômicos ou anúncios corporativos causam um breakout, você pode entrar em negociações na direção do movimento. Para implementar isso, monitore o cronograma de notícias e identifique níveis críticos de preço com antecedência.
Operar Contra a Notícia
Essa estratégia envolve negociar contra a reação inicial do mercado a um evento de notícia, assumindo que o primeiro movimento é um exagero. Após um pico acentuado de preço após a notícia, você aguarda o mercado se corrigir e então entra em uma negociação na direção oposta.
Filtrando Eventos de Notícias
Se sua estratégia funciona melhor em mercados de baixa volatilidade, você pode evitar negociar durante eventos de notícias de alto impacto. Ao verificar o calendário de notícias com antecedência, você pode programar seu sistema de negociação para fechar posições abertas ou pausar novas negociações até depois do evento, garantindo condições de mercado mais estáveis.
Scalping de Notícias
Essa ideia foca em capturar pequenos lucros a partir de movimentos de preço de curto prazo causados por eventos de notícias. Envolve entradas e saídas rápidas, stop-loss apertados e realização rápida de lucros. Essa estratégia é particularmente eficaz durante eventos voláteis que causam oscilações rápidas de preço.
Negociação com Calendário Econômico
Essa abordagem gira em torno de eventos programados no calendário econômico, como decisões sobre taxas de juros, anúncios de PIB ou relatórios de emprego. Ao analisar como os mercados reagiram historicamente a notícias semelhantes e considerar as expectativas atuais, é possível antecipar movimentos potenciais de preço e se preparar adequadamente.
Cada uma dessas estratégias depende da coleta e análise antecipada de dados de notícias relevantes, permitindo que você aproveite ao máximo a volatilidade do mercado causada por eventos importantes.
Conclusão
Neste artigo, criamos um arquivo include auxiliar para interpretar, formatar e armazenar dados de eventos de notícias do calendário. Em seguida, desenvolvemos um expert advisor de backtest para buscar os dados de eventos de notícias fornecidos pelos corretores, implementar a lógica da estratégia e testar os resultados da nossa estratégia de negociação utilizando esses dados. A estratégia apresentou uma lucratividade promissora, com mais de 1.600 amostras ao longo de 5 anos de dados de ticks. Por fim, compartilhamos o código do expert advisor de execução ao vivo para negociação em tempo real e delineamos aspirações futuras, incentivando o desenvolvimento contínuo de estratégias construídas sobre a estrutura apresentada neste artigo.
Tabela de Arquivos
| Nome do Arquivo | Uso dos Arquivos |
|---|---|
| CalendarHistory.mqh | O arquivo include auxiliar para lidar com dados de eventos de notícias do calendário. |
| News Breakout Backtest.mq5 | O expert advisor para armazenar dados de eventos de notícias e realizar backtests. |
| News Breakout.mq5 | O expert advisor para negociação ao vivo. |
Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/16752
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.
Análise espectral singular unidimensional
Trading por pares: negociação algorítmica com auto-otimização baseada na diferença de pontuação Z
Previsão de barras Renko com a ajuda de IA CatBoost
Redes neurais em trading: Ator–Diretor–Crítico (Conclusão)
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso
Olá, isso é ótimo, obrigado! Estou um pouco confuso com relação à inserção de várias moedas. Já tentei:
"USD"; "GBP"
"USD"; "GBP".
"USD"; "GBP";
Somente o último não produz um erro, mas não tenho certeza se funciona corretamente. Talvez ele só pegue o dólar americano. Você pode me orientar?
Olá, se você observar o código na função de inicialização, ele dividirá os dois pontos e armazenará moedas diferentes no atributo do objeto curr. A primeira deve funcionar, embora você não precise adicionar as aspas. O processo de armazenamento armazenará todos os eventos no arquivo binário, independentemente de seus atributos. Somente na lógica de negociação filtraremos os atributos. Aqui está o que eu executei agora:
Parece que essa implementação não considera as mudanças de fuso horário (DST) no servidor do corretor e, portanto, produz resultados imprecisos durante o backtesting e as otimizações.
Obrigado por me lembrar disso! Esqueci de considerar isso no artigo porque usei um corretor que não tem DST para demonstração.
https://www.mql5.com/pt/book/advanced/calendar
A partir dessa fonte, sabemos que os dados do calendário são fornecidos pelo lado da MQL5 e são automaticamente ajustados ao fuso horário atual do Timetradeserver() da corretora, o que significa que, para corretoras com horário de verão, seria necessário ajustar meu código e levá-lo em consideração.
A partir dessa fonte, sabemos que os dados do calendário são fornecidos pelo lado da MQL5 e são automaticamente ajustados ao fuso horário atual do Timetradeserver() da corretora, o que significa que, para corretoras com horário de verão, seria necessário ajustar meu código e levá-lo em consideração.
Como a implementação publicada no livro está um pouco desatualizada, a história real (atualizada) pode ser encontrada no blog e na base de código (indicador) e na base de código (script).