Nautilus Tutorial - Options Data and Greeks (Bybit), for Cortana

The tutorial builds two Rust v2 LiveNode examples that stream live BTC options data from Bybit: (1) a per-instrument Greeks tester that discovers nearest-expiry calls, calls subscribe_option_greeks on each, and logs delta/gamma/vega/theta/rho/IV via on_option_greeks; and (2) an option-chain aggregator that constructs an OptionSeriesId, subscribes via subscribe_option_chain with a StrikeRange::AtmRelative {3, 3} filter, and consumes OptionChainSlice snapshots through on_option_chain. For Cortana, the split is sharp: Strategy-side patterns transfer cleanly (instrument discovery, chain iteration, Greeks-field reading, strike selection by delta-percentile, on_ handler shapes); the adapter-side subscribe_option_greeks call does NOT transfer because IBKR has no native Greeks adapter* (per nautilus-ib.md and nautilus-greeks.md). Cortana substitutes UW custom-data Greeks (live primary) and the local GreeksCalculator Black-Scholes path (backtest + UW fallback). The tutorial is therefore a shape reference - same handler names, same event types, same field semantics - wired to a different upstream source.

What the tutorial actually delivers

Two Rust examples in crates/adapters/bybit/examples/:

FileBuild targetWhat it does
node_greeks.rsbybit-greeks-testerPer-instrument Greeks streaming for nearest-expiry BTC calls
node_option_chain.rsbybit-option-chainAggregated chain snapshots at ±3 strikes around ATM, every 5s

Both run as data-only LiveNodes - no execution client, no orders. This is critical: the tutorial is a “Greeks-on-the-bus” demo, not a trading strategy. It tests venue-side wiring, the data engine’s chain-aggregation plumbing, and the strategy-side handler shapes. It does not demonstrate sizing, entry, exits, hedging, or portfolio-level Greeks aggregation. Those live in the sibling delta-neutral options tutorial - see nautilus-tutorials.md.

The tutorial’s notable omissions (verbatim from the WebFetch summary): IV-based limit-order pricing, portfolio delta aggregation, rehedge triggers, session position hydration, strike-selection by delta-percentile. These are explicitly addressed in the delta-neutral-strategy tutorial, not this one. The framing question in the spike-prep brief asked for these - they exist in the next tutorial, and this page flags them as deferred.

Cortana transfer/no-transfer split

PatternTransfer?Why
subscribe_option_greeks(instrument_id)NoBybit-only adapter capability. IBKR has no Greeks adapter. Cortana uses UW custom data + local BS.
subscribe_option_chain(series_id, strike_range, snapshot_interval_ms)PartialThe framework’s OptionChainManager plumbing works for any venue with options instruments. The Greeks field on each OptionStrikeData will be None from IBKR’s wire data unless we hydrate it from UW custom data or the local calculator.
OptionSeriesId::new(venue, underlying, quote, expiry)YesGeneric identifier. Same shape for IBKR-routed SPY.SMART chain.
StrikeRange::{Fixed, AtmRelative, AtmPercent}YesPure config primitive. No venue dependency.
on_option_greeks(&self, greeks: &OptionGreeks) handlerTranslateSame handler signature, but events arrive via UW DataClient publishing OptionGreeks-shaped custom data, not subscribe_option_greeks.
on_option_chain(&self, slice: &OptionChainSlice) handlerYesThe framework publishes the slice regardless of where Greeks came from.
Cache-borrow-then-drop pattern before subscriptionsYesUniversal Rust borrow rule - applies in any actor. Python doesn’t have the borrow checker but the pattern (read instruments, then act) is still right.
Instrument discovery via cache.instruments(&venue, Some(&filter))YesSame call against IBKR-loaded SPY 0DTE chain.
OptionChainSlice field iteration (get_call/get_put/strikes)YesGeneric data type, same API regardless of venue.
with_delay_post_stop_secs(5) shutdown delayYesGeneric LiveNode pattern.
cargo run --example bybit-greeks-tester build invocationNoCortana runs Python; the strategy code lives in a Strategy subclass loaded by a TradingNode.

