1use std::collections::HashSet;
19
20use jiff::Timestamp;
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")]
157 #[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))]
158 #[expect(clippy::too_many_arguments)]
159 fn py_get_open_orders<'py>(
160 &self,
161 py: Python<'py>,
162 category: BybitProductType,
163 symbol: Option<String>,
164 base_coin: Option<String>,
165 settle_coin: Option<String>,
166 order_id: Option<String>,
167 order_link_id: Option<String>,
168 open_only: Option<BybitOpenOnly>,
169 order_filter: Option<BybitOrderFilter>,
170 limit: Option<u32>,
171 cursor: Option<String>,
172 ) -> PyResult<Bound<'py, PyAny>> {
173 let client = self.clone();
174
175 pyo3_async_runtimes::tokio::future_into_py(py, async move {
176 let response = client
177 .get_open_orders(
178 category,
179 symbol,
180 base_coin,
181 settle_coin,
182 order_id,
183 order_link_id,
184 open_only,
185 order_filter,
186 limit,
187 cursor,
188 )
189 .await
190 .map_err(to_pyvalue_err)?;
191
192 Python::attach(|py| {
193 let open_orders = BybitOrderCursorList::from(response.result);
194 let py_open_orders = Py::new(py, open_orders)?;
195 Ok(py_open_orders.into_any())
196 })
197 })
198 }
199}
200
201#[pymethods]
202#[pyo3_stub_gen::derive::gen_stub_pymethods]
203impl BybitHttpClient {
204 #[new]
210 #[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))]
211 #[expect(clippy::too_many_arguments)]
212 fn py_new(
213 api_key: Option<String>,
214 api_secret: Option<String>,
215 base_url: Option<String>,
216 demo: bool,
217 testnet: bool,
218 timeout_secs: u64,
219 max_retries: u32,
220 retry_delay_ms: u64,
221 retry_delay_max_ms: u64,
222 recv_window_ms: u64,
223 proxy_url: Option<String>,
224 ) -> PyResult<Self> {
225 Self::new_with_env(
226 api_key,
227 api_secret,
228 base_url,
229 demo,
230 testnet,
231 timeout_secs,
232 max_retries,
233 retry_delay_ms,
234 retry_delay_max_ms,
235 recv_window_ms,
236 proxy_url,
237 )
238 .map_err(to_pyvalue_err)
239 }
240
241 #[getter]
242 #[pyo3(name = "base_url")]
243 #[must_use]
244 pub fn py_base_url(&self) -> &str {
245 self.base_url()
246 }
247
248 #[getter]
249 #[pyo3(name = "api_key")]
250 #[must_use]
251 pub fn py_api_key(&self) -> Option<&str> {
252 self.credential().map(|c| c.api_key())
253 }
254
255 #[getter]
256 #[pyo3(name = "api_key_masked")]
257 #[must_use]
258 pub fn py_api_key_masked(&self) -> Option<String> {
259 self.credential().map(|c| c.api_key_masked())
260 }
261
262 #[pyo3(name = "cache_instrument")]
264 fn py_cache_instrument(&self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
265 let inst_any = pyobject_to_instrument_any(py, instrument)?;
266 self.cache_instrument(inst_any);
267 Ok(())
268 }
269
270 #[pyo3(name = "cancel_all_requests")]
271 fn py_cancel_all_requests(&self) {
272 self.cancel_all_requests();
273 }
274
275 #[pyo3(name = "set_use_spot_position_reports")]
276 fn py_set_use_spot_position_reports(&self, value: bool) {
277 self.set_use_spot_position_reports(value);
278 }
279
280 #[pyo3(name = "set_margin_mode")]
293 fn py_set_margin_mode<'py>(
294 &self,
295 py: Python<'py>,
296 margin_mode: BybitMarginMode,
297 ) -> PyResult<Bound<'py, PyAny>> {
298 let client = self.clone();
299
300 pyo3_async_runtimes::tokio::future_into_py(py, async move {
301 client
302 .set_margin_mode(margin_mode)
303 .await
304 .map_err(to_pyvalue_err)?;
305
306 Python::attach(|py| Ok(py.None()))
307 })
308 }
309
310 #[pyo3(name = "get_account_details")]
322 fn py_get_account_details<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
323 let client = self.clone();
324
325 pyo3_async_runtimes::tokio::future_into_py(py, async move {
326 let response = client.get_account_details().await.map_err(to_pyvalue_err)?;
327
328 Python::attach(|py| {
329 let account_details = Py::new(py, response.result)?;
330 Ok(account_details.into_any())
331 })
332 })
333 }
334
335 #[pyo3(name = "set_leverage")]
348 #[pyo3(signature = (product_type, symbol, buy_leverage, sell_leverage))]
349 fn py_set_leverage<'py>(
350 &self,
351 py: Python<'py>,
352 product_type: BybitProductType,
353 symbol: String,
354 buy_leverage: String,
355 sell_leverage: String,
356 ) -> PyResult<Bound<'py, PyAny>> {
357 let client = self.clone();
358
359 pyo3_async_runtimes::tokio::future_into_py(py, async move {
360 client
361 .set_leverage(product_type, &symbol, &buy_leverage, &sell_leverage)
362 .await
363 .map_err(to_pyvalue_err)?;
364
365 Python::attach(|py| Ok(py.None()))
366 })
367 }
368
369 #[pyo3(name = "switch_mode")]
382 #[pyo3(signature = (product_type, mode, symbol=None, coin=None))]
383 fn py_switch_mode<'py>(
384 &self,
385 py: Python<'py>,
386 product_type: BybitProductType,
387 mode: BybitPositionMode,
388 symbol: Option<String>,
389 coin: Option<String>,
390 ) -> PyResult<Bound<'py, PyAny>> {
391 let client = self.clone();
392
393 pyo3_async_runtimes::tokio::future_into_py(py, async move {
394 client
395 .switch_mode(product_type, mode, symbol, coin)
396 .await
397 .map_err(to_pyvalue_err)?;
398
399 Python::attach(|py| Ok(py.None()))
400 })
401 }
402
403 #[pyo3(name = "get_spot_borrow_amount")]
418 fn py_get_spot_borrow_amount<'py>(
419 &self,
420 py: Python<'py>,
421 coin: String,
422 ) -> PyResult<Bound<'py, PyAny>> {
423 let client = self.clone();
424
425 pyo3_async_runtimes::tokio::future_into_py(py, async move {
426 let borrow_amount = client
427 .get_spot_borrow_amount(&coin)
428 .await
429 .map_err(to_pyvalue_err)?;
430
431 Ok(borrow_amount)
432 })
433 }
434
435 #[pyo3(name = "borrow_spot")]
451 #[pyo3(signature = (coin, amount))]
452 fn py_borrow_spot<'py>(
453 &self,
454 py: Python<'py>,
455 coin: String,
456 amount: Quantity,
457 ) -> PyResult<Bound<'py, PyAny>> {
458 let client = self.clone();
459
460 pyo3_async_runtimes::tokio::future_into_py(py, async move {
461 client
462 .borrow_spot(&coin, amount)
463 .await
464 .map_err(to_pyvalue_err)?;
465
466 Python::attach(|py| Ok(py.None()))
467 })
468 }
469
470 #[pyo3(name = "repay_spot_borrow")]
487 #[pyo3(signature = (coin, amount=None))]
488 fn py_repay_spot_borrow<'py>(
489 &self,
490 py: Python<'py>,
491 coin: String,
492 amount: Option<Quantity>,
493 ) -> PyResult<Bound<'py, PyAny>> {
494 let client = self.clone();
495
496 pyo3_async_runtimes::tokio::future_into_py(py, async move {
497 client
498 .repay_spot_borrow(&coin, amount)
499 .await
500 .map_err(to_pyvalue_err)?;
501
502 Python::attach(|py| Ok(py.None()))
503 })
504 }
505
506 #[pyo3(name = "repay_spot_borrow_with_conversion")]
524 #[pyo3(signature = (coin, amount=None))]
525 fn py_repay_spot_borrow_with_conversion<'py>(
526 &self,
527 py: Python<'py>,
528 coin: String,
529 amount: Option<Quantity>,
530 ) -> PyResult<Bound<'py, PyAny>> {
531 let client = self.clone();
532
533 pyo3_async_runtimes::tokio::future_into_py(py, async move {
534 client
535 .repay_spot_borrow_with_conversion(&coin, amount)
536 .await
537 .map_err(to_pyvalue_err)?;
538
539 Python::attach(|py| Ok(py.None()))
540 })
541 }
542
543 #[pyo3(name = "request_instruments")]
553 #[pyo3(signature = (product_type, symbol=None, base_coin=None))]
554 fn py_request_instruments<'py>(
555 &self,
556 py: Python<'py>,
557 product_type: BybitProductType,
558 symbol: Option<String>,
559 base_coin: Option<String>,
560 ) -> PyResult<Bound<'py, PyAny>> {
561 let client = self.clone();
562 let base_coin = base_coin.map(|s| Ustr::from(&s));
563
564 pyo3_async_runtimes::tokio::future_into_py(py, async move {
565 let instruments = client
566 .request_instruments(product_type, symbol, base_coin)
567 .await
568 .map_err(to_pyvalue_err)?;
569
570 Python::attach(|py| {
571 let py_instruments: PyResult<Vec<_>> = instruments
572 .into_iter()
573 .map(|inst| instrument_any_to_pyobject(py, inst))
574 .collect();
575 let pylist = PyList::new(py, py_instruments?)?.into_any().unbind();
576 Ok(pylist)
577 })
578 })
579 }
580
581 #[pyo3(name = "request_instrument_statuses")]
591 fn py_request_instrument_statuses<'py>(
592 &self,
593 py: Python<'py>,
594 product_type: BybitProductType,
595 ) -> PyResult<Bound<'py, PyAny>> {
596 let client = self.clone();
597
598 pyo3_async_runtimes::tokio::future_into_py(py, async move {
599 let statuses = client
600 .request_instrument_statuses(product_type)
601 .await
602 .map_err(to_pyvalue_err)?;
603
604 Python::attach(|py| {
605 let dict = PyDict::new(py);
606 for (instrument_id, action) in statuses {
607 dict.set_item(
608 instrument_id.into_bound_py_any(py)?,
609 action.into_bound_py_any(py)?,
610 )?;
611 }
612 Ok(dict.into_any().unbind())
613 })
614 })
615 }
616
617 #[pyo3(name = "request_tickers")]
630 fn py_request_tickers<'py>(
631 &self,
632 py: Python<'py>,
633 params: crate::python::params::BybitTickersParams,
634 ) -> PyResult<Bound<'py, PyAny>> {
635 let client = self.clone();
636
637 pyo3_async_runtimes::tokio::future_into_py(py, async move {
638 let tickers = client
639 .request_tickers(¶ms.into())
640 .await
641 .map_err(to_pyvalue_err)?;
642
643 Python::attach(|py| {
644 let py_tickers: PyResult<Vec<_>> = tickers
645 .into_iter()
646 .map(|ticker| Py::new(py, ticker))
647 .collect();
648 let pylist = PyList::new(py, py_tickers?)?.into_any().unbind();
649 Ok(pylist)
650 })
651 })
652 }
653
654 #[pyo3(name = "submit_order")]
665 #[pyo3(signature = (
666 account_id,
667 product_type,
668 instrument_id,
669 client_order_id,
670 order_side,
671 order_type,
672 quantity,
673 time_in_force = None,
674 price = None,
675 trigger_price = None,
676 post_only = None,
677 reduce_only = false,
678 is_quote_quantity = false,
679 is_leverage = false,
680 position_idx = None,
681 bbo_side_type = None,
682 bbo_level = None,
683 native_tp_sl = None,
684 ))]
685 #[expect(clippy::too_many_arguments)]
686 fn py_submit_order<'py>(
687 &self,
688 py: Python<'py>,
689 account_id: AccountId,
690 product_type: BybitProductType,
691 instrument_id: InstrumentId,
692 client_order_id: ClientOrderId,
693 order_side: OrderSide,
694 order_type: OrderType,
695 quantity: Quantity,
696 time_in_force: Option<TimeInForce>,
697 price: Option<Price>,
698 trigger_price: Option<Price>,
699 post_only: Option<bool>,
700 reduce_only: bool,
701 is_quote_quantity: bool,
702 is_leverage: bool,
703 position_idx: Option<BybitPositionIdx>,
704 bbo_side_type: Option<String>,
705 bbo_level: Option<String>,
706 native_tp_sl: Option<BybitNativeTpSlParams>,
707 ) -> PyResult<Bound<'py, PyAny>> {
708 let client = self.clone();
709 let bbo_side_type = bbo_side_type
710 .map(|value| parse_bbo_side_type(&value))
711 .transpose()
712 .map_err(to_pyvalue_err)?;
713 let bbo_level = bbo_level
714 .map(parse_bbo_level)
715 .transpose()
716 .map_err(to_pyvalue_err)?;
717 if bbo_side_type.is_some() != bbo_level.is_some() {
718 return Err(to_pyvalue_err(anyhow::anyhow!(
719 "'bbo_side_type' and 'bbo_level' must be provided together"
720 )));
721 }
722
723 let native_tp_sl: Option<RustNativeTpSlParams> = native_tp_sl
724 .map(RustNativeTpSlParams::try_from)
725 .transpose()
726 .map_err(to_pyvalue_err)?;
727
728 pyo3_async_runtimes::tokio::future_into_py(py, async move {
729 let report = client
730 .submit_order(
731 account_id,
732 product_type,
733 instrument_id,
734 client_order_id,
735 order_side,
736 order_type,
737 quantity,
738 time_in_force,
739 price,
740 trigger_price,
741 post_only,
742 reduce_only,
743 is_quote_quantity,
744 is_leverage,
745 position_idx,
746 bbo_side_type,
747 bbo_level,
748 native_tp_sl.as_ref(),
749 )
750 .await
751 .map_err(to_pyvalue_err)?;
752
753 Python::attach(|py| report.into_py_any(py))
754 })
755 }
756
757 #[pyo3(name = "modify_order")]
768 #[pyo3(signature = (
769 account_id,
770 product_type,
771 instrument_id,
772 client_order_id=None,
773 venue_order_id=None,
774 quantity=None,
775 price=None
776 ))]
777 #[expect(clippy::too_many_arguments)]
778 fn py_modify_order<'py>(
779 &self,
780 py: Python<'py>,
781 account_id: AccountId,
782 product_type: BybitProductType,
783 instrument_id: InstrumentId,
784 client_order_id: Option<ClientOrderId>,
785 venue_order_id: Option<VenueOrderId>,
786 quantity: Option<Quantity>,
787 price: Option<Price>,
788 ) -> PyResult<Bound<'py, PyAny>> {
789 let client = self.clone();
790
791 pyo3_async_runtimes::tokio::future_into_py(py, async move {
792 let report = client
793 .modify_order(
794 account_id,
795 product_type,
796 instrument_id,
797 client_order_id,
798 venue_order_id,
799 quantity,
800 price,
801 )
802 .await
803 .map_err(to_pyvalue_err)?;
804
805 Python::attach(|py| report.into_py_any(py))
806 })
807 }
808
809 #[pyo3(name = "cancel_order")]
819 #[pyo3(signature = (account_id, product_type, instrument_id, client_order_id=None, venue_order_id=None))]
820 fn py_cancel_order<'py>(
821 &self,
822 py: Python<'py>,
823 account_id: AccountId,
824 product_type: BybitProductType,
825 instrument_id: InstrumentId,
826 client_order_id: Option<ClientOrderId>,
827 venue_order_id: Option<VenueOrderId>,
828 ) -> PyResult<Bound<'py, PyAny>> {
829 let client = self.clone();
830
831 pyo3_async_runtimes::tokio::future_into_py(py, async move {
832 let report = client
833 .cancel_order(
834 account_id,
835 product_type,
836 instrument_id,
837 client_order_id,
838 venue_order_id,
839 )
840 .await
841 .map_err(to_pyvalue_err)?;
842
843 Python::attach(|py| report.into_py_any(py))
844 })
845 }
846
847 #[pyo3(name = "cancel_all_orders")]
856 fn py_cancel_all_orders<'py>(
857 &self,
858 py: Python<'py>,
859 account_id: AccountId,
860 product_type: BybitProductType,
861 instrument_id: InstrumentId,
862 ) -> PyResult<Bound<'py, PyAny>> {
863 let client = self.clone();
864
865 pyo3_async_runtimes::tokio::future_into_py(py, async move {
866 let reports = client
867 .cancel_all_orders(account_id, product_type, instrument_id)
868 .await
869 .map_err(to_pyvalue_err)?;
870
871 Python::attach(|py| {
872 let py_reports: PyResult<Vec<_>> = reports
873 .into_iter()
874 .map(|report| report.into_py_any(py))
875 .collect();
876 let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
877 Ok(pylist)
878 })
879 })
880 }
881
882 #[pyo3(name = "query_order")]
891 #[pyo3(signature = (account_id, product_type, instrument_id, client_order_id=None, venue_order_id=None))]
892 fn py_query_order<'py>(
893 &self,
894 py: Python<'py>,
895 account_id: AccountId,
896 product_type: BybitProductType,
897 instrument_id: InstrumentId,
898 client_order_id: Option<ClientOrderId>,
899 venue_order_id: Option<VenueOrderId>,
900 ) -> PyResult<Bound<'py, PyAny>> {
901 let client = self.clone();
902
903 pyo3_async_runtimes::tokio::future_into_py(py, async move {
904 match client
905 .query_order(
906 account_id,
907 product_type,
908 instrument_id,
909 client_order_id,
910 venue_order_id,
911 )
912 .await
913 {
914 Ok(Some(report)) => Python::attach(|py| report.into_py_any(py)),
915 Ok(None) => Ok(Python::attach(|py| py.None())),
916 Err(e) => Err(to_pyvalue_err(e)),
917 }
918 })
919 }
920
921 #[pyo3(name = "request_trades")]
941 #[pyo3(signature = (product_type, instrument_id, limit=None))]
942 fn py_request_trades<'py>(
943 &self,
944 py: Python<'py>,
945 product_type: BybitProductType,
946 instrument_id: InstrumentId,
947 limit: Option<u32>,
948 ) -> PyResult<Bound<'py, PyAny>> {
949 let client = self.clone();
950
951 pyo3_async_runtimes::tokio::future_into_py(py, async move {
952 let trades = client
953 .request_trades(product_type, instrument_id, limit)
954 .await
955 .map_err(to_pyvalue_err)?;
956
957 Python::attach(|py| {
958 let py_trades: PyResult<Vec<_>> = trades
959 .into_iter()
960 .map(|trade| trade.into_py_any(py))
961 .collect();
962 let pylist = PyList::new(py, py_trades?)?.into_any().unbind();
963 Ok(pylist)
964 })
965 })
966 }
967
968 #[pyo3(name = "request_funding_rates")]
981 #[pyo3(signature = (product_type, instrument_id, start=None, end=None, limit=None))]
982 fn py_request_funding_rates<'py>(
983 &self,
984 py: Python<'py>,
985 product_type: BybitProductType,
986 instrument_id: InstrumentId,
987 start: Option<Timestamp>,
988 end: Option<Timestamp>,
989 limit: Option<u32>,
990 ) -> PyResult<Bound<'py, PyAny>> {
991 let client = self.clone();
992
993 pyo3_async_runtimes::tokio::future_into_py(py, async move {
994 let funding_rates = client
995 .request_funding_rates(product_type, instrument_id, start, end, limit)
996 .await
997 .map_err(to_pyvalue_err)?;
998
999 Python::attach(|py| {
1000 let py_funding_rates: PyResult<Vec<_>> = funding_rates
1001 .into_iter()
1002 .map(|funding_rate| funding_rate.into_py_any(py))
1003 .collect();
1004 let pylist = PyList::new(py, py_funding_rates?)?.into_any().unbind();
1005 Ok(pylist)
1006 })
1007 })
1008 }
1009
1010 #[pyo3(name = "request_orderbook_snapshot")]
1028 #[pyo3(signature = (product_type, instrument_id, limit=None))]
1029 fn py_request_orderbook_snapshot<'py>(
1030 &self,
1031 py: Python<'py>,
1032 product_type: BybitProductType,
1033 instrument_id: InstrumentId,
1034 limit: Option<u32>,
1035 ) -> PyResult<Bound<'py, PyAny>> {
1036 let client = self.clone();
1037
1038 pyo3_async_runtimes::tokio::future_into_py(py, async move {
1039 let deltas = client
1040 .request_orderbook_snapshot(product_type, instrument_id, limit)
1041 .await
1042 .map_err(to_pyvalue_err)?;
1043
1044 Python::attach(|py| deltas.into_py_any(py))
1045 })
1046 }
1047
1048 #[pyo3(name = "request_bars")]
1061 #[pyo3(signature = (product_type, bar_type, start=None, end=None, limit=None, timestamp_on_close=true))]
1062 #[expect(clippy::too_many_arguments)]
1063 fn py_request_bars<'py>(
1064 &self,
1065 py: Python<'py>,
1066 product_type: BybitProductType,
1067 bar_type: BarType,
1068 start: Option<Timestamp>,
1069 end: Option<Timestamp>,
1070 limit: Option<u32>,
1071 timestamp_on_close: bool,
1072 ) -> PyResult<Bound<'py, PyAny>> {
1073 let client = self.clone();
1074
1075 pyo3_async_runtimes::tokio::future_into_py(py, async move {
1076 let bars = client
1077 .request_bars(
1078 product_type,
1079 bar_type,
1080 start,
1081 end,
1082 limit,
1083 timestamp_on_close,
1084 )
1085 .await
1086 .map_err(to_pyvalue_err)?;
1087
1088 Python::attach(|py| {
1089 let py_bars: PyResult<Vec<_>> =
1090 bars.into_iter().map(|bar| bar.into_py_any(py)).collect();
1091 let pylist = PyList::new(py, py_bars?)?.into_any().unbind();
1092 Ok(pylist)
1093 })
1094 })
1095 }
1096
1097 #[pyo3(name = "request_fee_rates")]
1109 #[pyo3(signature = (product_type, symbol=None, base_coin=None))]
1110 fn py_request_fee_rates<'py>(
1111 &self,
1112 py: Python<'py>,
1113 product_type: BybitProductType,
1114 symbol: Option<String>,
1115 base_coin: Option<String>,
1116 ) -> PyResult<Bound<'py, PyAny>> {
1117 let client = self.clone();
1118
1119 pyo3_async_runtimes::tokio::future_into_py(py, async move {
1120 let fee_rates = client
1121 .request_fee_rates(product_type, symbol, base_coin)
1122 .await
1123 .map_err(to_pyvalue_err)?;
1124
1125 Python::attach(|py| {
1126 let py_fee_rates: PyResult<Vec<_>> = fee_rates
1127 .into_iter()
1128 .map(|rate| Py::new(py, rate))
1129 .collect();
1130 let pylist = PyList::new(py, py_fee_rates?)?.into_any().unbind();
1131 Ok(pylist)
1132 })
1133 })
1134 }
1135
1136 #[pyo3(name = "request_account_state")]
1148 fn py_request_account_state<'py>(
1149 &self,
1150 py: Python<'py>,
1151 account_type: crate::common::enums::BybitAccountType,
1152 account_id: AccountId,
1153 ) -> PyResult<Bound<'py, PyAny>> {
1154 let client = self.clone();
1155
1156 pyo3_async_runtimes::tokio::future_into_py(py, async move {
1157 let account_state = client
1158 .request_account_state(account_type, account_id)
1159 .await
1160 .map_err(to_pyvalue_err)?;
1161
1162 Python::attach(|py| account_state.into_py_any(py))
1163 })
1164 }
1165
1166 #[pyo3(name = "request_order_status_reports")]
1182 #[pyo3(signature = (account_id, product_type, instrument_id=None, open_only=false, start=None, end=None, limit=None))]
1183 #[expect(clippy::too_many_arguments)]
1184 fn py_request_order_status_reports<'py>(
1185 &self,
1186 py: Python<'py>,
1187 account_id: AccountId,
1188 product_type: BybitProductType,
1189 instrument_id: Option<InstrumentId>,
1190 open_only: bool,
1191 start: Option<Timestamp>,
1192 end: Option<Timestamp>,
1193 limit: Option<u32>,
1194 ) -> PyResult<Bound<'py, PyAny>> {
1195 let client = self.clone();
1196
1197 pyo3_async_runtimes::tokio::future_into_py(py, async move {
1198 let reports = client
1199 .request_order_status_reports(
1200 account_id,
1201 product_type,
1202 instrument_id,
1203 open_only,
1204 start,
1205 end,
1206 limit,
1207 )
1208 .await
1209 .map_err(to_pyvalue_err)?;
1210
1211 Python::attach(|py| {
1212 let py_reports: PyResult<Vec<_>> = reports
1213 .into_iter()
1214 .map(|report| report.into_py_any(py))
1215 .collect();
1216 let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
1217 Ok(pylist)
1218 })
1219 })
1220 }
1221
1222 #[pyo3(name = "request_fill_reports")]
1234 #[pyo3(signature = (account_id, product_type, instrument_id=None, start=None, end=None, limit=None))]
1235 #[expect(clippy::too_many_arguments)]
1236 fn py_request_fill_reports<'py>(
1237 &self,
1238 py: Python<'py>,
1239 account_id: AccountId,
1240 product_type: BybitProductType,
1241 instrument_id: Option<InstrumentId>,
1242 start: Option<i64>,
1243 end: Option<i64>,
1244 limit: Option<u32>,
1245 ) -> PyResult<Bound<'py, PyAny>> {
1246 let client = self.clone();
1247
1248 pyo3_async_runtimes::tokio::future_into_py(py, async move {
1249 let reports = client
1250 .request_fill_reports(account_id, product_type, instrument_id, start, end, limit)
1251 .await
1252 .map_err(to_pyvalue_err)?;
1253
1254 Python::attach(|py| {
1255 let py_reports: PyResult<Vec<_>> = reports
1256 .into_iter()
1257 .map(|report| report.into_py_any(py))
1258 .collect();
1259 let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
1260 Ok(pylist)
1261 })
1262 })
1263 }
1264
1265 #[pyo3(name = "request_position_status_reports")]
1278 #[pyo3(signature = (account_id, product_type, instrument_id=None))]
1279 fn py_request_position_status_reports<'py>(
1280 &self,
1281 py: Python<'py>,
1282 account_id: AccountId,
1283 product_type: BybitProductType,
1284 instrument_id: Option<InstrumentId>,
1285 ) -> PyResult<Bound<'py, PyAny>> {
1286 let client = self.clone();
1287
1288 pyo3_async_runtimes::tokio::future_into_py(py, async move {
1289 let reports = client
1290 .request_position_status_reports(account_id, product_type, instrument_id)
1291 .await
1292 .map_err(to_pyvalue_err)?;
1293
1294 Python::attach(|py| {
1295 let py_reports: PyResult<Vec<_>> = reports
1296 .into_iter()
1297 .map(|report| report.into_py_any(py))
1298 .collect();
1299 let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
1300 Ok(pylist)
1301 })
1302 })
1303 }
1304
1305 #[pyo3(name = "request_forward_prices")]
1310 #[pyo3(signature = (base_coin, instrument_id=None))]
1311 fn py_request_forward_prices<'py>(
1312 &self,
1313 py: Python<'py>,
1314 base_coin: String,
1315 instrument_id: Option<InstrumentId>,
1316 ) -> PyResult<Bound<'py, PyAny>> {
1317 let client = self.clone();
1318
1319 pyo3_async_runtimes::tokio::future_into_py(py, async move {
1320 let forward_prices: Vec<ForwardPrice> = if let Some(inst_id) = instrument_id {
1321 let raw_symbol = extract_raw_symbol(inst_id.symbol.as_str()).to_string();
1323 let params = crate::http::query::BybitTickersParams {
1324 category: BybitProductType::Option,
1325 symbol: Some(raw_symbol),
1326 base_coin: None,
1327 exp_date: None,
1328 };
1329 let tickers = client
1330 .request_option_tickers_raw_with_params(¶ms)
1331 .await
1332 .map_err(to_pyvalue_err)?;
1333
1334 let ts = UnixNanos::default();
1335 tickers
1336 .into_iter()
1337 .filter_map(|t| {
1338 let up: rust_decimal::Decimal = t.underlying_price.parse().ok()?;
1339 if up.is_zero() {
1340 return None;
1341 }
1342 Some(ForwardPrice::new(inst_id, up, None, ts, ts))
1343 })
1344 .collect()
1345 } else {
1346 let tickers = client
1348 .request_option_tickers_raw(&base_coin)
1349 .await
1350 .map_err(to_pyvalue_err)?;
1351
1352 let ts = nautilus_core::UnixNanos::default();
1353 let mut seen_expiries = HashSet::new();
1354 tickers
1355 .into_iter()
1356 .filter_map(|t| {
1357 let up: rust_decimal::Decimal = t.underlying_price.parse().ok()?;
1358 if up.is_zero() {
1359 return None;
1360 }
1361 let parts: Vec<&str> = t.symbol.splitn(3, '-').collect();
1362 let expiry_key = if parts.len() >= 2 {
1363 format!("{}-{}", parts[0], parts[1])
1364 } else {
1365 t.symbol.to_string()
1366 };
1367
1368 if !seen_expiries.insert(expiry_key) {
1369 return None;
1370 }
1371 let symbol_str = format!("{}-OPTION", t.symbol);
1372 let inst_id = InstrumentId::new(
1373 Symbol::new(&symbol_str),
1374 *crate::common::consts::BYBIT_VENUE,
1375 );
1376 Some(ForwardPrice::new(inst_id, up, None, ts, ts))
1377 })
1378 .collect()
1379 };
1380
1381 Python::attach(|py| {
1382 let py_prices: PyResult<Vec<_>> = forward_prices
1383 .into_iter()
1384 .map(|fp| Py::new(py, fp))
1385 .collect();
1386 let pylist = PyList::new(py, py_prices?)?.into_any().unbind();
1387 Ok(pylist)
1388 })
1389 })
1390 }
1391}
1392
1393impl From<BybitHttpError> for PyErr {
1394 fn from(error: BybitHttpError) -> Self {
1395 match error {
1396 BybitHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
1398 BybitHttpError::NetworkError(msg) => to_pyruntime_err(format!("Network error: {msg}")),
1399 BybitHttpError::UnexpectedStatus { status, body } => {
1400 to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
1401 }
1402 BybitHttpError::MissingCredentials => {
1404 to_pyvalue_err("Missing credentials for authenticated request")
1405 }
1406 BybitHttpError::ValidationError(msg) => {
1407 to_pyvalue_err(format!("Parameter validation error: {msg}"))
1408 }
1409 BybitHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
1410 BybitHttpError::BuildError(e) => to_pyvalue_err(format!("Build error: {e}")),
1411 BybitHttpError::BybitError {
1412 error_code,
1413 message,
1414 } => to_pyvalue_err(format!("Bybit error {error_code}: {message}")),
1415 }
1416 }
1417}