Options kit

View as MarkdownOpen the editor

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 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 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.

ExportSignatureDefinition
OPTIONS_TUPLEconst OPTIONS_TUPLE: i32 = 10f64 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
OptionsChainnew 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 in the colored pencil, 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.strikeput GEXcall GEX66k65k64k63k62k61k60k59k58k57kzero gammaspotcall wallput wallmax painnet GEX, summed from the lowest strike up
  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.

wrun
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 contracts whose expiry is not after now, keeping the N nearest expiries when asked; the second drops 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.droppedexpiry after nownowMs, neareststrikes within the windowspot, windowPctthe nearest maxStrikesfarthest from spot firstevery contractlive expiriesin the windownearest spot1,550 on BTC60k61k62kthe strike table, sorted ascending
  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). 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.

wrun
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 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); 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).
  • 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.