brokerclock — MetaTrader broker server time to UTC (and back), with daylight saving done right
MetaTrader 5 does not give you UTC. The time and time_msc values that copy_rates_*, copy_ticks_*,
history_deals_get and history_orders_get return are the broker's server clock written as epoch seconds,
even though they look like Unix timestamps. If you searched for "MT5 server time UTC", "broker GMT offset" or
"copy_rates time zone", this is why your bars are two or three hours off.
- Most brokers run GMT+2 in winter and GMT+3 in summer, switching with US daylight saving: New York time plus 7 hours, so the forex day starts at server midnight, 17:00 in New York.
- A fixed offset breaks for weeks every year.
+3is wrong all winter, and even a European daylight-saving rule is wrong for about three weeks in March and one week around the start of November, when the US and Europe switch on different dates. - The server clock works in the other direction too: the date arguments of
copy_rates_rangeand friends, and an order'sexpiration, are read as server time.
brokerclock models the server clock as an IANA time zone plus a shift, so every conversion uses the rule in force at
that moment. It finds the rule from price history alone, confirms it against live ticks, and converts single values
or whole pandas columns. It needs only the standard library (plus tzdata on Windows) and never imports
MetaTrader5, so it runs and tests on any operating system.
Install (after the first release)
pip install brokerclock
pip install "brokerclock[pandas]" # with the pandas helpers
Python 3.10 or newer.
Quick start
1. Find the broker's clock from H1 EURUSD bars
import MetaTrader5 as mt5
import brokerclock
mt5.initialize()
# About a year of H1 bars of a forex major.
rates = mt5.copy_rates_from_pos("EURUSD", mt5.TIMEFRAME_H1, 0, 6500)
result = brokerclock.detect(rates["time"], timeframe=3600)
print(result.zone) # GMT+2/+3 (US daylight saving)
print(f"{result.matched} of {result.total} weeks agree")
zone = result.zone
If result.ambiguous is true, the bars cannot separate the clocks in result.candidates (summer-only data, for
example): fetch a longer span. Running the same check on a second major is good practice. Once known, the clock is a
constant you can keep in your configuration, such as brokerclock.ServerZone.us().
2. Convert bars to UTC
import brokerclock.pandas as bcpd
bars = bcpd.frame_to_utc(rates, zone) # a DataFrame whose "time" column is aware UTC
print(bars[["time", "close"]].tail(3))
first_bar = zone.to_utc(rates["time"][0]) # one value, without pandas: an aware UTC datetime
3. Query by UTC and set an order expiration
from datetime import datetime, timedelta, timezone
# Bars of March 2025 in UTC terms. Pass server-clock ints: a naive datetime would go through the PC's time zone.
start = datetime(2025, 3, 1, tzinfo=timezone.utc)
end = datetime(2025, 4, 1, tzinfo=timezone.utc)
march = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_H1, zone.server_timestamp(start), zone.server_timestamp(end))
# A pending order that expires four hours from now.
expiry = datetime.now(timezone.utc) + timedelta(hours=4)
request = {
"action": mt5.TRADE_ACTION_PENDING,
"symbol": "EURUSD",
"volume": 0.1,
"type": mt5.ORDER_TYPE_BUY_LIMIT,
"price": 1.05,
"type_time": mt5.ORDER_TIME_SPECIFIED,
"expiration": zone.server_timestamp(expiry),
}
mt5.order_send(request)
4. Check the clock against live ticks
tick = mt5.symbol_info_tick("EURUSD")
if brokerclock.check(zone, [tick.time_msc / 1000]):
print("live ticks agree with", zone)
else:
print("no fresh tick to confirm it: the market is closed, or the zone is ahead of the server")
# Without a known zone: the current offset, while the market trades.
print(brokerclock.measure_offset([tick.time_msc / 1000])) # 3:00:00 in summer, 2:00:00 in winter
check raises ZoneMismatchError when the tick would be from the future under that zone (the zone is behind the
server, or the PC clock is behind). measure_offset raises StaleTickError or OffsetOutOfRange instead of
guessing when the tick is not fresh.
Broker clock conventions
| Server clock | Winter | Summer | Switch dates | ServerZone |
|---|---|---|---|---|
| New York close, the most common | GMT+2 | GMT+3 | US: second Sunday of March, first Sunday of November | ServerZone.us() |
| EU rule | GMT+2 | GMT+3 | EU: last Sundays of March and October | ServerZone.eu() |
| EU rule, central European | GMT+1 | GMT+2 | EU | ServerZone.eu(1) |
| Australian rule | GMT+2 (April to October) | GMT+3 (October to April) | AU: first Sundays of October and April | ServerZone.au() |
| Fixed GMT | GMT+0 | GMT+0 | none | ServerZone.fixed(0) |
| Fixed GMT+2 | GMT+2 | GMT+2 | none | ServerZone.fixed(2) |
| Any other clock | ServerZone.us(3), ServerZone.fixed(5.75), ServerZone("Asia/Tokyo"), ServerZone("Europe/Berlin", timedelta(hours=1)) |
A ServerZone is the wall clock of an IANA zone plus a shift in 15-minute steps; str(zone) describes it, for
example GMT+2/+3 (US daylight saving).
How detection works
- A gap of 40 hours or more after a bar is a weekend; that bar's end is the weekly close on the server clock.
- The forex week ends on Friday at 17:00 New York, so each close gives the server's offset that week. Brokers close at or before 17:00, never after, so the offset is rounded up to the next 15 minutes.
- Every candidate clock (fixed, or US, EU or Australian daylight saving, at any base offset) predicts each week.
- The clock that predicts the most weeks wins; the weeks it misses are
outliers(early holiday closes), and closes that fit no Friday areunexplained. - A tie means the data cannot tell the clocks apart, and
zoneisNone: use a longer span.
Detection needs a forex major and bars from M1 to H1 (timeframe 60 to 3600 seconds; longer bars are rejected
because they end on the server's own 4-hour or day boundaries and hide the switch weeks). Use M15 or shorter for
clocks with 30- or 45-minute offsets. The data should cover at least six months, or a span that includes March or
late October to early November.
API
| Name | Purpose |
|---|---|
ServerZone(zone, shift), .us(), .eu(), .au(), .fixed() |
the server clock |
zone.to_utc(server_time, ambiguous="raise") |
server epoch seconds or naive server datetime to aware UTC |
zone.server_timestamp(instant), zone.to_server(instant) |
aware instant to server epoch int, or naive server datetime |
zone.offset_at(instant) |
the server's UTC offset at an aware instant |
detect(bar_times, timeframe=3600) |
the clock from M1 to H1 bar history, as a Detection |
measure_offset(server_times, now=None, tolerance=60) |
the current offset from a fresh tick and the PC clock |
check(zone, server_times, now=None, tolerance=60, max_age=300) |
whether live ticks agree with a zone |
brokerclock.pandas: to_utc, frame_to_utc, to_server_timestamps |
the same conversions for whole columns |
The library's own errors share the base class BrokerClockError, a ValueError: AmbiguousTimeError,
NonexistentTimeError, NotEnoughData, StaleTickError, OffsetOutOfRange and ZoneMismatchError. Invalid
arguments raise a plain ValueError or TypeError.
Limitations
- When a broker switches the clock of 24/7 symbols (crypto CFDs) is broker-specific, so bars of those symbols right at a switch can be an hour off.
- Metals and indices follow their own exchange hours; detect the clock from a forex major.
- Live measurement depends on the PC clock; keep automatic time sync on.
- A broker's demo and live servers can run different clocks; detect each one.
- A broker that changes its clock may rewrite its history in the new clock; detect again after a change.
- Weekly closes less than 14 minutes before 17:00 New York are handled (16:50 in M5 bars, 16:47 or later in M1
bars). If the last bar of a week ends 14 minutes or more early (16:40, say), that week reads 15 minutes off. A
few such weeks become
outliers; if every week ends that way, the whole result is 15 minutes off, for example GMT+1:45/+2:45 instead of GMT+2/+3. H1 bars avoid this, because the last H1 bar still ends at 17:00.
Notes
Not affiliated with MetaQuotes. MetaTrader is a trademark of MetaQuotes Ltd.
Copyright 2026 Raeez alshamaly. Licensed under the Apache License, Version 2.0; see LICENSE.
Metadata
Release files for brokerclock 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| brokerclock-0.1.0.tar.gz | 31.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| brokerclock-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 52.8 kB
Release files / brokerclock-0.1.0.tar.gz
| Download URL | brokerclock-0.1.0.tar.gz |
|---|---|
| Size | 31.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
39e77388d672defc105acc93967e15fa0c195fc446a9f4d43a5d8667d96e5345
|
|
BLAKE2b-256 checksum How to use checksums |
97022c954145b03639a42b040d60156a77db4cfa98bf9eae00a75bfa88f5bd2e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.2
|
Release files / brokerclock-0.1.0-py3-none-any.whl
| Download URL | brokerclock-0.1.0-py3-none-any.whl |
|---|---|
| Size | 21.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2c9d791e56909eab7010efd021a458c3b3d6ae7a70d47fde67c151a16b1b39ab
|
|
BLAKE2b-256 checksum How to use checksums |
a7997dad0d0128c721470cc2073eaae7092f76d5e4017251d9450113ac4925e7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.2
|