Flow VWAP

View as MarkdownOpen the editor

The line on the candles is a VWAP (the average price weighted by what traded there).

Here the weight is premium (the money paid for an option), not volume, by default from buyers who paid up. Price above the line trades above where that money went in, and price below trades under it. The 1σ and 2σ bands (one and two standard deviations) show how far price has stretched from the line. Use it to see where options buyers committed money, on crypto, CME (Chicago futures exchange) futures or US stock charts.

Under the hood: the recipe reads options_flow premium fields with missing: "nan" on the symbol a setting names (underlying: "@chain", Data sources). A second set with dte: "0dte" is fetched only while a setting is on (when). The trade date and session come from the time source (Data sources). It draws five lines and two shaded bands on price (Plotting) and a render.hud card from data-only readings and two string texts (HUD cards). It is also the flow-vwap template, the Flow VWAP card under Order flow in the editor's starter list, and it compiles as written.

The wrun indicator

// Flow VWAP: the line on the candles is a VWAP (the average price weighted by what traded there). Here the weight is
// premium (the money paid for an option), not volume, by default from buyers who paid up. Price above the line trades
// above where that money went in, and price below trades under it. The 1σ and 2σ bands (one and two standard
// deviations) show how far price has stretched from the line. Use it to see where options buyers committed money, on
// crypto, CME (Chicago futures exchange) futures or US stock charts.
//
// Technical notes: the chart's price is weighted by aggressive options premium, summed from the anchor, with bands one
// and two premium-weighted standard deviations either side. Each bar's price (HLC3 by default: the high, the low and
// the close over 3) counts as much as the bar's options premium, by default the premium bought at the ask on calls and
// puts: the buyers who paid up. The premium is the options_flow row of the chain the Options chain setting picks, and
// the price is always the chart's own: on an NQ chart with the chain set to QQQ, QQQ's premium weighs NQ's bars, with
// no ratio between the two. From the anchor's first bar (each trade date under Session, the first trade date of each
// week under Week) the line is the sum of price x premium over the sum of premium. The bands sit one and two
// premium-weighted standard deviations of those prices from the line, lightly shaded. The sums run as a weighted
// Welford update (the mean, and the premium-weighted sum of squared gaps from it, whose ratio to the premium is the
// variance), so a price near 25,000 keeps its precision. A bar without a flow row, or with no premium, adds nothing:
// the lines keep their last value, and they read NaN until the anchor's first bar with premium. The rows come from
// options_flow, one per bar: 1-minute rows reach back 7 days and 5-minute rows 30 days, so older bars carry none. A
// card at the top right reads where the close sits against the line, in standard deviations, then the premium since the
// anchor, one standard deviation in price and the regular-session bars since the anchor that carried no flow row. Every
// print takes a side, cross trades and late prints too: the premium's hover says so.

