Option stance

View as MarkdownOpen the editor

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), the options kit's DayHistory for the IV rank and percentile (Options kit), and a render.hud card of pill tiles whose colours follow each stance's code (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

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

StanceWithAgainst
Sell premiumIV rank at or over High IV rank, dealers long gamma, no earnings report within Earnings windowIV rank at or under Low IV rank, or a report within the window
Sell vegaIV rank high and the 90-day IV at or over the 30-day (contango)IV rank low
Directionaldealers short gamma and the IV rank under highdealers long gamma at a high IV rank
Long-datedIV rank lowIV rank high
Cheap optionsIV rank low and dealers short gammaIV 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). 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