Contract odds
One option contract of the chart's own chain, held to its expiry, read as odds. A card at the bottom of the pane, in the terminal look, answers one question first: what are the odds this trade makes money, and how much of a bankroll would Kelly put on it ("Long 660 call: 41.2% to profit, half Kelly 6.2%")?
Under the answer the card reads the probability of profit at expiry, the breakeven, the premium and where it came from, the most the position can make and lose per contract, the odds ratio and Kelly's three fractions: full, half and quarter. The chart does not tell an indicator which contract it shows, so the settings name it: the strike, the expiry date, call or put, long or short, and the premium.
The parts are a celled options_chain.cells input read on the live bar (Data sources), the options kit's Greeks, normCdf, chainPricedAtMs and optionsTradeDay for the model price, the probability, the chain's clock and the calendar (Options kit), a price and a date setting that name the contract (Kinds of setting), and a render.hud card with look: "terminal" over data-only outputs and five text slots (HUD cards). This is also the contract-odds template: the Contract Odds card under HUDs in the editor's starter list, and it compiles as written.
The wrun indicator
// Contract Odds: one option contract of the chart's own chain held to its expiry, read as odds. A card at the bottom
// of the pane, in the terminal look, answers one question first: what are the odds this trade makes money, and how
// much of a bankroll would Kelly put on it? Under the answer: the probability of profit at expiry, the breakeven, the
// premium and where it came from, the most the position can make and lose per contract, the odds ratio and the Kelly
// fractions (full, half and quarter).
// The chart does not tell an indicator which contract it shows, so the settings name it: the strike (0 = the strike
// nearest the expiry's forward), the expiry (0 = the first expiry after today), call or put, long or short, and the
// premium (0 = the chart's close on the contract's own chart, else the model price from the contract's IV). The
// probability is lognormal from the contract's IV, with a risk-neutral drift at the rate on a stock (none on a futures
// or crypto forward). Kelly is p - (1 - p) / odds, odds the most it can make over the most it can lose: an unlimited
// gain reads p, an unlimited loss no edge; full Kelly here takes no volatility adjustment (1). The chain rides the
// live bar only: history bars draw nothing, and everything is measured on the last bar.
section("Contract");
param.price("strike", 0.0, { label: "Strike", description: "The contract's strike; 0 = the listed strike nearest the expiry's forward" });
param.time("expiry", 0, { label: "Expiry date", description: "The contract's expiry date; 0 = the first expiry after today" });
param.choice("type", ["call", "put"], "call", { label: "Type" });
param.choice("position", ["long", "short"], "long", { label: "Position", description: "long: bought, paying the premium; short: sold, collecting it" });
param.number("premium", 0, { min: 0, label: "Premium", description: "The price per unit paid or received; 0 = the chart's close on the contract's own chart, else the model price from its IV" });
param.number("rate_pct", 4, { min: 0, max: 20, step: 0.25, label: "Rate, percent", description: "The rate the model price and the stock's drift use; a crypto chain prices at 0" });
input("close", ohlcv.close); // the chart's own close: the premium on the contract's own chart, spot where a contract reports none
input("chain", options_chain.cells, { max_cells: 4000, venue: "auto", description: "The chart's own option chain: OPRA on a US stock or one of its contracts, 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 calendar and the model
// The readings, data-only: the card's tiles read them and the Console can too. Each is NaN on history bars.
output("matched_strike", none, overlay, { description: "The matched contract's strike; live bar only" });
output("expiry_days", none, overlay, { description: "Days to the matched expiry on the market's own calendar, 0 on the expiry day" });
output("contract_iv", none, overlay, { description: "The matched contract's implied volatility, percent a year" });
output("premium_used", none, overlay, { description: "The premium per unit the readings use (see premium_source)" });
output("premium_source", none, overlay, { description: "Where the premium came from: 0 the Premium setting, 1 the chart's close, 2 the model price" });
output("breakeven", none, overlay, { description: "The underlying's price at expiry where the position breaks even: strike plus the premium on a call, minus it on a put" });
output("prob_profit", none, overlay, { description: "Probability of profit at expiry, percent: lognormal from the contract's IV" });
output("max_gain", none, overlay, { description: "The most one contract can make at expiry, dollars; NaN when unlimited" });
output("max_loss", none, overlay, { description: "The most one contract can lose at expiry, dollars; NaN when unlimited" });
output("odds_ratio", none, overlay, { description: "Max gain over max loss; NaN when either is unlimited" });
output("kelly_full", none, overlay, { description: "Full Kelly, percent of the bankroll: p - (1 - p) / odds, no volatility adjustment; NaN when the loss is unlimited" });
output("kelly_half", none, overlay, { description: "Half Kelly, percent" });
output("kelly_quarter", none, overlay, { description: "Quarter Kelly, percent" });
output("tone", none, overlay, { description: "The headline's colour: 0 no edge (Kelly at or below 0), 1 no reading, 2 an edge; live bar only" });
string("read", { max_bytes: 64 }); // the headline: the position, its odds and half Kelly, or why there is no reading
string("title", { max_bytes: 48 }); // the card's title: the contract, its expiry and the days to go
string("premium_text", { max_bytes: 40 }); // the premium and where it came from
string("gain_text", { max_bytes: 24 }); // dollars, or unlimited
string("loss_text", { max_bytes: 24 });
// The card, in the terminal look: the odds lead as the headline, coloured by Kelly's sign (bear red for no edge, slate
// for no reading, bull green for an edge); the title names the contract. A tile's label is fixed text, so the premium's
// source and an unlimited gain or loss ride string slots.
render.hud("odds", {
position: "bottom_center",
look: "terminal",
title: "{{title}}",
columns: 2,
tiles: [
tile.pill("Odds", "read", { headline: true, color_by: "tone", colors: ["#ff003c", "#94a3b8", "#10b981"] }),
tile.value("Probability of profit", "prob_profit", { format: "%" }),
tile.value("Breakeven", "breakeven", { format: "price" }),
tile.rows([
["1) Premium", "premium_text"],
["2) Max gain", "gain_text"],
["3) Max loss", "loss_text"],
["4) Odds ratio", "odds_ratio", "0.00"],
["5) Full Kelly", "kelly_full", "%"],
["6) Half Kelly", "kelly_half", "%"],
["7) Quarter Kelly", "kelly_quarter", "%"],
["Implied volatility", "contract_iv", "%"],
]),
],
});
const TUPLE = 10; // f64s per contract: strike, expiry_ms, side, oi, gamma, delta, mark_iv, underlying, multiplier, vega
const MIN_T_MS = 900000.0; // 15 minutes: a contract closer to its expiry prices no time left, as the chart's greeks do
const MONTHS: String[] = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
const greeks = new Greeks(); // the model price; allocated once
const dateClock = new Clock("UTC"); // reads a date from its day number
let pickStrike = 0.0; // settings, read in onStart()
let pickDay: f64 = NaN; // the picked expiry's date, days since 1970-01-01; NaN = the first expiry after today
let isCall = true;
let isLong = true;
let pickPremium = 0.0;
let rate = 0.04;
let zone = 0;
let close: f64 = NaN; // this bar
let t: f64 = NaN;
let why = ""; // the no-reading line, set by measure()
let matched = false; // a contract was matched on the live bar
let strike: f64 = NaN; // the readings, measured on the live bar
let expiryMs: f64 = NaN;
let days: f64 = NaN;
let ivPoints: f64 = NaN;
let premium: f64 = NaN;
let source = 0;
let breakeven: f64 = NaN;
let prob: f64 = NaN;
let gain: f64 = NaN; // dollars; Infinity when unlimited
let loss: f64 = NaN;
let kelly: f64 = NaN; // raw Kelly as a fraction; -Infinity when the loss is unlimited
function onStart(): void {
pickStrike = p_strike();
pickDay = p_expiry() > 0.0 ? Math.floor(p_expiry() / 86400.0) : NaN; // the picker's UTC date
isCall = p_type() == 0.0;
isLong = p_position() == 0.0;
pickPremium = p_premium();
zone = i32(p_market_zone());
rate = zone == 0 ? 0.0 : p_rate_pct() / 100.0; // crypto chains are quoted at a rate of 0
}
function fin(v: f64): f64 { // an output never carries an infinity: NaN stands for "unlimited" and "none"
return isFinite(v) ? v : NaN;
}
// ── The contract: the expiry, then the strike on the picked side ──
function pickExpiry(cells: StaticArray<f64>, n: i32, nowMs: f64, todayKey: f64): f64 { // the expiry the settings name; NaN when the chain lists none
let best = Infinity;
for (let i = 0; i + TUPLE - 1 < n; i += TUPLE) {
const e = cells[i + 1];
if (!(cells[i] > 0.0) || !(e > nowMs) || e >= best) continue;
const day = optionsTradeDay(e, zone); // a calendar question: the expiry's trade date on the market's own clock
if (isNaN(pickDay) ? day > todayKey : day == pickDay) best = e;
}
return best == Infinity ? NaN : best;
}
function pickContract(cells: StaticArray<f64>, n: i32, expiry: f64, nowMs: f64): i32 { // the cell index of the contract; -1 when the side lists none
let at = -1;
let gap = Infinity;
for (let i = 0; i + TUPLE - 1 < n; i += TUPLE) {
if (cells[i + 1] != expiry || !(cells[i] > 0.0) || (cells[i + 2] > 0.0) != isCall) continue;
let target = pickStrike;
if (!(target > 0.0)) { // the strike nearest the expiry's forward: spot carried at the rate on a stock, the underlying itself elsewhere
const under = cells[i + 7] > 0.0 ? cells[i + 7] : close;
target = zone == 1 ? under * Math.exp((rate * (expiry - nowMs)) / OPTIONS_YEAR_MS) : under;
}
const d = Math.abs(cells[i] - target);
if (d < gap || (d == gap && at >= 0 && cells[i] < cells[at])) {
gap = d;
at = i;
}
}
return at;
}
// ── The odds, measured on the live bar ──
function measure(n: i32): void {
const cells = in_chain_view(); // the live chain in place: the first n values of the build's own buffer
const nowMs = chainPricedAtMs(cells, n, t * 1000.0); // when the chart priced the chain
const todayKey = optionsTradeDay(nowMs, zone);
matched = false;
let live = false;
for (let i = 0; i + TUPLE - 1 < n; i += TUPLE) if (cells[i] > 0.0 && cells[i + 1] > nowMs) live = true;
if (!live) {
why = "No live expiry in the chain";
return;
}
expiryMs = pickExpiry(cells, n, nowMs, todayKey);
if (isNaN(expiryMs)) {
why = isNaN(pickDay) ? "No expiry after today in the chain" : "No expiry on the picked date";
return;
}
const at = pickContract(cells, n, expiryMs, nowMs);
if (at < 0) {
why = isCall ? "No call listed at that expiry" : "No put listed at that expiry";
return;
}
matched = true;
strike = cells[at];
days = optionsTradeDay(expiryMs, zone) - todayKey;
ivPoints = cells[at + 6] > 0.0 ? cells[at + 6] : NaN; // mark IV rides the chain in percentage points
const spot = cells[at + 7] > 0.0 ? cells[at + 7] : close;
const multiplier = cells[at + 8] > 0.0 ? cells[at + 8] : 1.0;
const left = expiryMs - nowMs;
const years = left < MIN_T_MS ? 0.0 : left / OPTIONS_YEAR_MS;
const sigma = ivPoints / 100.0;
const side = isCall ? 1.0 : -1.0;
// The premium: the setting, else the chart's close on the contract's own chart (a close far from the underlying is
// the option's own price), else the model price from the contract's IV.
if (pickPremium > 0.0) {
premium = pickPremium;
source = 0;
} else if (close > 0.0 && Math.abs(close / spot - 1.0) > 0.2) {
premium = close;
source = 1;
} else {
premium = zone == 1 ? greeks.spot(side, spot, strike, years, sigma, rate) : greeks.forward(side, spot, strike, years, sigma, rate);
source = 2;
}
breakeven = isCall ? strike + premium : strike - premium;
// The probability the underlying ends past the breakeven: lognormal at the IV, drifting at the rate on a stock's spot,
// not at all on a forward; at expiry the answer is spot's own side of it.
const drift = zone == 1 ? rate : 0.0;
let above: f64 = NaN; // P(underlying at expiry > breakeven)
if (isNaN(breakeven)) above = NaN; // no premium (a contract without IV on the underlying's chart): unavailable
else if (breakeven <= 0.0) above = 1.0;
else if (years == 0.0) above = spot > breakeven ? 1.0 : 0.0;
else if (sigma > 0.0) above = normCdf((Math.log(spot / breakeven) + (drift - 0.5 * sigma * sigma) * years) / (sigma * Math.sqrt(years)));
const longWins = isCall ? above : 1.0 - above;
prob = isLong ? longWins : 1.0 - longWins;
// Per contract at expiry: a long pays the premium and a short collects it; a call's upside has no ceiling, a put's
// stops at the strike less the premium.
const capped = (strike - premium) * multiplier; // a put held to a worthless underlying
const paid = premium * multiplier;
if (isLong) {
gain = isCall ? Infinity : capped;
loss = paid;
} else {
gain = paid;
loss = isCall ? Infinity : capped;
}
const odds = gain / loss;
if (isNaN(prob) || isNaN(odds)) kelly = NaN;
else if (odds == Infinity) kelly = prob; // nothing to lose against an unlimited gain: Kelly reads p
else if (!(odds > 0.0)) kelly = -Infinity; // nothing to win, or an unlimited loss: no edge
else kelly = prob - (1.0 - prob) / odds;
}
// ── Text helpers ──
function sbGrouped(n: i64): void { // 84210 -> 84,210
if (n >= 1000) {
sbGrouped(n / 1000);
sb_text(",");
const r = n % 1000;
if (r < 100) sb_text("0");
if (r < 10) sb_text("0");
sb_int(r);
} else sb_int(n);
}
function sbPrice(v: f64): void { // decimals follow the price's size: 67,000 / 660.50 / 4.125
if (v >= 1000.0) sbGrouped(i64(Math.round(v)));
else if (v >= 100.0) sb_f64(v, 2);
else sb_f64(v, 3);
}
function sbDollars(v: f64): void { // $65,590, or unlimited
if (!isFinite(v)) {
sb_text("unlimited");
return;
}
if (v < 0.0) {
sb_text("-");
v = -v;
}
sb_text("$");
sbGrouped(i64(Math.round(v)));
}
function sbContract(): void { // 660 call, 662.50 put
if (strike == Math.floor(strike)) sbGrouped(i64(strike));
else sbPrice(strike);
sb_text(isCall ? " call" : " put");
}
function writeWords(): void {
sb_clear();
sb_text(isLong ? "Long " : "Short ");
sbContract();
if (isNaN(prob)) sb_text(": no implied volatility");
else {
sb_text(": ");
sb_f64(prob * 100.0, 1);
sb_text("% to profit, ");
if (kelly > 0.0) {
sb_text("half Kelly ");
sb_f64(kelly * 50.0, 1);
sb_text("%");
} else sb_text("no edge");
}
str_read_sb();
sb_clear();
dateClock.update(optionsTradeDay(expiryMs, zone) * 86400.0); // the expiry's trade date, read as a date
if (dateClock.dayOfMonth() < 10) sb_text("0");
sb_int(dateClock.dayOfMonth());
sb_text(MONTHS[dateClock.month() - 1]);
sb_text(" ");
sbContract();
sb_text(", ");
sb_int(i64(days));
sb_text("d");
str_title_sb();
sb_clear();
if (isNaN(premium)) sb_text("no price");
else {
sbPrice(premium);
sb_text(source == 0 ? ", your premium" : source == 1 ? ", the chart's close" : ", the model price");
}
str_premium_text_sb();
sb_clear();
if (isNaN(gain)) sb_text("-");
else sbDollars(gain);
str_gain_text_sb();
sb_clear();
if (isNaN(loss)) sb_text("-");
else sbDollars(loss);
str_loss_text_sb();
}
// onBar() runs once per bar: history rows carry an empty block and cost one read; the live bar matches the contract and
// measures its odds, then writes the readings (NaN on history rows) and the card's words.
function onBar(): void {
close = bar.close();
t = bar.time();
let chainCells = -1;
matched = false;
if (bar.isLast()) {
chainCells = in_chain_cells();
if (chainCells >= TUPLE && !isNaN(t)) measure(chainCells);
else why = "Waiting for the options chain";
}
const full = kelly * 100.0; // percent of the bankroll; the volatility adjustment is 1
out_matched_strike(matched ? strike : NaN);
out_expiry_days(matched ? days : NaN);
out_contract_iv(matched ? ivPoints : NaN);
out_premium_used(matched ? fin(premium) : NaN);
out_premium_source(matched && !isNaN(premium) ? f64(source) : NaN);
out_breakeven(matched ? fin(breakeven) : NaN);
out_prob_profit(matched ? fin(prob * 100.0) : NaN);
out_max_gain(matched ? fin(gain) : NaN);
out_max_loss(matched ? fin(loss) : NaN);
out_odds_ratio(matched ? fin(gain / loss) : NaN);
out_kelly_full(matched ? fin(full) : NaN);
out_kelly_half(matched ? fin(full * 0.5) : NaN);
out_kelly_quarter(matched ? fin(full * 0.25) : NaN);
out_tone(matched && !isNaN(kelly) ? (kelly > 0.0 ? 2.0 : 0.0) : bar.isLast() ? 1.0 : NaN);
if (matched) writeWords();
else if (bar.isLast()) {
// Nothing to read: the headline says why, so the card is never a row of blanks.
sb_clear();
sb_text(why);
str_read_sb();
sb_clear();
sb_text("no contract read");
str_title_sb();
}
}How it works
The settings name the contract. An indicator reads the chart's market, never which listed contract a chart shows, so the contract is four settings. Expiry date picks the expiry whose trade date, on the market's own clock (optionsTradeDay(): New York on a US stock, Chicago on CME, UTC on crypto), is the picked date; left at 0 it is the first expiry after today. Strike picks the listed strike nearest the setting at that expiry; left at 0 it is the strike nearest the expiry's forward (spot carried at the rate on a US stock, the underlying itself on futures and crypto). Type is call or put, Position long (bought, paying the premium) or short (sold, collecting it). The title names what was matched: 20NOV 680 call, 36d.
The premium, and where it came from. The Premium setting wins when it is above 0. Left at 0, a chart whose close sits more than 20% from the contract's underlying is taken for the contract's own chart, and its close is the premium; on the underlying's chart the premium is the model price, the contract's IV priced by Black-Scholes on spot on a US stock and by Black-76 on the underlying on futures and crypto, at the Rate setting (4% by default, 0 on crypto). The first row says which: 4.250, the chart's close, 4.105, the model price or 5.000, your premium. A deep in-the-money contract whose price sits within 20% of its underlying reads as the underlying's chart; set the premium there.
The breakeven and the probability. The breakeven is the strike plus the premium on a call and the strike minus it on a put. The probability of profit is the chance the underlying ends past it at expiry, lognormal at the contract's IV over the time left (from chainPricedAtMs(), in 365-day years): a stock's spot drifts at the rate, a futures or crypto forward does not drift. A long call profits above its breakeven, a long put below it; a short position profits exactly where the long one does not, so the two probabilities sum to 100%. A contract within 15 minutes of its expiry has no time left: the answer is which side of the breakeven the underlying is on.
Max gain and max loss, per contract. In dollars, times the multiplier (100 on a US listed option, the point value on CME, 1 on crypto): a long pays the premium and can lose no more; a long call's upside has no ceiling (unlimited), a long put's stops at the strike less the premium. A short collects the premium as its most; a short call's loss has no ceiling, a short put's stops at the strike less the premium.
Kelly. The odds ratio is the most it can make over the most it can lose, and Kelly's fraction of the bankroll is p - (1 - p) / odds with p the probability of profit. An unlimited gain against a capped loss reads p itself; an unlimited loss, or nothing to win, has no edge, and so does any fraction at or below zero. Some tools scale full Kelly by a volatility adjustment they do not publish; this card takes none, so full Kelly is the raw fraction, half Kelly half of it and quarter Kelly a quarter. The headline is green with an edge and red without one.
The card reads the outputs. The headline pill over read, the probability in percent and the breakeven at the chart's price digits, then numbered rows: the premium and its source, max gain and max loss ($65,500 or unlimited), the odds ratio, the three Kelly fractions in percent and the contract's IV. Each output is a number on the live bar (prob_profit, kelly_half, breakeven) for the Console and for a watch; an unlimited gain or loss reads NaN there, never an infinity.
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 and on the chart of one of its listed contracts venue: "auto" reads the OPRA chain of the underlying, 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 on the picked date" for a date the chain does not list (a weekend, or an expiry that has already settled), "No expiry after today in the chain" when only today's expiry is left, and "No put listed at that expiry" (or call) when that side lists none. A contract without implied volatility reads "no implied volatility" after its name, and the probability and Kelly read -; on the underlying's chart with no Premium set it has no model price either, so the premium reads "no price" and the breakeven stays empty instead of a certain 100% or 0%.
Customize it
- Your own fill. Type the price you paid or received into Premium: the breakeven, the probability and Kelly follow it.
- The contract on screen. On the contract's own chart, set its strike, expiry date and type; the close is its premium.
- A short premium seller. Set Position to
shorton a put: a capped loss gives a finite Kelly; a short call has none. - Another rate. Change Rate when rates move; crypto chains stay at 0.
Run it
- In the editor's Explorer, press the Templates icon ("Browse starter templates") and pick Contract Odds under HUDs.
- Press Run on a BTC chart: the card appears at the bottom centre over the at-the-money call of the first expiry after today.
- Set Strike, Expiry date, Type and Position to the trade you are weighing; at the editor's Console prompt, type
prob_profitandkelly_halfto read them on the live bar.
Concepts used
- Data sources for the
options_chaincelled class, its tuple and thevenueword - Options kit for
Greeks,normCdf,chainPricedAtMsandoptionsTradeDay - Kinds of setting for
param.priceandparam.time - HUD cards, Blocks and tiles and Looks for the card, its rows and the slot in the title