Nautilus Tutorial - Delta-Neutral Options (Bybit)

The Bybit delta-neutral options tutorial is a Rust-only v2 LiveNode strategy that sells an OTM short strangle on BTC options and delta-hedges via the BTCUSDT linear perpetual whenever portfolio delta drifts past a threshold. The thesis (short vol + continuous delta hedge) is the opposite of Cortana V1 (long premium, directional, single-leg). We file this page anyway because it is the only end-to-end live options strategy in the Nautilus docs, and the plumbing patterns - portfolio Greeks aggregation, strike selection by delta percentile, dual-trigger rehedge logic (event + timer), startup position hydration, and reconciliation-aware live wiring - are directly reusable for Cortana MK3 risk gates, contract selection, EOD power-hour timers, and startup-state rebuild. This page walks the tutorial step-by-step, labeling every pattern as Reusable, Strategy-thesis-specific (skip), or Bybit-adapter-specific (translate to IBKR + UW), and closes with the 3-5 specific code shapes Cortana MK3 should lift.

Core claim

The tutorial is architecturally instructive even though its strategy thesis is the opposite of Cortana’s. Three reusable patterns in particular are load-bearing for MK3:

  1. Portfolio delta computed as Σ(leg_delta × leg_position) + hedge_pos
    • a one-line aggregation pattern Cortana’s RiskEngine rule will use to veto entries that breach a portfolio-gamma cap.
  2. Strike selection via delta-percentile sort over the option chain - identical shape to Cortana’s “buy the call closest to delta=0.30” rule.
  3. Dual-trigger rehedge: Greeks-update-driven AND periodic timer-driven
    • the exact shape of Cortana’s “EOD power hour” timer + tick-driven exit logic.

What we explicitly do not lift:

  • The short-strangle entry geometry (Cortana is single-leg long premium).
  • Any vol-selling thesis or vega-management heuristics (we are long vol on momentum).
  • The Bybit order_iv / orderIv IV-priced order primitive (IBKR has no equivalent; we price via premium, not IV).
  • The BybitProductType::Option + Linear cross-product wiring (Cortana is single-product on IBKR with UW as data-only).

Tutorial at a glance - the five-stage pipeline

The tutorial decomposes the strategy into five stages. For each, we label the lift-status for Cortana.

StageTutorial behaviorCortana lift-status
1. Strike selectionFilter BTC options to nearest expiry, pick OTM call at 80th-percentile delta and OTM put at 20th-percentile deltaReusable - same shape, different target percentile (~30-delta long instead of ~20-delta short)
2. EntryPlace SELL limit orders on both legs priced via implied volatility (order_iv)Strategy-thesis-specific + Bybit-specific - Cortana places BUY market or marketable-limit orders priced by premium
3. Greeks trackingSubscribe OptionGreeks per leg; deltas/IVs from Bybit ticker streamBybit-adapter-specific - translate to UW custom data feed since IBKR has no native Greeks adapter
4. RehedgingWhen portfolio delta breaches threshold, market-order hedge on perp; trigger on every Greeks update and on a periodic safety timerReusable pattern - Cortana repurposes the dual-trigger shape for EOD power-hour entries and TP/SL software fallback
5. Position trackingon_order_filled updates leg/hedge positions; cache-hydrate on startup so a restart doesn’t lose stateReusable - Nautilus reconciliation does most of this, but the with_reconciliation(true) + cache-hydration pattern is the canonical shape

Step 1 - Strike selection by delta percentile

Lift-status: Reusable pattern for Cortana.

The tutorial:

“queries the instrument cache for all BTC options, filters to the nearest expiry, selects OTM call and put strikes by percentile rank. Call index: (1.0 - target_call_delta) * count → 80th percentile. Put index: |target_put_delta| * count → 20th percentile.”

The shape - sort the chain by delta, index by percentile - is exactly what Cortana’s plans/2026-05-06-contract-selection-plan.md calls for. The only differences:

  • Direction: tutorial picks short legs (OTM call + OTM put). Cortana picks one long leg (ATM-ish call OR ATM-ish put depending on side).
  • Target delta: tutorial uses 0.20 / -0.20 (deep OTM short strangle premium-collection profile). Cortana uses 0.30 / -0.30 (long premium with reasonable gamma but not pinned ATM).
  • Two legs vs. one leg: tutorial picks both. Cortana picks one.

