Skip to content

Data

Time, sessions and resolutions

Time zones, trading sessions, session-anchored intraday bars and aggregation.

Timestamps Are Milliseconds, UTC

Every timestamp in the library — bars, ticks, events, drawings, alerts, ranges — is milliseconds since the Unix epoch in UTC. There is no other convention anywhere and no per-series exception.

A time zone never changes a timestamp. It decides two things: where bar boundaries fall when the library builds or aggregates bars itself, and how the axis, crosshair and legend labels read.

const chart = createFinancialChart(el, {
  timeZone: "America/New_York",   // an IANA zone name
  locale: "en-GB",                // formatting locale for dates and numbers
  resolution: "1D",
});

Zone handling goes through Intl, so any IANA zone the browser knows works, daylight saving transitions are handled by the platform rather than by an offset table, and there is no time zone database in the bundle.

If your feed sends seconds, multiply by 1,000 before handing the rows over. If it sends YYYY-MM-DD for daily bars, the adapter layer has a date time unit that parses it as midnight in a named zone, which is almost always what you want for daily data.

Resolutions

A resolution is written in datafeed notation: a number of minutes, or a number with a D, W or M suffix.

Text Meaning
1, 5, 15, 30 Minutes.
60, 240 Minutes that happen to be whole hours; parsed as hours.
1H, 4H Hours, normalized to the minute form.
1D, 3D Days.
1W Weeks.
1M, 3M Months.
import { parseResolution, floorTimeToBar, nextBarTime, sameBar } from "@ortex-charts/math";

const res = parseResolution("15");
// { text: "15", unit: "minute", multiple: 15, isIntraday: true, durationMs: 900000 }

floorTimeToBar(Date.now(), res, "America/New_York");   // start of the current bar
nextBarTime(Date.now(), res, "America/New_York");      // start of the next one
sameBar(a, b, res, "America/New_York");                // do two times share a bar

durationMs is nominal: calendar units use one day, seven days and 30 days respectively, because a month is not a fixed length. Bar boundaries themselves are computed with calendar arithmetic in the chart time zone, not by dividing by durationMs.

The chart resolution matters in three places: it is what live ticks are folded into, it is what the axis labels are chosen for, and it is what a datafeed binding requests. Set it in options, or let the datafeed binding set it when the user changes it.

Sessions

A session string describes trading hours as local wall-clock minutes, with commas between segments and an optional day list after a colon.

0930-1600                       regular US equity hours
0400-0930,0930-1600,1600-2000   with pre-market and after-hours
24x7                            continuous, for crypto
0930-1600:12345                 Monday to Friday only (the default anyway)
1700-1600                       an overnight session that wraps midnight
import { parseSession, isInSession, sessionBoundsForDay, sessionMinuteAt } from "@ortex-charts/math";

const session = parseSession("0400-0930,0930-1600,1600-2000");
isInSession(Date.now(), session, "America/New_York");             // boolean
sessionBoundsForDay(Date.now(), session, "America/New_York");     // [[start, end], …] in ms
sessionMinuteAt(Date.now(), session, "America/New_York");         // minutes elapsed in the session, NaN when closed

Days are 0 for Sunday through 6 for Saturday, and the default is Monday to Friday. Adjacent segments are merged when parsed, so 0930-1200,1200-1600 becomes one segment.

The chart takes the session as an option, and the datafeed binding sets it for you from SymbolInfo.session when a symbol resolves.

createFinancialChart(el, { timeZone: "America/New_York", resolution: "60", session: "0930-1600" });

Session-Anchored Intraday Bars

This is the part that is different from most libraries, and it fixes a long-standing complaint about hourly candles.

If hourly bars are floored to the hour, a US equity session produces a first candle covering only 09:30 to 10:00 and then a run of clean hours. The half-hour stub is an artifact of the arithmetic, not of the market.

When the chart knows the session, intraday resolutions are phased from the session open instead. With 0930-1600, 60-minute bars run 09:30–10:30, 10:30–11:30 and so on, and pre-market bars keep the same phase going backwards, so 08:30–09:30 lines up with the rest.

import { anchoredResolution, parseResolution } from "@ortex-charts/math";

const res = anchoredResolution(parseResolution("60"), "0930-1600");
// { …, anchorMinute: 570 }   // 09:30 expressed in minutes from midnight

