Your London killzone gate says 02:00 to 05:00. Your expert advisor opened a position at 09:12. The journal shows no error, the code compiled clean, and the same logic looked fine in the backtest — where it was also, quietly, running on the wrong hours.
Nothing about a session gate looks hard. It is a time comparison. That is exactly why it breaks more often than the detection logic it protects: a wrong clock never throws. The order goes in, the log looks normal, and you find out weeks later when you finally plot your fill times against the window you thought you were trading.
This page is the whole fix. Which MQL5 clock actually tells you the truth, how to get from the New York hours killzones are published in to the hours your broker's server counts, how to survive both daylight-saving changeovers without editing an input twice a year, and the one branch a window that crosses midnight always needs. You leave with a complete InKillzone() function and the line that wires it into your entry check.
It assumes you already know what a killzone is and why you want to trade inside one. Nothing below re-teaches that.
Key Takeaways
A killzone filter only ever vetoes. It answers one boolean in front of your entry check and never produces a trade, which is what keeps it a pure, independently testable function.
Convert the clock, not the window. Pull server time back to New York time on each tick and compare against the hours as published; the input then never needs editing when a DST date passes.
In the Strategy Tester, TimeCurrent() — TimeGMT() is always zero — local, GMT and server time are deliberately equated, so a live-derived offset silently turns every backtest into a UTC-server backtest.
A window whose end is smaller than its start needs the || branch. Without it the Asian killzone check is unsatisfiable, and the symptom is zero trades rather than wrong ones.
Table of Contents (25 min read)Contents
What "on autopilot" means here: a gate, not a strategy
A killzone filter never produces a trade. It answers one boolean — is the market clock inside the window right now? — and the only thing it can do with that answer is veto. This is the plainest form of a trading session filter: a pure function in front of your entry check, with no side effects, no state, and no opinion about direction.
That framing is not pedantry. It decides how the code is shaped:
It is testable on its own. You can call InKillzone() in a script with a fabricated clock and see the answer, without running a strategy.
It has exactly one job, so it has exactly one place to be wrong. When trades appear at the wrong hour, you are debugging one function, not a tangle of entry conditions.
Your detection logic stays untouched. Whatever your expert advisor already does — order blocks, liquidity sweeps, structure shifts — does not change to accommodate the filter. One && goes in front of it.
Three things break that innocent-looking function, and all three fail silently: reading a clock that is not the server's, hardcoding an offset that stops being true twice a year, and comparing a window whose end is numerically smaller than its start. The rest of this page is those three, in the order you hit them.
A session gate reading the wrong clock still compiles, still trades, and still logs a clean journal - the disagreement never surfaces on its own.
The windows — and why the published hours disagree
Before you can encode a window you have to pick its numbers, and this is where the first bug is usually typed rather than coded. The killzones are published in New York time (ET), and the exact hours are not standardised. The commonly-quoted set, with the variants you will also run into:
Killzone
Most commonly published (ET)
Also published as
Asian
20:00 - 00:00
19:00 - 00:00 or 20:00 - 22:00
London
02:00 - 05:00
03:00 - 06:00 (when quoted against UK local time)
New York AM
07:00 - 10:00
08:30 - 11:00 or 09:30 - 11:00
London Close
10:00 - 12:00
10:00 - 11:00
Session map
ICT killzones on a 24-hour UTC track24-hour clock · times in UTC
UTC timeline
Asian KZ20:00-00:00 ETLondon KZ02:00-05:00 ETNew York KZ07:00-10:00 ETLondon Close10:00-12:00 ET
1:00–5:001:00
7:00–10:007:00
12:00–15:0012:00
15:00–17:0015:00
000306091215182124
Asian KZ
London KZ
New York KZ
London Close
These UTC bands only hold while US clocks are on standard time. When New York moves to daylight time every band shifts an hour earlier in UTC - which is exactly why the window belongs in ET and not in UTC.
The four windows in UTC, US-winter. Notice they never overlap: this is a schedule, not a confluence.
The disagreement is not a research failure you can settle by finding the right page. Different traders use different definitions, and some of the variance is real translation error — a "03:00 - 06:00 London killzone" is the same window quoted against UK local time in summer, not a different opinion. Treat the hours as a parameter, not a fact. The moment you hardcode one source's numbers as constants, you have baked an unfalsifiable assumption into the EA and lost the ability to test whether a different definition performs better on your instrument.
One more thing the table cannot tell you: the window that matters depends on what you trade. A JPY or AUD pair has a genuinely different Asian session profile than a EUR cross does, and an index CFD's hours are set by its exchange, not by the FX day. If you want to see how the underlying sessions themselves line up before you pick a window, the market hours calculator does that arithmetic for you.
Convert the clock, not the window
There are three clocks in this problem and only one of them is the one your code reads.
New York time (ET) — the clock the windows are defined in. It shifts by one hour on the US daylight-saving dates.
UTC — the only clock that never shifts. It is the pivot, not a place anything happens.
Your broker's server time — the clock TimeCurrent() returns and every timestamp in your journal is written in. Most retail MT5 brokers run their server at UTC+2 in winter and UTC+3 in summer; some run fixed UTC; a few sit elsewhere entirely.
Between ET and server time there are therefore two variable hops, each of which can change on its own schedule. Most implementations convert the window — take 02:00 ET, add the hops, store "09:00 server" in an input — and then have to re-derive that stored number whenever either hop moves.
Invert it. Pull the server clock back to New York time on every tick, and compare against the window exactly as it was published. The input stays 2 and 5 forever, the DST arithmetic happens in code where it can be tested, and the number a reader types is the number they read in the source material. That single decision removes an entire category of bug from the filter.
Two variable hops separate a published killzone from the clock your EA actually reads - which is why the gate converts the clock instead of the window.
You still want the server-time equivalent once — not for the code, but for your own eyes, so you can look at a chart and confirm the shaded hours are where you think they are. That conversion is the calculator below.
Work it out
Killzone in New York time to your broker's server clock
Type the window as it is published in ET plus your broker's GMT offset, and read back the hours the same window occupies on your server clock.
Window opens (ET hour)
Window length
Broker server GMT offset
h
US clocks are currently on
Opens (server time)
—
Closes (server time)
—
Server clock runs ahead of New York by
—
Hours spilling past server midnight
—
Flip the US-clock switch and watch both server hours move while the ET input stays put. That movement is the bug a hardcoded server-time window ships with.
The last output is the one to watch. Any value above zero means that on your server, this window crosses midnight — and the next two sections exist because of that.
Which MQL5 clock actually tells you the server's time?
MQL5 gives you four time functions that all return a plausible-looking datetime. Picking the wrong one produces an EA that compiles, runs, trades, and is simply wrong about what hour it is.
Pick the right clock
Function
What it actually returns
Inside the Strategy Tester
Use it for the gate?
TimeCurrent()
The time of the last quote received - the broker's own clock, advanced by ticks.
Simulated from the historical data being modelled.
Yes - it is the clock your entry decision is actually made on.
TimeTradeServer()
An estimate of server time that keeps advancing between quotes, built from the local machine's clock plus the known server offset.
Set equal to TimeCurrent().
Only for timer-driven work - it inherits any error in the machine's own clock.
TimeLocal()
The clock of the machine running the terminal - your VPS, in whatever timezone it happened to be provisioned with.
Set equal to TimeCurrent().
No - it says nothing about the broker and nothing about New York.
TimeGMT()
UTC, derived from that same local machine clock and its operating-system timezone and DST rules.
Set equal to TimeCurrent() - so any offset derived from it collapses to zero.
Live only, and only paired with TimeCurrent() to measure the offset.
Two of these four describe your VPS, not your broker. That is the whole reason a killzone can start two hours early with nothing in the log.
For a tick-driven gate, TimeCurrent() is the right call and the reason is precise: inside OnTick() it returns the time of the tick you are currently handling. The decision you are about to make belongs to that tick, so the clock you test should be that tick's clock. TimeTradeServer() is the better choice when something must fire on a schedule with no quotes arriving — an OnTimer() housekeeping routine — but it is an estimate built on the local clock, and on a VPS whose time has drifted, that estimate drifts with it.
That is also why the offset measurement belongs inside OnTick(). On a dead market — a weekend, a thin symbol, a holiday — TimeCurrent() freezes at the last quote while UTC keeps moving, so an offset measured in OnTimer() slowly inflates. Measure it on a tick and both sides of the subtraction are fresh by construction.
Handling DST without hardcoding an offset
A number you typed once is a number that stops being true. US clocks change on the second Sunday in March and the first Sunday in November; EU and UK clocks change on the last Sunday in March and the last Sunday in October. Those dates do not coincide, so there is a roughly three-week window each spring and a one-week window each autumn where the gap between New York and London is an hour off its usual value. If your broker's server follows one schedule and your window is defined against the other, a static offset is wrong for those weeks every single year.
Live trading: derive the offset from the terminal itself
Live, you do not need to know anything about your broker's timezone policy, because the terminal is holding both clocks at once.
Live path
mql5killzone_offset.mqh
// Hours the broker's server clock runs ahead of UTC, right now.
// Call this from OnTick(): TimeCurrent() is the time of the tick you are
// handling, so both sides of the subtraction are fresh. Called from OnTimer()
// on a dead market, it compares a stale quote time against a live UTC clock.
double ServerGmtOffsetLive()
{
double raw = (double)(TimeCurrent() - TimeGMT()) / 3600.0;
return(MathRound(raw * 4.0) / 4.0); // snap to the nearest quarter hour
}
Two built-in calls and a rounding step. Recomputed each tick, it is correct on both sides of every changeover date without you touching an input.
The live offset is measured, never remembered.
The rounding matters. TimeCurrent() is a quote timestamp, so the raw difference lands a few seconds either side of a round number; snapping to the nearest quarter hour absorbs that jitter and still supports the handful of server timezones that sit on a half-hour boundary.
The one assumption this makes is that the machine running the terminal knows what time it is, because TimeGMT() is computed from the local clock and the operating system's timezone rules. On an unsynchronised VPS that assumption fails quietly, and every derived hour inherits the error — which is why clock drift is worth ruling out with a time-sync check before you go looking for a bug in this function.
In the Strategy Tester, TimeGMT() stops telling the truth
Here is the part that catches almost everyone, because the code is identical and only the environment changes.
Inside the Strategy Tester, MetaTrader deliberately collapses the clocks: TimeLocal(), TimeGMT() and TimeTradeServer() are all set equal to the simulated TimeCurrent(). This is intentional — a test must produce the same result whether or not the terminal is connected to anything. But it means the expression TimeCurrent() - TimeGMT() evaluates to exactly zero in every backtest, on every broker.
Read what that does to the live function above. It reports an offset of 0, your code concludes the server runs at UTC, and the entire killzone shifts by however many hours the server is actually offset — with no error, no warning, and a full set of perfectly plausible trades. A backtest built on that is measuring a different strategy than the one you deploy.
So the tester needs its own path: an input for the broker's winter offset, plus an algorithmic DST rule so the offset still moves at the right moments during a multi-year test. MQLInfoInteger(MQL_TESTER) is the switch.
Backtest path
mql5killzone_offset.mqh
input int InpTesterSrvGmtWinter = 2; // Tester: server GMT offset, US winter
input bool InpTesterSrvFollowsUsDst = true; // Tester: server clock follows US DST
// Day-of-month of the first Sunday in a given month.
int FirstSunday(const int year, const int mon)
{
MqlDateTime d;
ZeroMemory(d);
d.year = year;
d.mon = mon;
d.day = 1;
MqlDateTime f;
TimeToStruct(StructToTime(d), f);
return(1 + (7 - f.day_of_week) % 7); // day_of_week: 0 = Sunday
}
// US daylight time: from 07:00 UTC on the 2nd Sunday of March
// until 06:00 UTC on the 1st Sunday of November.
bool IsUsDst(const datetime utc)
{
MqlDateTime t;
TimeToStruct(utc, t);
if(t.mon < 3 || t.mon > 11) return(false);
if(t.mon > 3 && t.mon < 11) return(true);
int switchDay = (t.mon == 3) ? FirstSunday(t.year, 3) + 7 : FirstSunday(t.year, 11);
int switchHour = (t.mon == 3) ? 7 : 6;
if(t.day > switchDay) return(t.mon == 3);
if(t.day < switchDay) return(t.mon == 11);
return((t.hour >= switchHour) == (t.mon == 3));
}
// One resolver for both worlds.
double ServerGmtOffsetHours()
{
if(MQLInfoInteger(MQL_TESTER))
{
// In the tester TimeLocal(), TimeGMT() and TimeTradeServer() are all set
// equal to TimeCurrent(), so TimeCurrent() - TimeGMT() is always zero.
// Server time stands in for UTC when picking the DST flag; that is off
// only within a few hours of a changeover, which the flag itself absorbs.
double off = (double)InpTesterSrvGmtWinter;
if(InpTesterSrvFollowsUsDst && IsUsDst(TimeCurrent())) off += 1.0;
return(off);
}
double raw = (double)(TimeCurrent() - TimeGMT()) / 3600.0;
return(MathRound(raw * 4.0) / 4.0);
}
One function, two environments. Live it measures; in the tester it reconstructs from an input plus a date rule, because the tester has no independent GMT to measure against.
The tester branch is not belt-and-braces - without it every backtest silently treats the server as UTC.
Set InpTesterSrvFollowsUsDst to match reality for your broker. A server that flips to UTC+3 on the US dates follows US DST; one that flips on the last Sunday in March follows the European schedule, and during the divergence weeks the two answers differ by an hour. If you do not know which yours is, the checklist further down settles it in one afternoon.
Check yourself
Knowledge check
Inside the MT5 Strategy Tester, what does TimeCurrent() - TimeGMT() evaluate to on a broker whose server runs at UTC+3?
Why
The tester deliberately equates TimeLocal(), TimeGMT() and TimeTradeServer() with the simulated TimeCurrent(), so that a run gives the same result with or without a connection. The subtraction is therefore always zero, and an offset derived from it makes your backtest treat the server as UTC - shifting every killzone by the server's real offset, silently.
The single most expensive assumption in a session filter, in one question.
The branch a midnight-crossing window always needs
Express both the window and the current time as minutes since midnight and the comparison becomes trivial — for three of the four killzones. The Asian window is the exception, and it fails in the most unhelpful way possible.
The wrap branch
mql5midnight_wrap.mq5
// The naive check. Correct for London, New York and London Close.
bool InWindowNaive(const int nowMin, const int startMin, const int endMin)
{
return(nowMin >= startMin && nowMin < endMin);
}
// Asian killzone, 20:00 -> 00:00 ET:
// startMin = 20 * 60 = 1200
// endMin = 0 * 60 = 0
//
// (nowMin >= 1200 && nowMin < 0) is unsatisfiable for every value of nowMin,
// so the window is shut every minute of every day - and nothing logs it.
// The fix is one extra branch, chosen by comparing the bounds.
bool InWindow(const int nowMin, const int startMin, const int endMin)
{
if(startMin == endMin)
return(false); // zero-length: gate closed
if(startMin < endMin)
return(nowMin >= startMin && nowMin < endMin); // ordinary window
return(nowMin >= startMin || nowMin < endMin); // wraps past midnight
}
The wrap case swaps && for || - the window becomes 'after the open OR before the close', which is exactly what a span across midnight means.
A quarter of your session coverage lives or dies on that one operator.
Two details in that function are worth naming. The half-open comparison — >= start and < end — means a window ending at 05:00 excludes 05:00 itself, so two adjacent windows can never both be open on the same minute. And the startMin == endMin guard turns a mistyped or deliberately blank window into a closed gate rather than an always-open one, which is the safer direction for a filter to fail in.
Note that the wrap can appear even where you did not expect it. The New York killzone does not cross midnight in ET, but on a UTC+3 server it runs 14:00 to 17:00 — and on a server far enough east, a mid-afternoon ET window lands after midnight local. Since the gate compares in ET this never affects you, but it is exactly the trap that catches implementations converting the window into server time instead of converting the clock.
Make the window an input, not a constant
Everything above only pays off if the numbers are reachable from the EA's Inputs tab. Four reasons, none of them stylistic:
You deploy the same EA to more than one account. A second broker means a different server offset, and possibly a different DST policy.
The published hours disagree. If the window is an input, "does 08:30 - 11:00 beat 07:00 - 10:00 on this pair?" is an optimisation pass. If it is a constant, it is a recompile and a guess.
The instrument changes the answer. The window worth trading on a JPY cross is not the window worth trading on an index CFD.
You have to be able to test the failure. Reproducing a changeover bug means running the gate against a specific date with a specific assumed offset. Constants make that impossible without editing source.
Keep the inputs in ET, minute-resolution, and named so nobody has to guess the timezone. InpKzStartHourET tells the next reader everything; InpStartHour tells them nothing.
The complete InKillzone() gate
This is the whole filter. It depends on IsUsDst(), FirstSunday() and ServerGmtOffsetHours() from the tester section above.
The deliverable
mql5killzone_gate.mqh
//--- The window, in New York time, exactly as it is published.
input bool InpUseKillzone = true; // Gate entries by killzone
input int InpKzStartHourET = 2; // Window opens (ET hour)
input int InpKzStartMinET = 0; // Window opens (ET minute)
input int InpKzEndHourET = 5; // Window closes (ET hour)
input int InpKzEndMinET = 0; // Window closes (ET minute)
// UTC offset of New York: -5 on standard time, -4 on daylight time.
int EtOffsetHours(const datetime utc)
{
return(IsUsDst(utc) ? -4 : -5);
}
// Minutes since midnight in New York, derived from the tick being handled.
int MinutesOfDayET()
{
datetime utc = TimeCurrent() - (int)(ServerGmtOffsetHours() * 3600.0);
datetime et = utc + EtOffsetHours(utc) * 3600;
MqlDateTime t;
TimeToStruct(et, t);
return(t.hour * 60 + t.min);
}
bool InWindow(const int nowMin, const int startMin, const int endMin)
{
if(startMin == endMin) return(false);
if(startMin < endMin) return(nowMin >= startMin && nowMin < endMin);
return(nowMin >= startMin || nowMin < endMin);
}
// The gate. True = entries allowed, false = veto. No side effects.
bool InKillzone()
{
if(!InpUseKillzone) return(true);
int nowMin = MinutesOfDayET();
int startMin = InpKzStartHourET * 60 + InpKzStartMinET;
int endMin = InpKzEndHourET * 60 + InpKzEndMinET;
return(InWindow(nowMin, startMin, endMin));
}
Server clock in, New York minutes out, one boolean back. Switching to the New York window means changing two inputs and nothing else.
The finished gate: pasteable, side-effect free, and callable from a test script with a fabricated clock.
Two design choices are deliberate. InpUseKillzone = false returns true rather than bypassing the call, so a run with the filter disabled is a genuine control against a run with it enabled. And the function reads a clock but writes nothing — no globals, no chart objects, no order calls — which is what lets you unit-test it away from the market.
Wiring the gate into your entry logic
The gate goes in front of entries. It must not go in front of everything.
Integration
mql5ict_ea.mq5
void OnTick()
{
// 1. Position management runs on EVERY tick, inside the window or outside it.
// Gating this too is the bug that leaves a stop untrailed at 05:01 and a
// take-profit unmoved until the window reopens tomorrow.
ManageOpenPositions();
// 2. Decide once per closed bar, not once per tick.
if(!IsNewBar()) return;
// 3. The killzone gate. It can only ever veto.
if(!InKillzone()) return;
// 4. Your own ICT detection logic - unchanged by any of the above.
int direction = 0;
if(!EntrySignal(direction)) return;
OpenTrade(direction);
}
One AND-style guard on line 12, and management deliberately above it. A trade opened at 04:55 must still be managed at 06:30.
The most common wiring bug is not the gate itself - it is putting position management behind it.
Line 5 is the one people get wrong. Put ManageOpenPositions() after the gate and every open position freezes the moment the window closes: no trailing, no break-even move, no time-based exit, until the next session lets the EA think again. The killzone constrains when you may open a trade, never when you may look after one.
Line 9 is worth a second look too. Calling the gate on every tick is harmless — it is two clock reads and some integer arithmetic — but calling your entry logic on every tick is not, since a condition that flickers mid-bar produces a different trade than the same condition evaluated on a closed bar. A new-bar guard in front of the gate keeps the trigger condition evaluated exactly once per bar.
Testing it: a DST-changeover checklist
A session filter cannot be verified by reading it. Both failure modes only appear on specific calendar dates, which is why they survive code review and reach production. Work through this once and the filter is settled for good.
Before you deploy
Prove the gate before you trust it
0 / 8
Confirm the VPS clock is time-synchronised - the live offset is only as good as TimeGMT().
Print server time, derived UTC and derived New York time on the first tick of each hour for one full day, and read the log.
Compare the offset your code computes against the server clock shown in MetaTrader's Market Watch.
Run a backtest across the US spring-forward date (second Sunday in March) and confirm entry timestamps hold the same ET hour before and after.
Repeat across the US fall-back date (first Sunday in November).
Repeat across the EU/UK changeover dates (last Sunday in March, last Sunday in October) - they sit weeks apart from the US ones.
Set the Asian window (20:00-00:00 ET) deliberately and confirm the gate actually opens; a wrap bug shows up as zero trades, not as wrong trades.
Run once with InpUseKillzone = false as a control, and confirm the trade count differs in the direction you expect.
★
Checklist complete — you’re cleared to proceed.
Seven of these eight are dates, not code. That is the honest shape of this bug class.
The Asian-window item earns its place: a wrap bug produces no trades at all in that session, and an EA that simply never fires looks far more like a quiet market than like a broken comparison. If a window is configured and the trade count for it is exactly zero across a long test, suspect the operator before you suspect the setup.
Once the log is clean in the tester, run the same build on a demo account through at least one real changeover weekend. A forward test is the only place the live branch of the offset resolver ever executes — the tester, by construction, never runs it.
What this filter doesn't do
The gate is a schedule. It says nothing about whether the market is worth trading right now, and it is important to be clear about what still has to sit behind it.
It does not detect anything: liquidity-sweep detection, order-block logic and the AMD daily cycle each remain their own problem, and this function neither helps nor hinders them. It is not a volatility or event filter either — an unexpected release lands inside the New York window as easily as outside it, so a news filter is a separate condition on the same && chain, not something a time window covers. And a clean gate does not make a strategy profitable; it only guarantees that when you measure one, you are measuring the hours you intended. If you are still assembling the surrounding system, the full ICT-to-EA automation workflow is where this component slots in.
Get the clock right and the killzone stops being a variable in your results. That is the entire point of the exercise: not a better window, but a window you can trust you actually traded.
FAQ
Should the killzone hours be inputs in ET or in server time?
In ET. Server-time inputs have to be re-derived every time either the US or your broker changes clocks, and there is no error when you forget — the EA just trades different hours. ET inputs are the numbers as published, and the two variable hops stay in code where they can be tested.
Does recalculating the offset on every tick slow the EA down?
No. TimeCurrent() and TimeGMT() are terminal-local reads with no network round trip, and the rest is integer arithmetic. Caching the result is exactly what re-introduces the staleness the recalculation exists to prevent, so cache it only within a single tick, if at all.
What happens on Sunday evening, when the Asian killzone opens the trading week?
The gate opens as configured, because it only knows about the clock. Whether you want to trade the first hours of the week is a separate decision, and if the answer is no, add a day-of-week guard next to the time check rather than inside it — keep each condition independently switchable.
My broker's server is UTC+2 in winter and UTC+3 in summer. Do I still need this?
Live, the offset function handles it automatically and you never think about it. In the tester you need to know whether that summer switch follows the US dates or the European ones, because the two differ by roughly three weeks in spring and a week in autumn, and InpTesterSrvFollowsUsDst is where you record the answer.
Can I reuse the same gate for a daily cutoff or a news window?
Yes — that is the advantage of the minutes-since-midnight form. Any "no new entries after 16:00" rule or blackout window is another InWindow() call with different bounds, and any window that runs overnight gets the wrap branch for free.
Why does my killzone start two hours early on MT5?
Almost always because the code is comparing against server time while the window is defined in New York time, and the server sits two or three hours ahead of UTC. The second-most-common cause is a live-derived offset running inside the Strategy Tester, where it collapses to zero and leaves the EA convinced the server is on UTC.
Sources & Further Reading
Want to go deeper? These independent, authoritative sources shaped this guide — each one is worth reading in full:
The Cross-Market Desk is the SignalBots editorial team for topics that span every market — platform connectors, copy trading, partnership and IB programs, and the general mechanics of trading automation. We research and write the guides that apply no matter what you trade.
Discussions 0
Leave a comment