---
title: "Session HUD"
description: "Where the day stands, in one card at the bottom of the price pane, drawn in the stage look: a green gradient card with the chart's base asset (BTC on a BTC…"
order: 96
section: "cookbook"
---

<!-- source: docs/indicators/cookbook/session-hud.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Session HUD

Where the day stands, in one card at the bottom of the price pane, drawn in the stage look: a green gradient card with the chart's base asset (BTC on a BTC chart) on a cover square beside the title, and a bright green for the headline and every fill. It names the session trading now as the headline (Asia, London, New York or Closed), how much of that session has passed on a track bar, and the session up next with a countdown ("London in 2 h 5 min"); then how much of an average day's range today has already used, on a split bar that runs to one and a half average days; where the close sits inside today's range, on a track from the low (0) to the high (100); how far the close is from the day's VWAP; and how far it is from the prior day's high and low, in percent. On price, the day's VWAP draws in sky, starting again each day, and the prior day's high and low run as dotted slate lines from today's first bar to the right edge.

The parts are three `param.session` windows read with `inSession` and a `Clock` per zone ([Sessions and units](../settings/sessions-and-units.md), [Clock and sessions kit](../functions/time-and-sessions-kit.md)), the bar's `time.trade_date` for the day ([Data sources](../core-concepts/data-sources.md)), a `candles` stream of daily candles kept by a `CandleList` for the average daily range ([Multi-timeframe](../core-concepts/multi-timeframe.md), [Higher-timeframe kit](../functions/higher-timeframe-kit.md)), a line output for the VWAP ([Plotting](../presentation/plotting.md)), two line handles for the prior day ([Drawing objects](../presentation/drawing-objects.md#handles)), and a two-column `render.hud` card with `look: "stage"` of two pills, two gauges, a meter, a value and a rows tile ([HUD cards](../presentation/hud-and-hover-cards.md#hud-cards), [Looks](../presentation/hud-and-hover-cards.md#looks), [Blocks and tiles](../presentation/hud-and-hover-cards.md#blocks-and-tiles)). This is also the `session-hud` template: the **Session HUD** card under **HUDs** in the editor's starter list, and it compiles as written.

## The wrun indicator