The reusable Cortana shape (already documented in nautilus-options.md):

def on_option_chain(self, chain) -> None:
    target_delta = 0.30
    best_strike = None
    best_diff = float("inf")
    for strike in chain.strikes():
        leg = chain.get_call(strike) if self.side == "BULL" else chain.get_put(strike)
        if leg is None or leg.greeks is None:
            continue
        delta = leg.greeks.delta
        diff = abs(delta - target_delta) if self.side == "BULL" else abs(delta - (-target_delta))
        if diff < best_diff:
            best_diff = diff
            best_strike = strike
    if best_strike is not None:
        self._chosen_id = (chain.get_call(best_strike) if self.side == "BULL"
                           else chain.get_put(best_strike)).quote.instrument_id

The tutorial’s percentile-index trick ((1.0 - target) * count) is a sortable-by-delta shortcut; Cortana’s “nearest delta” formulation is equivalent and cheaper on a streaming chain where the sort isn’t amortized.

Step 2 - Entry (Strategy-thesis-specific; SKIP)

Lift-status: Strategy-thesis-specific + Bybit-adapter-specific. Do not copy.

The tutorial places SELL limit orders priced by implied volatility using Bybit’s order_iv parameter, which the adapter maps to orderIv in the place-order payload. Bybit converts the IV to a server-side limit price and gives it precedence over any explicit price. The entry_iv_offset config subtracts vol points from mark IV for faster fills (sell two vol below mark for marketable-but-not-aggressive entry).

Why we don’t copy this:

  1. Wrong direction. Cortana buys premium; tutorial sells.
  2. No IBKR equivalent. IBKR’s API does not accept IV-priced orders. We price via premium (or use MARKET with a software-fallback LIMIT per feedback_dual_tp_defense_in_depth.md).
  3. Bybit-only quirk. The doc explicitly notes: “Bybit demo environment rejects IV orders; mainnet/testnet required.” Even if we were short-vol, this would not transfer.

Cortana’s entry geometry (per nautilus-orders.md and nautilus-ib.md):

bracket = self.order_factory.bracket(
    instrument_id=chosen_id,
    order_side=OrderSide.BUY,
    quantity=instrument.make_qty(contracts),
    tp_price=instrument.make_price(entry * 1.10),
    sl_trigger_price=instrument.make_price(entry * 0.75),
    tp_tags=[oca_tags.value],
    sl_tags=[oca_tags.value],
)
self.submit_order_list(bracket)

Different shape entirely. The tutorial’s entry is in the “did not transfer” pile.

Step 3 - Portfolio Greeks aggregation

Lift-status: Reusable pattern for Cortana - this is the single most useful pattern in the tutorial.

The tutorial’s portfolio-delta computation:

portfolio_delta = call_delta * call_position
                + put_delta * put_position
                + hedge_position

Three observations:

  1. It walks positions and multiplies by per-leg delta. This is the manual form of GreeksCalculator.portfolio_greeks() (see nautilus-greeks.md), used here because the tutorial sources Greeks from Bybit’s venue stream rather than the local calculator. The logical shape is identical.
  2. The hedge perpetual contributes delta = 1.0 automatically - that’s why the formula adds raw hedge_position rather than hedge_position * hedge_delta. Equities/futures behave the same way (per nautilus-greeks.md: “non-option instruments contribute delta=1 and no higher-order Greeks”).
  3. Direction matters. The tutorial’s call/put positions are negative (short), so the formula naturally captures signed exposure.

The Cortana-relevant rewrite

For Cortana, the equivalent “what’s my portfolio delta right now” query collapses to:

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

This is the cleanest pattern from the tutorial that we’d lift directly - except instead of hand-rolling the Σ(leg_delta × pos), Cortana uses Nautilus’s built-in portfolio_greeks() which does the aggregation across every open Position with strategy-id filtering. The tutorial does it by hand because Bybit’s venue Greeks stream attaches sensitivities to instruments, not positions; the calculator path that Cortana uses returns position-aware aggregation natively.