Walk-through, step by step

Step 1 - LiveNode + DataClient configuration

Tutorial code (Bybit):

let bybit_config = BybitDataClientConfig {
    api_key: None,    // loaded from env BYBIT_API_KEY
    api_secret: None,
    product_types: vec![BybitProductType::Option],
    ..Default::default()
};
 
let mut node = LiveNode::builder(trader_id, environment)?
    .with_name("BYBIT-OPTIONS-001".to_string())
    .add_data_client(None, Box::new(client_factory),
                     Box::new(bybit_config))?
    .with_delay_post_stop_secs(5)
    .build()?;

Cortana-specific deviation. Cortana wires an InteractiveBrokersDataClientConfig (per nautilus-ib.md) with build_options_chain=True, min_expiry_days=0, max_expiry_days=1 to materialize the SPY 0DTE chain. Plus a UW LiveDataClient (per nautilus-custom-data.md + nautilus-ib.md UW-as-data-only adapter sketch) that publishes UW Greeks as custom data onto the same bus. The with_delay_post_stop_secs(5) shutdown shim is identical and worth carrying - graceful Greeks-flush on close is good hygiene.

Step 2 - Instrument discovery (DataActor on_start)

Tutorial code (transfer verbatim - modulo Python translation):

fn on_start(&mut self) -> anyhow::Result<()> {
    let venue = Venue::new("BYBIT");
    let underlying_filter = Ustr::from("BTC");
 
    let mut options: Vec<(InstrumentId, f64, u64)> = {
        let cache = self.cache();
        let instruments = cache.instruments(&venue, Some(&underlying_filter));
        instruments
            .iter()
            .filter_map(|inst| {
                if inst.option_kind() == Some(OptionKind::Call) {
                    let expiry = inst.expiration_ns()?.as_u64();
                    let strike = inst.strike_price()?.as_f64();
                    Some((inst.id(), strike, expiry))
                } else {
                    None
                }
            })
            .collect()
    }; // cache borrow dropped here - important
 
    let now_ns = self.timestamp_ns().as_u64();
    options.retain(|(_, _, exp)| *exp > now_ns);
    // identify nearest expiry, etc.
}

Transfer verbatim for Cortana logic. The pattern is:

  1. Get a snapshot of instruments via the cache.
  2. Filter by (venue, underlying).
  3. Read kind / strike / expiry off each instrument.
  4. Drop the cache borrow before any subscription / subsequent call.
  5. Apply business filters (non-expired, target expiry).

Translate to Cortana (Python sketch):

class CortanaStrategy(Strategy):
    def on_start(self) -> None:
        venue = Venue("SMART")  # IBKR SMART routing
        instruments = self.cache.instruments(venue=venue, underlying="SPY")
        now_ns = self.clock.timestamp_ns()
        zero_dte = [
            inst for inst in instruments
            if inst.option_kind in (OptionKind.CALL, OptionKind.PUT)
            and inst.expiration_ns > now_ns
            and (inst.expiration_ns - now_ns) < ONE_DAY_NS
        ]
        # No explicit borrow drop - Python's RC handles it.
        # But still: read everything you need from the cache up front,
        # then do subscription work, then handler work.

Cortana-specific deviation. Python doesn’t have the Rc<RefCell<>> borrow contract, so the “cache borrow dropped here” comment is not load-bearing - but the discipline (read once, then act) is still right because the cache will be mutated by the DataEngine concurrently with strategy callbacks (per nautilus-architecture.md single-threaded dispatch - but if you yield mid-iteration, mutations land between calls).

Step 3 - Per-instrument Greeks subscription (Bybit-specific dead-end for Cortana)

Tutorial code:

let client_id = self.client_id;
for (instrument_id, _, _) in &options {
    self.subscribe_option_greeks(*instrument_id, Some(client_id), None);
    self.subscribed_instruments.push(*instrument_id);
}

Cortana-specific deviation - DO NOT TRANSFER. The subscribe_option_greeks adapter call is only implemented for Deribit, Bybit, OKX (per nautilus-greeks.md “Computation source” table and nautilus-ib.md Q2). IBKR’s adapter does not route the IB tick-13 model-Greeks frames through this method. If we call this against the IBKR client_id, we get a no-op or an error.

