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).
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.
| 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 Options; .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 |
- Strikes on the price axis, ascending from the bottom:
strike(i), withi0the lowest. - Call GEX to the right of each strike and put GEX to the left:
callGex(i)andputGex(i), both stored positive. - 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. - 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.
- 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)andputGex(i)sumgamma * oi * mult * spot_c * spot_c * 0.01over the calls and the puts at thei-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)iscallGex(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()andtotalNetGex()sum the table from the lowest strike up.callOi(i)andputOi(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,NaNwhen the call side holds none.callWall()is the strike at or abovespotholding the most call open interest,putWall()the strike at or belowspotholding the most put open interest;NaNwhen no strike on that side has any. Ties keep the lower strike.maxPain()is the listed strikeSminimisingsum(callOi * max(0, S - K)) + sum(putOi * max(0, K - S))over the table's strikesK, the settlement price that pays holders the least; ties keep the lower strike,NaNon an empty table.zeroGamma()walks the strikes upward summingnetGex: 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|));NaNwhen the sum never changes sign.strikes()is the table size,strike(i)thei-th strike ascending (0the lowest); every indexed reader isNaNoutside0..strikes() - 1. Before the firstload()the table is empty: the totals read0, everything elseNaN.
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.
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
- Every contract in the block enters at the top, about 1,550 on a BTC chain.
- Expiry: a contract whose
expiry_msis not afternowMsdrops out;nearest > 0keeps only the N soonest expiries. - Strike window:
windowPct > 0drops the strikes outside the window aroundspot. - Table size: once
maxStrikesstrikes are in, the strikes farthest fromspotdrop first. - 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 > nowMscount; pass the bar's open time times 1000 (bar.time() * 1000.0), the recipe's clock.nearest > 0keeps thenearestsoonest of those expiries, sonearest = 1is the front expiry alone andnearest = 2the front two; asking for more expiries than the chain lists keeps them all, and0keeps every live expiry. There is no cap on distinct expiries. - Strike window.
windowPct > 0keeps the strikes withinwindowPctpercent ofspoton each side, bounds included (spot * (1 - windowPct / 100)tospot * (1 + windowPct / 100));0keeps 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 withwindowPct0(the flip with the one exception above), and its walls those of a chain loaded with its window; an indicator that wants both constructs twoOptionsChainobjects inonStart()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 fromspotare dropped first, whatever order the contracts arrive in (two strikes at the same distance: the lower goes first), so the table is always themaxStrikesstrikes 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.
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_chainfills the newest row and leaves every history row an empty block (Limitations); read it underbar.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_cellsfor the chain. A BTC chain is about 1,550 contracts; a block overmax_cellsrefuses the run instead of being truncated, and thecellsarray costsmax_cells * 10 * 8bytes of module memory (Limits). - Browser lane.
options_chainis 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.