Flow marks

View as MarkdownOpen the editor

The large US listed option prints (OPRA) of the chart's underlying, drawn on price.

Each print of at least the smallest premium you set is a mark at the underlying's price when it printed, at its own time inside the bar: green when it leans bullish (a call bought at the ask, a put sold at the bid), red when it leans bearish (a call sold at the bid, a put bought at the ask), slate at the mid or with no quote. A mark's area grows with the premium, and the largest mark of each bar prints its premium. Under the chart, each bar's bullish premium minus its bearish premium stands as one column, green or red by its sign.

The parts are a celled option_prints.cells input at the large-print floor (Data sources) read through its present mask (The present mask and partial bars), a plot.marks section with a size law bound to a setting (Marks), a histogram coloured per bar by a data-only ladder output and a render.bgcolor wash over the bars the chart served in part (Plotting, tints). This is also the flow-marks template: the Flow Marks card under Order flow in the editor's starter list, and it compiles as written.

The wrun indicator

// Flow Marks: the large US listed option prints (OPRA) of the chart's underlying, drawn on price. Each print of at least
// the smallest premium you set is a mark at the underlying's price when it printed, at its own time inside the bar:
// green when it leans bullish (a call bought at the ask or above, a put sold at the bid or below), red when it leans
// bearish (a call sold at the bid, a put bought at the ask), slate at the mid or with no quote. A mark's area grows with
// the premium and the largest of each bar prints it. Under the chart, each bar's bullish minus bearish premium as a
// column. The chart serves prints of 25,000 and up, up to 90 days of them. A bar it served in part (a busy bar cut to its
// largest 4,000 prints, or history past what the data plane keeps) is a floor, not a value: its column (when it has a
// kept print) turns slate, a faint wash covers it and partial reads 1. It reads the chart's own underlying, so it belongs on a US stock chart; on an
// option contract's chart the price axis is the contract's own, so the marks stay off and the columns still draw.
section("Prints");
param.number("min_premium", 100000, { min: 25000, max: 1000000000, step: 5000, label: "Smallest premium, USD", description: "A print below this premium draws no mark and counts in no column; the chart serves prints from 25,000" });
param.choice("types", ["calls and puts", "calls", "puts"], "calls and puts", { label: "Calls or puts" });
param.int("max_marks", 12, { min: 1, max: 24, label: "Marks per bar", description: "The bar's largest prints that draw a mark, biggest first" });
param.number("mark_unit", 100000, { min: 1000, max: 1000000000, step: 1000, label: "Mark unit, USD", description: "A print of this premium or less draws the smallest mark; the area grows with the premium" });
input("close", ohlcv.close); // the chart's own close: where a print without its underlying is marked, and the check that the chart is the underlying's
input("prints", option_prints.cells, { max_cells: 4000, min_premium: 25000, history: "90d", overflow: "largest", description: "The underlying's option prints of 25,000 and up: up to 90 days of history, live on a real-time chart; a busy bar keeps its largest 4,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
// The readings, data-only: the Console reads them. NaN on a bar the chart served no block for.
output("bull_premium", none, overlay, { format: "usd", description: "Premium of the bar's bullish prints at or above the smallest premium: calls at the ask or above, puts at the bid or below, USD" });
output("bear_premium", none, overlay, { format: "usd", description: "Premium of the bar's bearish prints: calls at the bid or below, puts at the ask or above, USD" });
output("marked", none, overlay, { description: "Prints in the bar at or above the smallest premium, of the types kept" });
output("partial", none, overlay, { description: "1 on a bar the chart served in part: its prints and premiums are a floor" });
output("net_premium", histogram, lower, { color_by: "net_tone", colors: ["#e5484d", "#30a46c", "#64748b"], label: "Bullish minus bearish premium", format: "usd", description: "The bar's bullish premium minus its bearish premium, USD" });
output("net_tone", none, lower, { description: "The column's colour: 0 bearish, 1 bullish, 2 a bar served in part" });
// The marks: side 0 bearish red, 1 bullish green, 2 slate; the hover reads the premium, the price and the contracts.
plot.marks({ name: "large_prints", slots: 24, size: { law: "sqrt", unit: "@mark_unit" }, opacity_pct: 35, x: 1, text: { mode: 1, field: "qty", format: "compact", min_px: 24 }, colors: ["#e5484d", "#30a46c", "#94a3b8"], tooltip: "{{qty}} premium at {{price}}, {{fills}} contracts" });
render.bgcolor("served_in_part", { where: "partial", color: "#64748b14" }); // a faint wash over a bar served in part

const PRINT = 16; // f64s per print: OPTION_PRINTS_TUPLE
const MAX_MARKS = 24; // the marks section's slots
const NEAR_SPOT = 0.2; // a print's underlying within 20% of the close: the chart is the underlying's
const largest = new StaticArray<i32>(MAX_MARKS); // the bar's largest kept prints, as offsets into the block, biggest first

let minPremium = 100000.0; // settings, read in onStart()
let types = 0;
let maxMarks = 12;

function onStart(): void {
  minPremium = p_min_premium();
  types = i32(p_types());
  maxMarks = i32(p_max_marks());
}

// +1 bullish, -1 bearish, 0 neutral, from the contract's side (+1 call, -1 put) and the print's side of the quote
// (aggressor +1 the ask, -1 the bid, 0 the mid, 2 no 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;
}

// Slides the print at offset `at` into the list of the bar's largest, biggest first; returns the list's new length.
function offer(cells: StaticArray<f64>, at: i32, kept: i32): i32 {
  const premium = cells[at + 8];
  if (kept == maxMarks && premium <= cells[largest[kept - 1] + 8]) return kept; // smaller than every print kept
  let i = kept < maxMarks ? kept : kept - 1; // the slot that opens: the end, or the smallest kept
  for (; i > 0 && cells[largest[i - 1] + 8] < premium; i -= 1) largest[i] = largest[i - 1];
  largest[i] = at;
  return kept < maxMarks ? kept + 1 : kept;
}

// onBar() runs once per bar: the bar's prints at or above the smallest premium, summed by lean, the largest kept for
// the marks; then the outputs, the column and the marks. A tuple whose size or premium is 0 is the chart's marker for
// a bar served in part, never a print.
function onBar(): void {
  const close = bar.close();
  const n = in_prints_cells();
  if (n < 0) { // no block on this bar: before the history the chart serves
    out_bull_premium(NaN);
    out_bear_premium(NaN);
    out_marked(NaN);
    out_partial(NaN);
    out_net_premium(NaN);
    out_net_tone(NaN);
    return;
  }
  let bull = 0.0;
  let bear = 0.0;
  let count = 0;
  let kept = 0;
  let partial = false;
  const cells = in_prints_view();
  for (let i = 0; i + PRINT - 1 < n; i += PRINT) {
    if (isPartial(cells[i + 15])) partial = true;
    const premium = cells[i + 8];
    if (!(cells[i + 7] > 0.0) || !(premium >= minPremium)) continue; // the marker tuple, or under the smallest premium
    const sideCp = cells[i + 4];
    if ((types == 1 && sideCp < 0.0) || (types == 2 && sideCp > 0.0)) continue;
    count += 1;
    const lean = leanOf(sideCp, cells[i + 9]);
    if (lean > 0) bull += premium;
    else if (lean < 0) bear += premium;
    kept = offer(cells, i, kept);
  }
  out_bull_premium(bull);
  out_bear_premium(bear);
  out_marked(f64(count));
  out_partial(partial ? 1.0 : 0.0);
  out_net_premium(count > 0 ? bull - bear : NaN); // a bar without a kept print draws no column
  out_net_tone(partial ? 2.0 : bull >= bear ? 1.0 : 0.0);
  for (let k = 0; k < kept; k += 1) {
    const at = largest[k];
    const underlying = cells[at + 11];
    const priced = hasBit(cells[at + 15], 2) && underlying > 0.0; // bit 2: the print carries its underlying
    const price = priced ? underlying : close;
    if (!(close > 0.0) || !(price > 0.0) || Math.abs(price / close - 1.0) > NEAR_SPOT) continue; // not the underlying's chart: no mark
    const lean = leanOf(cells[at + 4], cells[at + 9]);
    const size = cells[at + 7];
    const offset = cells[at];
    out_large_prints_mark(k, price, cells[at + 8], lean > 0 ? 1 : lean < 0 ? 0 : 2, size >= 65535.0 ? 65535 : i32(size), !(offset > 0.0) ? 0 : offset >= 1073741824.0 ? 1073741824 : i32(offset));
  }
}

How it works

The large prints. The input asks for every print of 25,000 and up (min_premium: 25000) with up to 90 days of history (history: "90d"), and on a real-time chart the live prints as they print. Every print at that floor is a large print the data plane keeps 90 days, so the history this indicator reads is whole wherever the chart reaches. A busy bar keeps its largest 4,000 prints by premium (overflow: "largest", max_cells: 4000) instead of refusing the run, and marks itself partial.

What a bar keeps. Smallest premium (100,000 by default, 25,000 at least) drops the smaller prints, and Calls or puts keeps one side. Each print left counts in marked and adds its premium to bull_premium or bear_premium by its lean; a print at the mid or with no quote counts but leans neither way.

The side and the lean. 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. A call at the ask or a put at the bid leans bullish; a call at the bid or a put at the ask leans bearish.

The marks. The bar's largest prints by premium, up to Marks per bar (12, at most 24), are marks at the print's own time inside the bar (x: 1): at the underlying's price when the print carries it (bit 2), at the bar's close when it does not. Bearish red, bullish green, neutral slate; the area grows with the premium from Mark unit (100,000), which draws the smallest mark, and the largest mark of each bar prints its premium where it is wide enough. The hover reads the premium, the price and the contracts. A print whose underlying sits more than 20% from the bar's close draws no mark, since the chart is not the underlying's.

The column. net_premium is the bar's bullish premium minus its bearish premium, a histogram in its own pane under the chart, green at or above zero and red below (net_tone picks the colour); a bar with no kept print draws no column. A bar the chart served in part draws its column slate.

The readings. Data-only outputs on every bar: bull_premium, bear_premium (USD), marked (the prints kept) 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). Once those lanes ship, the chart in the browser serves them under the US options access rule (Limitations): OpenMarket's official Flow Marks 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 by name. On an option contract's chart the input reads the contract's underlying; that chart's price axis is the contract's own price, so the marks stay off and the columns still draw. 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: every reading reads NaN and the bar draws nothing.
  • An empty block. A bar with no print of 25,000 and up reads 0 marked and no premium, and draws no mark and no column.
  • A bar served in part. Bit 14 of present (isPartial) marks a bar the chart cut down to its largest 4,000 prints, or one older than the history the chart reaches; a bar served in part that holds nothing arrives as one zero tuple whose only set bit is 14, which this indicator never counts as a print. On such a bar partial reads 1, its column (if it has one) turns slate, a faint slate wash covers it, and its premiums are a floor, not a value.

Customize it

  • Only the whales. Raise Smallest premium to 1,000,000 and Mark unit to 1,000,000: a few marks a day, each one a large bet.
  • One side of the book. Set Calls or puts to puts to watch the hedges and the downside bets alone.
  • A quieter chart. Lower Marks per bar to 3 on a short timeframe: only the largest prints of each bar draw.

Run it

  1. In the editor's Explorer, press the Templates icon ("Browse starter templates") and pick Flow Marks under Order flow.
  2. Press Run on a US stock chart such as SPY: once the chart serves US option prints the marks sit on the candles and the columns stand under the chart; until then the legend's chip names the source the chart does not serve yet.
  3. At the editor's Console prompt, type bull_premium and bear_premium to read the newest bar's large-print premium by lean.

Concepts used