Nautilus Tutorial - Delta-Neutral Options (Bybit)
The Bybit delta-neutral options tutorial is a Rust-only v2
LiveNodestrategy 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:
- 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.
- Strike selection via delta-percentile sort over the option chain - identical shape to Cortana’s “buy the call closest to delta=0.30” rule.
- 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/orderIvIV-priced order primitive (IBKR has no equivalent; we price via premium, not IV). - The
BybitProductType::Option+Linearcross-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.
| Stage | Tutorial behavior | Cortana lift-status |
|---|---|---|
| 1. Strike selection | Filter BTC options to nearest expiry, pick OTM call at 80th-percentile delta and OTM put at 20th-percentile delta | Reusable - same shape, different target percentile (~30-delta long instead of ~20-delta short) |
| 2. Entry | Place 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 tracking | Subscribe OptionGreeks per leg; deltas/IVs from Bybit ticker stream | Bybit-adapter-specific - translate to UW custom data feed since IBKR has no native Greeks adapter |
| 4. Rehedging | When portfolio delta breaches threshold, market-order hedge on perp; trigger on every Greeks update and on a periodic safety timer | Reusable pattern - Cortana repurposes the dual-trigger shape for EOD power-hour entries and TP/SL software fallback |
| 5. Position tracking | on_order_filled updates leg/hedge positions; cache-hydrate on startup so a restart doesn’t lose state | Reusable - 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_idThe 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:
- Wrong direction. Cortana buys premium; tutorial sells.
- No IBKR equivalent. IBKR’s API does not accept IV-priced orders.
We price via premium (or use
MARKETwith a software-fallback LIMIT perfeedback_dual_tp_defense_in_depth.md). - 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:
- It walks positions and multiplies by per-leg delta. This is the
manual form of
GreeksCalculator.portfolio_greeks()(seenautilus-greeks.md), used here because the tutorial sources Greeks from Bybit’s venue stream rather than the local calculator. The logical shape is identical. - The hedge perpetual contributes
delta = 1.0automatically - that’s why the formula adds rawhedge_positionrather thanhedge_position * hedge_delta. Equities/futures behave the same way (pernautilus-greeks.md: “non-option instruments contributedelta=1and no higher-order Greeks”). - 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.thetaThis 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_sumSame 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:
- Event-driven -
on_option_greeksrecomputes portfolio delta after updating the leg’s delta. If|portfolio_delta| > threshold, fire a market hedge order. - 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 scalesquantity, vetoes viaOrderDenied, 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 TrueThis 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:
- Catalog:
ParquetDataCatalogholding (a) IBKR SPY quote ticks, (b) IBKR option quote ticks for the 0DTE chain, (c) UW Greeks as eitherOptionGreeksevents or a custom@customdataclassstream, (d) UW flow/GEX as custom data types. - BacktestEngine: standard high-level config-driven setup per
nautilus-tutorials.md§ “Backtest (High-Level API).” - Strategy: same
CortanaStrategyclass as live (the framework makes this code-identical pernautilus-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 pattern | IBKR translation |
|---|---|
BybitProductType::Option + Linear cross-product subscription | IBKR adapter doesn’t separate “option” from “linear” - both are products under one execution client. Single InteractiveBrokersExecClientConfig per tenant. |
order_iv / orderIv IV-priced limit orders | No equivalent. Use premium-priced LIMIT or MARKET orders. |
| Bybit’s “demo environment rejects IV orders” footgun | Not applicable. IBKR paper accepts the same order types as live. |
| Single-Greeks-source from Bybit ticker stream | IBKR has no Greeks stream. Use GreeksCalculator (local BSM) or UW custom data feed. |
ClientId namespacing as BYBIT-DELTA-NEUTRAL-001 | Cortana 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_pendingflag 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_pendingflag 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
OptionSpreadprimitive (pernautilus-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: falsedefault 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: anenabled: boolconfig flag on the entry path so paper and live can run the same code with entries off until paper-validated.hedge_pendingflag 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 fromcache.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
TradingNodewith 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/StrikeRangeconfig (read after this page when the data-only flow matters more than the strategy thesis)
- parallel sibling tutorial; Greeks subscription patterns and
- 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_eventdispatch; the dual-trigger pattern lives here - Nautilus Execution - RiskEngine integration,
OrderDeniedevents, 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).