1use nautilus_common::python::config_error_to_pyvalue_err;
19use nautilus_core::string::secret::SecretString;
20use nautilus_model::identifiers::AccountId;
21use nautilus_network::websocket::TransportBackend;
22use pyo3::prelude::*;
23
24use crate::{
25 common::enums::DeribitEnvironment,
26 config::{DeribitDataClientConfig, DeribitExecutionClientConfig},
27 http::models::DeribitProductType,
28};
29
30#[pymethods]
31#[pyo3_stub_gen::derive::gen_stub_pymethods]
32impl DeribitDataClientConfig {
33 #[new]
35 #[pyo3(signature = (
36 product_types = None,
37 environment = None,
38 api_key = None,
39 api_secret = None,
40 base_url_http = None,
41 base_url_ws = None,
42 proxy_url = None,
43 http_timeout_secs = None,
44 max_retries = None,
45 retry_delay_initial_ms = None,
46 retry_delay_max_ms = None,
47 heartbeat_interval_secs = None,
48 auth_timeout_secs = None,
49 update_instruments_interval_mins = None,
50 auto_load_missing_instruments = None,
51 transport_backend = None,
52 ))]
53 #[expect(clippy::too_many_arguments)]
54 fn py_new(
55 product_types: Option<Vec<DeribitProductType>>,
56 environment: Option<DeribitEnvironment>,
57 api_key: Option<String>,
58 api_secret: Option<String>,
59 base_url_http: Option<String>,
60 base_url_ws: Option<String>,
61 proxy_url: Option<String>,
62 http_timeout_secs: Option<u64>,
63 max_retries: Option<u32>,
64 retry_delay_initial_ms: Option<u64>,
65 retry_delay_max_ms: Option<u64>,
66 heartbeat_interval_secs: Option<u64>,
67 auth_timeout_secs: Option<u64>,
68 update_instruments_interval_mins: Option<u64>,
69 auto_load_missing_instruments: Option<bool>,
70 transport_backend: Option<TransportBackend>,
71 ) -> PyResult<Self> {
72 let defaults = Self::default();
73
74 let config = Self {
75 api_key: api_key.map(SecretString::from),
76 api_secret: api_secret.map(SecretString::from),
77 product_types: product_types.unwrap_or(defaults.product_types),
78 base_url_http,
79 base_url_ws,
80 proxy_url: proxy_url.map(SecretString::from),
81 environment: environment.unwrap_or(defaults.environment),
82 http_timeout_secs: http_timeout_secs.unwrap_or(defaults.http_timeout_secs),
83 max_retries: max_retries.unwrap_or(defaults.max_retries),
84 retry_delay_initial_ms: retry_delay_initial_ms
85 .unwrap_or(defaults.retry_delay_initial_ms),
86 retry_delay_max_ms: retry_delay_max_ms.unwrap_or(defaults.retry_delay_max_ms),
87 heartbeat_interval_secs: heartbeat_interval_secs
88 .unwrap_or(defaults.heartbeat_interval_secs),
89 auth_timeout_secs,
90 update_instruments_interval_mins: update_instruments_interval_mins
91 .unwrap_or(defaults.update_instruments_interval_mins),
92 auto_load_missing_instruments: auto_load_missing_instruments
93 .unwrap_or(defaults.auto_load_missing_instruments),
94 transport_backend: transport_backend.unwrap_or(defaults.transport_backend),
95 };
96
97 config.validate().map_err(config_error_to_pyvalue_err)?;
98 Ok(config)
99 }
100
101 #[getter]
102 const fn has_proxy_url(&self) -> bool {
103 self.proxy_url.is_some()
104 }
105
106 fn __repr__(&self) -> String {
107 stringify!(DeribitDataClientConfig).to_string()
108 }
109}
110
111#[pymethods]
112#[pyo3_stub_gen::derive::gen_stub_pymethods]
113impl DeribitExecutionClientConfig {
114 #[new]
116 #[pyo3(signature = (
117 account_id,
118 product_types = None,
119 environment = None,
120 api_key = None,
121 api_secret = None,
122 base_url_http = None,
123 base_url_ws = None,
124 proxy_url = None,
125 http_timeout_secs = None,
126 max_retries = None,
127 retry_delay_initial_ms = None,
128 retry_delay_max_ms = None,
129 auth_timeout_secs = None,
130 transport_backend = None,
131 ))]
132 #[expect(clippy::too_many_arguments)]
133 fn py_new(
134 account_id: AccountId,
135 product_types: Option<Vec<DeribitProductType>>,
136 environment: Option<DeribitEnvironment>,
137 api_key: Option<String>,
138 api_secret: Option<String>,
139 base_url_http: Option<String>,
140 base_url_ws: Option<String>,
141 proxy_url: Option<String>,
142 http_timeout_secs: Option<u64>,
143 max_retries: Option<u32>,
144 retry_delay_initial_ms: Option<u64>,
145 retry_delay_max_ms: Option<u64>,
146 auth_timeout_secs: Option<u64>,
147 transport_backend: Option<TransportBackend>,
148 ) -> PyResult<Self> {
149 let defaults = Self::default();
150
151 let config = Self {
152 account_id,
153 api_key: api_key.map(SecretString::from),
154 api_secret: api_secret.map(SecretString::from),
155 product_types: product_types.unwrap_or(defaults.product_types),
156 base_url_http,
157 base_url_ws,
158 proxy_url: proxy_url.map(SecretString::from),
159 environment: environment.unwrap_or(defaults.environment),
160 http_timeout_secs: http_timeout_secs.unwrap_or(defaults.http_timeout_secs),
161 max_retries: max_retries.unwrap_or(defaults.max_retries),
162 retry_delay_initial_ms: retry_delay_initial_ms
163 .unwrap_or(defaults.retry_delay_initial_ms),
164 retry_delay_max_ms: retry_delay_max_ms.unwrap_or(defaults.retry_delay_max_ms),
165 auth_timeout_secs,
166 transport_backend: transport_backend.unwrap_or(defaults.transport_backend),
167 };
168
169 config.validate().map_err(config_error_to_pyvalue_err)?;
170 Ok(config)
171 }
172
173 #[getter]
174 const fn has_proxy_url(&self) -> bool {
175 self.proxy_url.is_some()
176 }
177
178 fn __repr__(&self) -> String {
179 stringify!(DeribitExecutionClientConfig).to_string()
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use pyo3::{
186 Python,
187 exceptions::PyValueError,
188 types::{PyDict, PyList},
189 };
190 use rstest::rstest;
191
192 use super::*;
193
194 #[rstest]
195 #[case::data(false)]
196 #[case::execution(true)]
197 fn test_python_config_duplicate_types(
198 #[case] execution: bool,
199 #[values(false, true)] duplicate: bool,
200 ) {
201 Python::initialize();
202 Python::attach(|py| {
203 let values = if duplicate {
204 vec![
205 DeribitProductType::Future,
206 DeribitProductType::Option,
207 DeribitProductType::Future,
208 ]
209 } else {
210 vec![DeribitProductType::Future, DeribitProductType::Option]
211 };
212
213 let objects: Vec<_> = values
214 .iter()
215 .map(|value| Py::new(py, *value).unwrap())
216 .collect();
217 let list = PyList::new(py, objects).unwrap();
218 let kwargs = PyDict::new(py);
219 kwargs.set_item("product_types", list).unwrap();
220 if execution {
221 kwargs
222 .set_item(
223 "account_id",
224 Py::new(py, AccountId::from("DERIBIT-001")).unwrap(),
225 )
226 .unwrap();
227 }
228
229 let config_type = if execution {
230 py.get_type::<DeribitExecutionClientConfig>()
231 } else {
232 py.get_type::<DeribitDataClientConfig>()
233 };
234
235 let result = config_type.call((), Some(&kwargs));
236 if duplicate {
237 let error = result.unwrap_err();
238 assert!(error.is_instance_of::<PyValueError>(py));
239 assert_eq!(
240 error.value(py).str().unwrap().to_str().unwrap(),
241 "duplicate product_types"
242 );
243 } else {
244 let config = result.unwrap();
245 let actual: Vec<DeribitProductType> =
246 config.getattr("product_types").unwrap().extract().unwrap();
247 assert_eq!(actual, values);
248 }
249 });
250 }
251}