---
title: "Options stats"
description: "Today's US listed option activity (OPRA) of the chart's underlying in one card, calls against puts. A card at the bottom of the pane, in the terminal look…"
order: 120
section: "cookbook"
---

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

# Options stats

Today's US listed option activity (OPRA) of the chart's underlying in one card, calls against puts. A card at the bottom of the pane, in the terminal look, answers one question first: which side leads ("Calls lead: put/call 0.62")? Under the answer the card reads today's call and put contracts, then in numbered rows their ratio, the premium of each side and its ratio, the net premium (calls bought minus calls sold, minus puts bought minus puts sold), the bullish share of the premium that hit a side of the quote (calls at the ask and puts at the bid), and the open interest of each side with its ratio and its change since the prior day. Trade counts are not served, so the card counts contracts and premium, never trades.

The parts are sixteen `options_flow` fields, the underlying's option flow of each bar over every expiry and over the same-day expiries alone (`dte: "0dte"`), summed over today's bars ([Data sources](../core-concepts/data-sources.md#us-listed-options-by-the-charts-underlying)), a celled `option_contract_state.cells` input read on the live bar for the open interest through its `present` mask ([Data sources](../core-concepts/data-sources.md#option_contract_state), [The present mask and partial bars](../core-concepts/data-sources.md#the-present-mask-and-partial-bars)), the options kit's `optionsTradeDay` for today's date and the same-day expiry ([Options kit](../functions/options-kit.md#the-markets-own-date-and-the-chains-clock)), and a `render.hud` card with `look: "terminal"` over data-only outputs and two text slots ([HUD cards](../presentation/hud-and-hover-cards.md#hud-cards)). This is also the `options-stats` template: the **Options Stats** card under **HUDs** in the editor's starter list, and it compiles as written.

## The wrun indicator

```typescript sample=options-stats
// Options Stats: today's US listed option activity (OPRA) of the chart's underlying in one card, calls against puts. A
// card at the bottom of the pane, in the terminal look, says first which side leads (Calls lead: put/call 0.62), then
// today's call and put contracts and their ratio, the premium of each side and its ratio, the net premium (calls bought
// minus calls sold, minus puts bought minus puts sold, by premium at the ask and at the bid), the bullish share of the
// premium that hit a side of the quote (calls at the ask and puts at the bid), and the open interest of each side with
// its ratio and its change since the prior day. Contracts and premium are the underlying's option flow of each of
// today's bars, summed: every expiry, the same-day expiries (0DTE) or the ones after today (every expiry minus 0DTE).
// A bar with no flow row adds nothing. Open interest comes from the chain summary on the newest bar, over the twelve
// nearest expiries and the strikes within 25% of the underlying (a whole chain can hold more contracts than one read
// carries). Trade counts are not served. It reads the chart's own underlying: a US stock chart or an option contract's
// chart.
param.choice("expiries", ["every expiry", "0DTE only", "after today"], "every expiry", { label: "Expiries", description: "0DTE only keeps the contracts expiring today; after today leaves them out" });
input("close", ohlcv.close); // the chart's own candles: the grid the flow lines up on
// The underlying's option flow of each bar (the ticker scope), every expiry, then the same-day expiries alone.
input("call_volume", options_flow.call_contracts_total, { missing: "nan" });
input("put_volume", options_flow.put_contracts_total, { missing: "nan" });
input("call_premium", options_flow.call_prem_total, { missing: "nan" });
input("put_premium", options_flow.put_prem_total, { missing: "nan" });
input("call_ask", options_flow.call_prem_ask, { missing: "nan" });
input("call_bid", options_flow.call_prem_bid, { missing: "nan" });
input("put_ask", options_flow.put_prem_ask, { missing: "nan" });
input("put_bid", options_flow.put_prem_bid, { missing: "nan" });
input("call_volume_0dte", options_flow.call_contracts_total, { dte: "0dte", missing: "nan" });
input("put_volume_0dte", options_flow.put_contracts_total, { dte: "0dte", missing: "nan" });
input("call_premium_0dte", options_flow.call_prem_total, { dte: "0dte", missing: "nan" });
input("put_premium_0dte", options_flow.put_prem_total, { dte: "0dte", missing: "nan" });
input("call_ask_0dte", options_flow.call_prem_ask, { dte: "0dte", missing: "nan" });
input("call_bid_0dte", options_flow.call_prem_bid, { dte: "0dte", missing: "nan" });
input("put_ask_0dte", options_flow.put_prem_ask, { dte: "0dte", missing: "nan" });
input("put_bid_0dte", options_flow.put_prem_bid, { dte: "0dte", missing: "nan" });
input("state", option_contract_state.cells, { max_cells: 16000, expiries: "nearest:12", strike_window_pct: 25, description: "The chain summary on the newest bar: each contract's open interest and the prior day's, the twelve nearest expiries, strikes within 25% of the underlying" }); // [root_code, expiry_ms, side_cp, strike, bid, ask, last, prev_close, theta, volume_day, oi, oi_prev, present] per contract
// The readings, data-only: today's sums through each bar; the open interest rows on the newest bar only.
output("call_volume_today", none, overlay, { format: "si", description: "Call contracts traded today through this bar" });
output("put_volume_today", none, overlay, { format: "si", description: "Put contracts traded today through this bar" });
output("pc_volume", none, overlay, { format: "0.00", description: "Put contracts over call contracts, today" });
output("call_premium_today", none, overlay, { format: "usd", description: "Call premium traded today, USD" });
output("put_premium_today", none, overlay, { format: "usd", description: "Put premium traded today, USD" });
output("pc_premium", none, overlay, { format: "0.00", description: "Put premium over call premium, today" });
output("net_premium", none, overlay, { format: "usd", description: "Calls bought minus calls sold, minus puts bought minus puts sold, by premium at the ask and the bid, USD" });
output("bull_share", none, overlay, { format: "%", decimals: 1, description: "Calls at the ask plus puts at the bid, over all premium at the ask or the bid, percent" });
output("empty_bars", none, overlay, { description: "Today's bars, through this one, that carried no flow row" });
output("call_oi", none, overlay, { format: "si", description: "Call open interest over the twelve nearest expiries, strikes within 25%; newest bar only" });
output("put_oi", none, overlay, { format: "si", description: "Put open interest over the twelve nearest expiries, strikes within 25%; newest bar only" });
output("pc_oi", none, overlay, { format: "0.00", description: "Put over call open interest; newest bar only" });
output("oi_change", none, overlay, { format: "si", signed: true, description: "Open interest now minus the prior day's, calls and puts; newest bar only" });
output("tone", none, overlay, { 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 and the expiries read

render.hud("stats", {
  position: "bottom_center",
  look: "terminal",
  title: "{{title}}",
  columns: 2,
  tiles: [
    tile.pill("Who leads", "read", { headline: true, color_by: "tone", colors: ["#e5484d", "#94a3b8", "#30a46c"] }),
    tile.value("Call contracts", "call_volume_today", { format: "si" }),
    tile.value("Put contracts", "put_volume_today", { format: "si" }),
    tile.rows([
      ["1) Put/call, contracts", "pc_volume", "0.00"],
      ["2) Call premium", "call_premium_today", "usd"],
      ["3) Put premium", "put_premium_today", "usd"],
      ["4) Put/call, premium", "pc_premium", "0.00"],
      ["5) Net premium", "net_premium", "usd"],
      ["6) Bullish share", "bull_share", "%"],
      ["7) Call OI, 12 expiries", "call_oi", "si"],
      ["8) Put OI, 12 expiries", "put_oi", "si"],
      ["9) Put/call, OI", "pc_oi", "0.00"],
      ["10) OI change", "oi_change", "si"],
    ]),
  ],
});

const STATE = 13; // f64s per contract: OPTION_CONTRACT_STATE_TUPLE
const EVEN_BAND = 0.1; // a put/call ratio within 10% of 1 reads even
const MONTHS: StaticArray<string> = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
const SCOPE_WORDS: StaticArray<string> = ["", ", 0DTE", ", after today"];
const dateClock = new Clock("UTC"); // a trade date's day and month

let expiries = 0; // the setting, read in onStart()
let day: f64 = NaN; // today's New York trade date, as a day number
let callVolume = 0.0; // today's sums
let putVolume = 0.0;
let callPremium = 0.0;
let putPremium = 0.0;
let callAsk = 0.0;
let callBid = 0.0;
let putAsk = 0.0;
let putBid = 0.0;
let emptyBars = 0;

function onStart(): void {
  expiries = i32(p_expiries());
}

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

// A field under the expiries setting: every expiry, the same-day expiries, or every expiry minus them; a field the bar
// carries no row for counts 0.
function scoped(every: f64, sameDay: f64): f64 {
  const all = isNaN(every) ? 0.0 : every;
  const zero = isNaN(sameDay) ? 0.0 : sameDay;
  if (expiries == 1) return zero;
  if (expiries == 2) return all - zero > 0.0 ? all - zero : 0.0;
  return all;
}

// Whether the contract expiring at expiryMs counts under the expiries setting, on the trade date `today`.
function counts(expiryMs: f64, today: f64): bool {
  if (expiries == 0) return true;
  const sameDay = optionsTradeDay(expiryMs, 1) == today;
  return expiries == 1 ? sameDay : !sameDay;
}

// The open interest rows from the chain summary on the newest bar.
function writeOpenInterest(today: f64): void {
  const n = in_state_cells();
  let callOi = 0.0;
  let putOi = 0.0;
  let change = 0.0;
  let changeSeen = false;
  let seen = false;
  if (n > 0) {
    const cells = in_state_view();
    for (let i = 0; i + STATE - 1 < n; i += STATE) {
      const present = cells[i + 12];
      if (!(cells[i + 3] > 0.0) || !counts(cells[i + 1], today) || !hasBit(present, 10)) continue; // bit 10: the open interest
      seen = true;
      if (cells[i + 2] > 0.0) callOi += cells[i + 10];
      else putOi += cells[i + 10];
      if (hasBit(present, 11)) { // bit 11: the prior day's
        change += cells[i + 10] - cells[i + 11];
        changeSeen = true;
      }
    }
  }
  out_call_oi(seen ? callOi : NaN);
  out_put_oi(seen ? putOi : NaN);
  out_pc_oi(seen ? ratio(putOi, callOi) : NaN);
  out_oi_change(changeSeen ? change : NaN);
}

// onBar() runs once per bar: a new trade date starts today's sums again; the bar's flow joins them; the readings on
// every bar, the open interest, the headline and the title on the newest bar.
function onBar(): void {
  const barMs = bar.time() * 1000.0;
  const today = optionsTradeDay(barMs, 1);
  if (today != day) {
    day = today;
    callVolume = 0.0;
    putVolume = 0.0;
    callPremium = 0.0;
    putPremium = 0.0;
    callAsk = 0.0;
    callBid = 0.0;
    putAsk = 0.0;
    putBid = 0.0;
    emptyBars = 0;
  }
  const calls = in_call_volume();
  const puts = in_put_volume();
  if (isNaN(calls) && isNaN(puts) && isNaN(in_call_volume_0dte()) && isNaN(in_put_volume_0dte())) emptyBars += 1;
  callVolume += scoped(calls, in_call_volume_0dte());
  putVolume += scoped(puts, in_put_volume_0dte());
  callPremium += scoped(in_call_premium(), in_call_premium_0dte());
  putPremium += scoped(in_put_premium(), in_put_premium_0dte());
  callAsk += scoped(in_call_ask(), in_call_ask_0dte());
  callBid += scoped(in_call_bid(), in_call_bid_0dte());
  putAsk += scoped(in_put_ask(), in_put_ask_0dte());
  putBid += scoped(in_put_bid(), in_put_bid_0dte());
  const pcVolume = ratio(putVolume, callVolume);
  const sidedPremium = callAsk + callBid + putAsk + putBid;
  out_call_volume_today(callVolume);
  out_put_volume_today(putVolume);
  out_pc_volume(pcVolume);
  out_call_premium_today(callPremium);
  out_put_premium_today(putPremium);
  out_pc_premium(ratio(putPremium, callPremium));
  out_net_premium(callAsk - callBid - (putAsk - putBid));
  out_bull_share(sidedPremium > 0.0 ? ((callAsk + putBid) / sidedPremium) * 100.0 : NaN);
  out_empty_bars(f64(emptyBars));
  if (!bar.isLast()) {
    out_call_oi(NaN);
    out_put_oi(NaN);
    out_pc_oi(NaN);
    out_oi_change(NaN);
    out_tone(NaN);
    return;
  }
  writeOpenInterest(today);
  const traded = callVolume + putVolume > 0.0;
  out_tone(!traded ? 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
  sb_clear();
  if (!traded) sb_text("No contract traded 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();
  dateClock.update(today * 86400.0); // a day number is its date at 00:00 UTC
  sb_clear();
  sb_text("Options today, ");
  if (dateClock.dayOfMonth() < 10) sb_text("0");
  sb_int(dateClock.dayOfMonth());
  sb_text(MONTHS[dateClock.month() - 1]);
  sb_text(SCOPE_WORDS[expiries]);
  str_title_sb();
}
```

## How it works

**Today's sums.** Each bar carries the underlying's option flow of that bar, split by calls and puts and by the side of the quote: the contracts, the premium, and the premium at the ask and at the bid, once over every expiry and once over the same-day expiries alone. Today is the New York trade date of the bar's open: on a new trade date every sum starts again, and each bar's flow joins them. A bar with no flow row (no print in it) adds nothing.

**The expiries.** **Expiries** reads every expiry, only the contracts expiring today (`0DTE only`: the same-day fields), or the ones after today (every expiry minus the same-day fields, never below zero). The open interest rows follow the same choice, the same-day expiry being the one whose New York trade date is today's.

**The sides of the quote.** The data plane judges each print against the national best bid and offer just before it, by the midpoint: above the mid it is at the ask, below it at the bid. `net_premium` is the calls' premium at the ask minus at the bid, minus the puts' premium at the ask minus at the bid: calls bought and puts sold push it up. `bull_share` is the calls' premium at the ask plus the puts' at the bid, over all the premium at the ask or the bid, in percent; the mid and the unquoted prints count in neither.

**The open interest.** On the live bar the chain summary carries each contract's open interest (`oi`, bit 10) and the prior observation (`oi_prev`, bit 11), over the twelve nearest expiries and the strikes within 25% of the underlying (`expiries: "nearest:12"`, `strike_window_pct: 25`, `max_cells: 16000`): a whole chain of a busy underlying holds more contracts than one read carries, so the rows say which expiries they cover. `call_oi` and `put_oi` sum the open interest of each side, `pc_oi` is their ratio, and `oi_change` sums the open interest now minus the prior observation over the contracts that carry both.

**The headline.** The put/call ratio of contracts decides it: within 10% of 1 it reads "Even: put/call 1.03", below "Calls lead: put/call 0.62", above "Puts lead: put/call 1.43", and with puts traded but no call yet "Puts only: no call traded yet". `tone` colours it red while the puts lead, slate when even or with no reading, green while the calls lead. The title names today's date and the expiries read (`Options today, 16OCT, 0DTE`).

**The card reads the outputs.** The headline pill over `read`, two tiles (call contracts and put contracts), then ten numbered rows: the put/call ratio of contracts, call premium, put premium, the put/call premium ratio, the net premium, the bullish share, call open interest, put open interest, the put/call open interest ratio and the open interest change. Today's sums are outputs on every bar (`call_volume_today`, `put_volume_today`, `pc_volume`, `call_premium_today`, `put_premium_today`, `pc_premium`, `net_premium`, `bull_share`, and `empty_bars`, today's bars with no flow row), so the Console reads them through the day; the open interest rows are numbers on the live bar only.

## Where it runs

US stock and ETF charts, through the listed options (OPRA) of the chart's own underlying. The OPRA sources are upcoming: the kit declares them ahead of the chart lanes that will serve them, so this indicator builds today but no surface serves it yet ([Data sources](../core-concepts/data-sources.md#us-listed-options-opra)). Once those lanes ship, the chart in the browser serves them under the US options access rule ([Limitations](../reference/limitations.md)): OpenMarket's official Options Stats package reads them (it is not on the registry yet), and a copy you run from the editor or publish yourself stops with "Paused on US options", as a draft or a community indicator does under that rule. An alert, a watch and a Protected indicator on OpenMarket's servers refuse `options_flow` and `option_contract_state` by name. On an option contract's chart both read the contract's underlying, so the card is the same as on the stock's chart. A chain summary with more contracts than 16,000 inside its window refuses the run, never truncated. The chart routes the reads to the tier the account holds, 15 minutes delayed unless it holds real-time US options, and the legend names that tier.

## When data is missing

- **No flow row.** A bar the data plane holds no flow row for adds nothing to today's sums and counts in `empty_bars`; on a liquid underlying a run of empty bars inside the session is a gap in what the chart served, on a quiet one it is a bar without a print.
- **No trade yet.** Before the first print of the day the headline reads "No contract traded yet today".
- **No chain summary.** Without the summary on the live bar, the four open interest rows read `-`.

## Customize it

- **The same-day crowd.** Set Expiries to 0DTE only on an index ETF: the ratio and the net premium of the contracts expiring today.
- **The positioning that stays.** Set Expiries to after today to leave the same-day churn out of the ratios and the open interest.
- **Another threshold.** Change `EVEN_BAND` (0.1) to widen or narrow the ratios the headline calls even.

## Run it

1. In the editor's Explorer, press the **Templates** icon ("Browse starter templates") and pick **Options Stats** under **HUDs**.
2. Press **Run** on a US stock chart such as SPY: once the chart serves US option flow the card appears at the bottom centre, titled by today's date; until then the legend's chip names the source the chart does not serve yet.
3. At the editor's Console prompt, type `pc_volume` and `net_premium` to read today's put/call ratio and net premium through the newest bar.

## Concepts used

- [Data sources](../core-concepts/data-sources.md#us-listed-options-by-the-charts-underlying) for the `options_flow` fields and their `dte` knob, and [`option_contract_state`](../core-concepts/data-sources.md#option_contract_state), its tuple and [its knobs](../core-concepts/data-sources.md#the-knobs), with [The present mask and partial bars](../core-concepts/data-sources.md#the-present-mask-and-partial-bars) for `hasBit`
- [Options kit](../functions/options-kit.md#the-markets-own-date-and-the-chains-clock) for `optionsTradeDay`
- [HUD cards](../presentation/hud-and-hover-cards.md#hud-cards), [Blocks and tiles](../presentation/hud-and-hover-cards.md#blocks-and-tiles) and [Looks](../presentation/hud-and-hover-cards.md#looks) for the card, its tiles and the slot in the title
