---
title: "Volatility regime"
description: "Whether the chart's underlying's implied volatility is high, normal or low for that name, and how unsettled it is, from the daily options statistics. The…"
order: 152
section: "cookbook"
---

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

# Volatility regime

Whether the chart's underlying's implied volatility is high, normal or low for that name, and how unsettled it is, from the daily options statistics. The regime score is a z-score: the session's 30-day IV against the mean and standard deviation of the window's earlier sessions, high over +0.5, low under -0.5, neutral between, drawn as columns in a pane under the chart with guides at both edges. The stress class bins the IV's percentile among the window's earlier sessions: calm under the 55th, transitional from the 55th to the 80th, stressed from the 80th up, and the candles take its colour (teal, amber, red) when **Tint candles** is on. The vol of vol is how much the IV itself moves: the exponentially weighted standard deviation of its daily log changes over the last 30 sessions, the newest weighing most, annualized over 252 days, in percent, with its own percentile. A card at the bottom right reads the regime, the stress class and the numbers behind them.

The parts are a scalar `option_daily` input, two `DayHistory` rings from the options kit (the IV and the vol of vol, [Options kit](../functions/options-kit.md#a-history-of-daily-statistics)), regime-coloured columns and guides, a `render.barcolor` tint ([Plotting](../presentation/plotting.md)), and a `render.hud` card with a sparkline tile. This is also the `vol-regime` template: the **Volatility Regime** card under **Beyond the time axis** in the editor's starter list, and it compiles as written.

## The wrun indicator

```typescript sample=vol-regime
// Volatility Regime: whether the chart's underlying's implied volatility is high, normal or low for that name, and
// how unsettled it is, from the daily options statistics (option_daily, the 30-day at-the-money IV per session). The
// regime score is a z-score: the session's IV against the mean and standard deviation of the window's earlier
// sessions, high over +0.5, low under -0.5, neutral between, drawn as columns in a pane under the chart with guides at
// both edges. The stress class bins the IV's percentile among the window's earlier sessions: calm under the 55th,
// transitional from the 55th to the 80th, stressed from the 80th up, and the candles take its colour (teal, amber,
// red) when Tint candles is on. The vol of vol is how much the IV itself moves: the exponentially weighted standard
// deviation of its daily log changes over the last Vol of vol sessions (recent changes weigh more), annualized over
// 252 days, in percent, with its own percentile. A card at the bottom right reads the regime, the stress class and the
// numbers behind them. Each session's reading is the statistics job's close of that session, held on an intraday
// chart until the next one lands. The statistics are short today (they start when the data plane switches the daily
// job on): every reading needs Minimum history (60 sessions), and until then the card says how many there are. US
// stock charts, daily or finer.

param.int("lookback", 252, { min: 20, max: 520, label: "Window, sessions", description: "Sessions the score, the percentiles and the stress class read: 252 is about a year" });
param.int("min_sessions", 60, { min: 10, max: 520, label: "Minimum history, sessions", description: "Sessions of statistics before a reading shows; below it the card says how many there are" });
param.int("vov_days", 30, { min: 5, max: 90, label: "Vol of vol, sessions", description: "Daily IV changes the vol of vol weighs, the newest weighing most" });
param.bool("tint_candles", true, { label: "Tint candles", description: "Colour the candles by the stress class: teal calm, amber transitional, red stressed" });
input("close", ohlcv.close); // the chart's own close: the grid
input("iv30", option_daily.atm_iv_30d, { missing: "nan", description: "The underlying's 30-day constant-maturity at-the-money IV, percent, one row per session (the daily options statistics)" }); // lands on the first bar of its day, NaN elsewhere
output("score", histogram, lower, { color_by: "regime", colors: ["#2dd4bf", "#94a3b8", "#ef4444"], label: "Regime score", format: "0.00", description: "The session's 30-day IV against the window's earlier sessions, in standard deviations; empty below Minimum history" });
output("high_guide", line, lower, { role: "guide", color: "#ef444480", line_style: "dashed", label: "High", align: "right", description: "+0.5: over it the regime is high" });
output("low_guide", line, lower, { role: "guide", color: "#2dd4bf80", line_style: "dashed", label: "Low", align: "right", description: "-0.5: under it the regime is low" });
output("regime", none, lower, { description: "0 low (score under -0.5), 1 neutral, 2 high (over +0.5); 1 while there is no score" });
output("stress", none, lower, { description: "0 calm (IV percentile under 55), 1 transitional (55 to 80), 2 stressed (80 and up); NaN while there is no percentile" });
output("tint_on", none, lower, { description: "1 on a bar the candles take the stress colour: the tint's gate" });
output("iv", none, lower, { description: "The newest session's 30-day at-the-money IV, percent" });
output("iv_pct", none, lower, { description: "The newest IV's percentile among the window's earlier sessions, 0 to 100" });
output("vov", none, lower, { description: "The vol of vol: the weighted deviation of the daily IV changes, annualized, percent" });
output("vov_pct", none, lower, { description: "The newest vol of vol's percentile among the window's earlier sessions, 0 to 100" });
output("sessions", none, lower, { description: "Sessions of statistics the window holds" });
output("status", none, lower, { description: "0 not enough history yet, 1 the regime reads, 2 a chart coarser than a day" });
string("read", { max_bytes: 64 }); // the headline
string("stress_text", { max_bytes: 32 }); // "Stressed, 86th percentile"
string("title", { max_bytes: 64 });
render.barcolor("stress_tint", { where: "tint_on", color_by: "stress", colors: ["#2dd4bf", "#f59e0b", "#ef4444"] }); // the candles in the stress colour

render.hud("regime_card", {
  position: "bottom_right",
  look: "terminal",
  title: "{{title}}",
  columns: 2,
  tiles: [
    tile.pill("Regime", "read", { headline: true, color_by: "regime", colors: ["#2dd4bf", "#94a3b8", "#ef4444"] }),
    tile.pill("Stress", "stress_text", { color_by: "stress", colors: ["#2dd4bf", "#f59e0b", "#ef4444"] }),
    tile.spark("Vol of vol", "vov", { bars: 64 }),
    tile.rows([
      ["1) IV, 30 days", "iv", "0.00"],
      ["2) Regime score", "score", "0.00"],
      ["3) IV percentile", "iv_pct", "0"],
      ["4) Vol of vol", "vov", "0.0"],
      ["5) Vol of vol percentile", "vov_pct", "0"],
      ["6) Sessions read", "sessions", "int"],
    ]),
  ],
});

const DAY = 86400.0;
const YEAR_DAYS = 252.0;
const HIGH = 0.5; // the score's edges
const LOW = -0.5;
const TRANSITIONAL = 55.0; // the stress class's percentile edges
const STRESSED = 80.0;
const CAPACITY = 520;
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 vovs = new DayHistory(CAPACITY); // one vol of vol per session it could be measured
const dateClock = new Clock("UTC");

let lookback = 252; // settings, read in onStart()
let need = 60;
let vovDays = 30;
let tint = true;
let prevT: f64 = NaN; // the bar spacing
let barSec: f64 = NaN;
let scoreNow: f64 = NaN; // the session readings, held
let ivPctNow: f64 = NaN;
let vovNow: f64 = NaN;
let vovPctNow: f64 = NaN;

function onStart(): void {
  lookback = i32(p_lookback());
  need = i32(p_min_sessions());
  if (need > lookback) need = lookback;
  vovDays = i32(p_vov_days());
  tint = p_tint_candles() > 0.5;
}

/** The vol of vol over the newest vovDays daily log changes of the IV, each weighing (1 - a)^k with k sessions back
 *  and a = 2 / (vovDays + 1), annualized, percent; NaN until vovDays + 1 sessions are held. */
function volOfVol(): f64 {
  if (ivs.count() < vovDays + 1) return NaN;
  const keep = 1.0 - 2.0 / f64(vovDays + 1);
  let weight = 1.0;
  let sumW = 0.0;
  let sumSq = 0.0;
  for (let k = 0; k < vovDays; k += 1) {
    const r = Math.log(ivs.value(k) / ivs.value(k + 1));
    sumSq += weight * r * r;
    sumW += weight;
    weight *= keep;
  }
  return Math.sqrt((sumSq / sumW) * YEAR_DAYS) * 100.0;
}

function measure(day: f64): void {
  const v = volOfVol();
  if (!isNaN(v)) vovs.push(day, v);
  vovNow = v;
  const ready = ivs.span(lookback) >= need;
  scoreNow = ready ? ivs.zscore(lookback) : NaN;
  ivPctNow = ready ? ivs.percentile(lookback) : NaN;
  vovPctNow = ready && !isNaN(v) && vovs.span(lookback) >= 2 ? vovs.percentile(lookback) : NaN;
}

function regimeOf(): f64 {
  if (isNaN(scoreNow)) return 1.0;
  return scoreNow > HIGH ? 2.0 : scoreNow < LOW ? 0.0 : 1.0;
}

function stressOf(): f64 {
  if (isNaN(ivPctNow)) return NaN;
  return ivPctNow >= STRESSED ? 2.0 : ivPctNow >= TRANSITIONAL ? 1.0 : 0.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 writeWords(coarse: bool): void {
  const held = ivs.span(lookback);
  sb_clear();
  if (coarse) sb_text("Needs a daily or finer chart");
  else if (ivs.count() == 0) 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(scoreNow)) sb_text("No spread: the IV has not moved in the window");
  else {
    const r = regimeOf();
    sb_text(r == 2.0 ? "High regime, score " : r == 0.0 ? "Low regime, score " : "Neutral regime, score ");
    if (scoreNow >= 0.0) sb_text("+");
    sb_f64(scoreNow, 2);
  }
  str_read_sb();
  sb_clear();
  const s = stressOf();
  if (coarse || isNaN(s)) sb_text("No stress class");
  else {
    sb_text(s == 2.0 ? "Stressed, " : s == 1.0 ? "Transitional, " : "Calm, ");
    sb_f64(ivPctNow, 0);
    sb_text(" percentile");
  }
  str_stress_text_sb();
  sb_clear();
  sb_text("Volatility regime, 30-day IV");
  if (!coarse && ivs.count() > 0) {
    sb_text(", as of ");
    sbDay(ivs.day(0));
  }
  str_title_sb();
}

// onBar() runs once per bar: the spacing, then the session's IV when it lands (measured once, held after); every bar
// writes the held readings, and the candle takes the stress colour.
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;
  const iv = in_iv30();
  if (!coarse && !isNaN(iv) && iv > 0.0) {
    const day = Math.floor(t / DAY);
    if (ivs.push(day, iv)) measure(day);
  }
  out_high_guide(HIGH);
  out_low_guide(LOW);
  out_score(coarse ? NaN : scoreNow);
  out_regime(coarse ? 1.0 : regimeOf());
  const s = coarse ? NaN : stressOf();
  out_stress(s);
  out_tint_on(tint && !isNaN(s) ? 1.0 : 0.0);
  out_iv(coarse || ivs.count() == 0 ? NaN : ivs.value(0));
  out_iv_pct(coarse ? NaN : ivPctNow);
  out_vov(coarse ? NaN : vovNow);
  out_vov_pct(coarse ? NaN : vovPctNow);
  out_sessions(coarse ? NaN : f64(ivs.span(lookback)));
  out_status(coarse ? 2.0 : ivs.span(lookback) >= need ? 1.0 : 0.0);
  if (bar.isLast()) writeWords(coarse);
}
```

## How it works

**The score.** `zscore()` reads the newest IV against the window's earlier sessions, so today never dampens its own score. The edges are fixed at plus and minus 0.5 standard deviations.

**The stress class.** `percentile()` is the share of the window's earlier sessions with a lower IV; the class edges at the 55th and 80th percentiles colour the candles through a gate output, so turning **Tint candles** off leaves the chart's own colours.

**The vol of vol.** Over the newest `vov_days` daily log changes of the IV, each weighing (1 - a) to the power of its age in sessions with a = 2 / (`vov_days` + 1): recent changes count most. It is measured once per session and joins its own `DayHistory` for its percentile.

## 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 ([Limitations](../reference/limitations.md)). The class is declared ahead of its lane, 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 score, the percentile and the stress tint need **Minimum history** (60 sessions); until then the columns stay empty, the candles keep their colours and the card reads "Not enough history yet: 12 of 60 sessions". The vol of vol needs 31 sessions of its own. A window whose IV never moved has no spread and says so; 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 year.** `lookback` sets how many sessions the score and the percentiles read.
- **A faster or slower vol of vol.** `vov_days` of 10 reacts within two weeks, 90 over a quarter.
- **Keep the candles.** Turn **Tint candles** off to read the regime in the pane alone.

## Run it

1. In the editor's Explorer, press the **Templates** icon and pick **Volatility Regime** under **Beyond the time axis**.
2. Press **Run** on a US stock's daily chart.
3. At the editor's Console prompt, type `score`, `iv_pct` and `vov`.

## Concepts used

- [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`
- [Plotting](../presentation/plotting.md) for the columns, the guides and the candle tint
- [HUD cards](../presentation/hud-and-hover-cards.md#hud-cards) for the card and its sparkline
