How to code a time-of-day trading filter in MQL5
Time-of-day filters are the cheapest edge most algos never test. A breakout system that prints money during the London open can bleed steadily through the afternoon chop, and no amount of parameter tuning fixes that — only a clock does. Below is a filter you can drop into any expert advisor, with the timezone traps handled rather than ignored.
Server time is the only clock that matters
Before writing a single line, decide which clock you mean. Broker server time is what TimeCurrent() returns, and it is what your history, your spread data and your Strategy Tester all share. It is usually GMT+2 or GMT+3 depending on the broker's daylight-saving rules, not your local time and not London time.
This matters. A "trade only between 08:00 and 17:00" filter written against server time will drift by an hour twice a year as the broker shifts its offset. A filter written against TimeLocal() is worse: it backtests beautifully and then behaves differently on a VPS in a different region. Never build a session filter on TimeLocal().
If you need a genuine exchange session, measure the offset once rather than hardcoding it. TimeTradeServer() minus TimeGMT() gives you the broker's current offset in seconds, which you can print from OnInit() and sanity-check against your inputs. For most retail work, defining the window directly in server time is simpler and more robust.
Build the filter, step by step
1. Store the window as hours and minutes, not just hours. Half-hour session boundaries are common enough that hard-coding whole hours becomes annoying fast.
2. Convert the timestamp once. TimeToStruct() fills an MqlDateTime with year, month, day, hour, minute, second and day_of_week, where 0 is Sunday and 6 is Saturday.
3. Reject the weekend first. Saturday is always shut. Sunday is shut except for the evening open, which a wrapping window may legitimately cover.
4. Compare minutes since midnight. Collapsing the time to a single integer (hour * 60 + minute) makes the comparison trivial and lets you handle a window that wraps past midnight — 22:00 to 04:00 — with two conditions instead of a nest of special cases.
5. Gate entries, and decide about exits. Blocking new trades is the easy part. The question people forget is whether an open position should be flattened when the window closes. For intraday systems the answer is usually yes.
6. Keep the filter outside your signal logic. The window check should be the first thing OnTick() does, so no signal code runs outside hours. That keeps the logic readable and stops accidental order sends from helper functions.
The code
This compiles as a standalone EA. Replace the breakout entry with your own signal.
//+------------------------------------------------------------------+
//| TimeWindowEA.mq5 |
//| Time-of-day filter example for MQL5 |
//+------------------------------------------------------------------+
#property version "1.00"
#include <Trade\Trade.mqh>
input group "Trading window (broker server time)"
input int InpStartHour = 8; // Window start hour (0-23)
input int InpStartMinute = 0; // Window start minute
input int InpEndHour = 17; // Window end hour (0-23)
input int InpEndMinute = 0; // Window end minute
input bool InpFlattenAtEnd = true; // Close trades when window ends
input group "Trade settings"
input double InpLots = 0.10; // Volume
input int InpStopLossPts = 400; // Stop loss in points (0 = none)
input int InpTakeProfitPts = 800; // Take profit in points (0 = none)
input ulong InpMagic = 20240101; // Magic number
CTrade trade;
//+------------------------------------------------------------------+
int OnInit()
{
trade.SetExpertMagicNumber(InpMagic);
trade.SetDeviationInPoints(10);
trade.SetTypeFillingBySymbol(_Symbol);
// Sanity check: how far ahead of GMT is the broker right now?
PrintFormat("Broker GMT offset: %d minutes",
(int)((TimeTradeServer() - TimeGMT()) / 60));
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
void OnTick()
{
const bool inWindow = IsWithinTradingWindow(TimeCurrent());
if(!inWindow)
{
if(InpFlattenAtEnd && HasOpenPosition())
CloseAllPositions();
return;
}
if(!IsNewBar(_Period)) return;
if(HasOpenPosition()) return;
const double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
const int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
const double prevHigh = iHigh(_Symbol, _Period, 1);
const double prevLow = iLow(_Symbol, _Period, 1);
double sl = 0.0, tp = 0.0;
if(ask > prevHigh)
{
if(InpStopLossPts > 0) sl = NormalizeDouble(ask - InpStopLossPts * point, digits);
if(InpTakeProfitPts > 0) tp = NormalizeDouble(ask + InpTakeProfitPts * point, digits);
trade.Buy(InpLots, _Symbol, 0.0, sl, tp, "window buy");
}
else if(bid < prevLow)
{
if(InpStopLossPts > 0) sl = NormalizeDouble(bid + InpStopLossPts * point, digits);
if(InpTakeProfitPts > 0) tp = NormalizeDouble(bid - InpTakeProfitPts * point, digits);
trade.Sell(InpLots, _Symbol, 0.0, sl, tp, "window sell");
}
}
//+------------------------------------------------------------------+
//| True when server time falls inside the configured window. |
//+------------------------------------------------------------------+
bool IsWithinTradingWindow(const datetime serverTime)
{
MqlDateTime st;
TimeToStruct(serverTime, st);
if(st.day_of_week == 6) // Saturday: market is shut
return false;
const bool isSunday = (st.day_of_week == 0);
const int minute = st.hour * 60 + st.min;
const int start = InpStartHour * 60 + InpStartMinute;
const int end = InpEndHour * 60 + InpEndMinute;
if(start == end) // 24-hour window
return !isSunday;
if(start < end) // window inside one calendar day
return (!isSunday && minute >= start && minute < end);
// Window wraps midnight, e.g. 22:00 -> 04:00.
if(minute >= start) // evening leg, includes Sunday open
return true;
if(minute < end) // morning leg
return !isSunday; // Sunday morning is still shut
return false;
}
//+------------------------------------------------------------------+
bool IsNewBar(const ENUM_TIMEFRAMES tf)
{
static datetime lastBar = 0;
const datetime current = iTime(_Symbol, tf, 0);
if(current == lastBar)
return false;
lastBar = current;
return true;
}
//+------------------------------------------------------------------+
bool HasOpenPosition()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
const ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != (long)InpMagic) continue;
return true;
}
return false;
}
//+------------------------------------------------------------------+
void CloseAllPositions()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
const ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != (long)InpMagic) continue;
trade.PositionClose(ticket);
}
}
Testing a time filter honestly
Run the same signal twice: once with the filter off and once on, over identical history and with identical risk. If you only test the filtered version, you have no idea whether you found an edge or just curve-fitted a clock. Then vary the window in fifteen-minute steps and watch whether performance degrades smoothly or falls off a cliff. Smooth degradation is a real effect; a single magic hour usually is not.
As a reference point, [see a real backtest](https://mql.ranartech.com/r/VzNpp-wTJ-c5) — GBPUSD H1 EA (+$60, 38% win, 21 trades). That is a very small sample, so read it as an illustration of how modest the numbers often look once a session filter is applied, not as evidence of anything.
Traps worth knowing
A few things bite regularly. Broker daylight-saving shifts move your window relative to the sessions you actually care about, so re-check the offset printout twice a year. Friday afternoons carry weekend gap risk that a time filter does not remove — consider blocking new entries after a chosen hour on Fridays. And remember that InpFlattenAtEnd closes positions on any tick outside the window, including the first tick after a weekend gap, which can be an expensive exit if the market reopened against you.