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.

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.
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 };
}