1use 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#[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 pub api_key: Option<SecretString>,
49 pub api_secret: Option<SecretString>,
51 #[builder(default = vec![DeribitProductType::Future])]
53 pub product_types: Vec<DeribitProductType>,
54 #[builder(default)]
56 pub environment: DeribitEnvironment,
57 pub base_url_http: Option<String>,
59 pub base_url_ws: Option<String>,
61 pub proxy_url: Option<SecretString>,
63 #[builder(default = 60)]
65 pub http_timeout_secs: u64,
66 #[builder(default = 3)]
68 pub max_retries: u32,
69 #[builder(default = 1_000)]
71 pub retry_delay_initial_ms: u64,
72 #[builder(default = 10_000)]
74 pub retry_delay_max_ms: u64,
75 #[builder(default = 30)]
77 pub heartbeat_interval_secs: u64,
78 pub auth_timeout_secs: Option<u64>,
81 #[builder(default = 60)]
83 pub update_instruments_interval_mins: u64,
84 #[builder(default = false)]
86 pub auto_load_missing_instruments: bool,
87 #[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 #[must_use]
118 pub fn new() -> Self {
119 Self::default()
120 }
121
122 pub fn validate(&self) -> ConfigResult<()> {
128 check_unique_values("product_types", &self.product_types)
129 }
130
131 #[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 #[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 #[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#[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 #[builder(default = AccountId::from("DERIBIT-001"))]
171 pub account_id: AccountId,
172 pub api_key: Option<SecretString>,
174 pub api_secret: Option<SecretString>,
176 #[builder(default = vec![DeribitProductType::Future])]
178 pub product_types: Vec<DeribitProductType>,
179 #[builder(default)]
181 pub environment: DeribitEnvironment,
182 pub base_url_http: Option<String>,
184 pub base_url_ws: Option<String>,
186 pub proxy_url: Option<SecretString>,
188 #[builder(default = 60)]
190 pub http_timeout_secs: u64,
191 #[builder(default = 3)]
193 pub max_retries: u32,
194 #[builder(default = 1_000)]
196 pub retry_delay_initial_ms: u64,
197 #[builder(default = 10_000)]
199 pub retry_delay_max_ms: u64,
200 pub auth_timeout_secs: Option<u64>,
203 #[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 pub fn validate(&self) -> ConfigResult<()> {
236 check_unique_values("product_types", &self.product_types)
237 }
238
239 #[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 #[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 #[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}