Delta-Neutral Options Strategy (Derive)
This Rust-only tutorial 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 and selects an out-of-the-money call and put. It 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). Reconciliation still loads existing
positions. When portfolio delta exceeds the configured threshold, the strategy can submit market hedge orders
on the perpetual.
The runner reads its settings from constants at the top of the example file, not from environment variables.
For a smoke test without hedge orders, set HEDGE_ENABLED to false and rebuild. The strategy still loads
instruments, reconciles the account, and subscribes to Greeks. For an entry-order smoke test, set
ENTER_STRANGLE to true. The runner then 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 sufficient 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:
- Instrument load: configures the Derive data client with
currencies: ["ETH"], so the adapter loads ETH perps and options into the cache. - Strike selection: filters the cache to live ETH options, selects the nearest expiry, then chooses OTM call and put strikes by percentile rank.
- Greeks tracking: subscribes to
OptionGreeksfor both legs. Derive Greeks come from the sharedticker_slimfeed and theoption_pricingpayload. - Rehedging: calculates portfolio delta and submits a Derive market order on
ETH-PERP.DERIVEwhen the threshold is breached. - Position tracking: tracks call, put, and hedge positions through fills. Reconciliation loads existing positions before the strategy starts.
Portfolio delta
The strategy calculates net exposure as:
portfolio_delta = call_delta * call_position
+ put_delta * put_position
+ hedge_positionA 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. To change the run, edit these constants and rebuild the example. The cargo run command 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:
| Parameter | Default | Derive runner | Description |
|---|---|---|---|
option_family | required | "ETH" | Underlying filter for instrument discovery. |
hedge_instrument_id | required | ETH-PERP | Perpetual used for delta hedging. |
client_id | required | "DERIVE" | Data and execution client identifier. |
target_call_delta | 0.20 | 0.20 | Target call delta for strike selection. |
target_put_delta | -0.20 | -0.20 | Target put delta for strike selection. |
contracts | 1 | 1 | Contracts per option leg. |
rehedge_delta_threshold | 0.5 | 0.5 | Portfolio delta that triggers a hedge. |
rehedge_interval_secs | 30 | 30 | Periodic rehedge timer interval. |
expiry_filter | None | unset | Optional expiry substring filter. |
enter_strangle | true | false | Place entry orders when premium data arrives. |
entry_premium_offset_ticks | None | 1 | Ticks above option ask for sell entry. |
entry_iv_offset | 0.0 | 0.0 | Used only when premium mode is disabled. |
iv_param_key | "px_vol" | unused | IV 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. Entry orders use a live option ask when available. When the quote side is
empty, they use Derive IV fields instead. Bybit and OKX use the shared IV-parameter 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()
};When the config fields are unset, the execution client reads wallet, session key, and subaccount values from Derive environment variables. 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 to load open orders, positions, balances, and reports 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 removes expired options and applies expiry_filter if set. When no filter is set, it uses the nearest
expiry. It sorts calls and puts by strike:
- Call: index =
(1.0 - target_call_delta) * count. With the default0.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 keeps a reference count for the underlying ticker subscription. For the same instrument, these data types can share a single WebSocket channel:
- Quotes.
- Mark prices.
- Index prices.
- Funding rates.
- Option Greeks.
Rehedging on Derive
The Derive execution adapter sends market orders as signed orders with a slippage-bound limit price. Before it
signs the order, the adapter refreshes the ticker snapshot for the hedge instrument. It 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_thresholdA 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 through on_order_filled. It does not poll positions on every update.
Reconciliation loads 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. It unsubscribes from data feeds and leaves positions open. To close the strangle and hedge, use 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.

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 increases linearly past either strike.

Figure 2. Illustrative short call and short put deltas around entry, with the resulting portfolio delta before hedging.

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.

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_ENVIRONMENTtoDeriveEnvironment::Mainnet. - Set
HEDGE_ENABLEDtofalse. - Keep
ENTER_STRANGLEatfalse. - Export the mainnet credentials.
With both HEDGE_ENABLED and ENTER_STRANGLE set to false, the run submits no orders.
After the run, restore the constants.
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.pyThe 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. When the quote side is empty, it calculates a premium from Derive IV fields. A small or negative offset can cross the book and fill immediately.
- Collateral risk: Derive rejects orders when the subaccount lacks initial-margin headroom.
Before you enable live hedging, check
private/get_subaccountor the adapter account snapshot. - 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. If the subaccount holds positions in the selected legs or hedge instrument, portfolio delta can
exceed the threshold. The strategy then submits hedge orders on ETH-PERP.DERIVE.
cargo run --example derive-delta-neutral --package nautilus-derive --features examplesStop 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 examplesFor 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 examplesComplete 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
- Derive integration: environment setup, symbology, capabilities, and execution semantics.
- Options: option instrument types and data architecture.
- Delta-neutral options strategy on Bybit: the same shared strategy with Bybit-specific IV entry parameters.
Delta-Neutral Options Strategy (Bybit)
This tutorial runs a short out-of-the-money (OTM) strangle on Bybit BTC options and delta-hedges with the BTCUSDT perpetual. The strategy selects call and...
Book Imbalance Backtest (Betfair)
This tutorial backtests a BookImbalanceActor on a Betfair MATCH_ODDS market. It loads a raw historical streaming .gz file, feeds it through the Rust...