Premium by expiry

View as MarkdownOpen the editor

Which expiries the session's directional option money on the chart's underlying went into, as one bar per expiry under the chart.

Each contract that traded adds its premium by side: calls bought at the ask and puts sold at the bid are bullish premium, puts bought at the ask and calls sold at the bid bearish. Net, the default, draws bullish minus bearish per expiry, green above zero and red below; Sides draws the two apart, bullish up and bearish down. The bars run from the session's first bar over the nearest expiries that traded, each labelled by its date and its days to go.

The parts are a celled option_contract_flow.cells input, one tuple per contract that traded in each bar with its present mask last (Data sources), the options kit's optionsTradeDay for each expiry's date (Options kit), and a panel.bars below the chart with a per-run badge (Cards, frames and panels). This is also the premium-by-expiry template: the Premium by Expiry card under Beyond the time axis in the editor's starter list, and it compiles as written.

The wrun indicator

// Premium by Expiry: which expiries the session's directional option money went into, on the chart's underlying,
// as one bar per expiry under the chart. Each contract that traded adds its premium by side: calls bought at the
// ask and puts sold at the bid are bullish premium, puts bought at the ask and calls sold at the bid bearish. Net
// (the default) draws bullish minus bearish per expiry, green above zero and red below; Sides draws the two apart,
// bullish up and bearish down. The sums run from the session's first bar (each trade date starts again), over the
// nearest expiries that traded, labelled by date and days to go. Premium at the mid or with no quote has no side
// and counts in neither. The cells come from option_contract_flow, one tuple per contract that traded in each bar:
// a bar whose block was cut down (the partial bit, 14) is a floor, and the panel's badge says so; a tuple whose
// contract is not whole (a root the symbol codec refuses, no expiry, a side other than call or put, no strike) is
// skipped and counted. 1-minute cells reach back 7 days and 5-minute cells 30 days.

section("Expiries");
param.choice("view", ["net", "sides"], "net", { label: "View", description: "net: bullish minus bearish premium per expiry; sides: the bullish premium up and the bearish premium down" });
param.int("expiries", 12, { min: 1, max: 40, label: "Expiries shown", description: "The nearest expiries that traded this session" });
input("close", ohlcv.close); // the chart's own candles: the bar grid
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("flow", option_contract_flow.cells, { max_cells: 6000, description: "Each contract that traded in the bar: [root_code, expiry_ms, side_cp, strike, vol_ask, vol_bid, vol_mid, vol_none, prem_ask, prem_bid, share_delta_net, vwap, last_price, iv_last, vol_above_ask, vol_below_bid, present]" }); // every expiry and strike of the chart's underlying
// The readings, data-only: the Console reads them.
output("session_net", none, overlay, { description: "Bullish minus bearish premium over every expiry this session, USD" });
output("top_days", none, overlay, { description: "Days to go of the expiry holding the largest net premium either way this session" });
output("top_net", none, overlay, { description: "That expiry's net premium, USD" });
output("partial_bars", none, overlay, { description: "Bars of this session whose block was cut down (bit 14): the sums are a floor while it is above 0" });
output("skipped", none, overlay, { description: "Tuples of this session skipped because their contract is not whole" });
string("note", { max_bytes: 72 }); // the one line while there is nothing to draw, or the sums are a floor
const net_rows = frame("net_rows", { max_bytes: 8192 });
const side_rows = frame("side_rows", { max_bytes: 12288 });
panel.bars({ name: "net_by_expiry", title: "Net premium by expiry", x: "category", place: "below", frame: net_rows, orientation: "vertical", chrome: "grid", color_mode: "sign", positive_color: "#22c55e", negative_color: "#ef4444", series: [{ name: "Net" }], format: "usd", hover_card: true, legend_style: "title", height_frac: 0.2 });
panel.bars({ name: "sides_by_expiry", title: "Bullish and bearish premium by expiry", x: "category", place: "below", frame: side_rows, orientation: "vertical", chrome: "grid", color_mode: "series", series: [{ name: "Bullish", color: "#22c55e" }, { name: "Bearish", color: "#ef4444" }], format: "usd", hover_card: true, legend_style: "title", height_frac: 0.2 });
render.label("note_label", { position: "top_center", text: "note", color: "#94a3b8", style: "knockout", offset: [0, 8] });

