NautilusTrader
Tutorials

Nightly docs

These docs track unreleased changes and may change without notice. Their code examples can use APIs that the latest release lacks, so run them with a development wheel or switch to the latest release docs.

Delta-Neutral Options Strategy (Derive)

This is a Rust-only system tutorial. It runs the live delta-neutral short-volatility strategy on Derive using the Rust LiveNode.

This tutorial runs the shared DeltaNeutralVol strategy with the Derive adapter. The shipped example discovers ETH options, selects an out-of-the-money call and put, subscribes to venue-provided Greeks, and delta-hedges with ETH-PERP.DERIVE.

The Derive runner starts in hedge-only mode: it sets enter_strangle: false, so it does not place the initial option entry orders. Hedging is on by default (HEDGE_ENABLED = true), so it still hydrates existing positions through reconciliation and can submit market hedge orders on the perpetual when portfolio delta breaches the configured threshold. The runner reads its settings from constants at the top of the example file, not from environment variables. For smoke tests, set HEDGE_ENABLED to false and rebuild to keep the strategy from submitting hedge orders while it still loads instruments, reconciles the account, and subscribes to Greeks. For an entry-order smoke test, set ENTER_STRANGLE to true; the runner submits Derive-premium option orders instead of IV-priced option orders.

This strategy can trade real money on mainnet. Setting enter_strangle: false only disables the initial strangle entry orders. If the selected option legs or the hedge instrument already have open positions, the strategy can submit hedge orders.

Prerequisites

  • Completion of the Derive integration guide, including wallet, subaccount, session-key, and funding setup.
  • A Derive testnet or mainnet subaccount with enough USDC collateral for the hedge orders you plan to allow.
  • A working Rust toolchain and a built NautilusTrader workspace.
  • Credential environment variables for the Derive environment the example targets. The execution client reads them when the config leaves the credential fields unset.

For testnet:

export DERIVE_TESTNET_WALLET_ADDRESS="0x..."
export DERIVE_TESTNET_SESSION_PRIVATE_KEY="0x..."
export DERIVE_TESTNET_SUBACCOUNT_ID="12345"

For mainnet:

export DERIVE_WALLET_ADDRESS="0x..."
export DERIVE_SESSION_PRIVATE_KEY="0x..."
export DERIVE_SUBACCOUNT_ID="12345"

The example targets testnet through the DERIVE_ENVIRONMENT constant (DeriveEnvironment::Testnet) in node_delta_neutral.rs. It does not read the network from the environment. Edit the constant to DeriveEnvironment::Mainnet and rebuild only for real-funds runs.

Strategy overview

The DeltaNeutralVol strategy lives in the trading crate's examples module. The Derive runner uses it in five stages:

  1. Instrument load: configures the Derive data client with currencies: ["ETH"], so the adapter loads ETH perps and options into the cache.
  2. Strike selection: filters the cache to live ETH options, selects the nearest expiry, then chooses OTM call and put strikes by percentile rank.
  3. Greeks tracking: subscribes to OptionGreeks for both legs. Derive Greeks come from the shared ticker_slim feed and the option_pricing payload.
  4. Rehedging: computes portfolio delta and submits a Derive market order on ETH-PERP.DERIVE when the threshold is breached.
  5. Position tracking: tracks call, put, and hedge positions through fills. Reconciliation hydrates existing positions before the strategy starts.

Portfolio delta

The strategy computes net exposure as:

portfolio_delta = call_delta * call_position
                + put_delta * put_position
                + hedge_position

A short strangle starts close to delta-neutral when the call and put deltas offset. As the underlying moves, net delta drifts and the strategy hedges with the perpetual to bring the portfolio back toward zero.

Configuration

The example runner at crates/adapters/derive/examples/node_delta_neutral.rs sets its configuration through constants at the top of the file. Edit them and rebuild to change the run; cargo run rebuilds the example after an edit.

const DERIVE_ENVIRONMENT: DeriveEnvironment = DeriveEnvironment::Testnet;

