sifting/io
Developer Tutorials
6 min readSiftingIO Team

What timezone is market data in? UTC timestamps, DST pitfalls, and session times explained

Market data timestamps are UTC: epoch ms ticks, RFC 3339 bars, date-only filings. How to handle DST, weekend boundaries, and convert correctly in Python and JS.

What timezone is market data in? UTC timestamps, DST pitfalls, and session times explained

What timezone is market data in? Almost always UTC, and SiftingIO follows that convention across the whole API: full timestamps are RFC 3339 strings in UTC, live ticks carry an int64 Unix epoch timestamp in milliseconds, and date-only fields such as SEC filing dates are plain ISO 8601 (YYYY-MM-DD). Nothing arrives in the venue's local time or in your server's local time.

That one convention removes a whole category of ambiguity, but it doesn't remove the work. Trading sessions are still defined in venue-local time. Daylight saving time still moves those sessions around the UTC day twice a year. The forex week still opens on what is Sunday in one city and Monday in another. This post covers the timestamp formats the API actually returns, how session times relate to UTC, and how to convert in Python and JavaScript without the classic off-by-one-hour bugs.

The three timestamp formats you'll receive#

Field typeFormatExampleWhere it appears
Live ticksint64 Unix epoch milliseconds, field t1778019852426WebSocket tick frames, /v1/last/* snapshots
Full timestampsRFC 3339, UTC2026-07-31T13:00:00ZHistorical bars, meta.as_of, event times
Date-only fieldsISO 8601 date2026-07-31Filing dates, holiday calendars, XBRL periods

Epoch milliseconds are the fastest to compare and the hardest to misread: 1778019852426 names the same instant on every machine on Earth. RFC 3339 strings are self-describing (the trailing Z marks UTC), sort lexicographically, and parse in one call in every mainstream language. Date-only fields are deliberately zone-free. A 10-K filed on 2026-07-31 was filed on that date; attaching a time and zone would add precision the source doesn't have.

Historical bars follow the same rule and align to UTC boundaries: an hourly EURUSD bar stamped 13:00Z covers 13:00:00 to 13:59:59.999 UTC in every season.

curl -H "X-API-Key: $SIFTING_KEY" \
     -H "Accept-Encoding: gzip" --compressed \
     "https://api.sifting.io/v1/hist/forex/EURUSD/bars?interval=1h"

The gzip header isn't optional here. Historical bar endpoints require compression and return 406 gzip_required without it.

Market data is UTC, trading sessions are local time#

US equities trade 09:30 to 16:00 in New York local time. In UTC that window is 13:30 to 20:00 for part of the year and 14:30 to 21:00 for the rest, because daylight saving time moves New York relative to UTC while the session definition stays put. Tokyo doesn't observe DST, so its UTC window never moves. Europe changes clocks on a different week than the United States, which means the relative alignment of the two regions drifts for a few days every March and again in late autumn.

The consequence: you can't hardcode a UTC session window for any DST-observing market. SiftingIO exposes the schedule as data instead. GET /v1/fnd/markets/{market}/hours returns a market's weekly schedule in venue-local time, GET /v1/fnd/markets/{market}/status says whether it's open right now, and GET /v1/fnd/markets/{market}/calendar covers holidays and half-days up to 730 days out. The browsable version, covering all 23 markets, is on the market hours pages.

Forex adds the weekend boundary problem. The FX week runs from Sunday evening to Friday evening in New York terms, which lands near 21:00 or 22:00 UTC depending on the season. "Monday's session" therefore begins on Sunday's UTC date, and code that buckets FX bars by calendar date will assign the first hours of the week to Sunday, then wonder why Monday looks short.

Economic releases carry their own timestamp subtleties: a CPI print is a scheduled instant, and which bar it lands in depends on your bar alignment. That topic has its own post on economic calendar timestamps and revisions.

Converting timestamps correctly in Python and JavaScript#

Two rules cover nearly everything. Store and compute in UTC. Convert to a named IANA zone only at the moment a human reads the value.

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

# A live tick's "t" field: epoch milliseconds
t = 1778019852426
utc = datetime.fromtimestamp(t / 1000, tz=timezone.utc)

# An hourly bar timestamp: RFC 3339
bar_open = datetime.fromisoformat("2026-07-31T13:00:00+00:00")

# Display in a venue zone: IANA name, never a fixed offset
local = utc.astimezone(ZoneInfo("America/New_York"))

The one-line trap is the missing tz=. Called as datetime.fromtimestamp(t / 1000), Python interprets the epoch value in the machine's local zone and hands back a naive datetime that only looks correct when the machine happens to run in UTC. CI servers usually do; laptops usually don't. That's why this bug ships.

JavaScript's Date is epoch-milliseconds native, so tick timestamps need no parsing:

const utc = new Date(1778019852426);
const tokyo = new Intl.DateTimeFormat("en-GB", {
  timeZone: "Asia/Tokyo",
  dateStyle: "short",
  timeStyle: "medium",
}).format(utc);

Date stores UTC internally. The local-time behavior it gets blamed for lives in methods like getHours() and in default formatting; format through Intl.DateTimeFormat with an explicit timeZone and the value stays unambiguous end to end.

Common mistakes, and what each one breaks#

MistakeWhat goes wrongFix
Naive datetimes (fromtimestamp without tz=)Every timestamp silently shifts by the machine's UTC offset; joins between two sources land hours apartPass tz=timezone.utc everywhere; treat any naive datetime as a bug
Hardcoded offsets (UTC-5 for New York)Correct half the year, off by one hour after each DST transitionUse IANA zone names (America/New_York) via zoneinfo or Intl
Converting to local time at ingestionDST makes one local hour per year ambiguous and another nonexistent; stored data can't be disambiguated laterStore UTC, convert only at display time
Bucketing FX by calendar dateThe week's first bars carry Sunday's UTC date; daily aggregates misassign themBucket by session boundaries from the hours endpoint rather than by date string
Comparing sessions across regions during DST changeoverRegions switch on different weeks, so overlap windows (London and New York, for example) drift in March and autumnCompute each session in its own IANA zone, then compare in UTC

Two more gotchas surface in practice. The hours endpoint returns venue-local times by design, so converting this week's schedule to UTC once and caching it leaves a stale window after the next DST transition; recompute per day and check the calendar endpoint for half-days. And date-only strings bite in JavaScript: new Date("2026-07-31") parses as UTC midnight, so rendering it in a negative-offset zone displays July 30, which makes a filing list show every date one day early for users in the Americas.

UTC in, UTC through the pipeline, local only at the screen. Get that ordering right and DST transitions, weekend boundaries, and cross-market comparisons reduce to lookups against the hours and calendar endpoints. The full timestamp conventions, including pagination as_of fields and XBRL period codes, are in the API reference. Read the docs

Keep reading

Related posts