---
title: "Skew history"
description: "How the chart's underlying's options skew has moved, and where today's term structure sits in its own past. Three reads. The 30-day 25 delta risk reversal, one…"
order: 151
section: "cookbook"
---

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

# Skew history

How the chart's underlying's options skew has moved, and where today's term structure sits in its own past. Three reads. The 30-day 25 delta risk reversal, one per session from the daily options statistics (the call's IV minus the put's at the expiry nearest 30 days), drawn as a step line with its percentile and z-score against the window's earlier sessions. The risk reversal of one expiry, the one nearest the **Expiry, days out** setting, at 25 or 10 delta, read on every bar from the option surface, with each value checked against its present bit; a bar the data plane marks partial draws on a dotted line of its own and is counted, never mixed into the solid line. And the term structure now against its window: the at-the-money IV at seven constant maturities beside each one's median and 25th and 75th percentile over the window's sessions, in a panel below the chart. A card at the bottom right names the skew (puts rich when the put IV minus the call IV is over 0.06 of the 30-day IV, calls rich below 0, flat between) with the numbers behind it.

The parts are scalar `option_daily` inputs and the celled `option_surface` source ([Option surface](../core-concepts/data-sources.md#option-surface)) read through `hasBit` and `isPartial`, the options kit's `DayHistory` for the percentile, the z-score and the quantiles ([Options kit](../functions/options-kit.md#a-history-of-daily-statistics)), a `panel.line` over category maturities ([Line panels](../presentation/cards-frames-panels.md#line-panels)), and a `render.hud` card. This is also the `skew-history` template: the **Skew History** card under **Beyond the time axis** in the editor's starter list, and it compiles as written.

## The wrun indicator

```typescript sample=skew-history
// Skew History: how the chart's underlying's options skew has moved, and where today's term structure sits in its own
// past. Three reads. The 30-day 25 delta risk reversal, one per session from the daily options statistics
// (option_daily: the call's IV minus the put's at the expiry nearest 30 days), drawn as a step line with its percentile
// and z-score against the window's earlier sessions. The risk reversal of one expiry, the one nearest the Expiry
// setting's days out, at 25 or 10 delta, read on every bar from the option surface (option_surface: one tuple per
// expiry with present bits per value; a bar the data plane marks partial, bit 14, draws on a dotted line of its own
// and is counted, never mixed into the solid line). And the term structure now against its window: the at-the-money
// IV at seven constant maturities (7 days to a year) beside each one's median and 25th and 75th percentile over the
// window's sessions, in a panel below the chart. A card at the bottom right names the skew (puts rich over 0.06 of
// the 30-day IV, calls rich below 0, flat between) with the numbers behind it. The statistics are short today (they
// start when the data plane switches the daily job on): the percentile, the z-score and the distribution need Minimum
// history (60 sessions), and until then the card says how many sessions there are. The surface's five-minute history
// is kept 30 days, so older intraday bars arrive partial. US stock charts, daily or finer.

param.int("days", 30, { min: 1, max: 365, label: "Expiry, days out", description: "The surface line reads the expiry nearest this many days out (never one that settles today)" });
param.choice("delta", ["25 delta", "10 delta"], "25 delta", { label: "Surface delta", description: "The wings the surface line compares: 25 delta, or 10 delta further out" });
param.int("lookback", 252, { min: 20, max: 520, label: "Window, sessions", description: "Sessions the percentile, the z-score and the term distribution 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 percentile, the z-score and the distribution show" });
input("close", ohlcv.close); // the chart's own close: the grid
input("rr25", option_daily.rr25_30d, { missing: "nan", description: "The 30-day 25 delta risk reversal, call IV minus put IV, volatility points, one row per session" });
input("iv7", option_daily.atm_iv_7d, { missing: "nan", description: "The 7-day at-the-money IV, percent" });
input("iv14", option_daily.atm_iv_14d, { missing: "nan", description: "The 14-day at-the-money IV, percent" });
input("iv30", option_daily.atm_iv_30d, { missing: "nan", description: "The 30-day at-the-money IV, percent" });
input("iv60", option_daily.atm_iv_60d, { missing: "nan", description: "The 60-day at-the-money IV, percent" });
input("iv90", option_daily.atm_iv_90d, { missing: "nan", description: "The 90-day at-the-money IV, percent" });
input("iv180", option_daily.atm_iv_180d, { missing: "nan", description: "The 180-day at-the-money IV, percent" });
input("iv365", option_daily.atm_iv_365d, { missing: "nan", description: "The 365-day at-the-money IV, percent" });
input("surface", option_surface.cells, { max_cells: 1800, description: "The underlying's option surface on the bar: one tuple per expiry" }); // [expiry_ms, dte_days, forward, atm_iv, iv_p25, iv_c25, iv_p10, iv_c10, present] per expiry
output("rr30", line, lower, { color: "#38bdf8", width: 2, step: true, label: "RR 30d", format: "0.00", description: "The 30-day 25 delta risk reversal, call IV minus put IV, volatility points, per session" });
output("rr_expiry", line, lower, { color: "#f59e0b", width: 1, label: "RR expiry", format: "0.00", description: "The chosen expiry's risk reversal at the chosen delta, call IV minus put IV, volatility points, per bar" });
output("rr_partial", line, lower, { color: "#64748b", width: 1, line_style: "dotted", label: "RR partial", format: "0.00", description: "The same reading on a bar the data plane marks partial: a floor of the data, never the solid line" });
output("zero", line, lower, { role: "guide", color: "#475569", label: "0", description: "The zero line: below it puts are dearer than calls" });
output("rr_pct", none, lower, { description: "The newest 30-day risk reversal's percentile among the window's earlier sessions, 0 to 100" });
output("rr_z", none, lower, { description: "The newest 30-day risk reversal against the mean and deviation of the window's earlier sessions" });
output("scaled", none, lower, { description: "Put IV minus call IV over the 30-day IV (the risk reversal turned round and scaled): the skew word's number" });
output("skew_code", none, lower, { description: "0 calls rich (scaled under 0), 1 flat, 2 puts rich (scaled over 0.06); 1 with no reading" });
output("expiry_days", none, lower, { description: "Days to the expiry the surface line reads" });
output("partial_bars", none, lower, { description: "Bars so far whose surface the data plane marked partial" });
output("sessions", none, lower, { description: "Sessions of statistics the window holds" });
output("status", none, lower, { description: "0 not enough history yet, 1 the window reads, 2 a chart coarser than a day" });
string("read", { max_bytes: 64 }); // the headline
string("title", { max_bytes: 64 }); // the card's title
string("history_text", { max_bytes: 48 }); // "Percentile 18, z -1.42" or the wait
const termRows = frame("term_rows", { max_bytes: 4096 }); // one row per maturity: [label, now, median, 25th, 75th]
// The term structure now against its window: the maturities across, IV up; the frame carries the caption per run.
panel.line({ name: "term", title: "Term structure against its window", x: "category", place: "below", frame: termRows, height_frac: 0.24, chrome: "grid", hover_card: true, legend_style: "chips", format: "0.0", unit: "%", y_title: "At-the-money IV", series: [
  { name: "Now", color: "#e2e8f0", width: 2.5, points: true },
  { name: "Median", color: "#38bdf8", width: 1.5 },
  { name: "25th", color: "#64748b", width: 1, line_style: "dashed" },
  { name: "75th", color: "#64748b", width: 1, line_style: "dashed" },
] });

render.hud("skew_card", {
  position: "bottom_right",
  look: "terminal",
  title: "{{title}}",
  columns: 2,
  tiles: [
    tile.pill("Skew", "read", { headline: true, color_by: "skew_code", colors: ["#38bdf8", "#94a3b8", "#a78bfa"] }),
    tile.pill("Against its history", "history_text", { color_by: "status", colors: ["#94a3b8", "#e2e8f0", "#94a3b8"] }),
    tile.value("RR 30d", "rr30", { format: "0.00" }),
    tile.rows([
      ["1) RR, chosen expiry", "rr_expiry", "0.00"],
      ["2) Expiry, days out", "expiry_days", "int"],
      ["3) Skew over IV", "scaled", "0.000"],
      ["4) Percentile", "rr_pct", "0"],
      ["5) Z-score", "rr_z", "0.00"],
      ["6) Partial bars", "partial_bars", "int"],
    ]),
  ],
});

const DAY = 86400.0;
const TUPLE = 9; // option_surface: expiry_ms, dte_days, forward, atm_iv, iv_p25, iv_c25, iv_p10, iv_c10, present
const BIT_DTE = 1; // present bits: each value after expiry_ms, then the partial bar
const BIT_P25 = 4;
const BIT_C25 = 5;
const BIT_P10 = 6;
const BIT_C10 = 7;
const PUT_RICH = 0.06; // the scaled skew past which puts are rich (under 0, calls are)
const CAPACITY = 520;
const TENORS = 7;
const TENOR_LABELS: String[] = ["7d", "14d", "30d", "60d", "90d", "180d", "1y"];
const MONTHS: String[] = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
const rrs = new DayHistory(CAPACITY); // one 30-day risk reversal per session
const terms: DayHistory[] = []; // one history per maturity
for (let k = 0; k < TENORS; k += 1) terms.push(new DayHistory(CAPACITY));
const termNow = new StaticArray<f64>(TENORS); // the newest session's IV per maturity
const dateClock = new Clock("UTC");

let targetDays = 30.0; // settings, read in onStart()
let tenDelta = false;
let lookback = 252;
let need = 60;
let prevT: f64 = NaN; // the bar spacing
let barSec: f64 = NaN;
let rrNow: f64 = NaN; // the session readings, held
let pctNow: f64 = NaN;
let zNow: f64 = NaN;
let scaledNow: f64 = NaN;
let iv30Now: f64 = NaN;
let partialBars = 0;
let lastDays: f64 = NaN; // the surface line's expiry, held for the card

function onStart(): void {
  targetDays = p_days();
  tenDelta = i32(p_delta()) == 1;
  lookback = i32(p_lookback());
  need = i32(p_min_sessions());
  if (need > lookback) need = lookback;
  for (let k = 0; k < TENORS; k += 1) termNow[k] = NaN;
}

function tenorInput(k: i32): f64 {
  if (k == 0) return in_iv7();
  if (k == 1) return in_iv14();
  if (k == 2) return in_iv30();
  if (k == 3) return in_iv60();
  if (k == 4) return in_iv90();
  if (k == 5) return in_iv180();
  return in_iv365();
}

// ── The surface on this bar: the expiry nearest targetDays with both wings served, and whether the bar is partial ──
let surfaceRr: f64 = NaN;
let surfaceDays: f64 = NaN;
let surfacePartial = false;
function readSurface(): void {
  surfaceRr = NaN;
  surfaceDays = NaN;
  surfacePartial = false;
  const n = in_surface_cells();
  if (n < TUPLE) return; // an empty bar: no surface, a gap in the line
  const cells = in_surface_view();
  const putBit = tenDelta ? BIT_P10 : BIT_P25;
  const callBit = tenDelta ? BIT_C10 : BIT_C25;
  const putAt = tenDelta ? 6 : 4;
  const callAt = tenDelta ? 7 : 5;
  let bestGap: f64 = Infinity;
  let bestExpiry: f64 = Infinity;
  for (let i = 0; i + TUPLE <= n; i += TUPLE) {
    const present = cells[i + 8];
    if (isPartial(present)) surfacePartial = true;
    if (!hasBit(present, BIT_DTE) || !hasBit(present, putBit) || !hasBit(present, callBit)) continue;
    const dte = cells[i + 1];
    if (!(dte >= 1.0)) continue; // an expiry that settles today never stands for a tenor
    const gap = Math.abs(dte - targetDays);
    const expiry = cells[i];
    if (gap < bestGap || (gap == bestGap && expiry < bestExpiry)) {
      bestGap = gap;
      bestExpiry = expiry;
      surfaceDays = dte;
      surfaceRr = cells[i + callAt] - cells[i + putAt];
    }
  }
}

// ── The session readings, when a session lands ──
function measure(): void {
  rrNow = rrs.value(0);
  const ready = rrs.span(lookback) >= need;
  pctNow = ready ? rrs.percentile(lookback) : NaN;
  zNow = ready ? rrs.zscore(lookback) : NaN;
  scaledNow = iv30Now > 0.0 ? -rrNow / iv30Now : NaN;
}

function skewCode(): f64 {
  if (isNaN(scaledNow)) return 1.0;
  return scaledNow > PUT_RICH ? 2.0 : scaledNow < 0.0 ? 0.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 sbSigned(v: f64): void { // "+1.20", "-4.05"
  if (v >= 0.0) sb_text("+");
  sb_f64(v, 2);
}

// ── The term panel: each maturity now, its median and quartiles over the window ──
function writeTerm(): void {
  fb_clear();
  fb_text("{\"rows\":[");
  for (let k = 0; k < TENORS; k += 1) {
    if (k > 0) fb_text(",");
    fb_text("[");
    fb_str(TENOR_LABELS[k]);
    const h = terms[k];
    const ready = h.span(lookback) >= need;
    const now = termNow[k];
    fb_text(",");
    if (isNaN(now)) fb_text("null");
    else fb_f64(now, 2);
    for (let q = 0; q < 3; q += 1) {
      fb_text(",");
      const v = ready ? h.quantile(lookback, q == 0 ? 0.5 : q == 1 ? 0.25 : 0.75) : NaN;
      if (isNaN(v)) fb_text("null");
      else fb_f64(v, 2);
    }
    fb_text("]");
  }
  fb_text("],\"caption\":\"");
  const held = terms[2].span(lookback);
  if (held < need) {
    fb_text("Not enough history yet: ");
    fb_int(held);
    fb_text(" of ");
    fb_int(need);
    fb_text(" sessions");
  } else {
    fb_text("Now against ");
    fb_int(held);
    fb_text(" sessions");
  }
  fb_text("\"}");
  writeFrameBuffer(FRAME_TERM_ROWS);
}

function writeWords(coarse: bool): void {
  const held = rrs.span(lookback);
  sb_clear();
  if (coarse) sb_text("Needs a daily or finer chart");
  else if (rrs.count() == 0) sb_text("Waiting for the daily options statistics");
  else {
    const code = skewCode();
    sb_text(code == 2.0 ? "Puts rich, RR " : code == 0.0 ? "Calls rich, RR " : "Skew flat, RR ");
    sbSigned(rrNow);
  }
  str_read_sb();
  sb_clear();
  if (coarse || rrs.count() == 0) sb_text("No history");
  else if (held < need) {
    sb_text("Not enough history yet: ");
    sb_int(held);
    sb_text(" of ");
    sb_int(need);
    sb_text(" sessions");
  } else {
    sb_text("Percentile ");
    sb_f64(pctNow, 0);
    if (!isNaN(zNow)) {
      sb_text(", z ");
      sbSigned(zNow);
    }
  }
  str_history_text_sb();
  sb_clear();
  sb_text("Skew history, 25 delta");
  if (!coarse && rrs.count() > 0) {
    sb_text(", as of ");
    sbDay(rrs.day(0));
  }
  str_title_sb();
}

// onBar() runs once per bar: the spacing; the surface's expiry on every bar; the session's statistics when they land
// (the risk reversal and each maturity's IV). Each bar writes the held session readings beside its own surface line.
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;
  readSurface();
  if (surfacePartial) partialBars += 1;
  if (!isNaN(surfaceDays)) lastDays = surfaceDays;
  const day = Math.floor(t / DAY);
  if (!coarse) {
    const iv30 = in_iv30();
    if (!isNaN(iv30) && iv30 > 0.0) iv30Now = iv30;
    for (let k = 0; k < TENORS; k += 1) {
      const v = tenorInput(k);
      if (!isNaN(v) && v > 0.0 && terms[k].push(day, v)) termNow[k] = v;
    }
    const rr = in_rr25();
    if (!isNaN(rr) && rrs.push(day, rr)) measure();
  }
  out_zero(0.0);
  out_rr30(coarse ? NaN : rrNow);
  out_rr_expiry(coarse || surfacePartial ? NaN : surfaceRr);
  out_rr_partial(!coarse && surfacePartial ? surfaceRr : NaN);
  out_rr_pct(coarse ? NaN : pctNow);
  out_rr_z(coarse ? NaN : zNow);
  out_scaled(coarse ? NaN : scaledNow);
  out_skew_code(coarse ? 1.0 : skewCode());
  out_expiry_days(coarse ? NaN : lastDays);
  out_partial_bars(f64(partialBars));
  out_sessions(coarse ? NaN : f64(rrs.span(lookback)));
  out_status(coarse ? 2.0 : rrs.span(lookback) >= need ? 1.0 : 0.0);
  if (bar.isLast()) {
    writeWords(coarse);
    if (!coarse) writeTerm();
  }
}
```

## How it works

**The session line.** `rr25_30d` is the statistics' 30-day risk reversal, call IV minus put IV in volatility points: below zero the puts are dearer, the usual shape on a stock. Each session's reading joins a `DayHistory`; `percentile()` and `zscore()` read it against the window's earlier sessions.

**The surface line.** Every bar's `option_surface` block holds one tuple per expiry, `[expiry_ms, dte_days, forward, atm_iv, iv_p25, iv_c25, iv_p10, iv_c10, present]`. The module keeps the expiry whose `dte_days` is nearest the setting among those whose days and both wings are present (bits 1, 4 and 5 at 25 delta, bits 1, 6 and 7 at 10 delta), never an expiry that settles that day, and draws its call wing minus its put wing. When the nearest expiry lacks a wing the next nearest stands in. A bar with bit 14 set on any tuple is partial (older than the five-minute history the data plane keeps, or cut short): its reading goes to the dotted **RR partial** line, a floor of the data rather than a value.

**The term panel.** Each maturity's IV joins its own `DayHistory`; on the live bar `quantile()` reads the median and the quartiles over the window, and the panel draws them beside the newest session's curve.

## Where it runs

US stock charts, through OpenMarket's official package: the chart's browser lane serves `option_daily` for the chart's own underlying ([Limitations](../reference/limitations.md)). It is declared ahead of the lane that serves it, so the template builds today and the chart shows the lane's refusal until the data plane serves it. The surface line reads `option_surface`, which the chart's browser lane serves on crypto, CME and US stock charts ([Option surface](../core-concepts/data-sources.md#option-surface)). A daily or finer chart: the daily surface has no retention limit, the five-minute one keeps 30 days.

## When data is missing

The statistics start the day their job is switched on. The session line reads from the first session; the percentile, the z-score and the term distribution need **Minimum history** (60 sessions), and until then the pill and the panel's caption read "Not enough history yet: 12 of 60 sessions". A bar with no surface block draws nothing on either surface line; the card keeps the expiry it last read. 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

- **Another expiry.** Set **Expiry, days out** to 7 for the weekly or 90 for the quarter.
- **Further wings.** Switch **Surface delta** to 10 delta to read the tails.
- **Another year.** `lookback` sets the sessions the percentile, the z-score and the distribution read.

## Run it

1. In the editor's Explorer, press the **Templates** icon and pick **Skew History** under **Beyond the time axis**.
2. Press **Run** on a US stock's chart: the session line and the surface line fill as the statistics allow.
3. At the editor's Console prompt, type `rr30`, `rr_pct` and `partial_bars`.

## Concepts used

- [Option surface](../core-concepts/data-sources.md#option-surface) for `option_surface` on crypto, CME and US stock charts, and [The present mask and partial bars](../core-concepts/data-sources.md#the-present-mask-and-partial-bars) for its present mask and the partial bar
- [Data sources](../core-concepts/data-sources.md#us-listed-options-by-the-charts-underlying) for `option_daily`
- [Options kit](../functions/options-kit.md#a-history-of-daily-statistics) for `DayHistory`
- [Line panels](../presentation/cards-frames-panels.md#line-panels) for the term panel
