Anchored VWAP

Weekly and daily VWAP that reset on real UTC session boundaries, with a shaded band and a stretch readout for mean-reversion context. The kScript (legacy)…

Weekly and daily VWAP that reset on real UTC session boundaries, with a shaded band and a stretch readout for mean-reversion context. The kScript (legacy) recipe and the Indicator it became, side by side.

VWAP is the volume-weighted average price, the level large players benchmark fills against. A plain cumulative VWAP drifts: it averages from wherever the chart happened to start loading, so the line you see depends on how much history your browser fetched. This recipe pins VWAP to calendar sessions instead. The weekly line resets every Monday at 00:00 UTC and the daily line resets every midnight, so two traders looking at the same symbol see the same level. It adds a percentage band and a "stretch" number that tells you how far price has pulled from fair value.

The kScript (legacy) recipe

//@version=2
// ============================================================================
//  WEEKLY ANCHORED VWAP + DAILY SESSIONS  (v3.2 only)
//  v2's vwap was a single cumulative line from wherever the chart happened to
//  start loading: it drifted as more history loaded. v3 anchors to real UTC
//  sessions: the weekly VWAP resets every Monday 00:00 UTC and the daily one
//  every midnight, regardless of how much data the browser fetched.
// ============================================================================
define(title="Anchored VWAP (week + day)", position="onchart", axis=false);

var weekCol = input(name="weekCol", type="color", defaultValue="#f5a623", label="Weekly VWAP");
var dayCol  = input(name="dayCol",  type="color", defaultValue="#4a90d9", label="Daily VWAP");
var bandPct = input(name="bandPct", type="slider", defaultValue=0.5, label="Band %", constraints={min: 0.1, max: 3, step: 0.1});

timeseries d = ohlcv(symbol=currentSymbol, exchange=currentExchange);

// Session-anchored: na before the first complete session, exact after.
timeseries wVwap = vwap(anchor="week", source=d);
timeseries dVwap = vwap(anchor="day", source=d);

timeseries wUpper = wVwap * (1 + bandPct / 100);
timeseries wLower = wVwap * (1 - bandPct / 100);

plotLine(wVwap, colors=[weekCol], width=2, label=["Weekly VWAP"], desc=["Volume weighted average price anchored to the UTC week"]);
plotLine(dVwap, colors=[dayCol], width=1, label=["Daily VWAP"], desc=["Volume weighted average price anchored to the UTC day"]);
plotLine(wUpper, colors=[opacity(weekCol, 30)], width=1, label=["Week Upper"], desc=["Weekly VWAP plus band percent"]);
plotLine(wLower, colors=[opacity(weekCol, 30)], width=1, label=["Week Lower"], desc=["Weekly VWAP minus band percent"]);
fillBetween(wUpper, wLower, weekCol, 0.08);

// Price stretched from the weekly anchor = mean-reversion context.
var stretch = isnum(wVwap[0]) && wVwap[0] > 0 ? (d.close[0] - wVwap[0]) / wVwap[0] * 100 : 0;
if (isLastBar) {
  plotTable(
    data=[["Anchored VWAP", ""], ["Weekly", isnum(wVwap[0]) ? "".concat(math.round(wVwap[0] * 100) / 100) : "warming"], ["Stretch %", "".concat(math.round(stretch * 100) / 100)]],
    position="bottom_right", headerRow=true, backgroundColor="#0d1117", textColor="#e6edf3", fontSize=11
  );
}

The Indicator

import { box, input, line, lower, ohlcv, output, overlay, param, time } from "./sdk/declare";
import { in_bar_t, in_close, in_high, in_low, in_volume } from "./gen/inputs";
import {
  emitRow,
  out_day_vwap,
  out_stretch_pct,
  out_week_lower,
  out_week_upper,
  out_week_vwap,
} from "./gen/outputs";
import { p_band_pct } from "./gen/params";

param("band_pct", 0.5, { min: 0.1, max: 3, description: "Band distance from the weekly VWAP, in percent" });
input("close", ohlcv.close);
input("high", ohlcv.high);
input("low", ohlcv.low);
input("volume", ohlcv.volume);
input("bar_t", time.bar_open_sec);
output("week_vwap", line, overlay, { color: "#f5a623", width: 2, description: "VWAP anchored to the UTC week" });
output("day_vwap", line, overlay, { color: "#4a90d9", width: 1, description: "VWAP anchored to the UTC day" });
const bandTop = output("week_upper", line, overlay, { color: "#f5a623", opacity: 0.3 });
const bandBottom = output("week_lower", line, overlay, { color: "#f5a623", opacity: 0.3 });
box("week_band", { top: bandTop, bottom: bandBottom, color: "#f5a623", opacity: 0.08, borderWidth: 0 });
output("stretch_pct", line, lower, { unit: "%", color: "#f5a623", description: "Close distance from the weekly VWAP" });

let bandPct: f64 = 0.5;
let weekIdx: f64 = NaN;
let weekPv: f64 = 0.0;
let weekVol: f64 = 0.0;
let weekComplete: bool = false;
let dayIdx: f64 = NaN;
let dayPv: f64 = 0.0;
let dayVol: f64 = 0.0;
let dayComplete: bool = false;
let weekVwap: f64 = NaN;
let dayVwap: f64 = NaN;
let close: f64 = NaN;

export function init(): void {
  bandPct = p_band_pct();
}

