Skip to main content

nautilus_deribit/
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 structures for the Deribit adapter.
17
18use nautilus_common::config::{ConfigResult, check_unique_values};
19#[cfg(test)]
20use nautilus_core::string::secret::REDACTED;
21use nautilus_core::string::secret::SecretString;
22use nautilus_model::identifiers::AccountId;
23use nautilus_network::websocket::TransportBackend;
24use serde::{Deserialize, Serialize};
25
26use crate::{
27    common::{
28        credential::credential_env_vars,
29        enums::DeribitEnvironment,
30        urls::{get_http_base_url, get_ws_url},
31    },
32    http::models::DeribitProductType,
33};
34
35/// Configuration for the Deribit data client.
36#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
37#[serde(default, deny_unknown_fields)]
38#[cfg_attr(
39    feature = "python",
40    pyo3::pyclass(module = "nautilus_trader.adapters.deribit", from_py_object)
41)]
42#[cfg_attr(
43    feature = "python",
44    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.deribit")
45)]
46pub struct DeribitDataClientConfig {
47    /// Optional API key for authenticated endpoints.
48    pub api_key: Option<SecretString>,
49    /// Optional API secret for authenticated endpoints.
50    pub api_secret: Option<SecretString>,
51    /// Product types to load (e.g., Future, Option, Spot).
52    #[builder(default = vec![DeribitProductType::Future])]
53    pub product_types: Vec<DeribitProductType>,
54    /// The Deribit environment (mainnet or testnet).
55    #[builder(default)]
56    pub environment: DeribitEnvironment,
57    /// Optional override for the HTTP base URL.
58    pub base_url_http: Option<String>,
59    /// Optional override for the WebSocket URL.
60    pub base_url_ws: Option<String>,
61    /// Optional proxy URL for HTTP and WebSocket transports.
62    pub proxy_url: Option<SecretString>,
63    /// HTTP timeout in seconds.
64    #[builder(default = 60)]
65    pub http_timeout_secs: u64,
66    /// Maximum retry attempts for requests.
67    #[builder(default = 3)]
68    pub max_retries: u32,
69    /// Initial retry delay in milliseconds.
70    #[builder(default = 1_000)]
71    pub retry_delay_initial_ms: u64,
72    /// Maximum retry delay in milliseconds.
73    #[builder(default = 10_000)]
74    pub retry_delay_max_ms: u64,
75    /// Heartbeat interval in seconds for WebSocket connection.
76    #[builder(default = 30)]
77    pub heartbeat_interval_secs: u64,
78    /// Optional WebSocket authentication timeout (seconds), defaulting to
79    /// `AUTHENTICATION_TIMEOUT_SECS` when unset.
80    pub auth_timeout_secs: Option<u64>,
81    /// Interval for refreshing instruments (in minutes).
82    #[builder(default = 60)]
83    pub update_instruments_interval_mins: u64,
84    /// If `true`, subscribes for uncached instruments lazy-load via HTTP; otherwise fail fast.
85    #[builder(default = false)]
86    pub auto_load_missing_instruments: bool,
87    /// WebSocket transport backend (defaults to `Tungstenite`).
88    #[builder(default)]
89    pub transport_backend: TransportBackend,
90}
91
92#[cfg(feature = "python")]
93nautilus_core::impl_pyo3_config_getters!(DeribitDataClientConfig {
94    product_types: Vec<DeribitProductType>,
95    environment: DeribitEnvironment,
96    base_url_http: Option<String>,
97    base_url_ws: Option<String>,
98    http_timeout_secs: u64,
99    max_retries: u32,
100    retry_delay_initial_ms: u64,
101    retry_delay_max_ms: u64,
102    heartbeat_interval_secs: u64,
103    auth_timeout_secs: Option<u64>,
104    update_instruments_interval_mins: u64,
105    auto_load_missing_instruments: bool,
106    transport_backend: TransportBackend,
107});
108
109impl Default for DeribitDataClientConfig {
110    fn default() -> Self {
111        Self::builder().build()
112    }
113}
114
115impl DeribitDataClientConfig {
116    /// Creates a new configuration with default settings.
117    #[must_use]
118    pub fn new() -> Self {
119        Self::default()
120    }
121
122    /// Validates that configured selections contain no duplicates.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if `product_types` repeats a value.
127    pub fn validate(&self) -> ConfigResult<()> {
128        check_unique_values("product_types", &self.product_types)
129    }
130
131    /// Returns `true` when API credentials are available (in config or env vars).
132    #[must_use]
133    pub fn has_api_credentials(&self) -> bool {
134        let (key_env, secret_env) = credential_env_vars(self.environment);
135        let has_key = self.api_key.is_some() || std::env::var(key_env).is_ok();
136        let has_secret = self.api_secret.is_some() || std::env::var(secret_env).is_ok();
137        has_key && has_secret
138    }
139
140    /// Returns the HTTP base URL, falling back to the default when unset.
141    #[must_use]
142    pub fn http_base_url(&self) -> String {
143        self.base_url_http
144            .clone()
145            .unwrap_or_else(|| get_http_base_url(self.environment).to_string())
146    }
147
148    /// Returns the WebSocket URL, respecting the environment and overrides.
149    #[must_use]
150    pub fn ws_url(&self) -> String {
151        self.base_url_ws
152            .clone()
153            .unwrap_or_else(|| get_ws_url(self.environment).to_string())
154    }
155}
156
157/// Configuration for the Deribit execution client.
158#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
159#[serde(default, deny_unknown_fields)]
160#[cfg_attr(
161    feature = "python",
162    pyo3::pyclass(module = "nautilus_trader.adapters.deribit", from_py_object)
163)]
164#[cfg_attr(
165    feature = "python",
166    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.deribit")
167)]
168pub struct DeribitExecutionClientConfig {
169    /// The account ID for this client.
170    #[builder(default = AccountId::from("DERIBIT-001"))]
171    pub account_id: AccountId,
172    /// Optional API key for authenticated endpoints.
173    pub api_key: Option<SecretString>,
174    /// Optional API secret for authenticated endpoints.
175    pub api_secret: Option<SecretString>,
176    /// Product types to load (e.g., Future, Option, Spot).
177    #[builder(default = vec![DeribitProductType::Future])]
178    pub product_types: Vec<DeribitProductType>,
179    /// The Deribit environment (mainnet or testnet).
180    #[builder(default)]
181    pub environment: DeribitEnvironment,
182    /// Optional override for the HTTP base URL.
183    pub base_url_http: Option<String>,
184    /// Optional override for the WebSocket URL.
185    pub base_url_ws: Option<String>,
186    /// Optional proxy URL for HTTP and WebSocket transports.
187    pub proxy_url: Option<SecretString>,
188    /// HTTP timeout in seconds.
189    #[builder(default = 60)]
190    pub http_timeout_secs: u64,
191    /// Maximum retry attempts for requests.
192    #[builder(default = 3)]
193    pub max_retries: u32,
194    /// Initial retry delay in milliseconds.
195    #[builder(default = 1_000)]
196    pub retry_delay_initial_ms: u64,
197    /// Maximum retry delay in milliseconds.
198    #[builder(default = 10_000)]
199    pub retry_delay_max_ms: u64,
200    /// Optional WebSocket authentication timeout (seconds), defaulting to
201    /// `AUTHENTICATION_TIMEOUT_SECS` when unset.
202    pub auth_timeout_secs: Option<u64>,
203    /// WebSocket transport backend (defaults to `Tungstenite`).
204    #[builder(default)]
205    pub transport_backend: TransportBackend,
206}
207
208#[cfg(feature = "python")]
209nautilus_core::impl_pyo3_config_getters!(DeribitExecutionClientConfig {
210    account_id: AccountId,
211    product_types: Vec<DeribitProductType>,
212    environment: DeribitEnvironment,
213    base_url_http: Option<String>,
214    base_url_ws: Option<String>,
215    http_timeout_secs: u64,
216    max_retries: u32,
217    retry_delay_initial_ms: u64,
218    retry_delay_max_ms: u64,
219    auth_timeout_secs: Option<u64>,
220    transport_backend: TransportBackend,
221});
222
223impl Default for DeribitExecutionClientConfig {
224    fn default() -> Self {
225        Self::builder().build()
226    }
227}
228
229impl DeribitExecutionClientConfig {
230    /// Validates that configured selections contain no duplicates.
231    ///
232    /// # Errors
233    ///
234    /// Returns an error if `product_types` repeats a value.
235    pub fn validate(&self) -> ConfigResult<()> {
236        check_unique_values("product_types", &self.product_types)
237    }
238
239    /// Returns `true` when API credentials are available (in config or env vars).
240    #[must_use]
241    pub fn has_api_credentials(&self) -> bool {
242        let (key_env, secret_env) = credential_env_vars(self.environment);
243        let has_key = self.api_key.is_some() || std::env::var(key_env).is_ok();
244        let has_secret = self.api_secret.is_some() || std::env::var(secret_env).is_ok();
245        has_key && has_secret
246    }
247
248    /// Returns the HTTP base URL, falling back to the default when unset.
249    #[must_use]
250    pub fn http_base_url(&self) -> String {
251        self.base_url_http
252            .clone()
253            .unwrap_or_else(|| get_http_base_url(self.environment).to_string())
254    }
255
256    /// Returns the WebSocket URL, respecting the environment and overrides.
257    #[must_use]
258    pub fn ws_url(&self) -> String {
259        self.base_url_ws
260            .clone()
261            .unwrap_or_else(|| get_ws_url(self.environment).to_string())
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use std::{cell::RefCell, rc::Rc};
268
269    use nautilus_common::{cache::Cache, config::ConfigError};
270    use nautilus_live::ExecutionClientCore;
271    use nautilus_model::{
272        enums::{AccountType, OmsType},
273        identifiers::{ClientId, TraderId, Venue},
274    };
275    use rstest::rstest;
276
277    use super::*;
278    use crate::{data::DeribitDataClient, execution::DeribitExecutionClient};
279
280    #[rstest]
281    fn test_data_client_rejects_duplicate_types() {
282        let config = DeribitDataClientConfig {
283            product_types: vec![DeribitProductType::Future, DeribitProductType::Future],
284            ..Default::default()
285        };
286
287        let error = DeribitDataClient::new(ClientId::from("DERIBIT"), config)
288            .expect_err("duplicate config must fail before client setup");
289
290        assert_eq!(
291            error.downcast_ref::<ConfigError>(),
292            Some(&ConfigError::duplicate("product_types", None)),
293        );
294    }
295
296    #[rstest]
297    #[case::empty(vec![], false)]
298    #[case::distinct(vec![DeribitProductType::Option, DeribitProductType::Future], false)]
299    #[case::adjacent(vec![DeribitProductType::Future, DeribitProductType::Future], true)]
300    #[case::separated(vec![DeribitProductType::Future, DeribitProductType::Option, DeribitProductType::Future], true)]
301    fn test_config_duplicate_types(
302        #[case] values: Vec<DeribitProductType>,
303        #[case] duplicate: bool,
304    ) {
305        let data = DeribitDataClientConfig {
306            product_types: values.clone(),
307            ..Default::default()
308        };
309
310        let execution = DeribitExecutionClientConfig {
311            product_types: values.clone(),
312            ..Default::default()
313        };
314
315        let expected = if duplicate {
316            Err(ConfigError::duplicate("product_types", None))
317        } else {
318            Ok(())
319        };
320
321        let data_result = data.validate();
322        let execution_result = execution.validate();
323
324        assert_eq!(data_result, expected);
325        assert_eq!(execution_result, expected);
326        assert_eq!(data.product_types, values);
327        assert_eq!(execution.product_types, values);
328    }
329
330    #[rstest]
331    fn test_config_debug_redacts_credentials() {
332        let data = DeribitDataClientConfig {
333            api_key: Some("data-api-key".into()),
334            api_secret: Some("data-api-secret".into()),
335            proxy_url: Some("http://user:data-proxy@localhost".into()),
336            ..Default::default()
337        };
338        let execution = DeribitExecutionClientConfig {
339            api_key: Some("exec-api-key".into()),
340            api_secret: Some("exec-api-secret".into()),
341            proxy_url: Some("http://user:exec-proxy@localhost".into()),
342            ..Default::default()
343        };
344
345        let formatted = format!("{data:?} {execution:?}");
346
347        assert_eq!(formatted.matches(REDACTED).count(), 6);
348
349        for secret in [
350            "data-api-key",
351            "data-api-secret",
352            "data-proxy",
353            "exec-api-key",
354            "exec-api-secret",
355            "exec-proxy",
356        ] {
357            assert!(!formatted.contains(secret));
358        }
359    }
360
361    #[rstest]
362    fn test_default_config() {
363        let config = DeribitDataClientConfig::default();
364        assert_eq!(config.environment, DeribitEnvironment::Mainnet);
365        assert_eq!(config.product_types.len(), 1);
366        assert_eq!(config.http_timeout_secs, 60);
367    }
368
369    #[rstest]
370    fn test_http_base_url_default() {
371        let config = DeribitDataClientConfig::default();
372        assert_eq!(config.http_base_url(), "https://www.deribit.com");
373    }
374
375    #[rstest]
376    fn test_http_base_url_testnet() {
377        let config = DeribitDataClientConfig {
378            environment: DeribitEnvironment::Testnet,
379            ..Default::default()
380        };
381        assert_eq!(config.http_base_url(), "https://test.deribit.com");
382    }
383
384    #[rstest]
385    fn test_ws_url_default() {
386        let config = DeribitDataClientConfig::default();
387        assert_eq!(config.ws_url(), "wss://www.deribit.com/ws/api/v2");
388    }
389
390    #[rstest]
391    fn test_ws_url_testnet() {
392        let config = DeribitDataClientConfig {
393            environment: DeribitEnvironment::Testnet,
394            ..Default::default()
395        };
396        assert_eq!(config.ws_url(), "wss://test.deribit.com/ws/api/v2");
397    }
398
399    #[rstest]
400    fn test_has_api_credentials_in_config() {
401        let config = DeribitDataClientConfig {
402            api_key: Some("test_key".into()),
403            api_secret: Some("test_secret".into()),
404            ..Default::default()
405        };
406        assert!(config.has_api_credentials());
407    }
408
409    #[rstest]
410    fn test_data_config_toml_minimal() {
411        let config: DeribitDataClientConfig = toml::from_str(
412            r#"
413environment = "testnet"
414product_types = ["future", "option"]
415heartbeat_interval_secs = 15
416auto_load_missing_instruments = true
417"#,
418        )
419        .unwrap();
420
421        assert_eq!(config.environment, DeribitEnvironment::Testnet);
422        assert_eq!(
423            config.product_types,
424            vec![DeribitProductType::Future, DeribitProductType::Option]
425        );
426        assert_eq!(config.heartbeat_interval_secs, 15);
427        assert!(config.auto_load_missing_instruments);
428    }
429
430    #[rstest]
431    fn test_exec_config_toml_empty_uses_defaults() {
432        let config: DeribitExecutionClientConfig = toml::from_str("").unwrap();
433        let expected = DeribitExecutionClientConfig::default();
434        assert_eq!(config.account_id, expected.account_id);
435        assert_eq!(config.environment, expected.environment);
436        assert_eq!(config.product_types, expected.product_types);
437        assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
438        assert_eq!(config.max_retries, expected.max_retries);
439        assert_eq!(config.transport_backend, expected.transport_backend);
440    }
441
442    #[rstest]
443    fn test_execution_client_rejects_duplicate_types() {
444        let core = ExecutionClientCore::new(
445            TraderId::from("TESTER-001"),
446            ClientId::from("DERIBIT"),
447            Venue::from("DERIBIT"),
448            OmsType::Netting,
449            AccountId::from("DERIBIT-001"),
450            AccountType::Margin,
451            None,
452            Rc::new(RefCell::new(Cache::default())),
453        );
454
455        let config = DeribitExecutionClientConfig {
456            product_types: vec![DeribitProductType::Future, DeribitProductType::Future],
457            ..Default::default()
458        };
459
460        let error = DeribitExecutionClient::new(core, config)
461            .expect_err("duplicate config must fail before credentials or client setup");
462
463        assert_eq!(
464            error.downcast_ref::<ConfigError>(),
465            Some(&ConfigError::duplicate("product_types", None))
466        );
467    }
468}