1use std::collections::HashSet;
19
20use chrono::{DateTime, Utc};
21use nautilus_core::{
22 UnixNanos,
23 python::{to_pyruntime_err, to_pyvalue_err},
24};
25use nautilus_model::{
26 data::{BarType, forward::ForwardPrice},
27 enums::{OrderSide, OrderType, TimeInForce},
28 identifiers::{AccountId, ClientOrderId, InstrumentId, Symbol, VenueOrderId},
29 python::instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
30 types::{Price, Quantity},
31};
32use pyo3::{
33 conversion::IntoPyObjectExt,
34 prelude::*,
35 types::{PyDict, PyList},
36};
37use ustr::Ustr;
38
39use crate::{
40 common::{
41 enums::{
42 BybitMarginMode, BybitOpenOnly, BybitOrderFilter, BybitPositionIdx, BybitPositionMode,
43 BybitProductType,
44 },
45 parse::{extract_raw_symbol, parse_bbo_level, parse_bbo_side_type},
46 },
47 http::{
48 client::{BybitHttpClient, BybitRawHttpClient},
49 error::BybitHttpError,
50 models::BybitOrderCursorList,
51 query::BybitNativeTpSlParams as RustNativeTpSlParams,
52 },
53 python::params::BybitNativeTpSlParams,
54};
55
56#[pymethods]
57#[pyo3_stub_gen::derive::gen_stub_pymethods]
58impl BybitRawHttpClient {
59 #[new]
64 #[pyo3(signature = (api_key=None, api_secret=None, base_url=None, demo=false, testnet=false, timeout_secs=60, max_retries=3, retry_delay_ms=1000, retry_delay_max_ms=10_000, recv_window_ms=5_000, proxy_url=None))]
65 #[expect(clippy::too_many_arguments)]
66 fn py_new(
67 api_key: Option<String>,
68 api_secret: Option<String>,
69 base_url: Option<String>,
70 demo: bool,
71 testnet: bool,
72 timeout_secs: u64,
73 max_retries: u32,
74 retry_delay_ms: u64,
75 retry_delay_max_ms: u64,
76 recv_window_ms: u64,
77 proxy_url: Option<String>,
78 ) -> PyResult<Self> {
79 Self::new_with_env(
80 api_key,
81 api_secret,
82 base_url,
83 demo,
84 testnet,
85 timeout_secs,
86 max_retries,
87 retry_delay_ms,
88 retry_delay_max_ms,
89 recv_window_ms,
90 proxy_url,
91 )
92 .map_err(to_pyvalue_err)
93 }
94
95 #[getter]
97 #[pyo3(name = "base_url")]
98 #[must_use]
99 pub fn py_base_url(&self) -> &str {
100 self.base_url()
101 }
102
103 #[getter]
104 #[pyo3(name = "api_key")]
105 #[must_use]
106 pub fn py_api_key(&self) -> Option<String> {
107 self.credential().map(|c| c.api_key().to_string())
108 }
109
110 #[getter]
112 #[pyo3(name = "recv_window_ms")]
113 #[must_use]
114 pub fn py_recv_window_ms(&self) -> u64 {
115 self.recv_window_ms()
116 }
117
118 #[pyo3(name = "cancel_all_requests")]
120 fn py_cancel_all_requests(&self) {
121 self.cancel_all_requests();
122 }
123
124 #[pyo3(name = "get_server_time")]
134 fn py_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
135 let client = self.clone();
136
137 pyo3_async_runtimes::tokio::future_into_py(py, async move {
138 let response = client.get_server_time().await.map_err(to_pyvalue_err)?;
139
140 Python::attach(|py| {
141 let server_time = Py::new(py, response.result)?;
142 Ok(server_time.into_any())
143 })
144 })
145 }
146
147 #[pyo3(name = "get_open_orders")]
153 #[pyo3(signature = (category, symbol=None, base_coin=None, settle_coin=None, order_id=None, order_link_id=None, open_only=None, order_filter=None, limit=None, cursor=None))]
154 #[expect(clippy::too_many_arguments)]
155 fn py_get_open_orders<'py>(
156 &self,
157 py: Python<'py>,
158 category: BybitProductType,
159 symbol: Option<String>,
160 base_coin: Option<String>,
161 settle_coin: Option<String>,
162 order_id: Option<String>,
163 order_link_id: Option<String>,
164 open_only: Option<BybitOpenOnly>,
165 order_filter: Option<BybitOrderFilter>,
166 limit: Option<u32>,
167 cursor: Option<String>,
168 ) -> PyResult<Bound<'py, PyAny>> {
169 let client = self.clone();
170
171 pyo3_async_runtimes::tokio::future_into_py(py, async move {
172 let response = client
173 .get_open_orders(
174 category,
175 symbol,
176 base_coin,
177 settle_coin,
178 order_id,
179 order_link_id,
180 open_only,
181 order_filter,
182 limit,
183 cursor,
184 )
185 .await
186 .map_err(to_pyvalue_err)?;
187
188 Python::attach(|py| {
189 let open_orders = BybitOrderCursorList::from(response.result);
190 let py_open_orders = Py::new(py, open_orders)?;
191 Ok(py_open_orders.into_any())
192 })
193 })
194 }
195}
196
197#[pymethods]
198#[pyo3_stub_gen::derive::gen_stub_pymethods]
199impl BybitHttpClient {
200 #[new]
206 #[pyo3(signature = (api_key=None, api_secret=None, base_url=None, demo=false, testnet=false, timeout_secs=60, max_retries=3, retry_delay_ms=1000, retry_delay_max_ms=10_000, recv_window_ms=5_000, proxy_url=None))]
207 #[expect(clippy::too_many_arguments)]
208 fn py_new(
209 api_key: Option<String>,
210 api_secret: Option<String>,
211 base_url: Option<String>,
212 demo: bool,
213 testnet: bool,
214 timeout_secs: u64,
215 max_retries: u32,
216 retry_delay_ms: u64,
217 retry_delay_max_ms: u64,
218 recv_window_ms: u64,
219 proxy_url: Option<String>,
220 ) -> PyResult<Self> {
221 Self::new_with_env(
222 api_key,
223 api_secret,
224 base_url,
225 demo,
226 testnet,
227 timeout_secs,
228 max_retries,
229 retry_delay_ms,
230 retry_delay_max_ms,
231 recv_window_ms,
232 proxy_url,
233 )
234 .map_err(to_pyvalue_err)
235 }
236
237 #[getter]
238 #[pyo3(name = "base_url")]
239 #[must_use]
240 pub fn py_base_url(&self) -> &str {
241 self.base_url()
242 }
243
244 #[getter]
245 #[pyo3(name = "api_key")]
246 #[must_use]
247 pub fn py_api_key(&self) -> Option<&str> {
248 self.credential().map(|c| c.api_key())
249 }
250
251 #[getter]
252 #[pyo3(name = "api_key_masked")]
253 #[must_use]
254 pub fn py_api_key_masked(&self) -> Option<String> {
255 self.credential().map(|c| c.api_key_masked())
256 }
257
258 #[pyo3(name = "cache_instrument")]
260 fn py_cache_instrument(&self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
261 let inst_any = pyobject_to_instrument_any(py, instrument)?;
262 self.cache_instrument(inst_any);
263 Ok(())
264 }
265
266 #[pyo3(name = "cancel_all_requests")]
267 fn py_cancel_all_requests(&self) {
268 self.cancel_all_requests();
269 }
270
271 #[pyo3(name = "set_use_spot_position_reports")]
272 fn py_set_use_spot_position_reports(&self, value: bool) {
273 self.set_use_spot_position_reports(value);
274 }
275
276 #[pyo3(name = "set_margin_mode")]
282 fn py_set_margin_mode<'py>(
283 &self,
284 py: Python<'py>,
285 margin_mode: BybitMarginMode,
286 ) -> PyResult<Bound<'py, PyAny>> {
287 let client = self.clone();
288
289 pyo3_async_runtimes::tokio::future_into_py(py, async move {
290 client
291 .set_margin_mode(margin_mode)
292 .await
293 .map_err(to_pyvalue_err)?;
294
295 Python::attach(|py| Ok(py.None()))
296 })
297 }
298
299 #[pyo3(name = "get_account_details")]
311 fn py_get_account_details<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
312 let client = self.clone();
313
314 pyo3_async_runtimes::tokio::future_into_py(py, async move {
315 let response = client.get_account_details().await.map_err(to_pyvalue_err)?;
316
317 Python::attach(|py| {
318 let account_details = Py::new(py, response.result)?;
319 Ok(account_details.into_any())
320 })
321 })
322 }
323
324 #[pyo3(name = "set_leverage")]
330 #[pyo3(signature = (product_type, symbol, buy_leverage, sell_leverage))]
331 fn py_set_leverage<'py>(
332 &self,
333 py: Python<'py>,
334 product_type: BybitProductType,
335 symbol: String,
336 buy_leverage: String,
337 sell_leverage: String,
338 ) -> PyResult<Bound<'py, PyAny>> {
339 let client = self.clone();
340
341 pyo3_async_runtimes::tokio::future_into_py(py, async move {
342 client
343 .set_leverage(product_type, &symbol, &buy_leverage, &sell_leverage)
344 .await
345 .map_err(to_pyvalue_err)?;
346
347 Python::attach(|py| Ok(py.None()))
348 })
349 }
350
351 #[pyo3(name = "switch_mode")]
357 #[pyo3(signature = (product_type, mode, symbol=None, coin=None))]
358 fn py_switch_mode<'py>(
359 &self,
360 py: Python<'py>,
361 product_type: BybitProductType,
362 mode: BybitPositionMode,
363 symbol: Option<String>,
364 coin: Option<String>,
365 ) -> PyResult<Bound<'py, PyAny>> {
366 let client = self.clone();
367
368 pyo3_async_runtimes::tokio::future_into_py(py, async move {
369 client
370 .switch_mode(product_type, mode, symbol, coin)
371 .await
372 .map_err(to_pyvalue_err)?;
373
374 Python::attach(|py| Ok(py.None()))
375 })
376 }
377
378 #[pyo3(name = "get_spot_borrow_amount")]
386 fn py_get_spot_borrow_amount<'py>(
387 &self,
388 py: Python<'py>,
389 coin: String,
390 ) -> PyResult<Bound<'py, PyAny>> {
391 let client = self.clone();
392
393 pyo3_async_runtimes::tokio::future_into_py(py, async move {
394 let borrow_amount = client
395 .get_spot_borrow_amount(&coin)
396 .await
397 .map_err(to_pyvalue_err)?;
398
399 Ok(borrow_amount)
400 })
401 }
402
403 #[pyo3(name = "borrow_spot")]
412 #[pyo3(signature = (coin, amount))]
413 fn py_borrow_spot<'py>(
414 &self,
415 py: Python<'py>,
416 coin: String,
417 amount: Quantity,
418 ) -> PyResult<Bound<'py, PyAny>> {
419 let client = self.clone();
420
421 pyo3_async_runtimes::tokio::future_into_py(py, async move {
422 client
423 .borrow_spot(&coin, amount)
424 .await
425 .map_err(to_pyvalue_err)?;
426
427 Python::attach(|py| Ok(py.None()))
428 })
429 }
430
431 #[pyo3(name = "repay_spot_borrow")]
440 #[pyo3(signature = (coin, amount=None))]
441 fn py_repay_spot_borrow<'py>(
442 &self,
443 py: Python<'py>,
444 coin: String,
445 amount: Option<Quantity>,
446 ) -> PyResult<Bound<'py, PyAny>> {
447 let client = self.clone();
448
449 pyo3_async_runtimes::tokio::future_into_py(py, async move {
450 client
451 .repay_spot_borrow(&coin, amount)
452 .await
453 .map_err(to_pyvalue_err)?;
454
455 Python::attach(|py| Ok(py.None()))
456 })
457 }
458
459 #[pyo3(name = "request_instruments")]
465 #[pyo3(signature = (product_type, symbol=None, base_coin=None))]
466 fn py_request_instruments<'py>(
467 &self,
468 py: Python<'py>,
469 product_type: BybitProductType,
470 symbol: Option<String>,
471 base_coin: Option<String>,
472 ) -> PyResult<Bound<'py, PyAny>> {
473 let client = self.clone();
474 let base_coin = base_coin.map(|s| Ustr::from(&s));
475
476 pyo3_async_runtimes::tokio::future_into_py(py, async move {
477 let instruments = client
478 .request_instruments(product_type, symbol, base_coin)
479 .await
480 .map_err(to_pyvalue_err)?;
481
482 Python::attach(|py| {
483 let py_instruments: PyResult<Vec<_>> = instruments
484 .into_iter()
485 .map(|inst| instrument_any_to_pyobject(py, inst))
486 .collect();
487 let pylist = PyList::new(py, py_instruments?)
488 .unwrap()
489 .into_any()
490 .unbind();
491 Ok(pylist)
492 })
493 })
494 }
495
496 #[pyo3(name = "request_instrument_statuses")]
502 fn py_request_instrument_statuses<'py>(
503 &self,
504 py: Python<'py>,
505 product_type: BybitProductType,
506 ) -> PyResult<Bound<'py, PyAny>> {
507 let client = self.clone();
508
509 pyo3_async_runtimes::tokio::future_into_py(py, async move {
510 let statuses = client
511 .request_instrument_statuses(product_type)
512 .await
513 .map_err(to_pyvalue_err)?;
514
515 Python::attach(|py| {
516 let dict = PyDict::new(py);
517 for (instrument_id, action) in statuses {
518 dict.set_item(
519 instrument_id.into_bound_py_any(py)?,
520 action.into_bound_py_any(py)?,
521 )?;
522 }
523 Ok(dict.into_any().unbind())
524 })
525 })
526 }
527
528 #[pyo3(name = "request_tickers")]
537 fn py_request_tickers<'py>(
538 &self,
539 py: Python<'py>,
540 params: crate::python::params::BybitTickersParams,
541 ) -> PyResult<Bound<'py, PyAny>> {
542 let client = self.clone();
543
544 pyo3_async_runtimes::tokio::future_into_py(py, async move {
545 let tickers = client
546 .request_tickers(¶ms.into())
547 .await
548 .map_err(to_pyvalue_err)?;
549
550 Python::attach(|py| {
551 let py_tickers: PyResult<Vec<_>> = tickers
552 .into_iter()
553 .map(|ticker| Py::new(py, ticker))
554 .collect();
555 let pylist = PyList::new(py, py_tickers?).unwrap().into_any().unbind();
556 Ok(pylist)
557 })
558 })
559 }
560
561 #[pyo3(name = "submit_order")]
563 #[pyo3(signature = (
564 account_id,
565 product_type,
566 instrument_id,
567 client_order_id,
568 order_side,
569 order_type,
570 quantity,
571 time_in_force = None,
572 price = None,
573 trigger_price = None,
574 post_only = None,
575 reduce_only = false,
576 is_quote_quantity = false,
577 is_leverage = false,
578 position_idx = None,
579 bbo_side_type = None,
580 bbo_level = None,
581 native_tp_sl = None,
582 ))]
583 #[expect(clippy::too_many_arguments)]
584 fn py_submit_order<'py>(
585 &self,
586 py: Python<'py>,
587 account_id: AccountId,
588 product_type: BybitProductType,
589 instrument_id: InstrumentId,
590 client_order_id: ClientOrderId,
591 order_side: OrderSide,
592 order_type: OrderType,
593 quantity: Quantity,
594 time_in_force: Option<TimeInForce>,
595 price: Option<Price>,
596 trigger_price: Option<Price>,
597 post_only: Option<bool>,
598 reduce_only: bool,
599 is_quote_quantity: bool,
600 is_leverage: bool,
601 position_idx: Option<BybitPositionIdx>,
602 bbo_side_type: Option<String>,
603 bbo_level: Option<String>,
604 native_tp_sl: Option<BybitNativeTpSlParams>,
605 ) -> PyResult<Bound<'py, PyAny>> {
606 let client = self.clone();
607 let bbo_side_type = bbo_side_type
608 .map(|value| parse_bbo_side_type(&value))
609 .transpose()
610 .map_err(to_pyvalue_err)?;
611 let bbo_level = bbo_level
612 .map(parse_bbo_level)
613 .transpose()
614 .map_err(to_pyvalue_err)?;
615 if bbo_side_type.is_some() != bbo_level.is_some() {
616 return Err(to_pyvalue_err(anyhow::anyhow!(
617 "'bbo_side_type' and 'bbo_level' must be provided together"
618 )));
619 }
620
621 let native_tp_sl: Option<RustNativeTpSlParams> = native_tp_sl
622 .map(RustNativeTpSlParams::try_from)
623 .transpose()
624 .map_err(to_pyvalue_err)?;
625
626 pyo3_async_runtimes::tokio::future_into_py(py, async move {
627 let report = client
628 .submit_order(
629 account_id,
630 product_type,
631 instrument_id,
632 client_order_id,
633 order_side,
634 order_type,
635 quantity,
636 time_in_force,
637 price,
638 trigger_price,
639 post_only,
640 reduce_only,
641 is_quote_quantity,
642 is_leverage,
643 position_idx,
644 bbo_side_type,
645 bbo_level,
646 native_tp_sl.as_ref(),
647 )
648 .await
649 .map_err(to_pyvalue_err)?;
650
651 Python::attach(|py| report.into_py_any(py))
652 })
653 }
654
655 #[pyo3(name = "modify_order")]
657 #[pyo3(signature = (
658 account_id,
659 product_type,
660 instrument_id,
661 client_order_id=None,
662 venue_order_id=None,
663 quantity=None,
664 price=None
665 ))]
666 #[expect(clippy::too_many_arguments)]
667 fn py_modify_order<'py>(
668 &self,
669 py: Python<'py>,
670 account_id: AccountId,
671 product_type: BybitProductType,
672 instrument_id: InstrumentId,
673 client_order_id: Option<ClientOrderId>,
674 venue_order_id: Option<VenueOrderId>,
675 quantity: Option<Quantity>,
676 price: Option<Price>,
677 ) -> PyResult<Bound<'py, PyAny>> {
678 let client = self.clone();
679
680 pyo3_async_runtimes::tokio::future_into_py(py, async move {
681 let report = client
682 .modify_order(
683 account_id,
684 product_type,
685 instrument_id,
686 client_order_id,
687 venue_order_id,
688 quantity,
689 price,
690 )
691 .await
692 .map_err(to_pyvalue_err)?;
693
694 Python::attach(|py| report.into_py_any(py))
695 })
696 }
697
698 #[pyo3(name = "cancel_order")]
700 #[pyo3(signature = (account_id, product_type, instrument_id, client_order_id=None, venue_order_id=None))]
701 fn py_cancel_order<'py>(
702 &self,
703 py: Python<'py>,
704 account_id: AccountId,
705 product_type: BybitProductType,
706 instrument_id: InstrumentId,
707 client_order_id: Option<ClientOrderId>,
708 venue_order_id: Option<VenueOrderId>,
709 ) -> PyResult<Bound<'py, PyAny>> {
710 let client = self.clone();
711
712 pyo3_async_runtimes::tokio::future_into_py(py, async move {
713 let report = client
714 .cancel_order(
715 account_id,
716 product_type,
717 instrument_id,
718 client_order_id,
719 venue_order_id,
720 )
721 .await
722 .map_err(to_pyvalue_err)?;
723
724 Python::attach(|py| report.into_py_any(py))
725 })
726 }
727
728 #[pyo3(name = "cancel_all_orders")]
730 fn py_cancel_all_orders<'py>(
731 &self,
732 py: Python<'py>,
733 account_id: AccountId,
734 product_type: BybitProductType,
735 instrument_id: InstrumentId,
736 ) -> PyResult<Bound<'py, PyAny>> {
737 let client = self.clone();
738
739 pyo3_async_runtimes::tokio::future_into_py(py, async move {
740 let reports = client
741 .cancel_all_orders(account_id, product_type, instrument_id)
742 .await
743 .map_err(to_pyvalue_err)?;
744
745 Python::attach(|py| {
746 let py_reports: PyResult<Vec<_>> = reports
747 .into_iter()
748 .map(|report| report.into_py_any(py))
749 .collect();
750 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
751 Ok(pylist)
752 })
753 })
754 }
755
756 #[pyo3(name = "query_order")]
758 #[pyo3(signature = (account_id, product_type, instrument_id, client_order_id=None, venue_order_id=None))]
759 fn py_query_order<'py>(
760 &self,
761 py: Python<'py>,
762 account_id: AccountId,
763 product_type: BybitProductType,
764 instrument_id: InstrumentId,
765 client_order_id: Option<ClientOrderId>,
766 venue_order_id: Option<VenueOrderId>,
767 ) -> PyResult<Bound<'py, PyAny>> {
768 let client = self.clone();
769
770 pyo3_async_runtimes::tokio::future_into_py(py, async move {
771 match client
772 .query_order(
773 account_id,
774 product_type,
775 instrument_id,
776 client_order_id,
777 venue_order_id,
778 )
779 .await
780 {
781 Ok(Some(report)) => Python::attach(|py| report.into_py_any(py)),
782 Ok(None) => Ok(Python::attach(|py| py.None())),
783 Err(e) => Err(to_pyvalue_err(e)),
784 }
785 })
786 }
787
788 #[pyo3(name = "request_trades")]
801 #[pyo3(signature = (product_type, instrument_id, limit=None))]
802 fn py_request_trades<'py>(
803 &self,
804 py: Python<'py>,
805 product_type: BybitProductType,
806 instrument_id: InstrumentId,
807 limit: Option<u32>,
808 ) -> PyResult<Bound<'py, PyAny>> {
809 let client = self.clone();
810
811 pyo3_async_runtimes::tokio::future_into_py(py, async move {
812 let trades = client
813 .request_trades(product_type, instrument_id, limit)
814 .await
815 .map_err(to_pyvalue_err)?;
816
817 Python::attach(|py| {
818 let py_trades: PyResult<Vec<_>> = trades
819 .into_iter()
820 .map(|trade| trade.into_py_any(py))
821 .collect();
822 let pylist = PyList::new(py, py_trades?).unwrap().into_any().unbind();
823 Ok(pylist)
824 })
825 })
826 }
827
828 #[pyo3(name = "request_funding_rates")]
834 #[pyo3(signature = (product_type, instrument_id, start=None, end=None, limit=None))]
835 fn py_request_funding_rates<'py>(
836 &self,
837 py: Python<'py>,
838 product_type: BybitProductType,
839 instrument_id: InstrumentId,
840 start: Option<DateTime<Utc>>,
841 end: Option<DateTime<Utc>>,
842 limit: Option<u32>,
843 ) -> PyResult<Bound<'py, PyAny>> {
844 let client = self.clone();
845
846 pyo3_async_runtimes::tokio::future_into_py(py, async move {
847 let funding_rates = client
848 .request_funding_rates(product_type, instrument_id, start, end, limit)
849 .await
850 .map_err(to_pyvalue_err)?;
851
852 Python::attach(|py| {
853 let py_funding_rates: PyResult<Vec<_>> = funding_rates
854 .into_iter()
855 .map(|funding_rate| funding_rate.into_py_any(py))
856 .collect();
857 let pylist = PyList::new(py, py_funding_rates?)
858 .unwrap()
859 .into_any()
860 .unbind();
861 Ok(pylist)
862 })
863 })
864 }
865
866 #[pyo3(name = "request_orderbook_snapshot")]
877 #[pyo3(signature = (product_type, instrument_id, limit=None))]
878 fn py_request_orderbook_snapshot<'py>(
879 &self,
880 py: Python<'py>,
881 product_type: BybitProductType,
882 instrument_id: InstrumentId,
883 limit: Option<u32>,
884 ) -> PyResult<Bound<'py, PyAny>> {
885 let client = self.clone();
886
887 pyo3_async_runtimes::tokio::future_into_py(py, async move {
888 let deltas = client
889 .request_orderbook_snapshot(product_type, instrument_id, limit)
890 .await
891 .map_err(to_pyvalue_err)?;
892
893 Python::attach(|py| Ok(deltas.into_py_any(py).unwrap()))
894 })
895 }
896
897 #[pyo3(name = "request_bars")]
903 #[pyo3(signature = (product_type, bar_type, start=None, end=None, limit=None, timestamp_on_close=true))]
904 #[expect(clippy::too_many_arguments)]
905 fn py_request_bars<'py>(
906 &self,
907 py: Python<'py>,
908 product_type: BybitProductType,
909 bar_type: BarType,
910 start: Option<DateTime<Utc>>,
911 end: Option<DateTime<Utc>>,
912 limit: Option<u32>,
913 timestamp_on_close: bool,
914 ) -> PyResult<Bound<'py, PyAny>> {
915 let client = self.clone();
916
917 pyo3_async_runtimes::tokio::future_into_py(py, async move {
918 let bars = client
919 .request_bars(
920 product_type,
921 bar_type,
922 start,
923 end,
924 limit,
925 timestamp_on_close,
926 )
927 .await
928 .map_err(to_pyvalue_err)?;
929
930 Python::attach(|py| {
931 let py_bars: PyResult<Vec<_>> =
932 bars.into_iter().map(|bar| bar.into_py_any(py)).collect();
933 let pylist = PyList::new(py, py_bars?).unwrap().into_any().unbind();
934 Ok(pylist)
935 })
936 })
937 }
938
939 #[pyo3(name = "request_fee_rates")]
945 #[pyo3(signature = (product_type, symbol=None, base_coin=None))]
946 fn py_request_fee_rates<'py>(
947 &self,
948 py: Python<'py>,
949 product_type: BybitProductType,
950 symbol: Option<String>,
951 base_coin: Option<String>,
952 ) -> PyResult<Bound<'py, PyAny>> {
953 let client = self.clone();
954
955 pyo3_async_runtimes::tokio::future_into_py(py, async move {
956 let fee_rates = client
957 .request_fee_rates(product_type, symbol, base_coin)
958 .await
959 .map_err(to_pyvalue_err)?;
960
961 Python::attach(|py| {
962 let py_fee_rates: PyResult<Vec<_>> = fee_rates
963 .into_iter()
964 .map(|rate| Py::new(py, rate))
965 .collect();
966 let pylist = PyList::new(py, py_fee_rates?).unwrap().into_any().unbind();
967 Ok(pylist)
968 })
969 })
970 }
971
972 #[pyo3(name = "request_account_state")]
978 fn py_request_account_state<'py>(
979 &self,
980 py: Python<'py>,
981 account_type: crate::common::enums::BybitAccountType,
982 account_id: AccountId,
983 ) -> PyResult<Bound<'py, PyAny>> {
984 let client = self.clone();
985
986 pyo3_async_runtimes::tokio::future_into_py(py, async move {
987 let account_state = client
988 .request_account_state(account_type, account_id)
989 .await
990 .map_err(to_pyvalue_err)?;
991
992 Python::attach(|py| account_state.into_py_any(py))
993 })
994 }
995
996 #[pyo3(name = "request_order_status_reports")]
1000 #[pyo3(signature = (account_id, product_type, instrument_id=None, open_only=false, start=None, end=None, limit=None))]
1001 #[expect(clippy::too_many_arguments)]
1002 fn py_request_order_status_reports<'py>(
1003 &self,
1004 py: Python<'py>,
1005 account_id: AccountId,
1006 product_type: BybitProductType,
1007 instrument_id: Option<InstrumentId>,
1008 open_only: bool,
1009 start: Option<DateTime<Utc>>,
1010 end: Option<DateTime<Utc>>,
1011 limit: Option<u32>,
1012 ) -> PyResult<Bound<'py, PyAny>> {
1013 let client = self.clone();
1014
1015 pyo3_async_runtimes::tokio::future_into_py(py, async move {
1016 let reports = client
1017 .request_order_status_reports(
1018 account_id,
1019 product_type,
1020 instrument_id,
1021 open_only,
1022 start,
1023 end,
1024 limit,
1025 )
1026 .await
1027 .map_err(to_pyvalue_err)?;
1028
1029 Python::attach(|py| {
1030 let py_reports: PyResult<Vec<_>> = reports
1031 .into_iter()
1032 .map(|report| report.into_py_any(py))
1033 .collect();
1034 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
1035 Ok(pylist)
1036 })
1037 })
1038 }
1039
1040 #[pyo3(name = "request_fill_reports")]
1048 #[pyo3(signature = (account_id, product_type, instrument_id=None, start=None, end=None, limit=None))]
1049 #[expect(clippy::too_many_arguments)]
1050 fn py_request_fill_reports<'py>(
1051 &self,
1052 py: Python<'py>,
1053 account_id: AccountId,
1054 product_type: BybitProductType,
1055 instrument_id: Option<InstrumentId>,
1056 start: Option<i64>,
1057 end: Option<i64>,
1058 limit: Option<u32>,
1059 ) -> PyResult<Bound<'py, PyAny>> {
1060 let client = self.clone();
1061
1062 pyo3_async_runtimes::tokio::future_into_py(py, async move {
1063 let reports = client
1064 .request_fill_reports(account_id, product_type, instrument_id, start, end, limit)
1065 .await
1066 .map_err(to_pyvalue_err)?;
1067
1068 Python::attach(|py| {
1069 let py_reports: PyResult<Vec<_>> = reports
1070 .into_iter()
1071 .map(|report| report.into_py_any(py))
1072 .collect();
1073 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
1074 Ok(pylist)
1075 })
1076 })
1077 }
1078
1079 #[pyo3(name = "request_position_status_reports")]
1087 #[pyo3(signature = (account_id, product_type, instrument_id=None))]
1088 fn py_request_position_status_reports<'py>(
1089 &self,
1090 py: Python<'py>,
1091 account_id: AccountId,
1092 product_type: BybitProductType,
1093 instrument_id: Option<InstrumentId>,
1094 ) -> PyResult<Bound<'py, PyAny>> {
1095 let client = self.clone();
1096
1097 pyo3_async_runtimes::tokio::future_into_py(py, async move {
1098 let reports = client
1099 .request_position_status_reports(account_id, product_type, instrument_id)
1100 .await
1101 .map_err(to_pyvalue_err)?;
1102
1103 Python::attach(|py| {
1104 let py_reports: PyResult<Vec<_>> = reports
1105 .into_iter()
1106 .map(|report| report.into_py_any(py))
1107 .collect();
1108 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
1109 Ok(pylist)
1110 })
1111 })
1112 }
1113
1114 #[pyo3(name = "request_forward_prices")]
1119 #[pyo3(signature = (base_coin, instrument_id=None))]
1120 fn py_request_forward_prices<'py>(
1121 &self,
1122 py: Python<'py>,
1123 base_coin: String,
1124 instrument_id: Option<InstrumentId>,
1125 ) -> PyResult<Bound<'py, PyAny>> {
1126 let client = self.clone();
1127
1128 pyo3_async_runtimes::tokio::future_into_py(py, async move {
1129 let forward_prices: Vec<ForwardPrice> = if let Some(inst_id) = instrument_id {
1130 let raw_symbol = extract_raw_symbol(inst_id.symbol.as_str()).to_string();
1132 let params = crate::http::query::BybitTickersParams {
1133 category: BybitProductType::Option,
1134 symbol: Some(raw_symbol),
1135 base_coin: None,
1136 exp_date: None,
1137 };
1138 let tickers = client
1139 .request_option_tickers_raw_with_params(¶ms)
1140 .await
1141 .map_err(to_pyvalue_err)?;
1142
1143 let ts = UnixNanos::default();
1144 tickers
1145 .into_iter()
1146 .filter_map(|t| {
1147 let up: rust_decimal::Decimal = t.underlying_price.parse().ok()?;
1148 if up.is_zero() {
1149 return None;
1150 }
1151 Some(ForwardPrice::new(inst_id, up, None, ts, ts))
1152 })
1153 .collect()
1154 } else {
1155 let tickers = client
1157 .request_option_tickers_raw(&base_coin)
1158 .await
1159 .map_err(to_pyvalue_err)?;
1160
1161 let ts = nautilus_core::UnixNanos::default();
1162 let mut seen_expiries = HashSet::new();
1163 tickers
1164 .into_iter()
1165 .filter_map(|t| {
1166 let up: rust_decimal::Decimal = t.underlying_price.parse().ok()?;
1167 if up.is_zero() {
1168 return None;
1169 }
1170 let parts: Vec<&str> = t.symbol.splitn(3, '-').collect();
1171 let expiry_key = if parts.len() >= 2 {
1172 format!("{}-{}", parts[0], parts[1])
1173 } else {
1174 t.symbol.to_string()
1175 };
1176
1177 if !seen_expiries.insert(expiry_key) {
1178 return None;
1179 }
1180 let symbol_str = format!("{}-OPTION", t.symbol);
1181 let inst_id = InstrumentId::new(
1182 Symbol::new(&symbol_str),
1183 *crate::common::consts::BYBIT_VENUE,
1184 );
1185 Some(ForwardPrice::new(inst_id, up, None, ts, ts))
1186 })
1187 .collect()
1188 };
1189
1190 Python::attach(|py| {
1191 let py_prices: PyResult<Vec<_>> = forward_prices
1192 .into_iter()
1193 .map(|fp| Py::new(py, fp))
1194 .collect();
1195 let pylist = PyList::new(py, py_prices?)?.into_any().unbind();
1196 Ok(pylist)
1197 })
1198 })
1199 }
1200}
1201
1202impl From<BybitHttpError> for PyErr {
1203 fn from(error: BybitHttpError) -> Self {
1204 match error {
1205 BybitHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
1207 BybitHttpError::NetworkError(msg) => to_pyruntime_err(format!("Network error: {msg}")),
1208 BybitHttpError::UnexpectedStatus { status, body } => {
1209 to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
1210 }
1211 BybitHttpError::MissingCredentials => {
1213 to_pyvalue_err("Missing credentials for authenticated request")
1214 }
1215 BybitHttpError::ValidationError(msg) => {
1216 to_pyvalue_err(format!("Parameter validation error: {msg}"))
1217 }
1218 BybitHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
1219 BybitHttpError::BuildError(e) => to_pyvalue_err(format!("Build error: {e}")),
1220 BybitHttpError::BybitError {
1221 error_code,
1222 message,
1223 } => to_pyvalue_err(format!("Bybit error {error_code}: {message}")),
1224 }
1225 }
1226}