Exposure history
How option dealers' exposure on the chart's underlying has moved session by session, from the daily options statistics.
The Lens setting picks one greek's net exposure on the open-interest basis the statistics use (dealers long the calls and short the puts): gamma in USD per 1% move, delta in dollar delta (each option's own sign), vega in USD per volatility point, charm in dollar delta per day, vanna in dollar delta per volatility point. It is drawn as columns in a pane under the chart, green above zero and red below, with the front expiry's gamma as a step line on the gamma lens; the session's call wall, put wall, gamma flip and front max pain ride the price as step lines when Levels is on. A card at the bottom right reads the regime (long gamma dampens moves, short gamma amplifies them), the net exposure with its change from the prior session, its percentile among the window's earlier sessions, spot against the flip and the levels.
The parts are scalar option_daily inputs, the options kit's DayHistory for the change and the percentile (Options kit), sign-coloured columns and step lines (Plotting), and a render.hud card with a value tile that carries its change. This is also the exposure-history template: the Exposure History card under Beyond the time axis in the editor's starter list, and it compiles as written.
The wrun indicator
// Exposure History: how dealers' option exposure on the chart's underlying has moved session by session, from the
// daily options statistics (option_daily). The Lens setting picks one greek's net exposure on the open-interest basis
// the statistics use (dealers long the calls and short the puts): gamma in USD per 1% move, delta in dollar delta
// (each option's own sign), vega in USD per volatility point, charm in dollar delta per day, vanna in dollar delta per
// volatility point. It is drawn as columns in a pane under the chart, green above zero and red below, with the front
// expiry's gamma as a step line on the gamma lens; the session's call wall, put wall, gamma flip and front max pain are
// drawn on price as step lines when Levels is on. A card at the bottom right reads the regime (long gamma dampens
// moves, short gamma amplifies them), the net exposure, its change from the prior session, its percentile among the
// window's earlier sessions, spot against the flip and the levels. 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): the exposure and the levels read from the first session, the
// change from the second, and the percentile needs Minimum history (20 sessions); until then the card says how many
// sessions there are. US stock charts, daily or finer.
param.choice("lens", ["Gamma", "Delta", "Vega", "Charm", "Vanna"], "Gamma", { label: "Lens", description: "The greek whose net dealer exposure the columns draw" });
param.bool("show_levels", true, { label: "Levels", description: "Draw the session's call wall, put wall, gamma flip and front max pain on price" });
param.int("lookback", 252, { min: 20, max: 520, label: "Window, sessions", description: "Sessions the percentile reads: 252 is about a year" });
param.int("min_sessions", 20, { min: 5, max: 520, label: "Minimum history, sessions", description: "Sessions of statistics before the percentile shows" });
input("close", ohlcv.close); // the chart's own close: the grid
input("gex", option_daily.net_gex, { missing: "nan", description: "Net gamma exposure, USD per 1% move, one row per session (the daily options statistics)" }); // every daily input lands on the first bar of its day, NaN elsewhere
input("dex", option_daily.net_dex, { missing: "nan", description: "Net delta exposure, dollar delta, each option's own sign" });
input("vex", option_daily.net_vex, { missing: "nan", description: "Net vega exposure, USD per volatility point" });
input("charm", option_daily.net_charm, { missing: "nan", description: "Net charm, dollar delta per calendar day" });
input("vanna", option_daily.net_vanna, { missing: "nan", description: "Net vanna, dollar delta per volatility point" });
input("front_gex", option_daily.front_net_gex, { missing: "nan", description: "The nearest live expiry's net gamma exposure, USD per 1% move" });
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("flip", option_daily.gamma_flip, { missing: "nan", description: "The gamma flip, a price" });
input("max_pain", option_daily.max_pain_front, { missing: "nan", description: "The front expiry's max pain, a price" });
input("spot", option_daily.underlying_price, { missing: "nan", description: "The underlying's price the statistics read" });
output("call_wall_line", line, overlay, { color: "#22c55e", width: 1, step: true, line_style: "dashed", label: "Call wall", format: "price", description: "The session's call wall, held until the next session" });
output("put_wall_line", line, overlay, { color: "#ef4444", width: 1, step: true, line_style: "dashed", label: "Put wall", format: "price", description: "The session's put wall" });
output("flip_line", line, overlay, { color: "#f59e0b", width: 1, step: true, label: "Gamma flip", format: "price", description: "The session's gamma flip" });
output("max_pain_line", line, overlay, { color: "#a78bfa", width: 1, step: true, line_style: "dotted", label: "Max pain", format: "price", description: "The session's front-expiry max pain" });
output("exposure", histogram, lower, { color_by: "sign", colors: ["#ef4444", "#22c55e"], label: "Net exposure", format: "usd", description: "The lens's net dealer exposure for the session, in its unit (see the Lens setting)" });
output("front_line", line, lower, { color: "#e2e8f0", width: 1, step: true, label: "Front gamma", format: "usd", description: "The nearest expiry's net gamma exposure, gamma lens only" });
output("sign", none, lower, { description: "1 when the net exposure is at or above zero, 0 below" });
output("change", none, lower, { description: "The net exposure's change from the prior session, same unit" });
output("pctile", none, lower, { description: "The net exposure's percentile among the window's earlier sessions, 0 to 100" });
output("flip_dist", none, lower, { description: "Spot over the gamma flip, minus 1, percent: above zero spot is over the flip" });
output("regime", none, lower, { description: "Gamma lens: 1 long gamma (net at or above zero), 0 short gamma; other lenses the sign" });
output("wall_call", none, lower, { format: "price", description: "The call wall, for the card" });
output("wall_put", none, lower, { format: "price", description: "The put wall, for the card" });
output("pain", none, lower, { format: "price", description: "The front max pain, for the card" });
output("sessions", none, lower, { description: "Sessions of statistics the window holds" });
output("status", none, lower, { description: "0 not enough history for the percentile, 1 it reads, 2 a chart coarser than a day" });
string("read", { max_bytes: 64 }); // the headline
string("title", { max_bytes: 64 });
string("history_text", { max_bytes: 48 }); // "62nd percentile of 120 sessions" or the wait
render.hud("exposure_card", {
position: "bottom_right",
look: "terminal",
title: "{{title}}",
columns: 2,
tiles: [
tile.pill("Dealers", "read", { headline: true, color_by: "regime", colors: ["#ef4444", "#22c55e"] }),
tile.pill("Against its history", "history_text", { color_by: "status", colors: ["#94a3b8", "#e2e8f0", "#94a3b8"] }),
tile.value("Net exposure", "exposure", { format: "usd", delta: "change" }),
tile.rows([
["1) Change, prior session", "change", "usd"],
["2) Percentile", "pctile", "0"],
["3) Front expiry gamma", "front_line", "usd"],
["4) Spot against flip, %", "flip_dist", "0.00"],
["5) Call wall", "wall_call", "price"],
["6) Put wall", "wall_put", "price"],
["7) Max pain, front", "pain", "price"],
["8) Sessions read", "sessions", "int"],
]),
],
});
const DAY = 86400.0;
const CAPACITY = 520;
const LENS_WORDS: String[] = ["gamma", "delta", "vega", "charm", "vanna"]; // the lens in the card's words
const MONTHS: String[] = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
const history = new DayHistory(CAPACITY); // one net exposure per session, the lens's greek
const dateClock = new Clock("UTC");
let lens = 0; // settings, read in onStart()
let showLevels = true;
let lookback = 252;
let need = 20;
let prevT: f64 = NaN; // the bar spacing
let barSec: f64 = NaN;
let netNow: f64 = NaN; // the session readings, held
let changeNow: f64 = NaN;
let pctNow: f64 = NaN;
let frontNow: f64 = NaN;
let callWall: f64 = NaN;
let putWall: f64 = NaN;
let flip: f64 = NaN;
let pain: f64 = NaN;
let spot: f64 = NaN;
let levelsDay: f64 = NaN;
function onStart(): void {
lens = i32(p_lens());
showLevels = p_show_levels() > 0.5;
lookback = i32(p_lookback());
need = i32(p_min_sessions());
if (need > lookback) need = lookback;
}
function lensInput(): f64 {
if (lens == 1) return in_dex();
if (lens == 2) return in_vex();
if (lens == 3) return in_charm();
if (lens == 4) return in_vanna();
return in_gex();
}
function measure(): void {
netNow = history.value(0);
changeNow = history.count() >= 2 ? netNow - history.value(1) : NaN;
pctNow = history.span(lookback) >= need ? history.percentile(lookback) : NaN;
}
function sbUsd(v: f64): void { // "+$1.20M", "-$350.00K"
sb_text(v < 0.0 ? "-$" : "+$");
const a = Math.abs(v);
if (a >= 1.0e12) {
sb_f64(a / 1.0e12, 2);
sb_text("T");
} else if (a >= 1.0e9) {
sb_f64(a / 1.0e9, 2);
sb_text("B");
} else if (a >= 1.0e6) {
sb_f64(a / 1.0e6, 2);
sb_text("M");
} else if (a >= 1.0e3) {
sb_f64(a / 1.0e3, 2);
sb_text("K");
} else sb_f64(a, 2);
}
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 = history.span(lookback);
sb_clear();
if (coarse) sb_text("Needs a daily or finer chart");
else if (history.count() == 0) sb_text("Waiting for the daily options statistics");
else if (lens == 0) {
sb_text(netNow >= 0.0 ? "Long gamma, dealers dampen moves: " : "Short gamma, dealers amplify moves: ");
sbUsd(netNow);
} else {
sb_text("Net ");
sb_text(LENS_WORDS[lens]);
sb_text(" ");
sbUsd(netNow);
}
str_read_sb();
sb_clear();
if (coarse || history.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);
sb_text(" of ");
sb_int(held);
sb_text(" sessions");
}
str_history_text_sb();
sb_clear();
sb_text("Exposure history, ");
sb_text(LENS_WORDS[lens]);
if (!coarse && history.count() > 0) {
sb_text(", as of ");
sbDay(history.day(0));
}
str_title_sb();
}
// onBar() runs once per bar: the spacing, then the session's statistics when they land (the lens's exposure measured
// once, the levels kept); every bar writes the held readings, so an intraday chart draws each session as a step.
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 v = lensInput();
if (!isNaN(v) && history.push(day, v)) measure();
const landed = !isNaN(in_gex()) || !isNaN(in_call_wall()) || !isNaN(in_spot());
if (landed && day != levelsDay) { // the session's levels, once; a value the session lacks reads NaN
levelsDay = day;
frontNow = in_front_gex();
callWall = in_call_wall();
putWall = in_put_wall();
flip = in_flip();
pain = in_max_pain();
spot = in_spot();
}
}
const levels = showLevels && !coarse;
out_call_wall_line(levels ? callWall : NaN);
out_put_wall_line(levels ? putWall : NaN);
out_flip_line(levels ? flip : NaN);
out_max_pain_line(levels ? pain : NaN);
out_exposure(coarse ? NaN : netNow);
out_front_line(!coarse && lens == 0 ? frontNow : NaN);
out_sign(coarse || isNaN(netNow) ? NaN : netNow >= 0.0 ? 1.0 : 0.0);
out_change(coarse ? NaN : changeNow);
out_pctile(coarse ? NaN : pctNow);
out_flip_dist(!coarse && spot > 0.0 && flip > 0.0 ? (spot / flip - 1.0) * 100.0 : NaN);
out_regime(coarse || isNaN(netNow) ? NaN : netNow >= 0.0 ? 1.0 : 0.0);
out_wall_call(coarse ? NaN : callWall);
out_wall_put(coarse ? NaN : putWall);
out_pain(coarse ? NaN : pain);
out_sessions(coarse ? NaN : f64(history.span(lookback)));
out_status(coarse ? 2.0 : history.span(lookback) >= need ? 1.0 : 0.0);
if (bar.isLast()) writeWords(coarse);
}How it works
One reading per session. Each field lands on the session's own bar (every bar of a daily chart, the first bar of its day on an intraday chart) and reads NaN elsewhere. The lens's value joins a DayHistory; the levels are taken once per session, and a level the session lacks reads empty rather than the previous session's.
The regime. On the gamma lens the sign of net gamma says what dealers' hedging does: positive, they sell rallies and buy drops (moves dampen); negative, they chase both (moves extend). The other lenses read their own sign.
The change and the percentile. The change is the newest session's net exposure minus the one before it; the percentile is the share of the window's earlier sessions below today's.
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). 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 exposure and the levels read from the first session, the change from the second, and the percentile needs Minimum history (20 sessions): until then the pill reads "Not enough history yet: 7 of 20 sessions". 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 greek. Switch Lens to Delta, Vega, Charm or Vanna.
- A clean price pane. Turn Levels off.
- Another year.
lookbacksets the sessions the percentile reads.
Run it
- In the editor's Explorer, press the Templates icon and pick Exposure History under Beyond the time axis.
- Press Run on a US stock's daily chart.
- At the editor's Console prompt, type
exposure,changeandpctile.
Concepts used
- Data sources for
option_dailyand its units - Options kit for
DayHistory - Plotting for the columns and the step lines
- HUD cards for the card