Cortana substitute - UW custom-data subscription per nautilus-custom-data.md:

class CortanaStrategy(Strategy):
    def on_start(self) -> None:
        # ... instrument discovery as above ...
        for inst in zero_dte:
            self.subscribe_data(
                data_type=DataType(UWOptionGreeks,
                                   metadata={"instrument_id": str(inst.id)}),
                client_id=ClientId("UW"),
            )
            # Plus: warm the local calculator with the underlying
            # quote subscription so it can fall back if UW WS drops.
            self.subscribe_quote_ticks(inst.id)
        # Underlying SPY quote for BS calculator + ATM tracking
        self.subscribe_quote_ticks(InstrumentId.from_str("SPY.ARCA"))

The handler shape on_option_greeks(&mut self, greeks: &OptionGreeks) translates to on_data(self, data: Data) with a type-check (Python pattern from nautilus-custom-data.md):

def on_data(self, data: Data) -> None:
    if isinstance(data, UWOptionGreeks):
        self._handle_greeks(data)

The Greeks field set is nearly identical. UW publishes delta, gamma, vega, theta, mark IV - same fields the tutorial reads. We can either define UWOptionGreeks as a @customdataclass mirroring OptionGreeks (recommended - minimal cognitive overhead, can swap later) or actually emit OptionGreeks via the UW DataClient (more ambitious - bypasses the custom-data layer but requires the UW adapter to publish the framework’s native type, which is a deeper hook).

Recommended path for spike: UWOptionGreeks as a @customdataclass. Wraps cleanly, replays through the catalog, and the strategy reads the same delta/gamma/vega/theta fields the tutorial reads.

Step 4 - Greeks handler (transfer verbatim - Python translation)

Tutorial code:

fn on_option_greeks(&mut self, greeks: &OptionGreeks) -> anyhow::Result<()> {
    log::info!(
        "GREEKS | {} | delta={:.4} gamma={:.6} vega={:.4} \
         theta={:.4} rho={:.6} | mark_iv={}",
        greeks.instrument_id,
        greeks.delta,
        greeks.gamma,
        greeks.vega,
        greeks.theta,
        greeks.rho,
        greeks.mark_iv.map_or("-".to_string(), |v| format!("{v:.2}"))
    );
    Ok(())
}

Transfer verbatim. The field-access pattern is universal: greeks.delta is an f64, greeks.mark_iv is an Option<f64>. In Python, greeks.mark_iv is float | None. The Deref<Target = OptionGreekValues> Rust trick (mentioned in nautilus-greeks.md Computation source) means Greeks fields are accessed directly off the OptionGreeks event without nesting - Python sees the same flat attribute access.

OptionGreeks field set (verbatim from tutorial):

FieldTypeDescriptionCortana usage
instrument_idInstrumentIdOption contractCache key for Position lookup
deltaf64dV/dSStrike-selection target (BULL = 0.30, BEAR = -0.30)
gammaf64dDelta/dSPower-hour acceleration regime input
vegaf64per 1% IV move (×0.01)IV-shock sizing veto
thetaf64per calendar day (×1/365.25)Power-hour decay regime input
rhof64per unit rIgnored (0DTE r-sensitivity ~ 0)
mark_ivOption<f64>Mark IVSmile / regime input
bid_iv / ask_ivOption<f64>Side-aware IVLiquidity gate (wide IV spread = stale)
underlying_priceOption<f64>Forward at expiryATM tracker input
open_interestOption<f64>OILiquidity gate

Critical: rho is in OptionGreeks (venue path) but not in GreeksData (local-calc path) - see nautilus-greeks.md Computation source. For Cortana 0DTE, rho is irrelevant; this asymmetry is harmless.

Step 5 - OptionSeriesId (transfer verbatim)

Tutorial code:

let series_id = OptionSeriesId::new(
    Venue::new("BYBIT"),
    Ustr::from("BTC"),
    Ustr::from("USDT"),
    UnixNanos::from(expiry),
);

