Example 01
Session VWAP with bands
Volume weighted average price of each session, with bands at N standard deviations. It draws on the price chart, over the candles.

- Loads bars back to the session open, so the first VWAP is complete.
- Updates its sums once per bar. draw only reads the points.
- One polyline and one band polygon per session, and the last VWAP on the price axis.
1import { defineScript, input, type ScriptBar } from "@tradingthings/tt-create";2 3/** Volume sums of a session's confirmed bars. */4interface Sums {5 readonly volume: number;6 /** Sum of typical price × volume. */7 readonly priceVolume: number;8 /** Sum of typical price² × volume, for the standard deviation. */9 readonly squareVolume: number;10}11 12interface VwapPoint {13 readonly time: number;14 /** Open of the point's session: the line breaks between sessions. */15 readonly session: number;16 readonly vwap: number;17 readonly deviation: number;18}19 20const EMPTY: Sums = { volume: 0, priceVolume: 0, squareVolume: 0 };21 22/**23 * Volume weighted average price of each session, with bands at N standard24 * deviations. The sums update once per bar in the handler, `draw` only25 * reads the points.26 */27export default defineScript({28 name: "Session VWAP",29 surface: { kind: "ChartOverlay" },30 inputs: {31 bands: input.float({ default: 2, min: 0, step: 0.5, label: "Bands (std dev)" }),32 line: input.lineStyle({ default: { color: "#f59e0b", width: 2 }, label: "VWAP" }),33 fill: input.color({ default: "rgba(245,158,11,0.12)", label: "Band fill" }),34 },35 subscribe: (ctx) => ({36 symbol: ctx.market.symbol(),37 // Back to the session open, so the first session's VWAP is complete.38 bars: ctx.market.bars({ warmup: "session" }),39 }),40 state: () => ({41 /** Open and close of the current session, `null` before the first bar. */42 session: null as { readonly open: number; readonly close: number } | null,43 /** Sums of the confirmed bars of the current session. */44 sums: EMPTY,45 /** Ascending by time. The forming bar's point is replaced on each update. */46 points: [] as VwapPoint[],47 }),48 on: {49 bars: (s, { bar, confirmed }, ctx) => {50 const last = s.points.at(-1);51 52 // Older bars arrive as backfill when the chart scrolls left. This53 // example skips them: its sums only run forward.54 if (last && bar.time < last.time) return;55 56 // `sessionOf` is slow (time zones): called once per session, not per bar.57 if (!s.session || bar.time >= s.session.close) {58 const session = ctx.streams.symbol.info.sessionOf(bar.time);59 60 if (session.isErr()) return;61 62 if (session.value.open !== s.session?.open) s.sums = EMPTY;63 64 s.session = { open: session.value.open, close: session.value.close };65 }66 67 const sums = addBar(s.sums, bar);68 69 if (sums.volume === 0) return;70 71 const vwap = sums.priceVolume / sums.volume;72 const variance = Math.max(0, sums.squareVolume / sums.volume - vwap * vwap);73 const point = { time: bar.time, session: s.session.open, vwap, deviation: Math.sqrt(variance) };74 75 if (last?.time === bar.time) s.points[s.points.length - 1] = point;76 else s.points.push(point);77 78 if (confirmed) s.sums = sums;79 },80 },81 draw: (s, frame, g, ctx) => {82 const { bands, line, fill } = ctx.inputs;83 const { from, to } = frame.visibleTime;84 let session: number | null = null;85 let center: number[] = [];86 let upper: number[] = [];87 let lower: number[] = [];88 89 const flush = () => {90 if (bands > 0 && upper.length >= 4) {91 // Upper edge left to right, lower edge back: one closed band.92 const outline = [...upper];93 94 for (let i = lower.length - 2; i >= 0; i -= 2) outline.push(lower[i]!, lower[i + 1]!);95 96 g.polygon(outline, fill);97 }98 99 g.polyline(center, line);100 center = [];101 upper = [];102 lower = [];103 };104 105 for (const p of s.points) {106 if (p.time < from || p.time > to) continue;107 108 const x = frame.timeToX(p.time);109 110 if (x === null) continue;111 112 if (p.session !== session) {113 flush();114 session = p.session;115 }116 117 center.push(x, frame.priceToY(p.vwap));118 upper.push(x, frame.priceToY(p.vwap + bands * p.deviation));119 lower.push(x, frame.priceToY(p.vwap - bands * p.deviation));120 }121 122 flush();123 124 const last = s.points.at(-1);125 126 if (last) g.priceLabel(last.vwap, frame.formatPrice(last.vwap), line.color, "#000000");127 },128 legend: (s, crosshairTime) => {129 const point = crosshairTime === null ? s.points.at(-1) : s.points.findLast((p) => p.time <= crosshairTime);130 131 return point ? [{ label: "VWAP", value: point.vwap.toFixed(2) }] : [];132 },133});134 135function addBar(sums: Sums, bar: ScriptBar): Sums {136 const typical = (bar.high + bar.low + bar.close) / 3;137 138 return {139 volume: sums.volume + bar.volume,140 priceVolume: sums.priceVolume + typical * bar.volume,141 squareVolume: sums.squareVolume + typical * typical * bar.volume,142 };143}144 