StopFirst-Core/Include/StopFirst/News.mqh

334 lines
14 KiB
MQL5

//+------------------------------------------------------------------+
//| 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
//+------------------------------------------------------------------+