---
title: "Options kit"
description: "The chart serves a coin's listed options as options_chain cells, one ten-value tuple per contract, on the live bar only (Data sources). The ./sdk/options…"
order: 58
section: "functions"
---

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

# Options kit

The chart serves a coin's listed options as `options_chain` cells, one
ten-value tuple per contract, on the live bar only
([Data sources](../core-concepts/data-sources.md)). The `./sdk/options`
module is the arithmetic a gamma map runs over that block, with nothing
to import:
`OptionsChain` takes the chain, filters it by expiry and strike window,
aggregates every contract into a strike table sorted ascending, and
answers gamma exposure by strike and for the chain, the zero-gamma flip,
the call and put walls, max pain and the put/call open-interest ratio.
Its numbers are the [Gamma map](../cookbook/gamma-map.md) recipe's
numbers: the same operations in the same order, so a chain loaded here
and measured by the template agree to the last bit, with one exception:
when the running net sum crosses zero more than once (the far wings'
few dollars can flip it back and forth), the template's flip is the
crossing nearest spot, where `zeroGamma()` names the first one walking
up. Construct the object
in `onStart()`, load it once on the live bar, read it in the same bar; no
call allocates after construction, so the module never grows memory per
bar.

| Export | Signature | Definition |
| --- | --- | --- |
| `OPTIONS_TUPLE` | `const OPTIONS_TUPLE: i32 = 10` | f64 values per contract in an `options_chain` block: `[strike, expiry_ms, side, oi, gamma, delta, mark_iv, underlying, multiplier, vega]`, `side` `+1` for a call and `-1` for a put |
| `OptionsChain` | `new OptionsChain(maxStrikes)`; `.load(cells, count, nowMs, spot, nearest = 0, windowPct = 0.0): i32`; `.strikes()`, `.strike(i)`, `.callOi(i)`, `.putOi(i)`, `.callGex(i)`, `.putGex(i)`, `.netGex(i)`; `.totalCallGex()`, `.totalPutGex()`, `.totalNetGex()`; `.zeroGamma()`, `.callWall()`, `.putWall()`, `.maxPain()`, `.putCallRatio()` | One chain measured by strike: `load()` keeps the contracts expiring after `nowMs` (the `nearest` soonest expiries when `nearest > 0`, the strikes within `windowPct` percent of `spot` when `windowPct > 0`), sums open interest and gamma exposure per strike and side, and returns the strike count kept; the readers answer for the chain last loaded |

![The strike table on a price axis: call exposure drawn to the right of each strike and put exposure to the left, the call wall above spot and the put wall below it, the running net sum climbing from the lowest strike and crossing the axis at the zero-gamma point, and max pain circled among the strikes](/wrun/images/diagrams/options-strike-table.svg)

1. **Strikes** on the price axis, ascending from the bottom: `strike(i)`,
   with `i` `0` the lowest.
2. **Call GEX** to the right of each strike and **put GEX** to the left:
   `callGex(i)` and `putGex(i)`, both stored positive.
3. **The running net sum**, calls minus puts from the lowest strike up:
   `zeroGamma()` is where it crosses zero, placed between two strikes by
   the two sums' magnitudes.
4. **Call wall**, the strike at or above spot holding the most call open
   interest; **put wall**, the strike at or below spot holding the most
   put open interest.
5. **Max pain**, the listed strike that pays holders the least, circled
   among the strikes.

Per contract, with `spot_c` the contract's `underlying` when above 0 and
otherwise the `spot` you passed, and `mult` its `multiplier` when above 0
and otherwise 1:

