
Trading with the MQL5 Economic Calendar (Part 8): Optimizing News-Driven Backtesting with Smart Event Filtering and Targeted Logs
Introduction
In this article, we propel the MQL5 Economic Calendar series forward by optimizing our trading system for lightning-fast, visually intuitive backtesting, seamlessly integrating data visualization for both live and offline modes to enhance news-driven strategy development. Building on Part 7’s foundation of resource-based event analysis for Strategy Tester compatibility, we now introduce smart event filtering and targeted logging to streamline performance, ensuring we can efficiently visualize and test strategies across real-time and historical environments with minimal clutter. We structure the article with the following topics:
- A Visual Chronograph for Seamless News-Driven Trading Across Live and Offline Realms
- Implementation in MQL5
- Testing and Validation
- Conclusion
Let’s explore these advancements!
A Visual Chronograph for Seamless News-Driven Trading Across Live and Offline Realms
The ability to visualize and analyze economic news events in both live and offline environments is a game-changer for us, and in this part of the series, we introduce a visual chronograph—a metaphor for our optimized event processing and logging system—that will empower us to navigate the temporal landscape of news-driven trading with precision and efficiency.
By implementing smart event filtering, we will drastically reduce the computational load in the Strategy Tester, pre-selecting only the most relevant news events within a user-defined date range, which ensures that backtesting mirrors the speed and clarity of live trading. This filtering mechanism, akin to a chronograph’s precise timekeeping, will allow us to focus on critical events without sifting through irrelevant data, enabling seamless transitions between historical simulations and real-time market analysis.
Complementing this, our targeted logging system will act as the chronograph’s display, presenting only essential information—such as trade executions and dashboard updates—while suppressing extraneous logs, thus maintaining a clean, distraction-free interface for both live and offline modes. This dual-mode visualization capability will ensure we can test strategies with historical data in the Strategy Tester and apply the same intuitive dashboard in live trading, fostering a unified workflow that enhances decision-making and strategy refinement across all market conditions. Here is a visualization of what we aim to achieve.
Implementation in MQL5
To make the improvements in MQL5, we first will need to declare some variables that we will use to keep track of the downloaded events that we will then display seamlessly in the news dashboard using a similar format as we did in the previous articles when doing live trading, but first include the resource where we store the data as below.
//---- Include trading library #include <Trade\Trade.mqh> CTrade trade; //---- Define resource for CSV #resource "\\Files\\Database\\EconomicCalendar.csv" as string EconomicCalendarData
We start by integrating a trading library that enables seamless trade execution across live and offline modes. We use the "#include <Trade\Trade.mqh>" directive to incorporate the MQL5 trading library, which provides the "CTrade" class for managing trade operations. By declaring a "CTrade" object named "trade", we enable the program to execute buy and sell orders programmatically.
We then use the "#resource" directive to define "\Files\Database\EconomicCalendar.csv" as a string resource named "EconomicCalendarData". This Comma Separated Values (CSV), loaded via the "LoadEventsFromResource" function, will supply event details such as date, time, currency, and forecast, providing a unified data presentation without dependency on live data feeds. We can now define the rest of the control variables.
//---- Event name tracking string current_eventNames_data[]; string previous_eventNames_data[]; string last_dashboard_eventNames[]; // Added: Cache for last dashboard event names in tester mode datetime last_dashboard_update = 0; // Added: Track last dashboard update time in tester mode //---- Filter flags bool enableCurrencyFilter = true; bool enableImportanceFilter = true; bool enableTimeFilter = true; bool isDashboardUpdate = true; bool filters_changed = true; // Added: Flag to detect filter changes in tester mode //---- Event counters int totalEvents_Considered = 0; int totalEvents_Filtered = 0; int totalEvents_Displayable = 0; //---- Input parameters (PART 6) sinput group "General Calendar Settings" input ENUM_TIMEFRAMES start_time = PERIOD_H12; input ENUM_TIMEFRAMES end_time = PERIOD_H12; input ENUM_TIMEFRAMES range_time = PERIOD_H8; input bool updateServerTime = true; // Enable/Disable Server Time Update in Panel input bool debugLogging = false; // Added: Enable debug logging in tester mode //---- Input parameters for tester mode (from PART 7, minimal) sinput group "Strategy Tester CSV Settings" input datetime StartDate = D'2025.03.01'; // Download Start Date input datetime EndDate = D'2025.03.21'; // Download End Date //---- Structure for CSV events (from PART 7) struct EconomicEvent { string eventDate; // Date of the event string eventTime; // Time of the event string currency; // Currency affected string event; // Event description string importance; // Importance level double actual; // Actual value double forecast; // Forecast value double previous; // Previous value datetime eventDateTime; // Added: Store precomputed datetime for efficiency }; //---- Global array for tester mode events EconomicEvent allEvents[]; EconomicEvent filteredEvents[]; // Added: Filtered events for tester mode optimization //---- Trade settings enum ETradeMode { TRADE_BEFORE, TRADE_AFTER, NO_TRADE, PAUSE_TRADING }; input ETradeMode tradeMode = TRADE_BEFORE; input int tradeOffsetHours = 12; input int tradeOffsetMinutes = 5; input int tradeOffsetSeconds = 0; input double tradeLotSize = 0.01; //---- Trade control bool tradeExecuted = false; datetime tradedNewsTime = 0; int triggeredNewsEvents[];
Here, we store event names in "current_eventNames_data", "previous_eventNames_data", and "last_dashboard_eventNames", using "last_dashboard_eventNames" to cache tester-mode dashboard updates and "last_dashboard_update" to schedule refreshes only when needed, cutting down redundant processing.
We toggle event filtering with "enableCurrencyFilter", "enableImportanceFilter", "enableTimeFilter", and "filters_changed", resetting filters when "filters_changed" is true to process only relevant events and use "debugLogging" under "sinput group 'General Calendar Settings'" to log just trades and updates.
We define the backtesting period with "StartDate" and "EndDate" under "sinput group 'Strategy Tester CSV Settings'", structure events in "EconomicEvent" with "eventDateTime" for fast access, and filter "allEvents" into "filteredEvents" for quicker handling while setting "tradeMode" and related variables to execute trades efficiently. This now enables us to choose the testing period which we will download the data from and use the same time range for testing. This is the user interface we have.
From the image, we can see that we have extra inputs to control the display of the events in the tester mode as well as controlled updates to the time in the panel and the logging. We did that to optimize unnecessary resources when backtesting. Moving on, we need to define a function to handle the tester events filtering process.
//+------------------------------------------------------------------+ //| Filter events for tester mode | // Added: Function to pre-filter events by date range //+------------------------------------------------------------------+ void FilterEventsForTester() { ArrayResize(filteredEvents, 0); int eventIndex = 0; for (int i = 0; i < ArraySize(allEvents); i++) { datetime eventDateTime = allEvents[i].eventDateTime; if (eventDateTime < StartDate || eventDateTime > EndDate) { if (debugLogging) Print("Event ", allEvents[i].event, " skipped in filter due to date range: ", TimeToString(eventDateTime)); // Modified: Conditional logging continue; } ArrayResize(filteredEvents, eventIndex + 1); filteredEvents[eventIndex] = allEvents[i]; eventIndex++; } if (debugLogging) Print("Tester mode: Filtered ", eventIndex, " events."); // Modified: Conditional logging filters_changed = false; }
Here, we implement smart event filtering to accelerate backtesting by reducing the number of news events processed in the Strategy Tester. We use the "FilterEventsForTester" function, to clear the "filteredEvents" array with the ArrayResize function and rebuild it with relevant events from "allEvents". For each event, we check its "eventDateTime" against "StartDate" and "EndDate", skipping those outside the range and logging skips only if "debugLogging" is true using the Print function, ensuring minimal log clutter.
We copy qualifying events into "filteredEvents" at index "eventIndex", incrementing it with each addition, and use the "ArrayResize" function to allocate space dynamically. We log the total "eventIndex" count via "Print" only if "debugLogging" is enabled, keeping tester output clean, and set "filters_changed" to false to signal that filtering is complete. This focused filtering action shrinks the event set, speeding up subsequent processing and enabling efficient visualization of news events in offline mode. We then call this function in the OnInit event handler to pre-filter the news data.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //---- Create dashboard UI createRecLabel(MAIN_REC,50,50,740,410,clrSeaGreen,1); createRecLabel(SUB_REC1,50+3,50+30,740-3-3,410-30-3,clrWhite,1); createRecLabel(SUB_REC2,50+3+5,50+30+50+27,740-3-3-5-5,410-30-3-50-27-10,clrGreen,1); createLabel(HEADER_LABEL,50+3+5,50+5,"MQL5 Economic Calendar",clrWhite,15); //---- Create calendar buttons int startX = 59; for (int i = 0; i < ArraySize(array_calendar); i++) { createButton(ARRAY_CALENDAR+IntegerToString(i),startX,132,buttons[i],25, array_calendar[i],clrWhite,13,clrGreen,clrNONE,"Calibri Bold"); startX += buttons[i]+3; } //---- Initialize for live mode (unchanged) int totalNews = 0; bool isNews = false; MqlCalendarValue values[]; datetime startTime = TimeTradeServer() - PeriodSeconds(start_time); datetime endTime = TimeTradeServer() + PeriodSeconds(end_time); string country_code = "US"; string currency_base = SymbolInfoString(_Symbol,SYMBOL_CURRENCY_BASE); int allValues = CalendarValueHistory(values,startTime,endTime,NULL,NULL); //---- Load CSV events for tester mode if (MQLInfoInteger(MQL_TESTER)) { if (!LoadEventsFromResource()) { Print("Failed to load events from CSV resource."); return(INIT_FAILED); } Print("Tester mode: Loaded ", ArraySize(allEvents), " events from CSV."); FilterEventsForTester(); // Added: Pre-filter events for tester mode } //---- Create UI elements createLabel(TIME_LABEL,70,85,"Server Time: "+TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS)+ " ||| Total News: "+IntegerToString(allValues),clrBlack,14,"Times new roman bold"); createLabel(IMPACT_LABEL,70,105,"Impact: ",clrBlack,14,"Times new roman bold"); createLabel(FILTER_LABEL,370,55,"Filters:",clrYellow,16,"Impact"); //---- Create filter buttons string filter_curr_text = enableCurrencyFilter ? ShortToString(0x2714)+"Currency" : ShortToString(0x274C)+"Currency"; color filter_curr_txt_color = enableCurrencyFilter ? clrLime : clrRed; bool filter_curr_state = enableCurrencyFilter; createButton(FILTER_CURR_BTN,430,55,110,26,filter_curr_text,filter_curr_txt_color,12,clrBlack); ObjectSetInteger(0,FILTER_CURR_BTN,OBJPROP_STATE,filter_curr_state); string filter_imp_text = enableImportanceFilter ? ShortToString(0x2714)+"Importance" : ShortToString(0x274C)+"Importance"; color filter_imp_txt_color = enableImportanceFilter ? clrLime : clrRed; bool filter_imp_state = enableImportanceFilter; createButton(FILTER_IMP_BTN,430+110,55,120,26,filter_imp_text,filter_imp_txt_color,12,clrBlack); ObjectSetInteger(0,FILTER_IMP_BTN,OBJPROP_STATE,filter_imp_state); string filter_time_text = enableTimeFilter ? ShortToString(0x2714)+"Time" : ShortToString(0x274C)+"Time"; color filter_time_txt_color = enableTimeFilter ? clrLime : clrRed; bool filter_time_state = enableTimeFilter; createButton(FILTER_TIME_BTN,430+110+120,55,70,26,filter_time_text,filter_time_txt_color,12,clrBlack); ObjectSetInteger(0,FILTER_TIME_BTN,OBJPROP_STATE,filter_time_state); createButton(CANCEL_BTN,430+110+120+79,51,50,30,"X",clrWhite,17,clrRed,clrNONE); //---- Create impact buttons int impact_size = 100; for (int i = 0; i < ArraySize(impact_labels); i++) { color impact_color = clrBlack, label_color = clrBlack; if (impact_labels[i] == "None") label_color = clrWhite; else if (impact_labels[i] == "Low") impact_color = clrYellow; else if (impact_labels[i] == "Medium") impact_color = clrOrange; else if (impact_labels[i] == "High") impact_color = clrRed; createButton(IMPACT_LABEL+string(i),140+impact_size*i,105,impact_size,25, impact_labels[i],label_color,12,impact_color,clrBlack); } //---- Create currency buttons int curr_size = 51, button_height = 22, spacing_x = 0, spacing_y = 3, max_columns = 4; for (int i = 0; i < ArraySize(curr_filter); i++) { int row = i / max_columns; int col = i % max_columns; int x_pos = 575 + col * (curr_size + spacing_x); int y_pos = 83 + row * (button_height + spacing_y); createButton(CURRENCY_BTNS+IntegerToString(i),x_pos,y_pos,curr_size,button_height,curr_filter[i],clrBlack); } //---- Initialize filters if (enableCurrencyFilter) { ArrayFree(curr_filter_selected); ArrayCopy(curr_filter_selected, curr_filter); Print("CURRENCY FILTER ENABLED"); ArrayPrint(curr_filter_selected); for (int i = 0; i < ArraySize(curr_filter_selected); i++) { ObjectSetInteger(0, CURRENCY_BTNS+IntegerToString(i), OBJPROP_STATE, true); } } if (enableImportanceFilter) { ArrayFree(imp_filter_selected); ArrayCopy(imp_filter_selected, allowed_importance_levels); ArrayFree(impact_filter_selected); ArrayCopy(impact_filter_selected, impact_labels); Print("IMPORTANCE FILTER ENABLED"); ArrayPrint(imp_filter_selected); ArrayPrint(impact_filter_selected); for (int i = 0; i < ArraySize(imp_filter_selected); i++) { string btn_name = IMPACT_LABEL+string(i); ObjectSetInteger(0, btn_name, OBJPROP_STATE, true); ObjectSetInteger(0, btn_name, OBJPROP_BORDER_COLOR, clrNONE); } } //---- Update dashboard update_dashboard_values(curr_filter_selected, imp_filter_selected); ChartRedraw(0); return(INIT_SUCCEEDED); }
We use the "createRecLabel" function to build dashboard panels "MAIN_REC", "SUB_REC1", and "SUB_REC2" with distinct colors and sizes, and the "createLabel" function to add a "HEADER_LABEL" displaying "MQL5 Economic Calendar" as we did earlier on. We create calendar buttons dynamically from "array_calendar" using the "createButton" and ArraySize functions, positioning them with "startX" and "buttons" for event display.
We prepare live mode by fetching events with the CalendarValueHistory function into "values", using "startTime" and "endTime" calculated via TimeTradeServer and PeriodSeconds, and for tester mode, we use the MQLInfoInteger function to check MQL_TESTER, loading "EconomicCalendarData" with the "LoadEventsFromResource" function into "allEvents". We use the "FilterEventsForTester" function, the function that is most crucial here, to populate "filteredEvents", optimizing event processing.
We add UI elements like "TIME_LABEL", "IMPACT_LABEL", and "FILTER_LABEL" with "createLabel", and filter buttons "FILTER_CURR_BTN", "FILTER_IMP_BTN", "FILTER_TIME_BTN", and "CANCEL_BTN" with "createButton" and ObjectSetInteger, setting states like "filter_curr_state" based on "enableCurrencyFilter". We create impact and currency buttons from "impact_labels" and "curr_filter" using "createButton", initialize filters "curr_filter_selected" and "imp_filter_selected" with ArrayFree and ArrayCopy, and update the dashboard with "update_dashboard_values" and ChartRedraw, returning "INIT_SUCCEEDED" to confirm the setup. When we now initialize the program, we have the following outcome.
Since we can now load the relevant data after filtering, on the OnTick event handler, we need to make sure we get relevant data within a specified time and populate it to the dashboard instead of just all the data, just as we do in the live mode. Here is the logic we employ, and before we forget, we have added relevant comments to the specific and vital update sections where we have made the modifications.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { UpdateFilterInfo(); CheckForNewsTrade(); if (isDashboardUpdate) { if (MQLInfoInteger(MQL_TESTER)) { datetime currentTime = TimeTradeServer(); datetime timeRange = PeriodSeconds(range_time); datetime timeAfter = currentTime + timeRange; if (filters_changed || last_dashboard_update < timeAfter) { // Modified: Update on filter change or time range shift update_dashboard_values(curr_filter_selected, imp_filter_selected); ArrayFree(last_dashboard_eventNames); ArrayCopy(last_dashboard_eventNames, current_eventNames_data); last_dashboard_update = currentTime; } } else { update_dashboard_values(curr_filter_selected, imp_filter_selected); } } }
In the OnTick event handler, we use the "UpdateFilterInfo" function to refresh filter settings and the "CheckForNewsTrade" function to evaluate and execute trades based on news events. When "isDashboardUpdate" is true, we check MQL_TESTER with the MQLInfoInteger function to apply tester-specific logic, calculating "currentTime" with TimeTradeServer, "timeRange" with PeriodSeconds on "range_time", and "timeAfter" as "currentTime" plus "timeRange".
In tester mode, we use the condition "filters_changed" or "last_dashboard_update" less than "timeAfter", to trigger the "update_dashboard_values" function with "curr_filter_selected" and "imp_filter_selected", clearing "last_dashboard_eventNames" with ArrayFree function, copying "current_eventNames_data" to it with ArrayCopy, and updating "last_dashboard_update" to "currentTime", minimizing refreshes. In live mode, we directly call "update_dashboard_values" for continuous updates, ensuring optimized, targeted dashboard visualization in both modes. We can now modify functions that we use as follows making sure they incorporate the relevant modifications, specifically the time division.
//+------------------------------------------------------------------+ //| Load events from CSV resource | //+------------------------------------------------------------------+ bool LoadEventsFromResource() { string fileData = EconomicCalendarData; Print("Raw resource content (size: ", StringLen(fileData), " bytes):\n", fileData); string lines[]; int lineCount = StringSplit(fileData, '\n', lines); if (lineCount <= 1) { Print("Error: No data lines found in resource! Raw data: ", fileData); return false; } ArrayResize(allEvents, 0); int eventIndex = 0; for (int i = 1; i < lineCount; i++) { if (StringLen(lines[i]) == 0) { if (debugLogging) Print("Skipping empty line ", i); // Modified: Conditional logging continue; } string fields[]; int fieldCount = StringSplit(lines[i], ',', fields); if (debugLogging) Print("Line ", i, ": ", lines[i], " (field count: ", fieldCount, ")"); // Modified: Conditional logging if (fieldCount < 8) { Print("Malformed line ", i, ": ", lines[i], " (field count: ", fieldCount, ")"); continue; } string dateStr = fields[0]; string timeStr = fields[1]; string currency = fields[2]; string event = fields[3]; for (int j = 4; j < fieldCount - 4; j++) { event += "," + fields[j]; } string importance = fields[fieldCount - 4]; string actualStr = fields[fieldCount - 3]; string forecastStr = fields[fieldCount - 2]; string previousStr = fields[fieldCount - 1]; datetime eventDateTime = StringToTime(dateStr + " " + timeStr); if (eventDateTime == 0) { Print("Error: Invalid datetime conversion for line ", i, ": ", dateStr, " ", timeStr); continue; } ArrayResize(allEvents, eventIndex + 1); allEvents[eventIndex].eventDate = dateStr; allEvents[eventIndex].eventTime = timeStr; allEvents[eventIndex].currency = currency; allEvents[eventIndex].event = event; allEvents[eventIndex].importance = importance; allEvents[eventIndex].actual = StringToDouble(actualStr); allEvents[eventIndex].forecast = StringToDouble(forecastStr); allEvents[eventIndex].previous = StringToDouble(previousStr); allEvents[eventIndex].eventDateTime = eventDateTime; // Added: Store precomputed datetime if (debugLogging) Print("Loaded event ", eventIndex, ": ", dateStr, " ", timeStr, ", ", currency, ", ", event); // Modified: Conditional logging eventIndex++; } Print("Loaded ", eventIndex, " events from resource into array."); return eventIndex > 0; }
Here, we load historical news events from a CSV resource to enable offline backtesting with optimized event handling and targeted logging. We use the "LoadEventsFromResource" function to read "EconomicCalendarData" into "fileData", logging its size with the Print and StringLen functions. We split "fileData" into "lines" using the StringSplit function, checking "lineCount" to ensure data exists, and clear "allEvents" with the ArrayResize function.
We iterate through "lines", skipping empty ones with the "StringLen" function and logging skips only if "debugLogging" is true. We use "StringSplit" to parse each line into "fields", verify "fieldCount", and extract "dateStr", "timeStr", "currency", "event", "importance", "actualStr", "forecastStr", and "previousStr", combining event fields dynamically.
We convert "dateStr" and "timeStr" to "eventDateTime" with the StringToTime function, storing it in "allEvents[eventIndex].eventDateTime" for efficiency, populate "allEvents" using "ArrayResize" and StringToDouble, log successful loads conditionally, and return true if "eventIndex" is positive, ensuring a robust event dataset for backtesting. We now still update the function responsible for updating the dashboard values which is critical for visualizing the stored events data as below.
//+------------------------------------------------------------------+ //| Update dashboard values | //+------------------------------------------------------------------+ void update_dashboard_values(string &curr_filter_array[], ENUM_CALENDAR_EVENT_IMPORTANCE &imp_filter_array[]) { totalEvents_Considered = 0; totalEvents_Filtered = 0; totalEvents_Displayable = 0; ArrayFree(current_eventNames_data); datetime timeRange = PeriodSeconds(range_time); datetime timeBefore = TimeTradeServer() - timeRange; datetime timeAfter = TimeTradeServer() + timeRange; int startY = 162; if (MQLInfoInteger(MQL_TESTER)) { if (filters_changed) FilterEventsForTester(); // Added: Re-filter events if filters changed //---- Tester mode: Process filtered events for (int i = 0; i < ArraySize(filteredEvents); i++) { totalEvents_Considered++; datetime eventDateTime = filteredEvents[i].eventDateTime; if (eventDateTime < StartDate || eventDateTime > EndDate) { if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to date range."); // Modified: Conditional logging continue; } bool timeMatch = !enableTimeFilter; if (enableTimeFilter) { if (eventDateTime <= TimeTradeServer() && eventDateTime >= timeBefore) timeMatch = true; else if (eventDateTime >= TimeTradeServer() && eventDateTime <= timeAfter) timeMatch = true; } if (!timeMatch) { if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to time filter."); // Modified: Conditional logging continue; } bool currencyMatch = !enableCurrencyFilter; if (enableCurrencyFilter) { for (int j = 0; j < ArraySize(curr_filter_array); j++) { if (filteredEvents[i].currency == curr_filter_array[j]) { currencyMatch = true; break; } } } if (!currencyMatch) { if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to currency filter."); // Modified: Conditional logging continue; } bool importanceMatch = !enableImportanceFilter; if (enableImportanceFilter) { string imp_str = filteredEvents[i].importance; ENUM_CALENDAR_EVENT_IMPORTANCE event_imp = (imp_str == "None") ? CALENDAR_IMPORTANCE_NONE : (imp_str == "Low") ? CALENDAR_IMPORTANCE_LOW : (imp_str == "Medium") ? CALENDAR_IMPORTANCE_MODERATE : CALENDAR_IMPORTANCE_HIGH; for (int k = 0; k < ArraySize(imp_filter_array); k++) { if (event_imp == imp_filter_array[k]) { importanceMatch = true; break; } } } if (!importanceMatch) { if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to importance filter."); // Modified: Conditional logging continue; } totalEvents_Filtered++; if (totalEvents_Displayable >= 11) continue; totalEvents_Displayable++; color holder_color = (totalEvents_Displayable % 2 == 0) ? C'213,227,207' : clrWhite; createRecLabel(DATA_HOLDERS+string(totalEvents_Displayable),62,startY-1,716,26+1,holder_color,1,clrNONE); int startX = 65; string news_data[ArraySize(array_calendar)]; news_data[0] = filteredEvents[i].eventDate; news_data[1] = filteredEvents[i].eventTime; news_data[2] = filteredEvents[i].currency; color importance_color = clrBlack; if (filteredEvents[i].importance == "Low") importance_color = clrYellow; else if (filteredEvents[i].importance == "Medium") importance_color = clrOrange; else if (filteredEvents[i].importance == "High") importance_color = clrRed; news_data[3] = ShortToString(0x25CF); news_data[4] = filteredEvents[i].event; news_data[5] = DoubleToString(filteredEvents[i].actual, 3); news_data[6] = DoubleToString(filteredEvents[i].forecast, 3); news_data[7] = DoubleToString(filteredEvents[i].previous, 3); for (int k = 0; k < ArraySize(array_calendar); k++) { if (k == 3) { createLabel(ARRAY_NEWS+IntegerToString(i)+" "+array_calendar[k],startX,startY-(22-12),news_data[k],importance_color,22,"Calibri"); } else { createLabel(ARRAY_NEWS+IntegerToString(i)+" "+array_calendar[k],startX,startY,news_data[k],clrBlack,12,"Calibri"); } startX += buttons[k]+3; } ArrayResize(current_eventNames_data, ArraySize(current_eventNames_data)+1); current_eventNames_data[ArraySize(current_eventNames_data)-1] = filteredEvents[i].event; startY += 25; } } else { //---- Live mode: Unchanged } }
To display filtered news events efficiently, we use the "update_dashboard_values" function to reset "totalEvents_Considered", "totalEvents_Filtered", "totalEvents_Displayable", and clear "current_eventNames_data" with the ArrayFree function, setting "timeRange" via the PeriodSeconds function on "range_time" and calculating "timeBefore" and "timeAfter" with TimeTradeServer. We check "MQL_TESTER" with the MQLInfoInteger function and, if "filters_changed" is true, use the "FilterEventsForTester" function that we had earlier defined fully to refresh "filteredEvents".
We iterate through "filteredEvents" using the ArraySize function, incrementing "totalEvents_Considered" and skipping events outside "StartDate" or "EndDate" or failing "enableTimeFilter", "enableCurrencyFilter", or "enableImportanceFilter" checks, logging skips only if "debugLogging" is true.
For up to 11 matching events, we increment "totalEvents_Displayable", use the "createRecLabel" function to draw "DATA_HOLDERS" rows and use the "createLabel" function to populate "news_data" from "filteredEvents" fields like "eventDate" and "event", styled with "importance_color" and "array_calendar", resizing "current_eventNames_data" with ArrayResize to store event names, ensuring a fast, clear dashboard visualization. To trade in tester mode, we modify the function responsible for checking for trades and opening trades as below.
//+------------------------------------------------------------------+ //| Check for news trade (adapted for tester mode trading) | //+------------------------------------------------------------------+ void CheckForNewsTrade() { if (!MQLInfoInteger(MQL_TESTER) || debugLogging) Print("CheckForNewsTrade called at: ", TimeToString(TimeTradeServer(), TIME_SECONDS)); // Modified: Conditional logging if (tradeMode == NO_TRADE || tradeMode == PAUSE_TRADING) { if (ObjectFind(0, "NewsCountdown") >= 0) { ObjectDelete(0, "NewsCountdown"); Print("Trading disabled. Countdown removed."); } return; } datetime currentTime = TimeTradeServer(); int offsetSeconds = tradeOffsetHours * 3600 + tradeOffsetMinutes * 60 + tradeOffsetSeconds; if (tradeExecuted) { if (currentTime < tradedNewsTime) { int remainingSeconds = (int)(tradedNewsTime - currentTime); int hrs = remainingSeconds / 3600; int mins = (remainingSeconds % 3600) / 60; int secs = remainingSeconds % 60; string countdownText = "News in: " + IntegerToString(hrs) + "h " + IntegerToString(mins) + "m " + IntegerToString(secs) + "s"; if (ObjectFind(0, "NewsCountdown") < 0) { createButton1("NewsCountdown", 50, 17, 300, 30, countdownText, clrWhite, 12, clrBlue, clrBlack); Print("Post-trade countdown created: ", countdownText); } else { updateLabel1("NewsCountdown", countdownText); Print("Post-trade countdown updated: ", countdownText); } } else { int elapsed = (int)(currentTime - tradedNewsTime); if (elapsed < 15) { int remainingDelay = 15 - elapsed; string countdownText = "News Released, resetting in: " + IntegerToString(remainingDelay) + "s"; if (ObjectFind(0, "NewsCountdown") < 0) { createButton1("NewsCountdown", 50, 17, 300, 30, countdownText, clrWhite, 12, clrRed, clrBlack); ObjectSetInteger(0,"NewsCountdown",OBJPROP_BGCOLOR,clrRed); Print("Post-trade reset countdown created: ", countdownText); } else { updateLabel1("NewsCountdown", countdownText); ObjectSetInteger(0,"NewsCountdown",OBJPROP_BGCOLOR,clrRed); Print("Post-trade reset countdown updated: ", countdownText); } } else { Print("News Released. Resetting trade status after 15 seconds."); if (ObjectFind(0, "NewsCountdown") >= 0) ObjectDelete(0, "NewsCountdown"); tradeExecuted = false; } } return; } datetime lowerBound = currentTime - PeriodSeconds(start_time); datetime upperBound = currentTime + PeriodSeconds(end_time); if (debugLogging) Print("Event time range: ", TimeToString(lowerBound, TIME_SECONDS), " to ", TimeToString(upperBound, TIME_SECONDS)); // Modified: Conditional logging datetime candidateEventTime = 0; string candidateEventName = ""; string candidateTradeSide = ""; int candidateEventID = -1; if (MQLInfoInteger(MQL_TESTER)) { //---- Tester mode: Process filtered events int totalValues = ArraySize(filteredEvents); if (debugLogging) Print("Total events found: ", totalValues); // Modified: Conditional logging if (totalValues <= 0) { if (ObjectFind(0, "NewsCountdown") >= 0) ObjectDelete(0, "NewsCountdown"); return; } for (int i = 0; i < totalValues; i++) { datetime eventTime = filteredEvents[i].eventDateTime; if (eventTime < lowerBound || eventTime > upperBound || eventTime < StartDate || eventTime > EndDate) { if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to date range."); // Modified: Conditional logging continue; } bool currencyMatch = !enableCurrencyFilter; if (enableCurrencyFilter) { for (int k = 0; k < ArraySize(curr_filter_selected); k++) { if (filteredEvents[i].currency == curr_filter_selected[k]) { currencyMatch = true; break; } } if (!currencyMatch) { if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to currency filter."); // Modified: Conditional logging continue; } } bool impactMatch = !enableImportanceFilter; if (enableImportanceFilter) { string imp_str = filteredEvents[i].importance; ENUM_CALENDAR_EVENT_IMPORTANCE event_imp = (imp_str == "None") ? CALENDAR_IMPORTANCE_NONE : (imp_str == "Low") ? CALENDAR_IMPORTANCE_LOW : (imp_str == "Medium") ? CALENDAR_IMPORTANCE_MODERATE : CALENDAR_IMPORTANCE_HIGH; for (int k = 0; k < ArraySize(imp_filter_selected); k++) { if (event_imp == imp_filter_selected[k]) { impactMatch = true; break; } } if (!impactMatch) { if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to impact filter."); // Modified: Conditional logging continue; } } bool alreadyTriggered = false; for (int j = 0; j < ArraySize(triggeredNewsEvents); j++) { if (triggeredNewsEvents[j] == i) { alreadyTriggered = true; break; } } if (alreadyTriggered) { if (debugLogging) Print("Event ", filteredEvents[i].event, " already triggered a trade. Skipping."); // Modified: Conditional logging continue; } if (tradeMode == TRADE_BEFORE) { if (currentTime >= (eventTime - offsetSeconds) && currentTime < eventTime) { double forecast = filteredEvents[i].forecast; double previous = filteredEvents[i].previous; if (forecast == 0.0 || previous == 0.0) { if (debugLogging) Print("Skipping event ", filteredEvents[i].event, " because forecast or previous value is empty."); // Modified: Conditional logging continue; } if (forecast == previous) { if (debugLogging) Print("Skipping event ", filteredEvents[i].event, " because forecast equals previous."); // Modified: Conditional logging continue; } if (candidateEventTime == 0 || eventTime < candidateEventTime) { candidateEventTime = eventTime; candidateEventName = filteredEvents[i].event; candidateEventID = i; candidateTradeSide = (forecast > previous) ? "BUY" : "SELL"; if (debugLogging) Print("Candidate event: ", filteredEvents[i].event, " with event time: ", TimeToString(eventTime, TIME_SECONDS), " Side: ", candidateTradeSide); // Modified: Conditional logging } } } } } else { //---- Live mode: Unchanged } }
To evaluate and trigger news-driven trades in tester mode with optimized event filtering and targeted logging for efficient backtesting, we use the "CheckForNewsTrade" function to start, logging its execution only when "debugLogging" is true with the Print function, TimeToString, and "TimeTradeServer" for the current timestamp, keeping tester logs clean. We exit if "tradeMode" is "NO_TRADE" or "PAUSE_TRADING", using the ObjectFind function to check for "NewsCountdown" and removing it with ObjectDelete while logging via "Print", and manage post-trade states by computing "currentTime" with TimeTradeServer and "offsetSeconds" from "tradeOffsetHours", "tradeOffsetMinutes", and "tradeOffsetSeconds".
If "tradeExecuted" is true, we handle countdown timers for "tradedNewsTime", formatting "countdownText" with IntegerToString to show remaining time or reset delay, creating or updating "NewsCountdown" with "createButton1" or "updateLabel1" based on "ObjectFind", styling with ObjectSetInteger, and logging via "Print", resetting "tradeExecuted" after 15 seconds with "ObjectDelete" and "Print".
In tester mode, confirmed by MQLInfoInteger checking MQL_TESTER, we process "filteredEvents" using ArraySize to get "totalValues", logging it conditionally with "Print", and exit if empty after clearing "NewsCountdown". We set "lowerBound" and "upperBound" with "TimeTradeServer" and PeriodSeconds on "start_time" and "end_time", logging the range with "Print" if "debugLogging" is true, and initialize "candidateEventTime", "candidateEventName", "candidateEventID", and "candidateTradeSide" for trade selection.
We iterate "filteredEvents", skipping events outside "lowerBound", "upperBound", "StartDate", or "EndDate", or failing "enableCurrencyFilter" against "curr_filter_selected" or "enableImportanceFilter" against "imp_filter_selected" using "ArraySize", with skips logged via Print only if "debugLogging" is enabled. We use ArraySize on "triggeredNewsEvents" to exclude traded events, logging conditionally.
For "TRADE_BEFORE" mode, we target events within "offsetSeconds" before "eventDateTime", validating "forecast" and "previous", and select the earliest event into "candidateEventTime", "candidateEventName", "candidateEventID", and "candidateTradeSide" ("BUY" if "forecast" exceeds "previous", else "SELL"), logging with "Print" if "debugLogging" is true, ensuring efficient trade decisions with minimal logging. The rest of the live mode logic remains unchanged. Upon compilation, we have the following trade confirmation visualization.
From the image, we can see that we can get the data, filter it populate it in the dashboard, initialize countdowns when the respective time range of a certain data is reached, and trade the news event, simulating exactly what we have in live mode trading environment, hence achieving our integration objective. What now remains is backtesting the system thoroughly and that is handled in the next section.
Testing and Validation
We test the program by first loading it in a live environment, downloading the desired news events data, and running it in the MetaTrader 5 Strategy Tester with "StartDate" set to '2025.03.01', "EndDate" to '2025.03.21', and "debugLogging" disabled, using a Comma Separated Values (CSV) file in "EconomicCalendarData" to simulate trades via "CheckForNewsTrade" on "filteredEvents". A GIF showcases the dashboard, updated by "update_dashboard_values" only when "filters_changed" or "last_dashboard_update" triggers, displaying filtered events with "createLabel" and clean logs of trades and updates. Live mode tests with the CalendarValueHistory function confirm identical visualization, validating the program’s fast, clear performance across both modes. Here is the visualization.
Conclusion
In conclusion, we’ve elevated the MQL5 Economic Calendar series by optimizing backtesting with smart event filtering and streamlined logging, enabling rapid and clear strategy validation while preserving seamless live trading capabilities. This advancement bridges efficient offline testing with real-time event analysis, offering us a robust tool for refining news-driven strategies, as showcased in our testing visualization. You can use it as a backbone and enhance it further to meet your specific trading needs.





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use