Interval flow
The option contracts that traded on the chart's underlying in one bar's interval, one line per contract, for spotting the fills that take minutes rather than one print: a five-minute chart reads five-minute intervals.
A table under the chart lists the interval's top contracts by premium (or by volume) that pass the filters: the contract, its contracts, its premium, the share bought at the ask and sold at the bid, the average fill, the last IV and the interval's share of the contract's volume so far this session. The interval is the last closed bar by default, and a histogram under the table counts every bar's contracts that pass, so the busy intervals stand out.
The parts are a celled option_contract_flow.cells input read tuple by tuple (Data sources), hasBit for the values a tuple may lack and decodeSymbol for the contract's root (Data sources), a styled panel.table (Cards, frames and panels) and a histogram output. This is also the interval-flow template: the Interval Flow card under Order flow in the editor's starter list, and it compiles as written.
The wrun indicator
// Interval Flow: the option contracts that traded on the chart's underlying in one bar's interval, one line per
// contract, for spotting fills that take minutes rather than one print: a 5-minute chart reads 5-minute intervals.
// A table under the chart lists the interval's top contracts by premium (or by volume) that pass the filters: the
// contract, its contracts, its premium, the share bought at the ask and sold at the bid, the average fill, the last
// IV and the interval's share of the contract's volume so far this session. The interval is the last closed bar by
// default (Forming reads the bar trading now), and a histogram under the table counts every bar's contracts that
// pass, so the busy intervals stand out. Premium is the contract's VWAP times its contracts times 100 (a standard
// contract's shares); where the cells carry no VWAP (present bit 0 clear) it is the premium at the ask plus at the
// bid, a floor, printed in amber. The cells come from option_contract_flow, one tuple per contract that traded in
// each bar: an interval whose block was cut down (the partial bit, 14) says so in the table's caption; 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("Filters");
param.choice("rank", ["premium", "volume"], "premium", { label: "Rank by", description: "The table's order: premium or contracts" });
param.number("min_premium", 25000, { min: 0, max: 10000000, step: 1000, label: "Minimum premium", description: "A contract shows when its interval premium is at least this many USD" });
param.choice("side", ["any", "ask", "bid"], "any", { label: "Side", description: "any; ask: at least 55% of the contracts bought at the ask; bid: at least 55% sold at the bid" });
param.int("rows", 12, { min: 3, max: 30, label: "Rows", description: "Contracts the table lists" });
param.choice("shown", ["closed", "forming"], "closed", { label: "Interval", description: "closed: the last closed bar; forming: the bar trading now" });
input("close", ohlcv.close); // the chart's own candles: the bar grid, one interval each
input("trade_date", time.trade_date); // the bar's trade date: each contract's session volume starts 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
output("hits", histogram, lower, { color: "#f59e0b", label: "Contracts passing", format: "int", description: "The bar's contracts that pass the filters" });
output("hit_premium", none, lower, { description: "Their premium, USD" });
output("partial", none, lower, { description: "1 on a bar whose block was cut down (bit 14): its counts are a floor" });
output("skipped", none, lower, { description: "Tuples of this bar skipped because their contract is not whole" });
string("note", { max_bytes: 64 }); // the one line while there is no contract flow to list
render.label("note_label", { position: "top_center", text: "note", color: "#94a3b8", style: "knockout", offset: [0, 8] });
const contract_rows = frame("contract_rows", { max_bytes: 16384 });
panel.table({
name: "contracts",
title: "Interval flow",
x: "category",
place: "below",
frame: contract_rows,
height_frac: 0.3,
font_size: 10,
cell_padding: 3,
grid_lines: "rows",
header_text_color: "theme.muted",
series: [
{ name: "Contract" },
{ name: "Volume", align: "right", format: "int" },
{ name: "Premium", align: "right", format: "usd" },
{ name: "Ask", align: "right", format: "0", unit: "%" },
{ name: "Bid", align: "right", format: "0", unit: "%" },
{ name: "Avg fill", align: "right", format: "0.00" },
{ name: "IV", align: "right", format: "0.0", unit: "%" },
{ name: "Of day", align: "right", format: "0", unit: "%" },
],
});
const TUPLE = 17; // f64s per contract: OPTION_CONTRACT_FLOW_TUPLE
const MAX_ROWS = 30; // the table's rows, the Rows setting's ceiling
const CAP = 16384; // contracts a session's volume table holds (a power of two)
const SIDE_SHARE = 55.0; // the Side filter's share, percent
const NEW_YORK = 1;
const MONTHS: StaticArray<string> = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
// The session's volume per contract: open addressing, a slot free when it belongs to an earlier session.
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 slotSession = new StaticArray<i32>(CAP);
const dayVolume = new StaticArray<f64>(CAP);
// The top rows of this bar (cur) and of the bar before it (prev): one field per array, MAX_ROWS each.
const ROW_FIELDS = 11; // root, expiry, side, strike, volume, premium, floor, ask %, bid %, vwap, iv
const curRows = new StaticArray<f64>(MAX_ROWS * ROW_FIELDS);
const prevRows = new StaticArray<f64>(MAX_ROWS * ROW_FIELDS);
const curDay = new StaticArray<f64>(MAX_ROWS); // the interval's share of the contract's session volume, percent
const prevDay = new StaticArray<f64>(MAX_ROWS);
const timeClock = new Clock("America/New_York"); // the interval's open on OPRA's clock
const dateClock = new Clock("UTC"); // an expiry's date from its day number
let byVolume = false; // settings, read in onStart()
let minPremium = 25000.0;
let sideFilter = 0;
let rowLimit = 12;
let forming = false;
let day: f64 = NaN; // the trade date the volume table runs over
let sessionId = 1; // the volume table's session: a slot holding another number is free (every slot starts at 0)
let used = 0; // slots taken this session
let curCount = 0;
let prevCount = 0;
let curTime: f64 = NaN; // the bars' opens, epoch seconds
let prevTime: f64 = NaN;
let curPartial = false;
let prevPartial = false;
let curHits = 0;
let prevHits = 0;
let curHitPremium = 0.0; // the premium of every contract that passes this bar, not only the listed ones
let runCells = 0; // tuples read over the whole run
function onStart(): void {
byVolume = p_rank() == 1.0;
minPremium = p_min_premium();
sideFilter = i32(p_side());
rowLimit = i32(p_rows());
forming = p_shown() == 1.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);
}
function hashOf(root: f64, expiry: f64, side: f64, strike: f64): i32 {
let h: i64 = i64(root % 1000000007.0);
h = h * 31 + i64(expiry / 60000.0);
h = h * 31 + i64(Math.round(strike * 100.0));
h = h * 2 + (side > 0.0 ? 1 : 0);
h ^= h >> 17;
h *= 0x2545f491;
h ^= h >> 13;
return i32(h & i64(CAP - 1));
}
// The contract's slot in this session's volume table, claimed when new; -1 once the table is seven eighths full.
function slotOf(root: f64, expiry: f64, side: f64, strike: f64): i32 {
const h = hashOf(root, expiry, side, strike);
for (let probe = 0; probe < CAP; probe += 1) {
const at = (h + probe) & (CAP - 1);
if (slotSession[at] != sessionId) {
if (used >= CAP - CAP / 8) return -1;
slotSession[at] = sessionId;
keyRoot[at] = root;
keyExpiry[at] = expiry;
keySide[at] = side;
keyStrike[at] = strike;
dayVolume[at] = 0.0;
used += 1;
return at;
}
if (keyRoot[at] == root && keyExpiry[at] == expiry && keySide[at] == side && keyStrike[at] == strike) return at;
}
return -1;
}
// One row into this bar's top rows, kept in rank order; a row ranking below a full table is dropped.
function keepRow(cells: StaticArray<f64>, i: i32, volume: f64, premium: f64, floor: bool, askPct: f64, bidPct: f64, dayPct: f64): void {
const key = byVolume ? volume : premium;
let at = curCount;
while (at > 0) {
const above = (at - 1) * ROW_FIELDS;
const aboveKey = byVolume ? curRows[above + 4] : curRows[above + 5];
if (aboveKey >= key) break;
at -= 1;
}
if (at >= rowLimit) return;
const last = curCount < rowLimit ? curCount : rowLimit - 1;
for (let k = last; k > at; k -= 1) {
for (let f = 0; f < ROW_FIELDS; f += 1) curRows[k * ROW_FIELDS + f] = curRows[(k - 1) * ROW_FIELDS + f];
curDay[k] = curDay[k - 1];
}
const o = at * ROW_FIELDS;
const present = cells[i + 16];
curRows[o] = cells[i];
curRows[o + 1] = cells[i + 1];
curRows[o + 2] = cells[i + 2];
curRows[o + 3] = cells[i + 3];
curRows[o + 4] = volume;
curRows[o + 5] = premium;
curRows[o + 6] = floor ? 1.0 : 0.0;
curRows[o + 7] = askPct;
curRows[o + 8] = bidPct;
curRows[o + 9] = hasBit(present, 0) ? cells[i + 11] : NaN; // the VWAP when the cells carry it
curRows[o + 10] = hasBit(present, 1) ? cells[i + 13] : NaN; // the last IV, percent, when they carry it
curDay[at] = dayPct;
if (curCount < rowLimit) curCount += 1;
}
// One bar's block: every contract adds to its session volume; the ones that pass the filters count as hits and
// compete for the bar's top rows.
function fold(n: i32): i32 {
const cells = in_flow_view();
let skipped = 0;
for (let i = 0; i + TUPLE - 1 < n; i += TUPLE) {
if (!contractOk(cells, i)) {
skipped += 1;
continue;
}
runCells += 1;
const present = cells[i + 16];
if (isPartial(present)) curPartial = true;
const volAsk = cells[i + 4];
const volBid = cells[i + 5];
const volume = volAsk + volBid + cells[i + 6] + cells[i + 7];
if (!(volume > 0.0)) continue;
const slot = slotOf(cells[i], cells[i + 1], cells[i + 2], cells[i + 3]);
let dayPct: f64 = NaN;
if (slot >= 0) {
dayVolume[slot] += volume;
dayPct = (100.0 * volume) / dayVolume[slot];
}
const floor = !hasBit(present, 0); // no VWAP: the premium at the ask plus at the bid, a floor
const premium = floor ? cells[i + 8] + cells[i + 9] : cells[i + 11] * volume * 100.0;
if (premium < minPremium) continue;
const askPct = (100.0 * volAsk) / volume;
const bidPct = (100.0 * volBid) / volume;
if (sideFilter == 1 && askPct < SIDE_SHARE) continue;
if (sideFilter == 2 && bidPct < SIDE_SHARE) continue;
curHits += 1;
curHitPremium += premium;
keepRow(cells, i, volume, premium, floor, askPct, bidPct, dayPct);
}
return skipped;
}
function fbStrike(strike: f64): void { // "660", "662.5", "17.25": a whole strike prints bare
if (strike == Math.floor(strike)) fb_int(i64(strike));
else if (strike * 10.0 == Math.floor(strike * 10.0)) fb_f64(strike, 1);
else fb_f64(strike, 2);
}
function fbContract(o: i32, rows: StaticArray<f64>): void { // "SPY 16OCT 660C"
fb_text("\"");
fb_text(decodeSymbol(rows[o]));
dateClock.update(optionsTradeDay(rows[o + 1], NEW_YORK) * 86400.0);
fb_text(" ");
if (dateClock.dayOfMonth() < 10) fb_text("0");
fb_int(dateClock.dayOfMonth());
fb_text(MONTHS[dateClock.month() - 1]);
fb_text(" ");
fbStrike(rows[o + 3]);
fb_text(rows[o + 2] > 0.0 ? "C\"" : "P\"");
}
// The table on the live bar: the interval the setting picks, its top rows in rank order.
function writeTable(): void {
const rows = forming ? curRows : prevRows;
const dayPcts = forming ? curDay : prevDay;
const count = forming ? curCount : prevCount;
const t = forming ? curTime : prevTime;
const partial = forming ? curPartial : prevPartial;
const hits = forming ? curHits : prevHits;
fb_clear();
fb_text("{\"title\":\"Interval flow");
if (!isNaN(t)) {
timeClock.update(t);
fb_text(", ");
if (timeClock.hour() < 10) fb_text("0");
fb_int(timeClock.hour());
fb_text(":");
if (timeClock.minute() < 10) fb_text("0");
fb_int(timeClock.minute());
if (forming) fb_text(", forming");
}
fb_text("\",\"caption\":\"");
if (partial) fb_text("A floor: this interval's contracts were cut down");
else if (count == 0) fb_text("No contract passes the filters in this interval");
else {
fb_int(hits);
fb_text(hits == 1 ? " contract passes" : " contracts pass");
}
fb_text("\",\"rows\":[");
for (let k = 0; k < count; k += 1) {
const o = k * ROW_FIELDS;
if (k > 0) fb_text(",");
fb_text("[");
fbContract(o, rows);
fb_text(",");
fb_f64(rows[o + 4], 0);
fb_text(",");
if (rows[o + 6] > 0.0) { // a floor: amber
fb_text("{\"value\":");
fb_f64(rows[o + 5], 0);
fb_text(",\"text_color\":\"#f59e0b\"}");
} else fb_f64(rows[o + 5], 0);
fb_text(",");
fb_f64(rows[o + 7], 1);
fb_text(",");
fb_f64(rows[o + 8], 1);
fb_text(",");
fb_f64(rows[o + 9], 2);
fb_text(",");
fb_f64(rows[o + 10], 1);
fb_text(",");
fb_f64(dayPcts[k], 1);
fb_text("]");
}
fb_text("]}");
writeFrameBuffer(contract_rows);
}
// onBar() runs once per bar: this bar's top rows become the previous bar's, a new trade date starts the session's
// volume table again, then the bar's block is folded, the histogram drawn and, on the live bar, the table written.
function onBar(): void {
for (let k = 0; k < curCount * ROW_FIELDS; k += 1) prevRows[k] = curRows[k];
for (let k = 0; k < curCount; k += 1) prevDay[k] = curDay[k];
prevCount = curCount;
prevTime = curTime;
prevPartial = curPartial;
prevHits = curHits;
curCount = 0;
curTime = bar.time();
curPartial = false;
curHits = 0;
curHitPremium = 0.0;
const date = in_trade_date();
if (!isNaN(date) && date != day) {
day = date;
sessionId += 1;
used = 0;
}
const n = in_flow_cells();
const skipped = n >= TUPLE ? fold(n) : 0;
out_hits(n >= TUPLE ? f64(curHits) : NaN);
out_hit_premium(curHits > 0 ? curHitPremium : NaN);
out_partial(curPartial ? 1.0 : 0.0);
out_skipped(f64(skipped));
if (!bar.isLast()) return;
if (runCells > 0) writeTable();
str_note(runCells > 0 ? "" : "No contract flow on this chart yet"); // an empty text draws no label
}How it works
One line per contract. The bar's block holds one 17-value tuple per contract that traded in it. Its volume is the four sides added (vol_ask, vol_bid, vol_mid, vol_none); the ask and bid columns are those sides' shares of it. Premium is the contract's VWAP times its contracts times 100, a standard contract's shares. A tuple whose VWAP is not served (bit 0 of its present mask clear) has its premium at the ask plus at the bid instead, a floor, and the table prints it in amber; its average fill stays blank. The last IV, in percent, needs bit 1.
The filters. A contract shows when its interval premium reaches Minimum premium and, under Side, when at least 55% of its contracts were bought at the ask (ask) or sold at the bid (bid). The passing contracts compete for the table's Rows, ranked by premium or by volume; a tie keeps the contract the block listed first.
The share of the day. Every contract's volume, filtered or not, runs into a session table keyed by the contract (root, expiry, side, strike) that a new trade date empties. Of day is the interval's volume over that running total: a contract printing 40% of its day in one interval is a fill worth a look.
Which interval. Interval reads the last closed bar (closed) or the bar trading now (forming). The package keeps each bar's top rows as it passes, so the closed interval's table is ready on the live bar.
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
The table's caption says what the interval holds: how many contracts pass, "No contract passes the filters in this interval", or "A floor: this interval's contracts were cut down" when the block carries the partial bit (14), the data plane having cut it at a read's row cap. A bar with no block draws no histogram 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, never listed. "No contract flow on this chart yet" means no bar of the run carried a contract.
Customize it
- The ask-side intervals. Set Side to
askand Minimum premium to 100,000 for the aggressive buyers only. - Wider intervals. Read a 15-minute chart for fills spread over a quarter hour; the histogram then counts per quarter hour.
- Volume over open interest. Join the
option_contract_statecells on the live bar to compare each contract's day volume with its open interest.
Run it
- In the editor's Explorer, press the Templates icon ("Browse starter templates") and pick Interval Flow under Order flow.
- Press Run on a five-minute US stock chart: the official package lists the last closed interval's contracts under the chart.
- At the editor's Console prompt, type
last 20 hitsfor the last 20 intervals' passing contracts andhit_premiumfor the newest interval's premium.
Concepts used
- Data sources for the
option_contract_flowtuple - Data sources for
hasBit,isPartialand the symbol codec - Cards, frames and panels for the styled
panel.tableand its cells - Limitations for the US options rule and the upcoming OPRA sources