---
title: "Premium by strike"
description: "Where the options money on the chart's underlying went, strike by strike, drawn behind the candles as a heat map with one column per bar and one row per strike…"
order: 107
section: "cookbook"
---

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

# Premium by strike

Where the options money on the chart's underlying went, strike by strike, drawn behind the candles as a heat map with one column per bar and one row per strike step, and docked on the price axis as the session's net premium by strike. A strike's net premium is its bullish premium minus its bearish premium: calls bought at the ask and puts sold at the bid are bullish, puts bought at the ask and calls sold at the bid bearish. A green cell is money leaning up at that strike in that bar, a red cell money leaning down, and the docked bars sum the session.

The parts are a celled `option_contract_flow.cells` input with `strike_window_pct: 10` ([Data sources](../core-concepts/data-sources.md#option_contract_flow)), an `out.grid` and a `plot.heatmap` with `center: 0` over it ([Price canvases](../presentation/price-canvases.md#a-grid-the-module-computes)), a `plot.levels` profile with a colour per row ([Docked profiles](../presentation/cards-frames-panels.md#docked-profiles)) and `isPartial` for the bars the data plane cut down ([Data sources](../core-concepts/data-sources.md#the-present-mask-and-partial-bars)). This is also the `premium-by-strike` template: the **Premium by Strike** card under **Order flow** in the editor's starter list, and it compiles as written.

## The wrun indicator

```typescript sample=premium-by-strike
// Premium by Strike: where the options money on the chart's underlying went, strike by strike, drawn behind the
// candles as a heat map, one column per bar and one row per strike step, and docked on the price axis as the
// session's net premium by strike. A strike's net premium is its bullish premium minus its bearish premium: calls
// bought at the ask and puts sold at the bid are bullish, puts bought at the ask and calls sold at the bid bearish,
// so a green cell is money leaning up at that strike in that bar and a red cell money leaning down. Premium at the
// mid or with no quote has no side and counts in neither. The rows are the chain's strike spacing (read off the
// strikes that traded: the most common gap near the price) unless Row height sets one, 48 of them centred on each
// bar's close; the docked profile sums the session (each trade date starts again) over the strikes within Profile
// window of the close. The cells come from option_contract_flow with the strikes within 10% of the underlying: a
// bar whose block was cut down (the partial bit, 14) gets an amber dot under its candle, its column a floor; 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("Strikes");
param.number("row_step", 0, { min: 0, max: 1000, step: 0.5, label: "Row height", description: "Price per heat row: 0 reads the chain's strike spacing" });
param.number("profile_pct", 8, { min: 1, max: 50, label: "Profile window, percent", description: "Strikes docked on the price axis: percent around the close, each side" });
input("close", ohlcv.close); // the chart's own candles: the bar grid and each column's centre
input("trade_date", time.trade_date); // the bar's trade date: the docked profile starts again on each
input("flow", option_contract_flow.cells, { max_cells: 4000, strike_window_pct: 10, description: "Each contract within 10% of the underlying 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]" });
output("partial_mark", shape, overlay, { color: "#f59e0b", description: "A dot under the candle whose contract flow was cut down (bit 14): its column is a floor" }); // first output on price: the package homes on the chart
output("heat_low", none, overlay, { description: "The bottom edge of the heat map's lowest row on this bar" }); // data-only: the heatmap reads it
output("heat_step", none, overlay, { description: "The heat map's row height in price on this bar" });
out.grid("heat", { rows: 48 }); // 48 data-only outputs heat_0..heat_47, one per row, written with out_heat(row, value)
plot.heatmap({ name: "net_heat", grid: "heat", price_low: "heat_low", price_step: "heat_step", center: 0, auto_quantile: 0.98, opacity: 0.7, behind_candles: true, label: "Net premium by strike", tooltip: "{{value:usd}} net at {{price}}" }); // the diverging default: the chart's down colour, its background, its up colour
// The readings, data-only: the Console reads them.
output("session_net", none, overlay, { description: "Bullish minus bearish premium over the session's strikes, USD" });
output("top_bull_strike", none, overlay, { description: "The strike holding the session's largest bullish net premium" });
output("top_bull_net", none, overlay, { description: "Its net premium, USD" });
output("top_bear_strike", none, overlay, { description: "The strike holding the session's largest bearish net premium" });
output("top_bear_net", none, overlay, { description: "Its net premium, USD (below zero)" });
output("skipped", none, overlay, { description: "Tuples of this bar skipped because their contract is not whole" });
string("note", { max_bytes: 64 }); // the one line while there is nothing to draw
const strike_rows = frame("strike_rows", { max_bytes: 24576 });
plot.levels({ name: "session_by_strike", frame: strike_rows, dock: "right", width_frac: 0.14, baseline: "center", labels: false, thickness_px: 8, hover: true, format: "usd" });
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 ROWS = 48; // the heat map's rows, the grid's declared count
const MAX_SEEN = 1024; // distinct strikes kept over the run, the nearest the close: the spacing reads them
const MAX_STRIKES = 512; // the session's strikes, the nearest the close
const MAX_PROFILE = 256; // rows docked on the axis
const STEPS: StaticArray<f64> = [0.5, 1.0, 2.0, 2.5, 5.0, 10.0, 20.0, 25.0, 50.0, 100.0]; // the listed strike spacings
const stepCount = new StaticArray<i32>(10);
const seen = new StaticArray<f64>(MAX_SEEN); // the run's distinct strikes, ascending
const strikeAt = new StaticArray<f64>(MAX_STRIKES); // the session's strikes, ascending, and their net premium
const netAt = new StaticArray<f64>(MAX_STRIKES);
const rowHeat = new StaticArray<f64>(ROWS); // this bar's column
const rowTouched = new StaticArray<bool>(ROWS);

let fixedStep = 0.0; // settings, read in onStart()
let profilePct = 0.08;
let day: f64 = NaN; // the trade date the profile sums
let seenCount = 0;
let strikeCount = 0;
let runCells = 0; // tuples read over the whole run
let close: f64 = NaN; // this bar

function onStart(): void {
  fixedStep = p_row_step();
  profilePct = p_profile_pct() / 100.0;
}

// 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);
}

// Where a strike sits in an ascending table: its index when listed, else the index it would take.
function lowerBound(keys: StaticArray<f64>, count: i32, key: f64): i32 {
  let lo = 0;
  let hi = count;
  while (lo < hi) {
    const mid = (lo + hi) >> 1;
    if (keys[mid] < key) lo = mid + 1;
    else hi = mid;
  }
  return lo;
}

// Which end of a full table leaves for a new strike: 0 the low end, 1 the high end, -1 the new strike itself (it is
// the farthest from the close).
function evict(keys: StaticArray<f64>, count: i32, strike: f64): i32 {
  const dLow = Math.abs(keys[0] - close);
  const dHigh = Math.abs(keys[count - 1] - close);
  const dNew = Math.abs(strike - close);
  if (dNew >= dLow && dNew >= dHigh) return -1;
  return dLow >= dHigh ? 0 : 1;
}

// One more distinct strike into the run's table, the nearest the close kept.
function noteSeen(strike: f64): void {
  let at = lowerBound(seen, seenCount, strike);
  if (at < seenCount && seen[at] == strike) return;
  if (seenCount >= MAX_SEEN) {
    const end = evict(seen, seenCount, strike);
    if (end < 0) return;
    if (end == 0) {
      for (let k = 1; k < seenCount; k += 1) seen[k - 1] = seen[k];
      at -= 1;
    }
    seenCount -= 1;
    if (at > seenCount) at = seenCount;
  }
  for (let k = seenCount; k > at; k -= 1) seen[k] = seen[k - 1];
  seen[at] = strike;
  seenCount += 1;
}

// A contract's net premium onto its strike for the session, the nearest strikes to the close kept.
function addNet(strike: f64, net: f64): void {
  let at = lowerBound(strikeAt, strikeCount, strike);
  if (at < strikeCount && strikeAt[at] == strike) {
    netAt[at] += net;
    return;
  }
  if (strikeCount >= MAX_STRIKES) {
    const end = evict(strikeAt, strikeCount, strike);
    if (end < 0) return;
    if (end == 0) {
      for (let k = 1; k < strikeCount; k += 1) {
        strikeAt[k - 1] = strikeAt[k];
        netAt[k - 1] = netAt[k];
      }
      at -= 1;
    }
    strikeCount -= 1;
    if (at > strikeCount) at = strikeCount;
  }
  for (let k = strikeCount; k > at; k -= 1) {
    strikeAt[k] = strikeAt[k - 1];
    netAt[k] = netAt[k - 1];
  }
  strikeAt[at] = strike;
  netAt[at] = net;
  strikeCount += 1;
}

// The chain's strike spacing: the most common gap between neighbouring strikes within 10% of the close, matched to
// a listed spacing (the smaller on a tie); the smallest gap when none matches; NaN with fewer than two strikes.
function spacing(): f64 {
  for (let c = 0; c < 10; c += 1) stepCount[c] = 0;
  let smallest: f64 = NaN;
  let prev: f64 = NaN;
  for (let k = 0; k < seenCount; k += 1) {
    const strike = seen[k];
    if (Math.abs(strike - close) > close * 0.1) continue;
    if (!isNaN(prev)) {
      const gap = strike - prev;
      if (gap > 0.0 && (isNaN(smallest) || gap < smallest)) smallest = gap;
      for (let c = 0; c < 10; c += 1) {
        if (Math.abs(gap - STEPS[c]) <= STEPS[c] * 0.01) {
          stepCount[c] += 1;
          break;
        }
      }
    }
    prev = strike;
  }
  let best = -1;
  for (let c = 0; c < 10; c += 1) if (stepCount[c] > 0 && (best < 0 || stepCount[c] > stepCount[best])) best = c;
  return best >= 0 ? STEPS[best] : smallest;
}

function writeProfile(): void {
  const lo = close * (1.0 - profilePct);
  const hi = close * (1.0 + profilePct);
  let first = 0;
  while (first < strikeCount && strikeAt[first] < lo) first += 1;
  let last = first;
  while (last < strikeCount && strikeAt[last] <= hi) last += 1;
  while (last - first > MAX_PROFILE) { // a wide window on a fine ladder: drop the row farther from the close
    if (close - strikeAt[first] >= strikeAt[last - 1] - close) first += 1;
    else last -= 1;
  }
  if (last <= first) return; // no strike inside the window: the profile stays absent
  fb_clear();
  fb_text("{\"prices\":[");
  for (let k = first; k < last; k += 1) {
    if (k > first) fb_text(",");
    fb_num(strikeAt[k]);
  }
  fb_text("],\"values\":[");
  for (let k = first; k < last; k += 1) {
    if (k > first) fb_text(",");
    fb_f64(netAt[k], 0);
  }
  fb_text("],\"colors\":[");
  for (let k = first; k < last; k += 1) {
    if (k > first) fb_text(",");
    fb_text(netAt[k] > 0.0 ? "\"#22c55e\"" : netAt[k] < 0.0 ? "\"#ef4444\"" : "\"#94a3b8\""); // bullish green, bearish red
  }
  fb_text("]}");
  writeFrameBuffer(strike_rows);
}

// onBar() runs once per bar: a new trade date starts the profile again; the bar's block adds each contract's net
// premium to its strike for the session and to its heat row for this bar's column; then the readings, the column
// and, on the live bar, the docked profile.
function onBar(): void {
  close = bar.close();
  const date = in_trade_date();
  if (!isNaN(date) && date != day) {
    day = date;
    strikeCount = 0;
  }
  for (let r = 0; r < ROWS; r += 1) {
    rowHeat[r] = 0.0;
    rowTouched[r] = false;
  }
  const n = in_flow_cells();
  let skipped = 0;
  let partial = false;
  if (n >= TUPLE && !isNaN(close)) {
    const cells = in_flow_view();
    for (let i = 0; i + TUPLE - 1 < n; i += TUPLE) { // the strikes first: the spacing reads them
      if (contractOk(cells, i)) noteSeen(cells[i + 3]);
    }
    const step = fixedStep > 0.0 ? fixedStep : spacing();
    const gridLow = isNaN(step) ? NaN : (Math.round(close / step) - f64(ROWS / 2)) * step - step * 0.5; // rows centred on the spacing's multiples
    for (let i = 0; i + TUPLE - 1 < n; i += TUPLE) {
      if (!contractOk(cells, i)) {
        skipped += 1;
        continue;
      }
      runCells += 1;
      if (isPartial(cells[i + 16])) partial = true;
      const strike = cells[i + 3];
      const call = cells[i + 2] > 0.0;
      const net = call ? cells[i + 8] - cells[i + 9] : cells[i + 9] - cells[i + 8]; // bullish minus bearish
      addNet(strike, net);
      if (isNaN(gridLow)) continue;
      const r = i32(Math.floor((strike - gridLow) / step));
      if (r < 0 || r >= ROWS) continue; // off this bar's column
      rowHeat[r] += net;
      rowTouched[r] = true;
    }
    out_heat_low(gridLow);
    out_heat_step(step);
  } else {
    out_heat_low(NaN);
    out_heat_step(NaN);
  }
  for (let r = 0; r < ROWS; r += 1) out_heat(r, rowTouched[r] ? rowHeat[r] : NaN); // NaN = an empty cell
  out_partial_mark(partial ? bar.low() : NaN);
  out_skipped(f64(skipped));
  let sessionNet = 0.0;
  let bullStrike: f64 = NaN;
  let bullNet: f64 = NaN;
  let bearStrike: f64 = NaN;
  let bearNet: f64 = NaN;
  for (let k = 0; k < strikeCount; k += 1) {
    const net = netAt[k];
    sessionNet += net;
    if (net > 0.0 && (isNaN(bullNet) || net > bullNet)) {
      bullNet = net;
      bullStrike = strikeAt[k];
    }
    if (net < 0.0 && (isNaN(bearNet) || net < bearNet)) {
      bearNet = net;
      bearStrike = strikeAt[k];
    }
  }
  out_session_net(strikeCount > 0 ? sessionNet : NaN);
  out_top_bull_strike(bullStrike);
  out_top_bull_net(bullNet);
  out_top_bear_strike(bearStrike);
  out_top_bear_net(bearNet);
  if (!bar.isLast()) return;
  if (strikeCount > 0 && !isNaN(close)) writeProfile();
  str_note(runCells == 0 ? "No contract flow on this chart yet" : strikeCount == 0 ? "No contract flow yet this session" : ""); // an empty text draws no label
}
```

## How it works

**Net premium per contract.** Each tuple carries the contract's premium at the ask and at the bid in the bar. A call's net is the ask premium minus the bid premium, a put's the bid premium minus the ask premium, so both read bullish above zero. Premium at the mid or with no quote has no side and counts in neither.

**The rows.** The heat map's 48 rows sit on the chain's strike spacing: the package keeps every distinct strike that traded, and the spacing is the most common gap between neighbouring strikes within 10% of the close, matched to a listed spacing (0.5, 1, 2, 2.5, 5 and up); Row height sets one by hand. Each bar's rows are centred on its close, one row per spacing step with a strike in the middle of each, so a contract lands on the row holding its strike. The rows of a bar sum its contracts there; a row no contract touched draws nothing.

**The session by strike.** The same nets add into a table of the session's strikes, which a new trade date empties; on the live bar the strikes within Profile window of the close dock on the price axis, green above zero and red below. `top_bull_strike` and `top_bear_strike` name the session's strongest strikes either way.

## 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](../reference/limitations.md). The read keeps the strikes within 10% of the underlying; 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 (the partial bit, 14, past a read's row cap on a busy underlying) gets an amber dot under its candle: its column is a floor. A bar with no block draws an empty column. 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 in `skipped`. A label at the top of the chart 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.

## Customize it

- **Coarser rows.** Set Row height to 5 on a stock whose strikes crowd, so each row sums a few strikes.
- **The profile's reach.** Widen Profile window to see the far strikes the session's money reached.
- **One expiry.** Add `expiries: "nearest:1"` to the input to paint the front expiry alone.

## Run it

1. In the editor's Explorer, press the **Templates** icon ("Browse starter templates") and pick **Premium by Strike** under **Order flow**.
2. Press **Run** on a US stock chart: the official package paints the heat map behind the candles and docks the session's profile.
3. At the editor's Console prompt, type `session_net`, `top_bull_strike` and `top_bear_strike` to read the session.

## Concepts used

- [Data sources](../core-concepts/data-sources.md#option_contract_flow) for the `option_contract_flow` tuple and `strike_window_pct`
- [Price canvases](../presentation/price-canvases.md#a-grid-the-module-computes) for `out.grid` and `plot.heatmap`
- [Docked profiles](../presentation/cards-frames-panels.md#docked-profiles) for `plot.levels` and per-row colours
- [Data sources](../core-concepts/data-sources.md#the-present-mask-and-partial-bars) for the partial bit
- [Limitations](../reference/limitations.md) for the US options rule and the upcoming OPRA sources