```typescript sample=session-hud
// Session HUD: where the day stands, in one card at the bottom of the price pane, drawn by the chart in the stage
// look. The trader sees the session trading now (Asia, London, New York, or Closed) as the headline beside the
// chart's coin as a cover, how much of that session has passed as a track bar, which session opens next and when,
// how much of an average day's range today has already used, where the close sits inside today's range, and how far
// the close is from the day's VWAP and from the prior day's high and low (the minutes left ride an output). On price: the day's VWAP in sky, and the prior
// day's high and low as dotted slate lines across today. Every market with candles: crypto, stocks and forex.

// Settings: the three session windows, each in its own zone (daylight saving follows the zone, bar by bar), and the
// number of days in the average daily range.
section("Sessions");
param.session("asia", "00:00-08:00", { tz: "UTC", label: "Asia" });
param.session("london", "07:00-16:00", { tz: "UTC", label: "London" });
param.session("new_york", "13:30-20:00", { tz: "UTC", label: "New York" });
section("Range");
param.int("adr_days", 14, { min: 2, max: 30, label: "Days in average range", description:"Days in the average daily range that today's range is measured against" });
chart.interval_sec(); // the chart's bar interval in seconds, written by the chart: a 4h or coarser chart gets words instead
input("close", ohlcv.close); // the chart's own candles: the grid
input("trade_date", time.trade_date); // the bar's trade date, one number per day: the UTC date on crypto and forex, the New York date on US stocks
input("days", candles.cells, { interval: "1d", bars: 30, description: "The newest 30 closed daily candles: the average daily range and the prior day" });

// On price: the day's VWAP, starting again with each trade date.
output("vwap", line, overlay, { color: "#38bdf8", width: 1, label: "Day VWAP", format: "price", description: "The day's volume-weighted average price; on a market without volume every bar weighs the same" });
// The readings, data-only and written on every bar: the card's tiles read the newest one, the Console reads them all.
output("session_now", none, overlay, { description: "The session trading now: 0 closed, 1 Asia, 2 London, 3 New York (the later one where two overlap)" });
output("session_done", none, overlay, { format: "0", description: "Percent of the session trading now that has passed; NaN while no session trades" });
output("minutes_left", none, overlay, { description: "Minutes from the bar's open to the session's close; NaN while no session trades" });
output("range_used", none, overlay, { format: "0", description: "Today's high-low range as a percent of the average daily range" });
output("range_position", none, overlay, { description: "Where the close sits in today's range: 0 at the low, 100 at the high" });
output("vs_vwap", none, overlay, { color: "#38bdf8", description: "Close against the day's VWAP, percent" }); // a row's dot takes its output's colour: sky like the line
output("vs_prior_high", none, overlay, { color: "#94a3b8", description: "Close against the prior day's high, percent" });
output("vs_prior_low", none, overlay, { color: "#94a3b8", description: "Close against the prior day's low, percent" });
string("up_next", { max_bytes: 32 }); // the next session to open and when, written on the live bar
string("session", { max_bytes: 32 }); // the card's first tile: the session's name, or why there is none
handles.line({ color: "#94a3b8", width: 1, lineStyle: "dotted", extend: "right" }); // the prior day's high and low, from today's first bar to the right edge

// The card, in the stage look (the chart's coin as a cover, a heavy headline, track bars, one green accent): two
// columns at the bottom centre of the pane, clear of the legend at the top left, the price axis and its tags on the
// right, and the floating button at the bottom left. The session's name leads as the headline; every tile reads
// the newest row.
render.hud("session", {
  position: "bottom_center",
  look: "stage",
  title: "Session",
  columns: 2,
  tiles: [
    tile.pill("Trading now", "session", { headline: true }),
    tile.gauge("Session passed, %", "session_done", { min: 0, max: 100, format: "0" }),
    tile.pill("Up next", "up_next"),
    tile.meter("Day range vs average, %", "range_used", { min: 0, max: 150, marks: [100] }),
    tile.gauge("Close in today's range", "range_position", { min: 0, max: 100, format: "0" }),
    tile.value("Close vs day VWAP", "vs_vwap", { format: "%" }),
    tile.rows("Close vs prior day", [
      ["High", "vs_prior_high", "%"],
      ["Low", "vs_prior_low", "%"],
    ]),
  ],
});

const SESSIONS = 3; // Asia, London, New York, in the order a later one wins an overlap
const NAMES = ["Asia", "London", "New York"];
const MAX_DAYS = 30; // the daily stream's depth: adr_days is at most this
const starts = new StaticArray<f64>(SESSIONS); // each window's start and end in minutes from midnight, and its zone, read in onStart()
const ends = new StaticArray<f64>(SESSIONS);
const zones = new StaticArray<i32>(SESSIONS);
let clocks: Clock[] = []; // one clock per session, in the zone its window is written in, built in onStart()
const days = new CandleList(MAX_DAYS); // the newest closed daily candles, kept from the stream
const priorHighLine: LineHandle = draw.line(0); // handle objects allocate once, at module load
const priorLowLine: LineHandle = draw.line(1);

let adrDays = 14; // settings, read in onStart()
let coarse = false;
let dayKey: f64 = NaN; // today's trade date, and the open of its first bar
let dayStart: f64 = NaN;
let dayWhole = false; // today began inside the loaded history: the first day may have started before it
let dayHigh: f64 = NaN; // today so far
let dayLow: f64 = NaN;
let pv = 0.0; // today's VWAP sums: price times volume, volume, and the plain sum and count for a market without volume
let vol = 0.0;
let typicalSum = 0.0;
let typicalCount = 0;
let chartPriorHigh: f64 = NaN; // the prior day from the chart's own bars, where the daily stream is not served
let chartPriorLow: f64 = NaN;

// onStart() runs once before the first bar: the settings, and a clock per session zone.
function onStart(): void {
  starts[0] = p_asia_start(); ends[0] = p_asia_end(); zones[0] = i32(p_asia_tz());
  starts[1] = p_london_start(); ends[1] = p_london_end(); zones[1] = i32(p_london_tz());
  starts[2] = p_new_york_start(); ends[2] = p_new_york_end(); zones[2] = i32(p_new_york_tz());
  clocks = [new Clock(SESSION_TZ_IDS[zones[0]]), new Clock(SESSION_TZ_IDS[zones[1]]), new Clock(SESSION_TZ_IDS[zones[2]])];
  adrDays = i32(p_adr_days());
  if (adrDays > MAX_DAYS) adrDays = MAX_DAYS;
  coarse = p_chart_interval_sec() >= 14400.0; // 4h and coarser: a session is a bar or two, its minutes would say nothing
}

// Minutes from a bar's open to its session's close, on the session's own clock; a window may wrap midnight.
function minutesLeft(s: i32, t: f64): f64 {
  const clock = clocks[s];
  clock.update(t);
  const minute = f64(clock.hour() * 60 + clock.minute());
  const start = unchecked(starts[s]);
  const end = unchecked(ends[s]);
  if (end > start || minute < end) return end - minute;
  return 1440.0 - minute + end; // a window that wraps midnight, before midnight: it closes tomorrow
}

// The share of a session that has passed at a bar's open, percent.
function sessionDone(s: i32, t: f64): f64 {
  const start = unchecked(starts[s]);
  const end = unchecked(ends[s]);
  const length = end > start ? end - start : 1440.0 - start + end;
  return Math.max(0.0, Math.min(100.0, (1.0 - minutesLeft(s, t) / length) * 100.0));
}

// Minutes from a bar's open to a session's next start, on the session's own clock.
function minutesToStart(s: i32, t: f64): f64 {
  const clock = clocks[s];
  clock.update(t);
  const minute = f64(clock.hour() * 60 + clock.minute());
  const start = unchecked(starts[s]);
  return start > minute ? start - minute : 1440.0 - minute + start;
}

// The prior day's level across today, or nothing while it is unknown.
function drawPrior(line: LineHandle, price: f64, t: f64): void {
  if (isNaN(price) || isNaN(dayStart)) {
    line.delete(); // a no-op when never drawn
    return;
  }
  line.set(dayStart, price, t, price);
}

// onBar() runs once per bar: feed the daily stream, place the bar in its session and its day, then write the
// readings; the session's name and the two prior-day lines on the live bar only.
function onBar(): void {
  const t = bar.time();
  days.load(in_days_view(), in_days_cells(), t); // every bar feeds its block, an empty one too
  const close = bar.close();
  if (isNaN(close)) return;
  if (coarse) {
    if (bar.isLast()) {
      sb_clear(); sb_text("Needs a chart under 4h"); str_session_sb();
    }
    return;
  }

  // The session: the later window wins where two overlap (London over Asia, New York over London).
  let now = -1;
  for (let s = 0; s < SESSIONS; s += 1) if (inSession(t, unchecked(starts[s]), unchecked(ends[s]), unchecked(zones[s]))) now = s;
  out_session_now(f64(now + 1));
  out_minutes_left(now >= 0 ? minutesLeft(now, t) : NaN);
  out_session_done(now >= 0 ? sessionDone(now, t) : NaN);

  // The day: a new trade date starts a new one, and the day that ends becomes the prior day if it was seen whole.
  const high = bar.high();
  const low = bar.low();
  const key = in_trade_date();
  const fresh = key != dayKey;
  if (fresh) {
    if (dayWhole) {
      chartPriorHigh = dayHigh;
      chartPriorLow = dayLow;
    }
    dayWhole = !isNaN(dayKey);
    dayKey = key;
    dayStart = t;
    dayHigh = high;
    dayLow = low;
    pv = 0.0;
    vol = 0.0;
    typicalSum = 0.0;
    typicalCount = 0;
  } else {
    if (high > dayHigh) dayHigh = high;
    if (low < dayLow) dayLow = low;
  }
  const typical = (high + low + close) / 3.0;
  const volume = bar.volume();
  if (volume > 0.0) {
    pv += typical * volume;
    vol += volume;
  }
  typicalSum += typical;
  typicalCount += 1;
  const vwap = !dayWhole ? NaN : vol > 0.0 ? pv / vol : typicalSum / f64(typicalCount);
  out_vwap(fresh ? NaN : vwap); // the day's first bar draws nothing, so each day's line starts on its own

  // The average daily range of the newest closed days, and the prior day: the stream's newest closed day where it
  // is served, else the chart's own bars. The day's last bar is handed the daily candle that closes at its end, the
  // day's own: a held candle that opened on today's trade date or later is skipped, never read as a prior day.
  let held = days.count();
  while (held > 0 && days.openSec(held - 1) >= key) held -= 1;
  let adr: f64 = NaN;
  if (held >= adrDays) {
    let sum = 0.0;
    for (let i = held - adrDays; i < held; i += 1) sum += days.high(i) - days.low(i);
    adr = sum / f64(adrDays);
  }
  const priorHigh = held > 0 ? days.high(held - 1) : chartPriorHigh;
  const priorLow = held > 0 ? days.low(held - 1) : chartPriorLow;

  const range = dayHigh - dayLow;
  out_range_used(dayWhole && adr > 0.0 ? (100.0 * range) / adr : NaN);
  out_range_position(dayWhole && range > 0.0 ? (100.0 * (close - dayLow)) / range : NaN);
  out_vs_vwap(vwap > 0.0 ? 100.0 * (close / vwap - 1.0) : NaN);
  out_vs_prior_high(priorHigh > 0.0 ? 100.0 * (close / priorHigh - 1.0) : NaN);
  out_vs_prior_low(priorLow > 0.0 ? 100.0 * (close / priorLow - 1.0) : NaN);

  if (bar.isLast()) {
    sb_clear(); sb_text(now >= 0 ? NAMES[now] : "Closed"); str_session_sb();
    let next = -1;
    let wait = 1441.0;
    for (let s = 0; s < SESSIONS; s += 1) {
      if (s == now) continue;
      const m = minutesToStart(s, t);
      if (m < wait) { wait = m; next = s; }
    }
    sb_clear();
    if (next >= 0) {
      const h = i64(Math.floor(wait / 60.0));
      sb_text(NAMES[next]); sb_text(" in ");
      if (h > 0) { sb_int(h); sb_text(" h "); }
      sb_int(i64(wait) - h * 60); sb_text(" min");
    }
    str_up_next_sb();
    drawPrior(priorHighLine, priorHigh, t);
    drawPrior(priorLowLine, priorLow, t);
  }
}
```

