Clock and sessions kit
The ./sdk/clock module turns the one number bar.time() gives a
wrun indicator, the bar's open time in UTC epoch seconds, into the questions a
trader asks of a bar: what hour is it in New York, is this the first bar
of the day, is the cash session open, which bar is this, how long is a
bar, when does it close.
A Clock answers the calendar questions in a
named time zone with its daylight rule applied; a Session answers
whether the bar sits inside an 0930-1600 window on the days you list,
when a session instance starts and when it has just closed. Both are
fed once per bar with update(t) and read as often as you like, and
neither allocates per bar. A MarketSession asks the market instead of a
zone: it reads the bar's trade date and session phase from the time
source (Market sessions, below). A higher timeframe built from the chart's
own bars or from a candles stream is the
Higher-timeframe kit.
The hand-rolled integer math they replace is still on
Time and sessions, the manual way,
if you want to see what the modules do for you.
Construct both classes in onStart(), never at module start: a bad zone or
a bad session spec aborts in the constructor, and an abort raised in
onStart() shows its message in the Console, while an abort at module start
loses it.
The clock
| Export | Signature | Answers |
|---|---|---|
Clock | new Clock(zone: string = "UTC") | a bar clock in one zone; an unknown zone aborts with Clock: unknown zone <x> |
update | clock.update(t: f64): void | folds the bar's open time (bar.time(), UTC epoch seconds); call once per bar |
offsetSec | clock. | the zone's offset from UTC for this bar in seconds, daylight time applied (New York reads -18000 in winter, -14400 in summer) |
hour, minute, second | clock.hour(): i32 | the local time of the bar's open: 0..23, 0..59, 0..59 (second is 0 on every candle grid) |
weekday | clock. | 0 Sunday .. 6 Saturday |
dayOfMonth, month, year, dayOfYear | clock.month(): i32 | 1..31, 1..12, the four-digit year, 1..366 |
dayKey | clock.dayKey(): i32 | local days since 1970-01-01: the same number on every bar of one local day |
weekKey | clock. | local weeks since Monday 1970-01-05, a monotonic count (weeks start on Monday) |
monthKey | clock. | year * 12 + (month - 1) |
quarterKey | clock. | year * 4 + (month - 1) / 3 in integer division: the same number on every bar of one local quarter (the year's own key is year()) |
isNewDay, isNewWeek, isNewMonth, isNewQuarter, isNewYear | clock. | true on the first bar of the period and on the very first update; a quarter opens on January, April, July or October 1st |
index | clock.index(): i32 | updates seen so far, 0-based: 0 on the first bar, -1 before any |
intervalSec | clock. | the smallest positive gap between consecutive bar opens seen so far (a weekend gap never widens it); NaN until two bars |
barCloseSec | clock. | t + intervalSec(): where this bar closes, NaN while the interval is unknown |
dayOfWeek | clock. | Pine's dayofweek: 1 Sunday .. 7 Saturday (weekday() + 1), the scale of the dayofweek.* constants |
weekOfYear | clock. | Pine's weekofyear, the ISO 8601 week: weeks run Monday to Sunday and belong to the year their Thursday is in, so week 1 holds January 4th, January 1st to 3rd can still be week 52 or 53 of the year before, and a late-December Monday can already be week 1; 0 before any update |
epochSec | clock. | Pine's timestamp(...) in the clock's zone, in seconds: the UTC instant of a wall-clock time with the daylight rule applied; the parts normalise as Pine's do (month 13, day 0, dayOfMonth + 1 past a month end, hour 24, negative parts) |
reset | clock.reset(): void | back to the freshly constructed state; the zone stays |
Two more names come with the clock, for a wall-clock time that needs no zone and for Pine's weekday constants:
| Export | Signature | Answers |
|---|---|---|
epochSec | epochSec(year, month, day, hour = 0, minute = 0, second = 0): f64 | the same instant read in UTC, with no clock: Pine's timestamp(, in seconds, the parts normalised the same way |
dayofweek | dayofweek. .. dayofweek. (i32, 1..7) | Pine's dayofweek.* constants, the scale dayOfWeek() answers on |
epochSec answers seconds because every time in a wrun indicator is
seconds (bar.time(), a param.time default, a drawing's x); Pine's
timestamp() answers milliseconds, so a value a script holds in Pine's
unit is this times 1000.0. A wall time inside a daylight gap (02:30 on
the morning the clocks go forward) moves forward by the gap, and one in
the repeated autumn hour reads as its first, daylight, occurrence: the
answers the Java calendar gives TradingView. Neither call allocates, so
clock.epochSec(clock.year(), clock.month(), clock.dayOfMonth(), 9, 30)
on every bar is fine.
Dates from 1970-01-01 to 2099-12-31 are valid, and the calendar is
integer math (days from civil and civil from days, no floating-point
days), so every field is exact. A non-finite t does not move the
clock: the fields keep their last values, the new-period flags read
false, the interval is not fed; only index() advances.
Zones and their daylight rules
| Zone | Offset (standard / daylight) | Daylight rule |
|---|---|---|
"UTC", "GMT", "Etc/UTC", "Etc/GMT" | +00:00 | none |
"America/ | -05:00 / -04:00 | US: from the second Sunday of March 02:00 local standard time to the first Sunday of November 02:00 local daylight time |
"America/ | -05:00 / -04:00 | US |
"America/ | -06:00 / -05:00 | US |
"America/ | -07:00 / -06:00 | US |
"America/ | -07:00 | none |
"America/ | -08:00 / -07:00 | US |
"America/ | -06:00 | none since 2023 |
"America/ | -05:00 | none |
"America/Lima" | -05:00 | none |
"America/ | -03:00 | none since 2020 |
"America/, "America/ | -03:00 | none since 2010 |
"Europe/London" | +00:00 / +01:00 | EU: from the last Sunday of March 01:00 UTC to the last Sunday of October 01:00 UTC |
"Europe/Dublin" | +00:00 / +01:00 | EU |
"Europe/Lisbon" | +00:00 / +01:00 | EU |
"Atlantic/ | +00:00 | none |
"Europe/Paris" | +01:00 / +02:00 | EU |
"Europe/ | +01:00 / +02:00 | EU |
"Europe/ | +01:00 / +02:00 | EU |
"Europe/Berlin" | +01:00 / +02:00 | EU |
"Europe/Zurich" | +01:00 / +02:00 | EU |
"Europe/Madrid" | +01:00 / +02:00 | EU |
"Europe/Rome" | +01:00 / +02:00 | EU |
"Europe/Vienna" | +01:00 / +02:00 | EU |
"Europe/ | +01:00 / +02:00 | EU |
"Europe/Oslo" | +01:00 / +02:00 | EU |
"Europe/ | +01:00 / +02:00 | EU |
"Europe/Prague" | +01:00 / +02:00 | EU |
"Europe/Warsaw" | +01:00 / +02:00 | EU |
"Europe/ | +01:00 / +02:00 | EU |
"Europe/ | +01:00 / +02:00 | EU |
"Europe/Malta" | +01:00 / +02:00 | EU |
"Europe/Monaco" | +01:00 / +02:00 | EU |
"Europe/Zagreb" | +01:00 / +02:00 | EU |
"Europe/ | +01:00 / +02:00 | EU |
"Europe/ | +01:00 / +02:00 | EU |
"Europe/ | +01:00 / +02:00 | EU |
"Europe/ | +01:00 / +02:00 | EU |
"Europe/ | +01:00 / +02:00 | EU |
"Europe/Skopje" | +01:00 / +02:00 | EU |
"Europe/Tirane" | +01:00 / +02:00 | EU |
"Europe/Vaduz" | +01:00 / +02:00 | EU |
"Europe/ | +01:00 / +02:00 | EU |
"Europe/ | +01:00 / +02:00 | EU |
"Europe/ | +01:00 / +02:00 | EU |
"Europe/ | +01:00 / +02:00 | EU |
"Europe/ | +02:00 / +03:00 | EU |
"Europe/Athens" | +02:00 / +03:00 | EU |
"Europe/ | +02:00 / +03:00 | EU |
"Europe/Sofia" | +02:00 / +03:00 | EU |
"Europe/Riga" | +02:00 / +03:00 | EU |
"Europe/ | +02:00 / +03:00 | EU |
"Europe/ | +02:00 / +03:00 | EU |
"Europe/Kyiv", "Europe/Kiev" | +02:00 / +03:00 | EU |
"Europe/ | +02:00 / +03:00 | EU (since 2022; Moldova switched at 00:00 UTC before) |
"Asia/Nicosia", "Europe/ | +02:00 / +03:00 | EU |
"Europe/ | +03:00 | none since 2017 |
"Europe/Moscow" | +03:00 | none since 2015 |
"Europe/Minsk" | +03:00 | none since 2012 |
"Africa/ | +02:00 | none |
"Africa/Cairo" | +02:00 / +03:00 | Egypt: from the last Friday of April 00:00 local standard time to the last Thursday of October 24:00 local daylight time (since 2023) |
"Africa/Lagos" | +01:00 | none |
"Africa/ | +03:00 | none |
"Asia/, "Asia/Tel_Aviv" | +02:00 / +03:00 | Israel: from the Friday before the last Sunday of March 02:00 local standard time to the last Sunday of October 02:00 local daylight time (since 2013) |
"Asia/Riyadh" | +03:00 | none |
"Asia/Dubai" | +04:00 | none |
"Asia/Tehran" | +03:30 | none since 2023 |
"Asia/Karachi" | +05:00 | none since 2010 |
"Asia/Kolkata", "Asia/Calcutta" | +05:30 | none |
"Asia/, "Asia/Katmandu" | +05:45 | none |
"Asia/Dhaka" | +06:00 | none since 2010 |
"Asia/Yangon", "Asia/Rangoon" | +06:30 | none |
"Asia/Bangkok" | +07:00 | none |
"Asia/Jakarta" | +07:00 | none |
"Asia/, "Asia/Saigon" | +07:00 | none |
"Asia/ | +08:00 | none |
"Asia/ | +08:00 | none |
"Asia/Manila" | +08:00 | none |
"Asia/ | +08:00 | none |
"Asia/Shanghai" | +08:00 | none |
"Asia/Taipei" | +08:00 | none |
"Asia/Seoul" | +09:00 | none |
"Asia/Tokyo" | +09:00 | none |
"Australia/ | +08:00 | none since 2010 |
"Australia/ | +09:30 | none |
"Australia/ | +09:30 / +10:30 | AU: from the first Sunday of October 02:00 local standard time to the first Sunday of April 03:00 local daylight time (it spans the new year; since 2008) |
"Australia/ | +10:00 | none |
"Australia/ | +10:00 / +11:00 | AU (since 2008) |
"Australia/ | +10:00 / +11:00 | AU (since 2008) |
"Australia/ | +10:00 / +11:00 | AU (since 2008) |
"Pacific/ | +12:00 / +13:00 | NZ: from the last Sunday of September 02:00 local standard time to the first Sunday of April 03:00 local daylight time (it spans the new year; since 2008) |
"+HH:MM", "-HH:MM", "+H:MM" | as written | none: a fixed offset written with a colon, such as "+05:30" or "+5:30", hours 00..23 (the bound the first Clock had, so "+15:00" still builds) |
"+H", "+HHMM" | as written | none: the same without a colon, such as "-3" or "+0530", at most 14:00 either way so a typo cannot pass as an offset |
"UTC+5:30", "GMT-4", "UTC+0530" | as written | none: the same offsets after a UTC or GMT prefix, the way Pine scripts spell them, with the same bounds ("UTC+15:30" builds, "UTC+15" aborts) |
"Etc/GMT+N", "Etc/GMT-N" | the sign inverted | none: the tz database's Etc zones, whole hours up to 14 with the sign inverted ("Etc/GMT+5" is -05:00) |
Every year in the valid range follows today's rule for its zone; where a
rule changed after 2007 the row says since when (Mexico City kept
daylight time until 2022, so a 2019 summer bar reads one hour off here),
and a zone's history before that year is not modelled. The table holds
every European capital with a zone of its own in the tz database,
Reykjavik to Nicosia and Chisinau (Pristina has none: Kosovo keeps
Belgrade's time, so "Europe/Belgrade" serves it). Anything else passed
as a zone aborts at construction with Clock: unknown zone <x>.
Sessions
| Export | Signature | Answers |
|---|---|---|
Session | new Session(spec: string, zone: string = "UTC", days: string = "0123456") | an "HHMM-HHMM" window in the zone's local time on the weekdays listed; a spec that is not HHMM-HHMM aborts with Session: bad spec <x>, a day character outside 0..6 with Session: bad days <x> |
update | session. | folds the bar's open time; call once per bar |
isIn | session.isIn(): bool | the bar's open sits inside the window on a listed day |
isFirst | session. | this is the first in-session bar of an instance |
closed | session. | an instance ended before this bar: true on the first update after it, whether this bar is outside or already inside the next instance |
key | session.key(): i32 | the dayKey of the instance's opening day while in, -1 outside |
reset | session. | forgets the running instance; the spec, zone and days stay |
The rules, written out:
- Half-open. A bar is in when
open <= local time of day < close, tested on the bar's open time."0930-1600"includes the 09:30 bar and excludes the 16:00 bar.open == closeis never in."2400"is accepted as a close, so"0000-2400"is the whole day. - Crossing midnight. A close before its open wraps:
"2200-0400"runs from 22:00 to 04:00 the next morning. Every bar of that window belongs to the day it opened on, so a Friday-night session keyed to Friday runs into Saturday morning. - The day digits.
dayslists the weekdays the session may OPEN on, as the same digitsweekday()uses:0Sunday ..6Saturday."12345"is Monday to Friday; a"2200-0400"session on"12345"never opens on Saturday or Sunday night. Pine's session day string counts Sunday as1, so its"23456"is"12345"here. - One instance per opening day.
key()is the localdayKeyof the opening day; it stays the same across a midnight crossing and changes when the next instance opens.isFirst()marks the first bar of an instance.closed()marks the first update after an instance ended: on a daily chart, where every bar is a new instance,closed()andisFirst()are bothtrueon every bar after the first.
Market sessions
Session is a window you define; MarketSession is the market's own
calendar, read from the two market facts the chart fills on every bar:
time.trade_date (the exchange trade date, epoch seconds at 00:00 UTC)
and time.session (1 regular, 2 pre-market, 3 after-hours, 0
closed). A CME evening bar already belongs to the next trade date, a US
stock bar is pre-market, regular or after-hours by the New York clock,
and a market without sessions is regular on every bar with the UTC date
as its trade date (Data sources).
| Export | Signature | Answers |
|---|---|---|
MarketSession | new MarketSession() | the market's trade date and session phase, bar by bar |
update | ms. | folds time.trade_date and time.session; call once per bar |
isRegular, isPremarket, isAfterHours | ms.isRegular(): bool | the bar's phase: regular, pre-market, after-hours |
isExtended | ms. | pre-market or after-hours |
isClosed | ms.isClosed(): bool | the market is closed on this bar (any session value other than 1, 2, 3) |
isNewTradingDay | ms. | the first bar of a trade date, and the very first update |
is | ms. | the first regular bar of a trade date, after its pre-market bars |
tradingDayKey | ms. | the trade date as days since 1970-01-01 |
tradingWeekKey | ms. | weeks since Monday 1970-01-05 of the trade date: one number from a Monday trade date through its Sunday |
tradingMonthKey | ms. | year * 12 + (month - 1) of the trade date |
reset | ms.reset(): void | back to the freshly constructed state |
The warm-up rule is the Clock's: the very first update is a new trading
day, and the first regular bar seen is a first regular bar even when the
loaded history starts inside a session. A non-finite trade date or
session moves nothing (the phase and keys keep their values, both flags
read false); before the first update every check reads false and the
keys read 0. The checks map one to one onto Pine's session.*
variables (Pine equivalents).
A New York session filter
The close, drawn only while the cash session is open, with a data-only flag for the session and one for its first bar. The chart's bars are in UTC; the session is in New York time, daylight rule included, so the window moves with the clocks in March and November without any change to the file.
output("cash_close", line, overlay, { color: "#2563eb", width: 2, description: "The close while the New York cash session is open, NaN outside" });
output("session_open", none, overlay, { description: "1 from 09:30 to 16:00 New York time on a weekday" });
output("session_start", none, overlay, { description: "1 on the first bar of each cash session" });
let cash = new Session("0930-1600");
function onStart(): void {
// Constructed here so a bad spec or zone reports in the Console.
cash = new Session("0930-1600", "America/New_York", "12345");
}
function onBar(): void {
const close = bar.close();
cash.update(bar.time());
const inSession = cash.isIn();
const first = cash.isFirst();
out_cash_close(inSession ? close : NaN);
out_session_open(inSession ? 1.0 : 0.0);
out_session_start(first ? 1.0 : 0.0);
}session_open is the gate a mark or an alert reads; session_start is
where an anchored VWAP or a session high-low box begins.
A new-day reset of a running sum
Volume accumulated since the New York day started, and since the week
started, with the bar's position inside the day. isNewDay() is true
on the first bar after local midnight and on the very first bar, so the
sums start clean at both; isNewWeek() flips on the first bar of
Monday.
output("day_volume", histogram, lower, { color: "#2563eb", description: "Volume since the New York day started" });
output("week_volume", line, lower, { color: "#f59e0b", description: "Volume since the week started (Monday, New York time)" });
output("bars_today", none, lower, { description: "Bars since the day started, 0 on its first bar" });
let clock = new Clock();
let dayVolume: f64 = 0.0;
let weekVolume: f64 = 0.0;
let barsToday: i32 = 0;
function onStart(): void {
clock = new Clock("America/New_York");
}
function onBar(): void {
clock.update(bar.time());
if (clock.isNewDay()) {
dayVolume = 0.0;
barsToday = 0;
} else {
barsToday += 1;
}
if (clock.isNewWeek()) weekVolume = 0.0;
const volume = bar.volume();
dayVolume += volume;
weekVolume += volume;
if (isNaN(bar.close())) return;
out_day_volume(dayVolume);
out_week_volume(weekVolume);
out_bars_today(f64(barsToday));
}The same shape resets on isNewMonth(), isNewQuarter() or
isNewYear(), or compares dayKey() with a stored value when the
period is yours to define.
An interval-aware bar-close estimate
The module never sees a wall clock, but it sees the gaps between bar
opens: intervalSec() is the smallest positive gap so far, so after two
bars it is the chart interval, and a weekend gap never stretches it.
barCloseSec() adds that interval to the open. A second Clock fed the
close time answers what hour the bar closes in, which is how a file
knows that the 15:30 bar on a 1-hour chart runs past the cash close.
output("bar_close", none, overlay, { description: "The bar's close time in epoch seconds, NaN until the interval is known" });
output("bar_minutes", none, lower, { description: "The chart interval in minutes, from the smallest gap seen" });
output("close_hour", none, lower, { description: "The New York hour the bar closes in, NaN on the first bar" });
let opens = new Clock();
let closes = new Clock();
function onStart(): void {
opens = new Clock("America/New_York");
closes = new Clock("America/New_York");
}
function onBar(): void {
opens.update(bar.time());
// NaN on the first bar: a non-finite time does not move a clock.
closes.update(opens.barCloseSec());
if (isNaN(bar.close())) return;
const interval = opens.intervalSec();
out_bar_close(opens.barCloseSec());
out_bar_minutes(interval / 60.0);
out_close_hour(isNaN(interval) ? NaN : f64(closes.hour()));
}index() on the same clock is the bar index, 0 on the first bar the
chart loaded; it counts every update, so it restarts after reset().
Calendar questions
Pine's calendar calls answer on the same clock: a wall-clock time as a timestamp, the quarter, the ISO week and the weekday. The price at the New York cash open held through the day, the open of the running quarter, and two data-only readings:
// The price at the New York cash open, held through the day; the open of the running quarter; the ISO week and a Monday flag.
output("cash_open", line, overlay, { color: "#2563eb", width: 2, description: "The open of the bar that holds 09:30 New York time, held for the rest of the day" });
output("quarter_open", line, overlay, { color: "#94a3b8", line_style: "dashed", description: "The open of the running quarter, New York time" });
output("iso_week", none, lower, { description: "The ISO 8601 week of the bar, 1 to 53" });
output("monday", none, lower, { description: "1 on a Monday in New York, else 0" });
let clock = new Clock();
let cashOpen: f64 = NaN;
let quarterOpen: f64 = NaN;
function onStart(): void {
clock = new Clock("America/New_York");
}
function onBar(): void {
const t = bar.time();
clock.update(t);
if (clock.isNewDay()) cashOpen = NaN;
if (clock.isNewQuarter()) quarterOpen = bar.open();
// Today's 09:30 in New York as UTC epoch seconds, daylight time applied.
const cashOpenSec = clock.epochSec(clock.year(), clock.month(), clock.dayOfMonth(), 9, 30);
// The bar that holds 09:30 sets the level. The interval is NaN on the first bar, so nothing is set there.
if (t <= cashOpenSec && cashOpenSec < t + clock.intervalSec()) cashOpen = bar.open();
out_cash_open(cashOpen);
out_quarter_open(quarterOpen);
out_iso_week(f64(clock.weekOfYear()));
out_monday(clock.dayOfWeek() == dayofweek.MONDAY ? 1.0 : 0.0);
}- A wall-clock time.
clock.epochSec(year, month, day, 9, 30)is that day's 09:30 in New York as UTC epoch seconds: 14:30 UTC in winter, 13:30 UTC in summer. The bar that holds it sets the level, so on a 1-hour chart the line starts at the 09:00 New York bar. - The quarter.
isNewQuarter()is true on the first bar of January, April, July and October, so the open is taken there. The first quarter on the chart starts at the first loaded bar.quarterKey()holds one number for the whole quarter, for a reset you compare yourself. - The week.
weekOfYear()is Pine'sweekofyear, the ISO week: Monday to Sunday, and week 1 holds January 4th. - The weekday.
dayOfWeek()counts 1 Sunday to 7 Saturday, as Pine does, so it compares withdayofweek.MONDAY.weekday()counts from 0.
From Pine
One-to-one where the meaning is the same; the zone is the difference,
since Pine reads the chart's exchange time and a Clock reads the zone
you name.
| Pine | Here |
|---|---|
hour, minute, second | clock.hour(), clock.minute(), clock.second() |
dayofweek (1 Sunday .. 7 Saturday), dayofweek. | clock., dayofweek. (clock.weekday() counts the same day from 0) |
weekofyear | clock.: the ISO 8601 week, Monday to Sunday (TradingView's own week-of-month example shows a Sunday-evening open still in the week before, and PineTS implements the ISO number) |
timestamp(year, month, dayofmonth, 9, 30) | clock., in seconds (* 1000.0 where a script holds Pine's milliseconds) |
timestamp( | ny. on a Clock built in that zone |
timestamp(, or timestamp(2024, 1, 15, 9, 30) on a 24/7 venue (its exchange zone is UTC) | epochSec(2024, 1, 15, 9, 30); a literal date the user may change is a param.time default |
dayofmonth, month, year | clock., clock.month(), clock.year() |
timeframe., ("W"), ("M"), ("3M"), ("12M") | clock., isNewWeek(), isNewMonth(), isNewQuarter(), isNewYear() in the zone you name; a PeriodLevels built from the same word answers the same with isNew() (Levels kit) |
bar_index | clock.index() |
not na( | session.isIn() with new Session( |
time_close | clock. (in seconds, from the observed interval) |
session., session., time_tradingday | MarketSession over time.session and time.trade_date (Market sessions, above) |