---
title: "Strike footprint"
description: "Where the chart's underlying's US listed options (OPRA) traded, strike by strike and bar by bar, painted behind the candles. Each column is one bar and each…"
order: 99
section: "cookbook"
---

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

# Strike footprint

Where the chart's underlying's US listed options (OPRA) traded, strike by strike and bar by bar, painted behind the candles. Each column is one bar and each cell one strike, at its own price on the chart: green where the flow leaned bullish (calls bought at the ask and puts sold at the bid outweighed calls sold and puts bought), red where it leaned bearish, brighter with the premium. The default lens is that bullish net premium; the others are the calls' net premium alone, the puts' alone, and the contracts traded. The strikes sit around each bar's close, one row per strike step, every expiry summed or one expiry group alone.

The parts are a celled `option_contract_flow.cells` input ([Data sources](../core-concepts/data-sources.md#option_contract_flow)) read through its `present` mask ([The present mask and partial bars](../core-concepts/data-sources.md#the-present-mask-and-partial-bars)), an `out.grid` of 41 rows painted by `plot.heatmap` with a diverging scale, each bar placing its rows through two data-only outputs ([Heatmaps](../presentation/price-canvases.md#heatmaps), [A grid the module computes](../presentation/price-canvases.md#a-grid-the-module-computes)), a `render.bgcolor` wash over the bars the chart served in part ([Plotting](../presentation/plotting.md#text-labels-tables-strips-tints)), and the options kit's `optionsTradeDay` for the same-day expiry ([Options kit](../functions/options-kit.md#the-markets-own-date-and-the-chains-clock)). This is also the `strike-footprint` template: the **Strike Footprint** card under **Order flow** in the editor's starter list, and it compiles as written.

## The wrun indicator

```typescript sample=strike-footprint
// Strike Footprint: where the chart's underlying's US listed options (OPRA) traded, strike by strike, bar by bar,
// painted behind the candles. Each column is one bar and each cell one strike, at its own price on the chart: green
// where the flow leaned bullish (calls bought at the ask and puts sold at the bid outweighed calls sold and puts bought),
// red where it leaned bearish, brighter with the premium. The default lens is that bullish net premium; the others are
// the calls' net premium alone, the puts' alone, and the contracts traded. The strikes shown sit around each bar's close,
// one row per listed strike step (the most common gap between neighbouring strikes, or the step you set), every
// expiry summed, the same-day expiry alone, or the later ones. A bar the chart served in part draws what it holds and a
// faint wash marks it, since its cells are a floor. It reads the chart's own underlying and draws at strike prices, so
// it belongs on a US stock chart: on an option contract's chart no strike lands near the price axis and nothing draws.
param.choice("lens", ["bullish net premium", "call net premium", "put net premium", "contracts"], "bullish net premium", { label: "Lens" });
param.int("strikes", 17, { min: 5, max: 41, label: "Strikes shown", description: "Rows around the strike nearest each bar's close" });
param.number("strike_step", 0, { min: 0, max: 1000, step: 0.5, label: "Strike step", description: "The price between two rows; 0 reads the most common gap between neighbouring listed strikes" });
param.choice("expiries", ["every expiry", "0DTE only", "after today"], "every expiry", { label: "Expiries", description: "0DTE only keeps the contracts expiring on the bar's own trade date; after today leaves them out" });
input("close", ohlcv.close); // the chart's own close: the strike each bar's rows centre on
input("flow", option_contract_flow.cells, { max_cells: 10000, strike_window_pct: 25, description: "Each contract of the underlying that traded in the bar, strikes within 25% of the underlying" }); // [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] per contract
output("grid_low", none, overlay, { description: "The bottom edge of the lowest row on this bar: half a step under its strike" }); // data-only: the heatmap reads it
output("row_step", none, overlay, { description: "The strike step: one row's height in price on this bar" });
output("flow_total", none, overlay, { format: "si", description: "The lens summed over the strikes shown on this bar" });
output("partial", none, overlay, { description: "1 on a bar the chart served in part: its cells are a floor" });
out.grid("flow", { rows: 41 }); // one data-only output per row, flow_0 the lowest strike; out_flow(row, value)
plot.heatmap({ name: "strike_flow", grid: "flow", price_low: "grid_low", price_step: "row_step", center: 0, palette: ["#e5484d", "theme.bg", "#30a46c"], auto_quantile: 0.97, opacity: 0.8, behind_candles: true, label: "Strike flow", tooltip: "{{value:si}} at the {{price}} strike" });
render.bgcolor("served_in_part", { where: "partial", color: "#64748b14" }); // a faint wash over a bar served in part

const FLOW = 17; // f64s per contract: OPTION_CONTRACT_FLOW_TUPLE
const ROWS = 41; // the grid's declared rows
const NEAR = 0.1; // strikes within 10% of the close teach the step
const MAX_NEAR = 256; // distinct strikes a bar keeps for the step
const MAX_GAPS = 16; // distinct gaps the step's tally keeps
const cellSum = new StaticArray<f64>(ROWS); // this bar's cells
const cellSeen = new StaticArray<bool>(ROWS);
const nearStrikes = new StaticArray<f64>(MAX_NEAR); // this bar's distinct strikes near the close, ascending
const gapValue = new StaticArray<f64>(MAX_GAPS); // the gaps seen between neighbouring strikes, and how often
const gapCount = new StaticArray<f64>(MAX_GAPS);

let lens = 0; // settings, read in onStart()
let shown = 17;
let fixedStep = 0.0;
let expiries = 0;
let gaps = 0; // distinct gaps in the tally

function onStart(): void {
  lens = i32(p_lens());
  shown = i32(p_strikes());
  if (shown % 2 == 0) shown -= 1; // an odd count keeps the close's strike in the middle row
  fixedStep = p_strike_step();
  expiries = i32(p_expiries());
}

// Whether the contract expiring at expiryMs counts under the expiries setting, on the bar's 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;
}

// One contract's value under the lens: its net premium (at the ask minus at the bid) signed by the side it leans
// (a call +1, a put -1), one side's net premium alone, or its contracts; NaN when the lens leaves it out.
function valueOf(cells: StaticArray<f64>, at: i32): f64 {
  const sideCp = cells[at + 2];
  const net = cells[at + 8] - cells[at + 9];
  if (lens == 0) return sideCp > 0.0 ? net : -net;
  if (lens == 1) return sideCp > 0.0 ? net : NaN;
  if (lens == 2) return sideCp < 0.0 ? net : NaN;
  return cells[at + 4] + cells[at + 5] + cells[at + 6] + cells[at + 7];
}

// The bar's distinct strikes within 10% of the close teach the tally of gaps between neighbours (to the cent).
function learnStep(cells: StaticArray<f64>, n: i32, close: f64): void {
  let count = 0;
  for (let i = 0; i + FLOW - 1 < n; i += FLOW) {
    const strike = cells[i + 3];
    if (!(strike > 0.0) || Math.abs(strike / close - 1.0) > NEAR) continue;
    let j = 0;
    while (j < count && nearStrikes[j] < strike) j += 1;
    if (j < count && nearStrikes[j] == strike) continue; // listed already
    if (count == MAX_NEAR) continue;
    for (let k = count; k > j; k -= 1) nearStrikes[k] = nearStrikes[k - 1];
    nearStrikes[j] = strike;
    count += 1;
  }
  for (let k = 1; k < count; k += 1) {
    const gap = Math.round((nearStrikes[k] - nearStrikes[k - 1]) * 100.0) / 100.0;
    if (!(gap > 0.0)) continue;
    let g = 0;
    while (g < gaps && gapValue[g] != gap) g += 1;
    if (g == gaps) {
      if (gaps == MAX_GAPS) continue;
      gapValue[g] = gap;
      gapCount[g] = 0.0;
      gaps += 1;
    }
    gapCount[g] += 1.0;
  }
}

// The strike step: the setting, else the gap seen most often (the smaller on a tie); NaN before any gap.
function stepOf(): f64 {
  if (fixedStep > 0.0) return fixedStep;
  let best = -1;
  for (let g = 0; g < gaps; g += 1) {
    if (best < 0 || gapCount[g] > gapCount[best] || (gapCount[g] == gapCount[best] && gapValue[g] < gapValue[best])) best = g;
  }
  return best < 0 ? NaN : gapValue[best];
}

function clearRows(): void { // no column on this bar: every cell empty
  for (let r = 0; r < ROWS; r += 1) out_flow(r, NaN);
}

// onBar() runs once per bar: the bar's strikes teach the step; the strike nearest the close takes the middle row and
// each contract joins its strike's row; then the grid, its bottom edge and its step.
function onBar(): void {
  const close = bar.close();
  const n = in_flow_cells();
  let partial = false;
  if (n > 0) {
    const block = in_flow_view();
    for (let i = 0; i + FLOW - 1 < n; i += FLOW) if (isPartial(block[i + 16])) partial = true;
    if (close > 0.0) learnStep(block, n, close);
  }
  out_partial(n < 0 ? NaN : partial ? 1.0 : 0.0);
  const step = stepOf();
  if (n <= 0 || !(close > 0.0) || isNaN(step)) {
    out_grid_low(NaN);
    out_row_step(NaN);
    out_flow_total(n < 0 ? NaN : 0.0);
    clearRows();
    return;
  }
  const cells = in_flow_view();
  const today = optionsTradeDay(bar.time() * 1000.0, 1);
  const half = (shown - 1) / 2;
  const middle = Math.round(close / step) * step; // the strike nearest the close
  for (let r = 0; r < ROWS; r += 1) {
    cellSum[r] = 0.0;
    cellSeen[r] = false;
  }
  for (let i = 0; i + FLOW - 1 < n; i += FLOW) {
    const strike = cells[i + 3];
    if (!(strike > 0.0) || !counts(cells[i + 1], today)) continue; // no strike: the chart's marker for a bar served in part
    const value = valueOf(cells, i);
    if (isNaN(value)) continue;
    const r = i32(Math.round((strike - middle) / step)) + half;
    if (r < 0 || r >= shown) continue; // outside the strikes shown
    cellSum[r] += value;
    cellSeen[r] = true;
  }
  let total = 0.0;
  for (let r = 0; r < ROWS; r += 1) {
    const value = r < shown && cellSeen[r] ? cellSum[r] : NaN;
    if (!isNaN(value)) total += value;
    out_flow(r, value);
  }
  out_grid_low(middle - (f64(half) + 0.5) * step);
  out_row_step(step);
  out_flow_total(total);
}
```

## How it works

**Each contract's flow.** The input hands each bar one tuple per contract that traded in it, the strikes within 25% of the underlying (`strike_window_pct: 25`, `max_cells: 10000`): its contracts by side of the quote and its premium at the ask and at the bid. 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.

**The lens.** **Lens** picks each contract's value. **Bullish net premium** (the default) is a call's premium at the ask minus at the bid, and a put's the other way round, so calls bought and puts sold count up and calls sold and puts bought count down. **Call net premium** and **Put net premium** keep one side's premium at the ask minus at the bid. **Contracts** counts every contract traded, whatever its side, so it paints the green half of the scale alone. **Expiries** sums every expiry, keeps only the contracts expiring on the bar's own New York trade date (`0DTE only`), or leaves those out (`after today`).

**The strike step.** One row holds one strike. **Strike step** fixes the price between two rows; left at 0, the step is the gap seen most often between neighbouring strikes that traded within 10% of the close, counted to the cent over every bar so far (the smaller gap on a tie), so a chain listed every dollar near the money reads 1. Before any bar has taught a gap, nothing draws.

**The rows around the close.** The row in the middle holds the strike step's multiple nearest the bar's close, and **Strikes shown** (17, from 5 to 41) rows sit around it; an even count drops by one so the close's strike stays in the middle. Each contract adds its value to its strike's row, and a strike outside the rows shown adds nothing. `grid_low` (half a step under the lowest row's strike) and `row_step` place the rows on the price axis bar by bar, so each column follows its own close.

**The heatmap.** `plot.heatmap` paints the grid behind the candles: red below zero, the chart's background at zero, green above, the scale's ends at the 97th percentile of the run, at 80% opacity. The hover reads the cell's value and its strike. A row with no contract stays empty.

**The readings.** Data-only outputs on every bar: `flow_total` (the lens summed over the strikes shown), `grid_low`, `row_step` and `partial` (1 on a bar the chart served in part), with the grid's own outputs `flow_0` (the lowest strike) to `flow_40`.

## 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 Strike Footprint 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 and a Protected indicator on OpenMarket's servers refuse `option_contract_flow` by name. It draws at strike prices, so it belongs on the stock's own chart: on an option contract's chart the price axis is the contract's own price, no strike lands near it, and nothing draws. A bar with more contracts than 10,000 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 block.** A bar before the history the chart serves carries no block: `flow_total` and `partial` read `NaN` and the bar draws no column.
- **An empty block.** A bar where no contract traded draws no column and reads `flow_total` 0.
- **A bar served in part.** Bit 14 of `present` (`isPartial`) marks a bar the chart served in part: on a busy underlying the chart may send an older bar as one zero tuple whose only set bit is 14 instead of its contracts, which this indicator never counts as a contract. Such a bar draws the cells it holds, `partial` reads 1, and a faint slate wash covers it, since its cells are a floor.

## Customize it

- **The same-day expiry.** Set Expiries to 0DTE only on an index ETF: the strikes the day's expiring flow is fighting over.
- **One side.** Set Lens to call net premium to see where calls were bought and where they were sold, strike by strike.
- **A wider ladder.** Raise Strikes shown to 41 on a quiet chart, or set Strike step to 5 on a chain listed every five dollars.

## Run it

1. In the editor's Explorer, press the **Templates** icon ("Browse starter templates") and pick **Strike Footprint** under **Order flow**.
2. Press **Run** on a US stock chart such as SPY at 5 minutes: once the chart serves US option flow the cells paint behind the candles; until then the legend's chip names the source the chart does not serve yet.
3. At the editor's Console prompt, type `flow_total` and `row_step` to read the newest bar's total and the strike step it drew with.

## Concepts used

- [Data sources](../core-concepts/data-sources.md#option_contract_flow) for the `option_contract_flow` celled class, its tuple and [its knobs](../core-concepts/data-sources.md#the-knobs), and [The present mask and partial bars](../core-concepts/data-sources.md#the-present-mask-and-partial-bars) for `isPartial` and bit 14
- [Heatmaps](../presentation/price-canvases.md#heatmaps) and [A grid the module computes](../presentation/price-canvases.md#a-grid-the-module-computes) for `out.grid` and `plot.heatmap` with `price_low` and `price_step`
- [Plotting](../presentation/plotting.md#text-labels-tables-strips-tints) for `render.bgcolor` gated by a data-only output
- [Options kit](../functions/options-kit.md#the-markets-own-date-and-the-chains-clock) for `optionsTradeDay`