When Cortana would use the tutorial’s manual form

If we ever wanted conditional aggregation that portfolio_greeks() doesn’t expose (e.g., “delta of just the BULL positions opened in the last 5 minutes”), the manual walk-the-positions pattern is the fallback. Pseudocode:

delta_sum = 0.0
for pos in self.cache.positions_open(strategy_id=self.config.strategy_id):
    if not self._is_recent_bull(pos):
        continue
    g = self.calculator.instrument_greeks(
        instrument_id=pos.instrument_id,
        update_vol=True, cache_greeks=True,
    )
    if g is None:
        continue
    delta_sum += pos.signed_qty * g.delta * float(g.multiplier)
return delta_sum

Same pattern as the tutorial; just filtered.

Step 4 - Rehedge logic (the load-bearing transferable pattern)

Lift-status: Reusable pattern for Cortana - this is pattern #2 of the lift list.

The tutorial uses two independent triggers for the same rehedge action:

  1. Event-driven - on_option_greeks recomputes portfolio delta after updating the leg’s delta. If |portfolio_delta| > threshold, fire a market hedge order.
  2. Timer-driven - fires every rehedge_interval_secs (default 30s) as a safety net “when Greeks updates stop arriving.”

Plus a hedge_pending flag that prevents duplicate submissions while an order is in flight.

Why this dual-trigger pattern is exactly what Cortana needs

Cortana’s project_eod_power_hour.md mandate is “the last 15-30 min before close is a first-class regime; the system must detect and play it.” That detection is naturally timer-driven - set an alert at 14:30 CT, react. Cortana’s TP/SL software fallback (feedback_dual_tp_defense_in_depth.md) is naturally event-driven

  • react to every quote tick.

The tutorial fuses both into one rehedge action. Cortana’s adaptation:

# In on_start():
self.clock.set_timer(
    name="REHEDGE_SAFETY",
    interval=pd.Timedelta(seconds=30),
)
self.clock.set_time_alert(
    name="POWER_HOUR_ENTER",
    alert_time=self._market_close() - pd.Timedelta(minutes=30),
)
 
# Event-driven trigger
def on_quote_tick(self, tick) -> None:
    self._maybe_act(reason="tick")
 
# Timer-driven trigger
def on_event(self, event) -> None:
    if isinstance(event, TimeEvent):
        if event.name == "REHEDGE_SAFETY":
            self._maybe_act(reason="timer")
        elif event.name == "POWER_HOUR_ENTER":
            self._regime = "POWER_HOUR"
 
def _maybe_act(self, reason: str) -> None:
    if self._action_pending:
        return  # mirror tutorial's hedge_pending
    pg = self.calculator.portfolio_greeks(...)
    if self._should_exit(pg):
        self._action_pending = True
        self.submit_order(self._build_exit_order())

The _action_pending flag, the dual-trigger, the threshold-on-Greeks shape - all lifted directly. The action itself is different (exit position vs hedge position) but the decision plumbing is identical.

What the tutorial gets right that we should preserve

  • Idempotency via in-flight flag. A flag is more reliable than cache-counting open orders because it spans the submit→ack window where the order is in-flight but not yet in cache.orders_open().
  • Both triggers call the same decision function. No code duplication. The decision is “what should I do given current portfolio state?” and the triggers just decide “when to ask.”
  • The timer is a safety net, not the primary trigger. The event path is fast; the timer covers Greeks-stream gaps. For Cortana, the primary path is the quote tick (every ~50ms in liquid SPY); the timer covers thin-market gaps and is the heartbeat for regime transitions.

Step 5 - Position tracking and startup hydration

Lift-status: Reusable pattern, mostly free under Nautilus reconciliation.

The tutorial:

“tracks call, put, and hedge positions via on_order_filled. Hydrates existing positions from the cache at startup.”

Coupled with the node config:

LiveNode::builder(...)
    .with_reconciliation(true)
    .with_delay_post_stop_secs(5)
    .build()?