section("Which premium");
param.choice("chain", ["Chart's own", "Index or ETF this future tracks", "QQQ", "NDX", "SPY", "SPX"], "Chart's own", { label: "Options chain", style_values: ["auto", "derived", "QQQ", "NDX", "SPY", "SPX"], description: "Whose options premium (money paid for options) weighs each bar's price. Chart's own uses this symbol's options. Index or ETF this future tracks uses NDX on NQ and SPX on ES. Or pick QQQ, NDX, SPY or SPX by name. The price always stays the chart's own." });
param.choice("weight", ["Bought at the ask", "Ask and bid", "All premium"], "Bought at the ask", { label: "Premium", description: "Which premium weighs each bar. Bought at the ask counts buyers who paid the seller's price. Ask and bid adds sellers who took the buyer's price. All premium counts every trade, at the mid and with no quote too." });
param.choice("types", ["Calls and puts", "Calls", "Puts"], "Calls and puts", { label: "Contracts", description: "Whose premium counts: calls (bets on a rise) and puts (bets on a fall), calls alone or puts alone." });
param.bool("zero_dte", false, { label: "0DTE only", description: "Weighs each bar by the premium of options that expire today (0DTE) only. It loads that extra data only while this is on." });
section("Anchor");
param.choice("anchor", ["Session", "Week"], "Session", { label: "Anchor", description: "Where the line starts again: each session, or the first session of each week, which is Monday's." });
section("Price");
param.choice("price", ["HLC3", "Close", "HL2"], "HLC3", { label: "Price", description: "Each bar's price, the one the premium weighs. HLC3 averages the high, low and close. Close takes the close, and HL2 averages the high and low." });
section("Bands");
param.bool("band1", true, { label: "1σ band", description: "Lines one premium-weighted standard deviation (1σ) above and below the Flow VWAP, lightly shaded." });
param.bool("band2", true, { label: "2σ band", description: "Lines two premium-weighted standard deviations (2σ) above and below the Flow VWAP, shaded fainter." });
input("close", ohlcv.close); // the chart's own candles: the bar grid and the price the premium weighs
input("high", ohlcv.high);
input("low", ohlcv.low);
input("trade_date", time.trade_date); // the bar's trade date: Session starts the sums again on each, Week on each week's first
input("session", time.session); // 1 in the regular session: the bars a missing row is counted on
// Each bar's premium in the chain the Options chain setting picks ("@chain": the chart reads the picked choice's word).
input("call_ask", options_flow.call_prem_ask, { underlying: "@chain", missing: "nan", description: "Call premium bought at the ask in this bar, in dollars." }); // NaN on a bar the class serves no row for
input("put_ask", options_flow.put_prem_ask, { underlying: "@chain", missing: "nan", description: "Put premium bought at the ask in this bar, in dollars." });
input("call_bid", options_flow.call_prem_bid, { underlying: "@chain", missing: "nan", description: "Call premium sold at the bid in this bar, in dollars." });
input("put_bid", options_flow.put_prem_bid, { underlying: "@chain", missing: "nan", description: "Put premium sold at the bid in this bar, in dollars." });
input("call_total", options_flow.call_prem_total, { underlying: "@chain", missing: "nan", description: "Call premium of every trade in this bar in dollars, whatever its side." });
input("put_total", options_flow.put_prem_total, { underlying: "@chain", missing: "nan", description: "Put premium of every trade in this bar in dollars, whatever its side." });
// The same six over the contracts expiring that day: fetched only while 0DTE only is on.
input("zd_call_ask", options_flow.call_prem_ask, { underlying: "@chain", dte: "0dte", when: "zero_dte", missing: "nan", description: "0DTE (expiring today): call premium bought at the ask in this bar, in dollars." });
input("zd_put_ask", options_flow.put_prem_ask, { underlying: "@chain", dte: "0dte", when: "zero_dte", missing: "nan", description: "0DTE (expiring today): put premium bought at the ask in this bar, in dollars." });
input("zd_call_bid", options_flow.call_prem_bid, { underlying: "@chain", dte: "0dte", when: "zero_dte", missing: "nan", description: "0DTE (expiring today): call premium sold at the bid in this bar, in dollars." });
input("zd_put_bid", options_flow.put_prem_bid, { underlying: "@chain", dte: "0dte", when: "zero_dte", missing: "nan", description: "0DTE (expiring today): put premium sold at the bid in this bar, in dollars." });
input("zd_call_total", options_flow.call_prem_total, { underlying: "@chain", dte: "0dte", when: "zero_dte", missing: "nan", description: "0DTE (expiring today): call premium of every trade in this bar, in dollars." });
input("zd_put_total", options_flow.put_prem_total, { underlying: "@chain", dte: "0dte", when: "zero_dte", missing: "nan", description: "0DTE (expiring today): put premium of every trade in this bar, in dollars." });
output("fvwap", line, overlay, { color: "theme.text", width: 2, format: "price", label: "Flow VWAP", axis_label: true, description: "The price weighted by options premium (money paid for options) since the anchor. It is the sum of price times premium over the sum of premium. Empty until the anchor's first bar with premium." }); // first output on price: the package homes on the chart
output("up1", line, overlay, { color: "theme.muted", width: 1, line_style: "dotted", format: "price", label: "Flow VWAP +1σ", description: "The Flow VWAP plus one premium-weighted standard deviation (1σ) of the prices since the anchor. Empty while 1σ band is off." });
output("lo1", line, overlay, { color: "theme.muted", width: 1, line_style: "dotted", format: "price", label: "Flow VWAP -1σ", description: "The Flow VWAP minus one premium-weighted standard deviation (1σ). Empty while 1σ band is off." });
output("up2", line, overlay, { color: "theme.muted", opacity: 0.6, width: 1, line_style: "dotted", format: "price", label: "Flow VWAP +2σ", description: "The Flow VWAP plus two premium-weighted standard deviations (2σ). Empty while 2σ band is off." });
output("lo2", line, overlay, { color: "theme.muted", opacity: 0.6, width: 1, line_style: "dotted", format: "price", label: "Flow VWAP -2σ", description: "The Flow VWAP minus two premium-weighted standard deviations (2σ). Empty while 2σ band is off." });
fill("up1", "lo1", { color: "theme.muted", opacity: 0.08 }); // the 1σ band, lightly shaded
fill("up2", "lo2", { color: "theme.muted", opacity: 0.04 }); // the 2σ band, fainter: inside 1σ the two shades add up
// The readings, data-only: the card reads them and the Console can too.
output("sigma", none, overlay, { format: "price", description: "One premium-weighted standard deviation (1σ) of the prices since the anchor, in price. Empty until the anchor's first bar with premium." });
output("weight_sum", none, overlay, { format: "usd", description: "The premium since the anchor that weighs the line, in dollars. Empty until the anchor's first options data. Includes cross and late trades." });
output("bar_weight", none, overlay, { format: "usd", description: "This bar's premium in dollars, empty on a bar with no options data." });
output("distance", none, overlay, { description: "How far the close sits from the Flow VWAP, in premium-weighted standard deviations (σ). Above zero the close is above the line, below zero under it. Empty while σ is 0 or unknown." });
output("missing_bars", none, overlay, { description: "Regular-session bars since the anchor with no options data. The line leaves them out." });
string("read", { max_bytes: 48 }); // the headline: where the close sits against the line, or that no flow came yet
string("title", { max_bytes: 48 }); // the card's title: the chain, the anchor and 0DTE

