Skip to main content

nautilus_trading/examples/strategies/delta_neutral_vol/
config.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Configuration for the delta-neutral volatility hedger.
17
18use nautilus_model::{
19    enums::TimeInForce,
20    identifiers::{ClientId, InstrumentId, StrategyId},
21};
22
23use crate::strategy::StrategyConfig;
24
25/// Configuration for the delta-neutral short volatility hedger.
26///
27/// Tracks a short OTM call and put (strangle) and delta-hedges with the
28/// underlying perpetual swap. Rehedges when portfolio delta exceeds a
29/// configurable threshold or on a periodic timer.
30#[derive(Debug, Clone)]
31#[cfg_attr(
32    feature = "python",
33    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.trading", from_py_object)
34)]
35pub struct DeltaNeutralVolConfig {
36    /// Base strategy configuration.
37    pub base: StrategyConfig,
38    /// Option instrument family (e.g. "BTC-USD").
39    pub option_family: String,
40    /// Hedge instrument ID (e.g. BTC-USD-SWAP.OKX).
41    pub hedge_instrument_id: InstrumentId,
42    /// Data and execution client ID (e.g. "OKX").
43    pub client_id: ClientId,
44    /// Target call delta used by the startup strike heuristic.
45    pub target_call_delta: f64,
46    /// Target put delta used by the startup strike heuristic.
47    pub target_put_delta: f64,
48    /// Number of option contracts per leg.
49    pub contracts: u64,
50    /// Portfolio delta threshold that triggers a rehedge.
51    pub rehedge_delta_threshold: f64,
52    /// Periodic rehedge check interval in seconds.
53    pub rehedge_interval_secs: u64,
54    /// Optional expiry date filter (e.g. "260327").
55    pub expiry_filter: Option<String>,
56    /// Place strangle entry orders when Greeks are first initialized.
57    /// When false the strategy only hedges externally-entered positions.
58    pub enter_strangle: bool,
59    /// Implied volatility offset subtracted from mark IV for entry limit
60    /// price. A value of 0.02 sells 2 vol points below mark (more aggressive).
61    pub entry_iv_offset: f64,
62    /// Time-in-force for strangle entry orders.
63    pub entry_time_in_force: TimeInForce,
64    /// Param key for implied volatility passed to `submit_order_with_params`.
65    /// Adapter-specific: Bybit uses `"order_iv"`, OKX uses `"px_vol"`.
66    pub iv_param_key: String,
67}
68
69impl DeltaNeutralVolConfig {
70    /// Creates a new [`DeltaNeutralVolConfig`] with required fields and defaults.
71    #[must_use]
72    pub fn new(
73        option_family: String,
74        hedge_instrument_id: InstrumentId,
75        client_id: ClientId,
76    ) -> Self {
77        Self {
78            base: StrategyConfig {
79                strategy_id: Some(StrategyId::from("DELTA_NEUTRAL_VOL-001")),
80                order_id_tag: Some("001".to_string()),
81                ..Default::default()
82            },
83            option_family,
84            hedge_instrument_id,
85            client_id,
86            target_call_delta: 0.20,
87            target_put_delta: -0.20,
88            contracts: 1,
89            rehedge_delta_threshold: 0.5,
90            rehedge_interval_secs: 30,
91            expiry_filter: None,
92            enter_strangle: true,
93            entry_iv_offset: 0.0,
94            entry_time_in_force: TimeInForce::Gtc,
95            iv_param_key: "px_vol".to_string(),
96        }
97    }
98
99    #[must_use]
100    pub fn with_target_call_delta(mut self, delta: f64) -> Self {
101        self.target_call_delta = delta;
102        self
103    }
104
105    #[must_use]
106    pub fn with_target_put_delta(mut self, delta: f64) -> Self {
107        self.target_put_delta = delta;
108        self
109    }
110
111    #[must_use]
112    pub fn with_contracts(mut self, contracts: u64) -> Self {
113        self.contracts = contracts;
114        self
115    }
116
117    #[must_use]
118    pub fn with_rehedge_delta_threshold(mut self, threshold: f64) -> Self {
119        self.rehedge_delta_threshold = threshold;
120        self
121    }
122
123    #[must_use]
124    pub fn with_rehedge_interval_secs(mut self, secs: u64) -> Self {
125        self.rehedge_interval_secs = secs;
126        self
127    }
128
129    #[must_use]
130    pub fn with_expiry_filter(mut self, expiry: String) -> Self {
131        self.expiry_filter = Some(expiry);
132        self
133    }
134
135    #[must_use]
136    pub fn with_enter_strangle(mut self, enter: bool) -> Self {
137        self.enter_strangle = enter;
138        self
139    }
140
141    #[must_use]
142    pub fn with_entry_iv_offset(mut self, offset: f64) -> Self {
143        self.entry_iv_offset = offset;
144        self
145    }
146
147    #[must_use]
148    pub fn with_entry_time_in_force(mut self, tif: TimeInForce) -> Self {
149        self.entry_time_in_force = tif;
150        self
151    }
152
153    #[must_use]
154    pub fn with_strategy_id(mut self, strategy_id: StrategyId) -> Self {
155        self.base.strategy_id = Some(strategy_id);
156        self
157    }
158
159    #[must_use]
160    pub fn with_order_id_tag(mut self, tag: String) -> Self {
161        self.base.order_id_tag = Some(tag);
162        self
163    }
164
165    #[must_use]
166    pub fn with_iv_param_key(mut self, key: String) -> Self {
167        self.iv_param_key = key;
168        self
169    }
170}
171
172#[cfg(feature = "python")]
173#[pyo3::pymethods]
174impl DeltaNeutralVolConfig {
175    #[new]
176    #[pyo3(signature = (
177        option_family,
178        hedge_instrument_id,
179        client_id,
180        strategy_id=None,
181        order_id_tag=None,
182        target_call_delta=0.20,
183        target_put_delta=-0.20,
184        contracts=1,
185        rehedge_delta_threshold=0.5,
186        rehedge_interval_secs=30,
187        expiry_filter=None,
188        enter_strangle=true,
189        entry_iv_offset=0.0,
190        entry_time_in_force=TimeInForce::Gtc,
191        iv_param_key="px_vol",
192    ))]
193    #[expect(clippy::too_many_arguments)]
194    fn py_new(
195        option_family: String,
196        hedge_instrument_id: InstrumentId,
197        client_id: ClientId,
198        strategy_id: Option<StrategyId>,
199        order_id_tag: Option<String>,
200        target_call_delta: f64,
201        target_put_delta: f64,
202        contracts: u64,
203        rehedge_delta_threshold: f64,
204        rehedge_interval_secs: u64,
205        expiry_filter: Option<String>,
206        enter_strangle: bool,
207        entry_iv_offset: f64,
208        entry_time_in_force: TimeInForce,
209        iv_param_key: &str,
210    ) -> Self {
211        let mut config = Self::new(option_family, hedge_instrument_id, client_id)
212            .with_target_call_delta(target_call_delta)
213            .with_target_put_delta(target_put_delta)
214            .with_contracts(contracts)
215            .with_rehedge_delta_threshold(rehedge_delta_threshold)
216            .with_rehedge_interval_secs(rehedge_interval_secs)
217            .with_enter_strangle(enter_strangle)
218            .with_entry_iv_offset(entry_iv_offset)
219            .with_entry_time_in_force(entry_time_in_force)
220            .with_iv_param_key(iv_param_key.to_string());
221
222        if let Some(id) = strategy_id {
223            config.base.strategy_id = Some(id);
224        }
225
226        if let Some(tag) = order_id_tag {
227            config.base.order_id_tag = Some(tag);
228        }
229
230        if let Some(expiry) = expiry_filter {
231            config.expiry_filter = Some(expiry);
232        }
233
234        config
235    }
236
237    #[getter]
238    fn option_family(&self) -> &str {
239        &self.option_family
240    }
241
242    #[getter]
243    fn hedge_instrument_id(&self) -> InstrumentId {
244        self.hedge_instrument_id
245    }
246
247    #[getter]
248    fn client_id(&self) -> ClientId {
249        self.client_id
250    }
251
252    #[getter]
253    fn target_call_delta(&self) -> f64 {
254        self.target_call_delta
255    }
256
257    #[getter]
258    fn target_put_delta(&self) -> f64 {
259        self.target_put_delta
260    }
261
262    #[getter]
263    fn contracts(&self) -> u64 {
264        self.contracts
265    }
266
267    #[getter]
268    fn rehedge_delta_threshold(&self) -> f64 {
269        self.rehedge_delta_threshold
270    }
271
272    #[getter]
273    fn rehedge_interval_secs(&self) -> u64 {
274        self.rehedge_interval_secs
275    }
276
277    #[getter]
278    fn expiry_filter(&self) -> Option<&str> {
279        self.expiry_filter.as_deref()
280    }
281
282    #[getter]
283    fn enter_strangle(&self) -> bool {
284        self.enter_strangle
285    }
286
287    #[getter]
288    fn entry_iv_offset(&self) -> f64 {
289        self.entry_iv_offset
290    }
291
292    #[getter]
293    fn entry_time_in_force(&self) -> TimeInForce {
294        self.entry_time_in_force
295    }
296}