//+------------------------------------------------------------------+ //| News.mqh| //| Copyright 2026, bandersoft | //| https://www.mql5.com/en/users/fats | //+------------------------------------------------------------------+ //| | //| Economic calendar filter. | //| | //| Read-only: sends no trade requests. | //| | //| Uses the terminal's built-in calendar - CalendarValueHistory, | //| CalendarEventById, CalendarCountryById. No WebRequest, no DLL, | //| nothing that would block a Market submission. | //| | //| NOT AVAILABLE IN THE STRATEGY TESTER. The calendar functions | //| return nothing there - the tester has no calendar database. | //| Every query below detects that and returns a DOCUMENTED NEUTRAL | //| result: "no news found", with IsCalendarAvailable() false so a | //| caller can tell "nothing scheduled" from "cannot see". | //| | //| That choice matters. A news filter that fails closed would stop | //| every backtest dead; one that fails open silently makes | //| backtests disagree with live for reasons nobody can find. The | //| neutral result plus an explicit flag is the only honest option. | //| | //+------------------------------------------------------------------+ #ifndef STOPFIRST_NEWS_MQH #define STOPFIRST_NEWS_MQH #include "Defs.mqh" //+------------------------------------------------------------------+ //| One scheduled event | //+------------------------------------------------------------------+ struct SNewsEvent { ulong eventId; datetime time; // server time string name; string currency; ENUM_CALENDAR_EVENT_IMPORTANCE importance; int minutesAway; // negative once past }; //+------------------------------------------------------------------+ //| Can the calendar be read at all? | //| | //| False in the Strategy Tester, and on a terminal whose calendar | //| has never synchronised. Check this before believing an empty | //| result. | //| | //| Returns: true if the calendar answered a probe query. | //+------------------------------------------------------------------+ bool IsCalendarAvailable(void) { //--- the tester has no calendar database at all if(MQLInfoInteger(MQL_TESTER)) return false; MqlCalendarValue values[]; datetime from = TimeTradeServer() - 7 * 24 * 3600; datetime to = TimeTradeServer() + 7 * 24 * 3600; //--- a fortnight around now contains events for any live calendar; //--- an empty answer here means the database is absent, not quiet int count = CalendarValueHistory(values, from, to, NULL, NULL); return count > 0; } //+------------------------------------------------------------------+ //| The two currencies a symbol depends on | //| | //| Reads SYMBOL_CURRENCY_BASE and SYMBOL_CURRENCY_PROFIT, so it is | //| right for XAUUSD (XAU/USD) and for an index priced in a currency | //| as well as for a plain pair. | //| | //| Returns: true if at least one currency was found. | //+------------------------------------------------------------------+ bool SymbolCurrencies(const string symbol, string &base, string &profit) { string resolved = (symbol == "") ? _Symbol : symbol; base = SymbolInfoString(resolved, SYMBOL_CURRENCY_BASE); profit = SymbolInfoString(resolved, SYMBOL_CURRENCY_PROFIT); return (base != "" || profit != ""); } //+------------------------------------------------------------------+ //| Events for one currency within a time window | //| | //| Parameters: | //| currency - ISO code, e.g. "USD" | //| from, to - server time bounds | //| minImportance - events below this are dropped | //| events - receives the matches, soonest first | //| | //| Returns: how many were written. 0 in the tester - check | //| IsCalendarAvailable() before reading that as "quiet". | //+------------------------------------------------------------------+ int CalendarEventsFor(const string currency, const datetime from, const datetime to, const ENUM_CALENDAR_EVENT_IMPORTANCE minImportance, SNewsEvent &events[]) { ArrayResize(events, 0); if(currency == "") return 0; if(MQLInfoInteger(MQL_TESTER)) return 0; MqlCalendarValue values[]; int total = CalendarValueHistory(values, from, to, NULL, currency); if(total <= 0) return 0; int written = 0; datetime now = TimeTradeServer(); for(int i = 0; i < total; i++) { MqlCalendarEvent event; if(!CalendarEventById(values[i].event_id, event)) continue; if(event.importance < minImportance) continue; ArrayResize(events, written + 1); events[written].eventId = values[i].event_id; events[written].time = values[i].time; events[written].name = event.name; events[written].currency = currency; events[written].importance = event.importance; events[written].minutesAway = (int)((values[i].time - now) / 60); written++; } //--- soonest first; CalendarValueHistory does not promise an order for(int i = 0; i < written - 1; i++) for(int j = i + 1; j < written; j++) if(events[j].time < events[i].time) { SNewsEvent swap = events[i]; events[i] = events[j]; events[j] = swap; } return written; } //+------------------------------------------------------------------+ //| Is an event due near a given time, for a symbol's currencies? | //| | //| This is the question a trading filter actually asks. | //| | //| Parameters: | //| symbol - "" for the current chart symbol | //| minImportance - usually CALENDAR_IMPORTANCE_HIGH | //| minutesBefore, minutesAfter - the window around `when` | //| when - 0 for now | //| | //| Returns: true if a qualifying event falls inside the window. | //| ALWAYS false in the Strategy Tester, where the calendar | //| cannot be read - so a backtest behaves as though no news | //| ever happens. Gate on IsCalendarAvailable() if that | //| difference matters to the strategy. | //+------------------------------------------------------------------+ bool IsNewsNear(const string symbol = "", const ENUM_CALENDAR_EVENT_IMPORTANCE minImportance = CALENDAR_IMPORTANCE_HIGH, const int minutesBefore = 30, const int minutesAfter = 30, const datetime when = 0) { if(MQLInfoInteger(MQL_TESTER)) return false; string base, profit; if(!SymbolCurrencies(symbol, base, profit)) return false; datetime centre = (when == 0) ? TimeTradeServer() : when; datetime from = centre - minutesBefore * 60; datetime to = centre + minutesAfter * 60; SNewsEvent events[]; if(base != "" && CalendarEventsFor(base, from, to, minImportance, events) > 0) return true; //--- a pair depends on both sides; USD news moves EURUSD as much //--- as EUR news does if(profit != "" && profit != base && CalendarEventsFor(profit, from, to, minImportance, events) > 0) return true; return false; } //+------------------------------------------------------------------+ //| Upcoming events for a symbol's currencies | //| | //| Parameters: | //| hoursAhead - how far forward to look | //| events - receives the matches, soonest first | //| | //| Returns: how many were written. 0 in the tester. | //+------------------------------------------------------------------+ int UpcomingNews(SNewsEvent &events[], const string symbol = "", const ENUM_CALENDAR_EVENT_IMPORTANCE minImportance = CALENDAR_IMPORTANCE_HIGH, const int hoursAhead = 24) { ArrayResize(events, 0); if(MQLInfoInteger(MQL_TESTER)) return 0; string base, profit; if(!SymbolCurrencies(symbol, base, profit)) return 0; datetime now = TimeTradeServer(); datetime to = now + hoursAhead * 3600; SNewsEvent baseEvents[], profitEvents[]; int baseCount = 0, profitCount = 0; if(base != "") baseCount = CalendarEventsFor(base, now, to, minImportance, baseEvents); if(profit != "" && profit != base) profitCount = CalendarEventsFor(profit, now, to, minImportance, profitEvents); ArrayResize(events, baseCount + profitCount); for(int i = 0; i < baseCount; i++) events[i] = baseEvents[i]; for(int i = 0; i < profitCount; i++) events[baseCount + i] = profitEvents[i]; int total = baseCount + profitCount; for(int i = 0; i < total - 1; i++) for(int j = i + 1; j < total; j++) if(events[j].time < events[i].time) { SNewsEvent swap = events[i]; events[i] = events[j]; events[j] = swap; } return total; } //+------------------------------------------------------------------+ //| Minutes until the next qualifying event | //| Returns: minutes, or -1 if nothing is scheduled in the window or | //| the calendar cannot be read. | //+------------------------------------------------------------------+ int MinutesToNextNews(const string symbol = "", const ENUM_CALENDAR_EVENT_IMPORTANCE minImportance = CALENDAR_IMPORTANCE_HIGH, const int hoursAhead = 24) { SNewsEvent events[]; int count = UpcomingNews(events, symbol, minImportance, hoursAhead); if(count <= 0) return -1; return events[0].minutesAway; } //+------------------------------------------------------------------+ //| Importance as text | //| Returns: "none", "low", "moderate" or "high". | //+------------------------------------------------------------------+ string ImportanceToString(const ENUM_CALENDAR_EVENT_IMPORTANCE importance) { switch(importance) { case CALENDAR_IMPORTANCE_NONE: return "none"; case CALENDAR_IMPORTANCE_LOW: return "low"; case CALENDAR_IMPORTANCE_MODERATE: return "moderate"; case CALENDAR_IMPORTANCE_HIGH: return "high"; } return "unknown"; } //+------------------------------------------------------------------+ //| Multi-line upcoming-news summary | //| | //| States explicitly when the calendar is unreadable, so an empty | //| report is never mistaken for a quiet day. | //| | //| Returns: the report. Never empty. | //+------------------------------------------------------------------+ string NewsReport(const string symbol = "", const ENUM_CALENDAR_EVENT_IMPORTANCE minImportance = CALENDAR_IMPORTANCE_HIGH, const int hoursAhead = 24) { string resolved = (symbol == "") ? _Symbol : symbol; string report = "=== News for " + resolved + " ===\n"; if(MQLInfoInteger(MQL_TESTER)) { report += "Calendar UNAVAILABLE: the Strategy Tester has no calendar\n"; report += "database. Every query returns 'no news', which is a neutral\n"; report += "default and not an observation.\n"; return report; } if(!IsCalendarAvailable()) { report += "Calendar UNAVAILABLE: the terminal returned no events at all.\n"; report += "It may never have synchronised. Treat 'no news' as unknown.\n"; return report; } string base, profit; SymbolCurrencies(resolved, base, profit); report += "Currencies: " + base + (profit != base ? " / " + profit : "") + "\n"; SNewsEvent events[]; int count = UpcomingNews(events, resolved, minImportance, hoursAhead); report += "Importance: " + ImportanceToString(minImportance) + " or above\n"; report += "Window: next " + IntegerToString(hoursAhead) + " hours\n"; if(count == 0) { report += "Nothing scheduled.\n"; return report; } for(int i = 0; i < count; i++) report += StringFormat(" %s %-4s %-6s in %4d min %s\n", TimeToString(events[i].time, TIME_DATE | TIME_MINUTES), events[i].currency, ImportanceToString(events[i].importance), events[i].minutesAway, events[i].name); return report; } #endif // STOPFIRST_NEWS_MQH //+------------------------------------------------------------------+