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 |
|---|---|
| Resets the running sums at the daily, weekly, monthly or session boundary the user picks. |
| 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 |
|
| Memory follows the reset: one day for the daily and session 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);
}
}Related
Bars and clocks for how session resets land on the UTC grid, and Warmup and memory for session-reset state.
