feature

VWAP

Public library indicator VWAP (vwap). Same source as the Chartnaut-owned row in the live indicator table.

What it plots

Session (or period) volume-weighted average price on the overlay, plus optional ±σ bands from input.array / output.bandTemplate.

Key decisions

Piece

Why it matters

ctx.session.isStart(mode)

Resets cumulative TPV/volume at daily, session, or custom boundaries.

input.array + output.bandTemplate

Trader-editable band list drives both UI and rendered series.

Canonical upper_<dev> / lower_<dev> plot ids

Stable ids for definitions/studies that read VWAP bands as deps.

Full script

meta({
  shortName: "VWAP*",
  kind: "overlay",
  dataSource: "candles",
  category: "custom",
});

dialog({
  title: "VWAP Configuration",
  description: "Configure Volume Weighted Average Price (VWAP) settings",
  width: 600,
  height: 500,
  resizable: false,
  showEnabledCheckbox: true,
  resetToDefaults: {
    enabled: true,
    confirmationMessage: "Are you sure you want to reset all VWAP settings to their default values?",
  },
});

const calculationMode = input.select({
  id: "calculationMode",
  label: "Reset Time",
  layout: "radio",
  radioOrientation: "vertical",
  default: "daily",
  options: [
    { value: "consistent", label: "Remain Consistent", description: "VWAP calculation uses all available data points without resetting" },
    { value: "daily",      label: "Reset Daily",       description: "00:00 UTC each day" },
    { value: "weekly",     label: "Reset Weekly",      description: "00:00 UTC each Monday" },
    { value: "monthly",    label: "Reset Monthly",     description: "00:00 UTC on the 1st of each month" },
    { value: "sydney",     label: "Reset at Sydney Session Open", description: "22:00 UTC" },
    { value: "tokyo",      label: "Reset at Tokyo Session Open",  description: "00:00 UTC" },
    { value: "london",     label: "Reset at London Session Open", description: "08:00 UTC" },
    { value: "newyork",    label: "Reset at New York Session Open", description: "13:30 UTC" },
  ],
});

const labelsEnabled = input.boolean({
  id: "labelsEnabled",
  label: "Show Labels",
  description: "Display VWAP values and bands on chart",
  default: true,
});

const vwapColor = input.color({
  id: "vwapColor",
  label: "Line Color",
  default: "#2962FF",
  allowAlpha: true,
});

const mainLineStyle = input.lineStyle({
  id: "mainLineStyle",
  label: "Line Style",
  default: LineStyle.Solid,
});

const vwapBands = input.array({
  id: "vwapBands",
  label: "VWAP Bands",
  description: "Configure standard deviation bands around the VWAP line",
  maxItems: 10,
  addButtonText: "Add Band",
  collapsible: true,
  sortable: false,
  toggleEnableButton: { show: true, size: "sm" },
  deleteButton:       { show: true, size: "sm" },
  cloneButton:        { show: true, size: "sm" },
  layout: {
    direction: "horizontal",
    gap: "sm",
    rows: [
      { type: "row", fields: ["deviation", "color", "lineStyle"], className: "items-center", gap: "sm",
        distribution: "custom", fieldWidths: { deviation: "20%", color: "20%", lineStyle: "40%" } },
      { type: "row", fields: ["fillEnabled", "fillColor"], className: "items-center", gap: "sm",
        distribution: "custom", fieldWidths: { fillEnabled: "40%", fillColor: "40%" } },
    ],
    itemWrapper: { padding: "sm", border: true },
  },
  defaultItems: [
    { id: "band1", deviation: 1, color: "#ff9900", lineStyle: LineStyle.Dashed, fillEnabled: false, fillColor: "rgba(255, 153, 0, 0.1)", enabled: true },
    { id: "band2", deviation: 2, color: "#ff6600", lineStyle: LineStyle.Dashed, fillEnabled: false, fillColor: "rgba(255, 102, 0, 0.1)", enabled: true },
  ],
  itemTemplate: (input) => {
    input.boolean({ id: "enabled",     label: "Enabled",            default: true,  isEnableField: true });
    input.number({  id: "deviation",   label: "Deviation",          default: 1, min: 0.1, step: 0.1, precision: 1, unit: "σ" });
    input.color({   id: "color",       label: "Color",              default: "#ff9900", allowAlpha: true });
    input.lineStyle({ id: "lineStyle", label: "Line Style",         default: LineStyle.Dashed });
    input.boolean({ id: "fillEnabled", label: "Fill Between Bands", default: false });
    input.color({   id: "fillColor",   label: "Fill Color",         default: "rgba(255, 153, 0, 0.1)", allowAlpha: true,
      visibleWhen: when("fillEnabled").equals(true) });
  },
});

