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
24x7sessions, are unaffected. - With several segments, the regular session is the one that starts latest before noon, so
0400-0930,0930-1600,1600-2000anchors at 09:30 rather than at 04:00.
@ortex-charts/financial@ortex-charts/mathThe 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.