Skip to content
All library documents

Derive ETH Short Strangle Selection and Perpetual Delta Hedging

Article NautilusTrader

Summary

This tutorial describes running a shared short volatility strategy on Derive. It selects an out-of-the-money ETH call and put at the nearest expiry, tracks venue supplied option Greeks, and offsets net portfolio delta with ETH perpetual orders when exposure crosses a threshold. Startup reconciliation loads existing positions, so hedging can occur even when new strangle entries are disabled.

The example defaults to testnet and hedge only operation, with entry and hedge behavior controlled by source constants. Its strike selection uses percentile ranks as a heuristic, while the document notes that production selection could use Greeks across more options. It explains premium based option entries, a signed slippage bound for market hedges, collateral requirements, and the effect of stopping the strategy while positions remain open. This is an implementation tutorial, not performance evidence: it reports no backtest or returns. Short strangles carry negative gamma, and delayed rehedging, execution prices, collateral limits, and open positions all create material risks.

Key ideas

  • The strategy selects a nearest expiry ETH call and put using strike percentiles as a heuristic.
  • It tracks Derive option Greeks and hedges portfolio delta through the ETH perpetual.
  • Startup reconciliation can expose existing positions to hedging even when new entries are off.
  • Entry and rehedging settings are source constants, and testnet is the shipped environment.
  • Negative gamma, slippage, insufficient collateral, and stopping with positions open are key risks.

Tags

Full text
# Delta-Neutral Options Strategy (Derive)


# Delta-Neutral Options Strategy (Derive)

:::note
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.

:::warning
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](../integrations/derive.md), 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:

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

For mainnet:

```bash
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.

```mermaid
flowchart LR
    subgraph Derive ["Derive public + private APIs"]
        T["ticker_slim option payloads"]
        P["private positions and fills"]
        H["ETH-PERP order entry"]
    end

    subgraph Adapter ["nautilus-derive clients"]
        G["OptionGreeks from option_pricing"]
        R["Startup reconciliation"]
        M["Signed market hedge order"]
    end

    subgraph Strategy ["DeltaNeutralVol"]
        S["Select nearest-expiry ETH strangle"]
        D["portfolio_delta = call_delta * call_pos<br/>+ put_delta * put_pos<br/>+ hedge_pos"]
        C{{"|portfolio_delta| > threshold?"}}
    end

    T --> G --> S --> D --> C
    P --> R --> D
    C -->|yes| M --> H
```

### 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.

```rust
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:

```rust
// 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`, 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:

```rust
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`.

```rust
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.

```rust
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:

```rust
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:

```rust
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](./assets/delta_neutral_options_derive/panel_a_strangle_payoff.png)

**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](./assets/delta_neutral_options_derive/panel_b_delta_drift.png)

**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](./assets/delta_neutral_options_derive/panel_c_hedge_threshold.png)

**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](./assets/delta_neutral_options_derive/panel_d_strike_picker.png)

**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:

```bash
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.

```bash
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:

```bash
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:

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

## Complete source


- Strategy implementation: `crates/trading/src/examples/strategies/delta_neutral_vol/`


## See also

- [Derive integration](../integrations/derive.md): environment setup, symbology, capabilities, and
  execution semantics.
- [Options](../concepts/options.md): option instrument types and data architecture.
- [Delta-neutral options strategy on Bybit](delta_neutral_options_bybit.md): the same shared
  strategy with Bybit-specific IV entry parameters.

Shown in full with attribution under the source's licence. Licence: LGPL-3.0

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.