---
title: "Anchored VWAP"
description: "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)…"
order: 57
section: "cookbook"
---

<!-- source: docs/indicators/cookbook/anchored-vwap.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# 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) 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

```javascript
//@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

```typescript
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](../core-concepts/execution-model.md): 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](../functions/styling.md)).
- `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](../functions/drawing-objects.md)), 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](volume-spike.md) 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:

```bash
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

- [Time and sessions](../core-concepts/time-and-sessions.md) for the day and week indexes from `time.bar_open_sec`
- [Special indicators](../functions/special-indicators.md) for the anchored-VWAP pattern as a worked function
- [Drawing objects](../functions/drawing-objects.md) for the per-bar box that shades the band
- [na and scalar types](../core-concepts/na-and-scalar-types.md) for the `NaN` warm-up on each line
