Risk reversal skew
How the chart's own option chain prices its two wings for one expiry, the one nearest 30 days out by default.
The risk reversal is the call's implied volatility minus the put's at the same distance from the money, in vol points: below zero the puts are richer, so downside protection costs more than upside calls; above zero the calls are. Bars under the chart read it at nine deltas, from 27.5 near the money out to 7.5 in the wings, sky where the call side is richer and violet where the put side is, with the verdict as a chip on the title row. A card at the bottom of the pane, in the terminal look, reads the skew in one plain line first ("Puts rich: the downside wing costs more"), then the 25 and 10 delta risk reversals, the four wing IVs, the at-the-money IV and the smile's asymmetry, titled by the expiry and its days to go (20NOV, 36 days, call minus put). This page reads call minus put; some tools quote the same number as put minus call, with the sign flipped.
The parts are a celled options_chain.cells input read on the live bar (Data sources), the options kit's IvTerm for the expiry nearest a horizon and the IV at a target delta on each side, with chainPricedAtMs and optionsTradeDay for the chain's clock and the expiry's date (Options kit), a frame feeding panel.bars under the chart with a per-run title and badge (Cards, frames and panels), and a render.hud card with look: "terminal" over data-only outputs and two text slots (HUD cards). This is also the skew template: the Risk Reversal Skew card under HUDs in the editor's starter list, and it compiles as written.
The wrun indicator
// Risk Reversal Skew: how the chart's own option chain prices its two wings for one expiry, the one nearest 30 days
// out by default. The risk reversal is the call's implied volatility minus the put's at the same distance from the
// money, in vol points: below zero the puts are richer (downside protection costs more), above zero the calls. Bars
// under the chart read it at nine deltas, from 27.5 near the money to 7.5 in the wings; a card in the terminal look
// reads the skew in one plain line first (Puts rich: the downside wing costs more), then the 25 and 10 delta risk
// reversals, the four wing IVs, the at-the-money IV and the smile's asymmetry. Each wing is the strike where the
// contract's delta, read off its own IV at the expiry's forward, equals the target, interpolated between the two listed
// strikes around it. This reads call minus put; some tools quote put minus call, the same number with the sign
// flipped. Where the expiry lists no 25 delta wing, the asymmetry falls back to the calls' mean IV within 7% above spot
// minus the puts' within 7% below. The chain rides the live bar only: history bars draw nothing.
section("Expiry");
param.number("days", 30, { min: 1, max: 400, step: 1, label: "Days to expiry", description: "The expiry read: the one whose time to expiry is nearest this many calendar days; an expiry settling today never counts" });
param.number("flat_points", 1, { min: 0, max: 10, step: 0.25, label: "Flat band, vol points", description: "The headline reads flat while the risk reversal sits within this many vol points of zero" });
param.number("rate_pct", 4, { min: 0, max: 20, step: 0.25, label: "Rate, percent", description: "The rate that carries a US stock's spot to the expiry's forward; a futures or crypto chain quotes its forward already" });
input("close", ohlcv.close); // the chart's own close: spot where a contract reports no underlying
input("chain", options_chain.cells, { max_cells: 4000, venue: "auto", description: "The chart's own option chain: OPRA on a US stock, CME on futures, Deribit on BTC and ETH" }); // [strike, expiry_ms, side, oi, gamma, delta, mark_iv, underlying, multiplier, vega] per contract
market.zone(); // the market's clock: 0 UTC (crypto), 1 New York (US stocks), 2 Chicago (CME): the expiry's date and the forward
// The readings, data-only: the card's tiles read them and the Console can too. Each is NaN on history bars.
output("rr25", none, overlay, { description: "25 delta risk reversal, the call's IV minus the put's, vol points (below zero: puts rich); live bar only" });
output("rr10", none, overlay, { description: "10 delta risk reversal, the call's IV minus the put's, vol points" });
output("call25_iv", none, overlay, { color: "#38bdf8", description: "The 25 delta call's IV, percent" });
output("put25_iv", none, overlay, { color: "#a78bfa", description: "The 25 delta put's IV, percent" });
output("call10_iv", none, overlay, { color: "#38bdf8", description: "The 10 delta call's IV, percent" });
output("put10_iv", none, overlay, { color: "#a78bfa", description: "The 10 delta put's IV, percent" });
output("atm_iv", none, overlay, { description: "The expiry's at-the-money IV, percent: the mean of the call and put IV at the strike nearest its forward" });
output("asymmetry", none, overlay, { description: "Smile asymmetry, vol points: the 25 delta risk reversal, or where the expiry lists no 25 delta wing, the calls' mean IV within 7% above spot minus the puts' within 7% below" });
output("asymmetry_fallback", none, overlay, { description: "1 when the asymmetry is the 7% fallback, 0 when it is the 25 delta risk reversal" });
output("expiry_days", none, overlay, { description: "Days to the expiry read, on the market's own calendar" });
output("tone", none, overlay, { description: "The headline's colour: 0 puts rich, 1 flat or no reading, 2 calls rich; live bar only" });
string("read", { max_bytes: 64 }); // the headline: the skew in one plain line, or why there is no reading
string("title", { max_bytes: 48 }); // the card's title: the expiry, its days to go and the convention
const rr_rows = frame("rr_rows", { max_bytes: 2048 }); // the bars: [delta, call minus put] per delta, near the money first
// Under the chart: one bar per delta, sky where the call side is richer, violet where the put side is, the verdict as
// the chip on the title row (written per run into the frame with the expiry in the title).
panel.bars({ name: "rr_by_delta", title: "Call minus put by delta", x: "category", place: "below", frame: rr_rows, orientation: "vertical", chrome: "grid", color_mode: "sign", positive_color: "#38bdf8", negative_color: "#a78bfa", series: [{ name: "Call minus put" }], format: "0.00", unit: "pts", x_title: "Delta", y_title: "Vol points", hover_card: true, legend_style: "title", height_frac: 0.18 });
// The card, in the terminal look: the plain line leads as the headline, coloured by the side that is rich (violet for
// the puts, slate for flat or no reading, sky for the calls), the title names the expiry and the convention.
render.hud("skew", {
position: "bottom_center",
look: "terminal",
title: "{{title}}",
columns: 2,
tiles: [
tile.pill("Skew", "read", { headline: true, color_by: "tone", colors: ["#a78bfa", "#94a3b8", "#38bdf8"] }),
tile.value("25 delta, call minus put", "rr25", { format: "0.00" }),
tile.value("10 delta, call minus put", "rr10", { format: "0.00" }),
tile.rows([
["1) 25 delta call IV", "call25_iv", "0.00"],
["2) 25 delta put IV", "put25_iv", "0.00"],
["3) 10 delta call IV", "call10_iv", "0.00"],
["4) 10 delta put IV", "put10_iv", "0.00"],
["5) At-the-money IV", "atm_iv", "0.00"],
["6) Smile asymmetry", "asymmetry", "0.00"],
]),
],
});
const TUPLE = 10; // f64s per contract: strike, expiry_ms, side, oi, gamma, delta, mark_iv, underlying, multiplier, vega
const MAX_EXPIRIES = 64; // live expiries the term keeps, the nearest when the chain lists more
const WING_SHARE = 0.07; // the asymmetry fallback's strikes: within 7% of spot
const DELTAS: f64[] = [0.275, 0.25, 0.225, 0.2, 0.175, 0.15, 0.125, 0.1, 0.075]; // the bars, near the money first
const DELTA_LABELS: String[] = ["27.5", "25", "22.5", "20", "17.5", "15", "12.5", "10", "7.5"];
const MONTHS: String[] = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
const dateClock = new Clock("UTC"); // reads a date from its day number
let term = new IvTerm(1); // the chain's expiries and their wings, built in onStart()
let days = 30.0; // settings, read in onStart()
let flatPoints = 1.0;
let rate = 0.04;
let zone = 0;
let row = -1; // the expiry read on the live bar; -1 when there is none
let rr25: f64 = NaN; // the readings, measured on the live bar
let rr10: f64 = NaN;
let call25: f64 = NaN;
let put25: f64 = NaN;
let call10: f64 = NaN;
let put10: f64 = NaN;
let atm: f64 = NaN;
let asymmetry: f64 = NaN;
let fallback = false;
function onStart(): void {
term = new IvTerm(MAX_EXPIRIES); // the per-expiry buffers are the module's only allocation
days = p_days();
flatPoints = p_flat_points();
zone = i32(p_market_zone());
rate = zone == 1 ? p_rate_pct() / 100.0 : 0.0; // a futures or crypto underlying already is the forward
}
// ── The expiry's wings ──
function riskReversal(e: i32, delta: f64): f64 { // the call's IV minus the put's at |delta|, vol points; NaN where a wing is missing
return (term.deltaIv(e, 1.0, delta) - term.deltaIv(e, -1.0, delta)) * 100.0;
}
function wingAsymmetry(cells: StaticArray<f64>, n: i32, e: i32): f64 { // the calls' mean IV within 7% above spot minus the puts' within 7% below, vol points
const expiry = term.expiry(e);
const spot = term.underlying(e);
let callSum = 0.0;
let callCount = 0;
let putSum = 0.0;
let putCount = 0;
for (let i = 0; i + TUPLE - 1 < n; i += TUPLE) {
const strike = cells[i];
if (cells[i + 1] != expiry || !(strike > 0.0) || !(cells[i + 6] > 0.0)) continue;
if (Math.abs(strike / spot - 1.0) > WING_SHARE) continue;
if (cells[i + 2] > 0.0) {
if (strike > spot) {
callSum += cells[i + 6]; // mark IV rides the chain in vol points already
callCount += 1;
}
} else if (strike < spot) {
putSum += cells[i + 6];
putCount += 1;
}
}
if (callCount == 0 || putCount == 0) return NaN;
return callSum / f64(callCount) - putSum / f64(putCount);
}
function measure(cells: StaticArray<f64>, n: i32, e: i32): void {
rr25 = riskReversal(e, 0.25);
rr10 = riskReversal(e, 0.1);
call25 = term.deltaIv(e, 1.0, 0.25) * 100.0;
put25 = term.deltaIv(e, -1.0, 0.25) * 100.0;
call10 = term.deltaIv(e, 1.0, 0.1) * 100.0;
put10 = term.deltaIv(e, -1.0, 0.1) * 100.0;
atm = term.atmIv(e) * 100.0;
fallback = isNaN(rr25);
asymmetry = fallback ? wingAsymmetry(cells, n, e) : rr25;
}
// ── The bars and the card's words ──
function writeDate(e: i32): void { // the expiry's trade date as 21NOV, into the frame buffer or the string builder
dateClock.update(optionsTradeDay(term.expiry(e), zone) * 86400.0); // a day number is its date at 00:00 UTC
}
function writeBars(e: i32): void {
writeDate(e);
fb_clear();
fb_text("{\"title\":\"Call minus put by delta, ");
if (dateClock.dayOfMonth() < 10) fb_text("0");
fb_int(dateClock.dayOfMonth());
fb_text(MONTHS[dateClock.month() - 1]);
fb_text("\",\"badge\":{\"text\":");
fb_str(isNaN(asymmetry) ? "No reading" : asymmetry < -flatPoints ? "Puts rich" : asymmetry > flatPoints ? "Calls rich" : "Flat");
fb_text(",\"color\":");
fb_str(isNaN(asymmetry) ? "#94a3b8" : asymmetry < -flatPoints ? "#a78bfa" : asymmetry > flatPoints ? "#38bdf8" : "#94a3b8");
fb_text("},\"rows\":[");
for (let k = 0; k < DELTAS.length; k += 1) {
if (k > 0) fb_text(",");
fb_text("[");
fb_str(DELTA_LABELS[k]);
fb_text(",");
fb_num(riskReversal(e, DELTAS[k])); // null where a wing is missing
fb_text("]");
}
fb_text("]}");
writeFrameBuffer(rr_rows);
}
function writeWords(e: i32): void {
sb_clear();
if (isNaN(asymmetry)) sb_text("No wings to read on this expiry");
else if (asymmetry < -flatPoints) sb_text("Puts rich: the downside wing costs more");
else if (asymmetry > flatPoints) sb_text("Calls rich: the upside wing costs more");
else sb_text("Flat: both wings cost about the same");
str_read_sb();
writeDate(e);
sb_clear();
if (dateClock.dayOfMonth() < 10) sb_text("0");
sb_int(dateClock.dayOfMonth());
sb_text(MONTHS[dateClock.month() - 1]);
sb_text(", ");
const left = i64(term.days(e));
sb_int(left);
sb_text(left == 1 ? " day, call minus put" : " days, call minus put");
str_title_sb();
}
// onBar() runs once per bar: history rows carry an empty block and cost one read; the live bar loads the chain into the
// term, picks the expiry nearest the setting's days and reads its wings. Then the readings (NaN on history rows), the
// bars and the card's words on the live bar only.
function onBar(): void {
const close = bar.close();
const t = bar.time();
row = -1;
let chainCells = -1;
let kept = 0;
if (bar.isLast() && !isNaN(close) && !isNaN(t)) {
chainCells = in_chain_cells();
if (chainCells >= TUPLE) {
const cells = in_chain_view(); // the live chain in place: the first n values of the build's own buffer
const nowMs = chainPricedAtMs(cells, chainCells, t * 1000.0); // when the chart priced the chain
kept = term.load(cells, chainCells, nowMs, close, zone, rate);
row = term.nearest(days); // an expiry after today with an at-the-money IV, nearest the setting's days
if (row >= 0) measure(cells, chainCells, row);
}
}
const reading = row >= 0;
out_rr25(reading ? rr25 : NaN);
out_rr10(reading ? rr10 : NaN);
out_call25_iv(reading ? call25 : NaN);
out_put25_iv(reading ? put25 : NaN);
out_call10_iv(reading ? call10 : NaN);
out_put10_iv(reading ? put10 : NaN);
out_atm_iv(reading ? atm : NaN);
out_asymmetry(reading ? asymmetry : NaN);
out_asymmetry_fallback(reading && !isNaN(asymmetry) ? (fallback ? 1.0 : 0.0) : NaN);
out_expiry_days(reading ? term.days(row) : NaN);
out_tone(reading && !isNaN(asymmetry) ? (asymmetry < -flatPoints ? 0.0 : asymmetry > flatPoints ? 2.0 : 1.0) : bar.isLast() ? 1.0 : NaN);
if (reading) {
writeBars(row);
writeWords(row);
} else if (bar.isLast()) {
// Nothing to read on the live bar: the headline says why, so the card is never a row of blanks.
sb_clear();
sb_text(chainCells < TUPLE ? "Waiting for the options chain" : kept == 0 ? "No live expiry in the chain" : "No expiry with an at-the-money IV");
str_read_sb();
sb_clear();
sb_text("Skew, no chain read");
str_title_sb();
}
}How it works
One expiry, picked by its distance. The chain loads into an IvTerm on the live bar, and term.nearest(days) picks the expiry whose time to expiry is nearest the Days to expiry setting (30), among the expiries after today that carry an at-the-money IV: an expiry settling today never counts, because its implied volatility swings with the hours left. The card's title names it by its date on the market's own calendar (optionsTradeDay()) and its days to go.
A wing is a strike at a delta. For each target, 0.25 for the 25 delta wing, term.deltaIv(e, side, delta) finds the strike on the calls (or the puts) where the contract's delta, read off its own IV at the expiry's forward with no discount (N(d1) for a call, N(-d1) for a put, the convention skew is quoted in), equals the target, and interpolates the IV linearly in delta between the two listed strikes around it. The forward is the underlying times e^(rate * years) on a US stock, at the Rate setting (4%), and the underlying itself on futures and crypto, which already quote the forward. A target the expiry lists no strike beyond reads -: a narrow chain has no 10 delta wing.
The risk reversal is call minus put. rr25 is the 25 delta call's IV minus the 25 delta put's, rr10 the same at 10 delta, both in vol points; call25_iv, put25_iv, call10_iv and put10_iv are the four wings in percent, and atm_iv the expiry's at-the-money IV (the mean of the call and put IV at the strike nearest its forward). On most equity and index chains the puts are richer and the number is negative, steeper at 10 delta than at 25; a positive number means the market pays more for upside calls than for downside puts.
The smile's asymmetry. asymmetry is the 25 delta risk reversal. Where the expiry lists no 25 delta wing on one side, it falls back to the calls' mean IV over the strikes up to 7% above spot minus the puts' mean IV over the strikes up to 7% below, in vol points, and asymmetry_fallback reads 1.
The headline. Within the Flat band (1 vol point) of zero the card reads "Flat: both wings cost about the same"; below it, "Puts rich: the downside wing costs more"; above it, "Calls rich: the upside wing costs more". tone carries the same three answers as 0, 1 and 2 and colours the headline violet, slate or sky.
The bars. One bar per delta under the chart, 27.5 nearest the money on the left out to 7.5 on the right, each the call's IV minus the put's at that delta; the frame carries the expiry in the panel's title and the headline's word as the chip beside it.
Where it runs
Charts of a coin Deribit lists options for (BTC, ETH, SOL and the rest of its list): that is where the copy you run from the editor reads a chain. On a US stock chart venue: "auto" reads the chart's own OPRA chain, and on a CME futures chart the chain of its root; the chart serves both to OpenMarket's official wrun indicators and discloses the source in the legend. A copy you run from the editor or publish yourself does not read them yet.
When data is missing
The headline says why instead of the card going blank: "Waiting for the options chain" while the live bar carries none, "No live expiry in the chain" when every listed expiry has passed, "No expiry with an at-the-money IV" when the only live expiries settle today or carry no implied volatility near the money, and "No wings to read on this expiry" when neither the 25 delta wing nor the 7% fallback has a strike on both sides. A wing the expiry does not reach reads - on its row and leaves its bar out.
Customize it
- Another horizon. Set Days to expiry to 7 for the weekly smile or 90 for the quarterly; the title names the expiry it reads.
- A tighter verdict. Lower the Flat band to call a smaller tilt rich, or raise it to keep the headline flat through ordinary equity skew.
- The other convention. Negate
rr25,rr10and the bars where you want put minus call; the risk reversal is the same number with its sign flipped.
Run it
- In the editor's Explorer, press the Templates icon ("Browse starter templates") and pick Risk Reversal Skew under HUDs.
- Press Run on a BTC chart: the bars stand under the chart once the chain arrives and the terminal card appears at the bottom centre, titled by the expiry nearest 30 days.
- At the editor's Console prompt, type
rr25andasymmetryto read the 25 delta risk reversal and the smile's asymmetry on the live bar.
Concepts used
- Data sources for the
options_chaincelled class, its tuple and thevenueword - Options kit for
IvTerm,nearest,deltaIvand the expiry's forward, andchainPricedAtMsandoptionsTradeDay - Cards, frames and panels for frames and
panel.barswith a per-run title and badge - HUD cards, Blocks and tiles and Looks for the card, its tiles and the slot in the title