Nautilus Trader - Greeks

NautilusTrader exposes option Greeks through two parallel paths that can be used independently or together: (1) a venue-provided OptionGreeks stream from venues that publish them (Deribit, Bybit, OKX) - Rust types exposed via PyO3, arriving on the same data subscription bus as quotes; and (2) a local GreeksCalculator (Cython/Python) that computes Black-Scholes Greeks from cached prices, with first-class support for portfolio aggregation, shock scenarios, beta weighting, time-weighted vega, and percent Greeks. Either path produces a value that flows through the same Cache + MessageBus contracts as the rest of the framework. There is no IBKR adapter, so for Cortana the local calculator is the only built-in option for IBKR-sourced underlying data - but UW-sourced Greeks could also be ingested as a custom GreeksData feed.

Core claim

Nautilus does not force a single Greeks source on the strategy. The calculator and the venue stream both deposit results into the same in-process types (GreeksData and PortfolioGreeks) and the same handler shapes (on_option_greeks, on_option_chain), which means a strategy can consume “whichever Greeks are best for this venue” without knowing where they came from. Portfolio-level aggregation is a first-class operation, not a thing the strategy author has to build.

Greeks data model

Per-instrument types

Two parallel Rust+Python types carry single-instrument Greeks:

TypeSource pathCarries
OptionGreeksVenue stream (Rust/PyO3)delta, gamma, vega, theta, rho, mark/bid/ask IV, underlying_price, OI
GreeksDataLocal calculator (Python custom data)delta, gamma, vega, theta, vol, itm_prob, pnl, price, full strike/expiry/multiplier context

OptionGreeks is a Rust-native type with this shape:

FieldTypeMeaning
instrument_idInstrumentIdThe option contract
deltafloatdV/dS
gammafloatd2V/dS2
vegafloatper 1pp IV change (scaled by 0.01)
thetafloatper calendar day (scaled by 1/365.25)
rhofloatper unit interest rate change
mark_ivfloat or Nonemark IV
bid_iv / ask_ivfloat or Noneside-aware IV
underlying_pricefloat or Nonevenue’s forward price for that expiry
open_interestfloat or NoneOI
ts_eventint (ns)when venue produced the value
ts_initint (ns)when Nautilus initialized the object

GreeksData is broader because it carries the full computation context (strike, expiry, expiry_in_years, multiplier, underlying_price, interest_rate, cost_of_carry, vol used) - i.e., everything you would need to reproduce the calculation in a backtest. It extends Data and is a @customdataclass, which gives it Arrow serialization, Cache storage, and ParquetDataCatalog persistence “for free.”

Important asymmetry to note for Cortana: OptionGreeks does not carry strike, expiry, or multiplier - only instrument_id plus the sensitivities. The full instrument metadata lives on the Instrument in the Cache. GreeksData, by contrast, carries everything. This means recovery of “what strike was this delta on?” requires either the instrument-id-keyed cache lookup (venue path) or the embedded fields (local-calc path).

Portfolio-level aggregation

PortfolioGreeks is the aggregated result. Six fields:

FieldTypeMeaning
pnlfloatsummed across positions
pricefloatsummed model value
deltafloatportfolio delta
gammafloatportfolio gamma
vegafloatportfolio vega
thetafloatportfolio theta (daily)

It supports + (combining positions) and * scalar multiplication (scaling). GreeksData * signed_qty → PortfolioGreeks is the canonical promotion path: a per-instrument computation times a position quantity yields a contribution to portfolio Greeks. to_portfolio_greeks() also exists, which multiplies by the contract multiplier.

Computation source

Venue-supplied path (Rust/PyO3)

The framework declares three venues that publish Greeks natively on subscription: Deribit, Bybit, OKX. Subscribing is a single call from an actor or strategy:

self.subscribe_option_greeks(instrument_id, client_id=ClientId("DERIBIT"))

Updates flow back through the standard handler:

def on_option_greeks(self, greeks: OptionGreeks) -> None:
    self.log.info(f"delta={greeks.delta:.4f} gamma={greeks.gamma:.6f}")