const OPTION_FAMILY: &str = "ETH";
const HEDGE_INSTRUMENT_ID: &str = "ETH-PERP.DERIVE";

const ENTER_STRANGLE: bool = false;
const HEDGE_ENABLED: bool = true;
const REHEDGE_DELTA_THRESHOLD: f64 = 0.5;
const REHEDGE_INTERVAL_SECS: u64 = 30;

const TARGET_CALL_DELTA: f64 = 0.20;
const TARGET_PUT_DELTA: f64 = -0.20;
const CONTRACTS: u64 = 1;
const ENTRY_IV_OFFSET: f64 = 0.0;
const ENTRY_PREMIUM_OFFSET_TICKS: i32 = 1;
const EXPIRY_FILTER: Option<&str> = None;

const MAX_FEE_PER_CONTRACT: &str = "1000";
const MARKET_ORDER_SLIPPAGE_BPS: u32 = 50;

The runner builds the strategy config from these constants:

// Disable rehedging by pushing the threshold beyond any reachable delta.
let rehedge_delta_threshold = if HEDGE_ENABLED {
    REHEDGE_DELTA_THRESHOLD
} else {
    1.0e12
};

let mut strategy_config = DeltaNeutralVolConfig::builder()
    .option_family(option_family)
    .hedge_instrument_id(hedge_instrument_id)
    .client_id(client_id)
    .target_call_delta(TARGET_CALL_DELTA)
    .target_put_delta(TARGET_PUT_DELTA)
    .contracts(CONTRACTS)
    .rehedge_delta_threshold(rehedge_delta_threshold)
    .rehedge_interval_secs(REHEDGE_INTERVAL_SECS)
    .enter_strangle(ENTER_STRANGLE)
    .entry_iv_offset(ENTRY_IV_OFFSET)
    .entry_premium_offset_ticks(ENTRY_PREMIUM_OFFSET_TICKS)
    .build();

if let Some(expiry) = EXPIRY_FILTER {
    strategy_config.expiry_filter = Some(expiry.to_string());
}

let strategy = DeltaNeutralVol::new(strategy_config);

HEDGE_ENABLED has no matching strategy field. Setting it to false raises rehedge_delta_threshold to 1.0e12, so the strategy never submits hedge orders.

Parameters:

ParameterDefaultDerive runnerDescription
option_familyrequired"ETH"Underlying filter for instrument discovery.
hedge_instrument_idrequiredETH-PERPPerpetual used for delta hedging.
client_idrequired"DERIVE"Data and execution client identifier.
target_call_delta0.200.20Target call delta for strike selection.
target_put_delta-0.20-0.20Target put delta for strike selection.
contracts11Contracts per option leg.
rehedge_delta_threshold0.50.5Portfolio delta that triggers a hedge.
rehedge_interval_secs3030Periodic rehedge timer interval.
expiry_filterNoneunsetOptional expiry substring filter.
enter_strangletruefalsePlace entry orders when premium data arrives.
entry_premium_offset_ticksNone1Ticks above option ask for sell entry.
entry_iv_offset0.00.0Used only when premium mode is disabled.
iv_param_key"px_vol"unusedIV parameter key for IV-priced venues.

Derive signs explicit premium limit prices. The runner enables the strategy's premium-entry mode with entry_premium_offset_ticks=1, so entry orders use a live option ask when available and fall back to Derive IV fields when the quote side is empty. Bybit and OKX keep using the shared IV-param path.

Node setup

The Derive runner uses the live environment and selects testnet or mainnet from the DERIVE_ENVIRONMENT constant:

let environment = Environment::Live;
let derive_environment = DERIVE_ENVIRONMENT;
let trader_id = TraderId::from(TRADER_ID);
let account_id = AccountId::from(ACCOUNT_ID);
let client_id = *DERIVE_CLIENT_ID;

The data client bulk-loads ETH instruments. This is important because the strategy selects option legs from the cache during on_start.

let data_config = DeriveDataClientConfig {
    environment: derive_environment,
    currencies: vec![option_family.clone()],
    ..Default::default()
};

