A trade executes at 23:47 UTC. A user in New York sees 19:47; a user in Tokyo sees 08:47 the next day.
Both displays can be right. Trouble starts when the application uses the viewer’s local date to decide which trading day gets the trade.
That’s often called a timezone bug. The deeper problem is using one kind of value—a timestamp—to represent several different things: an event, a calendar date, or a scheduled local time.
JavaScript’s Temporal API gives these values distinct types. The examples below use it to make the differences explicit. Temporal support varies by runtime; use a compatible polyfill where needed.
Quick decision tree
What are you storing?
│
├─ A specific event: “When did the fill happen?” → Instant
├─ A scheduled local time: “09:30 New York” → ZonedDateTime
├─ A calendar date only: settlement or expiry → PlainDate
├─ A recurring month and day: coupon on Dec 15 → PlainMonthDay
└─ A length of time: timeout or elapsed period → Duration
The sections below explain each choice and show how it applies to trading systems.
Choose the type that matches the meaning
| Meaning | Temporal type | Example |
|---|---|---|
| A specific moment, such as a fill or log event | Temporal.Instant |
2026-10-26T10:59:05Z |
| A wall-clock time in a named timezone | Temporal.ZonedDateTime |
08:00 in Europe/London
|
| A calendar date without a time or timezone | Temporal.PlainDate |
Settlement date: 2026-10-28
|
| A recurring month and day | Temporal.PlainMonthDay |
Coupon date: December 15 |
| A length of time | Temporal.Duration |
A 30-minute timeout |
A useful question is: Would this value mean something different if the viewer changed timezone?
An event’s Instant does not change, though its displayed local time does. A PlainDate stays the same date. A scheduled time such as “08:00 London” depends on its named timezone, including its daylight-saving rules.
Send the right value over the wire
Don’t convert every date or time to UTC before sending it. UTC is suitable for an Instant, but it can lose the meaning of a calendar date or a future local schedule.
{
"executed_at": "2026-10-26T10:59:05Z",
"settlement_date": "2026-10-28",
"scheduled_time": {
"local": "2026-10-26T08:00:00",
"time_zone": "Europe/London"
}
}
Store an event timestamp as an instant. Store a date-only value as a date. For a future schedule tied to local time, keep the local date and time with an IANA timezone ID.
Front office: events and sessions
An exchange fill is an instant. Convert it to a date only after choosing which timezone defines the relevant trading day.
const fill = Temporal.Instant.from("2026-10-26T23:47:00Z");
// If the report defines its day in UTC:
const utcDate = fill.toZonedDateTimeISO("UTC").toPlainDate();
// If the business rule defines its day in New York:
const newYorkDate = fill
.toZonedDateTimeISO("America/New_York")
.toPlainDate();
Those dates may differ. The correct one depends on the reporting rule—not on the timezone of whoever opened the page.
Avoid using new Date(timestamp).getDate() for a business date. getDate() uses the computer’s local timezone, so different users can get different answers.
A session opening such as “09:30 New York time” is a local schedule in a named zone. Don’t hardcode its UTC offset: New York alternates between standard time and daylight time, and timezone rules can change.
const sessionOpen = Temporal.ZonedDateTime.from({
year: 2026,
month: 11,
day: 2,
hour: 9,
minute: 30,
timeZone: "America/New_York",
});
const fillTime = Temporal.Instant.from("2026-11-02T14:45:00Z");
const isAfterOpen =
Temporal.Instant.compare(fillTime, sessionOpen.toInstant()) >= 0;
For recurring schedules, store the local time and timezone, then resolve the actual instant for each occurrence. For production scheduling, define how to handle local times that are ambiguous or nonexistent during daylight-saving transitions.
FX rollover conventions are often described using a New York wall-clock time, but the applicable convention can depend on the product and institution. Treat it as configured business data rather than assuming one UTC hour works year-round.
Back office: settlement and expiry
Settlement is a calendar calculation, not “add 48 hours to the fill.” It usually requires a market-specific business calendar that accounts for weekends and holidays.
function addBusinessDays(date, count, holidays) {
let result = date;
let remaining = count;
while (remaining > 0) {
result = result.add({ days: 1 });
const isWeekday = result.dayOfWeek <= 5;
const isHoliday = holidays.has(result.toString());
if (isWeekday && !isHoliday) remaining--;
}
return result;
}
const tradeDate = Temporal.PlainDate.from("2026-10-30");
const holidays = new Set(["2026-11-02"]);
const settlementDate = addBusinessDays(tradeDate, 2, holidays);
console.log(settlementDate.toString());
This is a simplified example: real settlement rules may depend on the instrument, currency, and relevant market calendars.
An option’s expiry date is also a calendar date. The last trading date may be different, and time-to-expiry for pricing should follow the conventions of the pricing model. Don’t substitute a rough millisecond calculation for those rules.
Crypto: UTC days and timestamp units
Crypto trades continuously, but reports still need a defined daily boundary. If the report uses UTC days, derive the bucket date in UTC:
const trade = Temporal.Instant.from("2026-10-26T23:47:00Z");
const utcDay = trade.toZonedDateTimeISO("UTC").toPlainDate();
console.log(utcDay.toString()); // "2026-10-26"
Another common integration bug is confusing seconds with milliseconds. Temporal has fromEpochMilliseconds(), but no fromEpochSeconds(). Convert seconds explicitly at the API boundary:
const timestampSeconds = 1_730_000_000;
const instant = Temporal.Instant.fromEpochMilliseconds(
timestampSeconds * 1_000
);
Normalize timestamps once when data enters your system, and keep the original unit clear in the adapter. Don’t pass a raw number to a date API without knowing whether it represents seconds, milliseconds, or another unit.
Tax lots: keep the questions separate
A fill instant records exactly when an execution happened. A PlainDate can represent a date used by a report or a user interface. Those values answer different questions.
const fillAt = Temporal.Instant.from("2025-03-15T23:47:00Z");
const reportedPurchaseDate = Temporal.PlainDate.from("2025-03-15");
console.log(fillAt.toString());
console.log(reportedPurchaseDate.toString());
Which date or timestamp governs a tax calculation depends on the relevant rules and jurisdiction. Don’t assume that a user-entered calendar date overrides the execution timestamp.
The same idea in Java, .NET, and JavaScript
These ecosystems make similar distinctions, but their APIs do not match one-to-one.
| Concept | Java | .NET | JavaScript |
|---|---|---|---|
| Exact moment | Instant |
DateTimeOffset (or UTC DateTime) |
Temporal.Instant; traditionally Date
|
| Date only | LocalDate |
DateOnly |
Temporal.PlainDate; traditionally a convention around Date
|
| Time only | LocalTime |
TimeOnly |
Temporal.PlainTime |
| Date and time without a zone | LocalDateTime |
DateTime with Kind.Unspecified
|
Temporal.PlainDateTime |
| Date and time in a named zone | ZonedDateTime |
Local date/time plus TimeZoneInfo
|
Temporal.ZonedDateTime |
| Elapsed time |
Duration or Period
|
TimeSpan |
Temporal.Duration |
Java has had the java.time API since Java 8. Joda-Time is a separate older library whose design helped establish this style of distinct date and time types. For new Java code, java.time is generally the standard choice. .NET date and time types · Java java.time · Joda-Time
In .NET, DateOnly, TimeOnly, DateTimeOffset, TimeSpan, and TimeZoneInfo cover related needs. A DateTimeOffset carries an offset, but an offset alone does not preserve a named zone’s daylight-saving rules. Keep the zone ID when future local schedules depend on those rules. .NET date and time guidance
Where date-fns fits
If your project already uses Date, date-fns provides convenient functions for operations such as adding days, comparing dates, and formatting. With date-fns v4, timezone-aware calculations can use @date-fns/tz and an explicit timezone context:
import { isSameDay } from "date-fns";
import { tz } from "@date-fns/tz";
const sameNewYorkDay = isSameDay(
"2026-10-26T23:30:00Z",
"2026-10-27T01:00:00Z",
{ in: tz("America/New_York") }
);
The timezone context tells date-fns which local calendar day to use for the comparison. For older date-fns versions, timezone support is commonly provided by the separate date-fns-tz package. Check the version and package documentation for your project. date-fns timezone support
Date-fns is useful when you want a function-based toolkit around Date. Temporal’s main advantage is that its types express the difference between an instant, a date, and a zoned schedule directly. Neither library chooses the business rule for you: your application still has to decide which timezone defines a trading day or settlement date.
Summary
The same distinction appears across languages, even though the type names and APIs differ:
-
Event happened at a moment: Java
Instant, JavaScriptTemporal.Instant, or .NETDateTimeOffset. -
Calendar date with no time: Java
LocalDate, JavaScriptTemporal.PlainDate, or .NETDateOnly. -
Local schedule in a named timezone: Java
ZonedDateTime, JavaScriptTemporal.ZonedDateTime, or .NET local date/time combined withTimeZoneInfo. -
Elapsed or calendar-based amount: Java
DurationorPeriod, JavaScriptTemporal.Duration, or .NETTimeSpan.
For existing Java systems that use Joda-Time, keep it where compatibility requires it; for new Java code, prefer java.time. In JavaScript, date-fns can make Date operations easier, and date-fns v4 adds timezone-aware operations through @date-fns/tz. Temporal goes further by giving different date and time meanings distinct types, where supported.
The portable rule is simple: represent the meaning you have—an instant, a calendar date, a local time, or a zoned schedule—and make conversions explicit. Then apply the business rule that defines the trading day, settlement, or expiry.
Top comments (0)