Time and sessions

Read the current bar's clock and trading session from the time source: hour, day of week, day of month, month, year, and session predicates, all in UTC, all as…

Read the current bar's clock and trading session from the time source: hour, day of week, day of month, month, year, and session predicates, all in UTC, all as small functions you paste into the file. kScript (legacy) exposed them as builtins (hour(), dayOfMonth(), isAsianSession()); an Indicator reads one number per bar, the bar's open time in epoch seconds, and derives everything from it with integer math.

Introduction

Every bar happens at a moment in time, and that moment is often a signal in itself: London opening, the Asian session winding down, a fresh day, a weekend gap. An Indicator gets that moment from one declaration:

input("bar_t", time.bar_open_sec);   // the bar's open time, seconds since 1970-01-01 00:00 UTC

Three things to keep in mind:

  • It is seconds, not milliseconds. kScript's time() returned milliseconds; bar_open_sec is seconds. Multiply by 1000 for a millisecond value, and keep the math in i64 (a 32-bit integer cannot hold an epoch second past 2038 comfortably, and it cannot hold milliseconds at all).
  • It is the bar's open. Not the close, not "now". There is no wall clock inside the module (timenow() has no counterpart): the newest bar is the live edge, and the host tells you nothing about the clock on the wall.
  • Times are UTC. Every clock value and every session boundary below is in UTC. There is no local-timezone surprise, but it does mean you compare against UTC hours when you write a rule, and daylight-saving shifts in a venue's local session are yours to encode.

Lead example: the clock and the session

This file computes every clock value from the bar's open time, classifies the bar into a session, and tints the close line by session so you can sanity-check the math against the chart's time axis.

import { input, line, lower, none, ohlcv, output, overlay, time } from "./sdk/declare";
import { in_bar_t, in_close } from "./gen/inputs";
import { emitRow, out_close, out_day_of_month, out_day_of_week, out_hour, out_month, out_session, out_year } from "./gen/outputs";

input("close", ohlcv.close);
input("bar_t", time.bar_open_sec);
// The close, tinted by session: 0 off-hours grey, 1 Asia blue, 2 Europe amber, 3 America green.
output("close", line, overlay, { width: 2, color_by: "session", colors: ["#94a3b8", "#3b82f6", "#f59e0b", "#22c55e"], description: "Close, colored by the session the bar opened in" });
output("session", none, overlay, { description: "0 off-hours, 1 Asian, 2 European, 3 American (UTC)" });
output("hour", line, lower, { color: "#2563eb", description: "Hour of the bar, 0 to 23 UTC" });
output("day_of_week", line, lower, { color: "#7c3aed", description: "0 Sunday to 6 Saturday" });
output("day_of_month", line, lower, { color: "#16a34a", description: "Day of the month, 1 to 31" });
output("month", line, lower, { color: "#f59e0b", description: "Month, 1 to 12" });
output("year", line, lower, { color: "#94a3b8", description: "Four-digit year" });

// ---- Clock helpers over epoch seconds (UTC). Paste these into any file. ----
const DAY: i64 = 86400;

function hourOf(t: i64): i32 {
  return i32((t % DAY) / 3600);
}

function minuteOf(t: i64): i32 {
  return i32((t % 3600) / 60);
}

function dayIndex(t: i64): i64 {
  return t / DAY; // days since 1970-01-01
}

function dayOfWeek(t: i64): i32 {
  return i32((dayIndex(t) + 4) % 7); // 1970-01-01 was a Thursday: 0 Sunday .. 6 Saturday
}

// Civil date from a day index (integer math only; exact for every date after 1970).
let civilYear: i32 = 0;
let civilMonth: i32 = 0;
let civilDay: i32 = 0;

function civilFromDays(days: i64): void {
  const z = days + 719468;
  const era = (z >= 0 ? z : z - 146096) / 146097;
  const doe = z - era * 146097;
  const yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
  const y = yoe + era * 400;
  const doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
  const mp = (5 * doy + 2) / 153;
  const d = doy - (153 * mp + 2) / 5 + 1;
  const m = mp < 10 ? mp + 3 : mp - 9;
  civilYear = i32(m <= 2 ? y + 1 : y);
  civilMonth = i32(m);
  civilDay = i32(d);
}

// ---- Sessions (UTC). Edit the boundaries to taste; they are ordinary comparisons. ----
function isAsianSession(h: i32): bool {
  return h >= 0 && h < 8;
}

function isEuropeanSession(h: i32): bool {
  return h >= 7 && h < 16;
}

function isAmericanSession(h: i32): bool {
  return h >= 13 && h < 21;
}

function currentSession(h: i32): f64 {
  if (isAmericanSession(h)) return 3.0; // overlaps resolve to the later session
  if (isEuropeanSession(h)) return 2.0;
  if (isAsianSession(h)) return 1.0;
  return 0.0;
}

