Skip to main content

nautilus_bybit/python/
http.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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 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    /// Raw HTTP client for low-level Bybit API operations.
58    ///
59    /// This client handles request/response operations with the Bybit API,
60    /// returning venue-specific response types. It does not parse to Nautilus domain types.
61    #[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    /// Returns the base URL used for requests.
94    #[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    /// Returns the configured receive window in milliseconds.
109    #[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    /// Cancels all pending HTTP requests.
117    #[pyo3(name = "cancel_all_requests")]
118    fn py_cancel_all_requests(&self) {
119        self.cancel_all_requests();
120    }
121
122    /// Fetches the current server time from Bybit.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if the request fails or the response cannot be parsed.
127    ///
128    /// # References
129    ///
130    /// - <https://bybit-exchange.github.io/docs/v5/market/time>
131    #[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    /// Fetches open orders (requires authentication).
146    ///
147    /// # References
148    ///
149    /// - <https://bybit-exchange.github.io/docs/v5/order/open-order>
150    #[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    /// Provides a HTTP client for connecting to the [Bybit](https://bybit.com) REST API.
199    /// High-level HTTP client that wraps the raw client and provides Nautilus domain types.
200    ///
201    /// This client maintains an instrument cache and uses it to parse venue responses
202    /// into Nautilus domain objects.
203    #[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    /// Any existing instrument with the same symbol will be replaced.
257    #[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    /// Sets margin mode (requires authentication).
275    ///
276    /// # References
277    ///
278    /// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
279    #[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    /// Fetches API key information including account details (requires authentication).
298    ///
299    /// # Errors
300    ///
301    /// Returns an error if:
302    /// - The request fails.
303    /// - The response cannot be parsed.
304    ///
305    /// # References
306    ///
307    /// - <https://bybit-exchange.github.io/docs/v5/user/apikey-info>
308    #[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    /// Sets leverage for a symbol (requires authentication).
323    ///
324    /// # References
325    ///
326    /// - <https://bybit-exchange.github.io/docs/v5/position/leverage>
327    #[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    /// Switches position mode (requires authentication).
350    ///
351    /// # References
352    ///
353    /// - <https://bybit-exchange.github.io/docs/v5/position/position-mode>
354    #[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    /// Get the outstanding spot borrow amount for a specific coin.
377    ///
378    /// Returns zero if no borrow exists.
379    ///
380    /// # Parameters
381    ///
382    /// - `coin`: The coin to check (e.g., "BTC", "ETH")
383    #[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    /// Borrows coins for spot margin trading.
402    ///
403    /// This should be called before opening short spot positions.
404    ///
405    /// # Parameters
406    ///
407    /// - `coin`: The coin to repay (e.g., "BTC", "ETH")
408    /// - `amount`: Optional amount to borrow. If None, repays all outstanding borrows.
409    #[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    /// Repays spot borrows for a specific coin.
430    ///
431    /// This should be called after closing short spot positions to avoid accruing interest.
432    ///
433    /// # Parameters
434    ///
435    /// - `coin`: The coin to repay (e.g., "BTC", "ETH")
436    /// - `amount`: Optional amount to repay. If None, repays all outstanding borrows.
437    #[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    /// Request instruments for a given product type.
458    ///
459    /// When `base_coin` is provided, the request is narrowed to that base coin.
460    /// This is required for `Option`: Bybit's API returns only `BTC` options when
461    /// `baseCoin` is omitted.
462    #[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    /// Fetches instrument info and returns the current status of each symbol.
495    ///
496    /// Paginates through the instruments endpoint collecting only
497    /// `(InstrumentId, MarketStatusAction)` pairs. This avoids fee-rate
498    /// fetching and full instrument parsing.
499    #[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    /// Request ticker information for market data.
527    ///
528    /// Fetches ticker data from Bybit's `/v5/market/tickers` endpoint and returns
529    /// a unified `BybitTickerData` structure compatible with all product types.
530    ///
531    /// # References
532    ///
533    /// <https://bybit-exchange.github.io/docs/v5/market/tickers>
534    #[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(&params.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    /// Submit a new order.
560    #[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    /// Modify an existing order.
627    #[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    /// Cancel an order.
670    #[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    /// Cancel all orders for an instrument.
700    #[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    /// Query a single order by client order ID or venue order ID.
728    #[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    /// Request recent trade tick history for a given symbol.
760    ///
761    /// Returns the most recent public trades from Bybit's `/v5/market/recent-trade` endpoint.
762    /// This endpoint only provides recent trades (up to 1000 most recent), typically covering
763    /// only the last few minutes for active markets.
764    ///
765    /// **Note**: For historical trade data with time ranges, use the klines endpoint instead.
766    /// The Bybit public API does not support fetching historical trades by time range.
767    ///
768    /// # References
769    ///
770    /// <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
771    #[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    /// Request funding rate history for a given symbol.
800    ///
801    /// # References
802    ///
803    /// <https://bybit-exchange.github.io/docs/v5/market/history-fund-rate>
804    #[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    /// Request an orderbook snapshot for a given symbol.
838    ///
839    /// Bybit limits the amount of levels (depth) for each product type to:
840    /// - Spot: `1..=200` (default: `1`)
841    /// - Linear & Inverse: `1..=500` (default: `25`)
842    /// - Options: `1..=25` (default: `1`)
843    ///
844    /// # References
845    ///
846    /// <https://bybit-exchange.github.io/docs/v5/market/orderbook>
847    #[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    /// Request bar/kline history for a given symbol.
869    ///
870    /// # References
871    ///
872    /// <https://bybit-exchange.github.io/docs/v5/market/kline>
873    #[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    /// Requests trading fee rates for the specified product type and optional filters.
911    ///
912    /// # References
913    ///
914    /// <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
915    #[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    /// Requests the current account state for the specified account type.
944    ///
945    /// # References
946    ///
947    /// <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
948    #[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    /// Request multiple order status reports.
968    ///
969    /// Orders for instruments not currently loaded in cache will be skipped.
970    #[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    /// Fetches execution history (fills) for the account and returns a list of `FillReport`s.
1012    ///
1013    /// Executions for instruments not currently loaded in cache will be skipped.
1014    ///
1015    /// # References
1016    ///
1017    /// <https://bybit-exchange.github.io/docs/v5/order/execution>
1018    #[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    /// Fetches position information for the account and returns a list of `PositionStatusReport`s.
1051    ///
1052    /// Positions for instruments not currently loaded in cache will be skipped.
1053    ///
1054    /// # References
1055    ///
1056    /// <https://bybit-exchange.github.io/docs/v5/position>
1057    #[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    /// Request forward prices for option chain ATM determination.
1086    ///
1087    /// Single-instrument path (1 HTTP call) if `instrument_id` is provided,
1088    /// otherwise bulk path via option tickers.
1089    #[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                // Single-instrument path: fetch ticker for one symbol
1102                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(&params)
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                // Bulk path: fetch all option tickers for base coin
1127                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            // Runtime/operational errors
1177            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            // Validation/configuration errors
1183            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}