The tutorial explicitly notes: “with_reconciliation(true) queries Bybit at startup for open orders and positions, hydrating the cache before the strategy starts.”

This is the same LiveExecutionEngine reconciliation we documented in nautilus-execution.md § “Reconcile-on-startup pattern” and nautilus-ib.md § “Engine-level reconciliation on reconnect.” For Cortana, the equivalent is:

exec_client_config = InteractiveBrokersExecClientConfig(
    ...,
    fetch_all_open_orders=False,  # single-process Cortana
    track_option_exercise_from_position_update=False,  # V1 flat-by-close
)

plus the standard LiveNode / TradingNode config which has reconciliation on by default.

The strategy-side cleanup the tutorial does in on_order_filled is unnecessary under Nautilus’s reconciliation guarantees - the engine itself is responsible for converging Cache to broker truth. The tutorial’s manual hydration is a defensive belt-and-suspenders pattern that Cortana can adopt for the same reason: even with reconciliation, holding a per-strategy in-memory map of “which position-id is the current entry for this trigger” is useful for strategy logic. The map is a cache of cache state, not a parallel source of truth - the moment of truth is cache.positions_open(...).

Risk-engine integration - what the tutorial does NOT do

Lift-status: Strategy-thesis-specific gap. Cortana fills it.

The tutorial does not appear to use a custom RiskEngine rule. The “max delta breach threshold” is enforced inside the strategy’s rehedge loop, not in the RiskEngine. This is fine for a single-strategy live node where the strategy is its own arbiter, but Cortana wants stronger structural guarantees per nautilus-execution.md § “GH #88 dead-code meta-prob sizing”:

  • Cortana’s meta-prob sizing must live in the RiskEngine (or a pre-submit Actor) so it cannot become dead code.
  • A portfolio-gamma-cap veto is a natural RiskEngine rule too: every entry order routes through the RiskEngine; the rule reads the current portfolio_greeks(...) and either scales quantity, vetoes via OrderDenied, or passes.

The tutorial’s pattern of “compute delta, decide action” lives at the strategy level. Cortana would lift the computation but move the gate into the RiskEngine. Pseudocode for the Cortana RiskEngine rule:

def pre_trade_check(self, command: SubmitOrder) -> bool:
    pg = self.calculator.portfolio_greeks(
        underlyings=["SPY"],
        strategy_id=command.strategy_id,
    )
    projected_gamma = pg.gamma + self._projected_contribution(command).gamma
    if abs(projected_gamma) > self.config.max_portfolio_gamma:
        return False  # OrderDenied with reason="portfolio gamma cap"
    return True

This is the structural fix the tutorial doesn’t need (single-strategy node) but Cortana does (multi-trigger strategy where any of 5 triggers could push us into a gamma overhang).

Backtest replay setup - reuse pattern

Lift-status: Reusable pattern.

The tutorial is a live-only example (it runs against Bybit’s live or testnet WebSocket). It does not include a paired backtest setup. For Cortana, the backtest path is canonical:

  1. Catalog: ParquetDataCatalog holding (a) IBKR SPY quote ticks, (b) IBKR option quote ticks for the 0DTE chain, (c) UW Greeks as either OptionGreeks events or a custom @customdataclass stream, (d) UW flow/GEX as custom data types.
  2. BacktestEngine: standard high-level config-driven setup per nautilus-tutorials.md § “Backtest (High-Level API).”
  3. Strategy: same CortanaStrategy class as live (the framework makes this code-identical per nautilus-strategies.md § “Backtest vs live”).

The tutorial does exercise reconciliation (with_reconciliation(true)) which only matters in live mode. For Cortana’s backtest catalog, the deterministic path uses GreeksCalculator over IBKR mid prices (per nautilus-greeks.md § “Backtest replay reproduction”); for live, UW Greeks dominate.

Bybit-adapter-specific items to skip

Lift-status: Skip / translate.

The tutorial includes several Bybit-only mechanics that have no IBKR analog:

