VWAP

VWAP (Volume-Weighted Average Price) is the average price paid across the session, weighted by volume at each price level — widely used intraday because it approximates "what most participants actually paid today," a natural reference for institutional execution benchmarking and retail intraday bias.

def vwap(df: pd.DataFrame) -> pd.Series:
    """df: intraday candles for ONE session, indexed by datetime, with high/low/close/volume."""
    typical_price = (df["high"] + df["low"] + df["close"]) / 3
    cumulative_pv = (typical_price * df["volume"]).cumsum()
    cumulative_vol = df["volume"].cumsum()
    return cumulative_pv / cumulative_vol

Critical: VWAP resets every session — never compute it across multiple days

def vwap_per_session(df: pd.DataFrame) -> pd.Series:
    """df spans multiple days — VWAP must reset at each session boundary."""
    return df.groupby(df.index.date).apply(lambda day_df: vwap(day_df)).reset_index(level=0, drop=True)

A VWAP computed as a running cumulative sum across multiple days without resetting is not a meaningful number — this is one of the more common implementation bugs, since a naive .cumsum() on multi-day data "works" without erroring, just produces a wrong series silently.

Standard use: price relative to VWAP as an intraday bias filter

def vwap_bias_signal(df: pd.DataFrame) -> pd.Series:
    session_vwap = vwap_per_session(df)
    signal = pd.Series(0, index=df.index)
    signal[df["close"] > session_vwap] = 1    # above VWAP: intraday bullish bias
    signal[df["close"] < session_vwap] = -1   # below VWAP: intraday bearish bias
    return signal

Many intraday strategies use "above/below VWAP" purely as a directional filter (only take long signals above VWAP, only shorts below) rather than as a standalone entry trigger — combine with an entry signal (chapter 80's ORB, or an EMA crossover) gated by VWAP bias, another concrete instance of confluence (chapter 114).

VWAP bands — adding a volatility envelope

def vwap_bands(df: pd.DataFrame, num_std: float = 1.0) -> pd.DataFrame:
    typical_price = (df["high"] + df["low"] + df["close"]) / 3
    session_vwap = vwap_per_session(df)
    squared_diff = (typical_price - session_vwap) ** 2
    variance = (squared_diff * df["volume"]).groupby(df.index.date).cumsum() / df["volume"].groupby(df.index.date).cumsum()
    std = variance ** 0.5
    return pd.DataFrame({"vwap": session_vwap, "upper": session_vwap + num_std * std, "lower": session_vwap - num_std * std})

Analogous to Bollinger Bands (chapter 104) but anchored to VWAP instead of a simple moving average, and volume-weighted rather than time-weighted — a distinct construction, not a drop-in replacement.

Why brokers/exchanges show VWAP prominently

VWAP is also an execution benchmark — large institutional orders are often measured against "did I get filled better or worse than VWAP," which is part of why it functions as a meaningful reference level for where "fair value" sat for the session, distinct from purely price-based indicators.

Next: 109 — Ichimoku Cloud