Dealer nodes

View as MarkdownOpen the editor

The strikes where the dealers' exposure in the chart's own options sits, drawn on every bar as orbs at the strikes that hold most of it, so the levels the dealers' hedging leans on read against the candles.

Each bar's board is the chart's strike rows summed by strike over the soonest expiries, in one lens: gamma (GEX, dollars per 1% move), delta (DEX, delta dollars) or vega (VEX, dollars per IV point). The largest strikes by size become nodes, each an orb sized by its share of the top strike, in the up colour above zero and the down colour below, the top brightest. On the newest bar the nodes carry labels with their strike and percent of the top and run ahead of the live bar as thin bands, and a line follows each bar's size-weighted centre strike with a band one weighted standard deviation either side. Derived levels draw another root's nodes on the chart, such as SPX's on an ES chart, each strike moved to the chart's price.

The parts are a celled option_strikes.cells input with the strike kit fold, fold: "kit", whose nearest and lens follow two settings through "@<param>" handles and whose underlying follows a third (Data sources, Chain page views), read with the options kit's OptionStrikes.loadLevels, classified with its NodeBoard and moved to the chart's price with its derivedRatio (Options kit), four plot.marks sections for the orbs, round and square (Marks), two lines and a fill for the centre and its band (Plotting), label and box handles drawn on the newest bar (Handles), and market.tick_size(), market.option_contract() and chart.interval_sec() for the bands' height, the contract chart and the bands' length. This is also the dealer-nodes template: the Dealer Nodes card under On price in the editor's starter list, and it compiles as written.

The wrun indicator

// Dealer Nodes: the strikes where the dealers' exposure in the chart's own options sits, drawn on every bar as orbs
// at the strikes that hold most of it, so the levels the dealers' hedging leans on read against the candles. Each bar
// reads the data plane's strike kit fold of option_strikes: its newest minute of strike rows, summed by strike over
// the soonest Expiries in the lens Exposure picks (gex the dealers' gamma in dollars per 1% move, dex their delta
// dollars, vex their vega dollars per IV point), at the strikes within five strike steps of the one nearest spot and
// the first strike past 5% of spot each way. A strike's size is its value's absolute size: Nodes count keeps the
// Node count largest, Nodes pvalue every strike at Significance of the top or more (the options kit's NodeBoard and
// its pctOfTop). A node is an orb at its strike on its bar, 4 to 18 px across by the square root of its share of
// the top (the chart draws a mark at least 7 px across), in the up colour above zero and the down colour below; the
// top node draws brightest, every other node at 0.6 of it. On the newest bar the nodes carry labels (the strike, and
// its percent of the top) at the chart's left or right edge or just after the bar, and thin bands one tick tall run
// ahead of the live bar for Projection bars at the chart's interval. The exposure centre is each bar's size-weighted
// mean strike over the whole board, with a band one weighted standard deviation either side. The strike rows reach
// back about seven days; on an option contract's own chart the price axis is the contract's premium, so it draws
// nothing there.
// Derived levels: Chain reads another root's chain (the input's underlying knob): derived the root the options kit's
// DERIVED_ROOTS maps the chart's root to (SPX on ES, NDX on NQ, GLD on GC, SLV on SI, DIA on YM, IWM on RTY, SPX on
// SPY, NDX on QQQ), or one root. Its board is read against that root's own spot, and every strike it draws sits at
// strike x the bar's ratio, derivedRatio(the chart's close, the rows' underlying): the orbs, the centre, the labels
// and the bands. Derived orbs draw as squares (plot.marks draws no diamond) and their labels say derived. While the
// derived chain's rows lag the chart (its session closed, or the newest minute not landed yet) the newest board keeps
// its labels and bands on the live bar: Derived ratio drifting draws them at that board's own ratio, latest at the
// newest bar's (its close over the newest underlying read). Every earlier bar keeps its own bar's ratio, since a run
// reads the bars forward and never redraws one.