Transfer verbatim (Python translation):

series_id = OptionSeriesId(
    venue=Venue("SMART"),       # or specific options exchange
    underlying="SPY",
    quote_currency="USD",
    expiration_utc=expiry_ns,
)

The four-tuple (venue, underlying, quote, expiry) is generic. Since SPY equity options are physically settled in shares (per nautilus-options.md Premium / mark / settlement), quote_currency is USD, not the underlying. SPX is cash-settled - same USD quote.

Step 6 - subscribe_option_chain (transfer with caveat)

Tutorial code:

let strike_range = StrikeRange::AtmRelative {
    strikes_above: 3,
    strikes_below: 3,
};
 
let snapshot_interval_ms = Some(5_000); // every 5 seconds
 
self.subscribe_option_chain(
    series_id,
    strike_range,
    snapshot_interval_ms,
    Some(client_id),
    None, // params
);

Transfer with caveat. The framework’s OptionChainManager (per nautilus-options.md Option chain architecture) handles the plumbing identically across venues. The caveat: the Greeks field on each OptionStrikeData returned by the framework is populated from the venue’s wire stream. For IBKR, no Greeks arrive on that path - so slice.get_call(strike).greeks will be None for every strike, every snapshot, every time.

Cortana’s options:

  1. Hydrate the chain Greeks ourselves. Subscribe to UW Greeks per instrument (Step 3 substitute), maintain a local Dict[InstrumentId, UWOptionGreeks], and join on every chain slice. Lose the framework’s “single slice with everything” atomicity - but gain working Greeks.
  2. Run the local GreeksCalculator per chain snapshot. On each on_option_chain callback, walk the slice, call calculator.instrument_greeks(strike_id) for each strike, build our own enriched slice. Adds compute cost per snapshot but stays inside Nautilus primitives.
  3. Custom OptionChainManager-equivalent. Don’t subscribe to the framework’s chain; build a Cortana actor that aggregates UW Greeks + IBKR quotes and publishes a custom CortanaChainSlice event. Most work; cleanest separation. Defer to MK3 M2+.

Recommendation for spike: Option (2). The framework already discovers + caches the chain instruments via build_options_chain=True on the IBKR config. Hydrating Greeks via the local calculator on each slice is one loop per snapshot - tractable, and Cortana’s BSM fallback is the right shape regardless.

Snapshot interval choice. The tutorial uses 5000ms - appropriate for crypto options. Cortana’s “early and right” mandate (project_early_and_right_mandate) wants fresher Greeks at entry time. snapshot_interval_ms=None (raw mode) on the chain plus per-tick processing in the strategy is closer to what we want, at the cost of higher CPU. Spike-day test: measure CPU at 1000ms vs 500ms vs None.

ATM-relative strike count. Tutorial uses (3, 3) = 7 strikes. SPY 0DTE has ~80-100 active strikes; Cortana’s strike-selection rule (buy delta-0.30 call) needs strikes far enough OTM that delta = 0.30 typically lands 4-8 strikes from ATM depending on IV. Recommended default: AtmRelative(8, 8) - covers the delta-0.30 zone in either direction with margin.

Step 7 - Chain handler (transfer verbatim)

Tutorial code:

fn on_option_chain(&mut self, slice: &OptionChainSlice) -> anyhow::Result<()> {
    log::info!(
        "OPTION_CHAIN | {} | atm={} | calls={} puts={} | strikes={}",
        slice.series_id,
        slice.atm_strike.map_or("-".to_string(), |p| format!("{p}")),
        slice.call_count(),
        slice.put_count(),
        slice.strike_count(),
    );
 
    for strike in slice.strikes() {
        if let Some(call) = slice.get_call(&strike) {
            let delta = call.greeks.as_ref().map(|g| g.delta);
            // process call
        }
        if let Some(put) = slice.get_put(&strike) {
            let delta = put.greeks.as_ref().map(|g| g.delta);
            // process put
        }
    }
    Ok(())
}

Transfer verbatim for the iteration shape. Python:

