Charts
Examples

Stat card panel with widgets

Session change, range and volume as stat cards, with a button that pins badges to the session high and low.

Stat card panel with widgets

Shows: ChartOverlay with DOM widgets: a panel in the top-right corner with StatCards and a Button, and Anchor panels that follow the session high and low as the chart pans.

See also: Widgets.

sessionStats.ts
import type { ScriptBar, Session, WidgetPanel } from "@tradingthings/tt-create";
import { defineScript, format } from "@tradingthings/tt-create";

interface Stats {
  readonly open: number;
  readonly last: number;
  readonly high: ScriptBar;
  readonly low: ScriptBar;
  readonly volume: number;
}

/**
 * Stat cards of the current session in the chart's top-right corner. The
 * button pins badges to the session high and low, which follow the chart.
 */
export default defineScript({
  name: "Session stats",
  surface: { kind: "ChartOverlay" },
  subscribe: (ctx) => ({ symbol: ctx.market.symbol(), bars: ctx.market.bars() }),
  state: () => ({
    session: null as Session | null,
    /** Bars of the current session by open time. The forming bar is replaced on each update. */
    bars: new Map<number, ScriptBar>(),
    pinned: false,
  }),
  on: {
    bars: (s, { bar }, ctx) => {
      // Older sessions come as backfill when the chart scrolls left.
      if (s.session && bar.time < s.session.open) return;

      // `sessionOf` is slow (time zones): called once per session, not per bar.
      if (!s.session || bar.time >= s.session.close) {
        const session = ctx.streams.symbol.info.sessionOf(bar.time);

        if (session.isErr()) return;

        // A bar after the close, e.g. in the daily break, can still map to this session.
        if (session.value.open !== s.session?.open) s.bars.clear();

        s.session = session.value;
      }

      s.bars.set(bar.time, bar);
    },
  },
  widgets: (s, ctx) => {
    const stats = mapBarsToStats(s.bars);

    if (!stats) return [];

    const decimals = format.tickDecimals(ctx.streams.symbol.info.tickSize);
    const price = (value: number) => value.toFixed(decimals);
    const change = stats.last - stats.open;
    const panels: WidgetPanel[] = [
      {
        id: "stats",
        placement: { type: "Corner", corner: "TopRight" },
        widgets: [
          {
            type: "StatCard",
            label: "Session change",
            value: `${change >= 0 ? "+" : ""}${price(change)}`,
            detail: `${((change / stats.open) * 100).toFixed(2)}%`,
            tone: change >= 0 ? "Positive" : "Negative",
          },
          { type: "StatCard", label: "Range", value: price(stats.high.high - stats.low.low) },
          { type: "StatCard", label: "Volume", value: format.volume(stats.volume) },
          {
            type: "Button",
            label: s.pinned ? "Unpin high/low" : "Pin high/low",
            onClick: () => (s.pinned = !s.pinned),
          },
        ],
      },
    ];

    if (s.pinned) {
      panels.push(
        {
          id: "high",
          placement: { type: "Anchor", time: stats.high.time, price: stats.high.high },
          widgets: [{ type: "Badge", text: `High ${price(stats.high.high)}`, tone: "Positive" }],
        },
        {
          id: "low",
          placement: { type: "Anchor", time: stats.low.time, price: stats.low.low },
          widgets: [{ type: "Badge", text: `Low ${price(stats.low.low)}`, tone: "Negative" }],
        }
      );
    }

    return panels;
  },
});

function mapBarsToStats(bars: ReadonlyMap<number, ScriptBar>): Stats | null {
  let first: ScriptBar | null = null;
  let last: ScriptBar | null = null;
  let high: ScriptBar | null = null;
  let low: ScriptBar | null = null;
  let volume = 0;

  for (const bar of bars.values()) {
    if (!first || bar.time < first.time) first = bar;

    if (!last || bar.time > last.time) last = bar;

    if (!high || bar.high > high.high) high = bar;

    if (!low || bar.low < low.low) low = bar;

    volume += bar.volume;
  }

  if (!first || !last || !high || !low) return null;

  return { open: first.open, last: last.close, high, low, volume };
}