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) read through its present mask (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, A grid the module computes), a render.bgcolor wash over the bars the chart served in part (Plotting), and the options kit's optionsTradeDay for the same-day expiry (Options kit). 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
// 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). Once those lanes ship, the chart in the browser serves them under the US options access rule (Limitations): 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_totalandpartialreadNaNand the bar draws no column. - An empty block. A bar where no contract traded draws no column and reads
flow_total0. - 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,partialreads 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
- In the editor's Explorer, press the Templates icon ("Browse starter templates") and pick Strike Footprint under Order flow.
- 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.
- At the editor's Console prompt, type
flow_totalandrow_stepto read the newest bar's total and the strike step it drew with.
Concepts used
- Data sources for the
option_contract_flowcelled class, its tuple and its knobs, and The present mask and partial bars forisPartialand bit 14 - Heatmaps and A grid the module computes for
out.gridandplot.heatmapwithprice_lowandprice_step - Plotting for
render.bgcolorgated by a data-only output - Options kit for
optionsTradeDay