def on_option_chain(self, slice: OptionChainSlice) -> None:
    for strike in slice.strikes():
        call = slice.get_call(strike)
        if call is not None:
            delta = call.greeks.delta if call.greeks else None
            # for Cortana: hydrate from UW or local calc if None
        # ... same for puts

OptionChainSlice API recap (Cortana cheat sheet - verbatim from tutorial):

MethodReturnsCortana use
series_idOptionSeriesIdTrace logging, multi-series routing
atm_strikeOption<Price>ATM tracker output, regime input
call_count() / put_count()usizeLiquidity sanity check
strike_count()usizeActive strike set size
strikes()Vec<Price> (sorted)Strike-selection scan target
get_call(k) / get_put(k)Option<&OptionStrikeData>Per-strike read

Each OptionStrikeData carries quote: QuoteTick (bid/ask) and optional greeks: Option<OptionGreeks>. The quote is always present; greeks may be None (waiting for first Greeks event, or in Cortana’s case, IBKR-without-UW-Greeks situation).

Cortana subsections - patterns the next tutorial covers

The framing brief asked this page to cover strike-selection by delta-percentile, IV-based limit-order pricing, portfolio delta tracking, rehedge triggers, and session position hydration. The Bybit options-data tutorial does NOT cover these. They live in the sibling delta-neutral options strategy tutorial (also Bybit, Rust v2). This page describes them at the conceptual level here - authoritative coverage will live in the next tutorial brain page when filed (nautilus-tutorial-delta-neutral-options.md).

Strike selection by delta-percentile

Where it lives: delta-neutral options tutorial. The pattern from nautilus-options.md § “Strike selection by delta-percentile”:

def on_option_chain(self, slice: OptionChainSlice) -> None:
    target_delta = 0.30
    best_strike = None
    best_diff = float("inf")
    for strike in slice.strikes():
        call = slice.get_call(strike)
        if call is None or call.greeks is None:
            continue
        diff = abs(call.greeks.delta - target_delta)
        if diff < best_diff:
            best_diff = diff
            best_strike = strike

Transfer for Cortana - but the call.greeks source is UW custom data + local-calc fallback (see Step 3 substitute), not Bybit subscribe_option_greeks. The scan logic is venue-agnostic.

MK4+ flag. Cortana might want a richer target - e.g., “closest to delta-0.30 with OI > 500 and spread < 5%“. Layer those filters in the same loop:

if call.quote.bid_size < min_size:
    continue
spread_pct = (call.quote.ask_price - call.quote.bid_price) / call.quote.bid_price
if spread_pct > max_spread_pct:
    continue
if call.greeks.open_interest is None or call.greeks.open_interest < min_oi:
    continue

IV-based limit-order pricing

Where it lives: delta-neutral options tutorial. Concept: instead of a fixed-tick LIMIT relative to mid (e.g., mid + 0.02), price the limit at a target IV level computed via Black-Scholes inverse - “buy the call with IV equivalent to mark_iv + X bps”. This produces prices that auto-adjust as IV moves and avoids chasing on a fast tape.

Cortana V3 research flag. project_confluence_options.md already flags signal-latency reduction work; IV-based entry pricing is a candidate for the next research thread because it solves the “placed limit at 5.65 in 200ms, fill never came, signal aged out” failure mode. Use Nautilus’s imply_vol_and_greeks Rust/PyO3 function (per nautilus-greeks.md Black-Scholes functions) to invert IV → price at a chosen IV target.

Verdict for V1/V2: out of scope. V1 is MARKET orders for entry (thesis: speed > slippage). V2 may revisit when scoring stabilizes enough to absorb a few seconds of limit-fill latency.

Portfolio delta tracking

Where it lives: delta-neutral options tutorial. The GreeksCalculator.portfolio_greeks(...) call from nautilus-greeks.md § “Portfolio aggregation”:

pg = self.calculator.portfolio_greeks(
    underlyings=["SPY"],
    strategy_id=self.config.strategy_id,
)
# pg: PortfolioGreeks with pnl, price, delta, gamma, vega, theta

