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 jiff::Timestamp;
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::{
28    conversion::IntoPyObjectExt,
29    prelude::*,
30    types::{PyDict, PyList},
31};
32use ustr::Ustr;
33
34use crate::{
35    common::{
36        enums::{
37            BybitMarginMode, BybitOpenOnly, BybitOrderFilter, BybitPositionIdx, BybitPositionMode,
38            BybitProductType,
39        },
40        parse::{parse_bbo_level, parse_bbo_side_type, parse_smp_type},
41    },
42    http::{
43        client::{BybitHttpClient, BybitRawHttpClient},
44        error::BybitHttpError,
45        models::BybitOrderCursorList,
46        query::BybitNativeTpSlParams as RustNativeTpSlParams,
47    },
48    python::params::BybitNativeTpSlParams,
49};
50
51#[pymethods]
52#[pyo3_stub_gen::derive::gen_stub_pymethods]
53impl BybitRawHttpClient {
54    /// Raw HTTP client for low-level Bybit API operations.
55    ///
56    /// This client handles request/response operations with the Bybit API,
57    /// returning venue-specific response types. It does not parse to Nautilus domain types.
58    #[new]
59    #[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))]
60    #[expect(clippy::too_many_arguments)]
61    fn py_new(
62        api_key: Option<String>,
63        api_secret: Option<String>,
64        base_url: Option<String>,
65        demo: bool,
66        testnet: bool,
67        timeout_secs: u64,
68        max_retries: u32,
69        retry_delay_ms: u64,
70        retry_delay_max_ms: u64,
71        recv_window_ms: u64,
72        proxy_url: Option<String>,
73    ) -> PyResult<Self> {
74        Self::new_with_env(
75            api_key,
76            api_secret,
77            base_url,
78            demo,
79            testnet,
80            timeout_secs,
81            max_retries,
82            retry_delay_ms,
83            retry_delay_max_ms,
84            recv_window_ms,
85            proxy_url,
86        )
87        .map_err(to_pyvalue_err)
88    }
89
90    /// Returns the base URL used for requests.
91    #[getter]
92    #[pyo3(name = "base_url")]
93    #[must_use]
94    pub fn py_base_url(&self) -> &str {
95        self.base_url()
96    }
97
98    #[getter]
99    #[pyo3(name = "api_key")]
100    #[must_use]
101    pub fn py_api_key(&self) -> Option<String> {
102        self.credential().map(|c| c.api_key().to_string())
103    }
104
105    /// Returns the configured receive window in milliseconds.
106    #[getter]
107    #[pyo3(name = "recv_window_ms")]
108    #[must_use]
109    pub fn py_recv_window_ms(&self) -> u64 {
110        self.recv_window_ms()
111    }
112
113    /// Cancels all pending HTTP requests.
114    #[pyo3(name = "cancel_all_requests")]
115    fn py_cancel_all_requests(&self) {
116        self.cancel_all_requests();
117    }
118
119    /// Fetches the current server time from Bybit.
120    ///
121    /// # Errors
122    ///
123    /// Returns an error if the request fails or the response cannot be parsed.
124    ///
125    /// # References
126    ///
127    /// - <https://bybit-exchange.github.io/docs/v5/market/time>
128    #[pyo3(name = "get_server_time")]
129    fn py_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
130        let client = self.clone();
131
132        pyo3_async_runtimes::tokio::future_into_py(py, async move {
133            let response = client.get_server_time().await.map_err(to_pyvalue_err)?;
134
135            Python::attach(|py| {
136                let server_time = Py::new(py, response.result)?;
137                Ok(server_time.into_any())
138            })
139        })
140    }
141
142    /// Fetches open orders (requires authentication).
143    ///
144    /// # Errors
145    ///
146    /// Returns an error if the request fails or the response cannot be parsed.
147    ///
148    /// # References
149    ///
150    /// - <https://bybit-exchange.github.io/docs/v5/order/open-order>
151    #[pyo3(name = "get_open_orders")]
152    #[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))]
153    #[expect(clippy::too_many_arguments)]
154    fn py_get_open_orders<'py>(
155        &self,
156        py: Python<'py>,
157        category: BybitProductType,
158        symbol: Option<String>,
159        base_coin: Option<String>,
160        settle_coin: Option<String>,
161        order_id: Option<String>,
162        order_link_id: Option<String>,
163        open_only: Option<BybitOpenOnly>,
164        order_filter: Option<BybitOrderFilter>,
165        limit: Option<u32>,
166        cursor: Option<String>,
167    ) -> PyResult<Bound<'py, PyAny>> {
168        let client = self.clone();
169
170        pyo3_async_runtimes::tokio::future_into_py(py, async move {
171            let response = client
172                .get_open_orders(
173                    category,
174                    symbol,
175                    base_coin,
176                    settle_coin,
177                    order_id,
178                    order_link_id,
179                    open_only,
180                    order_filter,
181                    limit,
182                    cursor,
183                )
184                .await
185                .map_err(to_pyvalue_err)?;
186
187            Python::attach(|py| {
188                let open_orders = BybitOrderCursorList::from(response.result);
189                let py_open_orders = Py::new(py, open_orders)?;
190                Ok(py_open_orders.into_any())
191            })
192        })
193    }
194}
195
196#[pymethods]
197#[pyo3_stub_gen::derive::gen_stub_pymethods]
198impl BybitHttpClient {
199    /// Provides a HTTP client for connecting to the [Bybit](https://bybit.com) REST API.
200    /// High-level HTTP client that wraps the raw client and provides Nautilus domain types.
201    ///
202    /// This client maintains an instrument cache and uses it to parse venue responses
203    /// into Nautilus domain objects.
204    #[new]
205    #[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))]
206    #[expect(clippy::too_many_arguments)]
207    fn py_new(
208        api_key: Option<String>,
209        api_secret: Option<String>,
210        base_url: Option<String>,
211        demo: bool,
212        testnet: bool,
213        timeout_secs: u64,
214        max_retries: u32,
215        retry_delay_ms: u64,
216        retry_delay_max_ms: u64,
217        recv_window_ms: u64,
218        proxy_url: Option<String>,
219    ) -> PyResult<Self> {
220        Self::new_with_env(
221            api_key,
222            api_secret,
223            base_url,
224            demo,
225            testnet,
226            timeout_secs,
227            max_retries,
228            retry_delay_ms,
229            retry_delay_max_ms,
230            recv_window_ms,
231            proxy_url,
232        )
233        .map_err(to_pyvalue_err)
234    }
235
236    #[getter]
237    #[pyo3(name = "base_url")]
238    #[must_use]
239    pub fn py_base_url(&self) -> &str {
240        self.base_url()
241    }
242
243    #[getter]
244    #[pyo3(name = "api_key")]
245    #[must_use]
246    pub fn py_api_key(&self) -> Option<&str> {
247        self.credential().map(|c| c.api_key())
248    }
249
250    #[getter]
251    #[pyo3(name = "api_key_masked")]
252    #[must_use]
253    pub fn py_api_key_masked(&self) -> Option<String> {
254        self.credential().map(|c| c.api_key_masked())
255    }
256
257    /// Any existing instrument with the same symbol will be replaced.
258    #[pyo3(name = "cache_instrument")]
259    fn py_cache_instrument(&self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
260        let inst_any = pyobject_to_instrument_any(py, instrument)?;
261        self.cache_instrument(inst_any);
262        Ok(())
263    }
264
265    #[pyo3(name = "cancel_all_requests")]
266    fn py_cancel_all_requests(&self) {
267        self.cancel_all_requests();
268    }
269
270    #[pyo3(name = "set_use_spot_position_reports")]
271    fn py_set_use_spot_position_reports(&self, value: bool) {
272        self.set_use_spot_position_reports(value);
273    }
274
275    /// Sets margin mode (requires authentication).
276    ///
277    /// # Errors
278    ///
279    /// Returns an error if:
280    /// - Credentials are missing.
281    /// - The request fails.
282    /// - The API returns an error.
283    ///
284    /// # References
285    ///
286    /// - <https://bybit-exchange.github.io/docs/v5/account/set-margin-mode>
287    #[pyo3(name = "set_margin_mode")]
288    fn py_set_margin_mode<'py>(
289        &self,
290        py: Python<'py>,
291        margin_mode: BybitMarginMode,
292    ) -> PyResult<Bound<'py, PyAny>> {
293        let client = self.clone();
294
295        pyo3_async_runtimes::tokio::future_into_py(py, async move {
296            client
297                .set_margin_mode(margin_mode)
298                .await
299                .map_err(to_pyvalue_err)?;
300
301            Python::attach(|py| Ok(py.None()))
302        })
303    }
304
305    /// Fetches API key information including account details (requires authentication).
306    ///
307    /// # Errors
308    ///
309    /// Returns an error if:
310    /// - The request fails.
311    /// - The response cannot be parsed.
312    ///
313    /// # References
314    ///
315    /// - <https://bybit-exchange.github.io/docs/v5/user/apikey-info>
316    #[pyo3(name = "get_account_details")]
317    fn py_get_account_details<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
318        let client = self.clone();
319
320        pyo3_async_runtimes::tokio::future_into_py(py, async move {
321            let response = client.get_account_details().await.map_err(to_pyvalue_err)?;
322
323            Python::attach(|py| {
324                let account_details = Py::new(py, response.result)?;
325                Ok(account_details.into_any())
326            })
327        })
328    }
329
330    /// Sets leverage for a symbol (requires authentication).
331    ///
332    /// # Errors
333    ///
334    /// Returns an error if:
335    /// - Credentials are missing.
336    /// - The request fails.
337    /// - The API returns an error.
338    ///
339    /// # References
340    ///
341    /// - <https://bybit-exchange.github.io/docs/v5/position/leverage>
342    #[pyo3(name = "set_leverage")]
343    #[pyo3(signature = (product_type, symbol, buy_leverage, sell_leverage))]
344    fn py_set_leverage<'py>(
345        &self,
346        py: Python<'py>,
347        product_type: BybitProductType,
348        symbol: String,
349        buy_leverage: String,
350        sell_leverage: String,
351    ) -> PyResult<Bound<'py, PyAny>> {
352        let client = self.clone();
353
354        pyo3_async_runtimes::tokio::future_into_py(py, async move {
355            client
356                .set_leverage(product_type, &symbol, &buy_leverage, &sell_leverage)
357                .await
358                .map_err(to_pyvalue_err)?;
359
360            Python::attach(|py| Ok(py.None()))
361        })
362    }
363
364    /// Switches position mode (requires authentication).
365    ///
366    /// # Errors
367    ///
368    /// Returns an error if:
369    /// - Credentials are missing.
370    /// - The request fails.
371    /// - The API returns an error.
372    ///
373    /// # References
374    ///
375    /// - <https://bybit-exchange.github.io/docs/v5/position/position-mode>
376    #[pyo3(name = "switch_mode")]
377    #[pyo3(signature = (product_type, mode, symbol=None, coin=None))]
378    fn py_switch_mode<'py>(
379        &self,
380        py: Python<'py>,
381        product_type: BybitProductType,
382        mode: BybitPositionMode,
383        symbol: Option<String>,
384        coin: Option<String>,
385    ) -> PyResult<Bound<'py, PyAny>> {
386        let client = self.clone();
387
388        pyo3_async_runtimes::tokio::future_into_py(py, async move {
389            client
390                .switch_mode(product_type, mode, symbol, coin)
391                .await
392                .map_err(to_pyvalue_err)?;
393
394            Python::attach(|py| Ok(py.None()))
395        })
396    }
397
398    /// Get the outstanding spot borrow amount for a specific coin.
399    ///
400    /// Returns zero if no borrow exists.
401    ///
402    /// # Parameters
403    ///
404    /// - `coin`: The coin to check (e.g., "BTC", "ETH")
405    ///
406    /// # Errors
407    ///
408    /// Returns an error if:
409    /// - Credentials are missing.
410    /// - The request fails.
411    /// - The coin is not found in the wallet.
412    #[pyo3(name = "get_spot_borrow_amount")]
413    fn py_get_spot_borrow_amount<'py>(
414        &self,
415        py: Python<'py>,
416        coin: String,
417    ) -> PyResult<Bound<'py, PyAny>> {
418        let client = self.clone();
419
420        pyo3_async_runtimes::tokio::future_into_py(py, async move {
421            let borrow_amount = client
422                .get_spot_borrow_amount(&coin)
423                .await
424                .map_err(to_pyvalue_err)?;
425
426            Ok(borrow_amount)
427        })
428    }
429
430    /// Borrows coins for spot margin trading.
431    ///
432    /// This should be called before opening short spot positions.
433    ///
434    /// # Parameters
435    ///
436    /// - `coin`: The coin to repay (e.g., "BTC", "ETH")
437    /// - `amount`: Optional amount to borrow. If None, repays all outstanding borrows.
438    ///
439    /// # Errors
440    ///
441    /// Returns an error if:
442    /// - Credentials are missing.
443    /// - The request fails.
444    /// - Insufficient collateral for the borrow.
445    #[pyo3(name = "borrow_spot")]
446    #[pyo3(signature = (coin, amount))]
447    fn py_borrow_spot<'py>(
448        &self,
449        py: Python<'py>,
450        coin: String,
451        amount: Quantity,
452    ) -> PyResult<Bound<'py, PyAny>> {
453        let client = self.clone();
454
455        pyo3_async_runtimes::tokio::future_into_py(py, async move {
456            client
457                .borrow_spot(&coin, amount)
458                .await
459                .map_err(to_pyvalue_err)?;
460
461            Python::attach(|py| Ok(py.None()))
462        })
463    }
464
465    /// Repays spot borrows for a specific coin.
466    ///
467    /// This should be called after closing short spot positions to avoid accruing interest.
468    ///
469    /// # Parameters
470    ///
471    /// - `coin`: The coin to repay (e.g., "BTC", "ETH")
472    /// - `amount`: Optional amount to repay. If None, repays all outstanding borrows.
473    ///
474    /// # Errors
475    ///
476    /// Returns an error if:
477    /// - Credentials are missing.
478    /// - The request fails.
479    /// - Called during the hourly interest-calculation window (mm:04:00-mm:05:30 UTC each hour).
480    /// - Insufficient spot balance for repayment.
481    #[pyo3(name = "repay_spot_borrow")]
482    #[pyo3(signature = (coin, amount=None))]
483    fn py_repay_spot_borrow<'py>(
484        &self,
485        py: Python<'py>,
486        coin: String,
487        amount: Option<Quantity>,
488    ) -> PyResult<Bound<'py, PyAny>> {
489        let client = self.clone();
490
491        pyo3_async_runtimes::tokio::future_into_py(py, async move {
492            client
493                .repay_spot_borrow(&coin, amount)
494                .await
495                .map_err(to_pyvalue_err)?;
496
497            Python::attach(|py| Ok(py.None()))
498        })
499    }
500
501    /// Repays spot borrows for a specific coin, converting other assets if required.
502    ///
503    /// Unlike `Self.repay_spot_borrow`, this uses the venue's manual repay endpoint,
504    /// which may draw on other holdings when the debt coin's spot balance is insufficient.
505    ///
506    /// # Parameters
507    ///
508    /// - `coin`: The coin to repay (e.g., "BTC", "ETH")
509    /// - `amount`: Optional amount to repay. If None, repays all outstanding borrows.
510    ///
511    /// # Errors
512    ///
513    /// Returns an error if:
514    /// - Credentials are missing.
515    /// - The request fails.
516    /// - Called during the hourly interest-calculation window (mm:04:00-mm:05:30 UTC each hour).
517    /// - Insufficient balance for repayment.
518    #[pyo3(name = "repay_spot_borrow_with_conversion")]
519    #[pyo3(signature = (coin, amount=None))]
520    fn py_repay_spot_borrow_with_conversion<'py>(
521        &self,
522        py: Python<'py>,
523        coin: String,
524        amount: Option<Quantity>,
525    ) -> PyResult<Bound<'py, PyAny>> {
526        let client = self.clone();
527
528        pyo3_async_runtimes::tokio::future_into_py(py, async move {
529            client
530                .repay_spot_borrow_with_conversion(&coin, amount)
531                .await
532                .map_err(to_pyvalue_err)?;
533
534            Python::attach(|py| Ok(py.None()))
535        })
536    }
537
538    /// Request instruments for a given product type.
539    ///
540    /// When `base_coin` is provided, the request is narrowed to that base coin.
541    /// This is required for `Option`: Bybit's API returns only `BTC` options when
542    /// `baseCoin` is omitted.
543    ///
544    /// # Errors
545    ///
546    /// Returns an error if the request fails or parsing fails.
547    #[pyo3(name = "request_instruments")]
548    #[pyo3(signature = (product_type, symbol=None, base_coin=None))]
549    fn py_request_instruments<'py>(
550        &self,
551        py: Python<'py>,
552        product_type: BybitProductType,
553        symbol: Option<String>,
554        base_coin: Option<String>,
555    ) -> PyResult<Bound<'py, PyAny>> {
556        let client = self.clone();
557        let base_coin = base_coin.map(|s| Ustr::from(&s));
558
559        pyo3_async_runtimes::tokio::future_into_py(py, async move {
560            let instruments = client
561                .request_instruments(product_type, symbol, base_coin)
562                .await
563                .map_err(to_pyvalue_err)?;
564
565            Python::attach(|py| {
566                let py_instruments: PyResult<Vec<_>> = instruments
567                    .into_iter()
568                    .map(|inst| instrument_any_to_pyobject(py, inst))
569                    .collect();
570                let pylist = PyList::new(py, py_instruments?)?.into_any().unbind();
571                Ok(pylist)
572            })
573        })
574    }
575
576    /// Fetches instrument info and returns the current status of each symbol.
577    ///
578    /// Paginates through the instruments endpoint collecting only
579    /// `(InstrumentId, MarketStatusAction)` pairs. This avoids fee-rate
580    /// fetching and full instrument parsing.
581    ///
582    /// # Errors
583    ///
584    /// Returns an error if the request fails.
585    #[pyo3(name = "request_instrument_statuses")]
586    fn py_request_instrument_statuses<'py>(
587        &self,
588        py: Python<'py>,
589        product_type: BybitProductType,
590    ) -> PyResult<Bound<'py, PyAny>> {
591        let client = self.clone();
592
593        pyo3_async_runtimes::tokio::future_into_py(py, async move {
594            let statuses = client
595                .request_instrument_statuses(product_type)
596                .await
597                .map_err(to_pyvalue_err)?;
598
599            Python::attach(|py| {
600                let dict = PyDict::new(py);
601                for (instrument_id, action) in statuses {
602                    dict.set_item(
603                        instrument_id.into_bound_py_any(py)?,
604                        action.into_bound_py_any(py)?,
605                    )?;
606                }
607                Ok(dict.into_any().unbind())
608            })
609        })
610    }
611
612    /// Request ticker information for market data.
613    ///
614    /// Fetches ticker data from Bybit's `/v5/market/tickers` endpoint and returns
615    /// a unified `BybitTickerData` structure compatible with all product types.
616    ///
617    /// # Errors
618    ///
619    /// Returns an error if the request fails or parsing fails.
620    ///
621    /// # References
622    ///
623    /// <https://bybit-exchange.github.io/docs/v5/market/tickers>
624    #[pyo3(name = "request_tickers")]
625    fn py_request_tickers<'py>(
626        &self,
627        py: Python<'py>,
628        params: crate::python::params::BybitTickersParams,
629    ) -> PyResult<Bound<'py, PyAny>> {
630        let client = self.clone();
631
632        pyo3_async_runtimes::tokio::future_into_py(py, async move {
633            let tickers = client
634                .request_tickers(&params.into())
635                .await
636                .map_err(to_pyvalue_err)?;
637
638            Python::attach(|py| {
639                let py_tickers: PyResult<Vec<_>> = tickers
640                    .into_iter()
641                    .map(|ticker| Py::new(py, ticker))
642                    .collect();
643                let pylist = PyList::new(py, py_tickers?)?.into_any().unbind();
644                Ok(pylist)
645            })
646        })
647    }
648
649    /// Submit a new order.
650    ///
651    /// # Errors
652    ///
653    /// Returns an error if:
654    /// - Credentials are missing.
655    /// - The request fails.
656    /// - Order validation fails.
657    /// - The order is rejected.
658    /// - The API returns an error.
659    #[pyo3(name = "submit_order")]
660    #[pyo3(signature = (
661        account_id,
662        product_type,
663        instrument_id,
664        client_order_id,
665        order_side,
666        order_type,
667        quantity,
668        time_in_force = None,
669        price = None,
670        trigger_price = None,
671        post_only = None,
672        reduce_only = false,
673        is_quote_quantity = false,
674        is_leverage = false,
675        position_idx = None,
676        bbo_side_type = None,
677        bbo_level = None,
678        smp_type = None,
679        native_tp_sl = None,
680    ))]
681    #[expect(clippy::too_many_arguments)]
682    fn py_submit_order<'py>(
683        &self,
684        py: Python<'py>,
685        account_id: AccountId,
686        product_type: BybitProductType,
687        instrument_id: InstrumentId,
688        client_order_id: ClientOrderId,
689        order_side: OrderSide,
690        order_type: OrderType,
691        quantity: Quantity,
692        time_in_force: Option<TimeInForce>,
693        price: Option<Price>,
694        trigger_price: Option<Price>,
695        post_only: Option<bool>,
696        reduce_only: bool,
697        is_quote_quantity: bool,
698        is_leverage: bool,
699        position_idx: Option<BybitPositionIdx>,
700        bbo_side_type: Option<String>,
701        bbo_level: Option<String>,
702        smp_type: Option<String>,
703        native_tp_sl: Option<BybitNativeTpSlParams>,
704    ) -> PyResult<Bound<'py, PyAny>> {
705        let client = self.clone();
706        let bbo_side_type = bbo_side_type
707            .map(|value| parse_bbo_side_type(&value))
708            .transpose()
709            .map_err(to_pyvalue_err)?;
710        let bbo_level = bbo_level
711            .map(parse_bbo_level)
712            .transpose()
713            .map_err(to_pyvalue_err)?;
714        if bbo_side_type.is_some() != bbo_level.is_some() {
715            return Err(to_pyvalue_err(anyhow::anyhow!(
716                "'bbo_side_type' and 'bbo_level' must be provided together"
717            )));
718        }
719
720        let smp_type = smp_type
721            .map(|value| parse_smp_type(&value))
722            .transpose()
723            .map_err(to_pyvalue_err)?;
724
725        let native_tp_sl: Option<RustNativeTpSlParams> = native_tp_sl
726            .map(RustNativeTpSlParams::try_from)
727            .transpose()
728            .map_err(to_pyvalue_err)?;
729
730        pyo3_async_runtimes::tokio::future_into_py(py, async move {
731            let report = client
732                .submit_order(
733                    account_id,
734                    product_type,
735                    instrument_id,
736                    client_order_id,
737                    order_side,
738                    order_type,
739                    quantity,
740                    time_in_force,
741                    price,
742                    trigger_price,
743                    post_only,
744                    reduce_only,
745                    is_quote_quantity,
746                    is_leverage,
747                    position_idx,
748                    bbo_side_type,
749                    bbo_level,
750                    smp_type,
751                    native_tp_sl.as_ref(),
752                )
753                .await
754                .map_err(to_pyvalue_err)?;
755
756            Python::attach(|py| report.into_py_any(py))
757        })
758    }
759
760    /// Modify an existing order.
761    ///
762    /// # Errors
763    ///
764    /// Returns an error if:
765    /// - Credentials are missing.
766    /// - The request fails.
767    /// - The order doesn't exist.
768    /// - The order is already closed.
769    /// - The API returns an error.
770    #[pyo3(name = "modify_order")]
771    #[pyo3(signature = (
772        account_id,
773        product_type,
774        instrument_id,
775        client_order_id=None,
776        venue_order_id=None,
777        quantity=None,
778        price=None
779    ))]
780    #[expect(clippy::too_many_arguments)]
781    fn py_modify_order<'py>(
782        &self,
783        py: Python<'py>,
784        account_id: AccountId,
785        product_type: BybitProductType,
786        instrument_id: InstrumentId,
787        client_order_id: Option<ClientOrderId>,
788        venue_order_id: Option<VenueOrderId>,
789        quantity: Option<Quantity>,
790        price: Option<Price>,
791    ) -> PyResult<Bound<'py, PyAny>> {
792        let client = self.clone();
793
794        pyo3_async_runtimes::tokio::future_into_py(py, async move {
795            let report = client
796                .modify_order(
797                    account_id,
798                    product_type,
799                    instrument_id,
800                    client_order_id,
801                    venue_order_id,
802                    quantity,
803                    price,
804                )
805                .await
806                .map_err(to_pyvalue_err)?;
807
808            Python::attach(|py| report.into_py_any(py))
809        })
810    }
811
812    /// Cancel an order.
813    ///
814    /// # Errors
815    ///
816    /// Returns an error if:
817    /// - Credentials are missing.
818    /// - The request fails.
819    /// - The order doesn't exist.
820    /// - The API returns an error.
821    #[pyo3(name = "cancel_order")]
822    #[pyo3(signature = (account_id, product_type, instrument_id, client_order_id=None, venue_order_id=None))]
823    fn py_cancel_order<'py>(
824        &self,
825        py: Python<'py>,
826        account_id: AccountId,
827        product_type: BybitProductType,
828        instrument_id: InstrumentId,
829        client_order_id: Option<ClientOrderId>,
830        venue_order_id: Option<VenueOrderId>,
831    ) -> PyResult<Bound<'py, PyAny>> {
832        let client = self.clone();
833
834        pyo3_async_runtimes::tokio::future_into_py(py, async move {
835            let report = client
836                .cancel_order(
837                    account_id,
838                    product_type,
839                    instrument_id,
840                    client_order_id,
841                    venue_order_id,
842                )
843                .await
844                .map_err(to_pyvalue_err)?;
845
846            Python::attach(|py| report.into_py_any(py))
847        })
848    }
849
850    /// Cancel all orders for an instrument.
851    ///
852    /// # Errors
853    ///
854    /// Returns an error if:
855    /// - Credentials are missing.
856    /// - The request fails.
857    /// - The API returns an error.
858    #[pyo3(name = "cancel_all_orders")]
859    fn py_cancel_all_orders<'py>(
860        &self,
861        py: Python<'py>,
862        account_id: AccountId,
863        product_type: BybitProductType,
864        instrument_id: InstrumentId,
865    ) -> PyResult<Bound<'py, PyAny>> {
866        let client = self.clone();
867
868        pyo3_async_runtimes::tokio::future_into_py(py, async move {
869            let reports = client
870                .cancel_all_orders(account_id, product_type, instrument_id)
871                .await
872                .map_err(to_pyvalue_err)?;
873
874            Python::attach(|py| {
875                let py_reports: PyResult<Vec<_>> = reports
876                    .into_iter()
877                    .map(|report| report.into_py_any(py))
878                    .collect();
879                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
880                Ok(pylist)
881            })
882        })
883    }
884
885    /// Query a single order by client order ID or venue order ID.
886    ///
887    /// # Errors
888    ///
889    /// Returns an error if:
890    /// - Credentials are missing.
891    /// - The request fails.
892    /// - The API returns an error.
893    #[pyo3(name = "query_order")]
894    #[pyo3(signature = (account_id, product_type, instrument_id, client_order_id=None, venue_order_id=None))]
895    fn py_query_order<'py>(
896        &self,
897        py: Python<'py>,
898        account_id: AccountId,
899        product_type: BybitProductType,
900        instrument_id: InstrumentId,
901        client_order_id: Option<ClientOrderId>,
902        venue_order_id: Option<VenueOrderId>,
903    ) -> PyResult<Bound<'py, PyAny>> {
904        let client = self.clone();
905
906        pyo3_async_runtimes::tokio::future_into_py(py, async move {
907            match client
908                .query_order(
909                    account_id,
910                    product_type,
911                    instrument_id,
912                    client_order_id,
913                    venue_order_id,
914                )
915                .await
916            {
917                Ok(Some(report)) => Python::attach(|py| report.into_py_any(py)),
918                Ok(None) => Ok(Python::attach(|py| py.None())),
919                Err(e) => Err(to_pyvalue_err(e)),
920            }
921        })
922    }
923
924    /// Request recent trade tick history for a given symbol.
925    ///
926    /// Returns the most recent public trades from Bybit's `/v5/market/recent-trade` endpoint.
927    /// This endpoint only provides recent trades (up to 1000 most recent), typically covering
928    /// only the last few minutes for active markets.
929    ///
930    /// **Note**: For historical trade data with time ranges, use the klines endpoint instead.
931    /// The Bybit public API does not support fetching historical trades by time range.
932    ///
933    /// # Errors
934    ///
935    /// Returns an error if:
936    /// - The instrument is not found in cache.
937    /// - The request fails.
938    /// - Parsing fails.
939    ///
940    /// # References
941    ///
942    /// <https://bybit-exchange.github.io/docs/v5/market/recent-trade>
943    #[pyo3(name = "request_trades")]
944    #[pyo3(signature = (product_type, instrument_id, limit=None))]
945    fn py_request_trades<'py>(
946        &self,
947        py: Python<'py>,
948        product_type: BybitProductType,
949        instrument_id: InstrumentId,
950        limit: Option<u32>,
951    ) -> PyResult<Bound<'py, PyAny>> {
952        let client = self.clone();
953
954        pyo3_async_runtimes::tokio::future_into_py(py, async move {
955            let trades = client
956                .request_trades(product_type, instrument_id, limit)
957                .await
958                .map_err(to_pyvalue_err)?;
959
960            Python::attach(|py| {
961                let py_trades: PyResult<Vec<_>> = trades
962                    .into_iter()
963                    .map(|trade| trade.into_py_any(py))
964                    .collect();
965                let pylist = PyList::new(py, py_trades?)?.into_any().unbind();
966                Ok(pylist)
967            })
968        })
969    }
970
971    /// Request funding rate history for a given symbol.
972    ///
973    /// # Errors
974    ///
975    /// Returns an error if:
976    /// - The instrument is not found in cache.
977    /// - The request fails.
978    /// - Parsing fails.
979    ///
980    /// # References
981    ///
982    /// <https://bybit-exchange.github.io/docs/v5/market/history-fund-rate>
983    #[pyo3(name = "request_funding_rates")]
984    #[pyo3(signature = (product_type, instrument_id, start=None, end=None, limit=None))]
985    fn py_request_funding_rates<'py>(
986        &self,
987        py: Python<'py>,
988        product_type: BybitProductType,
989        instrument_id: InstrumentId,
990        start: Option<Timestamp>,
991        end: Option<Timestamp>,
992        limit: Option<u32>,
993    ) -> PyResult<Bound<'py, PyAny>> {
994        let client = self.clone();
995
996        pyo3_async_runtimes::tokio::future_into_py(py, async move {
997            let funding_rates = client
998                .request_funding_rates(product_type, instrument_id, start, end, limit)
999                .await
1000                .map_err(to_pyvalue_err)?;
1001
1002            Python::attach(|py| {
1003                let py_funding_rates: PyResult<Vec<_>> = funding_rates
1004                    .into_iter()
1005                    .map(|funding_rate| funding_rate.into_py_any(py))
1006                    .collect();
1007                let pylist = PyList::new(py, py_funding_rates?)?.into_any().unbind();
1008                Ok(pylist)
1009            })
1010        })
1011    }
1012
1013    /// Request an orderbook snapshot for a given symbol.
1014    ///
1015    /// Bybit limits the amount of levels (depth) for each product type to:
1016    /// - Spot: `1..=200` (default: `1`)
1017    /// - Linear & Inverse: `1..=500` (default: `25`)
1018    /// - Options: `1..=25` (default: `1`)
1019    ///
1020    /// # Errors
1021    ///
1022    /// Returns an error if:
1023    /// - The instrument is not found in cache.
1024    /// - The request fails.
1025    /// - Parsing fails.
1026    ///
1027    /// # References
1028    ///
1029    /// <https://bybit-exchange.github.io/docs/v5/market/orderbook>
1030    #[pyo3(name = "request_orderbook_snapshot")]
1031    #[pyo3(signature = (product_type, instrument_id, limit=None))]
1032    fn py_request_orderbook_snapshot<'py>(
1033        &self,
1034        py: Python<'py>,
1035        product_type: BybitProductType,
1036        instrument_id: InstrumentId,
1037        limit: Option<u32>,
1038    ) -> PyResult<Bound<'py, PyAny>> {
1039        let client = self.clone();
1040
1041        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1042            let deltas = client
1043                .request_orderbook_snapshot(product_type, instrument_id, limit)
1044                .await
1045                .map_err(to_pyvalue_err)?;
1046
1047            Python::attach(|py| deltas.into_py_any(py))
1048        })
1049    }
1050
1051    /// Request bar/kline history for a given symbol.
1052    ///
1053    /// # Errors
1054    ///
1055    /// Returns an error if:
1056    /// - The instrument is not found in cache.
1057    /// - The request fails.
1058    /// - Parsing fails.
1059    ///
1060    /// # References
1061    ///
1062    /// <https://bybit-exchange.github.io/docs/v5/market/kline>
1063    #[pyo3(name = "request_bars")]
1064    #[pyo3(signature = (product_type, bar_type, start=None, end=None, limit=None, timestamp_on_close=true))]
1065    #[expect(clippy::too_many_arguments)]
1066    fn py_request_bars<'py>(
1067        &self,
1068        py: Python<'py>,
1069        product_type: BybitProductType,
1070        bar_type: BarType,
1071        start: Option<Timestamp>,
1072        end: Option<Timestamp>,
1073        limit: Option<u32>,
1074        timestamp_on_close: bool,
1075    ) -> PyResult<Bound<'py, PyAny>> {
1076        let client = self.clone();
1077
1078        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1079            let bars = client
1080                .request_bars(
1081                    product_type,
1082                    bar_type,
1083                    start,
1084                    end,
1085                    limit,
1086                    timestamp_on_close,
1087                )
1088                .await
1089                .map_err(to_pyvalue_err)?;
1090
1091            Python::attach(|py| {
1092                let py_bars: PyResult<Vec<_>> =
1093                    bars.into_iter().map(|bar| bar.into_py_any(py)).collect();
1094                let pylist = PyList::new(py, py_bars?)?.into_any().unbind();
1095                Ok(pylist)
1096            })
1097        })
1098    }
1099
1100    /// Requests trading fee rates for the specified product type and optional filters.
1101    ///
1102    /// # Errors
1103    ///
1104    /// Returns an error if:
1105    /// - The request fails.
1106    /// - Parsing fails.
1107    ///
1108    /// # References
1109    ///
1110    /// <https://bybit-exchange.github.io/docs/v5/account/fee-rate>
1111    #[pyo3(name = "request_fee_rates")]
1112    #[pyo3(signature = (product_type, symbol=None, base_coin=None))]
1113    fn py_request_fee_rates<'py>(
1114        &self,
1115        py: Python<'py>,
1116        product_type: BybitProductType,
1117        symbol: Option<String>,
1118        base_coin: Option<String>,
1119    ) -> PyResult<Bound<'py, PyAny>> {
1120        let client = self.clone();
1121
1122        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1123            let fee_rates = client
1124                .request_fee_rates(product_type, symbol, base_coin)
1125                .await
1126                .map_err(to_pyvalue_err)?;
1127
1128            Python::attach(|py| {
1129                let py_fee_rates: PyResult<Vec<_>> = fee_rates
1130                    .into_iter()
1131                    .map(|rate| Py::new(py, rate))
1132                    .collect();
1133                let pylist = PyList::new(py, py_fee_rates?)?.into_any().unbind();
1134                Ok(pylist)
1135            })
1136        })
1137    }
1138
1139    /// Requests the current account state for the specified account type.
1140    ///
1141    /// # Errors
1142    ///
1143    /// Returns an error if:
1144    /// - The request fails.
1145    /// - Parsing fails.
1146    ///
1147    /// # References
1148    ///
1149    /// <https://bybit-exchange.github.io/docs/v5/account/wallet-balance>
1150    #[pyo3(name = "request_account_state")]
1151    fn py_request_account_state<'py>(
1152        &self,
1153        py: Python<'py>,
1154        account_type: crate::common::enums::BybitAccountType,
1155        account_id: AccountId,
1156    ) -> PyResult<Bound<'py, PyAny>> {
1157        let client = self.clone();
1158
1159        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1160            let account_state = client
1161                .request_account_state(account_type, account_id)
1162                .await
1163                .map_err(to_pyvalue_err)?;
1164
1165            Python::attach(|py| account_state.into_py_any(py))
1166        })
1167    }
1168
1169    /// Request multiple order status reports.
1170    ///
1171    /// Orders for instruments not currently loaded in cache will be skipped.
1172    ///
1173    /// When `open_only` is true the realtime endpoint is queried for currently
1174    /// open orders and again for recently closed orders, so terminal reports
1175    /// are included. The closed pass fetches the most recent page only and is
1176    /// not constrained by `start` or `end`.
1177    ///
1178    /// # Errors
1179    ///
1180    /// Returns an error if:
1181    /// - Credentials are missing.
1182    /// - The request fails.
1183    /// - The API returns an error.
1184    #[pyo3(name = "request_order_status_reports")]
1185    #[pyo3(signature = (account_id, product_type, instrument_id=None, open_only=false, start=None, end=None, limit=None))]
1186    #[expect(clippy::too_many_arguments)]
1187    fn py_request_order_status_reports<'py>(
1188        &self,
1189        py: Python<'py>,
1190        account_id: AccountId,
1191        product_type: BybitProductType,
1192        instrument_id: Option<InstrumentId>,
1193        open_only: bool,
1194        start: Option<Timestamp>,
1195        end: Option<Timestamp>,
1196        limit: Option<u32>,
1197    ) -> PyResult<Bound<'py, PyAny>> {
1198        let client = self.clone();
1199
1200        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1201            let reports = client
1202                .request_order_status_reports(
1203                    account_id,
1204                    product_type,
1205                    instrument_id,
1206                    open_only,
1207                    start,
1208                    end,
1209                    limit,
1210                )
1211                .await
1212                .map_err(to_pyvalue_err)?;
1213
1214            Python::attach(|py| {
1215                let py_reports: PyResult<Vec<_>> = reports
1216                    .into_iter()
1217                    .map(|report| report.into_py_any(py))
1218                    .collect();
1219                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
1220                Ok(pylist)
1221            })
1222        })
1223    }
1224
1225    /// Fetches execution history (fills) for the account and returns a list of `FillReport`s.
1226    ///
1227    /// Executions for instruments not currently loaded in cache will be skipped.
1228    ///
1229    /// # Errors
1230    ///
1231    /// This function returns an error if the request fails.
1232    ///
1233    /// # References
1234    ///
1235    /// <https://bybit-exchange.github.io/docs/v5/order/execution>
1236    #[pyo3(name = "request_fill_reports")]
1237    #[pyo3(signature = (account_id, product_type, instrument_id=None, start=None, end=None, limit=None))]
1238    #[expect(clippy::too_many_arguments)]
1239    fn py_request_fill_reports<'py>(
1240        &self,
1241        py: Python<'py>,
1242        account_id: AccountId,
1243        product_type: BybitProductType,
1244        instrument_id: Option<InstrumentId>,
1245        start: Option<i64>,
1246        end: Option<i64>,
1247        limit: Option<u32>,
1248    ) -> PyResult<Bound<'py, PyAny>> {
1249        let client = self.clone();
1250
1251        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1252            let reports = client
1253                .request_fill_reports(account_id, product_type, instrument_id, start, end, limit)
1254                .await
1255                .map_err(to_pyvalue_err)?;
1256
1257            Python::attach(|py| {
1258                let py_reports: PyResult<Vec<_>> = reports
1259                    .into_iter()
1260                    .map(|report| report.into_py_any(py))
1261                    .collect();
1262                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
1263                Ok(pylist)
1264            })
1265        })
1266    }
1267
1268    /// Fetches position information for the account and returns a list of `PositionStatusReport`s.
1269    ///
1270    /// Positions for instruments not currently loaded in cache will be skipped.
1271    ///
1272    /// # Errors
1273    ///
1274    /// This function returns an error if the request fails, or if SPOT position reports are enabled
1275    /// and no instrument is specified, because wallet balances carry no pair identity.
1276    ///
1277    /// # References
1278    ///
1279    /// <https://bybit-exchange.github.io/docs/v5/position>
1280    #[pyo3(name = "request_position_status_reports")]
1281    #[pyo3(signature = (account_id, product_type, instrument_id=None))]
1282    fn py_request_position_status_reports<'py>(
1283        &self,
1284        py: Python<'py>,
1285        account_id: AccountId,
1286        product_type: BybitProductType,
1287        instrument_id: Option<InstrumentId>,
1288    ) -> PyResult<Bound<'py, PyAny>> {
1289        let client = self.clone();
1290
1291        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1292            let reports = client
1293                .request_position_status_reports(account_id, product_type, instrument_id)
1294                .await
1295                .map_err(to_pyvalue_err)?;
1296
1297            Python::attach(|py| {
1298                let py_reports: PyResult<Vec<_>> = reports
1299                    .into_iter()
1300                    .map(|report| report.into_py_any(py))
1301                    .collect();
1302                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
1303                Ok(pylist)
1304            })
1305        })
1306    }
1307}
1308
1309impl From<BybitHttpError> for PyErr {
1310    fn from(error: BybitHttpError) -> Self {
1311        match error {
1312            // Runtime/operational errors
1313            BybitHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
1314            BybitHttpError::NetworkError(msg) => to_pyruntime_err(format!("Network error: {msg}")),
1315            BybitHttpError::UnexpectedStatus { status, body } => {
1316                to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
1317            }
1318            // Validation/configuration errors
1319            BybitHttpError::MissingCredentials => {
1320                to_pyvalue_err("Missing credentials for authenticated request")
1321            }
1322            BybitHttpError::ValidationError(msg) => {
1323                to_pyvalue_err(format!("Parameter validation error: {msg}"))
1324            }
1325            BybitHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
1326            BybitHttpError::BuildError(e) => to_pyvalue_err(format!("Build error: {e}")),
1327            BybitHttpError::BybitError {
1328                error_code,
1329                message,
1330            } => to_pyvalue_err(format!("Bybit error {error_code}: {message}")),
1331        }
1332    }
1333}