Build an MQL5 News Filter EA With the Economic Calendar
By HorizonAI Team · 14 min read · Advanced
How to Build an MQL5 News Filter EA With the Economic Calendar
An EA can have a sound entry model and still take its worst fill of the week seconds before CPI, an interest-rate decision, or employment data. The fix is not a second trading signal. It is an entry gate that asks whether a scheduled high-impact event affects either currency in the chart symbol, then refuses new orders for a defined window.
Short answer: use MT5's built-in economic calendar to query scheduled values around TimeTradeServer(), look up each event's importance with CalendarEventById(), and return true when a matching currency event falls inside your before/after window. Call that filter immediately before Buy() or Sell(), while keeping stop management outside the filter so existing positions remain managed.
The reusable EA below does exactly that for standard six-letter FX symbols such as EURUSD. It blocks fresh EMA-cross entries around high-impact EUR or USD releases, supports an event-name exclusion list, logs calendar failures, and can fail open or fail closed when the terminal cannot return calendar data.
Set the right expectation: a news filter is not a signal
A calendar filter answers one narrow question: may this EA create a new position right now? It doesn't predict whether a release is bullish or bearish, interpret the actual result, widen a stop, close a trade, or execute trades on your behalf. The strategy still supplies the signal.
That separation matters in code. Put ManageOpenPosition() at the top of OnTick(), then run the calendar gate only in the new-entry branch. A position opened 20 minutes before an event can still move its stop to break-even during the blocked window. What changes is that the EA won't add a fresh order while liquidity and spreads can behave differently.
MT5 exposes calendar values through CalendarValueHistory(). Each returned MqlCalendarValue identifies an event, while CalendarEventById() supplies its name, importance, and other event metadata. The date and time functions use trade-server time, so the window must use the same clock. See the official references for CalendarValueHistory, CalendarEventById, and TimeTradeServer.
Build target: block new EURUSD entries from 30 minutes before until 20 minutes after high-impact EUR or USD events. Keep managing any open EURUSD position.
Prerequisites and design choices
You need MetaTrader 5, an EA chart, and a broker symbol whose first six characters are the base and quote currencies. EURUSD, GBPJPY, and USDCHF fit. A broker suffix such as EURUSD.a is fine because the module reads the first six characters. Metals, indices, crypto, and nonstandard symbols need a mapping input instead of this simple derivation.
Use these defaults first:
| Input | Default | Why it is practical |
|---|---|---|
| Minimum impact | High | Avoids blocking every medium and low release. |
| Minutes before | 30 | Stops an entry as traders position ahead of a major release. |
| Minutes after | 20 | Gives the initial release reaction time to settle. |
| Refresh seconds | 15 | Reduces repeated calendar queries without leaving a long blind spot. |
| Failure mode | Fail open | Lets a tested signal continue if calendar retrieval temporarily fails. |
A fail-open setting is usually less disruptive for a strategy that has already been thoroughly tested without a filter. A fail-closed setting is stricter: a calendar error means no new entry. Neither choice is universally correct, so make it an input and log every failure.
If you are still assembling the trade side, start with the MT5 automation guide for the lifecycle of an Expert Advisor. If orders never arrive at all, use this separate MT5 EA debugging checklist before blaming the calendar gate.
Build the reusable calendar gate
The core function has four jobs:
- Read the trade-server clock with
TimeTradeServer(). - Derive the two currencies from the chart symbol.
- Request calendar values from
now - minutesBeforethroughnow + minutesAfterfor each currency. - Resolve the event, reject excluded names and lower importance, then report a block when the scheduled value time sits inside the window.
CalendarValueHistory() returns -1 on an error and 0 when the query succeeds but finds no values. Those are different states. An empty result means no scheduled matching events in the selected range. An error means the EA cannot make a reliable calendar decision, and your InpFailClosedOnCalendarError input decides the entry behavior.
The calendar records numeric outcomes in scaled long fields. When a value such as actual_value is unavailable, it can be LONG_MIN; do not treat that as zero. This filter deliberately ignores actual and forecast values because it is a scheduling filter, not a post-release direction model. MetaQuotes' economic calendar documentation describes the calendar structures and their value fields.
Calendar filter module
Paste this complete EA into MetaEditor as EconomicCalendarFilterEA.mq5. The trade signal is intentionally small, a confirmed 20/50 EMA crossover, so the calendar logic stays visible. Replace TryOpenEmaCrossTrade() later with your own entry function, but keep the CanOpenNewTrade() call directly before order submission.
#property strict
#property version "1.00"
#include <Trade/Trade.mqh>
input double InpLots = 0.10;
input ulong InpMagic = 26001;
input int InpStopLossPoints = 300;
input int InpTakeProfitPoints = 600;
input int InpBreakEvenTriggerPoints = 200;
input ENUM_CALENDAR_EVENT_IMPORTANCE InpMinImportance = CALENDAR_IMPORTANCE_HIGH;
input int InpMinutesBefore = 30;
input int InpMinutesAfter = 20;
input int InpRefreshSeconds = 15;
input string InpExcludedEventNames = "";
input bool InpFailClosedOnCalendarError = false;
CTrade trade;
int g_fastEmaHandle = INVALID_HANDLE;
int g_slowEmaHandle = INVALID_HANDLE;
datetime g_lastBarTime = 0;
datetime g_cacheExpires = 0;
bool g_cachedNewsWindow = false;
string g_cachedReason = "";
bool IsExcludedEventName(const string eventName)
{
if(StringLen(InpExcludedEventNames) == 0)
return false;
string exclusions[];
int count = StringSplit(InpExcludedEventNames, ';', exclusions);
for(int i = 0; i < count; i++)
{
StringTrimLeft(exclusions[i]);
StringTrimRight(exclusions[i]);
if(StringLen(exclusions[i]) > 0 && StringFind(eventName, exclusions[i]) >= 0)
return true;
}
return false;
}
bool GetSymbolCurrencies(string &baseCurrency, string "eCurrency)
{
string symbolName = _Symbol;
if(StringLen(symbolName) < 6)
return false;
baseCurrency = StringSubstr(symbolName, 0, 3);
quoteCurrency = StringSubstr(symbolName, 3, 3);
StringToUpper(baseCurrency);
StringToUpper(quoteCurrency);
return true;
}
bool CurrencyHasBlockingEvent(const string currency,
const datetime windowStart,
const datetime windowEnd,
bool &queryOk,
string &reason)
{
MqlCalendarValue values[];
ResetLastError();
int received = CalendarValueHistory(values, windowStart, windowEnd, NULL, currency);
if(received < 0)
{
queryOk = false;
reason = StringFormat("CalendarValueHistory failed for %s, error %d", currency, GetLastError());
return false;
}
for(int i = 0; i < received; i++)
{
MqlCalendarEvent event;
ResetLastError();
if(!CalendarEventById(values[i].event_id, event))
{
queryOk = false;
reason = StringFormat("CalendarEventById failed, error %d", GetLastError());
return false;
}
if((int)event.importance < (int)InpMinImportance)
continue;
if(IsExcludedEventName(event.name))
continue;
if(values[i].time >= windowStart && values[i].time <= windowEnd)
{
reason = StringFormat("%s event: %s at %s", currency, event.name,
TimeToString(values[i].time, TIME_DATE | TIME_MINUTES));
return true;
}
}
return false;
}
bool IsNewsWindowActive(string &reason)
{
datetime serverNow = TimeTradeServer();
if(serverNow == 0)
{
reason = "TimeTradeServer returned 0";
return InpFailClosedOnCalendarError;
}
string baseCurrency;
string quoteCurrency;
if(!GetSymbolCurrencies(baseCurrency, quoteCurrency))
{
reason = "Cannot derive two currencies from symbol " + _Symbol;
return InpFailClosedOnCalendarError;
}
datetime windowStart = serverNow - InpMinutesBefore * 60;
datetime windowEnd = serverNow + InpMinutesAfter * 60;
bool queryOk = true;
if(CurrencyHasBlockingEvent(baseCurrency, windowStart, windowEnd, queryOk, reason))
return true;
if(!queryOk)
return InpFailClosedOnCalendarError;
if(quoteCurrency != baseCurrency &&
CurrencyHasBlockingEvent(quoteCurrency, windowStart, windowEnd, queryOk, reason))
return true;
if(!queryOk)
return InpFailClosedOnCalendarError;
reason = "No matching calendar event";
return false;
}
bool CanOpenNewTrade()
{
datetime serverNow = TimeTradeServer();
if(serverNow == 0)
{
Print("News filter: TimeTradeServer returned 0");
return !InpFailClosedOnCalendarError;
}
if(serverNow >= g_cacheExpires)
{
g_cachedNewsWindow = IsNewsWindowActive(g_cachedReason);
g_cacheExpires = serverNow + MathMax(1, InpRefreshSeconds);
Print("News filter: ", g_cachedReason,
", entries ", (g_cachedNewsWindow ? "blocked" : "allowed"));
}
return !g_cachedNewsWindow;
}
bool IsNewBar()
{
datetime currentBarTime = iTime(_Symbol, _Period, 0);
if(currentBarTime == 0 || currentBarTime == g_lastBarTime)
return false;
g_lastBarTime = currentBarTime;
return true;
}
void ManageOpenPosition()
{
if(!PositionSelect(_Symbol))
return;
long positionType = PositionGetInteger(POSITION_TYPE);
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double stopLoss = PositionGetDouble(POSITION_SL);
double takeProfit = PositionGetDouble(POSITION_TP);
double currentPrice = (positionType == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
if(currentPrice == 0.0)
return;
double moveInPoints = (positionType == POSITION_TYPE_BUY) ? (currentPrice - openPrice) / _Point : (openPrice - currentPrice) / _Point;
if(moveInPoints < InpBreakEvenTriggerPoints)
return;
double newStop = NormalizeDouble(openPrice, _Digits);
bool needsMove = (positionType == POSITION_TYPE_BUY && (stopLoss == 0.0 || stopLoss < newStop)) ||
(positionType == POSITION_TYPE_SELL && (stopLoss == 0.0 || stopLoss > newStop));
if(needsMove && !trade.PositionModify(_Symbol, newStop, takeProfit))
Print("Break-even modification failed: ", trade.ResultRetcodeDescription());
}
void TryOpenEmaCrossTrade()
{
double fast[];
double slow[];
ArraySetAsSeries(fast, true);
ArraySetAsSeries(slow, true);
if(CopyBuffer(g_fastEmaHandle, 0, 0, 3, fast) != 3 || CopyBuffer(g_slowEmaHandle, 0, 0, 3, slow) != 3)
{
Print("EMA buffer data is not ready");
return;
}
bool buySignal = fast[2] <= slow[2] && fast[1] > slow[1];
bool sellSignal = fast[2] >= slow[2] && fast[1] < slow[1];
if(!buySignal && !sellSignal)
return;
if(!CanOpenNewTrade())
{
Print("New entry skipped by news filter: ", g_cachedReason);
return;
}
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
if(ask == 0.0 || bid == 0.0)
return;
if(buySignal)
{
double stopLoss = NormalizeDouble(ask - InpStopLossPoints * _Point, _Digits);
double takeProfit = NormalizeDouble(ask + InpTakeProfitPoints * _Point, _Digits);
if(!trade.Buy(InpLots, _Symbol, 0.0, stopLoss, takeProfit, "EMA cross buy"))
Print("Buy failed: ", trade.ResultRetcodeDescription());
}
else if(sellSignal)
{
double stopLoss = NormalizeDouble(bid + InpStopLossPoints * _Point, _Digits);
double takeProfit = NormalizeDouble(bid - InpTakeProfitPoints * _Point, _Digits);
if(!trade.Sell(InpLots, _Symbol, 0.0, stopLoss, takeProfit, "EMA cross sell"))
Print("Sell failed: ", trade.ResultRetcodeDescription());
}
}
int OnInit()
{
if(InpMinutesBefore < 0 || InpMinutesAfter < 0 || InpLots <= 0.0)
return INIT_PARAMETERS_INCORRECT;
trade.SetExpertMagicNumber(InpMagic);
g_fastEmaHandle = iMA(_Symbol, _Period, 20, 0, MODE_EMA, PRICE_CLOSE);
g_slowEmaHandle = iMA(_Symbol, _Period, 50, 0, MODE_EMA, PRICE_CLOSE);
if(g_fastEmaHandle == INVALID_HANDLE || g_slowEmaHandle == INVALID_HANDLE)
return INIT_FAILED;
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
if(g_fastEmaHandle != INVALID_HANDLE)
IndicatorRelease(g_fastEmaHandle);
if(g_slowEmaHandle != INVALID_HANDLE)
IndicatorRelease(g_slowEmaHandle);
}
void OnTick()
{
ManageOpenPosition();
if(PositionSelect(_Symbol) || !IsNewBar())
return;
TryOpenEmaCrossTrade();
}
Wire it into an EA you already use
You don't need the EMA example to adopt the filter. Copy the inputs, helper functions, and cache variables into your EA. Then identify the exact place where your code is about to send an entry order.
For a long condition, the pattern is this:
// Add this inside your existing entry function, after longSignal is calculated.
void SubmitLongWhenAllowed(const bool longSignal)
{
if(!longSignal)
return;
if(!CanOpenNewTrade())
{
Print("Long entry blocked: ", g_cachedReason);
return;
}
// Your existing trade.Buy(...) call goes here.
}
Do not put the filter at the top of OnTick() with an early return. That common shortcut also suppresses trailing stops, break-even moves, exit conditions, and cleanup code during the calendar window. Gate only the code paths that create a new position or add to one.
For a multi-symbol EA, pass the traded symbol to a version of GetSymbolCurrencies() rather than relying on _Symbol. For a basket strategy, make one calendar decision per instrument because GBPUSD and USDJPY share USD but have different second currencies.
If your position logic requires a magic number, isolate it carefully. The companion guide on MQL5 magic numbers shows why a symbol-only PositionSelect() check can be too broad when several EAs share an account.
Trade-server time prevents a silent timezone bug
Calendar event times are expressed in the trade-server time used by the terminal's calendar functions. Your PC clock, browser calendar, and a manually entered UTC offset are not a safe substitute. TimeTradeServer() supplies the calculated current server time, which is why this module uses it for windowStart, windowEnd, and cache expiry.
Don't hard-code “New York news time” or add a seasonal daylight-saving adjustment. Query the calendar using the terminal's server-time basis instead. The code also treats a zero TimeTradeServer() result as an operational problem rather than quietly falling back to TimeLocal().
A 30-minute-before, 20-minute-after window is a starting configuration, not a rule. FOMC statements and rate decisions may justify 45/30 for an intraday system. A slower H4 swing strategy might only block the event hour itself. Change the inputs, then measure the consequence in a forward test rather than assuming a larger window is safer.
Test the filter without fooling yourself
The native economic calendar is excellent for live scheduling, but it changes the way you validate an EA. A Strategy Tester run must not be treated as proof that historical calendar requests reproduced the live calendar state exactly. Calendar availability and historical event data need to be checked in the environment and date range you plan to use.
Start with a visual live-demo check:
- Attach the EA to EURUSD before a known high-impact EUR or USD release.
- Set
InpMinutesBeforeto 60 temporarily and open the Experts tab. - Confirm a line such as
entries blockedappears with the event name and server-time timestamp. - Confirm an existing position can still move to break-even while entries are blocked.
- After the configured end time, confirm the log changes to
entries allowedbefore judging the entry rule.
Then run a separate baseline backtest of the entry strategy without claiming the calendar layer was historically reproduced. Compare trade logic, stops, spread assumptions, and execution behavior using the practices in how to backtest a trading strategy. The filter is an operational control. It needs live or demo observation around actual scheduled events.
Handle no data, errors, and unavailable actual values
A successful query with zero records is normal. The code sets reason to “No matching calendar event” and permits entry. A retrieval failure is not normal: it logs the MQL5 error code and follows your failure-mode input.
Keep InpFailClosedOnCalendarError = false while proving that the calendar behaves reliably on your terminal. Switch to true only if skipping a trade is preferable to taking an unfiltered entry and you accept that temporary calendar outages can halt the EA's new entries.
LONG_MIN deserves a separate reminder. It means a calendar value such as actual, forecast, previous, or revised data may be unavailable. Never write if(values[i].actual_value == 0) to decide whether an event exists. This module doesn't inspect those fields because it filters on scheduled time, currency, name, and impact.
Common news-filter mistakes
❌ Mistake: Filtering only the base currency. EURUSD can move sharply on USD news even if the release has nothing to do with the euro.
✅ Do this: Query both EUR and USD, as CurrencyHasBlockingEvent() does. For GBPJPY, query GBP and JPY.
❌ Mistake: Calling CalendarValueHistory() on every tick. Busy symbols can generate many ticks per second, creating needless terminal work and repetitive log noise.
✅ Do this: Cache the decision for 15 seconds. If you trade a very fast M1 breakout, reduce the refresh interval to 5 seconds and observe terminal load.
❌ Mistake: Blocking the whole OnTick() handler. That freezes trailing stops and exit rules precisely when price movement may be largest.
✅ Do this: Run management first, then put CanOpenNewTrade() directly above each new Buy(), Sell(), or pending-order placement.
❌ Mistake: Treating an empty result as a calendar outage. A normal quiet window should not lock an EA out of the market.
✅ Do this: Distinguish received == 0 from received < 0. Only the negative return invokes your selected error policy.
❌ Mistake: Assuming every broker symbol starts with six FX characters. XAUUSD, US30, and symbols with a prefix may not match the base/quote extraction rule.
✅ Do this: Keep this automatic derivation for conventional FX pairs. Add explicit InpBaseCurrency and InpQuoteCurrency inputs for other markets.
Pro tips for a filter you can audit
Log the event name and server timestamp. A log entry that says “blocked” is weak evidence. USD event: Federal Funds Rate at 2026.10.28 18:00 gives you something concrete to verify against the terminal calendar.
Exclude only a named event you have reviewed. The InpExcludedEventNames input accepts semicolon-separated text, such as "Holiday;Bank Lending". It performs a substring match, so keep exclusions distinctive and check the Experts log after changes.
Use different windows by strategy speed. A 5-minute EMA crossover can use 30/20. A pending-order breakout strategy may need to cancel or prevent placement 60 minutes before a rate decision. That is separate logic from this entry filter.
Check order constraints after the gate. The filter decides whether to attempt an order, not whether your stop distance meets broker rules. If the trade call fails with an invalid-stop response, fix the order construction with the guide to MQL5 error 10016.
Generating this module without writing it from scratch
HorizonAI can generate MQL5 indicators, strategies, and EAs from a plain-English chat, then compile-check the code and help edit it. It writes the EA code, but it doesn't connect to a broker or place live trades for you.
Use a prompt that defines the entry boundary as tightly as the module above:
Build an MQL5 Expert Advisor module for standard six-character FX symbols. Use TimeTradeServer, CalendarValueHistory, and CalendarEventById to block new entries 30 minutes before through 20 minutes after high-impact calendar events affecting either currency in the chart symbol. Add semicolon-separated event-name exclusions, a 15-second cache, detailed Print logs, and a fail-open or fail-closed input for calendar errors. Do not block break-even, trailing-stop, or exit management. Return a complete compile-ready EA with a minimal 20/50 EMA crossover example and 300-point stop loss, 600-point target, and break-even at 200 points.
You can then ask:
Replace the EMA crossover example with my existing London-breakout entry logic, preserve the calendar filter, and apply magic number 26001 only to this EA's positions.
That produces working, compile-checked MQL5 you can inspect and revise in chat or the in-browser editor. Try it free →
FAQs
Does an MQL5 news filter close open trades before high-impact news?
Not in this build. It blocks new entries only, while ManageOpenPosition() continues handling an existing trade. Closing positions before news is a separate, explicit risk rule.
Does CalendarValueHistory use my computer's local timezone?
No. Build the query range from TimeTradeServer() so its timestamps align with the terminal's trade-server clock and the calendar API's time basis.
Why does my EA say no matching calendar event when a release is scheduled?
Check that the event affects one of the two currencies derived from your symbol, that its importance meets your selected threshold, and that the configured before/after window includes the trade-server event time. Also inspect the Experts log for a calendar retrieval error.
Can I use this filter on gold, indices, or crypto?
Not unchanged. The sample derives two three-letter currencies from the first six symbol characters. Add explicit affected-currency inputs or a market-specific mapping before using it on non-FX symbols.
Final thoughts
A reliable news filter is small by design: retrieve events on the terminal's server clock, match both currencies, apply a clear impact threshold, and prevent only fresh exposure. The most useful first test is not a long optimization run. Attach it to a demo EURUSD chart before a scheduled high-impact release and verify the log, the blocked order, and the still-active stop management.
Related articles
- How to Automate Your Trading Strategy on MetaTrader 5 — Understand the practical structure of an MT5 automation workflow.
- MT5 EA Not Opening Trades: MQL5 Debugging Checklist — Diagnose permissions, symbols, prices, and trade-request failures.
- MQL5 Magic Numbers: Safely Manage Your EA Positions — Keep this EA's position logic separate from other strategies.
- MQL5 Error 10016 (Invalid Stops): How to Fix Your EA Safely — Build stop and target prices that respect broker constraints.
- How to Code a Break-Even Stop Expert Advisor in MQL5 — Expand the included break-even example safely.
- How to Code a Supertrend Indicator and an EA in MQL5 — Replace the simple EMA trigger with a trend-following entry model.
- How to Calculate Lot Size by Risk Percentage in MQL5 — Add risk-based volume sizing to new entries.
- How to Backtest Trading Strategies — Evaluate the entry system separately from live calendar operations.
Questions about MQL5 economic-calendar news filters? Join our Discord to discuss with other traders!
