Many Expert Advisors should not open new trades around major economic releases. Spreads widen, prices jump and stops slip. The usual fix is a news filter, and a lot of EAs build one by downloading a calendar from a website with WebRequest and parsing it. That works until the site changes its layout.
MetaTrader 5 already has an economic calendar inside the terminal, with an MQL5 API. This tutorial uses it to block new entries for a few minutes before and after important events for the currencies of the chart symbol.
The two functions we need
CalendarValueHistory() returns the event values in a time range, optionally filtered by country code and currency. Each value is an MqlCalendarValue with an event_id and a time. It returns the number of values found, or minus one on failure.
CalendarEventById() turns an event_id into an MqlCalendarEvent, which holds the name, the importance and the time mode of the event.
One detail matters a lot: all calendar functions work in trade server time. The time field in MqlCalendarValue is server time, so compare it with TimeTradeServer(), not with your local clock.
Deciding whether an event matters
input int InpSecondsBefore = 1800; // block entries 30 minutes before an event
input int InpSecondsAfter = 900; // and 15 minutes after it
input bool InpIncludeModerate = false; // also block on moderate importance events
input bool InpBlockIfNoCalendar = true; // block when the calendar cannot be read
bool gNewsBlock = false;
bool gFilterOn = true;
bool EventMatters(const ulong event_id, string &name)
{
MqlCalendarEvent ev;
if(!CalendarEventById(event_id, ev))
return false;
// all day and tentative events have no exact time to block around
if(ev.time_mode != CALENDAR_TIMEMODE_DATETIME)
return false;
bool important = (ev.importance == CALENDAR_IMPORTANCE_HIGH) ||
(InpIncludeModerate && ev.importance == CALENDAR_IMPORTANCE_MODERATE);
if(important)
name = ev.name;
return important;
}
Checking one currency
An event at time T should block entries from T minus InpSecondsBefore until T plus InpSecondsAfter. Turned around, the EA is blocked right now if any important event falls between now minus InpSecondsAfter and now plus InpSecondsBefore. The inputs are in seconds because datetime values in MQL5 are counted in seconds, so no conversion is needed. That is the range we ask the calendar for.
// 1 = inside a news window, 0 = clear, -1 = calendar could not be read
int CheckCurrency(const string currency, const datetime now, string &why)
{
MqlCalendarValue values[];
datetime from = now - InpSecondsAfter;
datetime to = now + InpSecondsBefore;
ResetLastError();
int n = CalendarValueHistory(values, from, to, NULL, currency);
if(n < 0)
{
PrintFormat("Calendar read failed for %s, error %d", currency, GetLastError());
return -1;
}
for(int i = 0; i < n; i++)
{
string name = "";
if(EventMatters(values[i].event_id, name))
{
why = StringFormat("%s: %s at %s", currency, name,
TimeToString(values[i].time, TIME_DATE | TIME_MINUTES));
return 1;
}
}
return 0;
}
Filtering by currency rather than country is deliberate. EUR events come from the euro area and from Germany, France, Italy and others. Asking for currency "EUR" catches all of them.
Updating the block once a minute
There is no need to query the calendar on every tick. A one minute timer is plenty.
void UpdateNewsBlock()
{
datetime now = TimeTradeServer();
string base = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_BASE);
string profit = SymbolInfoString(_Symbol, SYMBOL_CURRENCY_PROFIT);
string why = "";
int a = CheckCurrency(base, now, why);
int b = (profit != base) ? CheckCurrency(profit, now, why) : 0;
bool block;
if(a < 0 || b < 0)
{
block = InpBlockIfNoCalendar;
why = "calendar unavailable";
}
else
block = (a == 1 || b == 1);
if(block != gNewsBlock)
{
if(block)
Print("News block ON, ", why);
else
Print("News block OFF");
gNewsBlock = block;
}
}
int OnInit()
{
if(MQLInfoInteger(MQL_TESTER))
{
// the calendar API is not available in the Strategy Tester
Print("News filter disabled in the tester");
gFilterOn = false;
return INIT_SUCCEEDED;
}
EventSetTimer(60);
UpdateNewsBlock();
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
EventKillTimer();
}
void OnTimer()
{
if(gFilterOn)
UpdateNewsBlock();
}
Using it in OnTick
The filter should only stop new entries. Trailing stops, breakeven moves and exits keep running during news, because that is exactly when they are needed.
void OnTick()
{
ManageOpenPositions(); // always runs
if(gFilterOn && gNewsBlock)
return; // no new entries inside a news window
CheckForEntry();
}
ManageOpenPositions() and CheckForEntry() stand for your own EA logic.
Things to know before relying on it
- The calendar functions do not work in the Strategy Tester. A backtest of this EA runs without the filter, which is why OnInit() says so in the log. If the filter matters to the strategy, export the event times to a file and read that file in the tester instead.
- A fresh terminal may need a little time to download calendar data. The InpBlockIfNoCalendar input decides whether a failed read blocks trading or lets it continue. Blocking is the safer default.
- Symbols such as XAUUSD have a base currency with no events, so only USD events will count. Index CFDs often report the same currency for base and profit. Check what SymbolInfoString() returns on your broker before trusting the filter on non forex symbols.
- Importance levels are set by the calendar provider. Before going live, print the events your filter would have blocked over the past month and check that the list matches what you expect.
That last step is cheap. Run CalendarValueHistory() over the previous 30 days in a script, pass every value through EventMatters(), and print the result. If an event you care about is missing, or the list is full of minor releases, adjust the importance setting before the EA ever sees a real release.
Top comments (0)