The underlying Rust types live at crates/model/src/data/greeks.rs and crates/model/src/data/option_chain.rs:

  • OptionGreekValues: a plain struct with delta/gamma/vega/theta/rho. Implements Add and Mul<f64> for aggregation.
  • OptionGreeks: wraps OptionGreekValues with instrument_id, IV fields, and timestamps. Implements Deref<Target = OptionGreekValues> so consumers access Greeks fields directly.
  • HasGreeks trait: provides greeks() -> OptionGreekValues. Implemented by both types.

This path runs no math locally. The values are whatever the venue sent. Latency: arrives with market data - no extra computation cost.

Black-Scholes functions (Rust/PyO3)

For when a strategy wants to call BS directly without going through the calculator object, four functions are exposed from nautilus_trader.core.nautilus_pyo3:

from nautilus_trader.core.nautilus_pyo3 import (
    black_scholes_greeks,
    imply_vol,
    imply_vol_and_greeks,
    refine_vol_and_greeks,
)
 
# Compute Greeks given known volatility
result = black_scholes_greeks(
    s=100.0, r=0.05, b=0.0, vol=0.20,
    is_call=True, k=100.0, t=0.25,
)
# result fields: price, vol, delta, gamma, vega, theta, itm_prob
 
# Imply vol from market price, then compute Greeks in one pass
result = imply_vol_and_greeks(
    s=100.0, r=0.05, b=0.0,
    is_call=True, k=100.0, t=0.25, price=5.0,
)
 
# Refine from a starting vol estimate (faster convergence)
result = refine_vol_and_greeks(
    s=100.0, r=0.05, b=0.0,
    is_call=True, k=100.0, t=0.25,
    target_price=5.0, initial_vol=0.18,
)

The BlackScholesGreeksResult returned is: price, vol, delta, gamma, vega, theta, itm_prob. Note that rho is not in the BS result - only OptionGreeks (venue-supplied) carries rho.

Conventions baked in:

  • Vega scaled by 0.01 - sensitivity to a 1 percentage point IV move.
  • Theta scaled by 1/365.25 - daily decay.
  • American-style options are priced as European for Greeks computation. This is a documented simplification - the docs explicitly note this.

Local GreeksCalculator (Cython/Python)

The Cython class at nautilus_trader/model/greeks.pyx. Constructed inside an actor or strategy:

from nautilus_trader.model.greeks import GreeksCalculator
 
# Typically created in on_start()
calculator = GreeksCalculator(cache=self.cache, clock=self.clock)

Per-instrument call:

greeks = calculator.instrument_greeks(
    instrument_id=option_id,
    flat_interest_rate=0.0425,  # used if no yield curve in cache
)
# Returns GreeksData or None

Internally the calculator:

  1. Looks up the instrument and its underlying in the Cache.
  2. Retrieves current prices - MID preferred, LAST as fallback.
  3. Looks up yield curves from the Cache, falling back to flat_interest_rate if none cached.
  4. Implies volatility from the option market price using imply_vol_and_greeks.
  5. Returns a populated GreeksData (or None if any input missing).

For non-option instruments (futures, equities) the calculator returns a GreeksData with delta=1 (or beta-weighted delta) and no gamma/vega/theta. This means a stock or future treated as a position contributes to portfolio delta but not the higher-order Greeks - which is the right Black-Scholes accounting.

Shock scenarios

greeks = calculator.instrument_greeks(
    instrument_id=option_id,
    spot_shock=10.0,             # +10 points on underlying
    vol_shock=0.02,              # +2% absolute vol
    time_to_expiry_shock=1/365,  # roll forward one day
)

These shocks are applied before the BS calculation, so they propagate into delta/gamma/vega/theta. Think “what does my book look like if SPY drops $5 and IV pops 3 points by EOD?” - that’s a single function call.

Volatility seeding

greeks = calculator.instrument_greeks(
    instrument_id=option_id,
    update_vol=True,        # use cached vol as starting point
    cache_greeks=True,      # store result for next iteration
)

This uses the cached vol from the previous iteration as the seed for refine_vol_and_greeks, which converges faster than a cold-start imply_vol_and_greeks.

Beta weighting and percent Greeks

greeks = calculator.instrument_greeks(
    instrument_id=option_id,
    index_instrument_id=InstrumentId.from_str("SPX.CBOE"),
    beta_weights={underlying_id: 1.15},
    percent_greeks=True,
)

