Skip to main content

nautilus_deribit/python/
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//! Python bindings for Deribit configuration.
17
18use 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    /// Configuration for the Deribit data client.
34    #[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    /// Configuration for the Deribit execution client.
115    #[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}