They referred to project documents that are not part of this repository. Comments only; PriceToPips is now documented as not rounding, which is what it does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
334 lines
14 KiB
MQL5
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
|
|
//+------------------------------------------------------------------+
|