ChartnautDocs

VWAP

The VWAP indicator from the public library (vwap), written against the current DSL. The library row's source only emits the upper_<dev> and lower_<dev> ids, which the band template does not draw. This version emits the template's ids too.

What it plots

The volume-weighted average price on price, reset at the chosen boundary, with optional standard deviation bands.

Key decisions

Piece

Why it matters

ctx.session.isStart(mode)

Resets the running sums at the daily, weekly, monthly or session boundary the user picks.

input.array with output.bandTemplate

The user edits the list of bands; the template turns each row into an upper line, a lower line and an optional fill.

Two sets of band ids

vwap_band_pos_<row id> and vwap_band_neg_<row id> are the template's series. upper_<dev> and lower_<dev> (upper_2, lower_1_5) are what other scripts read when they declare VWAP as a dependency.

warmup((w) => w.when(calculationMode)...)

Memory follows the reset: one day for the daily and session resets, w.span("7d") weekly, w.span("31d") monthly, and w.forever() for Remain Consistent, which never resets.

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" },
  ],
});

warmup((w) => w.when(calculationMode).is("consistent").then(w.forever()).is("weekly").then(w.span("7d")).is("monthly").then(w.span("31d")).otherwise(w.day()));

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}σ",
});

// onBar runs again for the forming bar on every tick. The chart rolls ctx.accum back between
// ticks; foldBar does not depend on that, so the same code is right on every runtime.
// foldBar keeps two states: `done` (as of the last closed bar) and `tip` (this bar). `step`
// builds the tip from `done` without mutating it, so a repeat of the same bar recomputes the tip
// instead of stacking on top of it. A new bar time commits the tip.
function foldBar(ctx, id, init, step) {
  const s = ctx.accum(id, () => ({ t: null, done: init, tip: init }), (s) => {
    if (s.t !== ctx.time) {
      s.done = s.tip;
      s.t = ctx.time;
    }
    s.tip = step(s.done);
    return s;
  });
  return s.tip;
}

function mean(values) {
  let sum = 0;
  for (const v of values) sum += v;
  return sum / values.length;
}

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

  const st = foldBar(ctx, "vwap", { tpv: 0, vol: 0, sumSq: 0, count: 0 }, (prev) => {
    // A session start throws away everything before it.
    const base = isStart ? { tpv: 0, vol: 0, sumSq: 0, count: 0 } : prev;
    const tpv = base.tpv + tp * ctx.volume;
    const vol = base.vol + ctx.volume;
    if (vol <= 0) return { tpv, vol, sumSq: base.sumSq, count: base.count };
    const dev = tp - tpv / vol;
    return { tpv, vol, sumSq: base.sumSq + dev * dev, count: base.count + 1 };
  });
  if (st.vol <= 0) return;

  const vwap = st.tpv / st.vol;
  const sd = Math.sqrt(st.sumSq / st.count);
  ctx.plot("vwap", vwap);

  const seen = {};
  for (const band of ctx.params.vwapBands) {
    if (!band || band.enabled === false) continue;
    const d = Math.abs(Number(band.deviation) || 0);
    const upper = vwap + d * sd;
    const lower = vwap - d * sd;

    // The series the band template draws: its id patterns with {id} replaced by the row's id.
    // Every row carries an `id` the dialog assigns; it is not one of the template's fields.
    const rowId = band["id"];
    ctx.plot("vwap_band_pos_" + rowId, upper);
    ctx.plot("vwap_band_neg_" + rowId, lower);

    // Deviation-keyed ids (upper_2, lower_1_5) for scripts that read VWAP as a dependency.
    const r = Math.round(d * 100) / 100;
    const key = String(r).replace(".", "_");
    if (seen[key]) continue;
    seen[key] = true;
    ctx.plot("upper_" + key, upper);
    ctx.plot("lower_" + key, lower);
  }
}

Bars and clocks for how session resets land on the UTC grid, and Warmup and memory for session-reset state.