---
title: "Spot exposures"
description: "The dealers' exposure in the chart's own options measured bar by bar: net gamma, delta or vega as a line, the walls on price and a heat map by strike, from the…"
order: 125
section: "cookbook"
---

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

# Spot exposures

The dealers' exposure in the chart's own options measured bar by bar: net gamma, delta or vega as a line, the walls on price and a heat map by strike, from the strike rows the data plane folds every minute (the [`option_strikes`](../core-concepts/data-sources.md#option-strikes) source: each bar's newest minute of strike rows for the expiries within seven days, about seven days back). A line under the chart follows the lens's net exposure (gamma per 1% move of the underlying by default, delta held, or vega per IV point, in dollars), coloured by its sign; three step lines on the price chart follow the zero-gamma strike and the call and put walls; and a heat map behind the candles paints the lens strike by strike on every bar, so where exposure built or unwound, and when, reads at a glance. Map values switches the map from each strike's level to its change from the previous bar with strike rows. A card at the bottom of the pane, in the terminal look, reads the newest strike rows: the gamma regime in one plain line, then the three nets, the change in net gamma since the session's first strike rows, the levels and the put/call open interest.

The parts are a celled `option_strikes.cells` input read on every bar ([Data sources](../core-concepts/data-sources.md#option-strikes)) through the options kit's `OptionStrikes` ([Options kit](../functions/options-kit.md#option-strikes-bar-by-bar)), a split line in the lower pane and three step lines on price ([Plotting](../presentation/plotting.md)), an `out.grid` heat map with a row step and a bottom edge of its own on every bar ([Price canvases](../presentation/price-canvases.md#a-grid-the-module-computes)), the bar's `time.trade_date` for the session ([The market's own calendar](../core-concepts/time-and-sessions.md#the-markets-own-calendar)) and a `render.hud` card with `look: "terminal"` ([HUD cards](../presentation/hud-and-hover-cards.md#hud-cards)). This is also the `spot-exposures` template: the **Spot Exposures** card under **HUDs** in the editor's starter list, and it compiles as written.

## The wrun indicator

```typescript sample=spot-exposures
// Spot Exposures: the dealers' exposure in the chart's own options, measured bar by bar from the strike rows the data
// plane folds every minute (the option_strikes source: each bar carries the newest minute of strike rows inside it, for
// the expiries within seven days, about seven days back). A line under the chart follows the lens's net exposure (gamma
// per 1% move by default, delta held, or vega per IV point, in dollars), three step lines on the price chart follow the
// zero-gamma strike and the call and put walls, and a heat map behind the candles paints the lens strike by strike on
// every bar with strike rows (Map values shows each strike's level, or its change from the previous bar with strike
// rows), so where exposure built or unwound, and when, reads at a glance. A card in the terminal look reads the newest
// strike rows: the gamma regime in one plain line, then the three nets, the change in net gamma since the session's
// first strike rows, the levels and the put/call open interest. The strike rows' convention: dealers long the calls and
// short the puts (calls add, puts subtract), delta in each option's own sign. The lines and the card carry the newest
// strike rows at or before each bar (Rows age counts the bars since); only a bar with strike rows paints the map.

section("Exposure");
param.choice("lens", ["gamma", "delta", "vega"], "gamma", { label: "Lens", description: "What the line and the map follow, in dollars: gamma per 1% move of the underlying, delta held, or vega per IV point" });
param.choice("map_mode", ["level", "change"], "level", { label: "Map values", description: "level: each strike's exposure on the bar; change: its change from the previous bar that carried strike rows" });
param.int("nearest", 0, { min: 0, max: 7, label: "Nearest expiries", description: "The N soonest expiries each bar's rows list; 0 counts every expiry the rows carry (those within seven days)" });
param.number("window_pct", 3, { min: 0.5, max: 20, step: 0.5, label: "Map window, percent", description: "Strikes on the map: percent around the underlying, each side, at most 48 strike steps each way" });
input("close", ohlcv.close); // the chart's own close: the map's centre where the rows report no underlying
input("strikes", option_strikes.cells, { max_cells: 4000, venue: "auto", description: "The chart's own strike rows: every (expiry, strike) row of the newest minute inside each bar, expiries within seven days" }); // [strike, expiry_ms, net_gex, call_gex, put_gex, net_dex, net_vex, call_oi, put_oi, call_iv, put_iv, underlying] per row
input("trade_date", time.trade_date); // the bar's trade date: the change in net gamma counts from the session's first strike rows
market.zone(); // the market's clock: the trade date where the chart writes none
output("exposure", line, lower, { width: 2, split: "level", up_color: "theme.up", down_color: "theme.down", split_fill: true, format: "usd", label: "Net exposure", description: "The lens's net exposure over the counted expiries, dollars: gamma per 1% move, delta held or vega per IV point" });
output("zero_gamma", line, overlay, { color: "theme.text", width: 1, step: true, line_style: "dashed", format: "price", label: "Zero gamma", description: "The first strike, walking up from the lowest, where net gamma summed up the strikes changes sign" });
output("call_wall", line, overlay, { color: "theme.up", width: 1, step: true, format: "price", label: "Call wall", description: "The strike holding the most call dollar gamma" });
output("put_wall", line, overlay, { color: "theme.down", width: 1, step: true, format: "price", label: "Put wall", description: "The strike holding the most put dollar gamma" });
// The readings, data-only: the card's tiles read them and the Console can too. Like the lines, each carries the newest
// strike rows at or before its bar.
output("net_gex", none, overlay, { description: "Net gamma exposure of the counted expiries, USD per 1% move of the underlying" });
output("net_dex", none, overlay, { description: "Net delta exposure, USD held, each option in its own sign" });
output("net_vex", none, overlay, { description: "Net vega exposure, USD per IV point" });
output("call_gex", none, overlay, { description: "The calls' dollar gamma, USD per 1% move" });
output("put_gex", none, overlay, { description: "The puts' dollar gamma, USD per 1% move, stored positive" });
output("gex_change", none, overlay, { description: "Net gamma minus net gamma at the session's first strike rows, USD per 1% move" });
output("max_pain", none, overlay, { description: "The listed strike where the open interest's payout at expiry is least" });
output("put_call_oi", none, overlay, { description: "Put open interest over call open interest" });
output("rows_age", none, overlay, { description: "Bars since the newest bar that carried strike rows: 0 on such a bar, NaN before the first" });
output("tone", none, overlay, { description: "The headline's colour: 0 short gamma, 1 no reading, 2 long gamma" });
output("map_low", none, overlay, { description: "The bottom edge of the map's lowest row on this bar" }); // the heat map reads it
output("map_step", none, overlay, { description: "The map's row height in price on this bar: the finest strike step inside the window" });
out.grid("map", { rows: 96 }); // 96 data-only outputs map_0..map_95, one per strike step, written with out_map(row, value)
plot.heatmap({ name: "exposure_map", grid: "map", price_low: "map_low", price_step: "map_step", palette: ["theme.down", "theme.bg", "theme.up"], center: 0, scale: "sqrt", auto_quantile: 0.98, opacity: 0.7, behind_candles: true, label: "Exposure by strike", tooltip: "{{value:usd}} at {{price}}" }); // the chart's down colour where puts carry the strike, its up colour where calls do
string("read", { max_bytes: 64 }); // the headline: the gamma regime in one plain line, or why there is none
string("title", { max_bytes: 64 }); // the card's title: the lens and the expiries counted

// The card, in the terminal look: the gamma regime leads as the headline, coloured by the sign of net gamma (violet
// short, slate for no reading, sky long), then the three nets and the levels of the newest strike rows.
render.hud("spot", {
  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 gamma", "net_gex", { format: "usd" }),
    tile.value("Since the session's first rows", "gex_change", { format: "usd" }),
    tile.value("Net delta", "net_dex", { format: "usd" }),
    tile.value("Net vega", "net_vex", { format: "usd" }),
    tile.rows([
      ["1) Zero gamma", "zero_gamma", "price"],
      ["2) Call wall", "call_wall", "price"],
      ["3) Put wall", "put_wall", "price"],
      ["4) Max pain", "max_pain", "price"],
      ["5) Put/call OI", "put_call_oi", "0.00"],
      ["6) Rows age, bars", "rows_age", "int"],
    ]),
  ],
});

const ROWS = 96; // the map's rows, the grid's declared count
const HALF = 48; // rows each side of the strike nearest the underlying
const GAMMA = 0;
const DELTA = 1;
const LENS_WORDS: String[] = ["gamma", "delta", "vega"];
const rowValue = new StaticArray<f64>(ROWS); // this bar's map, folded from the strike table
const prevStrike = new StaticArray<f64>(in_strikes_max_cells); // the previous strike rows' table: its strikes, ascending
const prevValue = new StaticArray<f64>(in_strikes_max_cells); // and the lens at each, for the change map
let table = new OptionStrikes(1); // one bar's strike rows measured by strike; sized in onStart()
let lens = GAMMA; // settings, read in onStart()
let changeMap = false;
let nearest = 0;
let windowPct = 0.03;
let zone = 0;
let prevCount = -1; // strikes in the previous table; -1 before the first bar with strike rows
let sessionKey: f64 = NaN; // the trade date the bars are in, days since 1970-01-01
let sessionFirstGex: f64 = NaN; // net gamma at the session's first strike rows
let age = -1; // bars since the newest bar with strike rows; -1 before the first
// The newest strike rows' readings, carried to the bars after them until the next rows arrive.
let exposure: f64 = NaN;
let zeroGamma: f64 = NaN;
let callWall: f64 = NaN;
let putWall: f64 = NaN;
let netGex: f64 = NaN;
let netDex: f64 = NaN;
let netVex: f64 = NaN;
let callGex: f64 = NaN;
let putGex: f64 = NaN;
let gexChange: f64 = NaN;
let maxPain: f64 = NaN;
let putCallOi: f64 = NaN;

function onStart(): void {
  table = new OptionStrikes(in_strikes_max_cells); // a block holds at most max_cells rows, so a load is never refused
  lens = i32(p_lens());
  changeMap = p_map_mode() == 1.0;
  nearest = i32(p_nearest());
  windowPct = p_window_pct() / 100.0;
  zone = i32(p_market_zone());
}

// ── The session: net gamma's change counts from the first strike rows of the bar's trade date ──
function trackSession(t: f64): void {
  const stamp = in_trade_date(); // epoch seconds of the trade date, as the chart writes it
  const key = isFinite(stamp) ? Math.floor(stamp / 86400.0) : optionsTradeDay(t * 1000.0, zone);
  if (isNaN(key)) return;
  if (isNaN(sessionKey) || key > sessionKey) {
    sessionKey = key;
    sessionFirstGex = NaN; // a new session: its first strike rows set the base
  }
}

// ── The lens, strike by strike, and the previous rows' value at a strike ──
function lensAt(k: i32): f64 { // the lens at the table's k-th strike (ascending)
  return lens == GAMMA ? table.netGex(k) : lens == DELTA ? table.netDex(k) : table.netVex(k);
}
function lensTotal(): f64 { // the lens over every strike of the table
  return lens == GAMMA ? table.totalNetGex() : lens == DELTA ? table.totalNetDex() : table.totalNetVex();
}
function previousAt(strike: f64): f64 { // the previous rows' lens at this strike; 0 where they did not list it
  let lo = 0;
  let hi = prevCount;
  while (lo < hi) {
    const mid = (lo + hi) >> 1;
    if (prevStrike[mid] < strike) lo = mid + 1;
    else hi = mid;
  }
  return lo < prevCount && prevStrike[lo] == strike ? prevValue[lo] : 0.0;
}
function keepTable(): void { // this bar's table becomes the previous rows
  const count = table.strikeCount();
  for (let k = 0; k < count; k += 1) {
    prevStrike[k] = table.strike(k);
    prevValue[k] = lensAt(k);
  }
  prevCount = count;
}

// ── The map: one row per strike step, centred on the strike nearest the underlying ──
function clearMap(): void { // no map on this bar: every cell empty
  for (let r = 0; r < ROWS; r += 1) out_map(r, NaN);
  out_map_low(NaN);
  out_map_step(NaN);
}
function writeMap(spot: f64): void {
  const count = table.strikeCount();
  const lo = spot * (1.0 - windowPct);
  const hi = spot * (1.0 + windowPct);
  let first = 0; // the window's strikes: [first, last)
  while (first < count && table.strike(first) < lo) first += 1;
  let last = first;
  while (last < count && table.strike(last) <= hi) last += 1;
  if (last <= first || (changeMap && prevCount < 0)) { // no strike inside the window, or no earlier rows to change from
    clearMap();
    return;
  }
  let step = Infinity; // the finest strike step inside the window: one row per step
  for (let k = first + 1; k < last; k += 1) {
    const gap = table.strike(k) - table.strike(k - 1);
    if (gap > 0.0 && gap < step) step = gap;
  }
  if (step == Infinity) { // one strike in the window: its step to a neighbour, else a tenth of a percent of spot
    if (first > 0) step = table.strike(first) - table.strike(first - 1);
    else if (first + 1 < count) step = table.strike(first + 1) - table.strike(first);
    else step = spot * 0.001;
  }
  let centre = first; // the strike nearest the underlying, the lower one on a tie
  for (let k = first + 1; k < last; k += 1) {
    if (Math.abs(table.strike(k) - spot) < Math.abs(table.strike(centre) - spot)) centre = k;
  }
  const anchor = table.strike(centre);
  for (let r = 0; r < ROWS; r += 1) rowValue[r] = NaN;
  for (let k = first; k < last; k += 1) {
    const strike = table.strike(k);
    const row = HALF + i32(Math.round((strike - anchor) / step));
    if (row < 0 || row >= ROWS) continue; // more than 48 steps from the centre
    const v = changeMap ? lensAt(k) - previousAt(strike) : lensAt(k);
    rowValue[row] = isNaN(rowValue[row]) ? v : rowValue[row] + v; // a strike off the step's grid shares the nearest row
  }
  for (let r = 0; r < ROWS; r += 1) out_map(r, rowValue[r]);
  out_map_low(anchor - (f64(HALF) + 0.5) * step); // each row spans its strike plus and minus half a step
  out_map_step(step);
}

// ── The card's words ──
function writeWords(): void {
  sb_clear();
  if (age < 0) sb_text("No strike rows on this chart yet");
  else if (isNaN(netGex) || netGex == 0.0) sb_text("Flat gamma: no net dealer gamma");
  else sb_text(netGex > 0.0 ? "Long gamma: dealer hedging dampens moves" : "Short gamma: dealer hedging amplifies moves");
  if (age > 0) { // the newest rows are on an earlier bar
    sb_text(" (");
    sb_int(age);
    sb_text(age == 1 ? " bar ago)" : " bars ago)");
  }
  str_read_sb();
  sb_clear();
  sb_text("Spot exposures, ");
  sb_text(LENS_WORDS[lens]);
  if (nearest == 0) sb_text(", every expiry within 7 days");
  else {
    sb_text(", nearest ");
    sb_int(nearest);
    sb_text(nearest == 1 ? " expiry" : " expiries");
  }
  str_title_sb();
}

// onBar() runs once per bar: a bar with strike rows measures them (the lines, the levels, the readings and the map); a
// bar without keeps the newest readings and paints no map; the live bar writes the card's words.
function onBar(): void {
  const close = bar.close();
  const t = bar.time();
  trackSession(t);
  const n = in_strikes_cells();
  const kept = n >= OPTION_STRIKES_TUPLE ? table.load(in_strikes_view(), n, nearest) : 0;
  if (kept > 0) {
    netGex = table.totalNetGex();
    netDex = table.totalNetDex();
    netVex = table.totalNetVex();
    callGex = table.totalCallGex();
    putGex = table.totalPutGex();
    maxPain = table.maxPain();
    putCallOi = table.putCallRatio();
    zeroGamma = table.zeroGamma();
    callWall = table.callWall();
    putWall = table.putWall();
    exposure = lensTotal();
    if (isNaN(sessionFirstGex)) sessionFirstGex = netGex;
    gexChange = netGex - sessionFirstGex;
    age = 0;
    const under = table.underlying();
    const spot = under > 0.0 ? under : close;
    if (spot > 0.0) writeMap(spot);
    else clearMap();
    keepTable();
  } else {
    if (age >= 0) age += 1;
    clearMap();
  }
  out_exposure(exposure);
  out_zero_gamma(zeroGamma);
  out_call_wall(callWall);
  out_put_wall(putWall);
  out_net_gex(netGex);
  out_net_dex(netDex);
  out_net_vex(netVex);
  out_call_gex(callGex);
  out_put_gex(putGex);
  out_gex_change(gexChange);
  out_max_pain(maxPain);
  out_put_call_oi(putCallOi);
  out_rows_age(age >= 0 ? f64(age) : NaN);
  out_tone(age < 0 || isNaN(netGex) ? 1.0 : netGex > 0.0 ? 2.0 : netGex < 0.0 ? 0.0 : 1.0);
  if (bar.isLast()) writeWords();
}
```

## How it works

**One snapshot a bar.** `input("strikes", option_strikes.cells, { max_cells: 4000, venue: "auto" })` hands every bar the newest minute of strike rows inside it, one `[strike, expiry_ms, net_gex, call_gex, put_gex, net_dex, net_vex, call_oi, put_oi, call_iv, put_iv, underlying]` tuple per expiry and strike. `OptionStrikes.load(cells, count, nearest)` folds them into one table by strike: a strike listed under two expiries is one row, its exposures summed; a side the strike does not list (NaN) counts as 0. **Nearest expiries** keeps the N soonest expiries each bar lists before the table fills (0 keeps every expiry the rows carry, those within seven days). The table is constructed with the input's `in_strikes_max_cells`, so a load is never refused for size.

**The convention.** The strike rows put the dealers long the calls and short the puts: `net_gex` is the calls' dollar gamma minus the puts', per 1% move of the underlying, `net_vex` the calls' vega dollars minus the puts', and `net_dex` the delta dollars of both sides with each option in its own sign (puts negative). The lens's line is the table's total: `totalNetGex()`, `totalNetDex()` or `totalNetVex()`.

**The levels.** On every bar with strike rows the kit reads the zero-gamma strike (`zeroGamma()`: walking up from the lowest strike, the first where the running sum of net gamma reaches zero or changes sign, a strike, never a point between two), the call wall and the put wall (`callWall()` and `putWall()`: the strikes holding the most call and put dollar gamma) and max pain (`maxPain()`: the listed strike where the open interest's payout at expiry is least). The three step lines draw the first three; max pain reads in the card.

**The map.** On a bar with strike rows the module writes 96 rows centred on the strike nearest the underlying (the rows' own `underlying`, else the chart's close), one row per strike step: the finest gap between neighbouring strikes inside **Map window** (3% each side by default), so a ladder of $1 strikes near the money and $5 strikes further out puts each strike on its own row and leaves the rows between the $5 strikes empty. A strike off the step's grid shares the nearest row. `map_low` is the bottom edge of the lowest row and `map_step` the row height, so each strike's row spans its price plus and minus half a step; `plot.heatmap({ grid: "map", palette: ["theme.down", "theme.bg", "theme.up"], center: 0 })` paints the chart's down colour where puts carry the strike and its up colour where calls do. With **Map values** on change, each strike reads its lens minus the previous strike rows' (0 where they did not list it), and the first bar with strike rows paints nothing.

**Carried readings.** The lines and the card carry the newest strike rows at or before each bar, so a minute the rows missed does not break them; `rows_age` counts the bars since (0 on a bar that carries its own) and the headline says "(3 bars ago)" when the newest rows are not the live bar's. Only a bar with strike rows paints the map.

**The session's change.** `gex_change` is net gamma minus net gamma at the first strike rows of the bar's trade date (`time.trade_date`, else the trade date on the market's clock), so it starts at 0 each session and reads how much dealer gamma the day added or removed.

## Where it runs

US stock and ETF charts with US listed options: the strike rows exist for OPRA chains, and the chart serves them to OpenMarket's official wrun indicators under the US options rule ([Limitations](../reference/limitations.md)); a draft or a community copy is refused by name. Any other chart reads empty blocks: the card says there are no strike rows yet, and nothing draws.

## When data is missing

A bar without strike rows (outside the session, a minute the rows missed) paints no map and keeps the newest readings; before the first strike rows every output reads NaN and the headline reads "No strike rows on this chart yet". A strike that lists one side reads that side alone; a side without an IV reads nothing in the IV weighting. The rows reach back about seven days, so on a chart that loads further back the older bars draw nothing.

## Customize it

- **The front expiry alone.** Set **Nearest expiries** to 1 for the exposure that settles first; the walls and the map follow it.
- **Where it changed.** Set **Map values** to change: green rows are strikes where dealer gamma grew since the previous bar, red where it fell.
- **A wider map.** Raise **Map window**; a fine ladder stops at 48 strike steps each side.
- **Delta or vega.** Set **Lens** to delta or vega: the line and the map follow it, the walls and the regime stay gamma's.

## Run it

1. In the editor's Explorer, press the **Templates** icon ("Browse starter templates") and pick **Spot Exposures** under **HUDs**.
2. Press **Run** on a US stock or ETF chart on a 1m or 5m interval: the line, the walls and the map fill bar by bar once the strike rows arrive.
3. At the editor's Console prompt, type `net_gex`, `zero_gamma` and `gex_change` to read them on the live bar.

## Concepts used

- [Data sources](../core-concepts/data-sources.md#option-strikes) for the `option_strikes` celled class and its tuple
- [Options kit](../functions/options-kit.md#option-strikes-bar-by-bar) for `OptionStrikes`
- [Price canvases](../presentation/price-canvases.md#a-grid-the-module-computes) for `out.grid` and `plot.heatmap`
- [Plotting](../presentation/plotting.md) for the split line and the step lines
- [HUD cards](../presentation/hud-and-hover-cards.md#hud-cards) and [Looks](../presentation/hud-and-hover-cards.md#looks) for the card