layout(
  tab({ id: "general", title: "General" },
    section({ id: "calculation", title: "Calculation Settings" }, calculationMode),
    section({ id: "display",     title: "Display Options"      }, labelsEnabled),
  ),
  tab({ id: "appearance", title: "Appearance" },
    section({ id: "main_line", title: "Main VWAP Line", columns: 2 }, vwapColor, mainLineStyle),
  ),
  tab({ id: "bands", title: "Bands" },
    section({ id: "deviation_bands", title: "Standard Deviation Bands" }, vwapBands),
  ),
);

output.line({
  id: "vwap",
  lineWidth: 2,
  colorFrom: vwapColor,
  lineStyleFrom: mainLineStyle,
  labelFrom: labelsEnabled,
  titleWhenLabeled: "VWAP",
});

output.bandTemplate({
  from: vwapBands,
  itemIdField: "id",
  itemEnabledField: "enabled",
  upperIdPattern: "vwap_band_pos_{id}",
  lowerIdPattern: "vwap_band_neg_{id}",
  fillIdPattern:  "vwap_band_fill_{id}",
  itemStyle: {
    colorField: "color",
    lineStyleField: "lineStyle",
    fillEnabledField: "fillEnabled",
    fillColorField: "fillColor",
  },
  titleTemplate: "VWAP {deviation}σ",
});

/** @param {ScriptedCtx} ctx */
export function onBar(ctx) {
  const mode = ctx.params.calculationMode || "daily";
  const isStart = ctx.session.isStart(mode);
  const tp = (ctx.high + ctx.low + ctx.close) / 3;

  const cumTPV = ctx.accum("tpv", 0, (prev) => isStart ? tp * ctx.volume : prev + tp * ctx.volume);
  const cumVol = ctx.accum("vol", 0, (prev) => isStart ? ctx.volume      : prev + ctx.volume);

  if (cumVol <= 0) return;

  const vwap = cumTPV / cumVol;
  ctx.plot("vwap", vwap);

  const dev = tp - vwap;
  const sumSq = ctx.accum("sumSq", 0, (prev) => isStart ? dev * dev : prev + dev * dev);
  const count = ctx.accum("count", 0, (prev) => isStart ? 1 : prev + 1);
  const stdDev = count > 0 ? Math.sqrt(sumSq / count) : 0;

  // Emit canonical, deviation-keyed band ids (upper_<devKey> /
  // lower_<devKey>) so flow consumers see the exact same accessor
  // surface on both engines: ctx.indicators.<as>.upper_2.value etc.
  // The output.bandTemplate above keeps {id}-style ids — that's the
  // RENDERER contract (per-band styling lookup); the script-side
  // ctx.plot ids are the canonical, settings-derived ids that the
  // backend's NativeIndicatorOutputs + NativeDynamicOutputs schema
  // promise. Keep the two in lockstep with the TS formatDeviationKey
  // helper in nativeOutputSchemas.ts and Go FormatDeviationKey in
  // authoring_shared/native_indicator_outputs.go.
  const bands = ctx.params.vwapBands || [];
  const seenDev = {};
  for (let i = 0; i < bands.length; i++) {
    const band = bands[i];
    if (!band || band.enabled === false) continue;
    const d = Number(band.deviation) || 0;
    const absD = Math.abs(d);
    const rounded = Math.round(absD * 100) / 100;
    const devKey = rounded === Math.trunc(rounded)
      ? String(rounded)
      : String(rounded).replace(/\./g, "_");
    if (seenDev[devKey]) continue;
    seenDev[devKey] = true;
    ctx.plot("upper_" + devKey, vwap + absD * stdDev);
    ctx.plot("lower_" + devKey, vwap - absD * stdDev);
  }
}

Next

  1. Indicator examples

  2. Indicator series types

  3. Declaring indicator dependencies