Bybit patternIBKR translation
BybitProductType::Option + Linear cross-product subscriptionIBKR adapter doesn’t separate “option” from “linear” - both are products under one execution client. Single InteractiveBrokersExecClientConfig per tenant.
order_iv / orderIv IV-priced limit ordersNo equivalent. Use premium-priced LIMIT or MARKET orders.
Bybit’s “demo environment rejects IV orders” footgunNot applicable. IBKR paper accepts the same order types as live.
Single-Greeks-source from Bybit ticker streamIBKR has no Greeks stream. Use GreeksCalculator (local BSM) or UW custom data feed.
ClientId namespacing as BYBIT-DELTA-NEUTRAL-001Cortana uses IB-{tenant_id} naming per nautilus-ib.md § per-tenant clients.

Cortana MK3 implications - the lift list

The 3-5 specific code patterns to copy-adapt into Cortana strategies/actors:

1. Strike selection by delta percentile (already lifted in nautilus-options.md)

Cortana’s contract-selection rule is the same shape as the tutorial’s strike picker. Already documented in nautilus-options.md § “Strike selection by delta-percentile - the cleanest pattern.” This tutorial confirms the pattern is canonical.

2. Portfolio Greeks aggregation for the meta-prob / gamma RiskEngine rule

The tutorial’s Σ(leg_delta × leg_position) + hedge_position formula is the manual form of what Cortana gets for free via GreeksCalculator.portfolio_greeks(underlyings=["SPY"], strategy_id=...). Used inside a custom RiskEngine rule, this is the structural fix for GH #88 (dead-code meta-prob sizing) - every order routes through the rule by construction.

Cleanest pattern from the tutorial that we’d lift directly:

# Inside a Cortana RiskEngine rule or pre-submit Actor
pg = self.calculator.portfolio_greeks(
    underlyings=["SPY"],
    strategy_id=command.strategy_id,
)
if abs(pg.gamma) > self.config.max_portfolio_gamma:
    return OrderDenied(reason="portfolio gamma cap exceeded")

One call, position-aware, strategy-id-scoped, replaces the tutorial’s hand-rolled walk.

3. Dual-trigger (event + timer) decision pattern

Use the tutorial’s “rehedge on every Greeks update + safety timer every N seconds, gated by an in-flight flag” shape for:

  • EOD power-hour entry gating: timer fires at 14:30 CT to set regime; quote ticks decide on entry; _action_pending flag prevents double-submission across overlapping decision points.
  • TP/SL software fallback: quote ticks evaluate TP/SL trigger; periodic timer is the safety net for thin-market quote gaps; _action_pending flag prevents racing the broker LMT.
  • Rehedge gating (if MK4+ ever adds delta-hedging on larger-multiplier positions): exact same pattern.

The dual-trigger shape with one decision function and an in-flight flag is the specific pattern.

4. Startup hydration via reconciliation

with_reconciliation(true) (or its Python LiveNode equivalent) combined with cache.positions_open(strategy_id=self.id) at strategy on_start is the supported pattern for “what positions does this strategy own across a restart?” Cortana’s MK2 tracker-rehydration work disappears under this model.

5. Live-node config shape with named ClientIds

LiveNode::builder(trader_id, environment)?
    .with_name("BYBIT-DELTA-NEUTRAL-001".to_string())
    .add_data_client(None, ...)
    .add_exec_client(None, ...)
    .with_reconciliation(true)
    .with_delay_post_stop_secs(5)
    .build()?

For Cortana’s Python TradingNode, the equivalent is the per-tenant exec_clients={"IB-{tenant}": ...} pattern from nautilus-ib.md. Same idea: named clients, per-tenant routing, reconciliation on, graceful stop delay.

Honest disclaimer - what NOT to copy

  • The delta-neutral thesis itself. Selling vol is structurally opposite to Cortana’s directional buy-side play. Do not absorb the short-strangle rationale or any vega-management from the tutorial
    • they would actively harm a long-premium book.
  • The IV-priced order primitive. Bybit-only; no IBKR analog; Cortana prices via premium not IV.
  • The pair-of-options entry geometry. Cortana V1 is single-leg; V2+ defined-risk plays might add spreads but those use Nautilus’s OptionSpread primitive (per nautilus-options.md), not the tutorial’s two-independent-legs shape.
  • Bybit-specific adapter wiring (BybitProductType::Option + Linear, order_iv, demo-environment caveats). Not transferable to IBKR.
  • The Rust v2 idiom. Cortana is Python. The patterns translate but the actual code does not - the tutorial is in crates/trading/ src/examples/strategies/delta_neutral_vol/, all Rust.

