Strike breakdown
The chart's underlying's US listed options (OPRA) by strike, one chart per upcoming expiry, calls beside puts.
The lens picks what each bar measures: open interest, its change since the prior day, contracts traded today, premium traded today, or today's net premium (at the ask minus at the bid). Up to three charts sit under the price chart, the nearest expiries first, each titled by its date and days to go with the calls' and the puts' totals on its badge (23OCT, 6 days over C 1.2M / P 900.0K). The three lenses that only grow draw the puts below zero, so each strike reads calls up and puts down; the two signed lenses keep each side's own sign.
The parts are a celled option_contract_state.cells input read on the live bar (Data sources) and a celled option_contract_flow.cells input summed by contract over today's bars (Data sources), both read through their present masks (The present mask and partial bars), three frames feeding three panel.bars on a number axis of strikes, each with a per-run title and badge (Bars, Frames, panels and compact widgets), and the options kit's optionsTradeDay for the expiries' dates and days to go (Options kit). This is also the strike-breakdown template: the Strike Breakdown card under Beyond the time axis in the editor's starter list, and it compiles as written.
The wrun indicator
// Strike Breakdown: the chart's underlying's US listed options (OPRA) by strike, one chart per upcoming expiry, calls
// beside puts. The lens picks what each bar measures: open interest, its change since the prior day, contracts traded
// today, premium traded today, or today's net premium (at the ask minus at the bid); the three lenses that only grow
// draw the puts below zero, and the two signed ones keep each side's own sign. Up to three charts sit under the price
// chart, the nearest expiries first, each titled by its date and days to go with the calls' and puts' totals on its
// badge. Open interest and today's contracts come from the chain summary on the newest bar; the premium lenses sum each
// contract's flow over today's bars (VWAP times contracts times 100, standard contracts). A bar of today the chart
// served in part makes the premium lenses a floor: the badge leads with the at-least sign. The strikes shown sit within
// the window you set around the close; on an option contract's chart, whose close is the contract's own price, each
// chart centres on the expiry's middle strike instead.
param.choice("lens", ["open interest", "open interest change", "contracts today", "premium today", "net premium today"], "open interest", { label: "Lens" });
param.int("charts", 2, { min: 1, max: 3, label: "Expiries", description: "Charts, one per upcoming expiry, nearest first" });
param.number("window_pct", 8, { min: 1, max: 15, step: 0.5, label: "Strike window, percent", description: "The strikes within this many percent of the close (the chart reads 15% at most)" });
input("close", ohlcv.close); // the chart's own close: the strike window's centre
input("state", option_contract_state.cells, { max_cells: 6000, expiries: "nearest:6", strike_window_pct: 15, description: "The chain summary on the newest bar: open interest, the prior day's, and contracts today, the six nearest expiries, strikes within 15% of the underlying" }); // [root_code, expiry_ms, side_cp, strike, bid, ask, last, prev_close, theta, volume_day, oi, oi_prev, present] per contract
input("flow", option_contract_flow.cells, { max_cells: 6000, expiries: "nearest:6", strike_window_pct: 15, description: "Each contract that traded in the bar, the six nearest expiries, strikes within 15% 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
// The readings, data-only, on the newest bar: the nearest expiry's totals under the lens.
output("front_calls", none, overlay, { format: "si", description: "The nearest expiry's calls under the lens, over the strikes shown; newest bar only" });
output("front_puts", none, overlay, { format: "si", description: "The nearest expiry's puts under the lens, over the strikes shown; newest bar only" });
output("front_days", none, overlay, { description: "Days to the nearest expiry; newest bar only" });
output("partial", none, overlay, { description: "1 once a bar of today was served in part: the premium lenses are a floor" });
const expiry_1 = frame("expiry_1", { max_bytes: 16384 });
const expiry_2 = frame("expiry_2", { max_bytes: 16384 });
const expiry_3 = frame("expiry_3", { max_bytes: 16384 });
panel.bars({ name: "first_expiry", title: "Nearest expiry", x: "number", place: "below", frame: expiry_1, orientation: "vertical", chrome: "grid", series: [{ name: "Calls", color: "#30a46c" }, { name: "Puts", color: "#e5484d" }], format: "si", x_title: "Strike", hover_card: true, legend_style: "title", height_frac: 0.2 });
panel.bars({ name: "second_expiry", title: "Second expiry", x: "number", place: "below", frame: expiry_2, orientation: "vertical", chrome: "grid", series: [{ name: "Calls", color: "#30a46c" }, { name: "Puts", color: "#e5484d" }], format: "si", x_title: "Strike", hover_card: true, legend_style: "title", height_frac: 0.2 });
panel.bars({ name: "third_expiry", title: "Third expiry", x: "number", place: "below", frame: expiry_3, orientation: "vertical", chrome: "grid", series: [{ name: "Calls", color: "#30a46c" }, { name: "Puts", color: "#e5484d" }], format: "si", x_title: "Strike", hover_card: true, legend_style: "title", height_frac: 0.2 });
const STATE = 13; // f64s per contract: OPTION_CONTRACT_STATE_TUPLE
const FLOW = 17; // OPTION_CONTRACT_FLOW_TUPLE
const CAP = 4096; // contracts today's flow table holds (a power of two), an eighth of it kept free
const MAX_EXPIRIES = 8; // expiries the newest bar sorts
const MAX_STRIKES = 160; // strikes one chart draws
const MONTHS: StaticArray<string> = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
// Today's flow by contract: the key (root, expiry, call or put, strike), then the premium and the net premium.
const keyRoot = new StaticArray<f64>(CAP);
const keyExpiry = new StaticArray<f64>(CAP);
const keySide = new StaticArray<f64>(CAP);
const keyStrike = new StaticArray<f64>(CAP);
const premiumOf = new StaticArray<f64>(CAP);
const netOf = new StaticArray<f64>(CAP);
const used = new StaticArray<bool>(CAP);
const expiryList = new StaticArray<f64>(MAX_EXPIRIES); // the upcoming expiries, nearest first
const strikeAt = new StaticArray<f64>(MAX_STRIKES); // one chart's strikes, ascending, and each side's value
const callAt = new StaticArray<f64>(MAX_STRIKES);
const putAt = new StaticArray<f64>(MAX_STRIKES);
const dateClock = new Clock("UTC"); // an expiry's trade date, read from its day number
let lens = 0; // settings, read in onStart()
let charts = 2;
let windowShare = 0.08;
let day: f64 = NaN; // today's New York trade date, as a day number
let stored = 0; // contracts in today's table
let dropped = false; // a contract found the table full
let partialToday = false;
let strikes = 0; // strikes in the chart being built
function onStart(): void {
lens = i32(p_lens());
charts = i32(p_charts());
windowShare = p_window_pct() / 100.0;
}
// ── Today's flow by contract ──
function slotOf(root: f64, expiry: f64, side: f64, strike: f64): i32 { // the contract's slot, or -1 when the table is full
let h: u64 = u64(expiry) * 0x2545f4914f6cdd1d;
h ^= u64(Math.round(strike * 1000.0)) * 0x27d4eb2f165667c5;
h ^= u64(root) * 0x165667b19e3779f9;
h ^= u64(side > 0.0 ? 1 : 2);
let at = i32(h & u64(CAP - 1));
for (let probe = 0; probe < CAP; probe += 1) {
if (!used[at]) {
if (stored >= CAP - CAP / 8) return -1; // keep an eighth free so a probe ends quickly
used[at] = true;
keyRoot[at] = root;
keyExpiry[at] = expiry;
keySide[at] = side;
keyStrike[at] = strike;
premiumOf[at] = 0.0;
netOf[at] = 0.0;
stored += 1;
return at;
}
if (keyExpiry[at] == expiry && keyStrike[at] == strike && keySide[at] == side && keyRoot[at] == root) return at;
at = (at + 1) & (CAP - 1);
}
return -1;
}
function startDay(today: f64): void {
day = today;
for (let i = 0; i < CAP; i += 1) used[i] = false;
stored = 0;
dropped = false;
partialToday = false;
}
function addFlow(cells: StaticArray<f64>, n: i32): void { // one bar's contracts into today's table
for (let i = 0; i + FLOW - 1 < n; i += FLOW) {
if (isPartial(cells[i + 16])) partialToday = true;
if (!(cells[i + 3] > 0.0)) continue; // no strike: the chart's marker for a bar served in part
const at = slotOf(cells[i], cells[i + 1], cells[i + 2] > 0.0 ? 1.0 : -1.0, cells[i + 3]);
if (at < 0) {
dropped = true;
continue;
}
const contracts = cells[i + 4] + cells[i + 5] + cells[i + 6] + cells[i + 7];
const sided = cells[i + 8] + cells[i + 9];
const total = hasBit(cells[i + 16], 0) ? cells[i + 11] * contracts * 100.0 : sided; // bit 0: the VWAP; else the two sides, a floor
premiumOf[at] += total > sided ? total : sided;
netOf[at] += cells[i + 8] - cells[i + 9];
}
}
// ── One chart ──
function addStrike(strike: f64, side: f64, value: f64): void { // a value into the chart's row for its strike
let j = 0;
while (j < strikes && strikeAt[j] < strike) j += 1;
if (j == strikes || strikeAt[j] != strike) {
if (strikes == MAX_STRIKES) return;
for (let k = strikes; k > j; k -= 1) {
strikeAt[k] = strikeAt[k - 1];
callAt[k] = callAt[k - 1];
putAt[k] = putAt[k - 1];
}
strikeAt[j] = strike;
callAt[j] = 0.0;
putAt[j] = 0.0;
strikes += 1;
}
if (side > 0.0) callAt[j] += value;
else putAt[j] += value;
}
// The expiry's strikes under the lens, within the window around `centre`.
function collect(expiry: f64, centre: f64): void {
strikes = 0;
if (lens <= 2) {
const n = in_state_cells();
if (n <= 0) return;
const cells = in_state_view();
for (let i = 0; i + STATE - 1 < n; i += STATE) {
const strike = cells[i + 3];
if (cells[i + 1] != expiry || !(strike > 0.0) || Math.abs(strike / centre - 1.0) > windowShare) continue;
const present = cells[i + 12];
let value: f64 = NaN;
if (lens == 0 && hasBit(present, 10)) value = cells[i + 10]; // bit 10: the open interest
else if (lens == 1 && hasBit(present, 10) && hasBit(present, 11)) value = cells[i + 10] - cells[i + 11]; // bit 11: the prior day's
else if (lens == 2 && hasBit(present, 9)) value = cells[i + 9]; // bit 9: contracts today
if (!isNaN(value)) addStrike(strike, cells[i + 2], value);
}
return;
}
for (let at = 0; at < CAP; at += 1) {
const strike = keyStrike[at];
if (!used[at] || keyExpiry[at] != expiry || Math.abs(strike / centre - 1.0) > windowShare) continue;
addStrike(strike, keySide[at], lens == 3 ? premiumOf[at] : netOf[at]);
}
}
// The centre the window sits on: the close, unless no strike of the expiry lies within the window around it (an
// option contract's chart), then the expiry's middle strike.
function centreOf(expiry: f64, close: f64): f64 {
let low = Infinity;
let high = -Infinity;
let near = false;
const n = in_state_cells();
if (n > 0) {
const cells = in_state_view();
for (let i = 0; i + STATE - 1 < n; i += STATE) {
const strike = cells[i + 3];
if (cells[i + 1] != expiry || !(strike > 0.0)) continue;
if (strike < low) low = strike;
if (strike > high) high = strike;
if (close > 0.0 && Math.abs(strike / close - 1.0) <= windowShare) near = true;
}
}
for (let at = 0; at < CAP; at += 1) {
if (!used[at] || keyExpiry[at] != expiry) continue;
const strike = keyStrike[at];
if (strike < low) low = strike;
if (strike > high) high = strike;
if (close > 0.0 && Math.abs(strike / close - 1.0) <= windowShare) near = true;
}
if (near) return close;
return high >= low ? (low + high) / 2.0 : close;
}
// The upcoming expiries in the chain summary and today's flow, nearest first; their count.
function listExpiries(today: f64): i32 {
let count = 0;
const n = in_state_cells();
if (n > 0) {
const cells = in_state_view();
for (let i = 0; i + STATE - 1 < n; i += STATE) count = offerExpiry(cells[i + 1], today, count);
}
for (let at = 0; at < CAP; at += 1) if (used[at]) count = offerExpiry(keyExpiry[at], today, count);
return count;
}
function offerExpiry(expiry: f64, today: f64, count: i32): i32 {
if (!(expiry > 0.0) || optionsTradeDay(expiry, 1) < today) return count;
let j = 0;
while (j < count && expiryList[j] < expiry) j += 1;
if (j < count && expiryList[j] == expiry) return count;
if (j == MAX_EXPIRIES) return count;
for (let k = (count < MAX_EXPIRIES ? count : MAX_EXPIRIES - 1); k > j; k -= 1) expiryList[k] = expiryList[k - 1];
expiryList[j] = expiry;
return count < MAX_EXPIRIES ? count + 1 : count;
}
function fbCompact(value: f64): void { // 1.2M, 340K, -12K: the number in a few characters
const size = Math.abs(value);
if (size >= 1e9) {
fb_f64(value / 1e9, 1);
fb_text("B");
} else if (size >= 1e6) {
fb_f64(value / 1e6, 1);
fb_text("M");
} else if (size >= 1e3) {
fb_f64(value / 1e3, 1);
fb_text("K");
} else fb_f64(value, 0);
}
function writeChart(slot: i32, expiry: f64, today: f64): void {
let calls = 0.0;
let puts = 0.0;
for (let k = 0; k < strikes; k += 1) {
calls += callAt[k];
puts += putAt[k];
}
if (slot == 0) {
out_front_calls(calls);
out_front_puts(puts);
out_front_days(optionsTradeDay(expiry, 1) - today);
}
dateClock.update(optionsTradeDay(expiry, 1) * 86400.0);
const days = i64(optionsTradeDay(expiry, 1) - today);
fb_clear();
fb_text("{\"title\":\"");
if (dateClock.dayOfMonth() < 10) fb_text("0");
fb_int(dateClock.dayOfMonth());
fb_text(MONTHS[dateClock.month() - 1]);
fb_text(", ");
if (days == 0) fb_text("today");
else {
fb_int(days);
fb_text(days == 1 ? " day" : " days");
}
fb_text("\",\"badge\":{\"text\":\"");
if (lens >= 3 && (partialToday || dropped)) fb_text("≥ "); // at least: a bar served in part, or a full table, makes it a floor
fb_text("C ");
fbCompact(calls);
fb_text(" / P ");
fbCompact(puts);
fb_text("\",\"color\":\"#334155\"},\"rows\":[");
for (let k = 0; k < strikes; k += 1) {
if (k > 0) fb_text(",");
fb_text("[");
fb_num(strikeAt[k]);
fb_text(",");
fb_f64(callAt[k], 0);
fb_text(",");
fb_f64(lens == 0 || lens == 2 || lens == 3 ? -putAt[k] : putAt[k], 0); // a one-sided lens draws the puts below zero
fb_text("]");
}
fb_text("]}");
writeFrameBuffer(slot == 0 ? expiry_1 : slot == 1 ? expiry_2 : expiry_3);
}
// onBar() runs once per bar: a new trade date empties today's table; the bar's contracts join it; on the newest bar,
// one chart per upcoming expiry from the chain summary or today's table.
function onBar(): void {
const close = bar.close();
const today = optionsTradeDay(bar.time() * 1000.0, 1);
if (today != day) startDay(today);
const n = in_flow_cells();
if (n > 0) addFlow(in_flow_view(), n);
out_partial(n < 0 ? NaN : partialToday ? 1.0 : 0.0);
if (!bar.isLast()) {
out_front_calls(NaN);
out_front_puts(NaN);
out_front_days(NaN);
return;
}
const count = listExpiries(today);
if (count == 0) {
out_front_calls(NaN);
out_front_puts(NaN);
out_front_days(NaN);
return; // no chart: an unwritten frame leaves its chart off
}
for (let slot = 0; slot < charts && slot < count; slot += 1) {
const expiry = expiryList[slot];
collect(expiry, centreOf(expiry, close));
writeChart(slot, expiry, today);
}
}How it works
Two inputs, the six nearest expiries. Both inputs keep the six nearest expiries (expiries: "nearest:6") and the strikes within 15% of the underlying (strike_window_pct: 15), up to 6,000 contracts each. The chain summary arrives on the live bar with each contract's open interest (oi, bit 10), the prior observation (oi_prev, bit 11) and its contracts today (volume_day, bit 9). The flow arrives on every bar, one tuple per contract that traded in it.
Today's flow by contract. Today is the New York trade date of the bar's open; on a new trade date the table starts again. Each bar's contracts join a table keyed by the contract (root, expiry, call or put, strike): its premium (its VWAP times its contracts times 100, a standard contract's multiplier, never less than its premium at the ask plus at the bid, which stands in alone when it carries no VWAP) and its net premium (at the ask minus at the bid, where the data plane judges each print by the midpoint of the national best bid and offer just before it). The table holds 3,584 contracts (4,096 slots, an eighth kept free); a contract past that is left out, and the premium badges then read as a floor.
The lens. Lens picks each strike's two numbers: open interest (the default), open interest change (open interest minus the prior observation, for the contracts that carry both), contracts today, all three from the chain summary, or premium today and net premium today from the flow table. Open interest, contracts and premium only grow, so the puts draw below zero; the change and the net premium are signed, so each side keeps its own sign.
One chart per expiry. Expiries (2, up to 3) is how many charts stand under the price chart: the upcoming expiries in the chain summary and in today's flow (today's included), nearest first. Each chart keeps that expiry's strikes within Strike window, percent (8, up to the 15 the inputs read) of the close, up to 160 strikes, calls green and puts red at each strike on a number axis, titled by the expiry's date and days to go (16OCT, today on the expiry day). The badge sums each side over the strikes shown (C 1.2M / P 900.0K); under a premium lens it leads with ≥ when a bar of today was served in part or the table was full. A chart the setting does not ask for is never written, so it stays off the screen.
The readings. Data-only outputs on the live bar: front_calls and front_puts (the nearest expiry's two totals under the lens, over the strikes shown) and front_days (its days to go), with partial on every bar (1 once a bar of today was served in part).
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 Breakdown 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_state and option_contract_flow by name. On an option contract's chart both inputs read the contract's underlying; that chart's close is the contract's own price, so when no strike of an expiry sits within the window of it, the chart centres on the expiry's middle strike (halfway between its lowest and highest) instead. A block of more than 6,000 contracts 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 upcoming expiry. With no chain summary and no flow today on the live bar, no chart is written and the three readings read
NaN. - No block. A bar before the history the chart serves carries no flow block and adds nothing;
partialreadsNaNon it. - 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. Once a bar of today is served in part,partialreads 1 and the premium lenses' badges lead with≥: today's premium is at least what they show. - A strike without a value. A contract that does not carry the lens's value (no open interest, no prior observation, no contracts today) adds nothing to its strike.
Customize it
- Where the new positions are. Set Lens to open interest change: the strikes that gained contracts since the prior day stand out above and below zero.
- Today's money. Set Lens to premium today with three charts: the dollars spent at each strike of the three nearest expiries.
- A wider view. Raise Strike window to 15, the widest the inputs read, on a quiet stock, or lower it to 3 on an index ETF, where strikes sit a dollar apart.
Run it
- In the editor's Explorer, press the Templates icon ("Browse starter templates") and pick Strike Breakdown under Beyond the time axis.
- Press Run on a US stock chart such as SPY: once the chart serves US options the two nearest expiries stand under the chart by strike; until then the legend's chip names the source the chart does not serve yet.
- At the editor's Console prompt, type
front_callsandfront_putsto read the nearest expiry's totals under the lens.
Concepts used
- Data sources for
option_contract_stateandoption_contract_flow, their tuples and their knobs, and The present mask and partial bars forhasBit,isPartialand bit 14 - Bars and Frames, panels and compact widgets for
panel.barsover frames on a number axis, a per-run title and badge, and an unwritten frame leaving its panel off - Options kit for
optionsTradeDay