DEV Community

Cover image for Your Trade Is on the Wrong Day: It’s a Type Error, Not a Timezone Bug
vladimir ivanov
vladimir ivanov

Posted on AI-assisted

Your Trade Is on the Wrong Day: It’s a Type Error, Not a Timezone Bug

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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") }
);
Enter fullscreen mode Exit fullscreen mode

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, JavaScript Temporal.Instant, or .NET DateTimeOffset.
  • Calendar date with no time: Java LocalDate, JavaScript Temporal.PlainDate, or .NET DateOnly.
  • Local schedule in a named timezone: Java ZonedDateTime, JavaScript Temporal.ZonedDateTime, or .NET local date/time combined with TimeZoneInfo.
  • Elapsed or calendar-based amount: Java Duration or Period, JavaScript Temporal.Duration, or .NET TimeSpan.

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)