## How it works

**The session is cut from the clock.** Each window is a `param.session` (Asia 00:00 to 08:00, London 07:00 to 16:00, New York 13:30 to 20:00, all UTC by default), read in `onStart()` as a start, an end and a zone. Per bar, `inSession(...)` says whether the bar's open falls inside each window, daylight saving included; where two overlap, the later one in the list wins (London over Asia from 07:00, New York over London from 13:30). `session_now` carries the answer as a number, 0 closed to 3 New York.

**Time from the bar's open.** No wall clock reaches an indicator: the newest bar's open is its clock. `minutes_left` counts from the live bar's open to the session's close on the session's own clock (a `Clock` built in the window's zone), and `session_done` turns it into the share of the session that has passed, 0 at its open and 100 at its close. Both step once per bar: on a 15m chart the track bar can sit up to 15 minutes behind, on a 1h chart up to an hour.

**Up next.** On the live bar the script measures, on each other session's own clock, the minutes from the bar's open to that session's next start, and writes the nearest one into the bounded `up_next` slot as words: "London in 2 h 5 min", or "New York in 45 min" under an hour. It steps once per bar too, so it can read up to one bar long.

**The day is the trade date.** `time.trade_date` gives each bar its trading day: the UTC date on crypto and forex, the New York date on US stocks. A new trade date starts a new day: today's high, low and VWAP sums start again, and the day that ended becomes the prior day when the chart saw it from its first bar. The first day of the loaded history may have begun before it, so its readings stay empty until the next day starts.