Filter by strategy_id for multi-strategy nodes - relevant once Cortana starts running multiple variants in parallel (MK3 M3+ multi-strategy phase).

For Cortana V1 single-leg: portfolio delta = position delta × signed_qty × multiplier - trivial. The full portfolio_greeks call is heavier than V1 needs but the right shape for V2+ when defined-risk spreads enter the picture (per nautilus-options.md Multi-leg primitives).

Rehedge triggers (Greeks-driven + timer-driven)

Where it lives: delta-neutral options tutorial. Two patterns:

  1. Greeks-driven: subscribe to per-instrument Greeks, on each update check abs(portfolio_delta) > threshold, hedge if so. The handler is on_option_greeks - fires every Greeks update.
  2. Timer-driven: self.clock.set_timer(...) (per nautilus-actors.md / nautilus-strategies.md) firing every N seconds, run portfolio_greeks, hedge on threshold cross.

Cortana V1 is directional, not delta-neutral - there is no “rehedge to flat” target. The pattern still applies inverted: a “rehedge” for Cortana would be position-management - TP1 trim, SL re-wider, exit-on-momentum-fade. The trigger structure (event-driven vs timer-driven) carries over even if the action (“hedge to flat” vs “adjust exit”) differs.

MK3 M2+ implication. A PositionManagerActor that subscribes to Greeks updates and re-evaluates exits on every change is the natural evolution of MK2’s PM. The Greeks-driven trigger pattern from this tutorial is the right shape for it.

Session position hydration / startup state recovery

Where it lives: delta-neutral options tutorial AND nautilus-execution.md § Reconcile-on-startup. The pattern: on LiveNode start, the framework’s LiveExecutionEngine automatically queries the venue for open positions, open orders, account state, and synthesizes events to bring the cache + portfolio in sync.

For Cortana (per nautilus-ib.md Reconnection behavior): IBKR’s adapter does this automatically. No strategy-side hydration code needed. When the strategy on_start() fires, the cache already reflects open positions; self.cache.positions(...) returns the real broker state.

This closes GH #46 by construction. MK2’s PM exit invariant (project_pm_ibkr_exit_invariant.md) - “did the broker actually close the position?” - becomes a non-question because the engine re-reads broker truth on every (re)connect.

Spike-day test (already in 2026-05-09-nautilus-spike.md): verify that after stopping the engine mid-position, restarting, on_start sees the open position via cache.positions(). If yes, this is the standalone win worth taking even if MK3 says NO-GO.

Cortana MK3 implications (M1-M2 milestone work)

M1 (Strategy plumbing):

  1. Build the UWOptionGreeks @customdataclass mirroring OptionGreeks field set (delta, gamma, vega, theta, mark_iv, underlying_price, open_interest, plus Cortana-specific extras like gex and flow_score if we want one event for everything).
  2. Wire the UW DataClient to publish these via _handle_data(...).
  3. Wire a CortanaStrategy on_data that type-checks and routes to a private _handle_greeks method matching the tutorial’s on_option_greeks shape.
  4. Subscribe to subscribe_option_chain with StrikeRange.AtmRelative(8, 8) against the IBKR-loaded SPY 0DTE series. Use snapshot_interval_ms=None (raw mode) for entry-time freshness.
  5. In on_option_chain, hydrate Greeks per-strike from either UW (cached from _handle_greeks) or GreeksCalculator.instrument_greeks (fallback). Carry the same field-iteration shape from the tutorial.
  6. Implement strike-selection by scanning the slice for abs(delta - 0.30) minimum (BULL CALL) or abs(delta - (-0.30)) (BEAR PUT). Reject strikes failing liquidity gates (OI, spread, bid_size).

M2 (PM + risk):

  1. Layer a PositionManagerActor that subscribes to UWOptionGreeks events for the open position and re-evaluates exit conditions on each update (Greeks-driven rehedge analog).
  2. RiskEngine subclass calls portfolio_greeks(strategy_id=...) pre-trade for delta-veto (per nautilus-greeks.md “Veto entry if portfolio delta > 0.5 SPY-equivalent”).
  3. Defer IV-based limit pricing - V3 research thread.