The execution client reads wallet, session key, and subaccount values from the Derive environment variables when the config fields are left unset. The example sets a fee cap and the market-order slippage bound.

let exec_config = DeriveExecutionClientConfig {
    account_id,
    environment: derive_environment,
    max_fee_per_contract: Some(Decimal::from_str_exact(MAX_FEE_PER_CONTRACT)?),
    market_order_slippage_bps: MARKET_ORDER_SLIPPAGE_BPS,
    ..Default::default()
};

The node enables reconciliation so open orders, positions, balances, and reports are loaded before the strategy starts:

let mut node = LiveNode::builder(trader_id, environment)?
    .with_name("DERIVE-DELTA-NEUTRAL-001".to_string())
    .add_data_client(None, Box::new(data_factory), Box::new(data_config))?
    .add_exec_client(None, Box::new(exec_factory), Box::new(exec_config))?
    .with_reconciliation(true)
    .with_delay_post_stop_secs(5)
    .build()?;

node.add_strategy(strategy)?;
node.run().await?;

How the strategy works

Strike selection

On start the strategy queries the cache for all option instruments matching option_family. For Derive, the example uses ETH, so matching instruments have symbols such as ETH-20260626-3000-C.DERIVE.

It discards expired options, optionally applies expiry_filter, and uses the nearest expiry when no filter is set. Calls and puts are sorted by strike:

  • Call: index = (1.0 - target_call_delta) * count. With the default 0.20, this selects around the 80th percentile strike.
  • Put: index = abs(target_put_delta) * count. With the default -0.20, this selects around the 20th percentile strike.

This is a strike-ordering heuristic. A production strategy can subscribe to Greeks for the full chain first and then select by actual delta.

Greeks and shared ticker feeds

Derive publishes option pricing fields on the same ticker_slim channel used for quotes. The adapter derives OptionGreeks from option_pricing, so the strategy only needs to subscribe to the two selected option legs:

self.subscribe_option_greeks(call_id, Some(client_id), None);
self.subscribe_option_greeks(put_id, Some(client_id), None);

The adapter reference-counts the underlying ticker subscription. Quotes, mark prices, index prices, funding rates, and option Greeks for the same instrument can share a single WebSocket channel.

Rehedging on Derive

The Derive execution adapter sends market orders as signed orders with a slippage-bound limit price. Before signing, it refreshes the current ticker snapshot for the hedge instrument and writes the worst acceptable price into the EIP-712 payload. The default market_order_slippage_bps is 50.

The strategy submits a hedge when both selected option legs have emitted Greeks and:

abs(portfolio_delta) > rehedge_delta_threshold

A positive portfolio delta triggers a SELL on ETH-PERP.DERIVE; a negative portfolio delta triggers a BUY. A hedge_pending flag blocks duplicate submissions while an order is in flight.

Position tracking

The strategy tracks positions via on_order_filled rather than polling positions on every update. Reconciliation hydrates existing positions at startup; subsequent fills update the in-memory call, put, and hedge counters.

Shutdown

On stop the strategy cancels open orders for the selected option legs and the hedge instrument, unsubscribes from data feeds, and leaves positions open. Unwinding the strangle and hedge requires manual action or a separate exit strategy.

What the run produces

On testnet with enter_strangle: false, the strategy should discover the selected legs, subscribe to Greeks, and place no entry orders. The selected symbols depend on the live Derive chain:

Selected call: ETH-<expiry>-<strike>-C.DERIVE (strike=<strike>)
Selected put: ETH-<expiry>-<strike>-P.DERIVE (strike=<strike>)
Strangle: 1 contracts per leg, hedge on ETH-PERP.DERIVE
Strangle entry disabled: hedging externally-held positions only.

If no existing positions are present, the run should remain data-only after startup. If the account already holds positions in the selected legs or hedge instrument, the periodic rehedge timer can submit hedge orders.

The panels below use the same selected-strike mechanics as the Derive runner. They parse selected strikes from the smoke-test log when available and otherwise fall back to illustrative ETH strikes.

