Nautilus Trader - Greeks
NautilusTrader exposes option Greeks through two parallel paths that can be used independently or together: (1) a venue-provided
OptionGreeksstream 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 localGreeksCalculator(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 customGreeksDatafeed.
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:
| Type | Source path | Carries |
|---|---|---|
OptionGreeks | Venue stream (Rust/PyO3) | delta, gamma, vega, theta, rho, mark/bid/ask IV, underlying_price, OI |
GreeksData | Local 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:
| Field | Type | Meaning |
|---|---|---|
instrument_id | InstrumentId | The option contract |
delta | float | dV/dS |
gamma | float | d2V/dS2 |
vega | float | per 1pp IV change (scaled by 0.01) |
theta | float | per calendar day (scaled by 1/365.25) |
rho | float | per unit interest rate change |
mark_iv | float or None | mark IV |
bid_iv / ask_iv | float or None | side-aware IV |
underlying_price | float or None | venue’s forward price for that expiry |
open_interest | float or None | OI |
ts_event | int (ns) | when venue produced the value |
ts_init | int (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:
| Field | Type | Meaning |
|---|---|---|
pnl | float | summed across positions |
price | float | summed model value |
delta | float | portfolio delta |
gamma | float | portfolio gamma |
vega | float | portfolio vega |
theta | float | portfolio 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. ImplementsAddandMul<f64>for aggregation.OptionGreeks: wrapsOptionGreekValueswith instrument_id, IV fields, and timestamps. ImplementsDeref<Target = OptionGreekValues>so consumers access Greeks fields directly.HasGreekstrait: providesgreeks() -> 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 NoneInternally the calculator:
- Looks up the instrument and its underlying in the Cache.
- Retrieves current prices - MID preferred, LAST as fallback.
- Looks up yield curves from the Cache, falling back to
flat_interest_rateif none cached. - Implies volatility from the option market price using
imply_vol_and_greeks. - Returns a populated
GreeksData(orNoneif 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:
- Recompute Greeks frequently with a small
time_to_expiry_shockand compare deltas (i.e., empirical theta on the actual instrument). - 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 interpolationIf 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, thetaFilter 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-positionPortfolioGreeks, returningTrueto 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 anOptionChainSliceon 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
| Criterion | Venue (OptionGreeks) | Local (GreeksCalculator) |
|---|---|---|
| Computation location | Venue | Local Black-Scholes |
| Latency | Arrives with market data | Computed on demand |
| Supported venues | Deribit, Bybit, OKX (only) | Any venue with option instruments |
| Shock scenarios | Not supported | Spot, vol, time shocks |
| Portfolio aggregation | Manual (iterate slices) | Built-in portfolio_greeks() |
| Beta weighting | Not supported | Built-in |
| Backtest support | Via recorded OptionGreeks | From cached prices any time |
| Greeks available | δ γ ν θ ρ + IV + OI | δ γ ν θ + itm_prob + vol |
| Strike/expiry context | Not on event (cache lookup) | On GreeksData directly |
| Data type | OptionGreeks (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:
- Local
GreeksCalculator. Feed IBKR option mid prices into the Cache, callinstrument_greeks(), get aGreeksDataback. This is the most “Nautilus-native” path. - UW-as-custom-data-feed. Build a
UwOptionGreekscustom data class (subclass ofData) and a UW DataClient that publishesGreeksData(or a Cortana-specific shape) onto the bus. Strategy subscribes toon_option_greeks-equivalent handler. This preserves our existing UW dependency for GEX/flow inputs. - 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.multiplierSingle 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.gammaSingle 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 TrueBecause 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:
- The BS theta itself is mechanically correct - there’s no benefit to re-implementing it.
- 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 sameinstrument_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
GreeksDatastream. 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:
-
No IBKR Greeks adapter. IBKR’s own computed Greeks (modelGreeks ticks via TWS API) are not piped into Nautilus’s
OptionGreeksstream. 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. -
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:
OptionSpreadaggregates 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.