1use 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 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 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 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}