// The card: where the close sits leads as the headline, then the premium since the anchor, one standard deviation in
// price and the bars with no flow row.
render.hud("flow_card", {
  position: "top_right",
  safe_area: true,
  title: "{{title}}",
  columns: 2,
  tiles: [
    tile.pill("Price", "read", { headline: true }),
    tile.value("Premium since anchor", "weight_sum", { format: "usd", hint: "Includes cross and late trades" }),
    tile.value("1σ in price", "sigma", { format: "price" }),
    tile.rows("Since the anchor", [["Bars with no options data", "missing_bars", "int"]]),
  ],
});

const DAY_SEC = 86400.0; // seconds in a day: trade_date over it is the day number since 1970-01-01
const AT_SIGMA = 0.05; // the card reads At Flow VWAP inside this many deviations, where one decimal would read 0.0
let chain = 0; // settings, read in onStart()
let weightMode = 0; // 0 bought at the ask, 1 at the ask and at the bid, 2 every print
let types = 0; // 0 calls and puts, 1 calls, 2 puts
let zeroDte = false;
let weekly = false;
let priceMode = 0; // 0 HLC3, 1 the close, 2 HL2
let band1 = true;
let band2 = true;
let anchor: f64 = NaN; // the anchor the sums run over: the trade date, or its week
let weight = 0.0; // the premium since the anchor
let mean = 0.0; // the premium-weighted mean price since the anchor: the Flow VWAP once weight is above 0
let m2 = 0.0; // the premium-weighted sum of squared gaps from the mean: the variance is m2 over weight
let rows = 0; // bars since the anchor that carried a flow row
let missing = 0; // regular-session bars since the anchor without one

function onStart(): void {
  chain = i32(p_chain());
  weightMode = i32(p_weight());
  types = i32(p_types());
  zeroDte = pb_zero_dte();
  weekly = p_anchor() == 1.0;
  priceMode = i32(p_price());
  band1 = pb_band1();
  band2 = pb_band2();
}

