1use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err};
19use nautilus_model::{
20 enums::{OrderSide, OrderType, TimeInForce},
21 identifiers::{AccountId, ClientOrderId, InstrumentId, VenueOrderId},
22 python::instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
23 types::{Price, Quantity},
24};
25use pyo3::{conversion::IntoPyObjectExt, prelude::*, types::PyList};
26
27use crate::{
28 common::enums::BybitProductType,
29 http::{client::BybitHttpClient, error::BybitHttpError},
30};
31
32#[pymethods]
33impl BybitHttpClient {
34 #[new]
35 #[pyo3(signature = (api_key=None, api_secret=None, base_url=None, timeout_secs=None, max_retries=None, retry_delay_ms=None, retry_delay_max_ms=None))]
36 #[allow(clippy::too_many_arguments)]
37 fn py_new(
38 api_key: Option<String>,
39 api_secret: Option<String>,
40 base_url: Option<String>,
41 timeout_secs: Option<u64>,
42 max_retries: Option<u32>,
43 retry_delay_ms: Option<u64>,
44 retry_delay_max_ms: Option<u64>,
45 ) -> PyResult<Self> {
46 let timeout = timeout_secs.or(Some(60));
47
48 let key = api_key.or_else(|| std::env::var("BYBIT_API_KEY").ok());
50 let secret = api_secret.or_else(|| std::env::var("BYBIT_API_SECRET").ok());
51
52 if let (Some(k), Some(s)) = (key, secret) {
53 Self::with_credentials(
54 k,
55 s,
56 base_url,
57 timeout,
58 max_retries,
59 retry_delay_ms,
60 retry_delay_max_ms,
61 )
62 .map_err(to_pyvalue_err)
63 } else {
64 Self::new(
65 base_url,
66 timeout,
67 max_retries,
68 retry_delay_ms,
69 retry_delay_max_ms,
70 )
71 .map_err(to_pyvalue_err)
72 }
73 }
74
75 #[getter]
76 #[pyo3(name = "base_url")]
77 #[must_use]
78 pub fn py_base_url(&self) -> &str {
79 self.base_url()
80 }
81
82 #[getter]
83 #[pyo3(name = "api_key")]
84 #[must_use]
85 pub fn py_api_key(&self) -> Option<&str> {
86 self.credential().map(|c| c.api_key()).map(|u| u.as_str())
87 }
88
89 #[pyo3(name = "add_instrument")]
90 fn py_add_instrument(&self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
91 let inst_any = pyobject_to_instrument_any(py, instrument)?;
92 self.add_instrument(inst_any);
93 Ok(())
94 }
95
96 #[pyo3(name = "cancel_all_requests")]
97 fn py_cancel_all_requests(&self) {
98 self.cancel_all_requests();
99 }
100
101 #[pyo3(name = "request_instruments")]
102 #[pyo3(signature = (product_type, symbol=None))]
103 fn py_request_instruments<'py>(
104 &self,
105 py: Python<'py>,
106 product_type: BybitProductType,
107 symbol: Option<String>,
108 ) -> PyResult<Bound<'py, PyAny>> {
109 let client = self.clone();
110
111 pyo3_async_runtimes::tokio::future_into_py(py, async move {
112 let instruments = client
113 .request_instruments(product_type, symbol)
114 .await
115 .map_err(to_pyvalue_err)?;
116
117 Python::attach(|py| {
118 let py_instruments: PyResult<Vec<_>> = instruments
119 .into_iter()
120 .map(|inst| instrument_any_to_pyobject(py, inst))
121 .collect();
122 let pylist = PyList::new(py, py_instruments?)
123 .unwrap()
124 .into_any()
125 .unbind();
126 Ok(pylist)
127 })
128 })
129 }
130
131 #[pyo3(name = "submit_order")]
132 #[pyo3(signature = (
133 product_type,
134 instrument_id,
135 client_order_id,
136 order_side,
137 order_type,
138 quantity,
139 time_in_force,
140 price = None,
141 reduce_only = false
142 ))]
143 #[allow(clippy::too_many_arguments)]
144 fn py_submit_order<'py>(
145 &self,
146 py: Python<'py>,
147 product_type: BybitProductType,
148 instrument_id: InstrumentId,
149 client_order_id: ClientOrderId,
150 order_side: OrderSide,
151 order_type: OrderType,
152 quantity: Quantity,
153 time_in_force: TimeInForce,
154 price: Option<Price>,
155 reduce_only: bool,
156 ) -> PyResult<Bound<'py, PyAny>> {
157 let client = self.clone();
158
159 pyo3_async_runtimes::tokio::future_into_py(py, async move {
160 let report = client
161 .submit_order(
162 product_type,
163 instrument_id,
164 client_order_id,
165 order_side,
166 order_type,
167 quantity,
168 time_in_force,
169 price,
170 reduce_only,
171 )
172 .await
173 .map_err(to_pyvalue_err)?;
174
175 Python::attach(|py| report.into_py_any(py))
176 })
177 }
178
179 #[pyo3(name = "modify_order")]
180 #[pyo3(signature = (
181 product_type,
182 instrument_id,
183 client_order_id=None,
184 venue_order_id=None,
185 quantity=None,
186 price=None
187 ))]
188 #[allow(clippy::too_many_arguments)]
189 fn py_modify_order<'py>(
190 &self,
191 py: Python<'py>,
192 product_type: BybitProductType,
193 instrument_id: InstrumentId,
194 client_order_id: Option<ClientOrderId>,
195 venue_order_id: Option<VenueOrderId>,
196 quantity: Option<Quantity>,
197 price: Option<Price>,
198 ) -> PyResult<Bound<'py, PyAny>> {
199 let client = self.clone();
200
201 pyo3_async_runtimes::tokio::future_into_py(py, async move {
202 let report = client
203 .modify_order(
204 product_type,
205 instrument_id,
206 client_order_id,
207 venue_order_id,
208 quantity,
209 price,
210 )
211 .await
212 .map_err(to_pyvalue_err)?;
213
214 Python::attach(|py| report.into_py_any(py))
215 })
216 }
217
218 #[pyo3(name = "cancel_order")]
219 #[pyo3(signature = (product_type, instrument_id, client_order_id=None, venue_order_id=None))]
220 fn py_cancel_order<'py>(
221 &self,
222 py: Python<'py>,
223 product_type: BybitProductType,
224 instrument_id: InstrumentId,
225 client_order_id: Option<ClientOrderId>,
226 venue_order_id: Option<VenueOrderId>,
227 ) -> PyResult<Bound<'py, PyAny>> {
228 let client = self.clone();
229
230 pyo3_async_runtimes::tokio::future_into_py(py, async move {
231 let report = client
232 .cancel_order(product_type, instrument_id, client_order_id, venue_order_id)
233 .await
234 .map_err(to_pyvalue_err)?;
235
236 Python::attach(|py| report.into_py_any(py))
237 })
238 }
239
240 #[pyo3(name = "cancel_all_orders")]
241 fn py_cancel_all_orders<'py>(
242 &self,
243 py: Python<'py>,
244 product_type: BybitProductType,
245 instrument_id: InstrumentId,
246 ) -> PyResult<Bound<'py, PyAny>> {
247 let client = self.clone();
248
249 pyo3_async_runtimes::tokio::future_into_py(py, async move {
250 let reports = client
251 .cancel_all_orders(product_type, instrument_id)
252 .await
253 .map_err(to_pyvalue_err)?;
254
255 Python::attach(|py| {
256 let py_reports: PyResult<Vec<_>> = reports
257 .into_iter()
258 .map(|report| report.into_py_any(py))
259 .collect();
260 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
261 Ok(pylist)
262 })
263 })
264 }
265
266 #[pyo3(name = "query_order")]
267 #[pyo3(signature = (account_id, product_type, instrument_id, client_order_id=None, venue_order_id=None))]
268 fn py_query_order<'py>(
269 &self,
270 py: Python<'py>,
271 account_id: AccountId,
272 product_type: BybitProductType,
273 instrument_id: InstrumentId,
274 client_order_id: Option<ClientOrderId>,
275 venue_order_id: Option<VenueOrderId>,
276 ) -> PyResult<Bound<'py, PyAny>> {
277 let client = self.clone();
278
279 pyo3_async_runtimes::tokio::future_into_py(py, async move {
280 match client
281 .query_order(
282 account_id,
283 product_type,
284 instrument_id,
285 client_order_id,
286 venue_order_id,
287 )
288 .await
289 {
290 Ok(Some(report)) => Python::attach(|py| report.into_py_any(py)),
291 Ok(None) => Ok(Python::attach(|py| py.None())),
292 Err(e) => Err(to_pyvalue_err(e)),
293 }
294 })
295 }
296
297 #[pyo3(name = "request_trades")]
298 #[pyo3(signature = (product_type, instrument_id, limit=None))]
299 fn py_request_trades<'py>(
300 &self,
301 py: Python<'py>,
302 product_type: BybitProductType,
303 instrument_id: InstrumentId,
304 limit: Option<u32>,
305 ) -> PyResult<Bound<'py, PyAny>> {
306 let client = self.clone();
307
308 pyo3_async_runtimes::tokio::future_into_py(py, async move {
309 let trades = client
310 .request_trades(product_type, instrument_id, limit)
311 .await
312 .map_err(to_pyvalue_err)?;
313
314 Python::attach(|py| {
315 let py_trades: PyResult<Vec<_>> = trades
316 .into_iter()
317 .map(|trade| trade.into_py_any(py))
318 .collect();
319 let pylist = PyList::new(py, py_trades?).unwrap().into_any().unbind();
320 Ok(pylist)
321 })
322 })
323 }
324
325 #[pyo3(name = "request_bars")]
326 #[pyo3(signature = (product_type, bar_type, start=None, end=None, limit=None))]
327 fn py_request_bars<'py>(
328 &self,
329 py: Python<'py>,
330 product_type: BybitProductType,
331 bar_type: nautilus_model::data::BarType,
332 start: Option<i64>,
333 end: Option<i64>,
334 limit: Option<u32>,
335 ) -> PyResult<Bound<'py, PyAny>> {
336 let client = self.clone();
337
338 pyo3_async_runtimes::tokio::future_into_py(py, async move {
339 let bars = client
340 .request_bars(product_type, bar_type, start, end, limit)
341 .await
342 .map_err(to_pyvalue_err)?;
343
344 Python::attach(|py| {
345 let py_bars: PyResult<Vec<_>> =
346 bars.into_iter().map(|bar| bar.into_py_any(py)).collect();
347 let pylist = PyList::new(py, py_bars?).unwrap().into_any().unbind();
348 Ok(pylist)
349 })
350 })
351 }
352
353 #[pyo3(name = "request_fee_rates")]
354 #[pyo3(signature = (product_type, symbol=None, base_coin=None))]
355 fn py_request_fee_rates<'py>(
356 &self,
357 py: Python<'py>,
358 product_type: BybitProductType,
359 symbol: Option<String>,
360 base_coin: Option<String>,
361 ) -> PyResult<Bound<'py, PyAny>> {
362 let client = self.clone();
363
364 pyo3_async_runtimes::tokio::future_into_py(py, async move {
365 let fee_rates = client
366 .request_fee_rates(product_type, symbol, base_coin)
367 .await
368 .map_err(to_pyvalue_err)?;
369
370 Python::attach(|py| {
371 let py_fee_rates: PyResult<Vec<_>> = fee_rates
372 .into_iter()
373 .map(|rate| Py::new(py, rate))
374 .collect();
375 let pylist = PyList::new(py, py_fee_rates?).unwrap().into_any().unbind();
376 Ok(pylist)
377 })
378 })
379 }
380
381 #[pyo3(name = "request_account_state")]
382 fn py_request_account_state<'py>(
383 &self,
384 py: Python<'py>,
385 account_type: crate::common::enums::BybitAccountType,
386 account_id: AccountId,
387 ) -> PyResult<Bound<'py, PyAny>> {
388 let client = self.clone();
389
390 pyo3_async_runtimes::tokio::future_into_py(py, async move {
391 let account_state = client
392 .request_account_state(account_type, account_id)
393 .await
394 .map_err(to_pyvalue_err)?;
395
396 Python::attach(|py| account_state.into_py_any(py))
397 })
398 }
399
400 #[pyo3(name = "request_order_status_reports")]
401 #[pyo3(signature = (account_id, product_type, instrument_id=None, open_only=false, limit=None))]
402 fn py_request_order_status_reports<'py>(
403 &self,
404 py: Python<'py>,
405 account_id: AccountId,
406 product_type: BybitProductType,
407 instrument_id: Option<InstrumentId>,
408 open_only: bool,
409 limit: Option<u32>,
410 ) -> PyResult<Bound<'py, PyAny>> {
411 let client = self.clone();
412
413 pyo3_async_runtimes::tokio::future_into_py(py, async move {
414 let reports = client
415 .request_order_status_reports(
416 account_id,
417 product_type,
418 instrument_id,
419 open_only,
420 limit,
421 )
422 .await
423 .map_err(to_pyvalue_err)?;
424
425 Python::attach(|py| {
426 let py_reports: PyResult<Vec<_>> = reports
427 .into_iter()
428 .map(|report| report.into_py_any(py))
429 .collect();
430 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
431 Ok(pylist)
432 })
433 })
434 }
435
436 #[pyo3(name = "request_fill_reports")]
437 #[pyo3(signature = (account_id, product_type, instrument_id=None, start=None, end=None, limit=None))]
438 #[allow(clippy::too_many_arguments)]
439 fn py_request_fill_reports<'py>(
440 &self,
441 py: Python<'py>,
442 account_id: AccountId,
443 product_type: BybitProductType,
444 instrument_id: Option<InstrumentId>,
445 start: Option<i64>,
446 end: Option<i64>,
447 limit: Option<u32>,
448 ) -> PyResult<Bound<'py, PyAny>> {
449 let client = self.clone();
450
451 pyo3_async_runtimes::tokio::future_into_py(py, async move {
452 let reports = client
453 .request_fill_reports(account_id, product_type, instrument_id, start, end, limit)
454 .await
455 .map_err(to_pyvalue_err)?;
456
457 Python::attach(|py| {
458 let py_reports: PyResult<Vec<_>> = reports
459 .into_iter()
460 .map(|report| report.into_py_any(py))
461 .collect();
462 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
463 Ok(pylist)
464 })
465 })
466 }
467
468 #[pyo3(name = "request_position_status_reports")]
469 #[pyo3(signature = (account_id, product_type, instrument_id=None))]
470 fn py_request_position_status_reports<'py>(
471 &self,
472 py: Python<'py>,
473 account_id: AccountId,
474 product_type: BybitProductType,
475 instrument_id: Option<InstrumentId>,
476 ) -> PyResult<Bound<'py, PyAny>> {
477 let client = self.clone();
478
479 pyo3_async_runtimes::tokio::future_into_py(py, async move {
480 let reports = client
481 .request_position_status_reports(account_id, product_type, instrument_id)
482 .await
483 .map_err(to_pyvalue_err)?;
484
485 Python::attach(|py| {
486 let py_reports: PyResult<Vec<_>> = reports
487 .into_iter()
488 .map(|report| report.into_py_any(py))
489 .collect();
490 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
491 Ok(pylist)
492 })
493 })
494 }
495}
496
497impl From<BybitHttpError> for PyErr {
498 fn from(error: BybitHttpError) -> Self {
499 match error {
500 BybitHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
502 BybitHttpError::NetworkError(msg) => to_pyruntime_err(format!("Network error: {msg}")),
503 BybitHttpError::UnexpectedStatus { status, body } => {
504 to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
505 }
506 BybitHttpError::MissingCredentials => {
508 to_pyvalue_err("Missing credentials for authenticated request")
509 }
510 BybitHttpError::ValidationError(msg) => {
511 to_pyvalue_err(format!("Parameter validation error: {msg}"))
512 }
513 BybitHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
514 BybitHttpError::BuildError(e) => to_pyvalue_err(format!("Build error: {e}")),
515 BybitHttpError::BybitError {
516 error_code,
517 message,
518 } => to_pyvalue_err(format!("Bybit error {error_code}: {message}")),
519 }
520 }
521}