percent_greeks=True returns delta/gamma in percent terms (delta = pct move per 1% underlying move). Beta weighting expresses the exposure in terms of an index - e.g., AAPL options re-expressed in SPX units.

Time-weighted vega

greeks = calculator.instrument_greeks(
    instrument_id=option_id,
    vega_time_weight_base=30,  # normalize to 30-day vega
)

Used to compare vega across different expirations on a common scale.

IV handling and surface

Nautilus does not ship a built-in vol-surface model. The local calculator works per-instrument: it implies a single vol from the market price of that one option. There is no SVI, no SABR, no moneyness-and-tenor smoothing - that is the strategy author’s responsibility if needed.

The venue path passes through whatever the venue sent: mark_iv, bid_iv, ask_iv - all on the OptionGreeks event. No surface fitting.

For Cortana’s purposes this means: if we want a smoothed IV curve (for example, “current ATM-25d skew” as a regime input), we are building it ourselves on top of either source.

Time-decay (theta) treatment

Theta is per calendar day in both paths - the 1/365.25 normalization is applied. There is no built-in “theta accelerates as expiry approaches” curve - that emerges naturally from the BS calculation as t → 0. To implement explicit power-hour / late-day acceleration logic, the strategy needs to either:

  1. Recompute Greeks frequently with a small time_to_expiry_shock and compare deltas (i.e., empirical theta on the actual instrument).
  2. Trust the BS theta for that instant and project forward analytically.

Neither requires a new framework feature - both are call-the-calculator patterns. The framework gives you the value at a moment; the “acceleration” interpretation lives in user code.

Yield curves

YieldCurveData stores an interest rate or dividend yield curve in the Cache. The calculator looks up curves by currency code (rates) or underlying instrument ID (dividend yields), with quadratic interpolation:

from nautilus_trader.model.greeks_data import YieldCurveData
import numpy as np
 
curve = YieldCurveData(
    ts_event=0, ts_init=0,
    curve_name="USD",
    tenors=np.array([0.25, 0.5, 1.0, 2.0]),
    interest_rates=np.array([0.04, 0.042, 0.045, 0.048]),
)
rate = curve(0.75)  # quadratic interpolation

If no curve is cached, the calculator falls back to the flat_interest_rate arg (default 0.0). For 0DTE, t < 1 day so r matters a few cents - flat rate is acceptable.

Portfolio aggregation

portfolio_greeks() walks open positions and sums their contributions:

portfolio = calculator.portfolio_greeks(
    underlyings=["AAPL", "MSFT"],
    venue=Venue("CBOE"),
    strategy_id=StrategyId("DELTA_HEDGE-001"),
    flat_interest_rate=0.0425,
    index_instrument_id=InstrumentId.from_str("SPX.CBOE"),
    beta_weights=beta_dict,
    percent_greeks=True,
)
# Returns PortfolioGreeks: pnl, price, delta, gamma, vega, theta

Filter parameters:

  • underlyings: list of symbol prefixes - ["AAPL"] matches AAPL stock AND all AAPL options.
  • venue: restrict to a single venue.
  • instrument_id: restrict to a single instrument.
  • strategy_id: restrict to a single strategy (multi-strategy nodes get per-strategy books).
  • side: filter by LONG/SHORT.
  • greeks_filter: callable that accepts the per-position PortfolioGreeks, returning True to include - arbitrary user predicate.

This is the call a Cortana risk engine would make every tick to ask “what’s my current portfolio delta/gamma/theta/vega across all open positions, in SPY units?” - single function call, no plumbing.

Snapshot vs raw mode (option chain)

For chain-level subscriptions (subscribe_option_chain), the snapshot_interval_ms parameter chooses behavior:

  • Snapshot mode (snapshot_interval_ms=1000): quotes and Greeks buffer and publish as an OptionChainSlice on a timer. For periodic rebalancing or UI.
  • Raw mode (snapshot_interval_ms=None): each update publishes a slice immediately. For latency-sensitive strategies that react to individual updates.

Per-instrument subscribe_option_greeks is always raw - every update calls on_option_greeks.

Comparison table - venue vs local