const TUPLE = 17; // f64s per contract: OPTION_CONTRACT_FLOW_TUPLE
const MAX_EXPIRIES = 96; // expiries kept a session, the nearest when more trade
const NEW_YORK = 1; // OPRA's calendar: an expiry's date and days to go
const MONTHS: StaticArray<string> = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
const expiryAt = new StaticArray<f64>(MAX_EXPIRIES); // the session's expiries, ascending
const bullAt = new StaticArray<f64>(MAX_EXPIRIES);
const bearAt = new StaticArray<f64>(MAX_EXPIRIES);
const dateClock = new Clock("UTC"); // reads a date from its day number

let sides = false; // settings, read in onStart()
let shown = 12;
let day: f64 = NaN; // the trade date the sums run over
let expiryCount = 0;
let partialBars = 0;
let skipped = 0;
let runCells = 0; // tuples read over the whole run

function onStart(): void {
  sides = p_view() == 1.0;
  shown = i32(p_expiries());
}

// The symbol codec's rule without decoding: a whole number from 1 to 38^10 - 1 with no zero digit in base 38.
function rootOk(code: f64): bool {
  if (!(code >= 1.0) || code > 6278211847988223.0 || code != Math.floor(code)) return false;
  let rest = code;
  while (rest > 0.0) {
    const digit = rest % 38.0;
    if (digit == 0.0) return false;
    rest = (rest - digit) / 38.0;
  }
  return true;
}

// A tuple counts when its contract is whole and its present mask is a whole number from 0 up.
function contractOk(cells: StaticArray<f64>, i: i32): bool {
  const present = cells[i + 16];
  const side = cells[i + 2];
  return rootOk(cells[i]) && cells[i + 1] > 0.0 && (side == 1.0 || side == -1.0) && cells[i + 3] > 0.0 && present >= 0.0 && present == Math.floor(present);
}

// The row of an expiry, inserted when new; -1 when the table is full and the expiry is later than every kept one.
function expirySlot(ms: f64): i32 {
  let lo = 0;
  let hi = expiryCount;
  while (lo < hi) {
    const mid = (lo + hi) >> 1;
    if (expiryAt[mid] < ms) lo = mid + 1;
    else hi = mid;
  }
  if (lo < expiryCount && expiryAt[lo] == ms) return lo;
  if (expiryCount >= MAX_EXPIRIES) {
    if (lo >= expiryCount) return -1;
    expiryCount -= 1; // the latest makes room
  }
  for (let k = expiryCount; k > lo; k -= 1) {
    expiryAt[k] = expiryAt[k - 1];
    bullAt[k] = bullAt[k - 1];
    bearAt[k] = bearAt[k - 1];
  }
  expiryAt[lo] = ms;
  bullAt[lo] = 0.0;
  bearAt[lo] = 0.0;
  expiryCount += 1;
  return lo;
}

// One bar's block into the session's sums.
function fold(n: i32): void {
  const cells = in_flow_view();
  let partial = false;
  for (let i = 0; i + TUPLE - 1 < n; i += TUPLE) {
    if (!contractOk(cells, i)) {
      skipped += 1;
      continue;
    }
    if (isPartial(cells[i + 16])) partial = true;
    runCells += 1;
    const slot = expirySlot(cells[i + 1]);
    if (slot < 0) continue;
    const call = cells[i + 2] > 0.0;
    const premAsk = cells[i + 8];
    const premBid = cells[i + 9];
    bullAt[slot] += call ? premAsk : premBid; // calls bought, puts sold
    bearAt[slot] += call ? premBid : premAsk; // puts bought, calls sold
  }
  if (partial) partialBars += 1;
}

function fbLabel(ms: f64, today: f64): void { // "16OCT 0d"
  const expiryDay = optionsTradeDay(ms, NEW_YORK);
  dateClock.update(expiryDay * 86400.0);
  fb_text("\"");
  if (dateClock.dayOfMonth() < 10) fb_text("0");
  fb_int(dateClock.dayOfMonth());
  fb_text(MONTHS[dateClock.month() - 1]);
  fb_text(" ");
  fb_int(i64(expiryDay - today));
  fb_text("d\"");
}