**The VWAP.** Each bar adds its typical price (high plus low plus close, over 3) weighted by its volume, and the VWAP is that sum over the day's volume. On a market that reports no volume every bar weighs the same, so the line is the day's average typical price. The day's first bar draws nothing, so each day's line starts on its own instead of joining the day before.

**The average day.** `input("days", candles.cells, { interval: "1d", bars: 30 })` streams the closed daily candles and a `CandleList` keeps the newest 30. The average daily range is the mean of high minus low over the newest `adr_days` (14) of them, and `range_used` is today's high minus low as a percent of it: 100 is an average day's whole range, and the meter runs to 150. The prior day's high and low come from the newest closed daily candle where the stream is served, else from the chart's own bars. A day's candle closes at the end of the day's last bar, so that bar (23:00 on a 1h crypto chart) is handed today's own candle: every held candle that opened on today's trade date or later (`days.openSec(i)` at or past `in_trade_date()`) is skipped before either reading, so the 23:00 bar still measures against yesterday's high and low and an average of the days before today.

**The card reads the outputs.** `render.hud("session", { position: "bottom_center", look: "stage", columns: 2, tiles })` pins a 300 px card of two columns: the headline pill over the `session` slot on the first row; a gauge of `session_done` (0 to 100) beside a pill over the `up_next` slot; a meter of `range_used` (0 to 150) across the card; a gauge of `range_position` (0 to 100) beside a value of `vs_vwap` in `%`; and a rows tile of the distances from the prior day's high and low in `%`. The distances are already percents: the card prints two decimals, a minus sign below the level and no sign above it. A row's dot takes its output's colour, slate for the prior day like its lines. Every tile reads the newest row; the session's name, the up-next words and the two dotted lines are written on the live bar only, the numbers on every bar.

