MT5 server time vs UTC: why your timestamps are off

8 min readMetaKit

A deal in your MT5 history says 2026-08-14 16:30:12. Your database says the same trade happened at 13:30:12. Your colleague in Sydney swears it was half past eleven at night. All three are right, and that is the problem.

MT5 server time is the broker's clock. Every timestamp the terminal gives you, on ticks, candles, deals, orders, and positions, is on that clock, and nothing in the data tells you which clock it is.

Server time is whatever the broker decided

There is no "MetaTrader 5 timezone". Each broker sets the timezone of their trade server, and the terminal shows that time everywhere. A common choice is UTC+2 in winter and UTC+3 in summer: it makes the daily candle close at 5pm New York, which lines up with the traditional forex rollover and gives you five daily bars a week instead of six. Some brokers run UTC+0. A few run UTC+3 all year. One or two do something nobody can explain.

The timestamps themselves carry no offset. 1755181812 is just a number of seconds, and the terminal treats it as if it were UTC while displaying the broker's wall-clock time. So when the MetaTrader5 Python package hands you datetime.fromtimestamp(deal.time), you get the broker's wall-clock reading shifted again by your own UTC offset. Wrong twice, in the same direction, and it only looks right if the broker happens to run on UTC.

This is why the same trade shows different hours in two terminals: two brokers, two server clocks. A copier log with the master on a UTC+3 broker and the follower on a UTC+0 broker shows every copy landing "three hours early", and someone always files a bug about it.

The offset changes twice a year, on someone else's dates

If your broker anchors to New York close, their server time follows US daylight saving: it jumps from UTC+2 to UTC+3 on the second Sunday of March and back on the first Sunday of November. Those are not the European dates (last Sunday of March and October), and they are certainly not the Australian dates, which go the other way entirely.

So for a few weeks every spring and autumn, the difference between server time and your local time is different from what it was last month. Code that hardcodes offset = 3 is right for seven months of the year and quietly one hour off for the other five. Historical data is worse: a fixed offset applied to a year of M1 bars is wrong for whichever half of the year you didn't check.

Finding the offset

You cannot read it from the data, so measure it. Compare a timestamp you know is fresh against real UTC.

In MQL5 the terminal does this for you: TimeTradeServer() - TimeGMT() is the current offset in seconds. From the Python package, the latest tick is the freshest clock you have:

import time
import MetaTrader5 as mt5
 
mt5.initialize()
tick = mt5.symbol_info_tick("EURUSD")
offset_hours = round((tick.time - time.time()) / 3600)
print(f"server is UTC{offset_hours:+d}")

Two caveats. Ticks only arrive while the market is open, so on a Saturday the "latest" tick is from Friday and the calculation is garbage; run it during the session. And this gives you the offset now, not the offset on the day a historical deal happened. For history you need the broker's DST rule as well, which brings us to the conversion.

Keep everything in UTC internally

The rule that saves you: convert to UTC at the boundary where data enters your system, store UTC, compute in UTC, and convert to a display timezone only when a human is looking. Never store server time. Never store local time. If a timestamp in your database doesn't end in Z, you already have the bug.

For a broker on the New York-anchored schedule, server time is New York time plus seven hours, all year. That is the trick that makes historical conversion correct across DST changes: subtract seven hours, localise the result as New York, and let the timezone database handle the rest.

import pandas as pd
 
 
def server_to_utc(server_epoch: pd.Series) -> pd.Series:
    """Broker clock = New York + 7h (UTC+2 in winter, UTC+3 in summer)."""
    ny_wall = pd.to_datetime(server_epoch, unit="s") - pd.Timedelta(hours=7)
    return (
        ny_wall.dt.tz_localize(
            "America/New_York", ambiguous="NaT", nonexistent="shift_forward"
        )
        .dt.tz_convert("UTC")
    )
 
 
deals = pd.DataFrame(mt5.history_deals_get(from_date, to_date))
deals["time_utc"] = server_to_utc(deals["time"])

For a fixed-offset broker it's simpler, with one trap: pandas and the IANA database spell UTC+3 as Etc/GMT-3. The sign is inverted, deliberately, for historical reasons that nobody enjoys.

deals["time_utc"] = (
    pd.to_datetime(deals["time"], unit="s")
    .dt.tz_localize("Etc/GMT-3")   # UTC+3, yes, minus three
    .dt.tz_convert("UTC")
)

The JavaScript version of the New York trick uses Intl to ask what the New York offset was at roughly that moment:

