Appearance
IvTermStructure
Preview on the public Rust client. The analytic ships in the engine and is reachable today through the Python wheel and the TypeScript / Node addon; a per-analytic builder on the public Rust thin client (
kairos::Client) is tracked on the roadmap. The output tick schema below is stable.
What it computes
For each subscribed underlying, the analytic builds the ATM implied-volatility ladder across every forward expiration on the chain. One tick per (symbol, watermark_ms, date) triple; the legs payload carries one IvTenorLeg per registered expiration, sorted ascending by tenor_days.
The ladder is the same shape Bloomberg's OVDV / OVME term structure pages render, sourced from per-strike NBBO snapshots rather than periodic dealer marks.
Methodology
For each (symbol, expiration) pair the analytic:
- Resolves the ATM strike — the call strike nearest to spot from the per-symbol chain.
- Reads the ATM call's most-recent NBBO mid as the option market value, then solves implied volatility against the cached
(spot, rate, t)via the same closed-form Black–Scholes bisection used byGreeks. - Reports the tenor in calendar days against the per-symbol engine market calendar's close-time convention (regular session close, plus 15 minutes for early-close days).
References:
- Black & Scholes (1973). The Pricing of Options and Corporate Liabilities. Journal of Political Economy 81(3): 637–654.
- Derman & Kani (1994). Riding on a Smile. RISK 7(2): 32–39 — surface-shape conventions.
- Hull, Options, Futures, and Other Derivatives (10th ed.) §20.4 — volatility term structure.
See /methodology/black-scholes for the formula derivation.
Inputs
- Option
QuoteTickper forward expiration's ATM call. - Stock
TradeTick(populates the shared engine spot-price cache). - A engine rate provider for
(event_date, expiry_date)lookups. - A engine market calendar for the option-trading cutoff per session.
Per-expiration leg (IvTenorLeg)
Each legs[i] row carries: expiration: i32 (YYYYMMDD), tenor_days: f64 (calendar days to close), atm_strike: f64, atm_iv: f64, plus the resolved bid / ask / mid for the ATM call quote. f64::NAN propagates on legs whose ATM quote has not yet landed.
Output schema (IvTermStructureTick)
The field / type / description table below is regenerated from the IvTermStructureTick Rust source by docs-site/scripts/inject-doc-tables.ts on every npm run docs:build. Do not hand-edit between the sentinels.
| Field | Type | Description |
|---|---|---|
symbol | Arc<str> | Underlying symbol. |
date | i32 | Trading session date (YYYYMMDD). |
ms_of_day | i32 | Milliseconds since midnight at emission time. |
spot | f64 | Spot price used for ATM-strike resolution. f64::NAN when no underlying trade has landed yet on the session. |
ivol30 | f64 | ATM IV for the 30-day tenor slot — picks the nearest-to-30-day expiration on the chain (subject to min_dte_buffer), the closest-to-spot listed strike on that expiration, and reports the parity-anchored call/put IV midpoint. f64::NAN when no expiration meets the slot. |
ivol60 | f64 | ATM IV for the 60-day tenor slot. See [Self::ivol30]. |
ivol90 | f64 | ATM IV for the 90-day tenor slot. See [Self::ivol30]. |
ivol120 | f64 | ATM IV for the 120-day tenor slot. See [Self::ivol30]. |
ivol180 | f64 | ATM IV for the 180-day tenor slot. See [Self::ivol30]. |
ivol360 | f64 | ATM IV for the 360-day tenor slot. See [Self::ivol30]. |
ivol720 | f64 | ATM IV for the 720-day tenor slot. See [Self::ivol30]. |
chg_ivol30 | f64 | Change in ivol30 vs the rolled-up prior-session reference. f64::NAN until a prior-session reference lands (either via the seeded [IvHistoryCache] or after the first watermark-driven session rollover). |
chg_ivol60 | f64 | Change in ivol60 vs the prior-session reference. See [Self::chg_ivol30]. |
chg_ivol90 | f64 | Change in ivol90 vs the prior-session reference. See [Self::chg_ivol30]. |
chg_ivol120 | f64 | Change in ivol120 vs the prior-session reference. See [Self::chg_ivol30]. |
chg_ivol180 | f64 | Change in ivol180 vs the prior-session reference. See [Self::chg_ivol30]. |
chg_ivol360 | f64 | Change in ivol360 vs the prior-session reference. See [Self::chg_ivol30]. |
chg_ivol720 | f64 | Change in ivol720 vs the prior-session reference. See [Self::chg_ivol30]. |
legs | Vec<IvTenorLeg> | Per-expiration ATM IV ladder — the full walk-every-expiration roll-up. Ordered ascending by tenor_days. Useful for downstream consumers that want the entire chain shape (e.g. calendar-spread mispricing scans) rather than just the seven ladder slots above. |
Configuration (IvTermStructureRequest)
Regenerated from the IvTermStructureRequest Rust source — see the note above.
| Field | Type | Description |
|---|---|---|
contracts | SecurityFilter | Contracts the subscription tracks. The institutional idiom is to subscribe by underlying symbol ([SecurityFilter::Symbols]) — the engine fans out across every option on the chain plus the underlying stock contract (the latter feeds the shared [SpotPriceCache]). |
rate | Arc<dyn RateService> | Risk-free rate provider. Looked up per emission with (event_date, expiry_date); production deployments inject a curvekit-backed provider that auto-refreshes on date rollover. |
annual_dividend | Option<f64> | Optional override of the implied dividend yield (annual decimal). None falls back to zero dividend yield (CBOE convention). |
dividend_cache | `std::sync::Arc<crate::reference_data::dividend_yield:: | |
| DividendYieldCache>` | Engine-wired per-symbol dividend-yield cache. Seated at subscribe time by DefaultSpec via wire_dividend_yield. Consulted on the dispatch path only when annual_dividend is None — the per-tick lookup resolves the continuous-dividend carry yield q = D / S from the live underlying spot the analytic already holds (O(1), lock-light, non-blocking). An explicit annual_dividend override always wins. | |
venues | ExchangeFilter | Exchange / venue admission policy. |
emit | EmitPolicy | Emission policy. - [EmitPolicy::OnEveryTick] (default) — fire one tick per admitted Quote that advances the per-symbol dedup stamp. - [EmitPolicy::OnClose] — accumulate; fire on watermark advance. |
tenor_ladder | [u16;TENOR_LADDER_LEN] | Fixed tenor ladder in calendar days. Per-slot the analytic picks the nearest-to-tenor expiration on the chain (subject to [Self::min_dte_buffer]) and reports its ATM IV plus the change vs the rolled-up prior-session reference. Default is [CANONICAL_TENOR_LADDER] = {30, 60, 90, 120, 180, 360, 720}, the trade-alert.com ivol30..720 glossary surface. |
min_dte_buffer | u16 | Minimum days-to-expiration buffer applied to every ladder slot. Expirations within this many days of the session date are excluded from every slot's nearest-tenor picker. Defaults to [DEFAULT_MIN_DTE_BUFFER] = 2 days, filtering 0-DTE / 1-DTE noise out of the front tenor. |
iv_history | Option<Arc<IvHistoryCache>> | Optional boot-time IV history. When supplied, the per-symbol chg_ivol_t columns seed from the cache's most-recent sample at construction time instead of starting NaN; downstream dashboards that load on top of a populated cache see live delta columns from emission #1 rather than emission #2. |
spot_cache | SpotPriceCache | Shared spot price cache, populated by the analytic from underlying stock trades and read by every option contract on the same subscription. Build via new_spot_price_cache and clone the returned Arc into the spec — sharing it with a paired GreeksParams gives both analytics the same per-symbol spot view. |
calendar | Arc<dyn MarketCalendar> | Market calendar provider. Looked up per emission with the triggering event's date to derive the option-trading cutoff. |
Operational characteristics
- Per-tick latency. One Black–Scholes bisection per emission leg (≈ 30 iterations max) and one shared chain-registry lookup.
- Allocation discipline. Per-symbol leg vector is reused across emissions; the only hot-path allocation is the result vector handed to the consumer.
- Replay parity. Deterministic given identical input tick order.
Example
Preview. This analytic ships in the engine and is reachable today through the Python wheel and the TypeScript / Node addon. A per-analytic builder on the public Rust thin client (
kairos::Client) is tracked on the roadmap — bare-string symbol filters passed to the analytic accessor will match the other analytics already exposed there. The output tick rows are stable and documented above.
Cross-language surface
python
import kairos_thetadata as kt
client = kt.Client.connect(kt.Credentials.from_env())
def on_event(tick):
for leg in tick.legs:
print(
f"{tick.symbol} exp={leg.expiration} "
f"t={leg.tenor_days:.1f}d iv={leg.atm_iv:.3f}"
)
sub = client.live().iv_term_structure(["SPX"]).on_event(on_event)
sub.wait(timeout_seconds=60.0)typescript
import { Client, Credentials } from "kairos-thetadata";
const client = await Client.connect(Credentials.fromEnv());
const sub = await client.live().ivTermStructure(["SPX"]).onEvent((tick) => {
for (const leg of tick.legs) {
console.log(
`${tick.symbol} exp=${leg.expiration} t=${leg.tenorDays.toFixed(1)}d iv=${leg.atmIv.toFixed(3)}`,
);
}
});