---
title: "Options market stats"
description: "The whole US listed options market's calls against its puts, through the day. Two lines under the chart read the day's put/call ratios so far: the put…"
order: 101
section: "cookbook"
---

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

# Options market stats

The whole US listed options market's calls against its puts, through the day. Two lines under the chart read the day's put/call ratios so far: the put contracts over the call contracts, and the put premium over the call premium, with a dashed line at 1.0 where puts match calls. On a daily chart each bar is that day's ratio, so the lines become the market's put/call history. A table under the chart reads the day for every underlying and, picked in Breakdown, for equities, ETFs and indices alone and for the contracts expiring that day (0DTE): call and put contracts, their ratio, call and put premium, its ratio and the prints. A card at the top right says which side leads by the contract ratio, then the day's sums and the 0DTE share of the contracts.

The parts are scalar `options_flow` inputs under the market scopes, the breakdown rows behind the options of a multi setting through `when` and `when_bit` ([Data sources](../core-concepts/data-sources.md#us-listed-options-by-the-charts-underlying)), the bar's trade date and session from the `time` source ([Data sources](../core-concepts/data-sources.md#the-time-source)), a `panel.table` over a frame written on the live bar ([Frames and panels](../presentation/cards-frames-panels.md)), and a `render.hud` card in the terminal look over data-only outputs and text slots ([HUD cards](../presentation/hud-and-hover-cards.md#hud-cards)). This is also the `options-market-stats` template: the **Options Market Stats** card under **Order flow** in the editor's starter list, and it compiles as written.

## The wrun indicator

```typescript sample=options-market-stats
// Options Market Stats: the whole US listed options market's calls against its puts, through the day. Two lines under
// the chart read the day's put/call ratios so far, by contracts and by premium (the put contracts over the call
// contracts, the put premium over the call premium, every bar of the trade date summed), with a dashed line at 1.0
// where puts match calls; on a daily chart each bar is that day's ratio. A table under the chart reads the day for
// every underlying and, picked in Breakdown, for equities, ETFs and indices alone and for the contracts expiring that
// day (0DTE): call and put contracts, their ratio, call and put premium, its ratio and the prints. A card at the top
// right says which side leads by the contract ratio, then the day's sums and the 0DTE share of the contracts. The
// equity and ETF rows need each underlying's kind, which the data plane stamps from a daily reference list: until it
// does they read empty, and the table and a line at the top of the chart say so. The rows come from options_flow's
// market scopes, one per bar: 1-minute rows reach back 7 days, 5-minute rows 30 and daily rows are kept, so older
// intraday bars carry none; a regular-session bar without a row adds nothing and is counted, the sums being a floor
// while that count is above 0.

param.multi("breakdown", ["Equities", "ETFs", "Indices", "0DTE"], ["Equities", "ETFs", "Indices", "0DTE"], { label: "Breakdown", description: "The rows the table adds under every underlying; each is one more read, fetched only while it is picked" });
input("close", ohlcv.close); // the chart's own candles: the bar grid (the stats read the whole market on any US stock chart)
input("trade_date", time.trade_date); // the bar's trade date, the New York date on a US stock: the sums start again on each
input("session", time.session); // 1 in the regular session: the bars a missing row is counted on
// Five fields per row of the table: every underlying always, the breakdown rows only while picked (when_bit is the
// row's place in Breakdown).
input("mkt_calls", options_flow.call_contracts_total, { scope: "market", missing: "nan", description: "Every underlying: call contracts traded in the bar" });
input("mkt_puts", options_flow.put_contracts_total, { scope: "market", missing: "nan", description: "Every underlying: put contracts traded in the bar" });
input("mkt_cprem", options_flow.call_prem_total, { scope: "market", missing: "nan", description: "Every underlying: call premium traded in the bar, USD" });
input("mkt_pprem", options_flow.put_prem_total, { scope: "market", missing: "nan", description: "Every underlying: put premium traded in the bar, USD" });
input("mkt_prints", options_flow.prints, { scope: "market", missing: "nan", description: "Every underlying: option prints in the bar" });
input("eq_calls", options_flow.call_contracts_total, { scope: "equity", when: "breakdown", when_bit: 0, missing: "nan", description: "Equities: call contracts traded in the bar" });
input("eq_puts", options_flow.put_contracts_total, { scope: "equity", when: "breakdown", when_bit: 0, missing: "nan", description: "Equities: put contracts traded in the bar" });
input("eq_cprem", options_flow.call_prem_total, { scope: "equity", when: "breakdown", when_bit: 0, missing: "nan", description: "Equities: call premium traded in the bar, USD" });
input("eq_pprem", options_flow.put_prem_total, { scope: "equity", when: "breakdown", when_bit: 0, missing: "nan", description: "Equities: put premium traded in the bar, USD" });
input("eq_prints", options_flow.prints, { scope: "equity", when: "breakdown", when_bit: 0, missing: "nan", description: "Equities: option prints in the bar" });
input("etf_calls", options_flow.call_contracts_total, { scope: "etf", when: "breakdown", when_bit: 1, missing: "nan", description: "ETFs: call contracts traded in the bar" });
input("etf_puts", options_flow.put_contracts_total, { scope: "etf", when: "breakdown", when_bit: 1, missing: "nan", description: "ETFs: put contracts traded in the bar" });
input("etf_cprem", options_flow.call_prem_total, { scope: "etf", when: "breakdown", when_bit: 1, missing: "nan", description: "ETFs: call premium traded in the bar, USD" });
input("etf_pprem", options_flow.put_prem_total, { scope: "etf", when: "breakdown", when_bit: 1, missing: "nan", description: "ETFs: put premium traded in the bar, USD" });
input("etf_prints", options_flow.prints, { scope: "etf", when: "breakdown", when_bit: 1, missing: "nan", description: "ETFs: option prints in the bar" });
input("idx_calls", options_flow.call_contracts_total, { scope: "index", when: "breakdown", when_bit: 2, missing: "nan", description: "Indices: call contracts traded in the bar" });
input("idx_puts", options_flow.put_contracts_total, { scope: "index", when: "breakdown", when_bit: 2, missing: "nan", description: "Indices: put contracts traded in the bar" });
input("idx_cprem", options_flow.call_prem_total, { scope: "index", when: "breakdown", when_bit: 2, missing: "nan", description: "Indices: call premium traded in the bar, USD" });
input("idx_pprem", options_flow.put_prem_total, { scope: "index", when: "breakdown", when_bit: 2, missing: "nan", description: "Indices: put premium traded in the bar, USD" });
input("idx_prints", options_flow.prints, { scope: "index", when: "breakdown", when_bit: 2, missing: "nan", description: "Indices: option prints in the bar" });
input("zd_calls", options_flow.call_contracts_total, { scope: "market", dte: "0dte", when: "breakdown", when_bit: 3, missing: "nan", description: "0DTE: call contracts traded in the bar" });
input("zd_puts", options_flow.put_contracts_total, { scope: "market", dte: "0dte", when: "breakdown", when_bit: 3, missing: "nan", description: "0DTE: put contracts traded in the bar" });
input("zd_cprem", options_flow.call_prem_total, { scope: "market", dte: "0dte", when: "breakdown", when_bit: 3, missing: "nan", description: "0DTE: call premium traded in the bar, USD" });
input("zd_pprem", options_flow.put_prem_total, { scope: "market", dte: "0dte", when: "breakdown", when_bit: 3, missing: "nan", description: "0DTE: put premium traded in the bar, USD" });
input("zd_prints", options_flow.prints, { scope: "market", dte: "0dte", when: "breakdown", when_bit: 3, missing: "nan", description: "0DTE: option prints in the bar" });
output("pc_volume", line, lower, { color: "#f59e0b", width: 2, label: "Put/call, contracts", format: "0.00", description: "The day's put contracts over its call contracts so far, every underlying; NaN before the day's first row" });
output("pc_premium", line, lower, { color: "#a78bfa", width: 2, label: "Put/call, premium", format: "0.00", description: "The day's put premium over its call premium so far, every underlying" });
output("parity", line, lower, { color: "#64748b", width: 1, line_style: "dashed", legend: false, description: "1.0: as many puts as calls" });
// The readings, data-only: the card reads them and the Console can too.
output("calls_today", none, lower, { format: "si", description: "Call contracts traded today through this bar, every underlying" });
output("puts_today", none, lower, { format: "si", description: "Put contracts traded today through this bar, every underlying" });
output("call_premium_today", none, lower, { format: "usd", description: "Call premium traded today, every underlying, USD" });
output("put_premium_today", none, lower, { format: "usd", description: "Put premium traded today, every underlying, USD" });
output("prints_today", none, lower, { format: "si", description: "Option prints today, every underlying" });
output("zero_dte_share", none, lower, { format: "%", decimals: 1, description: "Contracts expiring the same day over every contract traded today, percent; NaN while 0DTE is not picked" });
output("missing_bars", none, lower, { description: "Regular-session bars of today with no row for every underlying: the sums are a floor while it is above 0" });
output("tone", none, lower, { description: "The headline's colour: 0 puts lead, 1 even or no reading, 2 calls lead" });
string("read", { max_bytes: 48 }); // the headline: which side leads, or why there is no reading
string("title", { max_bytes: 48 }); // the card's title: the day
string("note", { max_bytes: 96 }); // the line at the top while a picked equity or ETF row has read nothing

// The table, rewritten on the live bar: one row per underlying group, the day's sums.
const stat_rows = frame("stat_rows", { max_bytes: 4096 });
panel.table({
  name: "market_stats",
  title: "Options market today",
  x: "category",
  place: "below",
  frame: stat_rows,
  height_frac: 0.18,
  font_size: 10,
  cell_padding: 3,
  valign: "middle",
  header_text_color: "theme.muted",
  grid_color: "theme.grid",
  grid_width: 1,
  grid_lines: "rows",
  border_width: 0,
  series: [
    { name: "Underlyings" },
    { name: "Calls", align: "right", format: "si" },
    { name: "Puts", align: "right", format: "si" },
    { name: "Put/call", align: "right", format: "0.00" },
    { name: "Call premium", align: "right", format: "usd" },
    { name: "Put premium", align: "right", format: "usd" },
    { name: "Put/call, premium", align: "right", format: "0.00" },
    { name: "Prints", align: "right", format: "si" },
  ],
});

// The card, in the terminal look: which side leads by the contract ratio, then the day's sums.
render.hud("market_card", {
  position: "top_right",
  safe_area: true,
  look: "terminal",
  title: "{{title}}",
  columns: 2,
  tiles: [
    tile.pill("Who leads", "read", { headline: true, color_by: "tone", colors: ["#ef4444", "#94a3b8", "#22c55e"] }),
    tile.value("Call contracts", "calls_today", { format: "si" }),
    tile.value("Put contracts", "puts_today", { format: "si" }),
    tile.rows([
      ["Put/call, contracts", "pc_volume", "0.00"],
      ["Call premium", "call_premium_today", "usd"],
      ["Put premium", "put_premium_today", "usd"],
      ["Put/call, premium", "pc_premium", "0.00"],
      ["Prints", "prints_today", "si"],
      ["0DTE share", "zero_dte_share", "%"],
      ["Bars with no flow row", "missing_bars", "int"],
    ]),
  ],
});
render.label("note_label", { position: "top_center", text: "note", color: "#94a3b8", style: "knockout", offset: [0, 8] });

const GROUPS = 5; // every underlying, then the four Breakdown rows in its order
const FIELDS = 5; // call and put contracts, call and put premium, prints
const EVEN_BAND = 0.1; // a put/call ratio within 10% of 1 reads even
const NAMES: StaticArray<string> = ["Every underlying", "Equities", "ETFs", "Indices", "0DTE"];
// The rows that read each underlying's kind as the data plane stamps it: equities and ETFs.
const KINDED: StaticArray<bool> = [false, true, true, false, false];
const MONTHS: StaticArray<string> = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
const sums = new StaticArray<f64>(GROUPS * FIELDS); // today's sums, group by group
const rows = new StaticArray<i32>(GROUPS); // today's bars that carried a row, per group
const runRows = new StaticArray<i32>(GROUPS); // rows over the whole run, per group
const cur = new StaticArray<f64>(FIELDS); // this bar's fields of one group
const dateClock = new Clock("UTC"); // the trade date's day and month

let breakdown = 0.0; // the Breakdown bitmask, read in onStart()
let day: f64 = NaN; // today's trade date, epoch seconds
let missing = 0; // regular-session bars of today without a row for every underlying

function onStart(): void {
  breakdown = p_breakdown();
}

// Whether group g is read: every underlying always, a breakdown row while picked (a host that fetches its lane
// anyway is not read).
function readOn(g: i32): bool {
  return g == 0 || multiHas(breakdown, g - 1);
}

// This bar's five fields of group g into cur.
function readGroup(g: i32): void {
  if (g == 0) { cur[0] = in_mkt_calls(); cur[1] = in_mkt_puts(); cur[2] = in_mkt_cprem(); cur[3] = in_mkt_pprem(); cur[4] = in_mkt_prints(); }
  else if (g == 1) { cur[0] = in_eq_calls(); cur[1] = in_eq_puts(); cur[2] = in_eq_cprem(); cur[3] = in_eq_pprem(); cur[4] = in_eq_prints(); }
  else if (g == 2) { cur[0] = in_etf_calls(); cur[1] = in_etf_puts(); cur[2] = in_etf_cprem(); cur[3] = in_etf_pprem(); cur[4] = in_etf_prints(); }
  else if (g == 3) { cur[0] = in_idx_calls(); cur[1] = in_idx_puts(); cur[2] = in_idx_cprem(); cur[3] = in_idx_pprem(); cur[4] = in_idx_prints(); }
  else if (g == 4) { cur[0] = in_zd_calls(); cur[1] = in_zd_puts(); cur[2] = in_zd_cprem(); cur[3] = in_zd_pprem(); cur[4] = in_zd_prints(); }
}

function ratio(top: f64, bottom: f64): f64 {
  return bottom > 0.0 ? top / bottom : NaN;
}

// A ratio cell: two decimals, or an empty cell when there is no ratio (no call or no call premium).
function fbRatio(value: f64): void {
  if (isNaN(value)) fb_text('""');
  else fb_f64(value, 2);
}

// The table on the live bar: every underlying, then each picked breakdown row. A row without a row today says why in
// its first value cell: the equity and ETF rows wait for the data plane's issue types while they never read one.
function writeTable(): void {
  fb_clear();
  fb_text('{"rows":[');
  let written = 0;
  for (let g = 0; g < GROUPS; g += 1) {
    if (!readOn(g)) continue;
    if (written > 0) fb_text(",");
    written += 1;
    fb_text("[");
    fb_str(NAMES[g]);
    if (rows[g] == 0) {
      fb_text(',{"text":');
      fb_str(KINDED[g] && runRows[g] == 0 ? "waits for issue types" : "no rows today");
      fb_text(',"text_color":"theme.muted"},"","","","","",""]');
      continue;
    }
    const at = g * FIELDS;
    fb_text(",");
    fb_f64(sums[at], 0);
    fb_text(",");
    fb_f64(sums[at + 1], 0);
    fb_text(",");
    fbRatio(ratio(sums[at + 1], sums[at]));
    fb_text(",");
    fb_f64(sums[at + 2], 0);
    fb_text(",");
    fb_f64(sums[at + 3], 0);
    fb_text(",");
    fbRatio(ratio(sums[at + 3], sums[at + 2]));
    fb_text(",");
    fb_f64(sums[at + 4], 0);
    fb_text("]");
  }
  fb_text("]}");
  writeFrameBuffer(FRAME_STAT_ROWS);
}

// The card's words and the line at the top, on the live bar.
function writeWords(pcVolume: f64): void {
  sb_clear();
  if (runRows[0] == 0) sb_text("No options flow on this chart yet");
  else if (rows[0] == 0) sb_text("No options flow yet today");
  else if (isNaN(pcVolume)) sb_text("Puts only: no call traded yet");
  else {
    sb_text(Math.abs(pcVolume - 1.0) <= EVEN_BAND ? "Even: put/call " : pcVolume < 1.0 ? "Calls lead: put/call " : "Puts lead: put/call ");
    sb_f64(pcVolume, 2);
  }
  str_read_sb();
  sb_clear();
  sb_text("Options market");
  if (!isNaN(day)) {
    dateClock.update(day); // the trade date is its date at 00:00 UTC
    sb_text(", ");
    if (dateClock.dayOfMonth() < 10) sb_text("0");
    sb_int(dateClock.dayOfMonth());
    sb_text(MONTHS[dateClock.month() - 1]);
  }
  str_title_sb();
  let waiting = false; // a picked equity or ETF row that read nothing in the whole run
  for (let g = 0; g < GROUPS; g += 1) if (KINDED[g] && readOn(g) && runRows[g] == 0) waiting = true;
  str_note(waiting ? "Equity and ETF rows read empty until the data plane stamps each underlying's kind" : ""); // an empty text draws no label
}

// onBar() runs once per bar: a new trade date starts today's sums again; every read group's row joins its sums (a
// regular-session bar without a row for every underlying counts as missing); then the ratio lines, the readings and,
// on the live bar, the table and the words.
function onBar(): void {
  const date = in_trade_date();
  if (!isNaN(date) && date != day) {
    day = date;
    for (let i = 0; i < GROUPS * FIELDS; i += 1) sums[i] = 0.0;
    for (let g = 0; g < GROUPS; g += 1) rows[g] = 0;
    missing = 0;
  }
  for (let g = 0; g < GROUPS; g += 1) {
    if (!readOn(g)) continue;
    readGroup(g);
    let served = true;
    for (let f = 0; f < FIELDS; f += 1) if (isNaN(cur[f])) served = false;
    if (!served) {
      if (g == 0 && in_session() == 1.0) missing += 1;
      continue;
    }
    for (let f = 0; f < FIELDS; f += 1) sums[g * FIELDS + f] += cur[f];
    rows[g] += 1;
    runRows[g] += 1;
  }
  const reading = rows[0] > 0;
  const calls = sums[0];
  const puts = sums[1];
  const pcVolume = reading ? ratio(puts, calls) : NaN;
  out_pc_volume(pcVolume);
  out_pc_premium(reading ? ratio(sums[3], sums[2]) : NaN);
  out_parity(1.0);
  out_calls_today(reading ? calls : NaN);
  out_puts_today(reading ? puts : NaN);
  out_call_premium_today(reading ? sums[2] : NaN);
  out_put_premium_today(reading ? sums[3] : NaN);
  out_prints_today(reading ? sums[4] : NaN);
  const zeroDte = readOn(4) && reading && rows[4] > 0 ? ratio(sums[4 * FIELDS] + sums[4 * FIELDS + 1], calls + puts) : NaN;
  out_zero_dte_share(isNaN(zeroDte) ? NaN : zeroDte * 100.0);
  out_missing_bars(f64(missing));
  out_tone(!reading ? 1.0 : isNaN(pcVolume) ? 0.0 : Math.abs(pcVolume - 1.0) <= EVEN_BAND ? 1.0 : pcVolume < 1.0 ? 2.0 : 0.0); // no call traded: the puts lead
  if (!bar.isLast()) return;
  writeTable();
  writeWords(pcVolume);
}
```

## How it works

**The day's sums.** Each bar's row carries the market's call and put contracts, call and put premium and prints (`call_contracts_total`, `put_contracts_total`, `call_prem_total`, `put_prem_total`, `prints`). The sums start again on each trade date (`time.trade_date`) and add every bar of the date, the pre-market included, since the market trades there too. The ratios on each bar are the sums' so far: put contracts over call contracts (`pc_volume`) and put premium over call premium (`pc_premium`). A day without a call has no ratio: the line breaks and the card says puts only.

**Who leads.** The headline reads the contract ratio: within 10% of 1 is even, under it the calls lead and over it the puts lead. The 0DTE share is the same-day contracts (`dte: "0dte"`) over every contract of the day.

**Breakdown.** `breakdown` is a multi setting, one option per extra row of the table. Every row's five inputs carry `when: "breakdown"` and that option's `when_bit`, so the chart fetches a row's rows only while it is picked; the code reads the same bits (`multiHas`), so a row that is not picked is neither read nor listed, even on a host that fetched its lane anyway.

**The kind of each underlying.** The equity, ETF and index rows read `scope: "equity"`, `"etf"` and `"index"`, which sum the rows of the underlyings of that kind. The data plane stamps each underlying's kind from a daily reference list; until it does, the equity and ETF rows read nothing, so the table says "waits for issue types" in their place and a line at the top of the chart says so. An index root is also stamped by its own chain, so the index row reads rows without the list.

## Where it runs

US stock and ETF charts, and the option contract charts of those underlyings, through OpenMarket's official package once the chart serves `options_flow`. The stats read the whole market whatever the chart's own underlying is. The class is upcoming: the kit declares it ahead of the chart lane, and a copy you run from the editor follows the US options rule on [Limitations](../reference/limitations.md). One-minute rows reach back 7 days, five-minute rows 30 days and daily rows are kept, so a daily chart reads the ratios back to the first day the data plane kept; an alert and a Protected indicator on OpenMarket's servers refuse the class by name.

## When data is missing

The card says why instead of going blank: "No options flow on this chart yet" while no bar of the run carried a row for every underlying, "No options flow yet today" while the newest day has none. Bars with no flow row prints the regular-session bars of the day that carried no row for every underlying: above 0, read the sums as at least what they show. In the table a row without a row today says "no rows today", and the equity and ETF rows say "waits for issue types" until the data plane stamps each underlying's kind. A refused source shows its own chip on the legend, with the reason.

## Customize it

- **Out of the money only.** Add `moneyness: "otm"` to every input to read the contracts whose strike is past the underlying; each new set of knobs is one more read.
- **The next seven days.** Change the 0DTE row's `dte: "0dte"` to `dte: "week"` for the contracts expiring in one to seven days.
- **A leaner chart.** Untick every option of Breakdown: the table keeps its every-underlying row and the chart reads one lane.

## Run it

1. In the editor's Explorer, press the **Templates** icon ("Browse starter templates") and pick **Options Market Stats** under **Order flow**.
2. Press **Run** on a US stock chart: the official package draws the two ratios under the chart, the table below it and the card at the top right; a copy that cannot read US options says so on the legend.
3. At the editor's Console prompt, type `pc_volume`, `pc_premium` and `zero_dte_share` to read the day on the live bar, or `missing_bars` to see how many bars carried no row.

## Concepts used

- [Data sources](../core-concepts/data-sources.md#us-listed-options-by-the-charts-underlying) for the `options_flow` class, its fields and the `scope` and `dte` knobs
- [Data sources](../core-concepts/data-sources.md#the-time-source) for `time.trade_date` and `time.session`
- [Setting kinds](../settings/kinds.md) for `param.multi` and its bitmask
- [Frames and panels](../presentation/cards-frames-panels.md) for the table and its frame
- [HUD cards](../presentation/hud-and-hover-cards.md#hud-cards) for the card, its pill and rows
- [Limitations](../reference/limitations.md) for the US options rule and the upcoming OPRA sources