// The anchor a trade date falls in: the date itself under Session; under Week its week's number. Day 0, 1970-01-01,
// was a Thursday, so adding 3 days before dividing by 7 starts each week on a Monday.
function anchorOf(date: f64): f64 {
  return weekly ? Math.floor((date / DAY_SEC + 3.0) / 7.0) : date;
}

// This bar's premium: the sides and the types the settings pick, from the 0DTE set while 0DTE only is on (its inputs
// read NaN while it is off: their lane is never fetched). NaN on a bar without a flow row; never below 0.
function barWeight(): f64 {
  let calls = zeroDte ? in_zd_call_ask() : in_call_ask();
  let puts = zeroDte ? in_zd_put_ask() : in_put_ask();
  if (weightMode == 1) {
    calls += zeroDte ? in_zd_call_bid() : in_call_bid();
    puts += zeroDte ? in_zd_put_bid() : in_put_bid();
  } else if (weightMode == 2) {
    calls = zeroDte ? in_zd_call_total() : in_call_total();
    puts = zeroDte ? in_zd_put_total() : in_put_total();
  }
  const w = types == 1 ? calls : types == 2 ? puts : calls + puts;
  return w < 0.0 ? 0.0 : w;
}

// This bar's price, the chart's own: HLC3, the close or HL2.
function barPrice(): f64 {
  const close = in_close();
  if (priceMode == 1) return close;
  if (priceMode == 2) return (in_high() + in_low()) / 2.0;
  return (in_high() + in_low() + close) / 3.0;
}

// The card's words, on the live bar.
function writeCard(centre: f64, distance: f64, close: f64): void {
  sb_clear();
  if (isNaN(centre)) sb_text("No options premium yet");
  else if (distance >= AT_SIGMA || (isNaN(distance) && close > centre)) sb_text("Above Flow VWAP");
  else if (distance <= -AT_SIGMA || (isNaN(distance) && close < centre)) sb_text("Below Flow VWAP");
  else sb_text("At Flow VWAP");
  if (!isNaN(centre) && Math.abs(distance) >= AT_SIGMA) {
    sb_text(" ");
    sb_signed(distance, 1);
    sb_text("σ");
  }
  str_read_sb();
  sb_clear();
  sb_text("Flow VWAP, ");
  sb_text(chain == 0 ? "this symbol" : chain == 1 ? "tracked index or ETF" : chain == 2 ? "QQQ" : chain == 3 ? "NDX" : chain == 4 ? "SPY" : "SPX");
  sb_text(weekly ? ", week" : ", session");
  if (zeroDte) sb_text(", 0DTE");
  str_title_sb();
}

// onBar() runs once per bar: a new anchor starts the sums again; this bar's premium weighs its price into the running
// mean and squared gaps (a weighted Welford step), or counts as missing on a regular-session bar without a row; then
// the line, the bands, the readings and, on the live bar, the card's words.
function onBar(): void {
  const date = in_trade_date();
  if (!isNaN(date) && anchorOf(date) != anchor) {
    anchor = anchorOf(date);
    weight = 0.0;
    mean = 0.0;
    m2 = 0.0;
    rows = 0;
    missing = 0;
  }
  const w = barWeight();
  const served = !isNaN(w);
  const price = barPrice();
  if (served) {
    rows += 1;
    if (w > 0.0 && !isNaN(price)) {
      const total = weight + w;
      const gap = price - mean; // from the mean before this bar
      mean += (w / total) * gap; // the anchor's first bar weighs 1 of 1: the mean starts at its price exactly
      m2 += w * gap * (price - mean); // the gap before times the gap after: 0 on a bar at the mean
      weight = total;
    }
  } else if (in_session() == 1.0) missing += 1;
  const centre = weight > 0.0 ? mean : NaN;
  const sigma = weight > 0.0 ? Math.sqrt(m2 > 0.0 ? m2 / weight : 0.0) : NaN; // a rounding step below 0 reads 0
  out_fvwap(centre);
  out_up1(band1 ? centre + sigma : NaN);
  out_lo1(band1 ? centre - sigma : NaN);
  out_up2(band2 ? centre + 2.0 * sigma : NaN);
  out_lo2(band2 ? centre - 2.0 * sigma : NaN);
  out_sigma(sigma);
  out_weight_sum(rows > 0 ? weight : NaN);
  out_bar_weight(served ? w : NaN);
  const close = in_close();
  const distance = sigma > 0.0 ? (close - centre) / sigma : NaN;
  out_distance(distance);
  out_missing_bars(f64(missing));
  if (bar.isLast()) writeCard(centre, distance, close);
}