CriterionVenue (OptionGreeks)Local (GreeksCalculator)
Computation locationVenueLocal Black-Scholes
LatencyArrives with market dataComputed on demand
Supported venuesDeribit, Bybit, OKX (only)Any venue with option instruments
Shock scenariosNot supportedSpot, vol, time shocks
Portfolio aggregationManual (iterate slices)Built-in portfolio_greeks()
Beta weightingNot supportedBuilt-in
Backtest supportVia recorded OptionGreeksFrom cached prices any time
Greeks availableδ γ ν θ ρ + IV + OIδ γ ν θ + itm_prob + vol
Strike/expiry contextNot on event (cache lookup)On GreeksData directly
Data typeOptionGreeks (Rust/PyO3)GreeksData (@customdataclass)

Cortana MK3 implications

Where do our Greeks come from?

IBKR is not in Nautilus’s venue-Greeks list. Deribit, Bybit, OKX publish Greeks; IBKR does not (in Nautilus’s adapter set). For Cortana’s SPY/SPX 0DTE options on IBKR, the architectural options are:

  1. Local GreeksCalculator. Feed IBKR option mid prices into the Cache, call instrument_greeks(), get a GreeksData back. This is the most “Nautilus-native” path.
  2. UW-as-custom-data-feed. Build a UwOptionGreeks custom data class (subclass of Data) and a UW DataClient that publishes GreeksData (or a Cortana-specific shape) onto the bus. Strategy subscribes to on_option_greeks-equivalent handler. This preserves our existing UW dependency for GEX/flow inputs.
  3. Hybrid. Local calculator for “what’s my delta right now for sizing?”, UW for GEX/flow regime inputs that the local BS does not model.

The hybrid is almost certainly what Cortana wants. Local calc owns “contract-level delta for sizing/notional and intraday risk” because it’s instant and consistent with our own price stream; UW owns regime inputs (dealer GEX, flow imbalance) that aren’t in BS.

”Give me current delta of position X”

greeks = self.calculator.instrument_greeks(
    instrument_id=position.instrument_id,
    update_vol=True,
    cache_greeks=True,
)
position_delta = position.signed_qty * greeks.delta * greeks.multiplier

Single call. Cached vol seeding makes this cheap to call every tick.

”What’s portfolio gamma right now”

pg = self.calculator.portfolio_greeks(
    underlyings=["SPY"],
    strategy_id=self.config.strategy_id,
)
current_gamma = pg.gamma

Single call. Filterable by strategy_id which means multi-strategy operation gets per-strategy books for free.

”Veto entry if portfolio delta > 0.5 SPY-equivalent”

This is a RiskEngine concern, not strategy code. The RiskEngine’s job (per nautilus-concepts.md) is centralized pre-trade validation. A custom RiskEngine subclass - or a custom risk component the RiskEngine calls - runs portfolio_greeks() before approving any new entry order:

# Inside a custom risk component
def pre_trade_check(self, command: SubmitOrder) -> bool:
    pg = self.calculator.portfolio_greeks(
        underlyings=["SPY"],
        strategy_id=command.strategy_id,
    )
    projected_delta = pg.delta + projected_contribution(command)
    if abs(projected_delta) > 0.5:
        return False  # veto
    return True

Because the RiskEngine is the single chokepoint, this check cannot be silently skipped - every order goes through it. This is the structural fix for the “dead-code meta-prob sizing” class of bug from MK2.

Interaction with delta-percentile strike selection

Cortana’s contract-selection rule picks the strike at a target delta percentile. The Bybit delta-neutral options tutorial does exactly this pattern (per nautilus-tutorials.md line 124-130):

chain = self.cache.option_chain(series_id)  # latest snapshot
strikes = chain.strikes()
deltas = [(s, chain.get_call(s).greeks.delta) for s in strikes]
target_strike = pick_at_percentile(deltas, percentile=0.30)

For Cortana on IBKR, the flow is the same except chain.get_call(s).greeks comes from our local calculator (looked up from Cache after we computed it on each chain refresh) rather than from a venue stream.

Power Hour theta math: ours or Nautilus’s?

The mandate (project_eod_power_hour) calls out late-day theta acceleration as a regime input. Nautilus computes theta as instantaneous daily decay via Black-Scholes. The “acceleration” language is a derivative of theta over time - δθ/δt - which Nautilus does not compute directly.