Caveats and gotchas

  • Tutorial is Rust v2 only. No Python port exists in the docs. Patterns transfer; literal code does not.
  • No backtest paired setup. Live-only. Cortana adds the catalog
    • BacktestEngine layer separately.
  • No RiskEngine custom rule. The tutorial enforces the delta threshold inside strategy code. Cortana should move equivalent gates into the RiskEngine for the structural-by-construction property.
  • Bybit demo environment rejects IV orders. A live-only curiosity, not relevant to Cortana.
  • enter_strangle: false default in the example runner. The example does not actually place strangle entries by default - it hydrates existing positions and only manages hedging. This is a defensive shape: someone running the example in production needs to opt in to entries. Cortana’s parallel: an enabled: bool config flag on the entry path so paper and live can run the same code with entries off until paper-validated.
  • hedge_pending flag is in-strategy state, not in Cache. A restart loses the flag. Reconciliation on startup will detect in-flight orders and re-emit events, so the practical impact is bounded - but the flag itself isn’t durable. Cortana’s analog flag should clear on startup and re-derive from cache.orders_inflight(strategy_id=...).

When this pattern applies (Cortana MK3)

  • Designing the RiskEngine rule for portfolio-gamma cap and meta-prob sizing.
  • Designing the EOD power-hour timer + tick-driven entry gate.
  • Designing the TP/SL software fallback shape.
  • Reviewing the strike-selection module for the V1 BULL CALL / BEAR PUT picker.
  • Standing up the live TradingNode with reconciliation, named per-tenant clients, and graceful stop.

When it doesn’t apply

  • Anything about short-vol thesis, vega management, or delta-neutral hedging strategy. Cortana is the opposite trade.
  • IV-priced limit orders. IBKR has none.
  • Two-leg straddle / strangle entry. Cortana is single-leg V1.
  • Rust v2 strategy authoring patterns. Cortana is Python.

See Also

  • Tutorial - Options Data and Greeks (Bybit)
    • parallel sibling tutorial; Greeks subscription patterns and OptionSeriesId / StrikeRange config (read after this page when the data-only flow matters more than the strategy thesis)
  • Nautilus Greeks - local GreeksCalculator, portfolio_greeks() aggregation, BS conventions; this tutorial’s manual Σ(leg_delta × pos) is the manual form of what the calculator does for free
  • Nautilus Options - strike selection by delta-percentile (Cortana’s V1 pattern; this tutorial confirms it as canonical)
  • Nautilus Portfolio - portfolio.net_exposures, is_completely_flat(), query patterns; the right home for Cortana’s “what’s my book right now” calls
  • Nautilus Positions - Position lifecycle, reconciliation guarantees, the engine-owned single store; tutorial’s position hydration leans on this
  • Nautilus Strategies - Strategy lifecycle, set_timer / set_time_alert, on_event dispatch; the dual-trigger pattern lives here
  • Nautilus Execution - RiskEngine integration, OrderDenied events, reconcile-on-startup; where Cortana’s gamma-cap rule lives
  • Nautilus IBKR Integration - IBKR adapter specifics (Greeks gap, OCA tags, Dockerized Gateway); contrast with Bybit
  • Nautilus Tutorials - tutorial index; this page is the deep-dive on entry #14 in the index
  • 2026-05-09 Nautilus Spike Plan
    • Saturday evaluation; this page supports Steps 4-5 (port a signal, validate Strategy outputs)
  • 2026-05-09 MK3 Roadmap
    • 6-milestone SaaS roadmap; this page supports M2 (signal + entry port) and M3 (RiskEngine rule for meta-prob)
  • Brain RESOLVER - page filing rules

Timeline

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