nautilus_bybit/python/
http.rs

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