How it works

The weight. options_flow serves one row of sums per bar: the premium of every print, split by where the print traded against the quote and by calls and puts (Data sources). Premium picks the sides that weigh the bar: Bought at the ask (the default) reads call_prem_ask and put_prem_ask, Ask and bid adds call_prem_bid and put_prem_bid, and All premium reads call_prem_total and put_prem_total, the prints at the mid and with no quote included. Contracts keeps the calls or the puts alone. A weight below 0 counts as 0, so the sums of premium and of squared gaps never fall below 0.

The line. From the anchor's first bar, the Flow VWAP is the sum of price x premium over the sum of premium: FVWAP = Σ(P W) / Σ W. Three bars at 600, 602 and 601 weighted 100,000, 300,000 and 600,000 give (600 x 100,000 + 602 x 300,000 + 601 x 600,000) / 1,000,000 = 601.20: most of the money printed while the price sat at 601. P is the chart's own bar price: HLC3, the high, the low and the close over 3 (the default); the close; or HL2, the high and the low over 2.

The bands. σ is the premium-weighted standard deviation of the prices since the anchor: the square root of Σ W (P - FVWAP)² / Σ W. In the example the squared gaps from 601.20 are 1.44, 0.64 and 0.04, weighted 1, 3 and 6 tenths of the premium, so the variance is 0.36 and σ is 0.6: the 1σ band runs from 600.60 to 601.80 and the 2σ band from 600.00 to 602.40. fill shades each band, the 1σ band at 8 percent opacity and the 2σ band at 4 percent, so inside 1σ the two shades add up. 1σ band and 2σ band turn each pair of lines off, and a band that is off writes NaN while the line stays.

Precision. The package keeps the mean and the premium-weighted sum of squared gaps from it as it goes (a weighted Welford update): each bar moves the mean by its share of the premium times its gap, and adds its premium times its gap before the move times its gap after it. It never takes Σ W P² over the premium minus the squared mean: at NQ's 25,000 each square is 625,000,000, and a variance of a few points squared would sit in the last digits of that difference.

The anchor. Session starts the sums again when time.trade_date changes: the New York date on a US stock, the CME trade date that opens at 17:00 Chicago, the UTC date on crypto. Week starts them again on the first trade date of each week: a trade date over 86,400 is its day number since 1970-01-01, a Thursday, so that number plus 3, over 7 and rounded down, is the same from Monday to Sunday. On CME the Sunday evening bars carry Monday's trade date, so the week starts at Sunday's 17:00 Chicago open.

A bar without flow. A bar the class serves no row for reads NaN, because every flow input declares missing: "nan": it adds nothing, and the lines keep their last value. On a regular-session bar (time.session reads 1) it also counts in missing_bars, which the card prints. A row whose premium is 0 adds nothing either, and counts as no missing bar. Before the anchor's first bar with premium the lines read NaN, so each session's line starts at its first row: on a US stock the pre-market bars carry none.

Another chain's premium. Every flow input declares underlying: "@chain": the chart reads the Options chain setting's picked word, its style_values entry ("auto", "derived", "QQQ", "NDX", "SPY" or "SPX"), and fetches that root's rows, so a change in the settings dialog fetches again without a rebuild. "auto" reads the chart's own chain; "derived" reads the first root the chart's future tracks in the derived table (Derived roots): NDX on NQ, SPX on ES. To weigh an NQ chart by QQQ's premium, pick QQQ by name. Only the weights move: the price is the chart's own bar, so no ETF-to-futures ratio enters the line, and the bands are in the chart's own points.

0DTE only. The second set of six inputs reads the same fields with dte: "0dte": the contracts expiring on the print's own trade date. They carry when: "zero_dte", so the chart fetches their rows only while 0DTE only is on; off, they read NaN and the package reads the first set.

