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