Derive short strangle payoff at expiry

Figure 1. Pnl at expiry of the short ETH put plus short ETH call combination, assuming a fixed USDC premium and zero discount. The flat top is the credit-only zone between strikes; loss grows linearly past either strike.

Derive portfolio delta drift around entry

Figure 2. Toy approximation of how the short call and short put leg deltas move around entry, plus the resulting portfolio delta before hedging.

Derive synthetic delta drift with rehedge

Figure 3. Synthetic Brownian delta drift over 150 seconds with rehedge_delta_threshold=0.5. Crosses show where the strategy would submit a hedge order when hedging is enabled.

Derive strike selection on the IV smile

Figure 4. The strike-selection heuristic against an illustrative IV smile. The call strike sits near the (1 - target_call_delta) percentile and the put near abs(target_put_delta).

Regenerate the panels

The log comes from the live runner, which connects to Derive with your credentials. Before the run, edit node_delta_neutral.rs: set DERIVE_ENVIRONMENT to DeriveEnvironment::Mainnet, set HEDGE_ENABLED to false, and keep ENTER_STRANGLE at false so the run submits no orders. Export the mainnet credentials, and revert the constants afterwards.

After building NautilusTrader from source, run these commands from the repository root:

make sync

timeout 45 cargo run --example derive-delta-neutral --package nautilus-derive --features examples \
    > /tmp/derive_dn.log 2>&1

export DN_LOG=/tmp/derive_dn.log
uv run --project python --no-sync \
    python docs/tutorials/assets/delta_neutral_options_derive/render_panels.py

The renderer only uses the log to pick strikes. The plots remain illustrative because the no-order smoke configuration disables entry and hedge submissions.

Risk considerations

  • Gamma risk: a short strangle has negative gamma. Large ETH moves can increase delta exposure faster than the rehedge timer responds.
  • Slippage risk: Derive market orders sign a slippage-bound limit price before submission. Tight bounds can reject useful hedges; loose bounds can fill worse than expected.
  • Entry-price risk: Derive entry uses live option asks plus a tick offset, or computes a premium from Derive IV fields when the quote side is empty. A small or negative offset can cross the book and fill immediately.
  • Collateral risk: Derive rejects orders when the subaccount lacks initial-margin headroom. Check private/get_subaccount or the adapter account snapshot before enabling live hedging.
  • Lifecycle risk: stopping the strategy stops hedging. Positions remain open and unhedged until they are managed elsewhere.

Running the example

With the shipped constants, the example targets Derive testnet with strangle entry disabled and hedging enabled. It submits hedge orders on ETH-PERP.DERIVE when the subaccount holds positions in the selected legs or the hedge instrument and portfolio delta breaches the threshold.

cargo run --example derive-delta-neutral --package nautilus-derive --features examples

Stop with Ctrl+C. The strategy cancels open orders and unsubscribes before shutdown, but it does not close positions.

For a smoke test that loads the venue and account without submitting orders, set HEDGE_ENABLED to false and keep ENTER_STRANGLE at false. To run it on mainnet, also set DERIVE_ENVIRONMENT to DeriveEnvironment::Mainnet. Then run:

timeout 45 cargo run --example derive-delta-neutral --package nautilus-derive --features examples

For a smoke test that submits Derive-premium option entry orders, set ENTER_STRANGLE to true (ENTRY_PREMIUM_OFFSET_TICKS is already 1). The orders go to the network DERIVE_ENVIRONMENT selects, and hedging stays on unless HEDGE_ENABLED is false. Then run:

timeout --signal=INT 45 cargo run --example derive-delta-neutral --package nautilus-derive \
    --features examples

Complete source

  • Example runner: crates/adapters/derive/examples/node_delta_neutral.rs
  • Strategy implementation: crates/trading/src/examples/strategies/delta_neutral_vol/
  • Strategy README: crates/trading/src/examples/strategies/delta_neutral_vol/README.md

See also

On this page