**The look draws the tiles.** `look: "stage"` sets the green gradient card with its cover square (the chart's base asset over the accent), the headline bold at 30 px, and its green accent on the headline and on every gauge and meter that declares no colour of its own. A gauge draws as a track with a knob, a meter as a split bar (the part used in the accent, the rest of the way to 150 in the chart's down colour, the two numbers at its ends) and a pill as its words alone. It keeps its colours on a dark and a light chart alike ([Looks](../presentation/hud-and-hover-cards.md#looks)).

**Where the card sits.** At the bottom centre: the legend holds the top left, the action bar and the High and Low tags the top right, the VWAP tag and the newest candles the right edge, and the app's floating button the bottom left.

## Where it runs

Every market with candles, on charts finer than 4h: crypto perpetuals and spot (Binance Futures BTCUSDT, Binance spot ETHUSDT, Hyperliquid PURR), US stocks and forex (gold). The windows are clock windows in their zones, so on crypto they run every day, weekends included; a forex chart has no bars over the weekend, so on a Saturday it shows Friday's last bar, after New York's close: "Closed". The average daily range needs the chart's daily candle stream, which crypto charts serve and forex and US stock charts do not (below).

## When data is missing

- **Forex and US stocks: no daily stream.** The chart does not serve a `candles` stream on those markets, so `days` reads no candles: the range meter prints "-", the prior day's high and low come from the chart's own bars, and the Console and the legend show the chart's warning, which names the input: `Input 'days' pins interval '1d' on an index-mode chart (FX_OTC): pinned legs are served on timestamp-mode markets only, so the input reads its missing fill (NaN) on every bar.` Everything else on the card reads as usual.
- **No volume.** Forex reports no volume, so the VWAP weighs every bar the same there: the line is the day's average typical price.
- **Between sessions.** Outside the three windows the headline reads "Closed", the session-passed bar prints "-", and Up next counts down to the next session to open.
- **Coarse charts.** On a 4h or coarser chart a session is a bar or two, so the headline reads "Needs a chart under 4h", the other tiles print "-", and nothing is drawn on price.
- **The first loaded day.** The chart's history may start inside a day; that day's VWAP, range and position stay empty, and the next day starts clean. On a short chart (a few hours of 1m bars) today itself can be that first day: the session word, the session-passed bar and Up next still read.
- **A flat day.** A day whose high equals its low leaves the close-in-range track reading "-".

## Customize it

- **Other sessions.** The three windows are settings, each with its own zone; a window that crosses midnight (`22:00-04:00`) wraps on its own, and the track bar and the countdown count across midnight.
- **A longer or shorter average.** `adr_days` goes from 2 to 30; the stream holds 30 days, so past that raise `bars` on the `days` input and `MAX_DAYS` with it.
- **The minutes as a number.** `tile.value("Minutes to close", "minutes_left", { format: "int" })` puts the minutes left in the session on the card beside the track bar's share.
- **Another place for the card.** Change `position` to another of the nine anchors (`middle_left` works too); keep the right edge free for the VWAP tag and the newest candles.
- **Change the look.** `look:` takes any of the twelve shipped looks, `default` to `classic` ([Looks](../presentation/hud-and-hover-cards.md#looks)), and a word declared beside it still wins; the Look row on the indicator's Style page switches it without code ([The Style page](../settings/style-page.md#the-look-row)).

## Run it

1. In the editor's Explorer, press the **Templates** icon ("Browse starter templates") and pick **Session HUD** under **HUDs**.
2. Press **Run** on a chart finer than 4h, such as 15m: the VWAP draws on price, the prior day's high and low run across today, and the stage card appears at the bottom centre.
3. At the editor's Console prompt, type `session_done` and `range_used` to read the newest values, or `last 60 session_now` for the session on each of the last 60 bars.

## Concepts used

- [Sessions and units](../settings/sessions-and-units.md) for `param.session`, its zones and `inSession`
- [Clock and sessions kit](../functions/time-and-sessions-kit.md) for `Clock`
- [Higher-timeframe kit](../functions/higher-timeframe-kit.md) for `CandleList`
- [Data sources](../core-concepts/data-sources.md) for `time.trade_date`
- [Multi-timeframe](../core-concepts/multi-timeframe.md) for the `candles` stream and where the chart serves it
- [Drawing objects](../presentation/drawing-objects.md#handles) for line handles from today's first bar to the right edge
- [HUD cards](../presentation/hud-and-hover-cards.md#hud-cards) and [Blocks and tiles](../presentation/hud-and-hover-cards.md#blocks-and-tiles) for the card, its tiles and the headline
- [Looks](../presentation/hud-and-hover-cards.md#looks) for `look: "stage"`, its cover and what it draws for each tile kind
- [The Style page](../settings/style-page.md#the-look-row) for the Look row that switches the look without code
