---
title: "Top contracts"
description: "The chart's most active option contracts on the newest bar, ranked by the day's volume (the default), the open interest, the premium, or the gain since the…"
order: 127
section: "cookbook"
---

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

# Top contracts

The chart's most active option contracts on the newest bar, ranked by the day's volume (the default), the open interest, the premium, or the gain since the prior session's close, from its own option chain summary (the [`option_contract_state`](../core-concepts/data-sources.md#option_contract_state) source). A table under the chart lists the top ten with each contract's last price, change, volume, open interest, volume over open interest, premium and share of the day's volume. The strikes of the three most traded contracts and of the three with the most open interest are drawn across the chart as dashed levels with tags at the price axis, and a card at the bottom of the pane, in the terminal look, leads with the top contract and its reading, then the day's volume and its split between calls and puts.

The parts are a celled `option_contract_state.cells` input read on the live bar ([Data sources](../core-concepts/data-sources.md#us-listed-options-opra)), its `present` bits read with `hasBit()` and its option roots spelled from their symbol codes ([The present mask and partial bars](../core-concepts/data-sources.md#the-present-mask-and-partial-bars)), a `panel.table` over a frame of styled cells ([Cards, frames and panels](../presentation/cards-frames-panels.md#table)), line and label handles for the levels ([Drawing objects](../presentation/drawing-objects.md#handles)) and a `render.hud` card with `look: "terminal"` ([HUD cards](../presentation/hud-and-hover-cards.md#hud-cards)). This is also the `top-contracts` template: the **Top Contracts** card under **HUDs** in the editor's starter list, and it compiles as written.

## The wrun indicator

```typescript sample=top-contracts
// Top Contracts: the chart's most active option contracts on the newest bar, from its own option chain summary (the
// option_contract_state source). The Rank setting orders them by the day's volume, the open interest, the premium (the
// day's volume at the last price times 100 shares: an estimate, since the summary carries no traded premium), or the
// gain since the prior session's close (a contract needs the Smallest volume to rank by gain), and a table under the
// chart lists the top ones with the last price, the change, the volume, the open interest, volume over open interest,
// the premium and the share of the day's volume. The strikes of the three most traded contracts and of the three with
// the most open interest are drawn across the chart as dashed levels with tags (on the underlying's own chart: the
// close inside the strikes the summary lists), and a card in the terminal look leads with the top contract. The summary
// is a snapshot of now: history bars draw nothing, and everything is measured on the last bar.

section("Ranking");
param.choice("rank", ["volume", "oi", "premium", "gain"], "volume", { label: "Rank by", description: "volume: the day's; oi: open interest; premium: the day's volume at the last price, times 100; gain: the last price over the prior close" });
param.choice("side", ["both", "calls", "puts"], "both", { label: "Side", description: "The contracts the table and the levels rank" });
param.int("rows", 10, { min: 3, max: 25, label: "Rows", description: "Contracts in the table" });
param.int("min_volume", 100, { min: 0, max: 1000000, label: "Smallest volume for gain", description: "A contract needs this many contracts traded today to rank by gain" });
param.bool("levels", true, { label: "Strike levels", description: "Draw the strikes of the three most traded contracts and the three with the most open interest across the chart" });
input("close", ohlcv.close); // the chart's own close: whether the strikes belong on this chart's price axis
input("state", option_contract_state.cells, { max_cells: 24000, expiries: "nearest:24", strike_window_pct: 10, description: "The chart's own option chain summary on the newest bar: the 24 nearest expiries, strikes within 10% of the underlying" }); // [root_code, expiry_ms, side_cp, strike, bid, ask, last, prev_close, theta, volume_day, oi, oi_prev, present] per contract
market.zone(); // the market's clock: each expiry's trade date
// The readings, data-only: the card's tiles read them and the Console can too. Each is NaN on history bars.
output("top_strike", none, overlay, { description: "The top contract's strike; live bar only" });
output("top_value", none, overlay, { description: "The top contract's reading on the Rank setting: contracts, USD or percent" });
output("total_volume", none, overlay, { description: "The day's volume of every contract the summary lists, contracts" });
output("call_volume", none, overlay, { description: "The calls' day volume, contracts" });
output("put_volume", none, overlay, { description: "The puts' day volume, contracts" });
output("put_call_volume", none, overlay, { description: "Put volume over call volume" });
output("traded", none, overlay, { description: "Contracts that traded today" });
output("partial", none, overlay, { description: "1 when the data plane marked the summary partial, else 0" });
output("top_side", none, overlay, { description: "The top contract's side, the headline's colour: 0 a put, 1 none, 2 a call" });
string("read", { max_bytes: 64 }); // the headline: the top contract and its reading, or why there is none
string("title", { max_bytes: 80 }); // the card's title: the rank and what the summary held
string("tag", { max_bytes: 40 }); // one bounded slot the six level tags are written through, one at a time
const top = frame("top", { max_bytes: 16384 }); // the table's rows
panel.table({ name: "top_contracts", title: "Most active contracts", x: "category", place: "below", frame: top, height_frac: 0.25, header_column: true, header_text_color: "theme.muted", grid_lines: "rows", grid_color: "theme.grid", grid_width: 1, valign: "middle", cell_padding: 4, font_size: 11, series: [{ name: "Contract" }, { name: "Last", align: "right", format: "0.00" }, { name: "Change", align: "right", format: "0.0", signed: true, unit: "%" }, { name: "Volume", align: "right", format: "int" }, { name: "OI", align: "right", format: "int" }, { name: "Vol/OI", align: "right", format: "0.00" }, { name: "Premium", align: "right", format: "usd" }, { name: "Share", align: "right", format: "0.0", unit: "%" }] });
handles.line({ width: 1, extend: "both" }); // the six strike levels, across the whole chart
handles.label({ anchor: "right", align: "right", size: 11, color: "#e2e8f0" }); // their tags: x in pixels from the right edge, y in price

// The card, in the terminal look: the top contract leads as the headline, sky on a call and violet on a put, then the
// day's volume and its split.
render.hud("top_contracts", {
  position: "bottom_center",
  look: "terminal",
  title: "{{title}}",
  columns: 2,
  tiles: [
    tile.pill("Top contract", "read", { headline: true, color_by: "top_side", colors: ["#a78bfa", "#94a3b8", "#38bdf8"] }),
    tile.value("Day volume", "total_volume", { format: "si" }),
    tile.value("Put/call volume", "put_call_volume", { format: "0.00" }),
    tile.rows([
      ["1) Top strike", "top_strike", "price"],
      ["2) Its reading", "top_value", "auto"],
      ["3) Calls traded", "call_volume", "si"],
      ["4) Puts traded", "put_volume", "si"],
      ["5) Contracts that traded", "traded", "int"],
    ]),
  ],
});

const TUPLE = 13; // f64s per contract: root_code, expiry_ms, side_cp, strike, bid, ask, last, prev_close, theta, volume_day, oi, oi_prev, present
const BIT_LAST = 6; // present bits: last, prev_close, volume_day, oi
const BIT_PREV_CLOSE = 7;
const BIT_VOLUME = 9;
const BIT_OI = 10;
const MAX_ROWS = 25; // the Rows setting's cap
const MAX_EXPIRIES = 64; // distinct expiries counted for the title
const LEVELS = 3; // levels per list: the most traded, the most open interest
const RANK_VOLUME = 0;
const RANK_OI = 1;
const RANK_PREMIUM = 2;
const RANK_GAIN = 3;
const RANK_WORDS: String[] = ["volume", "open interest", "premium", "gain"];
const MONTHS: String[] = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
const SYMBOL_CHARS: String[] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "."]; // a symbol code's characters, value 1 to 37
const SKY = rgba(56, 189, 248, 255);
const AMBER = rgba(248, 192, 0, 255);
const PILL = rgba(15, 23, 42, 235); // the tags' backdrop
const expiryMs = new StaticArray<f64>(MAX_EXPIRIES); // the listed expiries, ascending, for the count in the title
const rankAt = new StaticArray<i32>(MAX_ROWS); // the table: each contract's offset in the block, and its key, the largest first
const rankKey = new StaticArray<f64>(MAX_ROWS);
const volAt = new StaticArray<i32>(LEVELS); // the most traded contracts, for the levels
const volKey = new StaticArray<f64>(LEVELS);
const oiAt = new StaticArray<i32>(LEVELS); // the contracts with the most open interest
const oiKey = new StaticArray<f64>(LEVELS);
const rootDigits = new StaticArray<i32>(10); // a root's characters, the last first, while it is spelled
const lines: LineHandle[] = [draw.line(0), draw.line(1), draw.line(2), draw.line(3), draw.line(4), draw.line(5)]; // the three volume levels, then the three open interest levels
const tags: LabelHandle[] = [draw.label(6), draw.label(7), draw.label(8), draw.label(9), draw.label(10), draw.label(11)]; // ids are one space across kinds
const tagPrices = new StaticArray<f64>(6); // the tags placed so far on this bar, so a level near another steps inward
const tagSlots = new StaticArray<i32>(6); // the step inward each placed tag took
const dateClock = new Clock("UTC"); // reads a date from its day number

let rank = RANK_VOLUME; // settings, read in onStart()
let side = 0; // 0 both, 1 calls, 2 puts
let rowsWanted = 10;
let minVolume = 100.0;
let drawLevels = true;
let zone = 0;
let close: f64 = NaN; // this bar
let t: f64 = NaN;
let firstT: f64 = NaN; // the first bar's open time: where the levels start
let rankCount = 0;
let volCount = 0;
let oiCount = 0;
let expiries = 0; // distinct expiries the summary lists
let contracts = 0; // contracts it lists
let traded = 0; // contracts with volume today
let partial = false;
let totalVolume = 0.0;
let callVolume = 0.0;
let putVolume = 0.0;
let lowStrike: f64 = NaN; // the strikes the summary lists, lowest and highest
let highStrike: f64 = NaN;

function onStart(): void {
  rank = i32(p_rank());
  side = i32(p_side());
  rowsWanted = i32(p_rows());
  if (rowsWanted > MAX_ROWS) rowsWanted = MAX_ROWS;
  minVolume = p_min_volume();
  drawLevels = pb_levels();
  zone = i32(p_market_zone());
}

// ── The rankings: insertion into a short list, the largest first, ties kept in the summary's order ──
function insert(at: StaticArray<i32>, keys: StaticArray<f64>, count: i32, cap: i32, offset: i32, key: f64): i32 { // returns the new count
  let k = count < cap ? count : cap;
  if (k == cap && key <= keys[cap - 1]) return count;
  if (k == cap) k = cap - 1;
  while (k > 0 && keys[k - 1] < key) {
    keys[k] = keys[k - 1];
    at[k] = at[k - 1];
    k -= 1;
  }
  keys[k] = key;
  at[k] = offset;
  return count < cap ? count + 1 : cap;
}
function sideKept(isCall: bool): bool {
  return side == 0 || (side == 1) == isCall;
}
function keyOf(cells: StaticArray<f64>, i: i32): f64 { // the contract's reading on the Rank setting; NaN when it does not rank
  const present = cells[i + 12];
  const volume = hasBit(present, BIT_VOLUME) ? cells[i + 9] : NaN;
  const last = hasBit(present, BIT_LAST) ? cells[i + 6] : NaN;
  if (rank == RANK_VOLUME) return volume > 0.0 ? volume : NaN;
  if (rank == RANK_OI) return hasBit(present, BIT_OI) && cells[i + 10] > 0.0 ? cells[i + 10] : NaN;
  if (rank == RANK_PREMIUM) return volume > 0.0 && last > 0.0 ? volume * last * 100.0 : NaN;
  const prev = hasBit(present, BIT_PREV_CLOSE) ? cells[i + 7] : NaN;
  if (!(volume >= minVolume) || !(volume > 0.0) || !(last > 0.0) || !(prev > 0.0)) return NaN; // gain: traded enough, both prices
  return (last / prev - 1.0) * 100.0;
}

// ── The summary, measured on the live bar ──
function measure(n: i32): void {
  const cells = in_state_view(); // the live summary in place: the first n values of the build's own buffer
  rankCount = 0;
  volCount = 0;
  oiCount = 0;
  expiries = 0;
  contracts = 0;
  traded = 0;
  partial = false;
  totalVolume = 0.0;
  callVolume = 0.0;
  putVolume = 0.0;
  lowStrike = NaN;
  highStrike = NaN;
  for (let i = 0; i + TUPLE - 1 < n; i += TUPLE) {
    const present = cells[i + 12];
    if (isPartial(present)) partial = true;
    const strike = cells[i + 3];
    if (!(strike > 0.0) || !isFinite(cells[i + 1])) continue;
    contracts += 1;
    noteExpiry(cells[i + 1]);
    if (isNaN(lowStrike) || strike < lowStrike) lowStrike = strike;
    if (isNaN(highStrike) || strike > highStrike) highStrike = strike;
    const isCall = cells[i + 2] > 0.0;
    const volume = hasBit(present, BIT_VOLUME) ? cells[i + 9] : 0.0;
    if (volume > 0.0) {
      traded += 1;
      totalVolume += volume;
      if (isCall) callVolume += volume;
      else putVolume += volume;
    }
    if (!sideKept(isCall)) continue;
    const key = keyOf(cells, i);
    if (!isNaN(key)) rankCount = insert(rankAt, rankKey, rankCount, rowsWanted, i, key);
    if (volume > 0.0) volCount = insert(volAt, volKey, volCount, LEVELS, i, volume);
    if (hasBit(present, BIT_OI) && cells[i + 10] > 0.0) oiCount = insert(oiAt, oiKey, oiCount, LEVELS, i, cells[i + 10]);
  }
}
function noteExpiry(ms: f64): void { // a distinct expiry into the sorted list; past the cap only the count stops
  let lo = 0;
  let hi = expiries;
  while (lo < hi) {
    const mid = (lo + hi) >> 1;
    if (expiryMs[mid] < ms) lo = mid + 1;
    else hi = mid;
  }
  if ((lo < expiries && expiryMs[lo] == ms) || expiries >= MAX_EXPIRIES) return;
  for (let k = expiries; k > lo; k -= 1) expiryMs[k] = expiryMs[k - 1];
  expiryMs[lo] = ms;
  expiries += 1;
}

// ── Text helpers, written into the frame buffer and the line buffer without allocating ──
function rootCount(code: f64): i32 { // the characters of the root a symbol code names, the last first in rootDigits; 0 when it names none
  if (!isFinite(code) || code < 1.0 || code > 6278211847988223.0 || code != Math.floor(code)) return 0;
  let rest = u64(code);
  let count = 0;
  while (rest > 0) {
    const value = i32(rest % 38);
    if (value == 0 || count >= 10) return 0; // no text encodes to a zero digit
    rootDigits[count] = value;
    count += 1;
    rest = rest / 38;
  }
  return count;
}
function strikeDecimals(v: f64): i32 { // a strike as it is quoted: 660, 662.5, 0.25
  return v == Math.floor(v) ? 0 : v * 10.0 == Math.floor(v * 10.0) ? 1 : 2;
}
function fbContract(cells: StaticArray<f64>, i: i32): void { // "\"SPY 17OCT 660C\"", a root that spells nothing as "?"
  fb_text("\"");
  const count = rootCount(cells[i]);
  if (count == 0) fb_text("?");
  for (let k = count - 1; k >= 0; k -= 1) fb_text(SYMBOL_CHARS[rootDigits[k] - 1]);
  fb_text(" ");
  dateClock.update(optionsTradeDay(cells[i + 1], zone) * 86400.0); // the expiry's trade date, read as a date
  if (dateClock.dayOfMonth() < 10) fb_text("0");
  fb_int(dateClock.dayOfMonth());
  fb_text(MONTHS[dateClock.month() - 1]);
  fb_text(" ");
  const decimals = strikeDecimals(cells[i + 3]);
  if (decimals == 0) fb_int(i64(cells[i + 3]));
  else fb_f64(cells[i + 3], decimals);
  fb_text(cells[i + 2] > 0.0 ? "C\"" : "P\"");
}
function sbContract(cells: StaticArray<f64>, i: i32): void { // the same words in the line buffer, unquoted
  const count = rootCount(cells[i]);
  if (count == 0) sb_text("?");
  for (let k = count - 1; k >= 0; k -= 1) sb_text(SYMBOL_CHARS[rootDigits[k] - 1]);
  sb_text(" ");
  dateClock.update(optionsTradeDay(cells[i + 1], zone) * 86400.0);
  if (dateClock.dayOfMonth() < 10) sb_text("0");
  sb_int(dateClock.dayOfMonth());
  sb_text(MONTHS[dateClock.month() - 1]);
  sb_text(" ");
  const decimals = strikeDecimals(cells[i + 3]);
  if (decimals == 0) sb_int(i64(cells[i + 3]));
  else sb_f64(cells[i + 3], decimals);
  sb_text(cells[i + 2] > 0.0 ? "C" : "P");
}
function sbShort(v: f64): void { // "84.2K", "1.3M", "850"
  const a = Math.abs(v);
  if (v < 0.0) sb_text("-");
  if (a >= 1.0e9) {
    sb_f64(a / 1.0e9, 2);
    sb_text("B");
  } else if (a >= 1.0e6) {
    sb_f64(a / 1.0e6, 1);
    sb_text("M");
  } else if (a >= 1.0e3) {
    sb_f64(a / 1.0e3, 1);
    sb_text("K");
  } else sb_f64(a, 0);
}

// ── The table, appended as JSON text without allocating ──
function writeTable(): void {
  const cells = in_state_view();
  fb_clear();
  fb_text("{\"caption\":\"");
  if (rankCount == 0) fb_text("No contract ranks on this setting");
  else {
    fb_text("Ranked by ");
    fb_text(RANK_WORDS[rank]);
    if (rank == RANK_PREMIUM) fb_text(": the day's volume at the last price, times 100, an estimate");
  }
  fb_text("\",\"rows\":[");
  for (let k = 0; k < rankCount; k += 1) {
    const i = rankAt[k];
    const present = cells[i + 12];
    const last = hasBit(present, BIT_LAST) ? cells[i + 6] : NaN;
    const prev = hasBit(present, BIT_PREV_CLOSE) ? cells[i + 7] : NaN;
    const volume = hasBit(present, BIT_VOLUME) ? cells[i + 9] : NaN;
    const oi = hasBit(present, BIT_OI) ? cells[i + 10] : NaN;
    if (k > 0) fb_text(",");
    fb_text("[");
    fbContract(cells, i);
    fb_text(",");
    fb_num(last);
    fb_text(",");
    const change = last > 0.0 && prev > 0.0 ? (last / prev - 1.0) * 100.0 : NaN;
    if (isNaN(change)) fb_text("null");
    else {
      fb_text("{\"value\":");
      fb_num(change);
      fb_text(change >= 0.0 ? ",\"text_color\":\"theme.up\"}" : ",\"text_color\":\"theme.down\"}");
    }
    fb_text(",");
    fb_num(volume);
    fb_text(",");
    fb_num(oi);
    fb_text(",");
    fb_num(volume >= 0.0 && oi > 0.0 ? volume / oi : NaN);
    fb_text(",");
    fb_num(volume >= 0.0 && last > 0.0 ? volume * last * 100.0 : NaN);
    fb_text(",");
    fb_num(volume >= 0.0 && totalVolume > 0.0 ? (100.0 * volume) / totalVolume : NaN);
    fb_text("]");
  }
  fb_text("]}");
  writeFrameBuffer(top);
}

// ── The levels: the most traded strikes and the strikes with the most open interest, tagged at the price axis ──
function near(a: f64, b: f64): bool { // two levels within 0.3% of each other would share a tag row
  return !isNaN(a) && !isNaN(b) && Math.abs(a - b) <= Math.abs(b) * 0.003;
}
function stepTaken(which: i32, price: f64, step: i32): bool { // an earlier tag within 0.3% of this price already sits at this step
  for (let j = 0; j < which; j += 1) if (tagSlots[j] == step && near(tagPrices[j], price)) return true;
  return false;
}
function clearLevel(which: i32): void {
  lines[which].delete(); // no-ops when never drawn
  tags[which].delete();
  tagPrices[which] = NaN;
  tagSlots[which] = -1;
}
function drawLevel(which: i32, cells: StaticArray<f64>, i: i32, word: String, value: f64, ink: i32, style: LineStyle): void {
  const strike = cells[i + 3];
  lines[which].set(firstT, strike, t, strike).color(ink).width(1.0).style(style).extend(Extend.Both);
  let step = 0; // the first step inward that no earlier tag within 0.3% holds
  while (stepTaken(which, strike, step)) step += 1;
  tagPrices[which] = strike;
  tagSlots[which] = step;
  sb_clear();
  sb_text(word);
  sb_text(" ");
  sbContract(cells, i);
  sb_text(" ");
  sbShort(value);
  tags[which].set(56.0 + 132.0 * f64(step), strike).text(str_tag_sb).color(ink).fill(PILL); // 56 px in from the right edge clears the axis tags; each step is 132 px further in
}
function writeLevels(onThisChart: bool): void {
  const cells = in_state_view();
  for (let k = 0; k < LEVELS; k += 1) {
    if (drawLevels && onThisChart && k < volCount) drawLevel(k, cells, volAt[k], k == 0 ? "V1" : k == 1 ? "V2" : "V3", volKey[k], SKY, LineStyle.Dashed);
    else clearLevel(k);
  }
  for (let k = 0; k < LEVELS; k += 1) {
    if (drawLevels && onThisChart && k < oiCount) drawLevel(LEVELS + k, cells, oiAt[k], k == 0 ? "OI1" : k == 1 ? "OI2" : "OI3", oiKey[k], AMBER, LineStyle.Dotted);
    else clearLevel(LEVELS + k);
  }
}

// ── The card's words ──
function writeWords(): void {
  const cells = in_state_view();
  sb_clear();
  if (rankCount == 0) sb_text(traded == 0 ? "No contract traded today" : "No contract ranks on this setting");
  else {
    sbContract(cells, rankAt[0]);
    sb_text(": ");
    if (rank == RANK_PREMIUM) sb_text("$");
    sbShort(rankKey[0]);
    if (rank == RANK_GAIN) sb_text("%");
  }
  str_read_sb();
  sb_clear();
  sb_text("Top contracts by ");
  sb_text(RANK_WORDS[rank]);
  sb_text(", ");
  sb_int(expiries);
  sb_text(expiries == 1 ? " expiry, " : " expiries, ");
  sb_int(contracts);
  sb_text(" contracts");
  if (partial) sb_text(", partial summary");
  str_title_sb();
}

// onBar() runs once per bar: history rows carry an empty block and cost one read; the live bar measures the summary,
// writes the readings (NaN on history rows), the table, the levels and the card's words.
function onBar(): void {
  close = bar.close();
  t = bar.time();
  if (isNaN(firstT) && !isNaN(t)) firstT = t;
  let measured = false; // the live bar carried a summary with a contract in it
  let stateCells = -1;
  if (bar.isLast() && !isNaN(t)) {
    stateCells = in_state_cells();
    if (stateCells >= TUPLE) {
      measure(stateCells);
      measured = contracts > 0;
    }
  }
  const cells = in_state_view();
  const topAt = measured && rankCount > 0 ? rankAt[0] : -1;
  out_top_strike(topAt >= 0 ? cells[topAt + 3] : NaN);
  out_top_value(topAt >= 0 ? rankKey[0] : NaN);
  out_total_volume(measured ? totalVolume : NaN);
  out_call_volume(measured ? callVolume : NaN);
  out_put_volume(measured ? putVolume : NaN);
  out_put_call_volume(measured && callVolume > 0.0 ? putVolume / callVolume : NaN);
  out_traded(measured ? f64(traded) : NaN);
  out_partial(measured ? (partial ? 1.0 : 0.0) : NaN);
  out_top_side(topAt >= 0 ? (cells[topAt + 2] > 0.0 ? 2.0 : 0.0) : bar.isLast() ? 1.0 : NaN);
  if (!bar.isLast()) return;
  if (measured) {
    writeTable();
    // The levels belong on the underlying's own chart: its close inside the strikes the summary lists.
    writeLevels(!isNaN(close) && close >= lowStrike && close <= highStrike);
    writeWords();
  } else {
    writeLevels(false);
    sb_clear();
    sb_text(stateCells < TUPLE ? "Waiting for the option chain summary" : "No contract in the summary");
    str_read_sb();
    sb_clear();
    sb_text("Top contracts, no summary read");
    str_title_sb();
  }
}
```

## How it works

**The summary is a snapshot of now.** `input("state", option_contract_state.cells, { max_cells: 24000, expiries: "nearest:24", strike_window_pct: 10 })` delivers one 13-value tuple per contract of the 24 nearest expiries with a strike within 10% of the underlying, on the live row; history bars carry an empty block, so the ranking runs on the last bar.

**The four rankings.** A contract ranks only on readings its `present` mask carries:

- **volume**: the day's volume (bit 9), above 0;
- **oi**: the open interest (bit 10), above 0;
- **premium**: the day's volume times the last price (bit 6) times 100 shares. The summary carries no traded premium, so this is the day's volume valued at the last price: an estimate that reads high when the price rose through the day and low when it fell, and assumes the standard 100-share contract;
- **gain**: the last price over the prior session's close (bit 7), minus 1, in percent, among the contracts that traded at least **Smallest volume for gain** today (100 by default), so a contract that changed hands once does not lead.

**Side** keeps the calls or the puts alone. The top **Rows** (10 by default, at most 25) are kept by insertion as the summary is read, the largest first, ties in the summary's order.

**The table.** Each row names the contract (`SPY 17OCT 660C`: the root from its symbol code, `?` when the code spells nothing, the expiry's trade date on the market's clock, the strike, C or P), then the last price, the change over the prior close in the up or down colour, the day's volume, the open interest, volume over open interest (above 1 when the day traded more than the open interest: positions opened or turned over), the premium as above and the contract's share of the day's volume across the summary. A reading a contract does not carry prints nothing. The caption names the ranking and, on premium, says it is an estimate.

**The levels.** The three most traded contracts on the **Side** setting draw their strikes as dashed sky lines across the chart (`V1 SPY 17OCT 660C 84.2K`) and the three with the most open interest as dotted amber lines (`OI1 ...`), each tagged at the price axis; a tag within 0.3% of an earlier one steps 132 px inward. They draw only on the underlying's own chart, where the close lies inside the strikes the summary lists: on an option contract's chart the strikes are not on its price axis. **Strike levels** turns them off.

**The card.** The headline is the top contract and its reading (`SPY 17OCT 660C: 84.2K`, a dollar amount on premium, a percent on gain), sky on a call and violet on a put; then the day's volume, put volume over call volume, the top strike and reading, the calls and the puts traded, and the contracts that traded at all. The title names the ranking and how many expiries and contracts the summary held, and says "partial summary" when the data plane marked it so.

## Where it runs

US stock and ETF charts with US listed options, through OpenMarket's official wrun indicator, under the US options rule ([Limitations](../reference/limitations.md)). The `option_contract_state` source is upcoming: the kit declares it ahead of the chart lane that will serve it, so the package builds, and the chart shows its refusal until that lane ships.

## When data is missing

The headline says why instead of the card going blank: "Waiting for the option chain summary" while the live bar carries none, "No contract in the summary" for an empty one, "No contract traded today" before the open, and "No contract ranks on this setting" when no contract carries the ranking's readings (the gain needs the last price, the prior close and the volume floor). An empty table keeps its caption.

## Customize it

- **Where the positions sit.** Set **Rank by** to oi for the contracts with the most open interest; the levels already show both.
- **The day's winners.** Set **Rank by** to gain and raise **Smallest volume for gain** to keep it to contracts that really traded.
- **Calls only.** Set **Side** to calls: the table and the levels rank the calls.
- **A longer list.** Raise **Rows** to 25.

## Run it

1. In the editor's Explorer, press the **Templates** icon ("Browse starter templates") and pick **Top Contracts** under **HUDs**.
2. Press **Run** on a US stock or ETF chart: the table opens under the chart once the summary arrives, the six levels draw across it and the card sits at the bottom centre.
3. At the editor's Console prompt, type `top_strike`, `total_volume` and `put_call_volume` to read them on the live bar.

## Concepts used

- [Data sources](../core-concepts/data-sources.md#us-listed-options-opra) for the `option_contract_state` celled class, its tuple, its knobs and the present mask
- [Cards, frames and panels](../presentation/cards-frames-panels.md#table) for `panel.table` and its styled cells
- [Drawing objects](../presentation/drawing-objects.md#handles) for line handles extended across the chart and right-anchored tags
- [HUD cards](../presentation/hud-and-hover-cards.md#hud-cards) and [Looks](../presentation/hud-and-hover-cards.md#looks) for the card
