---
title: "Option stance"
description: "One card that summarizes where the chart's underlying's options stand, from the daily options statistics and the issuer's next earnings date, and which of five…"
order: 121
section: "cookbook"
---

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

# Option stance

One card that summarizes where the chart's underlying's options stand, from the daily options statistics and the issuer's next earnings date, and which of five trade-idea stances that picture runs with. The volatility regime is the IV rank (where the session's 30-day IV sits between the window's low and high) and percentile. The positioning is dealer gamma on the open-interest basis (long gamma dampens moves, short gamma amplifies them), spot against the gamma flip, the call and put walls, the put/call volume and open-interest ratios, the 25 delta risk reversal and the front expiry's implied move. Each stance reads plain rules over those, and the card says **With**, **Neutral** or **Against** for each. The rules describe the regime, not a trade.

The parts are scalar `option_daily` inputs, the `market.earnings_next_ms()` fact read once in `onStart()` through `earningsMs()` ([Earnings dates](../core-concepts/data-sources.md#earnings-dates)), the options kit's `DayHistory` for the IV rank and percentile ([Options kit](../functions/options-kit.md#a-history-of-daily-statistics)), and a `render.hud` card of pill tiles whose colours follow each stance's code ([HUD cards](../presentation/hud-and-hover-cards.md#hud-cards)). This is also the `option-stance` template: the **Option Stance** card under **HUDs** in the editor's starter list, and it compiles as written.

## The wrun indicator

```typescript sample=option-stance
// Option Stance: one card that summarizes where the chart's underlying's options stand, from the daily options
// statistics (option_daily) and the issuer's next earnings date, and which of five trade-idea stances that picture
// runs with. The volatility regime is the IV rank (where the session's 30-day IV sits between the window's low and
// high) and percentile; the positioning is dealer gamma on the open-interest basis (long gamma dampens moves, short
// gamma amplifies them), spot against the gamma flip, the call and put walls, the put/call volume and open-interest
// ratios, the 25 delta risk reversal and the front expiry's implied move. Each stance reads plain rules over those, and
// the card says With, Neutral or Against for each: selling premium runs with a high IV rank, long dealer gamma and no
// earnings report inside the earnings window, and against a low rank or a report inside it; selling vega with a high
// rank and the term in contango, against a low rank; directional buying with short dealer gamma below a high rank,
// against long gamma at a high rank; long-dated buying with a low rank, against a high one; cheap out-of-the-money
// buying with a low rank and short gamma, against a high rank. The rules describe the regime, not a trade. The
// statistics are short today (they start when the data plane switches the daily job on): the positioning reads from
// the first session, but the IV rank, and with it every stance, needs Minimum history (60 sessions); until then the
// stances read Needs history and the card says how many sessions there are. US stock charts, daily or finer.

param.int("lookback", 252, { min: 20, max: 520, label: "Window, sessions", description: "Sessions the IV rank and percentile read: 252 is about a year" });
param.int("min_sessions", 60, { min: 10, max: 520, label: "Minimum history, sessions", description: "Sessions of statistics before the IV rank, and the stances, show" });
param.number("high_rank", 50, { min: 0, max: 100, step: 1, label: "High IV rank", description: "An IV rank at or over it is high: the premium sellers' side" });
param.number("low_rank", 30, { min: 0, max: 100, step: 1, label: "Low IV rank", description: "An IV rank at or under it is low: the premium buyers' side" });
param.int("earnings_days", 30, { min: 0, max: 120, label: "Earnings window, days", description: "An earnings report within this many days counts against selling premium" });
input("close", ohlcv.close); // the chart's own close: the grid
input("iv30", option_daily.atm_iv_30d, { missing: "nan", description: "The 30-day at-the-money IV, percent, one row per session (the daily options statistics)" }); // every daily input lands on the first bar of its day, NaN elsewhere
input("iv90", option_daily.atm_iv_90d, { missing: "nan", description: "The 90-day at-the-money IV, percent" });
input("rr25", option_daily.rr25_30d, { missing: "nan", description: "The 30-day 25 delta risk reversal, call IV minus put IV, volatility points" });
input("gex", option_daily.net_gex, { missing: "nan", description: "Net gamma exposure, USD per 1% move" });
input("flip", option_daily.gamma_flip, { missing: "nan", description: "The gamma flip, a price" });
input("call_wall", option_daily.call_wall, { missing: "nan", description: "The call wall, a price" });
input("put_wall", option_daily.put_wall, { missing: "nan", description: "The put wall, a price" });
input("call_vol", option_daily.call_volume, { missing: "nan", description: "Call contracts traded in the session" });
input("put_vol", option_daily.put_volume, { missing: "nan", description: "Put contracts traded in the session" });
input("call_oi", option_daily.call_oi, { missing: "nan", description: "Call open interest, contracts" });
input("put_oi", option_daily.put_oi, { missing: "nan", description: "Put open interest, contracts" });
input("move", option_daily.implied_move_front, { missing: "nan", description: "The front expiry's implied move, a price distance" });
input("spot", option_daily.underlying_price, { missing: "nan", description: "The underlying's price the statistics read" });
market.earnings_next_ms(); // the issuer's next earnings report, epoch ms (0 when the chart does not know it)
output("iv_rank", none, overlay, { description: "Where the 30-day IV sits between the window's low (0) and high (100); NaN below Minimum history" });
output("iv_pct", none, overlay, { description: "The share of the window's earlier sessions with a lower 30-day IV, percent" });
output("gex", none, overlay, { format: "usd", description: "Net gamma exposure, USD per 1% move" });
output("gamma_code", none, overlay, { description: "0 short gamma, 1 long gamma (net at or above zero)" });
output("flip_dist", none, overlay, { description: "Spot over the gamma flip, minus 1, percent" });
output("wall_call", none, overlay, { format: "price", description: "The call wall" });
output("wall_put", none, overlay, { format: "price", description: "The put wall" });
output("pc_volume", none, overlay, { description: "Put contracts over call contracts" });
output("pc_oi", none, overlay, { description: "Put open interest over call open interest" });
output("rr25", none, overlay, { description: "The 30-day 25 delta risk reversal, call IV minus put IV" });
output("move_pct", none, overlay, { description: "The front expiry's implied move over the price, percent" });
output("earnings_days", none, overlay, { description: "Days to the next earnings report; NaN when none is known" });
output("sell_premium", none, overlay, { description: "Selling premium: 0 against, 1 neutral, 2 with; 1 while the IV rank needs history" });
output("sell_vega", none, overlay, { description: "Selling vega: 0 against, 1 neutral, 2 with" });
output("directional", none, overlay, { description: "Directional buying: 0 against, 1 neutral, 2 with" });
output("long_dated", none, overlay, { description: "Long-dated buying: 0 against, 1 neutral, 2 with" });
output("cheap_options", none, overlay, { description: "Cheap out-of-the-money buying: 0 against, 1 neutral, 2 with" });
output("status", none, overlay, { description: "0 the IV rank needs history, 1 every reading reads, 2 a chart coarser than a day; NaN before the first session" });
string("read", { max_bytes: 64 }); // the headline: the volatility regime
string("gamma_text", { max_bytes: 48 }); // "Long gamma, spot 1.2% over the flip"
string("earnings_text", { max_bytes: 32 }); // "Earnings 28OCT, in 11 days"
string("s_sell_premium", { max_bytes: 16 }); // each stance's word
string("s_sell_vega", { max_bytes: 16 });
string("s_directional", { max_bytes: 16 });
string("s_long_dated", { max_bytes: 16 });
string("s_cheap_options", { max_bytes: 16 });
string("title", { max_bytes: 64 });

render.hud("stance_card", {
  position: "bottom_right",
  look: "terminal",
  title: "{{title}}",
  columns: 2,
  tiles: [
    tile.pill("Volatility", "read", { headline: true, color_by: "status", colors: ["#94a3b8", "#e2e8f0", "#94a3b8"] }),
    tile.pill("Dealer gamma", "gamma_text", { color_by: "gamma_code", colors: ["#ef4444", "#22c55e"] }),
    tile.pill("Sell premium", "s_sell_premium", { color_by: "sell_premium", colors: ["#ef4444", "#94a3b8", "#22c55e"] }),
    tile.pill("Sell vega", "s_sell_vega", { color_by: "sell_vega", colors: ["#ef4444", "#94a3b8", "#22c55e"] }),
    tile.pill("Directional", "s_directional", { color_by: "directional", colors: ["#ef4444", "#94a3b8", "#22c55e"] }),
    tile.pill("Long-dated", "s_long_dated", { color_by: "long_dated", colors: ["#ef4444", "#94a3b8", "#22c55e"] }),
    tile.pill("Cheap options", "s_cheap_options", { color_by: "cheap_options", colors: ["#ef4444", "#94a3b8", "#22c55e"] }),
    tile.pill("Earnings", "earnings_text"),
    tile.rows([
      ["1) IV rank", "iv_rank", "0"],
      ["2) IV percentile", "iv_pct", "0"],
      ["3) Net gamma", "gex", "usd"],
      ["4) Call wall", "wall_call", "price"],
      ["5) Put wall", "wall_put", "price"],
      ["6) Put/call volume", "pc_volume", "0.00"],
      ["7) Put/call OI", "pc_oi", "0.00"],
      ["8) Risk reversal, 25 delta", "rr25", "0.00"],
      ["9) Front move, %", "move_pct", "0.00"],
    ]),
  ],
});

const DAY = 86400.0;
const CAPACITY = 520;
const WORDS: String[] = ["Against", "Neutral", "With"];
const MONTHS: String[] = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
const ivs = new DayHistory(CAPACITY); // one 30-day IV per session
const dateClock = new Clock("UTC");

let lookback = 252; // settings, read in onStart()
let need = 60;
let highRank = 50.0;
let lowRank = 30.0;
let earningsDays = 30.0;
let nextEarningsMs: f64 = NaN;
let prevT: f64 = NaN; // the bar spacing
let barSec: f64 = NaN;
let sessionDay: f64 = NaN; // the newest session, and what it carried, held
let rankNow: f64 = NaN;
let pctNow: f64 = NaN;
let iv30: f64 = NaN;
let iv90: f64 = NaN;
let rr: f64 = NaN;
let gex: f64 = NaN;
let flip: f64 = NaN;
let callWall: f64 = NaN;
let putWall: f64 = NaN;
let callVol: f64 = NaN;
let putVol: f64 = NaN;
let callOi: f64 = NaN;
let putOi: f64 = NaN;
let move: f64 = NaN;
let spot: f64 = NaN;

function onStart(): void {
  lookback = i32(p_lookback());
  need = i32(p_min_sessions());
  if (need > lookback) need = lookback;
  highRank = p_high_rank();
  lowRank = p_low_rank();
  earningsDays = p_earnings_days();
  nextEarningsMs = earningsMs(p_market_earnings_next_ms());
}

function land(day: f64): void {
  sessionDay = day;
  iv30 = in_iv30();
  iv90 = in_iv90();
  rr = in_rr25();
  gex = in_gex();
  flip = in_flip();
  callWall = in_call_wall();
  putWall = in_put_wall();
  callVol = in_call_vol();
  putVol = in_put_vol();
  callOi = in_call_oi();
  putOi = in_put_oi();
  move = in_move();
  spot = in_spot();
  if (iv30 > 0.0) ivs.push(day, iv30);
  const ready = ivs.span(lookback) >= need;
  rankNow = ready ? ivs.rank(lookback) : NaN;
  pctNow = ready ? ivs.percentile(lookback) : NaN;
}

function ratio(a: f64, b: f64): f64 {
  return b > 0.0 && !isNaN(a) ? a / b : NaN;
}

/** Days from the bar to the next earnings report; NaN when none is known or it has passed. */
function daysToEarnings(t: f64): f64 {
  if (isNaN(nextEarningsMs)) return NaN;
  const d = (nextEarningsMs - t * 1000.0) / (DAY * 1000.0);
  return d >= 0.0 ? d : NaN;
}

// ── The five stances, each 0 against, 1 neutral, 2 with; NaN while the IV rank needs history ──
function sellPremium(t: f64): f64 {
  if (isNaN(rankNow)) return NaN;
  const e = daysToEarnings(t);
  const earningsSoon = !isNaN(e) && e <= earningsDays;
  if (rankNow <= lowRank || earningsSoon) return 0.0;
  return rankNow >= highRank && gex >= 0.0 ? 2.0 : 1.0;
}
function sellVega(): f64 {
  if (isNaN(rankNow)) return NaN;
  if (rankNow <= lowRank) return 0.0;
  return rankNow >= highRank && iv90 >= iv30 ? 2.0 : 1.0;
}
function directional(): f64 {
  if (isNaN(rankNow) || isNaN(gex)) return NaN;
  if (gex >= 0.0 && rankNow >= highRank) return 0.0;
  return gex < 0.0 && rankNow < highRank ? 2.0 : 1.0;
}
function longDated(): f64 {
  if (isNaN(rankNow)) return NaN;
  return rankNow <= lowRank ? 2.0 : rankNow >= highRank ? 0.0 : 1.0;
}
function cheapOptions(): f64 {
  if (isNaN(rankNow)) return NaN;
  if (rankNow >= highRank) return 0.0;
  return rankNow <= lowRank && gex < 0.0 ? 2.0 : 1.0;
}

function sbDay(day: f64): void { // "16OCT"
  dateClock.update(day * DAY);
  if (dateClock.dayOfMonth() < 10) sb_text("0");
  sb_int(dateClock.dayOfMonth());
  sb_text(MONTHS[dateClock.month() - 1]);
}

function sbStance(code: f64): void {
  sb_clear();
  if (isNaN(code)) sb_text("Needs history");
  else sb_text(WORDS[i32(code)]);
}

function writeWords(coarse: bool, t: f64): void {
  const held = ivs.span(lookback);
  sb_clear();
  if (coarse) sb_text("Needs a daily or finer chart");
  else if (isNaN(sessionDay)) sb_text("Waiting for the daily options statistics");
  else if (held < need) {
    sb_text("Not enough history yet: ");
    sb_int(held);
    sb_text(" of ");
    sb_int(need);
    sb_text(" sessions");
  } else if (isNaN(rankNow)) sb_text("No range: the IV has not moved in the window");
  else {
    sb_text(rankNow >= highRank ? "High IV, rank " : rankNow <= lowRank ? "Low IV, rank " : "Middling IV, rank ");
    sb_f64(rankNow, 0);
    sb_text(", percentile ");
    sb_f64(pctNow, 0);
  }
  str_read_sb();
  sb_clear();
  if (coarse || isNaN(gex)) sb_text("No gamma reading");
  else {
    sb_text(gex >= 0.0 ? "Long gamma" : "Short gamma");
    if (spot > 0.0 && flip > 0.0) {
      const dist = (spot / flip - 1.0) * 100.0;
      sb_text(", spot ");
      sb_f64(Math.abs(dist), 1);
      sb_text(dist >= 0.0 ? "% over the flip" : "% under the flip");
    }
  }
  str_gamma_text_sb();
  sb_clear();
  const e = daysToEarnings(t);
  if (isNaN(e)) sb_text("No report known");
  else {
    sbDay(Math.floor(nextEarningsMs / (DAY * 1000.0)));
    sb_text(", in ");
    sb_int(i64(Math.floor(e)));
    sb_text(Math.floor(e) == 1.0 ? " day" : " days");
  }
  str_earnings_text_sb();
  sbStance(coarse ? NaN : sellPremium(t));
  str_s_sell_premium_sb();
  sbStance(coarse ? NaN : sellVega());
  str_s_sell_vega_sb();
  sbStance(coarse ? NaN : directional());
  str_s_directional_sb();
  sbStance(coarse ? NaN : longDated());
  str_s_long_dated_sb();
  sbStance(coarse ? NaN : cheapOptions());
  str_s_cheap_options_sb();
  sb_clear();
  sb_text("Option stance");
  if (!coarse && !isNaN(sessionDay)) {
    sb_text(", as of ");
    sbDay(sessionDay);
  }
  str_title_sb();
}

// onBar() runs once per bar: the spacing, then the session's statistics when they land; every bar writes the held
// readings and the stances, and the live bar the card's words.
function onBar(): void {
  const t = bar.time();
  if (!isNaN(prevT) && t > prevT && (isNaN(barSec) || t - prevT < barSec)) barSec = t - prevT;
  prevT = t;
  const coarse = !isNaN(barSec) && barSec > 1.5 * DAY;
  if (!coarse) {
    const day = Math.floor(t / DAY);
    const any = !isNaN(in_iv30()) || !isNaN(in_gex()) || !isNaN(in_spot());
    if (any && (isNaN(sessionDay) || day >= sessionDay)) land(day);
  }
  const has = !coarse && !isNaN(sessionDay);
  out_iv_rank(has ? rankNow : NaN);
  out_iv_pct(has ? pctNow : NaN);
  out_gex(has ? gex : NaN);
  out_gamma_code(has && !isNaN(gex) ? (gex >= 0.0 ? 1.0 : 0.0) : NaN);
  out_flip_dist(has && spot > 0.0 && flip > 0.0 ? (spot / flip - 1.0) * 100.0 : NaN);
  out_wall_call(has ? callWall : NaN);
  out_wall_put(has ? putWall : NaN);
  out_pc_volume(has ? ratio(putVol, callVol) : NaN);
  out_pc_oi(has ? ratio(putOi, callOi) : NaN);
  out_rr25(has ? rr : NaN);
  out_move_pct(has && spot > 0.0 && !isNaN(move) ? (move / spot) * 100.0 : NaN);
  out_earnings_days(daysToEarnings(t));
  const sp = has ? sellPremium(t) : NaN;
  const sv = has ? sellVega() : NaN;
  const dr = has ? directional() : NaN;
  const ld = has ? longDated() : NaN;
  const co = has ? cheapOptions() : NaN;
  out_sell_premium(isNaN(sp) ? 1.0 : sp);
  out_sell_vega(isNaN(sv) ? 1.0 : sv);
  out_directional(isNaN(dr) ? 1.0 : dr);
  out_long_dated(isNaN(ld) ? 1.0 : ld);
  out_cheap_options(isNaN(co) ? 1.0 : co);
  out_status(coarse ? 2.0 : isNaN(sessionDay) ? NaN : ivs.span(lookback) >= need ? 1.0 : 0.0);
  if (bar.isLast()) writeWords(coarse, t);
}
```

## How it works

**The five stances, by rule.**

| Stance | With | Against |
| --- | --- | --- |
| Sell premium | IV rank at or over **High IV rank**, dealers long gamma, no earnings report within **Earnings window** | IV rank at or under **Low IV rank**, or a report within the window |
| Sell vega | IV rank high and the 90-day IV at or over the 30-day (contango) | IV rank low |
| Directional | dealers short gamma and the IV rank under high | dealers long gamma at a high IV rank |
| Long-dated | IV rank low | IV rank high |
| Cheap options | IV rank low and dealers short gamma | IV rank high |

Anything else is **Neutral**. The weights a fit score would need are not public, so the card says which side of each rule the regime sits on instead of scoring contracts.

**The positioning.** Every reading is the newest session's: net gamma's sign, spot over the flip in percent, the walls, the two put/call ratios, the risk reversal (call IV minus put IV) and the front move over the price.

## Where it runs

US stock charts, through OpenMarket's official package: `option_daily` is served by the chart's browser lane for the chart's own underlying, and the chart fills the earnings date on a US stock chart ([Limitations](../reference/limitations.md)). Both are declared ahead of the lanes that serve them, so the template builds today and the chart shows the lane's refusal until the data plane serves the statistics. A daily or finer chart.

## When data is missing

The statistics start the day the job is switched on. The positioning reads from the first session; the IV rank, and with it every stance, needs **Minimum history** (60 sessions): until then each stance reads **Needs history** and the headline reads "Not enough history yet: 12 of 60 sessions". An unknown earnings date reads "No report known" and never counts against a stance. Before the first session the card reads "Waiting for the daily options statistics"; a chart coarser than a day reads "Needs a daily or finer chart".

## Customize it

- **Your own thresholds.** Move **High IV rank** and **Low IV rank** to where you sell and buy volatility.
- **A wider earnings guard.** Set **Earnings window** to the expiries you trade.
- **Another year.** `lookback` sets the sessions the IV rank reads.

## Run it

1. In the editor's Explorer, press the **Templates** icon and pick **Option Stance** under **HUDs**.
2. Press **Run** on a US stock's chart.
3. At the editor's Console prompt, type `iv_rank`, `sell_premium` and `earnings_days`.

## Concepts used

- [Data sources](../core-concepts/data-sources.md#us-listed-options-by-the-charts-underlying) for `option_daily`
- [Earnings dates](../core-concepts/data-sources.md#earnings-dates) for `market.earnings_next_ms()`
- [Options kit](../functions/options-kit.md#a-history-of-daily-statistics) for `DayHistory`
- [HUD cards](../presentation/hud-and-hover-cards.md#hud-cards) for the card and its pills
