ChartnautDocs

Sessions

ctx.session tells a script when a session opens and closes, so you key logic off session boundaries instead of clock arithmetic. Indicators and definitions share one API; studies have their own helpers for grouping events by time.

ctx.session(opts): sessions you define

Call it with the session you care about. It returns that session's state on the current bar.

export function onBar(ctx) {
  const s = ctx.session({
    period: "daily",
    open: { hour: 8, minute: 0 },
    close: { hour: 16, minute: 30 },
    tz: "Europe/London",
  });
  if (s.didOpen) {
    ctx.emit("london_open", { key: s.key }, { label: "London open", price: ctx.open });
  }
}

Option

Meaning

period

"daily", "weekly" or "monthly"

open / close

{ hour, minute? } wall-clock time inside each period. open defaults to the period start. Without close the session runs for the whole period. A close earlier than open runs overnight

tz

IANA time zone for open and close, DST-aware. Default "UTC"

custom: { startUtc, endUtc }

Instead of period: one explicit span in unix seconds

Field

Meaning

key

The session's start as an ISO timestamp string. Use it in oncePer keys and payloads, but do not parse it or compare it with text: use start for time

start / end

Unix seconds, end exclusive

isOpen

This bar is inside the session

didOpen

True on the first bar inside the session

didClose

True on the first bar at or after the session's end, or on the bar that rolls into the next session

prev

{ key, start, end } of the previous session, or null before the first one closes

Each distinct set of options is tracked separately, so call ctx.session with the same options on every bar. Gate on didOpen, isOpen and didClose. Never count bars to decide when a session ends: a missing bar or a different chart timeframe breaks the count.

didClose fires on a bar after the session, often the next session's first bar. On a rollover bar, finish the old session first, then reset, then add the current bar:

const st = ctx.accum("st", () => ({ hi: -Infinity }), (x) => x);
const s = ctx.session({ period: "daily" });
if (s.didOpen && s.prev) {
  ctx.emit("day_high", { dayKey: s.prev.key, high: st.hi }, { label: "Day high" }); // finish
}
if (s.didOpen) st.hi = -Infinity; // reset
if (s.isOpen) st.hi = Math.max(st.hi, ctx.high); // accumulate

A script that resets on a session boundary declares the longest gap between two resets as a length of time: warmup((w) => w.day()) for a daily session, warmup((w) => w.span("7d")) for a weekly one. See Warmup and memory.

ctx.session.isStart(mode): named boundaries

if (ctx.session.isStart("london")) {
  ctx.mark("ln_open", { price: ctx.open, text: "LN", position: "atPriceMiddle" });
}

True on the first bar of a new session of that mode. The times are fixed in UTC and do not move with daylight saving:

Mode

Boundary

"daily"

00:00 UTC

"weekly"

Monday 00:00 UTC

"monthly"

The 1st, 00:00 UTC

"london"

08:00 UTC

"newyork"

13:30 UTC

"tokyo"

00:00 UTC

"sydney"

22:00 UTC

"consistent"

Never: the whole history is one session

"newyork" is 13:30 UTC all year, so it is the New York cash open only while the US is on daylight time. For an exchange session that follows daylight saving, use ctx.session({ period: "daily", open, close, tz }).

In a mode: "once" indicator, ctx.at(i).session.isStart works; the callable ctx.session(opts) does not.

Studies: key, boundsOf, bucket and temporal

A study has no bars, so its helpers take a time:

Call

Returns

ctx.session.key(t)

The UTC day, "YYYY-MM-DD". A string, so no arithmetic on it

ctx.session.boundsOf(t)

{ start, end, key } of the UTC day containing t

ctx.session.bucket(t, mode)

"dow": "Mon" … "Sun"; "hour": 0 to 23; "halfHour": 0 to 47; "day": day of the year. All UTC

ctx.temporal.containing(t, { period, tz? })

{ start, end, key } of the "day", "week", "month" or "year" containing t, in tz (default UTC)

ctx.temporal.custom({ startUtc, endUtc })

An explicit span

ctx.temporal.forwardFrom(t, endUtc)

{ start, end } from t to endUtc

Use these in scope(ctx) to group events by session, and in indexedBy for tables and pins split that way. See Study API.

Common mistakes

  • Treating "newyork" as the exchange's regular hours all year.

  • Flooring ctx.time by 86,400 to find the day instead of calling ctx.session.

  • Emitting once per session without oncePer on the event: see Emit and labels.

  • Running session logic on weekly charts. Limit it with runOn.