Skip to main content

nautilus_bybit/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 Bybit configuration.
17
18use nautilus_common::python::config_error_to_pyvalue_err;
19use nautilus_core::{python::to_pyvalue_err, string::secret::SecretString};
20use nautilus_model::identifiers::AccountId;
21use nautilus_network::websocket::TransportBackend;
22use pyo3::{PyResult, pymethods};
23
24use crate::{
25    common::{
26        enums::{BybitEnvironment, BybitMarginMode, BybitProductType},
27        parse::parse_smp_type,
28    },
29    config::{BybitDataClientConfig, BybitExecutionClientConfig},
30};
31
32#[pymethods]
33#[pyo3_stub_gen::derive::gen_stub_pymethods]
34impl BybitDataClientConfig {
35    /// Configuration for the Bybit live data client.
36    #[new]
37    #[pyo3(signature = (
38        product_types = None,
39        environment = None,
40        api_key = None,
41        api_secret = None,
42        base_url_http = None,
43        base_url_ws_public = None,
44        base_url_ws_private = None,
45        proxy_url = None,
46        http_timeout_secs = None,
47        max_retries = None,
48        retry_delay_initial_ms = None,
49        retry_delay_max_ms = None,
50        heartbeat_interval_secs = None,
51        recv_window_ms = None,
52        update_instruments_interval_mins = None,
53        instrument_status_poll_secs = None,
54        transport_backend = None,
55        book_snapshot_timeout_secs = None,
56        bars_timestamp_on_close = None,
57    ))]
58    #[expect(clippy::too_many_arguments)]
59    fn py_new(
60        product_types: Option<Vec<BybitProductType>>,
61        environment: Option<BybitEnvironment>,
62        api_key: Option<String>,
63        api_secret: Option<String>,
64        base_url_http: Option<String>,
65        base_url_ws_public: Option<String>,
66        base_url_ws_private: Option<String>,
67        proxy_url: Option<String>,
68        http_timeout_secs: Option<u64>,
69        max_retries: Option<u32>,
70        retry_delay_initial_ms: Option<u64>,
71        retry_delay_max_ms: Option<u64>,
72        heartbeat_interval_secs: Option<u64>,
73        recv_window_ms: Option<u64>,
74        update_instruments_interval_mins: Option<u64>,
75        instrument_status_poll_secs: Option<u64>,
76        transport_backend: Option<TransportBackend>,
77        book_snapshot_timeout_secs: Option<u64>,
78        bars_timestamp_on_close: Option<bool>,
79    ) -> PyResult<Self> {
80        let defaults = Self::default();
81
82        let config = Self {
83            api_key: api_key.map(SecretString::from),
84            api_secret: api_secret.map(SecretString::from),
85            product_types: product_types.unwrap_or(defaults.product_types),
86            environment: environment.unwrap_or(defaults.environment),
87            base_url_http,
88            base_url_ws_public,
89            base_url_ws_private,
90            proxy_url: proxy_url.map(SecretString::from),
91            http_timeout_secs: http_timeout_secs.unwrap_or(defaults.http_timeout_secs),
92            max_retries: max_retries.unwrap_or(defaults.max_retries),
93            retry_delay_initial_ms: retry_delay_initial_ms
94                .unwrap_or(defaults.retry_delay_initial_ms),
95            retry_delay_max_ms: retry_delay_max_ms.unwrap_or(defaults.retry_delay_max_ms),
96            heartbeat_interval_secs: heartbeat_interval_secs
97                .unwrap_or(defaults.heartbeat_interval_secs),
98            recv_window_ms: recv_window_ms.unwrap_or(defaults.recv_window_ms),
99            update_instruments_interval_mins: update_instruments_interval_mins
100                .or(defaults.update_instruments_interval_mins),
101            instrument_poll_interval_secs: instrument_status_poll_secs
102                .or(defaults.instrument_poll_interval_secs),
103            book_snapshot_timeout_secs: book_snapshot_timeout_secs
104                .unwrap_or(defaults.book_snapshot_timeout_secs),
105            transport_backend: transport_backend.unwrap_or(defaults.transport_backend),
106            bars_timestamp_on_close: bars_timestamp_on_close
107                .unwrap_or(defaults.bars_timestamp_on_close),
108        };
109
110        config.validate().map_err(config_error_to_pyvalue_err)?;
111        Ok(config)
112    }
113
114    #[getter]
115    const fn instrument_status_poll_secs(&self) -> Option<u64> {
116        self.instrument_poll_interval_secs
117    }
118
119    #[getter]
120    const fn has_proxy_url(&self) -> bool {
121        self.proxy_url.is_some()
122    }
123
124    fn __repr__(&self) -> String {
125        stringify!(BybitDataClientConfig).to_string()
126    }
127}
128
129#[pymethods]
130#[pyo3_stub_gen::derive::gen_stub_pymethods]
131impl BybitExecutionClientConfig {
132    /// Configuration for the Bybit live execution client.
133    #[new]
134    #[pyo3(signature = (
135        product_types = None,
136        environment = None,
137        api_key = None,
138        api_secret = None,
139        base_url_http = None,
140        base_url_ws_private = None,
141        base_url_ws_trade = None,
142        proxy_url = None,
143        http_timeout_secs = None,
144        max_retries = None,
145        retry_delay_initial_ms = None,
146        retry_delay_max_ms = None,
147        heartbeat_interval_secs = None,
148        auth_timeout_secs = None,
149        recv_window_ms = None,
150        account_id = None,
151        use_spot_position_reports = None,
152        auto_repay_spot_borrows = None,
153        margin_mode = None,
154        smp_type = None,
155        transport_backend = None,
156    ))]
157    #[expect(clippy::too_many_arguments)]
158    fn py_new(
159        product_types: Option<Vec<BybitProductType>>,
160        environment: Option<BybitEnvironment>,
161        api_key: Option<String>,
162        api_secret: Option<String>,
163        base_url_http: Option<String>,
164        base_url_ws_private: Option<String>,
165        base_url_ws_trade: Option<String>,
166        proxy_url: Option<String>,
167        http_timeout_secs: Option<u64>,
168        max_retries: Option<u32>,
169        retry_delay_initial_ms: Option<u64>,
170        retry_delay_max_ms: Option<u64>,
171        heartbeat_interval_secs: Option<u64>,
172        auth_timeout_secs: Option<u64>,
173        recv_window_ms: Option<u64>,
174        account_id: Option<AccountId>,
175        use_spot_position_reports: Option<bool>,
176        auto_repay_spot_borrows: Option<bool>,
177        margin_mode: Option<BybitMarginMode>,
178        smp_type: Option<String>,
179        transport_backend: Option<TransportBackend>,
180    ) -> PyResult<Self> {
181        let smp_type = smp_type
182            .map(|value| parse_smp_type(&value))
183            .transpose()
184            .map_err(to_pyvalue_err)?;
185
186        let defaults = Self::default();
187
188        let config = Self {
189            api_key: api_key.map(SecretString::from),
190            api_secret: api_secret.map(SecretString::from),
191            product_types: product_types.unwrap_or(defaults.product_types),
192            environment: environment.unwrap_or(defaults.environment),
193            base_url_http,
194            base_url_ws_private,
195            base_url_ws_trade,
196            proxy_url: proxy_url.map(SecretString::from),
197            http_timeout_secs: http_timeout_secs.unwrap_or(defaults.http_timeout_secs),
198            max_retries: max_retries.unwrap_or(defaults.max_retries),
199            retry_delay_initial_ms: retry_delay_initial_ms
200                .unwrap_or(defaults.retry_delay_initial_ms),
201            retry_delay_max_ms: retry_delay_max_ms.unwrap_or(defaults.retry_delay_max_ms),
202            heartbeat_interval_secs: heartbeat_interval_secs
203                .unwrap_or(defaults.heartbeat_interval_secs),
204            auth_timeout_secs,
205            recv_window_ms: recv_window_ms.unwrap_or(defaults.recv_window_ms),
206            account_id,
207            use_spot_position_reports: use_spot_position_reports
208                .unwrap_or(defaults.use_spot_position_reports),
209            auto_repay_spot_borrows: auto_repay_spot_borrows
210                .unwrap_or(defaults.auto_repay_spot_borrows),
211            futures_leverages: None,
212            position_mode: None,
213            margin_mode,
214            smp_type,
215            transport_backend: transport_backend.unwrap_or(defaults.transport_backend),
216        };
217
218        config.validate().map_err(config_error_to_pyvalue_err)?;
219        Ok(config)
220    }
221
222    #[getter]
223    #[pyo3(name = "smp_type")]
224    fn py_smp_type(&self) -> Option<String> {
225        self.smp_type.map(|smp_type| smp_type.as_ref().to_string())
226    }
227
228    #[getter]
229    const fn has_proxy_url(&self) -> bool {
230        self.proxy_url.is_some()
231    }
232
233    fn __repr__(&self) -> String {
234        stringify!(BybitExecutionClientConfig).to_string()
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use pyo3::{
241        Python,
242        exceptions::PyValueError,
243        prelude::*,
244        types::{PyDict, PyList},
245    };
246    use rstest::rstest;
247
248    use super::*;
249
250    #[rstest]
251    #[case::data(false)]
252    #[case::execution(true)]
253    fn test_python_config_duplicate_types(
254        #[case] execution: bool,
255        #[values(false, true)] duplicate: bool,
256    ) {
257        Python::initialize();
258        Python::attach(|py| {
259            let values = if duplicate {
260                vec![
261                    BybitProductType::Linear,
262                    BybitProductType::Spot,
263                    BybitProductType::Linear,
264                ]
265            } else {
266                vec![BybitProductType::Linear, BybitProductType::Spot]
267            };
268
269            let objects: Vec<_> = values
270                .iter()
271                .map(|value| Py::new(py, *value).unwrap())
272                .collect();
273            let list = PyList::new(py, objects).unwrap();
274            let kwargs = PyDict::new(py);
275            kwargs.set_item("product_types", list).unwrap();
276            if execution {
277                kwargs
278                    .set_item(
279                        "account_id",
280                        Py::new(py, AccountId::from("BYBIT-001")).unwrap(),
281                    )
282                    .unwrap();
283            }
284
285            let config_type = if execution {
286                py.get_type::<BybitExecutionClientConfig>()
287            } else {
288                py.get_type::<BybitDataClientConfig>()
289            };
290
291            let result = config_type.call((), Some(&kwargs));
292            if duplicate {
293                let error = result.unwrap_err();
294                assert!(error.is_instance_of::<PyValueError>(py));
295                assert_eq!(
296                    error.value(py).str().unwrap().to_str().unwrap(),
297                    "duplicate product_types"
298                );
299            } else {
300                let config = result.unwrap();
301                let actual: Vec<BybitProductType> =
302                    config.getattr("product_types").unwrap().extract().unwrap();
303                assert_eq!(actual, values);
304            }
305        });
306    }
307}