export function state(): i32 {
  close = in_close();
  const typical = (in_high() + in_low() + close) / 3.0;
  const volume = in_volume();
  // UTC day index of this bar; epoch day 0 was a Thursday, so +3 makes weeks start on Monday 00:00 UTC.
  const day = Math.floor(in_bar_t() / 86400.0);
  const week = Math.floor((day + 3.0) / 7.0);
  if (week != weekIdx) {
    if (!isNaN(weekIdx)) weekComplete = true;
    weekIdx = week;
    weekPv = 0.0;
    weekVol = 0.0;
  }
  if (day != dayIdx) {
    if (!isNaN(dayIdx)) dayComplete = true;
    dayIdx = day;
    dayPv = 0.0;
    dayVol = 0.0;
  }
  weekPv += typical * volume;
  weekVol += volume;
  dayPv += typical * volume;
  dayVol += volume;
  // A session that was already running when the history starts is incomplete: stay NaN until the first boundary.
  weekVwap = weekComplete && weekVol > 0.0 ? weekPv / weekVol : NaN;
  dayVwap = dayComplete && dayVol > 0.0 ? dayPv / dayVol : NaN;
  return 1;
}

export function finalize(): void {
  out_week_vwap(weekVwap);
  out_day_vwap(dayVwap);
  out_week_upper(weekVwap * (1.0 + bandPct / 100.0));
  out_week_lower(weekVwap * (1.0 - bandPct / 100.0));
  out_stretch_pct(isNaN(weekVwap) || weekVwap <= 0.0 ? NaN : ((close - weekVwap) / weekVwap) * 100.0);
  emitRow();
}

export function reset(): void {
  weekIdx = NaN;
  weekPv = 0.0;
  weekVol = 0.0;
  weekComplete = false;
  dayIdx = NaN;
  dayPv = 0.0;
  dayVol = 0.0;
  dayComplete = false;
  weekVwap = NaN;
  dayVwap = NaN;
  close = NaN;
}

How it works

The anchor is the whole trick. The time source hands the module each bar's open time in epoch seconds. Divide by 86400 and you have a UTC day index; shift by three days and divide by seven and you have a week index whose boundary is Monday 00:00 UTC. When the index changes, the price-times-volume and volume sums reset. The reset is tied to the calendar, not to your scroll position or how much history loaded, which is exactly why the level is stable and shared. vwap(anchor="week") did this for the kScript; here it is eight lines of arithmetic you can read.

Warm-up is honest. A session that was already running when the loaded history starts is incomplete, so both lines stay NaN until their first boundary passes, exactly like the kScript's na before the first complete session. The daily line fills in within the first day of loaded data, the weekly line needs about a week. state() still returns 1 on those bars: the row exists, and the lines that are ready draw while the ones that are warming do not. That is the second warm-up policy from the Execution model: write NaN to one output rather than abstaining the whole row.

The band frames the move. week_upper and week_lower are the weekly VWAP scaled by a percentage, drawn faint with opacity: 0.3, and box("week_band", ...) shades between them: with from and to left at 0 every bar contributes a one-bar-wide slice and the slices tile into a channel. Its borderWidth: 0 keeps the slices from showing seams. The two band outputs are bound to consts because the box names them by handle. When price rides the upper band it is stretched rich versus the week's fair value; when it sags to the lower band it is cheap.

The stretch readout. (close - week_vwap) / week_vwap * 100 is the most actionable number, so it gets its own pane as stretch_pct with a % unit, NaN while the weekly line is warming. Where the kScript printed it once, in a table on the last bar, the Indicator has it on every bar: a series you can read back in the legend, and a metric a watch can compare.

What changed in the port

  • vwap(anchor="week") is your own arithmetic. There is no anchored-VWAP builtin; the time source and a reset-on-boundary sum are the building blocks, and they port to any anchor.
  • The color inputs are gone: color is declared on each output, and the band tint is a box color with its own opacity instead of opacity() calls. There is no color picker param; a runtime color knob exists only as a style knob in a hand-written sheet (Styling).
  • fillBetween became a box on every bar. fills exists in hand-written sheets only, and range() bands are not drawn by the chart today, so the box is the form that works everywhere.
  • The isLastBar table became a lower-pane series: the readout is now visible for every bar, and it is a metric an alert could watch. A one-cell table is possible with a string slot and render.table (Drawing objects), at the cost of switching the file to the second runtime contract.

Customize it

  • Band width. band_pct sets how far the band sits from the VWAP. Tighten it toward 0.2 on calm majors, widen it toward 2 on volatile names so the band actually contains the noise.
  • Anchor period. For a monthly anchor, compute the month index from the day index (a lookup over cumulative month lengths, leap years included); the reset logic does not change. Monthly needs about a month of loaded history before it shows anything.
  • Band the daily line too. Duplicate the two band outputs and the box against day_vwap if you trade the intraday session instead.
  • Stretch as a signal. Add a data-only gate output (stretch_pct > 1.5) and a shape output with shape_where on it, the way the volume spike marks its bars. Or skip the mark and watch the metric directly on your machine; --once makes the watch fire a single time and pause itself:
om watch create "Stretched from VWAP" --condition '{"metric":"wrun/@you/anchored-vwap/stretch_pct","selector":{"exchange":"BINANCE_FUTURES","symbol":"BTCUSDT","interval":"HOUR"},"op":"gt","value":1.5}' --once

Concepts used