// The panel the View setting picks, on the live bar: the nearest expiries, ascending.
function writePanel(today: f64): void {
  const last = expiryCount < shown ? expiryCount : shown;
  fb_clear();
  fb_text("{");
  if (partialBars > 0) fb_text("\"badge\":{\"text\":\"Floor\",\"color\":\"#f59e0b\"},");
  fb_text("\"rows\":[");
  for (let e = 0; e < last; e += 1) {
    if (e > 0) fb_text(",");
    fb_text("[");
    fbLabel(expiryAt[e], today);
    fb_text(",");
    if (sides) {
      fb_f64(bullAt[e], 0);
      fb_text(",");
      fb_f64(-bearAt[e], 0);
    } else fb_f64(bullAt[e] - bearAt[e], 0);
    fb_text("]");
  }
  fb_text("]}");
  if (sides) writeFrameBuffer(side_rows);
  else writeFrameBuffer(net_rows);
}

// onBar() runs once per bar: a new trade date starts the sums again; the bar's block adds each contract's bullish
// and bearish premium to its expiry; then the readings and, on the live bar, the panel and the note.
function onBar(): void {
  const date = in_trade_date();
  if (!isNaN(date) && date != day) {
    day = date;
    expiryCount = 0;
    partialBars = 0;
    skipped = 0;
  }
  const n = in_flow_cells();
  if (n >= TUPLE) fold(n);
  let net = 0.0;
  let topDays: f64 = NaN;
  let topNet: f64 = NaN;
  const today = Math.floor(day / 86400.0); // the bar's own date as a day number (the chart fills trade_date on every bar)
  for (let e = 0; e < expiryCount; e += 1) {
    const row = bullAt[e] - bearAt[e];
    net += row;
    if (isNaN(topNet) || Math.abs(row) > Math.abs(topNet)) {
      topNet = row;
      topDays = optionsTradeDay(expiryAt[e], NEW_YORK) - today;
    }
  }
  const reading = expiryCount > 0;
  out_session_net(reading ? net : NaN);
  out_top_days(topDays);
  out_top_net(topNet);
  out_partial_bars(f64(partialBars));
  out_skipped(f64(skipped));
  if (!bar.isLast()) return;
  if (reading && !isNaN(today)) writePanel(today);
  sb_clear();
  if (runCells == 0) sb_text("No contract flow on this chart yet");
  else if (!reading) sb_text("No contract flow yet this session");
  else if (partialBars > 0) {
    sb_text("A floor: ");
    sb_int(partialBars);
    sb_text(partialBars == 1 ? " bar's contracts were cut down" : " bars' contracts were cut down");
  }
  str_note_sb(); // an empty text draws no label
}

How it works

A contract's premium by side. Each tuple carries prem_ask and prem_bid, the premium of the contract's prints at the ask and at the bid in the bar. A call adds its ask premium to bullish and its bid premium to bearish; a put the other way round. Premium at the mid or with no quote has no side, so neither sum holds it.

By expiry, through the session. The expiries live in a small table kept in date order; a new trade date (time.trade_date) empties it. On the live bar the panel reads the nearest Expiries shown that traded this session, each labelled 16OCT 1d: the expiry's New York date and its days to go from the bar's trade date, 0 on expiration day. session_net is the session's bullish minus bearish premium over every expiry, and top_days and top_net name the expiry holding the most either way.

Whole contracts only. A tuple counts when its contract is whole: a root code the symbol codec reads, an expiry, a side of +1 or -1, a strike above 0 and a present mask that is a whole number. Anything else is skipped and counted in skipped, never summed under a guess.

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 option_contract_flow. 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. One-minute cells reach back 7 days and five-minute cells 30 days.

When data is missing

A bar whose block the data plane cut down, past a read's row cap on a busy underlying, carries the partial bit (14) on its tuples: the sums are then a floor, the panel wears a Floor badge, and a label at the top of the chart counts the bars. A bar with no block (an empty bar) adds nothing. The label says "No contract flow on this chart yet" while no bar of the run carried a contract, and "No contract flow yet this session" while the newest session has none, the panel staying absent.

Customize it

  • The sides apart. Switch View to sides to see both bets on an expiry, not only their difference.
  • More expiries. Raise Expiries shown to follow the longer-dated money; the table keeps every expiry that traded.
  • Calls and puts. Keep one sum per side and per type to split each expiry's bar into the four flows.

Run it

  1. In the editor's Explorer, press the Templates icon ("Browse starter templates") and pick Premium by Expiry under Beyond the time axis.
  2. Press Run on a US stock chart: the official package draws the bars under the chart.
  3. At the editor's Console prompt, type session_net, top_days and top_net to read the session on the live bar.

Concepts used