Four things worth being precise about:

  • The anchor applies to everything the library builds or aggregates itself: ticks folded into bars by streamTo, aggregateBars, and the higher-timeframe followers in bar replay.
  • Bars delivered ready-made by a feed are shown exactly as given. The library does not re-cut somebody else's candles.
  • Daily and longer resolutions, and 24x7 sessions, are unaffected.
  • With several segments, the regular session is the one that starts latest before noon, so 0400-0930,0930-1600,1600-2000 anchors at 09:30 rather than at 04:00.
Session-anchored intraday@ortex-charts/financial@ortex-charts/math
Loading session-anchored intraday
Hourly bars that start at the 09:30 open rather than at 10:00, with pre-market and after-hours kept in phase.

Open this example with its source

The Time Axis

The axis is indexed by bar position, so closed periods do not exist on it. There is no gap to hide, no breaks configuration and no special case for holidays: a day the market did not trade simply has no bars.

Tick labels are chosen by weight rather than by a fixed interval, so a year label beats a month label, a month beats a day and a day beats an hour. When there is not room for everything, the more significant labels survive, which is why a two-year daily chart reads as years and months rather than as a row of collided dates.

chart.applyOptions({
  timeScale: {
    barSpacing: 8,          // pixels per bar
    minBarSpacing: 0.5,
    maxBarSpacing: 60,
    rightOffsetPx: 60,      // empty space right of the last bar
    fixLeftEdge: false,     // stop scrolling past the first bar
    fixRightEdge: false,
    followRealtime: true,   // follow new bars while the view is at the right edge
    borderVisible: true,
    visible: true,
  },
  timeLabelGap: 72,         // minimum pixels between time labels
});

followRealtime is what makes a live chart scroll with new bars only while the user is already at the right edge. Scrolling back into history stops the follow until scrollToRealtime() is called or the user pans back.

Working With Ranges

chart.timeScale.setVisibleRange(fromIndex, toIndex);
chart.timeScale.setVisibleTimeRange(fromTime, toTime);
chart.timeScale.fitContent();
chart.timeScale.scrollToRealtime();
chart.timeScale.setBarSpacing(12);

chart.timeScale.timeToCoordinate(time);
chart.timeScale.coordinateToTime(px);
chart.timeScale.timeToIndex(time);
chart.timeScale.indexToTime(index);

const off = chart.timeScale.subscribeVisibleRangeChange((r) => {
  console.log(r.fromIndex, r.toIndex, r.fromTime, r.toTime, r.barSpacing);
});

Index ranges are exact and time ranges are converted to them, so setVisibleRange is the right call when you know how many bars you want on screen and setVisibleTimeRange is the right call when you are synchronizing with something that thinks in dates.

Range presets — 1D, 5D, 1M, 3M, 6M, YTD, 1Y, 5Y, All — are not built into the toolbar. They are a few lines on top of setVisibleTimeRange, and adding them as toolbar actions is the usual approach:

const RANGES = { "1M": 30, "3M": 91, "6M": 182, "1Y": 365, "5Y": 1826 };

function applyRange(days: number) {
  const to = chart.main.rawTimes[chart.main.rawLength - 1];
  chart.chart.timeScale.setVisibleTimeRange(to - days * 86_400_000, to);
}

Aggregating Bars Yourself

When you have fine bars and want coarse ones without another request:

import { aggregateBars, anchoredResolution, barSeriesFromRows, barSeriesToRows, parseResolution } from "@ortex-charts/math";

const minute = barSeriesFromRows(minuteBars);
const hourly = aggregateBars(minute, anchoredResolution(parseResolution("60"), "0930-1600"), "America/New_York");

chart.setData(barSeriesToRows(hourly));

Aggregation carries buyVolume and sellVolume through when the source has them, so an order-flow chart keeps exact delta after aggregation.

Formatting Time Yourself

import { formatBarTime, formatClock, formatDate } from "@ortex-charts/math";

formatBarTime(time, { timeZone: "America/New_York", resolution: parseResolution("15"), locale: "en-US" });
formatClock(time, { timeZone: "America/New_York" });
formatDate(time, { timeZone: "America/New_York" });

formatBarTime picks the shape from the resolution, which is what the crosshair label uses: a date for daily bars, a date and a time for intraday ones. All three go through Intl, so locale controls the ordering and the month names.

There is no string table for the rest of the interface. The toolbar, menus and dialogs in @ortex-charts/ui are English only today; dates and numbers localize, labels do not.