UW-side adapter glue needed (the missing piece):

The tutorial’s subscribe_option_greeks is a one-liner because Bybit’s adapter does the work. Cortana must build the equivalent UW glue. Per nautilus-custom-data.md and nautilus-ib.md UW-as-data-only sketch:

  1. A UWLiveDataClient (subclass LiveDataClient) opening the UW WebSocket.
  2. On each UW frame: build a UWOptionGreeks instance with ts_event = uw_payload.ts_ms * 1_000_000, ts_init = self._clock.timestamp_ns(), fields parsed from the payload.
  3. self._handle_data(greeks) to route through DataEngine → Cache → MessageBus.
  4. Subscription routing: subscribe_data(DataType(UWOptionGreeks, metadata={"underlying": "SPY"})) from the strategy fans out to all SPY 0DTE Greeks updates without per-instrument subscriptions.

This adapter glue is the load-bearing M1 deliverable. Without it, the tutorial’s whole on_option_greeks pattern doesn’t work for Cortana - not because the framework lacks it, but because the specific UW-IBKR-substituted-for-Bybit wiring is Cortana’s to build.

Open spike-day questions (specific to this tutorial’s patterns)

  1. Does subscribe_option_chain work over the IBKR adapter at all? Per nautilus-options.md Adapter Support table, only Deribit / Bybit / OKX list ✓ for subscribe_option_chain. Test on Saturday: call subscribe_option_chain against the IBKR client, confirm whether (a) it returns OptionChainSlice events with hydrated quotes (Greeks None), (b) it errors out, or (c) it silently does nothing. If (b) or (c), Cortana must build a custom CortanaChainAggregator actor - defer to M2.
  2. What’s the latency of UW → strategy on_data callback? UW publishes ~5-10 events/sec for SPY chain; round-trip through the DataClient → DataEngine → MessageBus → Strategy is the path. If median latency > 50ms, the “early and right” mandate is at risk for entry decisions.
  3. Does cache.instruments(venue, underlying) return the IBKR 0DTE chain after build_options_chain=True materialization? The tutorial assumes the chain is in the cache by on_start. Confirm IBKR chain materialization is complete before the strategy’s on_start fires (or that the strategy can safely defer subscription to on_data after instruments arrive).
  4. StrikeRange.AtmRelative cooldown / hysteresis behavior (already a question in nautilus-options.md) - does it churn during 9:30 ET open volatility?

See Also

  • Nautilus Greeks - OptionGreeks vs GreeksData, local GreeksCalculator, dual-path decision (UW primary live, BSM primary backtest)
  • Nautilus Options - OptionContract, OptionChainSlice, StrikeRange variants, ATM tracker
  • Nautilus Strategies - Strategy lifecycle, on_data / on_option_chain / on_option_greeks handlers
  • Nautilus Actors - Actor vs Strategy decision, subscribe_data patterns
  • Nautilus Custom Data - @customdataclass, UW custom DataClient sketch, the missing UW-side adapter glue
  • Nautilus IBKR - IBKR adapter limits, no native Greeks streaming, Greeks gap (carryover #8) decision
  • Nautilus Tutorials - index page; flags the delta-neutral tutorial as the next one to read for the patterns this tutorial omitted
  • Nautilus Instruments - OptionContract metadata, multiplier, expiry, strike fields
  • 2026-05-09 Nautilus Spike Plan: ~/conductor/workspaces/cortanaroi-mk2/belo-horizonte/plans/2026-05-09-nautilus-spike.md
  • MK3 Roadmap: ~/conductor/workspaces/cortanaroi-mk2/belo-horizonte/plans/2026-05-09-mk3-roadmap.md
  • Source: https://nautilustrader.io/docs/latest/tutorials/options_data_bybit/
  • Companion (next to file): delta-neutral options strategy tutorial - covers strike-selection by delta-percentile, IV-based limit pricing, portfolio delta tracking, rehedge triggers, session position hydration

Timeline

2026-05-07 | Cody - Filed during pre-spike concept mastery sweep batch 5 (tutorials).