The right architecture: trust Nautilus’s theta as the value and compute the acceleration ourselves in a Cortana actor that subscribes to Greeks and tracks the time-derivative across the last N updates. Two reasons:

  1. The BS theta itself is mechanically correct - there’s no benefit to re-implementing it.
  2. The “acceleration” framing is Cortana-specific regime logic, not a universal Greek. It belongs in our regime engine, not in a generic Greeks library.

Concretely: a Cortana ThetaAccelerationActor receives on_option_greeks, maintains a per-position rolling window of (ts, theta, underlying_price) tuples, and emits a theta_acceleration signal on the MessageBus when the slope crosses a threshold. Other components (sizing, exit timing) subscribe to that signal.

Backtest replay reproduction

For “did the engine see the exact same Greeks live as in backtest?”, the choice is between the two paths:

  • If using local calculator: we record IBKR mid prices and IV-curve cache state to a ParquetDataCatalog. Backtest replay calls the same instrument_greeks() against the cached prices. Bitwise reproducible because BS is deterministic given inputs.
  • If using UW custom feed: we record the raw UW Greeks events into the catalog as a custom GreeksData stream. Backtest replays them unchanged. Reproducibility is “did UW give us the same value?” - trivially yes if we recorded the event.

Both paths reproduce. The hybrid above means we record both: IBKR prices for local-calc reproduction, UW events for regime-input reproduction.

What forces Cortana to compute its own?

Two things Nautilus does not give us out of the box:

  1. No IBKR Greeks adapter. IBKR’s own computed Greeks (modelGreeks ticks via TWS API) are not piped into Nautilus’s OptionGreeks stream. We either ignore them and use local BS, or build our own adapter. Recommendation: ignore IBKR Greeks; local BS on our recorded mids is more reproducible and consistent.

  2. No vol surface. The local calculator implies vol per-instrument without smoothing. If Cortana wants “ATM 25d skew” as a regime input - which we do - we build a smoother on top.

Beyond those two, Nautilus’s primitives cover what Cortana needs: delta for sizing, portfolio aggregation for risk vetoes, theta as the per-instant value to feed our acceleration logic, beta-weighting for index-relative reporting.

When it applies

  • Any time a Cortana subsystem needs delta for notional sizing.
  • Risk-engine pre-trade vetoes based on portfolio-Greeks limits.
  • Regime detection that consumes implied vol or theta.
  • Backtest reproduction of “what did the engine see?”

When it breaks

  • IBKR-sourced Greeks: no built-in adapter; we use the local calculator or a custom UW feed.
  • American-style early-exercise pricing: the docs note these are priced as European for Greeks. For 0DTE SPY options this is acceptable (no dividend before expiry); for longer-dated equity options near ex-div it would matter.
  • Vol surface: not provided. If we need a smoothed surface, we build it.
  • Greeks-on-spreads: OptionSpread aggregates per-leg Greeks; the framework computes per leg, then sums. If Cortana ever trades spreads, we would lean on this and verify the per-leg vs combined numbers match expectations.

Examples

Working venue-stream examples in the Nautilus repo:

  • examples/live/bybit/bybit_option_greeks.py - Bybit subscription.
  • examples/live/deribit/deribit_option_greeks.py - Deribit subscription.
  • examples/live/okx/okx_option_greeks.py - OKX subscription.

For local calculator usage, the SPY 0DTE example referenced in nautilus-tutorials.md line 126 is the closest reference pattern (strike selection by delta percentile, portfolio delta tracking).

See Also

  • Nautilus Options - instrument types, chain subscription, strike-range filtering (parallel page).
  • Nautilus Concepts - architecture overview, Cache, MessageBus, RiskEngine.
  • Nautilus Tutorials - Bybit Greeks example and delta-neutral options tutorial.
  • ~/.claude/projects/-Users-codysmith-conductor-workspaces-cortanaroi-cortanaroi-mk2/memory/project_eod_power_hour.md
    • Power Hour mandate; the theta-acceleration logic that sits on top of Nautilus’s per-instant theta.

Timeline

  • 2026-05-07 | Cody - Filed during pre-spike concept mastery sweep.