---
title: "Flow tape"
description: "The US listed option prints (OPRA) of the chart's underlying, merged into the orders they most likely came from and tagged, newest first, in a table under the…"
order: 96
section: "cookbook"
---

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

# Flow tape

The US listed option prints (OPRA) of the chart's underlying, merged into the orders they most likely came from and tagged, newest first, in a table under the chart. Prints of one contract on one side of the quote within a short window of the first are one row: a **SWEEP** when they printed on two venues or more, a **SPLIT** when they printed on one venue, and a **BLOCK** when one print alone holds at least the block size. A row also reads **OPEN** when its contracts exceed the contract's open interest before the day, **VOL>OI** when the contract's volume today has passed that open interest, and **UNUSUAL**, this tape's own rule, when an order of today was bought at the ask out of the money, within 60 days of expiry, with VOL>OI and at least the unusual premium. Each row names its contract, so the chart can open that contract's chart from the row once the row click ships.

The parts are a celled `option_prints.cells` input ([Data sources](../core-concepts/data-sources.md#option_prints)) and a celled `option_contract_state.cells` input for the open interest and the volume today ([Data sources](../core-concepts/data-sources.md#option_contract_state)), both read through their `present` masks ([The present mask and partial bars](../core-concepts/data-sources.md#the-present-mask-and-partial-bars)), a list of open orders that the prints merge into, a ring of the closed rows, a frame feeding a `panel.table` under the chart whose rows name their contracts in `markets` with `fb_contract` ([Table](../presentation/cards-frames-panels.md#table), [Rows that open a market](../presentation/cards-frames-panels.md#rows-that-open-a-market)) and the options kit's `optionsTradeDay` for the trade dates ([Options kit](../functions/options-kit.md#the-markets-own-date-and-the-chains-clock)). This is also the `flow-tape` template: the **Flow Tape** card under **Order flow** in the editor's starter list, and it compiles as written.

## The wrun indicator

```typescript sample=flow-tape
// Flow Tape: the US listed option prints (OPRA) of the chart's underlying, merged into the orders they most likely
// came from and tagged, newest first, in a table under the chart. Prints of one contract on one side of the quote within
// a short window of the first are one row: a SWEEP when they printed on two venues or more (an order that took the best
// price on each venue in turn, inferred from the prints since the trade conditions are not served), a SPLIT when they
// printed on one venue, and a BLOCK when one print alone holds at least the block size. A row also reads OPEN when its
// contracts exceed the contract's open interest before the day (a print that size cannot all be closing), VOL>OI when
// the contract's volume today has passed that open interest, and UNUSUAL (this tape's own rule) when a row of today
// was bought at the ask, out of the money, within 60 days of expiry, with VOL>OI and at least the unusual premium.
// Open interest and volume come from the chain summary, so those tags read for today's prints, and OPEN for the prior
// day's too. A row reads the time (New York), the contract, the days to expiry, the kind, the side (AA above the ask,
// A at the ask, M the mid, B at the bid, BB below the bid), the contracts, the average price, the premium, the
// underlying's price, the prints and venues, the tags and the lean; each row names its contract so the chart can open
// it. The chart serves three days of prints of 5,000 and up, so a leg under 5,000 is not seen, and a busy bar keeps
// its largest 5,000 prints: the caption says when rows may miss legs.
section("Orders");
param.int("window_ms", 1000, { min: 50, max: 10000, label: "Merge window, ms", description: "Prints of one contract on one side within this many milliseconds of the first are one order" });
param.int("block_size", 500, { min: 10, max: 100000, label: "Block size, contracts", description: "One print of at least this many contracts reads BLOCK" });
param.number("unusual_premium", 100000, { min: 5000, max: 1000000000, step: 5000, label: "Unusual premium, USD", description: "The smallest premium an UNUSUAL row holds" });
section("Table");
param.number("min_premium", 50000, { min: 5000, max: 1000000000, step: 5000, label: "Smallest premium, USD", description: "A row below this premium is left out; the chart serves prints from 5,000" });
param.choice("show", ["every order", "sweeps, splits and blocks", "sweeps only"], "every order", { label: "Rows" });
param.int("rows", 30, { min: 5, max: 100, label: "Table rows", description: "The newest rows the table lists" });
input("close", ohlcv.close); // the chart's own candles: the grid the prints line up on
input("prints", option_prints.cells, { max_cells: 5000, min_premium: 5000, history: "3d", overflow: "largest", description: "The underlying's option prints of 5,000 and up: three days of history, live on a real-time chart; a busy bar keeps its largest 5,000 and says so" }); // [offset_ms, print_key, root_code, expiry_ms, side_cp, strike, price, size, premium, aggressor, outside, underlying, iv, delta, venue, present] per print
input("state", option_contract_state.cells, { max_cells: 16000, expiries: "nearest:12", strike_window_pct: 25, description: "The chain summary on the newest bar: each contract's open interest, the prior day's and its volume today, the twelve nearest expiries, strikes within 25% of the underlying" }); // [root_code, expiry_ms, side_cp, strike, bid, ask, last, prev_close, theta, volume_day, oi, oi_prev, present] per contract
// The readings, data-only: rows that closed on the bar (an order closes once its window has passed).
output("sweeps", none, overlay, { description: "Sweep rows that closed on the bar, at or above the smallest premium" });
output("sweep_lean", none, overlay, { format: "usd", description: "Premium of those sweeps that lean bullish minus those that lean bearish, USD" });
output("partial", none, overlay, { description: "1 on a bar the chart served in part: legs under its largest prints are missing" });
const tape_rows = frame("tape_rows", { max_bytes: 65536 }); // the table, rewritten on the live bar
panel.table({
  name: "tape",
  title: "Flow tape",
  x: "category",
  place: "below",
  frame: tape_rows,
  height_frac: 0.35,
  font_size: 10,
  cell_padding: 3,
  valign: "middle",
  header_text_color: "theme.muted",
  grid_color: "theme.grid",
  grid_width: 1,
  grid_lines: "rows",
  border_width: 0,
  series: [
    { name: "Time" },
    { name: "Contract" },
    { name: "DTE", align: "right", format: "int" },
    { name: "Kind" },
    { name: "Side", align: "center" },
    { name: "Size", align: "right", format: "int" },
    { name: "Price", align: "right", format: "0.00" },
    { name: "Premium", align: "right", format: "usd" },
    { name: "Spot", align: "right", format: "0.00" },
    { name: "Prints" },
    { name: "Tags" },
    { name: "Lean" },
  ],
});

const PRINT = 16; // f64s per print: OPTION_PRINTS_TUPLE
const STATE = 13; // OPTION_CONTRACT_STATE_TUPLE
const OPEN = 64; // orders still open to more prints
const VENUES = 8; // distinct venues an order counts
const ROW = 17; // f64s a closed row holds (the R_ offsets below)
const MAX_ROWS = 100; // the rows setting's largest value
// A row: its first print's time, the contract, the side of the quote, size, premium, price times size, prints, venues,
// whether a leg printed above the ask (1) or below the bid (2), the last underlying and IV seen, which of those two it
// holds (bit 2 the underlying, bit 0 the IV) and its bar's open.
const R_TIME = 0;
const R_ROOT = 1;
const R_EXPIRY = 2;
const R_SIDE = 3;
const R_STRIKE = 4;
const R_AGGRESSOR = 5;
const R_SIZE = 6;
const R_PREMIUM = 7;
const R_PX_SIZE = 8;
const R_LEGS = 9;
const R_VENUES = 10;
const R_OUTSIDE = 11;
const R_UNDERLYING = 12;
const R_IV = 13;
const R_HELD = 14;
const R_BAR = 15;
const R_PARTIAL = 16;
const MONTHS: StaticArray<string> = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
// A symbol code's characters (0-9 are 1 to 10, A-Z 11 to 36, the dot 37), written one by one so no row allocates.
const CODE_CHARS: StaticArray<string> = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "."];
const rootDigits = new StaticArray<i32>(10);
const orders = new StaticArray<f64>(OPEN * ROW); // the open orders, built like rows
const orderVenues = new StaticArray<f64>(OPEN * VENUES); // each open order's distinct venues
const orderUsed = new StaticArray<bool>(OPEN);
const tape = new StaticArray<f64>(MAX_ROWS * ROW); // the newest rows, a ring
const newYork = new Clock("America/New_York"); // a row's time of day
const dateClock = new Clock("UTC"); // an expiry's trade date, read from its day number

let windowMs = 1000.0; // settings, read in onStart()
let blockSize = 500.0;
let unusualPremium = 100000.0;
let minPremium = 50000.0;
let show = 0;
let rows = 30;
let head = 0; // the ring's next slot, and how many slots hold a row
let filled = 0;
let barSweeps = 0; // this bar's sweeps and their lean, for the outputs
let barSweepLean = 0.0;
let lastPartialMs: f64 = NaN; // the open of the newest bar served in part
let lastDay: f64 = NaN; // the newest bar's New York trade date, and the trade date before it
let priorDay: f64 = NaN;

function onStart(): void {
  windowMs = f64(p_window_ms());
  blockSize = f64(p_block_size());
  unusualPremium = p_unusual_premium();
  minPremium = p_min_premium();
  show = i32(p_show());
  rows = i32(p_rows());
}

// +1 bullish, -1 bearish, 0 neutral, from the contract's side (+1 call, -1 put) and the side of the quote.
function leanOf(sideCp: f64, aggressor: f64): i32 {
  if (aggressor == 1.0) return sideCp > 0.0 ? 1 : -1;
  if (aggressor == -1.0) return sideCp > 0.0 ? -1 : 1;
  return 0;
}

// 2 a sweep (two venues or more), 1 a split (one venue), 3 a block (one print of the block size), 0 one print.
function kindOf(row: StaticArray<f64>, at: i32): i32 {
  if (row[at + R_LEGS] >= 2.0) return row[at + R_VENUES] >= 2.0 ? 2 : 1;
  return row[at + R_SIZE] >= blockSize ? 3 : 0;
}

// ── Merging prints into orders ──
// An order leaves the open list: into the tape when it passes the table's filters.
function closeOrder(o: i32): void {
  orderUsed[o] = false;
  const at = o * ROW;
  if (!(orders[at + R_PREMIUM] >= minPremium)) return;
  const kind = kindOf(orders, at);
  if ((show == 1 && kind == 0) || (show == 2 && kind != 2)) return;
  if (kind == 2) {
    barSweeps += 1;
    const lean = leanOf(orders[at + R_SIDE], orders[at + R_AGGRESSOR]);
    if (lean > 0) barSweepLean += orders[at + R_PREMIUM];
    else if (lean < 0) barSweepLean -= orders[at + R_PREMIUM];
  }
  const base = head * ROW;
  for (let f = 0; f < ROW; f += 1) tape[base + f] = orders[at + f];
  head = (head + 1) % rows;
  if (filled < rows) filled += 1;
}
// Every order whose window has passed by `nowMs` closes, oldest first; with `all`, every order closes.
function closeDue(nowMs: f64, all: bool): void {
  for (;;) {
    let oldest = -1;
    for (let o = 0; o < OPEN; o += 1) {
      if (orderUsed[o] && (oldest < 0 || orders[o * ROW + R_TIME] < orders[oldest * ROW + R_TIME])) oldest = o;
    }
    if (oldest < 0 || (!all && orders[oldest * ROW + R_TIME] + windowMs >= nowMs)) return;
    closeOrder(oldest);
  }
}
// The print at offset `at` of the block joins its open order, or opens one.
function take(cells: StaticArray<f64>, at: i32, barMs: f64, partial: bool): void {
  const ms = barMs + cells[at];
  closeDue(ms, false);
  const root = cells[at + 2];
  const expiry = cells[at + 3];
  const sideCp = cells[at + 4];
  const strike = cells[at + 5];
  const aggressor = cells[at + 9];
  let o = -1;
  for (let k = 0; k < OPEN; k += 1) {
    const kb = k * ROW;
    if (orderUsed[k] && orders[kb + R_STRIKE] == strike && orders[kb + R_EXPIRY] == expiry && orders[kb + R_SIDE] == sideCp && orders[kb + R_ROOT] == root && orders[kb + R_AGGRESSOR] == aggressor) {
      o = k;
      break;
    }
  }
  if (o < 0) {
    for (let k = 0; k < OPEN && o < 0; k += 1) if (!orderUsed[k]) o = k;
    if (o < 0) { // every slot is open: the oldest order closes first
      let oldest = 0;
      for (let k = 1; k < OPEN; k += 1) if (orders[k * ROW + R_TIME] < orders[oldest * ROW + R_TIME]) oldest = k;
      closeOrder(oldest);
      o = oldest;
    }
    const fresh = o * ROW;
    for (let f = 0; f < ROW; f += 1) orders[fresh + f] = 0.0;
    orders[fresh + R_TIME] = ms;
    orders[fresh + R_ROOT] = root;
    orders[fresh + R_EXPIRY] = expiry;
    orders[fresh + R_SIDE] = sideCp;
    orders[fresh + R_STRIKE] = strike;
    orders[fresh + R_AGGRESSOR] = aggressor;
    orders[fresh + R_BAR] = barMs;
    orderUsed[o] = true;
  }
  const b = o * ROW;
  const size = cells[at + 7];
  const present = cells[at + 15];
  orders[b + R_SIZE] += size;
  orders[b + R_PREMIUM] += cells[at + 8];
  orders[b + R_PX_SIZE] += cells[at + 6] * size;
  orders[b + R_LEGS] += 1.0;
  if (partial) orders[b + R_PARTIAL] = 1.0;
  if (hasBit(present, 3)) { // bit 3: the quote was valid, so outside says where the leg printed
    const outside = cells[at + 10];
    const flags = i32(orders[b + R_OUTSIDE]);
    if (outside > 0.0) orders[b + R_OUTSIDE] = f64(flags | 1);
    else if (outside < 0.0) orders[b + R_OUTSIDE] = f64(flags | 2);
  }
  const held = i32(orders[b + R_HELD]);
  if (hasBit(present, 2)) { // bit 2: the underlying
    orders[b + R_UNDERLYING] = cells[at + 11];
    orders[b + R_HELD] = f64(held | 4);
  }
  if (hasBit(present, 0)) { // bit 0: the IV, percent
    orders[b + R_IV] = cells[at + 12];
    orders[b + R_HELD] = f64(i32(orders[b + R_HELD]) | 1);
  }
  const venue = cells[at + 14];
  const seen = i32(orders[b + R_VENUES]);
  let known = false;
  for (let v = 0; v < seen; v += 1) if (orderVenues[o * VENUES + v] == venue) known = true;
  if (!known && seen < VENUES) {
    orderVenues[o * VENUES + seen] = venue;
    orders[b + R_VENUES] = f64(seen + 1);
  }
}

// ── The table ──
// The characters of a symbol code into rootDigits, last first, and their count; 0 for anything no symbol encodes to.
function rootLength(code: f64): i32 {
  if (!(code >= 1.0) || code > 6278211847988223.0 || code != Math.floor(code)) return 0;
  let rest = u64(code);
  let count = 0;
  while (rest > 0) {
    const value = i32(rest % 38);
    if (value == 0 || count == 10) return 0;
    rootDigits[count] = value;
    count += 1;
    rest = rest / 38;
  }
  return count;
}
function fbRoot(code: f64): void { // the option root as listed, or ? when the code names none
  const count = rootLength(code);
  if (count == 0) fb_text("?");
  for (let i = count - 1; i >= 0; i -= 1) fb_text(CODE_CHARS[rootDigits[i] - 1]);
}
function fbPad(value: i32): void { // two digits, zero first
  if (value < 10) fb_text("0");
  fb_int(value);
}
function fbStrike(strike: f64): void { // 450, 452.5, 1.25: the strike as listed
  fb_f64(strike, strike == Math.floor(strike) ? 0 : strike * 10.0 == Math.floor(strike * 10.0) ? 1 : 2);
}
function fbContract(base: i32): void { // "SPY 450C 21NOV": the root, the strike, call or put, the expiry's date
  fb_text("\"");
  fbRoot(tape[base + R_ROOT]);
  fb_text(" ");
  fbStrike(tape[base + R_STRIKE]);
  fb_text(tape[base + R_SIDE] > 0.0 ? "C " : "P ");
  dateClock.update(optionsTradeDay(tape[base + R_EXPIRY], 1) * 86400.0);
  fbPad(dateClock.dayOfMonth());
  fb_text(MONTHS[dateClock.month() - 1]);
  fb_text("\"");
}
function fbTime(ms: f64, todayKey: i32): void { // 15:59:58 today, 16OCT 15:59 on an earlier day, New York time
  newYork.update(Math.floor(ms / 1000.0));
  const today = newYork.dayKey() == todayKey;
  fb_text("\"");
  if (!today) {
    fbPad(newYork.dayOfMonth());
    fb_text(MONTHS[newYork.month() - 1]);
    fb_text(" ");
  }
  fbPad(newYork.hour());
  fb_text(":");
  fbPad(newYork.minute());
  if (today) {
    fb_text(":");
    fbPad(newYork.second());
  }
  fb_text("\"");
}
// The chain summary's row for a contract: its offset into the block, or -1 when the summary does not hold it.
function stateOf(cells: StaticArray<f64>, n: i32, base: i32): i32 {
  const strike = tape[base + R_STRIKE];
  const expiry = tape[base + R_EXPIRY];
  const sideCp = tape[base + R_SIDE];
  const root = tape[base + R_ROOT];
  for (let i = 0; i + STATE - 1 < n; i += STATE) {
    if (cells[i + 3] == strike && cells[i + 1] == expiry && cells[i + 2] == sideCp && cells[i] == root) return i;
  }
  return -1;
}
// The row's tags into the frame, and whether it reads UNUSUAL. Open interest before the row's day: the summary's
// open interest for today's rows, the prior day's for the trade date before; volume today for today's rows only.
function fbTags(base: i32, state: StaticArray<f64>, n: i32, today: f64): bool {
  const day = optionsTradeDay(tape[base + R_TIME], 1);
  const at = n > 0 && (day == today || day == priorDay) ? stateOf(state, n, base) : -1;
  let priorOi: f64 = NaN;
  let volumeToday: f64 = NaN;
  if (at >= 0) {
    const present = state[at + 12];
    if (day == today) {
      if (hasBit(present, 10)) priorOi = state[at + 10]; // bit 10: the open interest
      if (hasBit(present, 9)) volumeToday = state[at + 9]; // bit 9: contracts today
    } else if (hasBit(present, 11)) priorOi = state[at + 11]; // bit 11: the prior day's, for the trade date before
  }
  const opening = !isNaN(priorOi) && tape[base + R_SIZE] > priorOi;
  const volOverOi = !isNaN(priorOi) && !isNaN(volumeToday) && volumeToday > priorOi;
  const spot = tape[base + R_UNDERLYING];
  const strike = tape[base + R_STRIKE];
  const sideCp = tape[base + R_SIDE];
  const otm = (i32(tape[base + R_HELD]) & 4) != 0 && spot > 0.0 && (sideCp > 0.0 ? strike >= spot * 0.995 : strike <= spot * 1.005);
  const dte = optionsTradeDay(tape[base + R_EXPIRY], 1) - day;
  const unusual = day == today && volOverOi && otm && dte <= 60.0 && tape[base + R_AGGRESSOR] == 1.0 && tape[base + R_PREMIUM] >= unusualPremium;
  fb_text(unusual ? "{\"text\":\"" : "\"");
  let words = 0;
  if (opening) {
    fb_text("OPEN");
    words += 1;
  }
  if (volOverOi) {
    fb_text(words > 0 ? " VOL>OI" : "VOL>OI");
    words += 1;
  }
  if (unusual) {
    fb_text(words > 0 ? " UNUSUAL" : "UNUSUAL");
    fb_text("\",\"text_color\":\"#f59e0b\",\"font_weight\":\"bold\"}");
  } else fb_text("\"");
  return unusual;
}
// The contract a row names so the chart can open it: a root of 1 to 6 characters, an expiry, a call or a put and a
// strike below 100,000; any other row names no market.
function fbMarket(base: i32): void {
  const count = rootLength(tape[base + R_ROOT]);
  const strike = tape[base + R_STRIKE];
  if (count < 1 || count > 6 || !(tape[base + R_EXPIRY] > 0.0) || !(strike > 0.0) || strike >= 100000.0) fb_text("null");
  else fb_contract(tape[base + R_ROOT], tape[base + R_EXPIRY], tape[base + R_SIDE] > 0.0 ? 1.0 : -1.0, strike);
}
function slotOf(k: i32): i32 { // the ring slot of the k-th newest row
  return ((head - 1 - k + rows * 2) % rows) * ROW;
}
function writeTable(nowMs: f64): void {
  newYork.update(Math.floor(nowMs / 1000.0));
  const todayKey = newYork.dayKey();
  const today = optionsTradeDay(nowMs, 1);
  const n = in_state_cells();
  const state = in_state_view();
  const oldestBarMs = filled > 0 ? tape[slotOf(filled - 1) + R_BAR] : NaN;
  let unusualRows = 0;
  fb_clear();
  fb_text("{\"rows\":[");
  for (let k = 0; k < filled; k += 1) {
    const base = slotOf(k);
    const sideCp = tape[base + R_SIDE];
    const aggressor = tape[base + R_AGGRESSOR];
    const lean = leanOf(sideCp, aggressor);
    const kind = kindOf(tape, base);
    const outside = i32(tape[base + R_OUTSIDE]);
    if (k > 0) fb_text(",");
    fb_text("[");
    fbTime(tape[base + R_TIME], todayKey);
    fb_text(",");
    fbContract(base);
    fb_text(",");
    fb_f64(optionsTradeDay(tape[base + R_EXPIRY], 1) - optionsTradeDay(tape[base + R_TIME], 1), 0);
    fb_text(kind == 2 ? ",{\"text\":\"SWEEP\",\"font_weight\":\"bold\"}," : kind == 1 ? ",\"SPLIT\"," : kind == 3 ? ",{\"text\":\"BLOCK\",\"font_weight\":\"bold\"}," : ",\"-\",");
    fb_text("{\"text\":\"");
    if (aggressor == 1.0) fb_text((outside & 1) != 0 ? "AA" : "A");
    else if (aggressor == -1.0) fb_text((outside & 2) != 0 ? "BB" : "B");
    else if (aggressor == 0.0) fb_text("M");
    else fb_text("-");
    fb_text(lean > 0 ? "\",\"text_color\":\"theme.up\"}," : lean < 0 ? "\",\"text_color\":\"theme.down\"}," : "\",\"text_color\":\"theme.muted\"},");
    fb_f64(tape[base + R_SIZE], 0);
    fb_text(",");
    fb_num(tape[base + R_SIZE] > 0.0 ? tape[base + R_PX_SIZE] / tape[base + R_SIZE] : NaN);
    fb_text(",");
    fb_f64(tape[base + R_PREMIUM], 0);
    fb_text(",");
    if ((i32(tape[base + R_HELD]) & 4) != 0) fb_num(tape[base + R_UNDERLYING]);
    else fb_text("\"\"");
    fb_text(",\"");
    fb_int(i64(tape[base + R_LEGS]));
    if (tape[base + R_LEGS] >= 2.0) {
      fb_text(" on ");
      fb_int(i64(tape[base + R_VENUES]));
      fb_text(tape[base + R_VENUES] >= 2.0 ? " venues" : " venue");
    }
    if (tape[base + R_PARTIAL] > 0.0) fb_text(", in part");
    fb_text("\",");
    if (fbTags(base, state, n, today)) unusualRows += 1;
    fb_text(lean > 0 ? ",{\"text\":\"Bullish\",\"text_color\":\"theme.up\"}]" : lean < 0 ? ",{\"text\":\"Bearish\",\"text_color\":\"theme.down\"}]" : ",{\"text\":\"Neutral\",\"text_color\":\"theme.muted\"}]");
  }
  fb_text("],\"markets\":[");
  for (let k = 0; k < filled; k += 1) {
    if (k > 0) fb_text(",");
    fbMarket(slotOf(k));
  }
  fb_text("],\"title\":\"Flow tape, ");
  fb_int(filled);
  fb_text(filled == 1 ? " order" : " orders");
  if (unusualRows > 0) {
    fb_text(", ");
    fb_int(unusualRows);
    fb_text(" unusual");
  }
  fb_text("\",\"caption\":\"");
  if (filled == 0) fb_text("No order passes the filters yet");
  else if (lastPartialMs >= oldestBarMs) fb_text("Some bars here were served in part: rows may miss legs");
  else fb_text("Newest first, New York time; sweeps inferred");
  fb_text("\"}");
  writeFrameBuffer(tape_rows);
}

// onBar() runs once per bar: the orders whose window passed before the bar opened close; the bar's prints merge into
// orders in time order; the outputs on every bar; on the live bar every open order closes into the table (the chart
// restores the state before the next tick, so an order still open keeps merging).
function onBar(): void {
  const barMs = bar.time() * 1000.0;
  const day = optionsTradeDay(barMs, 1);
  if (day != lastDay) {
    if (!isNaN(lastDay)) priorDay = lastDay;
    lastDay = day;
  }
  barSweeps = 0;
  barSweepLean = 0.0;
  closeDue(barMs, false);
  const n = in_prints_cells();
  let partial = false;
  if (n > 0) {
    const cells = in_prints_view();
    for (let i = 0; i + PRINT - 1 < n; i += PRINT) if (isPartial(cells[i + 15])) partial = true;
    for (let i = 0; i + PRINT - 1 < n; i += PRINT) {
      if (!(cells[i + 7] > 0.0) || !(cells[i + 8] > 0.0)) continue; // the chart's marker for a bar served in part, never a print
      take(cells, i, barMs, partial);
    }
  }
  if (partial) lastPartialMs = barMs;
  if (bar.isLast()) {
    closeDue(barMs, true);
    writeTable(barMs);
  }
  out_sweeps(n < 0 ? NaN : f64(barSweeps));
  out_sweep_lean(n < 0 ? NaN : barSweepLean);
  out_partial(n < 0 ? NaN : partial ? 1.0 : 0.0);
}
```

## How it works

**The kinds are inferred.** The data does not carry the exchanges' trade conditions (the code an exchange stamps on an intermarket sweep, a floor trade or a cross), so the tape infers each order's kind from the prints themselves: the same contract, the same side of the quote, and how many prints and venues an order took. A sweep here is an order that took the best price on two venues or more within the window; a split took several prints on one venue; a block is one print of at least **Block size, contracts** (500). The caption says so: "Newest first, New York time; sweeps inferred".

**Prints merge into orders.** A print joins the open order with the same contract (root, expiry, call or put, strike) and the same side of the quote (`aggressor`) whose first print is within **Merge window, ms** (1,000) of it; otherwise it opens a new order. An order closes once a print, or a bar's open, comes later than its window: the oldest order first, and the oldest when a 65th order would open. An order sums its contracts and its premium, counts its prints and up to eight distinct venues, keeps the volume-weighted average price of its legs, and keeps the last underlying and IV a leg carried. On the live bar every open order closes into the table; the chart restores the state before the next tick, so an order still inside its window keeps merging.

**The side of an order.** 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, exactly on it at the mid, and with no valid quote it has no side. An order's side cell reads `A`, `B`, `M` or a dash, and `AA` when one of its legs printed above the ask or `BB` when one printed below the bid (judged only where the quote was valid, bit 3). The lean reads the side against the contract: a call at the ask or a put at the bid leans bullish, a call at the bid or a put at the ask bearish, the mid and no side neutral; the side and lean cells take the chart's up colour, its down colour or the muted ink.

**The open interest tags.** The chain summary on the newest bar carries each contract's open interest (`oi`, bit 10), the prior observation (`oi_prev`, bit 11) and its contracts today (`volume_day`, bit 9). The open interest before an order's day is the summary's `oi` for an order of the newest bar's trade date, and its `oi_prev` for an order of the trade date before it; older orders read no open interest tag. **OPEN**: the order's contracts exceed that open interest, so not all of them can be closing. **VOL>OI**, for today's orders: the contract's volume today has passed its open interest. A contract the summary does not hold (an expiry past the twelve nearest, or a strike more than 25% from the underlying: `expiries: "nearest:12"`, `strike_window_pct: 25`) reads neither tag.

**UNUSUAL, this tape's own rule.** An order reads UNUSUAL when all of these hold: it is from today, its contract reads VOL>OI, it printed on the ask side (`A` or `AA`), it is out of the money against its last underlying (a call struck at or above 99.5% of it, a put at or below 100.5%), its expiry is within 60 days of the order's trade date, and its premium is at least **Unusual premium, USD** (100,000). The tags cell then turns amber and bold, and the title counts the unusual rows (`Flow tape, 30 orders, 2 unusual`).

**The table.** Closed orders at or above **Smallest premium, USD** (50,000) enter a ring as large as **Table rows** (30, up to 100); **Rows** keeps every order, only sweeps, splits and blocks, or only sweeps. The table is rewritten on the live bar, newest first: the first print's time in New York (hours, minutes and seconds on the newest bar's trade date, the day, the month, hours and minutes before it), the contract (`SPY 450C 21NOV`), its days to expiry, the kind (`SWEEP` and `BLOCK` in bold, `SPLIT`, or a dash for one print), the side, the contracts, the average price, the premium, the underlying's price (empty without one), the prints and venues (`4 on 3 venues`, with `, in part` when a leg came from a bar served in part), the tags and the lean.

**Rows that name their contract.** The frame's `markets` list holds one entry per row, in the rows' order, the order's contract written with `fb_contract(root_code, expiry_ms, side_cp, strike)`. A row whose root does not decode to 1 to 6 characters, or whose strike is not above 0 and below 100,000, names no market (`null`) and reads `?` where the root would be. The chart's click that opens a row's contract in a new chart tab is upcoming.

**The readings.** Three data-only outputs on every bar: `sweeps` (the sweep rows at or above the smallest premium that closed on the bar), `sweep_lean` (their bullish premium minus their bearish premium, USD) and `partial` (1 on a bar the chart served in part). Each reads `NaN` on a bar the chart served no block for.

## 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 Flow Tape 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_prints` and `option_contract_state` by name. On an option contract's chart both inputs read the contract's underlying, so the tape is the same as on the stock's chart. A chain summary with more contracts than 16,000 inside its window 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: the three readings read `NaN`.
- **An empty block.** A bar with no print of 5,000 and up merges nothing; the orders whose window has passed still close on it.
- **A bar served in part.** Bit 14 of `present` (`isPartial`) marks a bar the chart cut down to its largest 5,000 prints, or one past the history the data plane keeps; a bar served in part that holds nothing arrives as one zero tuple whose only set bit is 14, which this indicator never merges as a print. On such a bar `partial` reads 1, an order that took a leg from it reads `, in part` in its Prints cell, and the caption reads "Some bars here were served in part: rows may miss legs" while such a bar falls inside the rows the table lists. A leg under 5,000 of premium is never served, so a sweep made of smaller legs shows only its larger ones.
- **No chain summary.** Without the summary on the newest bar no row reads OPEN, VOL>OI or UNUSUAL; the kinds and the rest of the row are unchanged.
- **Nothing passes the filters.** The table lists no row and its caption reads "No order passes the filters yet".

## Customize it

- **A tighter merge.** Lower Merge window to 250 ms: only legs that printed together merge, which is how a sweep across venues usually prints.
- **The sweep tape.** Set Rows to sweeps only and Smallest premium to 100,000: the urgent orders that went venue to venue.
- **Bigger blocks.** Raise Block size to 2,000 on an index or a large ETF, where 500 contracts is an ordinary print.

## Run it

1. In the editor's Explorer, press the **Templates** icon ("Browse starter templates") and pick **Flow Tape** under **Order flow**.
2. Press **Run** on a US stock chart such as SPY: once the chart serves US option prints the tape stands under the chart, newest order first; until then the legend's chip names the source the chart does not serve yet.
3. At the editor's Console prompt, type `sweeps` and `sweep_lean` to read the sweeps that closed on the newest bar.

## Concepts used

- [Data sources](../core-concepts/data-sources.md#option_prints) for `option_prints` and [`option_contract_state`](../core-concepts/data-sources.md#option_contract_state), their tuples and their knobs, and [The present mask and partial bars](../core-concepts/data-sources.md#the-present-mask-and-partial-bars) for `hasBit`, `isPartial` and bit 14
- [Table](../presentation/cards-frames-panels.md#table) and [Rows that open a market](../presentation/cards-frames-panels.md#rows-that-open-a-market) for `panel.table` over a frame, styled cells and `markets`
- [Options kit](../functions/options-kit.md#the-markets-own-date-and-the-chains-clock) for `optionsTradeDay`
