nautilus_bybit/python/
http.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Python bindings for the Bybit HTTP client.
17
18use chrono::{DateTime, Utc};
19use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err};
20use nautilus_model::{
21    data::BarType,
22    enums::{OrderSide, OrderType, TimeInForce},
23    identifiers::{AccountId, ClientOrderId, InstrumentId, VenueOrderId},
24    python::instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
25    types::{Price, Quantity},
26};
27use pyo3::{conversion::IntoPyObjectExt, prelude::*, types::PyList};
28
29use crate::{
30    common::enums::{
31        BybitMarginMode, BybitOpenOnly, BybitOrderFilter, BybitPositionMode, BybitProductType,
32    },
33    http::{
34        client::{BybitHttpClient, BybitRawHttpClient},
35        error::BybitHttpError,
36        models::BybitOrderCursorList,
37    },
38};
39
40#[pymethods]
41impl BybitRawHttpClient {
42    #[new]
43    #[pyo3(signature = (api_key=None, api_secret=None, base_url=None, demo=false, testnet=false, timeout_secs=None, max_retries=None, retry_delay_ms=None, retry_delay_max_ms=None, recv_window_ms=None, proxy_url=None))]
44    #[allow(clippy::too_many_arguments)]
45    fn py_new(
46        api_key: Option<String>,
47        api_secret: Option<String>,
48        base_url: Option<String>,
49        demo: bool,
50        testnet: bool,
51        timeout_secs: Option<u64>,
52        max_retries: Option<u32>,
53        retry_delay_ms: Option<u64>,
54        retry_delay_max_ms: Option<u64>,
55        recv_window_ms: Option<u64>,
56        proxy_url: Option<String>,
57    ) -> PyResult<Self> {
58        Self::new_with_env(
59            api_key,
60            api_secret,
61            base_url,
62            demo,
63            testnet,
64            timeout_secs.or(Some(60)),
65            max_retries,
66            retry_delay_ms,
67            retry_delay_max_ms,
68            recv_window_ms,
69            proxy_url,
70        )
71        .map_err(to_pyvalue_err)
72    }
73
74    #[getter]
75    #[pyo3(name = "base_url")]
76    #[must_use]
77    pub fn py_base_url(&self) -> &str {
78        self.base_url()
79    }
80
81    #[getter]
82    #[pyo3(name = "api_key")]
83    #[must_use]
84    pub fn py_api_key(&self) -> Option<String> {
85        self.credential().map(|c| c.api_key().to_string())
86    }
87
88    #[getter]
89    #[pyo3(name = "recv_window_ms")]
90    #[must_use]
91    pub fn py_recv_window_ms(&self) -> u64 {
92        self.recv_window_ms()
93    }
94
95    #[pyo3(name = "cancel_all_requests")]
96    fn py_cancel_all_requests(&self) {
97        self.cancel_all_requests();
98    }
99
100    #[pyo3(name = "get_server_time")]
101    fn py_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
102        let client = self.clone();
103
104        pyo3_async_runtimes::tokio::future_into_py(py, async move {
105            let response = client.get_server_time().await.map_err(to_pyvalue_err)?;
106
107            Python::attach(|py| {
108                let server_time = Py::new(py, response.result)?;
109                Ok(server_time.into_any())
110            })
111        })
112    }
113
114    #[pyo3(name = "get_open_orders")]
115    #[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))]
116    #[allow(clippy::too_many_arguments)]
117    fn py_get_open_orders<'py>(
118        &self,
119        py: Python<'py>,
120        category: BybitProductType,
121        symbol: Option<String>,
122        base_coin: Option<String>,
123        settle_coin: Option<String>,
124        order_id: Option<String>,
125        order_link_id: Option<String>,
126        open_only: Option<BybitOpenOnly>,
127        order_filter: Option<BybitOrderFilter>,
128        limit: Option<u32>,
129        cursor: Option<String>,
130    ) -> PyResult<Bound<'py, PyAny>> {
131        let client = self.clone();
132
133        pyo3_async_runtimes::tokio::future_into_py(py, async move {
134            let response = client
135                .get_open_orders(
136                    category,
137                    symbol,
138                    base_coin,
139                    settle_coin,
140                    order_id,
141                    order_link_id,
142                    open_only,
143                    order_filter,
144                    limit,
145                    cursor,
146                )
147                .await
148                .map_err(to_pyvalue_err)?;
149
150            Python::attach(|py| {
151                let open_orders = BybitOrderCursorList::from(response.result);
152                let py_open_orders = Py::new(py, open_orders)?;
153                Ok(py_open_orders.into_any())
154            })
155        })
156    }
157}
158
159#[pymethods]
160impl BybitHttpClient {
161    #[new]
162    #[pyo3(signature = (api_key=None, api_secret=None, base_url=None, demo=false, testnet=false, timeout_secs=None, max_retries=None, retry_delay_ms=None, retry_delay_max_ms=None, recv_window_ms=None, proxy_url=None))]
163    #[allow(clippy::too_many_arguments)]
164    fn py_new(
165        api_key: Option<String>,
166        api_secret: Option<String>,
167        base_url: Option<String>,
168        demo: bool,
169        testnet: bool,
170        timeout_secs: Option<u64>,
171        max_retries: Option<u32>,
172        retry_delay_ms: Option<u64>,
173        retry_delay_max_ms: Option<u64>,
174        recv_window_ms: Option<u64>,
175        proxy_url: Option<String>,
176    ) -> PyResult<Self> {
177        Self::new_with_env(
178            api_key,
179            api_secret,
180            base_url,
181            demo,
182            testnet,
183            timeout_secs.or(Some(60)),
184            max_retries,
185            retry_delay_ms,
186            retry_delay_max_ms,
187            recv_window_ms,
188            proxy_url,
189        )
190        .map_err(to_pyvalue_err)
191    }
192
193    #[getter]
194    #[pyo3(name = "base_url")]
195    #[must_use]
196    pub fn py_base_url(&self) -> &str {
197        self.base_url()
198    }
199
200    #[getter]
201    #[pyo3(name = "api_key")]
202    #[must_use]
203    pub fn py_api_key(&self) -> Option<&str> {
204        self.credential().map(|c| c.api_key()).map(|u| u.as_str())
205    }
206
207    #[getter]
208    #[pyo3(name = "api_key_masked")]
209    #[must_use]
210    pub fn py_api_key_masked(&self) -> Option<String> {
211        self.credential().map(|c| c.api_key_masked())
212    }
213
214    #[pyo3(name = "cache_instrument")]
215    fn py_cache_instrument(&self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
216        let inst_any = pyobject_to_instrument_any(py, instrument)?;
217        self.cache_instrument(inst_any);
218        Ok(())
219    }
220
221    #[pyo3(name = "cancel_all_requests")]
222    fn py_cancel_all_requests(&self) {
223        self.cancel_all_requests();
224    }
225
226    #[pyo3(name = "set_use_spot_position_reports")]
227    fn py_set_use_spot_position_reports(&self, value: bool) {
228        self.set_use_spot_position_reports(value);
229    }
230
231    #[pyo3(name = "set_margin_mode")]
232    fn py_set_margin_mode<'py>(
233        &self,
234        py: Python<'py>,
235        margin_mode: BybitMarginMode,
236    ) -> PyResult<Bound<'py, PyAny>> {
237        let client = self.clone();
238
239        pyo3_async_runtimes::tokio::future_into_py(py, async move {
240            client
241                .set_margin_mode(margin_mode)
242                .await
243                .map_err(to_pyvalue_err)?;
244
245            Python::attach(|py| Ok(py.None()))
246        })
247    }
248
249    #[pyo3(name = "get_account_details")]
250    fn py_get_account_details<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
251        let client = self.clone();
252
253        pyo3_async_runtimes::tokio::future_into_py(py, async move {
254            let response = client.get_account_details().await.map_err(to_pyvalue_err)?;
255
256            Python::attach(|py| {
257                let account_details = Py::new(py, response.result)?;
258                Ok(account_details.into_any())
259            })
260        })
261    }
262
263    #[pyo3(name = "set_leverage")]
264    #[pyo3(signature = (product_type, symbol, buy_leverage, sell_leverage))]
265    fn py_set_leverage<'py>(
266        &self,
267        py: Python<'py>,
268        product_type: BybitProductType,
269        symbol: String,
270        buy_leverage: String,
271        sell_leverage: String,
272    ) -> PyResult<Bound<'py, PyAny>> {
273        let client = self.clone();
274
275        pyo3_async_runtimes::tokio::future_into_py(py, async move {
276            client
277                .set_leverage(product_type, &symbol, &buy_leverage, &sell_leverage)
278                .await
279                .map_err(to_pyvalue_err)?;
280
281            Python::attach(|py| Ok(py.None()))
282        })
283    }
284
285    #[pyo3(name = "switch_mode")]
286    #[pyo3(signature = (product_type, mode, symbol=None, coin=None))]
287    fn py_switch_mode<'py>(
288        &self,
289        py: Python<'py>,
290        product_type: BybitProductType,
291        mode: BybitPositionMode,
292        symbol: Option<String>,
293        coin: Option<String>,
294    ) -> PyResult<Bound<'py, PyAny>> {
295        let client = self.clone();
296
297        pyo3_async_runtimes::tokio::future_into_py(py, async move {
298            client
299                .switch_mode(product_type, mode, symbol, coin)
300                .await
301                .map_err(to_pyvalue_err)?;
302
303            Python::attach(|py| Ok(py.None()))
304        })
305    }
306
307    #[pyo3(name = "get_spot_borrow_amount")]
308    fn py_get_spot_borrow_amount<'py>(
309        &self,
310        py: Python<'py>,
311        coin: String,
312    ) -> PyResult<Bound<'py, PyAny>> {
313        let client = self.clone();
314
315        pyo3_async_runtimes::tokio::future_into_py(py, async move {
316            let borrow_amount = client
317                .get_spot_borrow_amount(&coin)
318                .await
319                .map_err(to_pyvalue_err)?;
320
321            Ok(borrow_amount)
322        })
323    }
324
325    #[pyo3(name = "borrow_spot")]
326    #[pyo3(signature = (coin, amount))]
327    fn py_borrow_spot<'py>(
328        &self,
329        py: Python<'py>,
330        coin: String,
331        amount: Quantity,
332    ) -> PyResult<Bound<'py, PyAny>> {
333        let client = self.clone();
334
335        pyo3_async_runtimes::tokio::future_into_py(py, async move {
336            client
337                .borrow_spot(&coin, amount)
338                .await
339                .map_err(to_pyvalue_err)?;
340
341            Python::attach(|py| Ok(py.None()))
342        })
343    }
344
345    #[pyo3(name = "repay_spot_borrow")]
346    #[pyo3(signature = (coin, amount=None))]
347    fn py_repay_spot_borrow<'py>(
348        &self,
349        py: Python<'py>,
350        coin: String,
351        amount: Option<Quantity>,
352    ) -> PyResult<Bound<'py, PyAny>> {
353        let client = self.clone();
354
355        pyo3_async_runtimes::tokio::future_into_py(py, async move {
356            client
357                .repay_spot_borrow(&coin, amount)
358                .await
359                .map_err(to_pyvalue_err)?;
360
361            Python::attach(|py| Ok(py.None()))
362        })
363    }
364
365    #[pyo3(name = "request_instruments")]
366    #[pyo3(signature = (product_type, symbol=None))]
367    fn py_request_instruments<'py>(
368        &self,
369        py: Python<'py>,
370        product_type: BybitProductType,
371        symbol: Option<String>,
372    ) -> PyResult<Bound<'py, PyAny>> {
373        let client = self.clone();
374
375        pyo3_async_runtimes::tokio::future_into_py(py, async move {
376            let instruments = client
377                .request_instruments(product_type, symbol)
378                .await
379                .map_err(to_pyvalue_err)?;
380
381            Python::attach(|py| {
382                let py_instruments: PyResult<Vec<_>> = instruments
383                    .into_iter()
384                    .map(|inst| instrument_any_to_pyobject(py, inst))
385                    .collect();
386                let pylist = PyList::new(py, py_instruments?)
387                    .unwrap()
388                    .into_any()
389                    .unbind();
390                Ok(pylist)
391            })
392        })
393    }
394
395    #[pyo3(name = "request_tickers")]
396    fn py_request_tickers<'py>(
397        &self,
398        py: Python<'py>,
399        params: crate::python::params::BybitTickersParams,
400    ) -> PyResult<Bound<'py, PyAny>> {
401        let client = self.clone();
402
403        pyo3_async_runtimes::tokio::future_into_py(py, async move {
404            let tickers = client
405                .request_tickers(&params.into())
406                .await
407                .map_err(to_pyvalue_err)?;
408
409            Python::attach(|py| {
410                let py_tickers: PyResult<Vec<_>> = tickers
411                    .into_iter()
412                    .map(|ticker| Py::new(py, ticker))
413                    .collect();
414                let pylist = PyList::new(py, py_tickers?).unwrap().into_any().unbind();
415                Ok(pylist)
416            })
417        })
418    }
419
420    #[pyo3(name = "submit_order")]
421    #[pyo3(signature = (
422        account_id,
423        product_type,
424        instrument_id,
425        client_order_id,
426        order_side,
427        order_type,
428        quantity,
429        time_in_force,
430        price = None,
431        reduce_only = false,
432        is_leverage = false
433    ))]
434    #[allow(clippy::too_many_arguments)]
435    fn py_submit_order<'py>(
436        &self,
437        py: Python<'py>,
438        account_id: AccountId,
439        product_type: BybitProductType,
440        instrument_id: InstrumentId,
441        client_order_id: ClientOrderId,
442        order_side: OrderSide,
443        order_type: OrderType,
444        quantity: Quantity,
445        time_in_force: TimeInForce,
446        price: Option<Price>,
447        reduce_only: bool,
448        is_leverage: bool,
449    ) -> PyResult<Bound<'py, PyAny>> {
450        let client = self.clone();
451
452        pyo3_async_runtimes::tokio::future_into_py(py, async move {
453            let report = client
454                .submit_order(
455                    account_id,
456                    product_type,
457                    instrument_id,
458                    client_order_id,
459                    order_side,
460                    order_type,
461                    quantity,
462                    time_in_force,
463                    price,
464                    reduce_only,
465                    is_leverage,
466                )
467                .await
468                .map_err(to_pyvalue_err)?;
469
470            Python::attach(|py| report.into_py_any(py))
471        })
472    }
473
474    #[pyo3(name = "modify_order")]
475    #[pyo3(signature = (
476        account_id,
477        product_type,
478        instrument_id,
479        client_order_id=None,
480        venue_order_id=None,
481        quantity=None,
482        price=None
483    ))]
484    #[allow(clippy::too_many_arguments)]
485    fn py_modify_order<'py>(
486        &self,
487        py: Python<'py>,
488        account_id: AccountId,
489        product_type: BybitProductType,
490        instrument_id: InstrumentId,
491        client_order_id: Option<ClientOrderId>,
492        venue_order_id: Option<VenueOrderId>,
493        quantity: Option<Quantity>,
494        price: Option<Price>,
495    ) -> PyResult<Bound<'py, PyAny>> {
496        let client = self.clone();
497
498        pyo3_async_runtimes::tokio::future_into_py(py, async move {
499            let report = client
500                .modify_order(
501                    account_id,
502                    product_type,
503                    instrument_id,
504                    client_order_id,
505                    venue_order_id,
506                    quantity,
507                    price,
508                )
509                .await
510                .map_err(to_pyvalue_err)?;
511
512            Python::attach(|py| report.into_py_any(py))
513        })
514    }
515
516    #[pyo3(name = "cancel_order")]
517    #[pyo3(signature = (account_id, product_type, instrument_id, client_order_id=None, venue_order_id=None))]
518    fn py_cancel_order<'py>(
519        &self,
520        py: Python<'py>,
521        account_id: AccountId,
522        product_type: BybitProductType,
523        instrument_id: InstrumentId,
524        client_order_id: Option<ClientOrderId>,
525        venue_order_id: Option<VenueOrderId>,
526    ) -> PyResult<Bound<'py, PyAny>> {
527        let client = self.clone();
528
529        pyo3_async_runtimes::tokio::future_into_py(py, async move {
530            let report = client
531                .cancel_order(
532                    account_id,
533                    product_type,
534                    instrument_id,
535                    client_order_id,
536                    venue_order_id,
537                )
538                .await
539                .map_err(to_pyvalue_err)?;
540
541            Python::attach(|py| report.into_py_any(py))
542        })
543    }
544
545    #[pyo3(name = "cancel_all_orders")]
546    fn py_cancel_all_orders<'py>(
547        &self,
548        py: Python<'py>,
549        account_id: AccountId,
550        product_type: BybitProductType,
551        instrument_id: InstrumentId,
552    ) -> PyResult<Bound<'py, PyAny>> {
553        let client = self.clone();
554
555        pyo3_async_runtimes::tokio::future_into_py(py, async move {
556            let reports = client
557                .cancel_all_orders(account_id, product_type, instrument_id)
558                .await
559                .map_err(to_pyvalue_err)?;
560
561            Python::attach(|py| {
562                let py_reports: PyResult<Vec<_>> = reports
563                    .into_iter()
564                    .map(|report| report.into_py_any(py))
565                    .collect();
566                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
567                Ok(pylist)
568            })
569        })
570    }
571
572    #[pyo3(name = "query_order")]
573    #[pyo3(signature = (account_id, product_type, instrument_id, client_order_id=None, venue_order_id=None))]
574    fn py_query_order<'py>(
575        &self,
576        py: Python<'py>,
577        account_id: AccountId,
578        product_type: BybitProductType,
579        instrument_id: InstrumentId,
580        client_order_id: Option<ClientOrderId>,
581        venue_order_id: Option<VenueOrderId>,
582    ) -> PyResult<Bound<'py, PyAny>> {
583        let client = self.clone();
584
585        pyo3_async_runtimes::tokio::future_into_py(py, async move {
586            match client
587                .query_order(
588                    account_id,
589                    product_type,
590                    instrument_id,
591                    client_order_id,
592                    venue_order_id,
593                )
594                .await
595            {
596                Ok(Some(report)) => Python::attach(|py| report.into_py_any(py)),
597                Ok(None) => Ok(Python::attach(|py| py.None())),
598                Err(e) => Err(to_pyvalue_err(e)),
599            }
600        })
601    }
602
603    #[pyo3(name = "request_trades")]
604    #[pyo3(signature = (product_type, instrument_id, limit=None))]
605    fn py_request_trades<'py>(
606        &self,
607        py: Python<'py>,
608        product_type: BybitProductType,
609        instrument_id: InstrumentId,
610        limit: Option<u32>,
611    ) -> PyResult<Bound<'py, PyAny>> {
612        let client = self.clone();
613
614        pyo3_async_runtimes::tokio::future_into_py(py, async move {
615            let trades = client
616                .request_trades(product_type, instrument_id, limit)
617                .await
618                .map_err(to_pyvalue_err)?;
619
620            Python::attach(|py| {
621                let py_trades: PyResult<Vec<_>> = trades
622                    .into_iter()
623                    .map(|trade| trade.into_py_any(py))
624                    .collect();
625                let pylist = PyList::new(py, py_trades?).unwrap().into_any().unbind();
626                Ok(pylist)
627            })
628        })
629    }
630
631    #[pyo3(name = "request_bars")]
632    #[pyo3(signature = (product_type, bar_type, start=None, end=None, limit=None, timestamp_on_close=true))]
633    #[allow(clippy::too_many_arguments)]
634    fn py_request_bars<'py>(
635        &self,
636        py: Python<'py>,
637        product_type: BybitProductType,
638        bar_type: BarType,
639        start: Option<DateTime<Utc>>,
640        end: Option<DateTime<Utc>>,
641        limit: Option<u32>,
642        timestamp_on_close: bool,
643    ) -> PyResult<Bound<'py, PyAny>> {
644        let client = self.clone();
645
646        pyo3_async_runtimes::tokio::future_into_py(py, async move {
647            let bars = client
648                .request_bars(
649                    product_type,
650                    bar_type,
651                    start,
652                    end,
653                    limit,
654                    timestamp_on_close,
655                )
656                .await
657                .map_err(to_pyvalue_err)?;
658
659            Python::attach(|py| {
660                let py_bars: PyResult<Vec<_>> =
661                    bars.into_iter().map(|bar| bar.into_py_any(py)).collect();
662                let pylist = PyList::new(py, py_bars?).unwrap().into_any().unbind();
663                Ok(pylist)
664            })
665        })
666    }
667
668    #[pyo3(name = "request_fee_rates")]
669    #[pyo3(signature = (product_type, symbol=None, base_coin=None))]
670    fn py_request_fee_rates<'py>(
671        &self,
672        py: Python<'py>,
673        product_type: BybitProductType,
674        symbol: Option<String>,
675        base_coin: Option<String>,
676    ) -> PyResult<Bound<'py, PyAny>> {
677        let client = self.clone();
678
679        pyo3_async_runtimes::tokio::future_into_py(py, async move {
680            let fee_rates = client
681                .request_fee_rates(product_type, symbol, base_coin)
682                .await
683                .map_err(to_pyvalue_err)?;
684
685            Python::attach(|py| {
686                let py_fee_rates: PyResult<Vec<_>> = fee_rates
687                    .into_iter()
688                    .map(|rate| Py::new(py, rate))
689                    .collect();
690                let pylist = PyList::new(py, py_fee_rates?).unwrap().into_any().unbind();
691                Ok(pylist)
692            })
693        })
694    }
695
696    #[pyo3(name = "request_account_state")]
697    fn py_request_account_state<'py>(
698        &self,
699        py: Python<'py>,
700        account_type: crate::common::enums::BybitAccountType,
701        account_id: AccountId,
702    ) -> PyResult<Bound<'py, PyAny>> {
703        let client = self.clone();
704
705        pyo3_async_runtimes::tokio::future_into_py(py, async move {
706            let account_state = client
707                .request_account_state(account_type, account_id)
708                .await
709                .map_err(to_pyvalue_err)?;
710
711            Python::attach(|py| account_state.into_py_any(py))
712        })
713    }
714
715    #[pyo3(name = "request_order_status_reports")]
716    #[pyo3(signature = (account_id, product_type, instrument_id=None, open_only=false, start=None, end=None, limit=None))]
717    #[allow(clippy::too_many_arguments)]
718    fn py_request_order_status_reports<'py>(
719        &self,
720        py: Python<'py>,
721        account_id: AccountId,
722        product_type: BybitProductType,
723        instrument_id: Option<InstrumentId>,
724        open_only: bool,
725        start: Option<DateTime<Utc>>,
726        end: Option<DateTime<Utc>>,
727        limit: Option<u32>,
728    ) -> PyResult<Bound<'py, PyAny>> {
729        let client = self.clone();
730
731        pyo3_async_runtimes::tokio::future_into_py(py, async move {
732            let reports = client
733                .request_order_status_reports(
734                    account_id,
735                    product_type,
736                    instrument_id,
737                    open_only,
738                    start,
739                    end,
740                    limit,
741                )
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 = "request_fill_reports")]
757    #[pyo3(signature = (account_id, product_type, instrument_id=None, start=None, end=None, limit=None))]
758    #[allow(clippy::too_many_arguments)]
759    fn py_request_fill_reports<'py>(
760        &self,
761        py: Python<'py>,
762        account_id: AccountId,
763        product_type: BybitProductType,
764        instrument_id: Option<InstrumentId>,
765        start: Option<i64>,
766        end: Option<i64>,
767        limit: Option<u32>,
768    ) -> PyResult<Bound<'py, PyAny>> {
769        let client = self.clone();
770
771        pyo3_async_runtimes::tokio::future_into_py(py, async move {
772            let reports = client
773                .request_fill_reports(account_id, product_type, instrument_id, start, end, limit)
774                .await
775                .map_err(to_pyvalue_err)?;
776
777            Python::attach(|py| {
778                let py_reports: PyResult<Vec<_>> = reports
779                    .into_iter()
780                    .map(|report| report.into_py_any(py))
781                    .collect();
782                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
783                Ok(pylist)
784            })
785        })
786    }
787
788    #[pyo3(name = "request_position_status_reports")]
789    #[pyo3(signature = (account_id, product_type, instrument_id=None))]
790    fn py_request_position_status_reports<'py>(
791        &self,
792        py: Python<'py>,
793        account_id: AccountId,
794        product_type: BybitProductType,
795        instrument_id: Option<InstrumentId>,
796    ) -> PyResult<Bound<'py, PyAny>> {
797        let client = self.clone();
798
799        pyo3_async_runtimes::tokio::future_into_py(py, async move {
800            let reports = client
801                .request_position_status_reports(account_id, product_type, instrument_id)
802                .await
803                .map_err(to_pyvalue_err)?;
804
805            Python::attach(|py| {
806                let py_reports: PyResult<Vec<_>> = reports
807                    .into_iter()
808                    .map(|report| report.into_py_any(py))
809                    .collect();
810                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
811                Ok(pylist)
812            })
813        })
814    }
815}
816
817impl From<BybitHttpError> for PyErr {
818    fn from(error: BybitHttpError) -> Self {
819        match error {
820            // Runtime/operational errors
821            BybitHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
822            BybitHttpError::NetworkError(msg) => to_pyruntime_err(format!("Network error: {msg}")),
823            BybitHttpError::UnexpectedStatus { status, body } => {
824                to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
825            }
826            // Validation/configuration errors
827            BybitHttpError::MissingCredentials => {
828                to_pyvalue_err("Missing credentials for authenticated request")
829            }
830            BybitHttpError::ValidationError(msg) => {
831                to_pyvalue_err(format!("Parameter validation error: {msg}"))
832            }
833            BybitHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
834            BybitHttpError::BuildError(e) => to_pyvalue_err(format!("Build error: {e}")),
835            BybitHttpError::BybitError {
836                error_code,
837                message,
838            } => to_pyvalue_err(format!("Bybit error {error_code}: {message}")),
839        }
840    }
841}