section("Nodes");
param.choice("metric", ["gex", "dex", "vex"], "gex", { label: "Exposure", description: "What a node measures at its strike, summed over the expiries kept: gex the dealers' gamma in dollars per 1% move, dex their delta dollars, vex their vega dollars per IV point" });
param.choice("expiries", ["all", "front", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"], "4", { label: "Expiries", description: "The soonest expiries summed at each strike: front the nearest alone, all every expiry the strike rows hold (about seven days out), or that many" });
param.choice("nodes_mode", ["count", "pvalue"], "count", { label: "Nodes", description: "count: the Node count largest strikes by size; pvalue: every strike at Significance of the top's size or more" });
param.choice("nodes_count", ["1", "3", "5", "10", "15", "20"], "5", { label: "Node count", when: "nodes_mode=0", description: "With Nodes count: the strikes kept on each bar, largest first" });
param.choice("nodes_pvalue", ["15", "20", "30", "40", "50", "60"], "15", { label: "Significance, percent of the top", description: "With Nodes pvalue: a strike is kept at this share of the top's size or more" });
section("Labels and projection");
param.choice("labels", ["off", "strike", "strike_pct"], "strike_pct", { label: "Labels", description: "The newest bar's nodes: no label, the strike, or the strike and its percent of the top" });
param.choice("label_side", ["left", "right", "newest"], "right", { label: "Label place", when: "labels", description: "left or right: at that edge of the chart; newest: just after the newest bar" });
param.bool("projection", true, { label: "Projection", description: "The newest bar's nodes drawn ahead of the live bar as thin bands, one tick tall" });
param.int("projection_bars", 12, { min: 1, max: 50, label: "Projection, bars", when: "projection", description: "How far the bands run ahead of the live bar, in the chart's bars" });
param.bool("exposure_vwap", true, { label: "Exposure centre", description: "Each bar's size-weighted mean strike over the board, with a band one weighted standard deviation either side" });
section("Derived levels");
param.choice("derived", ["auto", "derived", "SPX", "SPY", "NDX", "QQQ", "GLD", "SLV", "DIA", "IWM"], "auto", { label: "Chain", description: "Whose chain the nodes read: auto the chart's own; derived the root the derived table maps the chart's root to (SPX on ES, NDX on NQ, GLD on GC, SLV on SI, DIA on YM, IWM on RTY, SPX on SPY, NDX on QQQ); or one root. Another root's nodes draw at the chart's price, each strike times the close over the rows' underlying" });
param.choice("derived_mode", ["latest", "drifting"], "drifting", { label: "Derived ratio", description: "While the derived chain's rows lag the chart, the newest board's labels and bands stay on the live bar: drifting at that board's own ratio, latest at the newest bar's. Earlier bars keep their own bar's ratio" });
input("close", ohlcv.close); // the chart's own close: the spot where the rows report no underlying, and the derived ratio's numerator
input("strikes", option_strikes.cells, { max_cells: 4000, venue: "auto", underlying: "@derived", fold: "kit", nearest: "@expiries", lens: "@metric", window_pct: 5, step_rows: 4, levels: true, description: "Each bar's strike rows folded by the data plane: a levels row (its spot), then [0, strike, value] at the strikes near spot, value the Exposure lens summed over the kept expiries; Chain names whose chain" });
market.tick_size(); // one tick: a projection band's height
market.option_contract(); // 1 on an option contract's own chart: its price axis is the contract's premium, so no nodes
chart.interval_sec(); // the chart's bar in seconds: a projection band's length
output("centre", line, overlay, { color: "theme.accent", width: 1, format: "price", label: "Exposure centre", description: "The board's size-weighted mean strike on the bar: the sum of |value| x strike over the sum of |value|, at the chart's price" }); // first output on price: the package homes on the chart
output("band_hi", line, overlay, { color: "theme.accent", width: 1, line_style: "dotted", format: "price", label: "Centre +1σ", description: "The centre plus one weighted standard deviation of the board's strikes" });
output("band_lo", line, overlay, { color: "theme.accent", width: 1, line_style: "dotted", format: "price", label: "Centre -1σ", description: "The centre minus one weighted standard deviation of the board's strikes" });
fill("band_hi", "band_lo", { color: "theme.accent", opacity: 0.06 }); // the band, lightly shaded
// The orbs: side 0 a value below zero (the down colour), 1 above it (the up colour); the size and the hover's percent
// are the node's share of the top. The top node draws brightest, the others at 0.6 of it. A derived chain's nodes
// draw in the two square sections instead (plot.marks draws no diamond).
plot.marks({ name: "top_node", slots: 1, size: { law: "sqrt", unit: 1, scale: 1 }, opacity_pct: 80, look: 0, text: { mode: 0 }, colors: ["theme.down", "theme.up"], tooltip: "Top node at {{price}}" });
plot.marks({ name: "nodes", slots: 19, size: { law: "sqrt", unit: 1, scale: 1 }, opacity_pct: 48, look: 0, text: { mode: 0 }, colors: ["theme.down", "theme.up"], tooltip: "Node at {{price}}, {{fills}}% of the top" });
plot.marks({ name: "top_derived", slots: 1, size: { law: "sqrt", unit: 1, scale: 1 }, opacity_pct: 80, look: 3, text: { mode: 0 }, colors: ["theme.down", "theme.up"], tooltip: "Top derived node at {{price}}" });
plot.marks({ name: "derived_nodes", slots: 19, size: { law: "sqrt", unit: 1, scale: 1 }, opacity_pct: 48, look: 3, text: { mode: 0 }, colors: ["theme.down", "theme.up"], tooltip: "Derived node at {{price}}, {{fills}}% of the top" });
// The readings, data-only: the Console reads them.
output("top_strike", none, overlay, { description: "The strike holding the bar's largest size, at the chart's price" });
output("top_value", none, overlay, { description: "Its value in the Exposure lens, USD" });
output("kept", none, overlay, { description: "The nodes kept on the bar" });
output("spot", none, overlay, { description: "The price the board reads against: the soonest kept expiry's underlying, else the close" });
output("ratio", none, overlay, { description: "A derived chain's ratio on the bar: the chart's close over the rows' underlying; NaN on the chart's own chain" });
string("node_label", { max_bytes: 40 }); // one bounded slot every node label is written through, one at a time
string("note", { max_bytes: 64 }); // the one line while there is nothing to draw
handles.label({ size: 11, style: "plain" }); // the newest bar's node labels
handles.box({ opacity: 0.6, borderWidth: 1 }); // the projection bands ahead of the live bar
render.label("note_label", { position: "top_center", text: "note", color: "theme.muted", style: "plain", offset: [0, 8] });

const MAX_NODES = 20; // the most nodes a bar keeps: the top's slot and the other 19
const NODE_COUNTS: StaticArray<i32> = [1, 3, 5, 10, 15, 20]; // Node count's choices
const NODE_PVALUES: StaticArray<f64> = [15.0, 20.0, 30.0, 40.0, 50.0, 60.0]; // Significance's choices, percent of the top
const MIN_PX = 4.0; // a node's diameter at no share of the top
const MAX_PX = 18.0; // the top's
const BASE_PX = 6.6; // the chart's sqrt mark of qty 1 at unit 1 and scale 1: 11 x (0.5 + 0.1 x scale) px across
const OTHER_SHARE = 0.6; // a node's brightness against the top's
const EDGE_PX = 8.0; // a left label: px from the chart's left edge
const AXIS_PX = 56.0; // a right label: px from the chart's right edge, clear of the price-axis pills
const keptAt = new StaticArray<i32>(MAX_NODES); // the bar's kept board indices, largest first
// The newest board's nodes, largest first, in its own root's strikes: the newest bar's labels and bands read them.
const shownStrike = new StaticArray<f64>(MAX_NODES);
const shownValue = new StaticArray<f64>(MAX_NODES);
const shownShare = new StaticArray<f64>(MAX_NODES);

function makeLabels(first: i32): LabelHandle[] { const out: LabelHandle[] = []; for (let i = 0; i < MAX_NODES; i += 1) out.push(draw.label(first + i)); return out; }
function makeBoxes(first: i32): BoxHandle[] { const out: BoxHandle[] = []; for (let i = 0; i < MAX_NODES; i += 1) out.push(draw.box(first + i)); return out; }
const nodeLabels = makeLabels(0); // handle objects allocate once; ids are one space across kinds
const projectionBands = makeBoxes(MAX_NODES);

let table = new OptionStrikes(1); // one bar's fold rows by strike; sized in onStart()
let nodeBoard = new NodeBoard(1); // one bar's board: the top and each strike's share of it; sized in onStart()
let boardStrikes = new StaticArray<f64>(1); // the board's strikes, ascending, and their values; sized in onStart()
let boardValues = new StaticArray<f64>(1);
let countMode = true; // settings, read in onStart()
let nodeCount = 5;
let pvalue = 15.0;
let labelMode = 2;
let labelSide = 1;
let projection = true;
let projectionBars = 12.0;
let centreOn = true;
let derivedOn = false; // Chain reads another root's chain
let latestRatio = false; // Derived ratio latest
let tickSize = 0.0; // the market's tick, read in onStart(); 0 where the host does not know it
let intervalSec = 0.0; // the chart's bar, read in onStart(); 0 where the host writes none
let optionContract = false;
let prevT: f64 = NaN; // the bar spacing, where the host names no interval
let barSec: f64 = NaN;
let boardCount = 0; // this bar's board
let keptCount = 0;
let boardSpot: f64 = NaN;
let ratio = 1.0; // this bar's derived ratio, 1 on the chart's own chain
let shownCount = 0; // the newest board's nodes, its ratio and its underlying
let shownRatio = 1.0;
let shownUnder: f64 = NaN;
let anyRows = false; // whether any bar of the run carried strike rows

// A host that hands a setting no value (a short init array) leaves it NaN, and i32(NaN) reads 0, the first choice: so
// each setting reads its declared default then, or when its value lies outside what it takes. Exposure and Expiries
// are the host's (the fold's lens and nearest), never read here.
function choiceOr(value: f64, choices: i32, fallback: i32): i32 { // a choice's picked index
  return value >= 0.0 && value < f64(choices) && value == Math.floor(value) ? i32(value) : fallback;
}
function flagOr(value: f64, fallback: bool): bool { // a bool
  return isNaN(value) ? fallback : value > 0.5;
}

function onStart(): void {
  table = new OptionStrikes(in_strikes_max_cells); // a block holds at most max_cells rows, so a load is never refused
  nodeBoard = new NodeBoard(in_strikes_max_cells);
  boardStrikes = new StaticArray<f64>(in_strikes_max_cells);
  boardValues = new StaticArray<f64>(in_strikes_max_cells);
  // The declared defaults: count, 5 nodes, Significance 15, strike_pct labels at the right, the projection on for
  // 12 bars, the centre on, Chain auto, Derived ratio drifting.
  countMode = choiceOr(p_nodes_mode(), 2, 0) == 0;
  nodeCount = NODE_COUNTS[choiceOr(p_nodes_count(), NODE_COUNTS.length, 2)];
  pvalue = NODE_PVALUES[choiceOr(p_nodes_pvalue(), NODE_PVALUES.length, 0)];
  labelMode = choiceOr(p_labels(), 3, 2);
  labelSide = choiceOr(p_label_side(), 3, 1);
  projection = flagOr(p_projection(), true);
  const bars = p_projection_bars();
  projectionBars = bars >= 1.0 && bars <= 50.0 ? Math.floor(bars) : 12.0;
  centreOn = flagOr(p_exposure_vwap(), true);
  derivedOn = choiceOr(p_derived(), 10, 0) != 0; // 0 is auto: the chart's own chain
  latestRatio = choiceOr(p_derived_mode(), 2, 1) == 0;
  tickSize = p_market_tick_size(); // NaN or 0 where the host knows none: a band's height then falls back
  intervalSec = p_chart_interval_sec(); // NaN or 0: the bars' own spacing
  optionContract = p_market_option_contract() == 1.0;
}

// Whether board index a outranks b: the larger size, then the strike nearer spot, then the lower strike (the top's
// own tie rule).
function outranks(a: i32, b: i32): bool {
  const sa = Math.abs(nodeBoard.value(a));
  const sb = Math.abs(nodeBoard.value(b));
  if (sa != sb) return sa > sb;
  const ga = Math.abs(nodeBoard.strike(a) - boardSpot);
  const gb = Math.abs(nodeBoard.strike(b) - boardSpot);
  if (ga != gb) return ga < gb;
  return nodeBoard.strike(a) < nodeBoard.strike(b);
}

// The bar's nodes, largest first: the strikes with a size, the Node count largest or every one at Significance of
// the top or more, at most MAX_NODES.
function keepNodes(): void {
  keptCount = 0;
  if (nodeBoard.top() < 0) return; // every size 0: no node
  for (let i = 0; i < boardCount; i += 1) {
    const size = Math.abs(nodeBoard.value(i));
    if (!(size > 0.0)) continue;
    if (!countMode && !nodeBoard.isSignificant(i)) continue;
    let at = keptCount < MAX_NODES ? keptCount : MAX_NODES - 1; // the slot that opens: the end, or the smallest kept
    if (keptCount == MAX_NODES && !outranks(i, keptAt[at])) continue;
    for (; at > 0 && outranks(i, keptAt[at - 1]); at -= 1) keptAt[at] = keptAt[at - 1];
    keptAt[at] = i;
    if (keptCount < MAX_NODES) keptCount += 1;
  }
  if (countMode && keptCount > nodeCount) keptCount = nodeCount;
}

// The orbs: the top node's slot, then the others' in order, each at its strike times the bar's ratio, in the round
// sections on the chart's own chain and the square ones on a derived chain; a slot left NaN draws nothing.
function writeMarks(): void {
  for (let r = 0; r < keptCount; r += 1) {
    const i = keptAt[r];
    const share = nodeBoard.pctOfTop(i);
    const px = MIN_PX + (MAX_PX - MIN_PX) * Math.sqrt(share / 100.0); // 4 to 18 px by the square root of the share
    const qty = (px / BASE_PX) * (px / BASE_PX); // the qty the chart's sqrt law draws that many px across
    const side = nodeBoard.value(i) > 0.0 ? 1 : 0;
    const percent = i32(Math.round(share));
    const price = nodeBoard.strike(i) * ratio;
    if (derivedOn) {
      if (r == 0) out_top_derived_mark(0, price, qty, side, percent, 0);
      else out_derived_nodes_mark(r - 1, price, qty, side, percent, 0);
    } else if (r == 0) out_top_node_mark(0, price, qty, side, percent, 0);
    else out_nodes_mark(r - 1, price, qty, side, percent, 0);
  }
}

// The exposure centre: the size-weighted mean strike over the whole board and one weighted standard deviation, both
// at the chart's price (times the bar's ratio).
function writeCentre(): void {
  let weight = 0.0;
  let moment = 0.0;
  for (let i = 0; i < boardCount; i += 1) {
    const size = Math.abs(nodeBoard.value(i));
    if (!(size > 0.0)) continue;
    weight += size;
    moment += size * nodeBoard.strike(i);
  }
  if (!centreOn || !(weight > 0.0)) {
    out_centre(NaN);
    out_band_hi(NaN);
    out_band_lo(NaN);
    return;
  }
  const centre = moment / weight;
  let spread = 0.0;
  for (let i = 0; i < boardCount; i += 1) {
    const size = Math.abs(nodeBoard.value(i));
    if (!(size > 0.0)) continue;
    const gap = nodeBoard.strike(i) - centre;
    spread += size * gap * gap;
  }
  const sigma = Math.sqrt(spread / weight);
  out_centre(centre * ratio);
  out_band_hi((centre + sigma) * ratio);
  out_band_lo((centre - sigma) * ratio);
}

// This bar's nodes become the newest board the labels and bands read, with its ratio and underlying.
function keepShown(under: f64): void {
  for (let r = 0; r < keptCount; r += 1) {
    const i = keptAt[r];
    shownStrike[r] = nodeBoard.strike(i);
    shownValue[r] = nodeBoard.value(i);
    shownShare[r] = nodeBoard.pctOfTop(i);
  }
  shownCount = keptCount;
  shownRatio = ratio;
  shownUnder = under;
}

function sbGrouped(n: i64): void { // 67,000
  if (n >= 1000) {
    sbGrouped(n / 1000);
    sb_text(",");
    const r = n % 1000;
    if (r < 100) sb_text("0");
    if (r < 10) sb_text("0");
    sb_int(r);
  } else sb_int(n);
}
function sbStrike(v: f64): void { // a whole strike without decimals (103, 6,650, 67,000), else as a price
  if (v >= 1.0 && v == Math.floor(v)) sbGrouped(i64(v));
  else if (v >= 1000.0) sbGrouped(i64(Math.round(v)));
  else if (v >= 100.0) sb_f64(v, 2);
  else if (v >= 1.0) sb_f64(v, 3);
  else sb_f64(v, 5);
}

// The newest board's labels at the ratio they are drawn at: the strike, and its percent of the top, and on a derived
// chain the word derived, at the chart's left or right edge or just after the bar; the top's at full strength, the
// others at 0.6 of it.
function drawLabels(after: f64, count: i32, at: f64): void {
  for (let r = 0; r < MAX_NODES; r += 1) {
    const handle = nodeLabels[r];
    if (labelMode == 0 || r >= count) {
      handle.delete(); // no-ops when never drawn
      continue;
    }
    const strike = shownStrike[r] * at;
    sb_clear();
    sbStrike(strike);
    if (labelMode == 2) {
      sb_text(" ");
      sb_int(i64(Math.round(shownShare[r])));
      sb_text("%");
    }
    if (derivedOn) sb_text(" derived");
    const tone = shownValue[r] > 0.0 ? theme.UP : theme.DOWN;
    if (labelSide == 0) handle.set(EDGE_PX, strike).text(str_node_label_sb).anchor(ANCHOR_LEFT).align(ALIGN_LEFT);
    else if (labelSide == 1) handle.set(AXIS_PX, strike).text(str_node_label_sb).anchor(ANCHOR_RIGHT).align(ALIGN_RIGHT);
    else handle.set(after, strike).text(str_node_label_sb).anchor(ANCHOR_CHART).align(ALIGN_LEFT);
    handle.color(tone).opacity(r == 0 ? 1.0 : OTHER_SHARE);
  }
}

// The projection: each node of the newest board as a band one tick tall around its strike at the ratio it is drawn
// at, from the live bar's close for Projection bars of the chart's interval.
function drawProjection(after: f64, step: f64, count: i32, at: f64): void {
  for (let r = 0; r < MAX_NODES; r += 1) {
    const band = projectionBands[r];
    if (!projection || r >= count || !(step > 0.0)) {
      band.delete();
      continue;
    }
    const strike = shownStrike[r] * at;
    const half = (tickSize > 0.0 ? tickSize : strike * 0.0001) * 0.5; // one tick around the strike
    const tone = shownValue[r] > 0.0 ? theme.UP : theme.DOWN;
    const edge = r == 0 ? tone : alpha(tone, OTHER_SHARE);
    band.set(after, strike + half, after + projectionBars * step, strike - half).fill(tone).color(edge).opacity(r == 0 ? 1.0 : OTHER_SHARE).border(1.0);
  }
}

// onBar() runs once per bar: the fold's rows become the bar's board (on a derived chain read against its own spot and
// drawn at the bar's ratio), its nodes the orbs, its sizes the centre; the newest bar labels the newest board's nodes
// and projects them ahead.
function onBar(): void {
  const t = bar.time();
  if (!isNaN(prevT) && t > prevT && (isNaN(barSec) || t - prevT < barSec)) barSec = t - prevT;
  prevT = t;
  const close = bar.close();
  const n = in_strikes_cells();
  boardCount = 0;
  keptCount = 0;
  boardSpot = NaN;
  ratio = 1.0;
  if (n > 0 && !optionContract) {
    anyRows = true;
    const count = table.loadLevels(in_strikes_view(), n); // -1: the bar held more rows than max_cells
    const under = table.underlying();
    if (derivedOn) ratio = derivedRatio(close, under); // NaN without an underlying: no board
    for (let i = 0; i < count; i += 1) {
      boardStrikes[i] = table.strike(i);
      boardValues[i] = table.value(i);
    }
    if (count > 0 && ratio > 0.0) {
      boardSpot = under > 0.0 ? under : close;
      boardCount = nodeBoard.load(boardStrikes, boardValues, count, boardSpot, pvalue);
      keepNodes();
      keepShown(under);
    }
  }
  writeMarks();
  writeCentre();
  const top = boardCount > 0 ? nodeBoard.top() : -1;
  out_top_strike(top >= 0 ? nodeBoard.strike(top) * ratio : NaN);
  out_top_value(top >= 0 ? nodeBoard.value(top) : NaN);
  out_kept(boardCount > 0 ? f64(keptCount) : NaN);
  out_spot(boardSpot);
  out_ratio(derivedOn && boardCount > 0 ? ratio : NaN);
  if (!bar.isLast()) return;
  const step = intervalSec > 0.0 ? intervalSec : barSec;
  const after = t + (step > 0.0 ? step : 0.0); // the live bar's close: everything drawn ahead starts there
  // The newest board: this bar's, or on a derived chain the newest one before it, at its own ratio (drifting) or at
  // the newest bar's (latest).
  const fresh = boardCount > 0;
  const carried = !fresh && derivedOn && shownCount > 0;
  const count = fresh || carried ? shownCount : 0;
  const at = fresh ? ratio : !carried ? 1.0 : latestRatio ? derivedRatio(close, shownUnder) : shownRatio;
  drawLabels(after, at > 0.0 ? count : 0, at);
  drawProjection(after, step, at > 0.0 ? count : 0, at);
  str_note(optionContract ? "Dealer nodes read the underlying's chart" : !anyRows ? "No strike rows on this chart yet" : carried ? "Derived levels from an earlier bar" : boardCount == 0 ? "No strike rows on the newest bar" : ""); // an empty text draws no label
}

How it works

One board a bar, folded by the data plane. input("strikes", option_strikes.cells, { fold: "kit", nearest: "@expiries", lens: "@metric", window_pct: 5, step_rows: 4, levels: true, max_cells: 4000 }) asks the data plane to fold each bar's newest minute of strike rows with the options kit's rules before it ships them: a levels row first (the snapshot's totals and spot), then one [0, strike, value] row per strike near spot, ascending, value the lens summed over the kept expiries. Expiries keeps the soonest expiries the rows list (front the nearest alone, all every one), and Exposure picks the lens: gex the strike rows' net_gex, dex their net_dex, vex their net_vex. The fold keeps the strikes within five strike steps of the one nearest spot, inside 5% of spot, and the first strike past that window on each side, so a bar ships about sixteen rows instead of thousands. OptionStrikes.loadLevels(cells, count) reads them: strike(i), value(i) and underlying(), the soonest kept expiry's underlying, the board's spot (the close where the rows report none).

The nodes. NodeBoard.load(strikes, values, count, spot, pct) sorts the board: its top is the strike with the largest size (the value's absolute size; a tie goes to the strike nearest spot, then the lower one), and pctOfTop(i) is a strike's size as a percent of the top's. Nodes count keeps the Node count largest strikes (1, 3, 5, 10, 15 or 20); pvalue keeps every strike whose share is at least Significance (15 to 60 percent of the top), at most 20. A strike with no size is never a node, and a board whose every size is 0 has none.

The orbs. Each node is a mark at its strike on its bar: the top in the top_node section, the others in nodes, largest first. Its diameter is 4 px plus 14 px times the square root of its share of the top, so the top draws 18 px across and a node at a quarter of the top 11 px; the chart draws a mark at least 7 px across, so the smallest nodes meet there. The value's sign picks the colour, the up colour above zero and the down colour below. The top's section fills at opacity_pct: 80, the brightest a mark draws, and every other node's at 48, 0.6 of it. A mark's hover reads the strike and its percent of the top.

Labels and projection. On the newest bar each node carries a label: its strike, and with Labels at strike_pct its percent of the top ("103 100%"), in its orb's colour, the top's at full strength and the others at 0.6. Label place puts them at the chart's left edge, at its right edge beside the price axis, or just after the newest bar. With Projection on, each node also runs ahead of the live bar as a band one tick tall around its strike (market.tick_size()), from the live bar's close for Projection, bars of the chart's interval (chart.interval_sec(), else the bars' own spacing): 12 bars on a 1-minute chart is 12 minutes ahead. Labels and bands are handles, redrawn on the newest bar alone.

The exposure centre. centre is each bar's size-weighted mean strike over the whole board, the sum of each strike's size times the strike over the sum of the sizes, and band_hi and band_lo sit one weighted standard deviation of the board's strikes either side, the square root of the size-weighted mean of each strike's squared distance from the centre. On the gex lens it is the board's gamma centre, on vex its vega centre. The band is lightly shaded.

Derived levels. Chain picks whose chain the nodes read, and the input hands its pick to the fold's underlying: "@derived": auto (the default) the chart's own chain, derived the root the options kit's DERIVED_ROOTS maps the chart's root to (SPX on ES, NDX on NQ, GLD on GC, SLV on SI, DIA on YM, IWM on RTY, SPX on SPY, NDX on QQQ), or one root by name. The host reads that root's chain on the venue family that lists it, and its rows keep their own underlying, that root's spot or forward. The board is sorted against that spot, so the nodes are the same ones the root's own chart shows; every strike it draws then sits at the chart's price, strike * derivedRatio(close, underlying), the chart's close over the rows' underlying on the bar: the orbs, the centre and its band, the labels and the bands. On an ES chart at 5,050 over an SPX board at 5,000 the ratio is 1.01, so the 5,000 strike draws at 5,050. Derived orbs draw in the two square sections, top_derived and derived_nodes, since plot.marks draws no diamond (its looks are a bubble, a soft dot, a ring and a square), and every derived label ends in "derived". ratio reads the bar's ratio.

The derived ratio. A derived root often trades other hours than the chart (SPX's options close while ES trades on), so the newest bar can carry no derived rows. With Chain on a derived root the newest board then keeps its labels and bands on the live bar, and Derived ratio says where: drifting (the default) at that board's own ratio, so they stay where it drew them, latest at the newest bar's ratio, its close over the newest underlying read, so they follow the chart. A bar with its own rows draws at its own ratio in both modes. Every earlier bar keeps its own bar's ratio: the host runs the bars forward and never redraws an earlier one, so no later ratio reaches it.

The readings. top_strike and top_value name the bar's top (at the chart's price) and its value in the lens, kept the nodes kept, spot the price the board reads against and ratio a derived chain's ratio.

Where it runs

Coins with listed options: the chart's browser lane serves option_strikes and its kit fold for the coin's own chain, and a copy you run from the editor reads it too. On a US stock or ETF chart with US listed options and on a CME futures chart the chart serves the strike rows to OpenMarket's official wrun indicators only, under the US options rule on a US stock (Limitations); a draft or a community copy is refused by name there. A chart whose market lists no options shows no strike rows. A derived root of US listed options (SPX, SPY, NDX, QQQ, GLD, SLV, DIA, IWM) is read under the same US options rule, so derived levels on a futures chart come through OpenMarket's official package. The strike rows reach back about seven days and cover the expiries within about seven days. The read stays under the 1 MB an indicator may download on every standard window (How much an indicator may download).

When data is missing

A bar without strike rows (outside the session, a minute the rows missed) draws no orb and no centre; its readings read NaN. A bar whose snapshot held more rows than max_cells arrives refused and draws nothing. Before any bar of the chart carried strike rows a line at the top of the chart says "No strike rows on this chart yet", and while the newest bar carries none it says "No strike rows on the newest bar" and draws no labels and no bands. With Chain on a derived root it keeps the newest board's labels and bands instead and says "Derived levels from an earlier bar". Chain derived on a chart whose root the derived table maps to none is refused by name, and so is a root no venue lists options for. A bar whose rows report no underlying draws no derived nodes. On an option contract's own chart (market.option_contract()) the price axis is the contract's premium, so the package draws nothing and says "Dealer nodes read the underlying's chart". On a chart that loads further back than the rows reach, the older bars draw nothing.

Customize it

  • Fewer, stronger nodes. Set Nodes to pvalue and Significance to 50: only the strikes at half the top's size or more remain.
  • The front expiry alone. Set Expiries to front for the exposure that settles first.
  • Vega instead of gamma. Set Exposure to vex: the nodes, the labels and the centre follow the dealers' vega by strike.
  • A longer projection. Raise Projection, bars, or turn Projection off to keep the space after the newest bar clear.
  • SPX levels on ES. Set Chain to derived on an ES chart (or to SPX on any chart): the SPX board draws as squares at the ES price. Set Derived ratio to latest to keep its newest labels with the price while SPX's options are closed.

Run it

  1. In the editor's Explorer, press the Templates icon ("Browse starter templates") and pick Dealer Nodes under On price.
  2. Press Run on a chart of a coin with listed options on a 1m or 5m interval, or through the official package on a US stock: the orbs fill bar by bar once the strike rows arrive.
  3. At the editor's Console prompt, type top_strike, kept and centre to read the newest bar's board.

Concepts used