1use 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 #[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 #[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}