---
title: "Greek exposure"
description: "The dealers' exposure in the chart's own option chain, one greek at a time. The Lens setting picks gamma (the default), delta, vanna, charm or vega, or the…"
order: 115
section: "cookbook"
---

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

# Greek exposure

The dealers' exposure in the chart's own option chain, one greek at a time. The **Lens** setting picks gamma (the default), delta, vanna, charm or vega, or the open interest itself and IV times open interest; the **Expiries** setting counts every live expiry, the one settling today, those within a week or a month, the N nearest, or every expiry stacked by days to go. The exposure by strike docks on the price axis (net, calls against puts, or the stacked days), the exposure by expiry stands in bars under the chart, and a card at the bottom of the pane, in the terminal look, reads the lens in one plain line first ("Long gamma: dealer hedging dampens moves", "Long vanna: an IV rise makes dealers sell"), then the net, the calls, the puts, the same-day expiry, the strike holding the most exposure, where the running sum crosses zero, and the sums above and below spot.

The parts are a celled `options_chain.cells` input read on the live bar ([Data sources](../core-concepts/data-sources.md#options-chain)), the options kit's `Greeks`, `chainPricedAtMs` and `optionsTradeDay` for the priced greeks, the chain's clock and the calendar ([Options kit](../functions/options-kit.md#pricing-the-chain)), three frames feeding `plot.levels` docked on the price axis and one feeding `panel.bars` under the chart ([Cards, frames and panels](../presentation/cards-frames-panels.md#frames-panels-and-compact-widgets)), and a `render.hud` card with `look: "terminal"` over data-only outputs and two text slots ([HUD cards](../presentation/hud-and-hover-cards.md#hud-cards)). This is also the `greek-exposure` template: the **Greek Exposure** card under **HUDs** in the editor's starter list, and it compiles as written.

## The wrun indicator

```typescript sample=greek-exposure
// Greek Exposure: the dealers' exposure in the chart's own option chain, one greek at a time. The Lens setting picks
// gamma (the default), delta, vanna, charm or vega, or the open interest itself and IV times open interest; the
// Expiries setting counts every live expiry, the one settling today, those within a week or a month, the N nearest,
// or every expiry stacked by days to go (0, 1-7, 8-30, 31-60, 61-120, 121+). The exposure by strike docks on the
// price axis (net, calls against puts, or the stacked days), the exposure by expiry stands in bars under the chart,
// and a card in the terminal look reads the lens in one plain line first (Long gamma: dealer hedging dampens moves),
// then the net, the calls, the puts, the same-day expiry and the strikes that matter.
// The convention is the open-interest model flow tools use: dealers long the calls and short the puts, so calls add
// and puts subtract on every lens but delta, which keeps each option's own sign (calls plus, puts minus) as those
// tools' delta views do. Units: dollars, or units of the underlying (shares on a US stock, contracts on CME, coins on
// crypto) for gamma per 1% move of spot, delta held, vanna per IV point and charm per calendar day (some tools
// multiply charm by 365; this does not). Vega reads dollars per IV point either way. The chain rides the live bar
// only: history bars draw nothing, and everything is measured on the last bar.

section("Lens");
param.choice("lens", ["gamma", "delta", "vanna", "charm", "vega", "iv_x_oi", "oi"], "gamma", { label: "Lens", description: "gamma per 1% move, delta held, vanna per IV point, charm per day, vega per IV point, IV times open interest, or open interest" });
param.choice("units", ["dollars", "underlying"], "dollars", { label: "Units", description: "Dollars, or units of the underlying (shares, contracts or coins) on gamma, delta, vanna and charm" });
param.choice("split", ["net", "calls_puts"], "net", { label: "By strike", description: "net: one bar per strike, calls minus puts; calls_puts: the calls one way, the puts the other" });
section("Expiries and strikes");
param.choice("scope", ["all", "0dte", "week", "month", "nearest", "buckets"], "all", { label: "Expiries", description: "all; 0dte: the expiry settling today; week: within 7 days; month: within 30 days; nearest: the N nearest; buckets: every expiry, stacked by days to go" });
param.int("nearest", 4, { min: 1, max: 24, label: "Nearest expiries", description: "The N nearest live expiries, under Expiries: nearest" });
param.number("window_pct", 8, { min: 1, max: 50, label: "Strike window, percent", description: "Strikes docked on the price axis: percent around spot, each side" });
param.number("rate_pct", 4, { min: 0, max: 20, step: 0.25, label: "Rate, percent", description: "The rate vanna, charm and vega are priced at on a US stock or CME chain; a crypto chain prices at 0" });
input("close", ohlcv.close); // the chart's own close: spot where a contract reports no underlying, and the window's centre
input("chain", options_chain.cells, { max_cells: 4000, venue: "auto", description: "The chart's own option chain: OPRA on a US stock, CME on futures, Deribit on BTC and ETH" }); // [strike, expiry_ms, side, oi, gamma, delta, mark_iv, underlying, multiplier, vega] per contract
market.zone(); // the market's clock: 0 UTC (crypto), 1 New York (US stocks), 2 Chicago (CME): the calendar, the model and the unit word
// The readings, data-only: the card's tiles read them and the Console can too. Each is NaN on history bars.
output("net_exposure", none, overlay, { description: "The lens over the counted expiries, calls plus and puts minus (delta: each option's own sign), in the card title's unit; live bar only" });
output("calls_exposure", none, overlay, { color: "#38bdf8", description: "The calls' part of the net" });
output("puts_exposure", none, overlay, { color: "#a78bfa", description: "The puts' part of the net, signed as it counts" });
output("today_exposure", none, overlay, { description: "The net of the expiry settling today; NaN when none is listed or counted" });
output("top_strike", none, overlay, { description: "The strike holding the largest net exposure, either sign (the gamma magnet on the gamma lens)" });
output("top_exposure", none, overlay, { description: "That strike's net exposure" });
output("zero_cross", none, overlay, { description: "Where the net, summed up the strikes, crosses zero nearest spot (the gamma flip on the gamma lens)" });
output("above_spot", none, overlay, { description: "The net over the strikes above spot" });
output("below_spot", none, overlay, { description: "The net over the strikes at or below spot" });
output("tone", none, overlay, { description: "The headline's colour: 0 net below zero, 1 no reading, 2 net above zero; live bar only" });
output("expiries_counted", none, overlay, { description: "The live expiries the Expiries setting counts" });
string("read", { max_bytes: 64 }); // the headline: the lens's one plain line, or why there is no reading
string("title", { max_bytes: 64 }); // the card's title: the lens, its unit and the expiries counted
// The docked profiles, one per shape; only the one the settings pick is written, so the other two stay absent.
const net_rows = frame("net_rows", { max_bytes: 24576 });
const split_rows = frame("split_rows", { max_bytes: 32768 });
const day_rows = frame("day_rows", { max_bytes: 65536 });
const expiry_rows = frame("expiry_rows", { max_bytes: 8192 });
plot.levels({ name: "by_strike", frame: net_rows, dock: "right", width_frac: 0.14, baseline: "center", labels: false, color: "#38bdf8", thickness_px: 10, hover: true, format: "si" });
plot.levels({ name: "calls_puts", frame: split_rows, dock: "right", width_frac: 0.14, baseline: "center", labels: false, thickness_px: 10, hover: true, format: "si", series: [{ name: "Calls", color: "#38bdf8" }, { name: "Puts", color: "#a78bfa" }] });
plot.levels({ name: "by_days", frame: day_rows, dock: "right", width_frac: 0.16, baseline: "center", labels: false, thickness_px: 10, hover: true, format: "si", series: [{ name: "0d", color: "#f43f5e" }, { name: "1-7d", color: "#f59e0b" }, { name: "8-30d", color: "#eab308" }, { name: "31-60d", color: "#22c55e" }, { name: "61-120d", color: "#38bdf8" }, { name: "121d+", color: "#a78bfa" }] });
panel.bars({ name: "by_expiry", title: "Net by expiry", x: "category", place: "below", frame: expiry_rows, orientation: "vertical", chrome: "grid", color_mode: "sign", positive_color: "#38bdf8", negative_color: "#a78bfa", series: [{ name: "Net" }], format: "si", hover_card: true, legend_style: "title", height_frac: 0.18 });

// The card, in the terminal look: the lens's plain line leads as the headline, coloured by the sign of the net
// (violet below zero, slate for no reading, sky above), the title names the lens, its unit and the expiries counted.
render.hud("exposure", {
  position: "bottom_center",
  look: "terminal",
  title: "{{title}}",
  columns: 2,
  tiles: [
    tile.pill("Dealers", "read", { headline: true, color_by: "tone", colors: ["#a78bfa", "#94a3b8", "#38bdf8"] }),
    tile.value("Net", "net_exposure", { format: "si" }),
    tile.value("Same-day expiry", "today_exposure", { format: "si" }),
    tile.value("Calls", "calls_exposure", { format: "si" }),
    tile.value("Puts", "puts_exposure", { format: "si" }),
    tile.rows([
      ["1) Largest strike", "top_strike", "price"],
      ["2) Its exposure", "top_exposure", "si"],
      ["3) Zero crossing", "zero_cross", "price"],
      ["4) Above spot", "above_spot", "si"],
      ["5) Below spot", "below_spot", "si"],
    ]),
  ],
});

const TUPLE = 10; // f64s per contract: strike, expiry_ms, side, oi, gamma, delta, mark_iv, underlying, multiplier, vega
const MAX_STRIKES = 1024; // distinct strikes kept, the nearest spot when the chain lists more
const MAX_EXPIRIES = 64; // live expiries kept, the nearest when the chain lists more
const MAX_ROWS = 256; // rows in the docked profile, the strikes nearest spot inside the window
const DAYS = 6; // the stacked buckets: 0, 1-7, 8-30, 31-60, 61-120, 121+ days to go
const MIN_T_MS = 900000.0; // 15 minutes: a contract closer to its expiry prices no time left, as the chart's greeks do
const GAMMA = 0;
const DELTA = 1;
const VANNA = 2;
const CHARM = 3;
const VEGA = 4;
const IV_X_OI = 5;
const SCOPE_ALL = 0;
const SCOPE_TODAY = 1;
const SCOPE_WEEK = 2;
const SCOPE_MONTH = 3;
const SCOPE_NEAREST = 4;
const SCOPE_BUCKETS = 5;
const LENS_NAMES: String[] = ["Gamma", "Delta", "Vanna", "Charm", "Vega", "IV x OI", "Open interest"];
const LENS_WORDS: String[] = ["gamma", "delta", "vanna", "charm", "vega", "implied volatility", "open interest"]; // the no-reading line
const MONTHS: String[] = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
const strikePrice = new StaticArray<f64>(MAX_STRIKES); // the strike table, ascending
const callAt = new StaticArray<f64>(MAX_STRIKES); // the calls' exposure per strike
const putAt = new StaticArray<f64>(MAX_STRIKES); // the puts' exposure per strike, signed as it counts
const dayAt = new StaticArray<f64>(MAX_STRIKES * DAYS); // the net per strike and bucket: [strike * DAYS + bucket]
const expiryAt = new StaticArray<f64>(MAX_EXPIRIES); // the live expiries, ascending
const expiryDays = new StaticArray<f64>(MAX_EXPIRIES); // days to go on the market's calendar, 0 on the expiry day
const expiryNet = new StaticArray<f64>(MAX_EXPIRIES);
const contractValue = new StaticArray<f64>(in_chain_max_cells); // each contract's exposure on the lens; NaN when it does not count
const expiryKept = new StaticArray<bool>(MAX_EXPIRIES); // counted by the Expiries setting
const greeks = new Greeks(); // prices vanna, charm and vega per contract; allocated once
const dateClock = new Clock("UTC"); // reads a date from its day number

let lens = GAMMA; // settings, read in onStart()
let inUnderlying = false;
let splitSides = false;
let scope = SCOPE_ALL;
let nearestCount = 4;
let windowPct = 0.08;
let rate = 0.04;
let zone = 0;
let close: f64 = NaN; // this bar
let t: f64 = NaN;
let strikeCount = 0;
let expiryCount = 0;
let counted = 0; // expiries the setting counts
let readings = 0; // contracts that carried a nonzero reading for the lens
let net: f64 = NaN; // the readings, measured on the live bar
let calls: f64 = NaN;
let puts: f64 = NaN;
let today: f64 = NaN;
let topStrike: f64 = NaN;
let topValue: f64 = NaN;
let zeroCross: f64 = NaN;
let above: f64 = NaN;
let below: f64 = NaN;

function onStart(): void {
  lens = i32(p_lens());
  inUnderlying = p_units() == 1.0;
  splitSides = p_split() == 1.0;
  scope = i32(p_scope());
  nearestCount = i32(p_nearest());
  windowPct = p_window_pct() / 100.0;
  zone = i32(p_market_zone());
  rate = zone == 0 ? 0.0 : p_rate_pct() / 100.0; // crypto chains are quoted at a rate of 0
}

// ── The tables: sorted insertion, the nearest kept when full ──
function noteExpiry(ms: f64): void {
  let lo = 0;
  let hi = expiryCount;
  while (lo < hi) {
    const mid = (lo + hi) >> 1;
    if (expiryAt[mid] < ms) lo = mid + 1;
    else hi = mid;
  }
  if (lo < expiryCount && expiryAt[lo] == ms) return;
  if (expiryCount >= MAX_EXPIRIES) {
    if (lo >= expiryCount) return; // later than every kept expiry
    expiryCount -= 1; // the latest makes room
  }
  for (let k = expiryCount; k > lo; k -= 1) expiryAt[k] = expiryAt[k - 1];
  expiryAt[lo] = ms;
  expiryCount += 1;
}
function findExpiry(ms: f64): i32 {
  let lo = 0;
  let hi = expiryCount;
  while (lo < hi) {
    const mid = (lo + hi) >> 1;
    if (expiryAt[mid] < ms) lo = mid + 1;
    else hi = mid;
  }
  return lo < expiryCount && expiryAt[lo] == ms ? lo : -1;
}
function copyRow(from: i32, to: i32): void {
  strikePrice[to] = strikePrice[from];
  callAt[to] = callAt[from];
  putAt[to] = putAt[from];
  for (let b = 0; b < DAYS; b += 1) dayAt[to * DAYS + b] = dayAt[from * DAYS + b];
}
function findStrike(strike: f64): i32 { // the row of a kept strike, -1 when the table does not hold it
  let lo = 0;
  let hi = strikeCount;
  while (lo < hi) {
    const mid = (lo + hi) >> 1;
    if (strikePrice[mid] < strike) lo = mid + 1;
    else hi = mid;
  }
  return lo < strikeCount && strikePrice[lo] == strike ? lo : -1;
}
function strikeSlot(strike: f64): i32 { // the row of this strike, inserted when new; -1 when the table is full and it is the farthest from spot
  let lo = 0;
  let hi = strikeCount;
  while (lo < hi) {
    const mid = (lo + hi) >> 1;
    if (strikePrice[mid] < strike) lo = mid + 1;
    else hi = mid;
  }
  if (lo < strikeCount && strikePrice[lo] == strike) return lo;
  if (strikeCount >= MAX_STRIKES) { // full: the strike farthest from spot leaves, the lower one on a tie
    const dLow = Math.abs(strikePrice[0] - close);
    const dHigh = Math.abs(strikePrice[strikeCount - 1] - close);
    const dNew = Math.abs(strike - close);
    if (dLow >= dHigh) {
      if (dNew > dLow || (dNew == dLow && strike < strikePrice[0])) return -1;
      for (let k = 1; k < strikeCount; k += 1) copyRow(k, k - 1);
      strikeCount -= 1;
      if (lo > 0) lo -= 1;
    } else {
      if (dNew > dHigh || (dNew == dHigh && strike < strikePrice[strikeCount - 1])) return -1;
      strikeCount -= 1;
      if (lo > strikeCount) lo = strikeCount;
    }
  }
  for (let k = strikeCount; k > lo; k -= 1) copyRow(k - 1, k);
  strikePrice[lo] = strike;
  callAt[lo] = 0.0;
  putAt[lo] = 0.0;
  for (let b = 0; b < DAYS; b += 1) dayAt[lo * DAYS + b] = 0.0;
  strikeCount += 1;
  return lo;
}
function bucketOf(days: f64): i32 { // 0, 1-7, 8-30, 31-60, 61-120, 121+ days to go
  return days <= 0.0 ? 0 : days <= 7.0 ? 1 : days <= 30.0 ? 2 : days <= 60.0 ? 3 : days <= 120.0 ? 4 : 5;
}

// ── One contract's exposure on the lens, in the setting's unit, signed as it counts ──
function exposureOf(cells: StaticArray<f64>, i: i32, nowMs: f64): f64 {
  const side = cells[i + 2] > 0.0 ? 1.0 : -1.0; // dealers long the calls, short the puts
  const oi = cells[i + 3];
  if (lens == IV_X_OI) return side * cells[i + 6] * oi; // IV points times open interest
  if (lens > IV_X_OI) return side * oi; // open interest
  const under = cells[i + 7] > 0.0 ? cells[i + 7] : close;
  const multiplier = cells[i + 8] > 0.0 ? cells[i + 8] : 1.0;
  let perUnit: f64 = NaN; // the delta change (or delta) one unit of the underlying carries per unit of open interest
  if (lens == GAMMA) perUnit = side * cells[i + 4] * under * 0.01; // the delta change for a 1% move of spot
  else if (lens == DELTA) perUnit = cells[i + 5]; // each option's own sign, as delta views read it
  else {
    const left = cells[i + 1] - nowMs;
    const years = left < MIN_T_MS ? 0.0 : left / OPTIONS_YEAR_MS;
    const sigma = cells[i + 6] / 100.0; // mark IV rides the chain in percentage points
    if (zone == 1) greeks.spot(side, under, cells[i], years, sigma, rate); // a US stock: Black-Scholes on spot
    else greeks.forward(side, under, cells[i], years, sigma, rate); // futures and crypto: the underlying is the forward
    if (lens == VEGA) return side * (cells[i + 9] > 0.0 ? cells[i + 9] : greeks.vega) * oi * multiplier; // dollars per IV point
    perUnit = side * (lens == VANNA ? greeks.vanna : greeks.charm);
  }
  const dollars = perUnit * oi * multiplier * under;
  if (!inUnderlying) return dollars;
  return zone == 2 ? dollars / (under * multiplier) : dollars / under; // CME: futures contracts; else shares or coins
}

// ── The chain, measured on the live bar ──
function measure(n: i32): void {
  const cells = in_chain_view(); // the live chain in place: the first n values of the build's own buffer
  const nowMs = chainPricedAtMs(cells, n, t * 1000.0); // when the chart priced the chain's greeks
  const todayKey = optionsTradeDay(nowMs, zone);
  strikeCount = 0;
  expiryCount = 0;
  for (let i = 0; i + TUPLE - 1 < n; i += TUPLE) if (cells[i] > 0.0 && cells[i + 1] > nowMs) noteExpiry(cells[i + 1]);
  counted = 0;
  for (let e = 0; e < expiryCount; e += 1) {
    const days = optionsTradeDay(expiryAt[e], zone) - todayKey; // a calendar question: 0 on the expiry day, never the hours left
    expiryDays[e] = days;
    expiryNet[e] = 0.0;
    const kept = scope == SCOPE_TODAY ? days <= 0.0 : scope == SCOPE_WEEK ? days <= 7.0 : scope == SCOPE_MONTH ? days <= 30.0 : scope == SCOPE_NEAREST ? e < nearestCount : true;
    expiryKept[e] = kept;
    if (kept) counted += 1;
  }
  readings = 0;
  // Two passes, so every total reads the same strikes whatever order the chain arrives in: the first fills the strike
  // table (the MAX_STRIKES nearest spot when the chain lists more) and notes each contract's exposure; the second adds
  // the exposures of the contracts whose strike was kept, to the strikes, the day buckets and the expiries alike.
  const count = n / TUPLE < contractValue.length ? n / TUPLE : contractValue.length;
  for (let c = 0; c < count; c += 1) {
    const i = c * TUPLE;
    contractValue[c] = NaN;
    const strike = cells[i];
    if (!(strike > 0.0) || !(cells[i + 3] > 0.0) || !(cells[i + 1] > nowMs)) continue;
    const e = findExpiry(cells[i + 1]);
    if (e < 0 || !expiryKept[e]) continue;
    const v = exposureOf(cells, i, nowMs);
    if (!isFinite(v)) continue; // no reading for this lens (no IV to price, a NaN greek)
    contractValue[c] = v;
    strikeSlot(strike);
  }
  for (let c = 0; c < count; c += 1) {
    const v = contractValue[c];
    if (isNaN(v)) continue;
    const i = c * TUPLE;
    const slot = findStrike(cells[i]);
    if (slot < 0) continue; // left out of the full table, so out of every total
    const e = findExpiry(cells[i + 1]);
    if (cells[i + 2] > 0.0) callAt[slot] += v;
    else putAt[slot] += v;
    dayAt[slot * DAYS + bucketOf(expiryDays[e])] += v;
    expiryNet[e] += v;
    if (v != 0.0) readings += 1;
  }
  // The chain-wide readings, ascending: the totals, the largest strike, the sums each side of spot.
  calls = 0.0;
  puts = 0.0;
  above = 0.0;
  below = 0.0;
  topStrike = NaN;
  topValue = NaN;
  let best = 0.0;
  for (let k = 0; k < strikeCount; k += 1) {
    const row = callAt[k] + putAt[k];
    calls += callAt[k];
    puts += putAt[k];
    if (strikePrice[k] > close) above += row;
    else below += row;
    if (Math.abs(row) > best) {
      best = Math.abs(row);
      topStrike = strikePrice[k];
      topValue = row;
    }
  }
  net = calls + puts;
  today = NaN;
  for (let e = 0; e < expiryCount; e += 1) if (expiryKept[e] && expiryDays[e] <= 0.0) today = (isNaN(today) ? 0.0 : today) + expiryNet[e];
  // The zero crossing: the running net walked up the strikes, the crossing nearest spot (the far wings' small sums can
  // cross it back and forth); a sum landing exactly on zero crosses at that strike and starts a new segment.
  zeroCross = NaN;
  let cum = 0.0;
  let prevCum = 0.0;
  let prevStrike: f64 = NaN;
  for (let k = 0; k < strikeCount; k += 1) {
    cum += callAt[k] + putAt[k];
    let cross: f64 = NaN;
    if (!isNaN(prevStrike) && prevCum != 0.0) {
      if (cum == 0.0) cross = strikePrice[k];
      else if ((cum > 0.0) != (prevCum > 0.0)) cross = prevStrike + (strikePrice[k] - prevStrike) * (Math.abs(prevCum) / (Math.abs(prevCum) + Math.abs(cum)));
    }
    if (!isNaN(cross) && (isNaN(zeroCross) || Math.abs(cross - close) < Math.abs(zeroCross - close))) zeroCross = cross;
    if (cum == 0.0) prevStrike = NaN;
    else {
      prevCum = cum;
      prevStrike = strikePrice[k];
    }
  }
}

// ── The frames: the docked profile and the bars by expiry, built in the kit's frame buffer without allocating ──
function writeProfile(): void {
  const lo = close * (1.0 - windowPct);
  const hi = close * (1.0 + windowPct);
  let first = 0; // the window's rows: [first, last)
  while (first < strikeCount && strikePrice[first] < lo) first += 1;
  let last = first;
  while (last < strikeCount && strikePrice[last] <= hi) last += 1;
  while (last - first > MAX_ROWS) { // a wide window on a fine ladder: drop the row farther from spot
    if (close - strikePrice[first] >= strikePrice[last - 1] - close) first += 1;
    else last -= 1;
  }
  if (last <= first) return; // no strike inside the window: the profile stays absent
  fb_clear();
  fb_text("{\"prices\":[");
  for (let k = first; k < last; k += 1) {
    if (k > first) fb_text(",");
    fb_num(strikePrice[k]);
  }
  fb_text("],\"values\":[");
  for (let k = first; k < last; k += 1) {
    if (k > first) fb_text(",");
    fb_num(callAt[k] + putAt[k]);
  }
  if (scope == SCOPE_BUCKETS) {
    fb_text("],\"series\":[");
    for (let b = 0; b < DAYS; b += 1) {
      fb_text(b > 0 ? ",[" : "[");
      for (let k = first; k < last; k += 1) {
        if (k > first) fb_text(",");
        fb_num(dayAt[k * DAYS + b]);
      }
      fb_text("]");
    }
    fb_text("]}");
    writeFrameBuffer(day_rows);
  } else if (splitSides) {
    fb_text("],\"series\":[[");
    for (let k = first; k < last; k += 1) {
      if (k > first) fb_text(",");
      fb_num(callAt[k]);
    }
    fb_text("],[");
    for (let k = first; k < last; k += 1) {
      if (k > first) fb_text(",");
      fb_num(putAt[k]);
    }
    fb_text("]]}");
    writeFrameBuffer(split_rows);
  } else {
    fb_text("],\"colors\":[");
    for (let k = first; k < last; k += 1) {
      if (k > first) fb_text(",");
      const row = callAt[k] + putAt[k];
      fb_text(row > 0.0 ? "\"#38bdf8\"" : row < 0.0 ? "\"#a78bfa\"" : "\"#94a3b8\""); // above zero sky, below violet
    }
    fb_text("]}");
    writeFrameBuffer(net_rows);
  }
}
function writeExpiries(): void {
  fb_clear();
  fb_text("{\"title\":");
  fb_text("\"");
  fb_text(LENS_NAMES[lens]);
  fb_text(" by expiry\",\"rows\":[");
  let wrote = 0;
  for (let e = 0; e < expiryCount; e += 1) {
    if (!expiryKept[e]) continue;
    if (wrote > 0) fb_text(",");
    dateClock.update(optionsTradeDay(expiryAt[e], zone) * 86400.0); // the expiry's trade date, read as a date
    fb_text("[\"");
    if (dateClock.dayOfMonth() < 10) fb_text("0");
    fb_int(dateClock.dayOfMonth());
    fb_text(MONTHS[dateClock.month() - 1]);
    fb_text(" ");
    fb_int(i64(expiryDays[e]));
    fb_text("d\",");
    fb_num(expiryNet[e]);
    fb_text("]");
    wrote += 1;
  }
  fb_text("]}");
  writeFrameBuffer(expiry_rows);
}

// ── The card's words ──
function writeRead(): void {
  sb_clear();
  if (readings == 0 || isNaN(net)) {
    sb_text("No ");
    sb_text(LENS_WORDS[lens]);
    sb_text(" in this chain");
  } else if (lens == GAMMA) sb_text(net >= 0.0 ? "Long gamma: dealer hedging dampens moves" : "Short gamma: dealer hedging amplifies moves");
  else if (lens == DELTA) sb_text(net >= 0.0 ? "Net long delta: the calls outweigh the puts" : "Net short delta: the puts outweigh the calls");
  else if (lens == VANNA) sb_text(net >= 0.0 ? "Long vanna: an IV rise makes dealers sell" : "Short vanna: an IV rise makes dealers buy");
  else if (lens == CHARM) sb_text(net >= 0.0 ? "Charm: each day passing makes dealers sell" : "Charm: each day passing makes dealers buy");
  else if (lens == VEGA) sb_text(net >= 0.0 ? "Dealers long vega: an IV rise pays them" : "Dealers short vega: an IV rise costs them");
  else if (lens == IV_X_OI) sb_text(net >= 0.0 ? "The calls hold the richer open interest" : "The puts hold the richer open interest");
  else sb_text(net >= 0.0 ? "More call than put open interest" : "More put than call open interest");
  str_read_sb();
}
function unitWord(): String {
  return zone == 1 ? "shares" : zone == 2 ? "contracts" : "coins";
}
function writeTitle(): void {
  sb_clear();
  sb_text(LENS_NAMES[lens]);
  sb_text(", ");
  if (lens == IV_X_OI) sb_text("IV points x OI");
  else if (lens > IV_X_OI) sb_text(zone == 0 ? "coins" : "contracts"); // open interest in the venue's own unit
  else if (lens == VEGA) sb_text("$ per IV point");
  else {
    sb_text(inUnderlying ? unitWord() : "$");
    sb_text(lens == GAMMA ? " per 1% move" : lens == DELTA ? " held" : lens == VANNA ? " per IV point" : " per day");
  }
  sb_text(", ");
  if (scope == SCOPE_TODAY) sb_text("same day");
  else if (scope == SCOPE_WEEK) sb_text("within 7 days");
  else if (scope == SCOPE_MONTH) sb_text("within 30 days");
  else if (scope == SCOPE_NEAREST && nearestCount < expiryCount) {
    sb_text("nearest ");
    sb_int(nearestCount);
    sb_text(" of ");
    sb_int(expiryCount);
    sb_text(" expiries");
    str_title_sb();
    return;
  } else {
    sb_text("all ");
    sb_int(expiryCount);
    sb_text(" expiries");
    str_title_sb();
    return;
  }
  sb_text(" (");
  sb_int(counted);
  sb_text(counted == 1 ? " expiry)" : " expiries)");
  str_title_sb();
}

// onBar() runs once per bar: history rows carry an empty block and cost one read; the live bar measures the chain, writes
// the readings (NaN on history rows), the profile, the bars by expiry and the card's words.
function onBar(): void {
  close = bar.close();
  t = bar.time();
  let measured = false; // the live bar carried a chain with a live expiry the setting counts
  let chainCells = -1;
  if (bar.isLast()) {
    chainCells = in_chain_cells();
    if (chainCells >= TUPLE && !isNaN(close) && !isNaN(t)) {
      measure(chainCells);
      measured = counted > 0;
    }
  }
  const reading = measured && readings > 0;
  out_net_exposure(reading ? net : NaN);
  out_calls_exposure(reading ? calls : NaN);
  out_puts_exposure(reading ? puts : NaN);
  out_today_exposure(reading ? today : NaN);
  out_top_strike(reading ? topStrike : NaN);
  out_top_exposure(reading ? topValue : NaN);
  out_zero_cross(reading ? zeroCross : NaN);
  out_above_spot(reading ? above : NaN);
  out_below_spot(reading ? below : NaN);
  out_tone(reading ? (net > 0.0 ? 2.0 : net < 0.0 ? 0.0 : 1.0) : bar.isLast() ? 1.0 : NaN);
  out_expiries_counted(measured ? f64(counted) : NaN);
  if (measured) {
    if (reading) writeProfile();
    writeExpiries();
    writeRead();
    writeTitle();
  } else if (bar.isLast()) {
    // The live bar carried nothing to measure: the headline says why, so the card is never a row of blanks.
    sb_clear();
    sb_text(chainCells < TUPLE ? "Waiting for the options chain" : expiryCount == 0 ? "No live expiry in the chain" : "No expiry in this setting's range");
    str_read_sb();
    sb_clear();
    sb_text(LENS_NAMES[lens]);
    sb_text(", no chain read");
    str_title_sb();
  }
}
```

## How it works

**One lens at a time.** Every contract the Expiries setting counts adds one number to its strike and to its expiry. Per contract, before the sign, with `oi` its open interest, `multiplier` its multiplier (100 on a US listed option, the point value on CME, 1 on Deribit) and `spot` its underlying:

| Lens | Per contract | Unit in dollars |
| --- | --- | --- |
| `gamma` | `gamma * oi * multiplier * spot * spot * 0.01` | the dealers' delta change for a 1% move of spot, in dollars |
| `delta` | `delta * oi * multiplier * spot` | the delta held, in dollars |
| `vanna` | `vanna * oi * multiplier * spot` | the delta change for one IV point |
| `charm` | `charm * oi * multiplier * spot` | the delta change as one calendar day passes |
| `vega` | `vega * oi * multiplier` | the value change for one IV point |
| `iv_x_oi` | `mark_iv * oi` | IV points times open interest (where the expensive open positions sit) |
| `oi` | `oi` | open interest, contracts (coins on crypto) |

**The sign: dealers long the calls, short the puts.** That is the open-interest model the gamma map uses: customers sell the calls dealers hold and buy the puts dealers write, so the calls add and the puts subtract on gamma, vanna, charm, vega, IV times open interest and open interest. Delta keeps each option's own sign (a call's delta is positive, a put's negative), the way delta views read the chain. Net gamma above zero is long gamma: hedging leans against moves. Net vanna above zero means an IV rise lifts the dealers' delta, so they sell to stay hedged; below zero they buy. Charm above zero means each day passing lifts their delta, so they sell; below zero they buy.

**Priced greeks.** Gamma and delta are the chain's own. Vanna, charm and, where a venue serves none, vega are priced per contract by the kit's `Greeks`: Black-Scholes on spot on a US stock, at the Rate setting (4% by default); Black-76 on the underlying on futures and crypto, whose underlying is already the forward (crypto at a rate of 0). The time to expiry runs from `chainPricedAtMs()`, the moment the chart priced the chain, in 365-day years; a contract within 15 minutes of its expiry prices no time left. Charm is per calendar day: tools that multiply it by 365 print a yearly figure 365 times larger.

**Units.** `dollars` is the table above. `underlying` divides the four delta lenses by spot: shares on a US stock, coins on crypto, and on CME by spot times the point value, which is futures contracts. Vega stays in dollars per IV point, and the last two lenses keep their own units. The card's title names the unit in use (`Gamma, $ per 1% move, all 11 expiries`).

**Expiries by the calendar.** `0dte` counts the expiry whose trade date is today's on the market's own clock (`optionsTradeDay()`: New York on a US stock, Chicago on CME, UTC on crypto), never the one under 24 hours away; `week` and `month` count the expiries 7 and 30 days out or less on the same calendar; `nearest` the N nearest live expiries; `buckets` every expiry, stacked by days to go: 0, 1 to 7, 8 to 30, 31 to 60, 61 to 120 and 121 or more. A tall bar built from the near buckets is exposure that decays within days; one built from the far buckets persists.

**By strike.** The strikes within `window_pct` (8) percent of the chart's close dock on the price axis, at most 256, one row per strike, centred on zero: net above zero to one side in sky, below zero to the other in violet. `split: calls_puts` draws the calls one way and the puts the other; `buckets` stacks the six day buckets. Each shape has its own `plot.levels`, and only the one the settings pick gets a frame, so the other two stay absent.

**By expiry.** One bar per counted expiry under the chart, labelled by its date and its days to go (`16OCT 1d`), sky above zero and violet below. The panel's title names the lens.

**The card reads the outputs.** The headline is the lens's plain line, coloured by the sign of the net; under it the net, the same-day expiry, the calls and the puts in `si` (`1.24B`), then five rows: the strike holding the largest net either way (the gamma magnet on the gamma lens), its exposure, where the running net summed up the strikes crosses zero nearest spot (the gamma flip on the gamma lens), and the sums over the strikes above and at or below spot.

## Where it runs

Charts of a coin Deribit lists options for (BTC, ETH, SOL and the rest of its list): that is where the copy you run from the editor reads a chain. On a US stock chart `venue: "auto"` reads the chart's own OPRA chain, and on a CME futures chart the chain of its root; the chart serves both to OpenMarket's official wrun indicators and discloses the source in the legend. A copy you run from the editor or publish yourself does not read them yet.

## When data is missing

The headline says why instead of the card going blank: "Waiting for the options chain" while the live bar carries none, "No live expiry in the chain" when every listed expiry has passed, "No expiry in this setting's range" when the Expiries setting counts none (`0dte` on a day with no expiry), and "No gamma in this chain" (or the lens's own word) when no counted contract carries a reading, as on an index chain whose greeks are not served yet or a chain without implied volatility. The numbers then read `-` and the profile does not draw; the bars by expiry still stand while the expiries are counted.

## Customize it

- **Another greek.** Set Lens to `vanna` before an event that moves implied volatility, or to `charm` into the close and over a weekend.
- **Shares instead of dollars.** Set Units to `underlying` to read the hedge in the market's own units.
- **What decays and what stays.** Set Expiries to `buckets` and read which days build each strike's bar.
- **A wider ladder.** Raise `window_pct` on a volatile underlying; the totals always count every strike.

## Run it

1. In the editor's Explorer, press the **Templates** icon ("Browse starter templates") and pick **Greek Exposure** under **HUDs**.
2. Press **Run** on a BTC chart: the profile docks on the price axis once the chain arrives, the bars by expiry stand under the chart and the terminal card appears at the bottom centre.
3. At the editor's Console prompt, type `net_exposure` and `zero_cross` to read the net and the zero crossing on the live bar.

## Concepts used

- [Data sources](../core-concepts/data-sources.md#options-chain) for the `options_chain` celled class, its tuple and the `venue` word
- [Options kit](../functions/options-kit.md#pricing-the-chain) for `Greeks`, `chainPricedAtMs` and `optionsTradeDay`
- [Cards, frames and panels](../presentation/cards-frames-panels.md#frames-panels-and-compact-widgets) for frames, `plot.levels` with series and `panel.bars`
- [HUD cards](../presentation/hud-and-hover-cards.md#hud-cards), [Blocks and tiles](../presentation/hud-and-hover-cards.md#blocks-and-tiles) and [Looks](../presentation/hud-and-hover-cards.md#looks) for the card, its tiles and the slot in the title