function nyOffsetHours(at: Date): number {
  const name = new Intl.DateTimeFormat("en-US", {
    timeZone: "America/New_York",
    timeZoneName: "shortOffset",
  })
    .formatToParts(at)
    .find((p) => p.type === "timeZoneName")!.value; // "GMT-4" or "GMT-5"
  return Number(name.replace("GMT", "")) || 0;
}
 
export function serverToUtc(serverEpochSeconds: number): Date {
  const approx = new Date(serverEpochSeconds * 1000); // a few hours off, close enough to pick DST
  const serverOffset = nyOffsetHours(approx) + 7;     // -5 + 7 = +2 winter, -4 + 7 = +3 summer
  return new Date((serverEpochSeconds - serverOffset * 3600) * 1000);
}

Check your broker actually follows this schedule before trusting it. Pull the D1 candles for a week in January and a week in July and look at where the bar opens in UTC. If it's 22:00 in winter and 21:00 in summer, you're on the New York-anchored schedule. If it's 00:00 both times, the broker is on UTC and none of this applies.

Server time wrecks daily drawdown

Here is where the offset stops being a display issue and starts costing money.

Many prop firms enforce a daily loss limit (a common setup is 5% daily and 10% overall), and the "day" is defined by the firm. Very often it is 00:00 server time, sometimes a different anchor that they'll state in the rules, and in neither case is it midnight where you live. A trader in Sydney on a UTC+3 server sees the firm's day roll over at 7am local. A trader in Los Angeles sees it at 2pm.

Now imagine your own risk tracker computes today's loss from local midnight. The Sydney trader loses 3% between 1am and 6am local, and their tracker shows 3% for today. The firm's tracker shows 3% for yesterday, because 06:00 Sydney is 23:00 server. Two hours later, the firm's day resets, the trader has a clean 5% again, and their own tracker is still nagging about a limit that no longer applies. Or the reverse, which is the one that actually hurts: the tracker says you have room, the firm says you breached at 23:58 server time.

The same misalignment corrupts every daily statistic you compute from deal history: trades per day, best day, worst day, consecutive losing days. Any metric bucketed by day is bucketed by somebody's day, and you need to know whose. The prop firm drawdown post goes into the rules themselves; the timezone is the part most trackers get wrong.

The fix is the same UTC rule with one extra step: convert everything to UTC, then bucket by the firm's reset instant expressed in UTC. If the firm resets at 00:00 server and the server is UTC+3, the day boundary is 21:00 UTC. Store that as configuration per firm, not as a constant.

What MetaKit returns

MetaKit formats every timestamp as ISO 8601 with a Z suffix: deal times, position open times, candle and tick times. For anything MetaKit stamps itself, like webhook timestamp fields, that Z is real UTC. For data that comes out of the terminal it's more subtle, and since it's our own product it's worth being precise.

The terminal hands over bar and deal times as epoch seconds, and those seconds encode server wall-clock time, not true UTC. MetaKit passes them through and formats them, so a deal that closed at 16:30 server time comes back as 16:30:00Z. Nothing shifted it by the broker's offset, because nothing upstream knows what that offset is. The honest reading of a terminal-sourced timestamp from the API is "server time, ISO formatted". The check takes ten seconds: pull one D1 candle and look at its open. 00:00:00Z means you're looking at server time; 21:00:00Z or 22:00:00Z would mean something had converted it. On the accounts we run, it's midnight.

So the offset step doesn't go away. Detect it as above, store it per broker, and apply it before you compare a MetaKit timestamp with anything from another clock. from and to query parameters are matched against the same values, so a plain YYYY-MM-DD means midnight server time on that date.

The sessions array on /v1/accounts/{id}/performance is bucketed by the calendar day of those timestamps, which means the server day. For most prop firms that's exactly the day you want, because they reset at server midnight too. If your firm's day is anchored somewhere else, build the bucketing yourself from /v1/accounts/{id}/deals. Group by positionId, take the time of the entry: "out" deal, shift it by the firm's offset, then take the date. The historical data post has the pandas plumbing for pulling deals and candles into a frame in the first place.

For live daily-loss monitoring there's a more direct route. An equity monitor with metric: "equity_percent" measures the change from the equity captured when the monitor was armed. Re-arm it (PATCH /v1/monitors/{id} with status: "armed") from a scheduled job at the firm's reset instant, and the baseline moves with the firm's day rather than yours. Set the threshold to a little inside the firm's limit and route it to Telegram, and the 23:58 server time breach becomes a 23:40 warning instead.

Either way, you're now comparing UTC to UTC. The moment a server-time value gets into the comparison without being converted, the drawdown figure is wrong by up to a whole session, in whichever direction hurts most.

Go and check what your broker's D1 candle opens at. In UTC. You'll want to know before November.