---
title: "Options history"
description: "The chart's underlying's options activity session by session, from the daily options statistics. The View setting picks the pane under the chart: Volume (call…"
order: 154
section: "cookbook"
---

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

# Options history

The chart's underlying's options activity session by session, from the daily options statistics. The **View** setting picks the pane under the chart: Volume (call and put contracts stacked as columns, green calls and red puts, with the average daily volume over the **Average window** as a step line), Open interest (call and put open interest as step lines) or Premium (call and put premium in USD, stacked). A table below the chart lists the newest sessions one per row: the date, the underlying's close, the put/call volume ratio, the volume and its distance from the average, the total open interest and its change from the prior session and the 30-day IV. A card at the bottom right reads the newest session, with the front expiry's implied move.

The parts are scalar `option_daily` inputs, one `DayHistory` per field from the options kit ([Options kit](../functions/options-kit.md#a-history-of-daily-statistics)), stacked `bar` outputs and step lines ([Plotting](../presentation/plotting.md)), a `panel.table` ([Table](../presentation/cards-frames-panels.md#table)), and a `render.hud` card. This is also the `options-history` template: the **Options History** card under **Beyond the time axis** in the editor's starter list, and it compiles as written.

## The wrun indicator

```typescript sample=options-history
// Options History: the chart's underlying's options volume and open interest session by session, from the daily
// options statistics (option_daily). The View setting picks the pane under the chart: Volume (call and put contracts
// stacked as columns, green calls and red puts, with the average daily volume over the Average window as a step
// line), Open interest (call and put open interest as step lines) or Premium (call and put premium in USD, stacked). A
// table below the chart lists the newest sessions one per row: the date, the underlying's close, the put/call volume
// ratio, the volume and its distance from the average, the total open interest and its change from the prior
// session and the 30-day IV. A card at the bottom right reads the newest session and the front expiry's move.
// Volume and premium are the whole session's (the statistics job reads them at the close); open interest is the
// clearing house's figure the session opened with. Each session's reading is held on an intraday chart until the
// next one lands. The statistics are short today (they start when the data plane switches the daily job on): every
// count reads from the first session, the open interest change from the second, and the distance from the average
// needs the Average window's sessions; until then the card says how many there are. US stock charts, daily or finer.

param.choice("view", ["Volume", "Open interest", "Premium"], "Volume", { label: "View", description: "The pane under the chart: contracts traded, open interest, or premium traded" });
param.int("avg_sessions", 30, { min: 5, max: 90, label: "Average window, sessions", description: "Sessions the average daily volume reads" });
param.int("table_rows", 10, { min: 3, max: 30, label: "Table rows", description: "The newest sessions the table lists" });
input("close", ohlcv.close); // the chart's own close: the grid
input("call_vol", option_daily.call_volume, { missing: "nan", description: "Call contracts traded in the session (the daily options statistics)" }); // every daily input lands on the first bar of its day, NaN elsewhere
input("put_vol", option_daily.put_volume, { missing: "nan", description: "Put contracts traded in the session" });
input("call_prem", option_daily.call_premium, { missing: "nan", description: "Call premium traded in the session, USD" });
input("put_prem", option_daily.put_premium, { missing: "nan", description: "Put premium traded in the session, USD" });
input("call_oi", option_daily.call_oi, { missing: "nan", description: "Call open interest, contracts" });
input("put_oi", option_daily.put_oi, { missing: "nan", description: "Put open interest, contracts" });
input("iv30", option_daily.atm_iv_30d, { missing: "nan", description: "The 30-day at-the-money IV, percent" });
input("move", option_daily.implied_move_front, { missing: "nan", description: "The front expiry's implied move, a price distance" });
input("spot", option_daily.underlying_price, { missing: "nan", description: "The underlying's price the statistics read" });
output("calls", bar, lower, { color: "#22c55e", stack: "flow", label: "Calls", format: "si", description: "Call contracts (Volume view) or call premium in USD (Premium view) for the session" });
output("puts", bar, lower, { color: "#ef4444", stack: "flow", label: "Puts", format: "si", description: "Put contracts (Volume view) or put premium in USD (Premium view) for the session" });
output("avg_line", line, lower, { color: "#e2e8f0", width: 1, step: true, label: "Average volume", format: "si", description: "Average daily contracts over the Average window, Volume view" });
output("call_oi_line", line, lower, { color: "#22c55e", width: 2, step: true, label: "Call OI", format: "si", description: "Call open interest, Open interest view" });
output("put_oi_line", line, lower, { color: "#ef4444", width: 2, step: true, label: "Put OI", format: "si", description: "Put open interest, Open interest view" });
output("volume", none, lower, { description: "Contracts traded in the session, calls and puts" });
output("pc_volume", none, lower, { description: "Put contracts over call contracts" });
output("pc_oi", none, lower, { description: "Put open interest over call open interest" });
output("pc_premium", none, lower, { description: "Put premium over call premium" });
output("vol_vs_avg", none, lower, { description: "The session's volume over the average before it, minus 1, percent" });
output("total_oi", none, lower, { description: "Call and put open interest" });
output("oi_change", none, lower, { description: "Total open interest over the prior session's, minus 1, percent" });
output("move_pct", none, lower, { description: "The front expiry's implied move over the underlying's price, percent" });
output("sessions", none, lower, { description: "Sessions of statistics held" });
output("status", none, lower, { description: "0 the average needs more sessions, 1 every reading reads, 2 a chart coarser than a day" });
string("read", { max_bytes: 64 }); // the headline
string("title", { max_bytes: 64 });
const tableRows = frame("history_rows", { max_bytes: 16384 }); // one row per session, newest first
panel.table({ name: "history", title: "Daily options history", x: "category", place: "below", frame: tableRows, height_frac: 0.26, header_text_color: "#94a3b8", text_color: "#e2e8f0", grid_color: "#334155", grid_width: 1, grid_lines: "rows", font_size: 11, series: [
  { name: "Date" },
  { name: "Close", align: "right", format: "price" },
  { name: "P/C", align: "right", format: "0.00" },
  { name: "Volume", align: "right", format: "si" },
  { name: "vs avg, %", align: "right", format: "0.0" },
  { name: "Total OI", align: "right", format: "si" },
  { name: "OI chg, %", align: "right", format: "0.00" },
  { name: "IV 30d, %", align: "right", format: "0.0" },
] });

render.hud("history_card", {
  position: "bottom_right",
  look: "terminal",
  title: "{{title}}",
  columns: 2,
  tiles: [
    tile.pill("Session", "read", { headline: true, color_by: "status", colors: ["#94a3b8", "#e2e8f0", "#94a3b8"] }),
    tile.value("Contracts", "volume", { format: "si" }),
    tile.value("Put/call volume", "pc_volume", { format: "0.00" }),
    tile.rows([
      ["1) vs average, %", "vol_vs_avg", "0.0"],
      ["2) Total OI", "total_oi", "si"],
      ["3) OI change, %", "oi_change", "0.00"],
      ["4) Put/call OI", "pc_oi", "0.00"],
      ["5) Put/call premium", "pc_premium", "0.00"],
      ["6) Front move, %", "move_pct", "0.00"],
      ["7) Sessions held", "sessions", "int"],
    ]),
  ],
});

const DAY = 86400.0;
const CAPACITY = 128; // the sessions each history keeps: the average and the table read no further back
const MONTHS: String[] = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
const callVols = new DayHistory(CAPACITY); // per session, one history per field
const putVols = new DayHistory(CAPACITY);
const totals = new DayHistory(CAPACITY); // calls and puts, for the average
const callPrems = new DayHistory(CAPACITY);
const putPrems = new DayHistory(CAPACITY);
const ois = new DayHistory(CAPACITY); // total open interest
const ivs = new DayHistory(CAPACITY);
const moves = new DayHistory(CAPACITY); // implied move over price, percent
const spots = new DayHistory(CAPACITY);
const sessions = new DayHistory(CAPACITY); // every session that landed, whatever it lacks: the table's rows
const dateClock = new Clock("UTC");

let view = 0; // settings, read in onStart()
let avgSessions = 30;
let tableRowsN = 10;
let prevT: f64 = NaN; // the bar spacing
let barSec: f64 = NaN;
let sessionDay: f64 = NaN; // the newest session read, and its numbers, held
let callVol: f64 = NaN;
let putVol: f64 = NaN;
let callPrem: f64 = NaN;
let putPrem: f64 = NaN;
let callOi: f64 = NaN;
let putOi: f64 = NaN;
let avgNow: f64 = NaN;
let vsAvgNow: f64 = NaN;
let oiChangeNow: f64 = NaN;
let movePctNow: f64 = NaN;

function onStart(): void {
  view = i32(p_view());
  avgSessions = i32(p_avg_sessions());
  tableRowsN = i32(p_table_rows());
}

function ratio(a: f64, b: f64): f64 {
  return b > 0.0 && !isNaN(a) ? a / b : NaN;
}

/** The average daily volume over the avgSessions sessions before the one ago back; NaN while fewer are held. */
function averageBefore(ago: i32): f64 {
  if (totals.count() < ago + 1 + avgSessions) return NaN;
  let sum = 0.0;
  for (let k = ago + 1; k <= ago + avgSessions; k += 1) sum += totals.value(k);
  return sum / f64(avgSessions);
}

/** The session ago back's total open interest over the one before it, minus 1, percent. */
function oiChangeAt(ago: i32): f64 {
  const now = ois.value(ago);
  const before = ois.value(ago + 1);
  return before > 0.0 && !isNaN(now) ? (now / before - 1.0) * 100.0 : NaN;
}

/** The value of h for the session on day; NaN when that session lacks it. */
function onDay(h: DayHistory, day: f64): f64 {
  const ago = h.agoOf(day);
  return ago < 0 ? NaN : h.value(ago);
}

// ── A session lands: every field it carries is kept, then the held readings move ──
function land(day: f64): void {
  sessionDay = day;
  sessions.push(day, 1.0);
  callVol = in_call_vol();
  putVol = in_put_vol();
  callPrem = in_call_prem();
  putPrem = in_put_prem();
  callOi = in_call_oi();
  putOi = in_put_oi();
  const spot = in_spot();
  callVols.push(day, callVol);
  putVols.push(day, putVol);
  if (!isNaN(callVol) && !isNaN(putVol)) totals.push(day, callVol + putVol);
  callPrems.push(day, callPrem);
  putPrems.push(day, putPrem);
  if (!isNaN(callOi) && !isNaN(putOi)) ois.push(day, callOi + putOi);
  ivs.push(day, in_iv30());
  const move = in_move();
  if (spot > 0.0 && !isNaN(move)) moves.push(day, (move / spot) * 100.0);
  spots.push(day, spot);
  const totalAgo = totals.agoOf(day);
  avgNow = totalAgo < 0 ? NaN : averageBefore(totalAgo);
  vsAvgNow = avgNow > 0.0 ? (totals.value(totalAgo) / avgNow - 1.0) * 100.0 : NaN;
  const oiAgo = ois.agoOf(day);
  oiChangeNow = oiAgo < 0 ? NaN : oiChangeAt(oiAgo);
  movePctNow = onDay(moves, day);
}

function sbDay(day: f64): void { // "16OCT"
  dateClock.update(day * DAY);
  if (dateClock.dayOfMonth() < 10) sb_text("0");
  sb_int(dateClock.dayOfMonth());
  sb_text(MONTHS[dateClock.month() - 1]);
}

function fbNumber(v: f64, decimals: i32): void {
  fb_text(",");
  if (isNaN(v)) fb_text("null");
  else fb_f64(v, decimals);
}

// ── The table: the newest sessions, newest first ──
function writeTable(): void {
  fb_clear();
  fb_text("{\"rows\":[");
  const shown = sessions.count() < tableRowsN ? sessions.count() : tableRowsN;
  for (let ago = 0; ago < shown; ago += 1) {
    const day = sessions.day(ago);
    if (ago > 0) fb_text(",");
    fb_text("[\"");
    dateClock.update(day * DAY);
    if (dateClock.dayOfMonth() < 10) fb_text("0");
    fb_int(dateClock.dayOfMonth());
    fb_text(MONTHS[dateClock.month() - 1]);
    fb_text("\"");
    fbNumber(onDay(spots, day), 2);
    fbNumber(ratio(onDay(putVols, day), onDay(callVols, day)), 2);
    const totalAgo = totals.agoOf(day);
    const total = totalAgo < 0 ? NaN : totals.value(totalAgo);
    fbNumber(total, 0);
    const avg = totalAgo < 0 ? NaN : averageBefore(totalAgo);
    fbNumber(avg > 0.0 ? (total / avg - 1.0) * 100.0 : NaN, 1);
    const oiAgo = ois.agoOf(day);
    fbNumber(oiAgo < 0 ? NaN : ois.value(oiAgo), 0);
    fbNumber(oiAgo < 0 ? NaN : oiChangeAt(oiAgo), 2);
    fbNumber(onDay(ivs, day), 1);
    fb_text("]");
  }
  fb_text("],\"caption\":\"");
  if (sessions.count() == 0) fb_text("Waiting for the daily options statistics");
  else {
    fb_int(sessions.count());
    fb_text(sessions.count() == 1 ? " session held" : " sessions held");
    if (totals.count() < avgSessions + 1) {
      fb_text("; the average needs ");
      fb_int(avgSessions);
      fb_text(" sessions before the newest");
    }
  }
  fb_text("\"}");
  writeFrameBuffer(FRAME_HISTORY_ROWS);
}

function writeWords(coarse: bool): void {
  sb_clear();
  if (coarse) sb_text("Needs a daily or finer chart");
  else if (sessions.count() == 0) sb_text("Waiting for the daily options statistics");
  else if (isNaN(vsAvgNow)) {
    sb_text("Not enough history yet: ");
    sb_int(totals.count() > 0 ? totals.count() - 1 : 0);
    sb_text(" of ");
    sb_int(avgSessions);
    sb_text(" sessions for the average");
  } else {
    sb_text(vsAvgNow >= 0.0 ? "Volume over its average by " : "Volume under its average by ");
    sb_f64(Math.abs(vsAvgNow), 0);
    sb_text("%");
  }
  str_read_sb();
  sb_clear();
  sb_text("Options history");
  if (!coarse && sessions.count() > 0) {
    sb_text(", as of ");
    sbDay(sessionDay);
  }
  str_title_sb();
}

// onBar() runs once per bar: the spacing, then the session's statistics when they land; every bar writes the held
// session, so an intraday chart draws each session as a block.
function onBar(): void {
  const t = bar.time();
  if (!isNaN(prevT) && t > prevT && (isNaN(barSec) || t - prevT < barSec)) barSec = t - prevT;
  prevT = t;
  const coarse = !isNaN(barSec) && barSec > 1.5 * DAY;
  if (!coarse) {
    const day = Math.floor(t / DAY);
    const any = !isNaN(in_call_vol()) || !isNaN(in_put_vol()) || !isNaN(in_call_oi()) || !isNaN(in_spot());
    if (any && (isNaN(sessionDay) || day >= sessionDay)) land(day);
  }
  const has = !coarse && !isNaN(sessionDay);
  out_calls(has && view == 0 ? callVol : has && view == 2 ? callPrem : NaN);
  out_puts(has && view == 0 ? putVol : has && view == 2 ? putPrem : NaN);
  out_avg_line(has && view == 0 ? avgNow : NaN);
  out_call_oi_line(has && view == 1 ? callOi : NaN);
  out_put_oi_line(has && view == 1 ? putOi : NaN);
  out_volume(has && !isNaN(callVol) && !isNaN(putVol) ? callVol + putVol : NaN);
  out_pc_volume(has ? ratio(putVol, callVol) : NaN);
  out_pc_oi(has ? ratio(putOi, callOi) : NaN);
  out_pc_premium(has ? ratio(putPrem, callPrem) : NaN);
  out_vol_vs_avg(has ? vsAvgNow : NaN);
  out_total_oi(has && !isNaN(callOi) && !isNaN(putOi) ? callOi + putOi : NaN);
  out_oi_change(has ? oiChangeNow : NaN);
  out_move_pct(has ? movePctNow : NaN);
  out_sessions(coarse ? NaN : f64(sessions.count()));
  out_status(coarse ? 2.0 : isNaN(vsAvgNow) ? 0.0 : 1.0);
  if (bar.isLast()) {
    writeWords(coarse);
    if (!coarse) writeTable();
  }
}
```

## How it works

**What a session holds.** Volume and premium are the whole session's: the statistics job reads them at the close. Open interest is the clearing house's figure the session opened with, so its change from the prior session is the positions opened or closed the session before.

**The average.** Each session's volume is compared with the mean of the `avg_sessions` sessions before it, never with a window that holds itself.

**The table.** On the live bar the module walks the newest `table_rows` sessions and writes one row each; a value a session lacks prints empty, and the row stays.

## Where it runs

US stock charts, through OpenMarket's official package: `option_daily` is served by the chart's browser lane for the chart's own underlying ([Limitations](../reference/limitations.md)). The class is declared ahead of its lane, so the template builds today and the chart shows the lane's refusal until the data plane serves the statistics. A daily or finer chart.

## When data is missing

The statistics start the day the job is switched on. Every count reads from the first session and the open-interest change from the second; the distance from the average needs the **Average window** of sessions before the newest, and until then the card reads "Not enough history yet: 11 of 30 sessions for the average" and the table's caption says the same. Before the first session the card reads "Waiting for the daily options statistics"; a chart coarser than a day reads "Needs a daily or finer chart".

## Customize it

- **Another pane.** Switch **View** to Open interest or Premium.
- **Another average.** `avg_sessions` of 10 compares with the last two weeks.
- **A longer table.** `table_rows` lists up to 30 sessions.

## Run it

1. In the editor's Explorer, press the **Templates** icon and pick **Options History** under **Beyond the time axis**.
2. Press **Run** on a US stock's daily chart.
3. At the editor's Console prompt, type `volume`, `vol_vs_avg` and `oi_change`.

## Concepts used

- [Data sources](../core-concepts/data-sources.md#us-listed-options-by-the-charts-underlying) for `option_daily`
- [Options kit](../functions/options-kit.md#a-history-of-daily-statistics) for `DayHistory`
- [Table](../presentation/cards-frames-panels.md#table) for the table panel
- [HUD cards](../presentation/hud-and-hover-cards.md#hud-cards) for the card