let close: f64 = NaN;
let hour: i32 = 0;
let dow: i32 = 0;
let session: f64 = 0.0;

export function init(): void {}

export function state(): i32 {
  close = in_close();
  const t = i64(in_bar_t());
  hour = hourOf(t);
  dow = dayOfWeek(t);
  civilFromDays(dayIndex(t));
  session = currentSession(hour);
  return 1;
}

export function finalize(): void {
  out_close(close);
  out_session(session);
  out_hour(f64(hour));
  out_day_of_week(f64(dow));
  out_day_of_month(f64(civilDay));
  out_month(f64(civilMonth));
  out_year(f64(civilYear));
  emitRow();
}

export function reset(): void {
  close = NaN;
  hour = 0;
  dow = 0;
  session = 0.0;
  civilYear = 0;
  civilMonth = 0;
  civilDay = 0;
}

Each session predicate is a plain bool over the hour, so it drops straight into an if or a gate. The close line picks up a blue, amber, or green tint depending on which session the bar opened in, and the lower pane shows the raw clock values so you can check them against the axis.

Function reference

Every helper takes the bar's open time as an i64 of epoch seconds and returns the value for the current bar.

Clock

kScriptHelperReturns
hour()hourOf(t)Hour of the bar, 0..23 (UTC)
minute()minuteOf(t)Minute of the bar, 0..59
second()i32(t % 60)Second of the bar, 0..59 (always 0 on candle grids)
dayOfWeek()dayOfWeek(t)0 Sunday .. 6 Saturday (kScript's returned na; this one works)
dayOfMonth()civilDay after civilFromDays(dayIndex(t))Day of the month, 1..31
month()civilMonthMonth, 1..12
year()civilYearFour-digit year

civilFromDays is the standard days-to-civil algorithm in integer math, exact for every date from 1970 onward; it writes three module-level variables because a function returns one value. Prefer dayIndex(t) for "is this a new day" tests; the civil fields are for calendar rules and labels.

Timestamps

kScriptIndicator
time() (milliseconds)in_bar_t() (seconds); i64(in_bar_t()) * 1000 for milliseconds
timenow()none: no wall clock reaches the module. The newest bar is the live edge, and a run-level renderer or drawing evaluates it on its own

The bar's interval is the difference between consecutive open times: keep the previous bar_t in a module-level variable and subtract.

Sessions

kScriptHelperReturns
isAsianSession()isAsianSession(h)true in the Asian session (00:00 to 08:00 UTC in the example)
isEuropeanSession()isEuropeanSession(h)true in the European session (07:00 to 16:00 UTC)
isAmericanSession()isAmericanSession(h)true in the American session (13:00 to 21:00 UTC)
currentSession()currentSession(h)0 off-hours, 1, 2, 3, as a number an output or a color_by ladder can carry

The boundaries are ordinary comparisons in your file, so they are yours: kScript fixed them in the engine, an Indicator states them where you can read and change them. Sessions overlap (London and New York share the 13:00 to 16:00 UTC hours), which is why the predicates are independent and currentSession picks one by priority.

Common patterns

Only signal during a session. Wrap your trigger in a session predicate so it can only fire when the market you care about is active. The predicate becomes a data-only gate and the mark's shape_where:

const brokeOut = close > prevHigh && isEuropeanSession(hour);
// finalize():
out_london_break(high);                          // a shape output at the bar's high
out_is_london_break(brokeOut ? 1.0 : 0.0);       // its shape_where gate

Gate by session instead of weekday. dayOfWeek(t) works here, so both are available: dow >= 1 && dow <= 5 is weekdays, and isEuropeanSession(hour) || isAmericanSession(hour) is "skip the quiet Asian hours." Combine them freely.

Once-per-day reset. Detect a new day by watching the day index change from the previous bar, then reset whatever daily accumulator you keep. The cookbook's anchored VWAP and key levels are built on exactly this:

const day = dayIndex(t);
if (day != dayIdx) {
  // the day that just closed is complete; the running sums start over
  dayIdx = day;
  dayPv = 0.0;
  dayVol = 0.0;
}

The same idea works for a new week ((dayIndex(t) + 3) / 7 changes; the + 3 makes weeks start on Monday 00:00 UTC), a new month (civilMonth changes), or a new year (civilYear changes). Hold the previous index in a module-level variable so you can compare, and remember that a period already running when the loaded history starts is incomplete: keep its values NaN until the first boundary passes.

Weekend gaps. Crypto trades through; a venue with a session calendar simply has no bars off-hours, and the time source follows the primary grid, so there is nothing to skip. The bucket fold that turns these day and hour indexes into higher-timeframe views is multi-timeframe.md; the per-bar background tint that replaces barcolor() is render.bgcolor (functions/plotting.md).