The card. The headline reads where the close sits: Above Flow VWAP or Below Flow VWAP with the distance in σ at one decimal (distance, the close minus the line over σ), At Flow VWAP within 0.05σ (where one decimal would read 0.0), and No options premium yet before the anchor's first bar with premium. With σ at 0 (one price weighted so far) it reads Above, Below or At without a number. The title names the chain, the anchor and 0DTE; under the headline sit the premium since the anchor (weight_sum), one σ in price (sigma) and the regular-session bars since the anchor with no flow row.

Cross and late trades. The data plane sides every print by the quote, cross trades and late or out-of-sequence prints included, because the feed carries no sale conditions. Bought at the ask and Ask and bid weigh those prints too: the premium tile's hover and the description of weight_sum say so.

What the settings do

  • Options chain: Picks whose options weigh the price: this symbol's, the index or ETF this future tracks, or one by name.
  • Premium: Picks which premium counts: bought at the ask, at the ask and the bid, or every trade.
  • Contracts: Counts calls and puts together, calls alone or puts alone.
  • 0DTE only: Counts only options that expire today.
  • Anchor: Starts the line again each session, or on the first session of each week.
  • Price: Picks each bar's price: HLC3 averages high, low and close, Close is the close, HL2 averages high and low.
  • 1σ band: Shows lines one standard deviation above and below the Flow VWAP, lightly shaded.
  • 2σ band: Shows lines two standard deviations above and below the Flow VWAP, shaded fainter.

The card at the top right shows four readings.

  • Price: The card's headline, not the Price setting: where the close sits against the Flow VWAP, in standard deviations (σ).
  • Premium since anchor: The options premium that weighs the line since it last started, in dollars.
  • 1σ in price: The price distance from the line to the 1σ band.
  • Bars with no options data: Regular-session bars since the anchor without data, which the line leaves out.

Where it runs

It works on three kinds of charts. On a coin with listed options, it reads that coin's own options. A copy you run from the editor (where you write and run your own indicators) reads them too. On a US stock or ETF chart with US listed options, every indicator in your browser can read it. The same goes for the charts of that stock's option contracts. US options data runs 15 minutes behind unless you have real-time US options data. On a CME futures chart, every indicator in your browser can read the future's own options too, once you accept the CME market data terms (Limitations). CME data runs 10 minutes behind unless you have real-time CME data. A symbol you pick in Options chain follows its own market's rules. QQQ's options on a CME NQ (Nasdaq 100 future) chart are US listed options, at your US options delay. It shows the last 7 days on 1-minute charts and the last 30 days on 5-minute charts. On a 1-minute chart, older sessions stay blank. Alerts and Protected indicators (published ones that run on OpenMarket's servers) cannot read it. They say which data they lack.

When data is missing

The card says "No options premium yet" until the anchor's first bar with premium. The lines stay empty until then. Bars with no options data counts the regular-session bars since the anchor without data. The line leaves them out, so it follows the money that traded, not the time that passed. Index or ETF this future tracks works only on charts that track one, such as NQ, ES or SPY. On BTC or a single stock there is none, so pick Chart's own there. If the chart cannot load a data source, the legend shows a chip with the reason.

Customize it

  • The week's value. Set Anchor to Week to carry the line and its bands across the week's sessions.
  • Calls against puts. Run two copies, one with Contracts on Calls and one on Puts: where the call buyers and the put buyers each paid up.
  • Every print. Set Premium to All premium to weigh the price by all the option money, sided or not.
  • A third band. Declare up3 and lo3 lines after lo2, then write out_up3(centre + 3.0 * sigma) and out_lo3(centre - 3.0 * sigma) beside the 2σ lines in onBar(), for the extreme moves.

Run it

  1. In the editor's Explorer, press the Templates icon ("Browse starter templates") and pick Flow VWAP under Order flow.
  2. Press Run on a US stock or ETF chart (QQQ, SPY) or on a coin with listed options: the line and its bands draw on the candles and the card at the top right. On an NQ chart, set Options chain to QQQ to weigh NQ's prices by QQQ's premium.
  3. At the editor's Console prompt, type fvwap, sigma and distance to read the live bar, or missing_bars to see how many bars carried no row.

Concepts used