- `callGex(i)` and `putGex(i)` sum `gamma * oi * mult * spot_c * spot_c * 0.01`
  over the calls and the puts at the `i`-th strike: the dollar change of
  the open interest's delta for a 1% move of spot, in USD per 1% move.
  Both are stored positive (a listed option's gamma is positive).
- `netGex(i)` is `callGex(i) - putGex(i)`: the dealer-naive sign
  convention, calls positive and puts negative, taking dealers as long
  the calls customers sold them and short the puts customers bought.
  `totalCallGex()`, `totalPutGex()` and `totalNetGex()` sum the table
  from the lowest strike up.
- `callOi(i)` and `putOi(i)` are the open interest summed per side at the
  strike, in the chain's own unit (coins on Deribit, contracts on CME).
  `putCallRatio()` is total put over total call open interest, `NaN`
  when the call side holds none.
- `callWall()` is the strike at or above `spot` holding the most call
  open interest, `putWall()` the strike at or below `spot` holding the
  most put open interest; `NaN` when no strike on that side has any.
  Ties keep the lower strike.
- `maxPain()` is the listed strike `S` minimising
  `sum(callOi * max(0, S - K)) + sum(putOi * max(0, K - S))` over the
  table's strikes `K`, the settlement price that pays holders the least;
  ties keep the lower strike, `NaN` on an empty table.
- `zeroGamma()` walks the strikes upward summing `netGex`: the strike
  where the running sum lands on exactly 0, or the point between the two
  strikes where it changes sign, placed by the two sums' magnitudes
  (`prev + (next - prev) * |cum_prev| / (|cum_prev| + |cum_next|)`);
  `NaN` when the sum never changes sign.
- `strikes()` is the table size, `strike(i)` the `i`-th strike ascending
  (`0` the lowest); every indexed reader is `NaN` outside
  `0..strikes() - 1`. Before the first `load()` the table is empty: the
  totals read `0`, everything else `NaN`.

## Reading the chain on the live bar

History bars carry an empty block and the chain arrives on the newest
row, so the read sits under `bar.isLast()`: `in_chain_cells()` is the
f64 count the bar carries (`-1` on a bar without a block),
`in_chain_view()` is the block itself, read in place from the one buffer
the build reserves at module start, and `load()` takes that array, the
count, the time in milliseconds and the chart's close as spot. The
numbers hold on the following bars, so an output or a card written in
`onBar()` reads the same chain until the next live bar refreshes it.

```typescript
param("nearest_expiries", 0, { min: 0, max: 24, description: "Expiries counted: 0 = every listed expiry, N = the N nearest" });
param("window_pct", 0, { min: 0, max: 50, description: "Strikes kept: percent around spot each side, 0 = the whole chain" });
input("close", ohlcv.close); // the chart's own close: the grid, and spot
input("chain", options_chain.cells, { max_cells: 4000, venue: "auto" });
output("net_gex", none, overlay, { description: "Net gamma exposure, USD per 1% move of spot; live bar only" });
output("flip", none, overlay, { description: "Zero gamma: where cumulative net GEX crosses zero" });
output("call_wall", none, overlay, { description: "The heaviest call strike at or above spot" });
output("put_wall", none, overlay, { description: "The heaviest put strike at or below spot" });
output("max_pain", none, overlay, { description: "The strike that pays option holders the least" });

let chain = new OptionsChain(1);
let nearest: i32 = 0;
let windowPct: f64 = 0.0;

function onStart(): void {
  // Construct here: the per-strike buffers are the module's only allocation.
  chain = new OptionsChain(512);
  nearest = i32(p_nearest_expiries());
  windowPct = p_window_pct();
}

function onBar(): void {
  if (bar.isLast()) {
    const n = in_chain_cells();
    // The live chain in place: the first n values of the build's own buffer.
    if (n >= OPTIONS_TUPLE) chain.load(in_chain_view(), n, bar.time() * 1000.0, bar.close(), nearest, windowPct);
  }
  out_net_gex(chain.totalNetGex());
  out_flip(chain.zeroGamma());
  out_call_wall(chain.callWall());
  out_put_wall(chain.putWall());
  out_max_pain(chain.maxPain());
}
```

Read `last 1 net_gex` at the editor's Console prompt for the chain's
total, or loop `chain.strikes()` and draw `chain.netGex(i)` at
`chain.strike(i)` into a frame for the docked profile the Gamma Map
recipe shows.

## Expiries, the strike window and the table size

![The three load() filters as a funnel: every contract in the block enters at the top, about 1,550 on a BTC chain; the first cut drops the contracts whose expiry is not after now and keeps the N nearest expiries when asked; the second drops the strikes outside the window around spot; the third drops the strikes farthest from spot once the table holds maxStrikes; what comes out of the neck is the strike table, sorted ascending](/wrun/images/diagrams/options-load-funnel.svg)

1. **Every contract** in the block enters at the top, about 1,550 on a
   BTC chain.
2. **Expiry**: a contract whose `expiry_ms` is not after `nowMs` drops
   out; `nearest > 0` keeps only the N soonest expiries.
3. **Strike window**: `windowPct > 0` drops the strikes outside the
   window around `spot`.
4. **Table size**: once `maxStrikes` strikes are in, the strikes farthest
   from `spot` drop first.
5. **The strike table** comes out of the neck, sorted ascending; `load()`
   returns its size.

`load(cells, count, nowMs, spot, nearest, windowPct)` applies three
filters before anything is summed:

- **Expiry.** Only contracts with `expiry_ms > nowMs` count; pass the
  bar's open time times 1000 (`bar.time() * 1000.0`), the recipe's clock.
  `nearest > 0` keeps the `nearest` soonest of those expiries, so
  `nearest = 1` is the front expiry alone and `nearest = 2` the front
  two; asking for more expiries than the chain lists keeps them all, and
  `0` keeps every live expiry. There is no cap on distinct expiries.
- **Strike window.** `windowPct > 0` keeps the strikes within
  `windowPct` percent of `spot` on each side, bounds included
  (`spot * (1 - windowPct / 100)` to `spot * (1 + windowPct / 100)`);
  `0` keeps every strike. The window narrows the whole chain: the walls,
  the totals, the flip and max pain are all measured over the strikes
  inside it. The Gamma Map template narrows only its walls, so its net
  GEX, flip and max pain are the readings of a chain loaded with
  `windowPct` `0` (the flip with the one exception above), and its walls
  those of a chain loaded with its window;
  an indicator that wants both constructs two `OptionsChain` objects in
  `onStart()` and loads each.
- **Table size.** `maxStrikes` (clamped to at least 1) is the most
  distinct strikes the table holds; the Gamma Map keeps 512. When a
  chain lists more, the strikes farthest from `spot` are dropped first,
  whatever order the contracts arrive in (two strikes at the same
  distance: the lower goes first), so the table is always the
  `maxStrikes` strikes nearest the chart. The template instead refuses
  the strikes that arrive after its table is full.

A contract whose strike or open interest is not above 0 is skipped, so a
strike only enters the table through live open interest; a contract
whose expiry is not a number is skipped too. A gamma that is not a
number poisons its strike's exposure and the totals, exactly as the
template's sums do, and the walls, max pain and the open-interest ratio
never read gamma, so they stay finite.

## A strike matrix on the price axis

The per-strike readings are a table whose rows are prices, which is what
`plot.matrix` draws: one row per strike docked on the price axis, one
column per expiry, each cell filled from a diverging palette
([Price canvases](../presentation/price-canvases.md#strike-matrices)). Load one
`OptionsChain` per expiry window on the live bar, write the board into a
frame (prices ascending, one cell row per price, the ATM strike in
`highlight`) and declare the matrix over it; the chart keeps the rows on
their prices as you pan and zoom. The frame rides `wrun-4`, which a
`frame(...)` declaration stamps for you.

```typescript
input("close", ohlcv.close);
input("chain", options_chain.cells, { max_cells: 4000, venue: "auto" });
const board = frame("board", { max_bytes: 65536 });
plot.matrix({ name: "gex_board", frame: board, dock: "right", columns: ["Front", "Next", "Third"], price_column: true, palette: ["theme.down", "theme.bg", "theme.up"], center: 0, format: "si", tooltip: "{{column}} net GEX {{value:usd}} at {{price}}" });
```

The [Strike Matrix](../cookbook/strike-matrix.md) recipe is the whole
file: net gamma exposure by strike across the four nearest expiries (a
setting), the ATM row highlighted, rewritten on every live bar.

## Limits

- **The chain is the live row's only.** `options_chain` fills the
  newest row and leaves every history row an empty block
  ([Limitations](../reference/limitations.md)); read it under `bar.isLast()`
  and keep the readings for the bars that follow. A gamma map over past
  chains has no form yet.
- **The chains are the chart's.** `venue: "auto"` reads the chart's own
  options venue when it lists options, else the coin's Deribit chain; a
  venue's name (`deribit`, `cme`, ...) pins one. Open interest and the
  multiplier arrive as
  the venue reports them: Deribit in coins with a multiplier of 1, CME in
  contracts with the point value as the multiplier, which is why the
  exposure is in USD on both. A venue the chart cannot serve is refused
  by name.
- **Size `max_cells` for the chain.** A BTC chain is about 1,550
  contracts; a block over `max_cells` refuses the run instead of being
  truncated, and the `cells` array costs `max_cells * 10 * 8` bytes of
  module memory ([Limits](../reference/limits.md)).
- **Browser lane.** `options_chain` is served by the chart; any other
  host refuses an indicator that declares it, by name.

## From Pine

Nothing on this page maps to Pine. Pine cannot read an options chain,
so there is no `request.*` call or `ta.*` function to translate: gamma
exposure, the walls and max pain are computed from cells the chart
serves to the indicator, and the class is this chart's arithmetic for
them.
