1use jiff::Timestamp;
19use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err};
20use nautilus_model::{
21 data::BarType,
22 enums::{ContingencyType, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType},
23 identifiers::{AccountId, ClientOrderId, InstrumentId, OrderListId, 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::{
31 credential::credential_env_vars,
32 enums::{BitmexEnvironment, BitmexPegPriceType},
33 },
34 http::{client::BitmexHttpClient, error::BitmexHttpError},
35};
36
37#[pymethods]
38#[pyo3_stub_gen::derive::gen_stub_pymethods]
39impl BitmexHttpClient {
40 #[new]
45 #[pyo3(signature = (api_key=None, api_secret=None, base_url=None, environment=BitmexEnvironment::Mainnet, timeout_secs=60, max_retries=3, retry_delay_ms=1_000, retry_delay_max_ms=10_000, recv_window_ms=10_000, max_requests_per_second=10, max_requests_per_minute=120, proxy_url=None))]
46 #[expect(clippy::too_many_arguments)]
47 fn py_new(
48 api_key: Option<&str>,
49 api_secret: Option<&str>,
50 base_url: Option<&str>,
51 environment: BitmexEnvironment,
52 timeout_secs: u64,
53 max_retries: u32,
54 retry_delay_ms: u64,
55 retry_delay_max_ms: u64,
56 recv_window_ms: u64,
57 max_requests_per_second: u32,
58 max_requests_per_minute: u32,
59 proxy_url: Option<&str>,
60 ) -> PyResult<Self> {
61 let (final_api_key, final_api_secret) = if api_key.is_none() && api_secret.is_none() {
63 let (key_var, secret_var) = credential_env_vars(environment);
64
65 let env_key = std::env::var(key_var).ok();
66 let env_secret = std::env::var(secret_var).ok();
67 (env_key, env_secret)
68 } else {
69 (api_key.map(String::from), api_secret.map(String::from))
70 };
71
72 Self::new(
73 base_url.map(String::from),
74 final_api_key,
75 final_api_secret,
76 environment,
77 timeout_secs,
78 max_retries,
79 retry_delay_ms,
80 retry_delay_max_ms,
81 recv_window_ms,
82 max_requests_per_second,
83 max_requests_per_minute,
84 proxy_url.map(String::from),
85 )
86 .map_err(to_pyvalue_err)
87 }
88
89 #[staticmethod]
96 #[pyo3(name = "from_env")]
97 fn py_from_env() -> PyResult<Self> {
98 Self::from_env().map_err(to_pyvalue_err)
99 }
100
101 #[getter]
103 #[pyo3(name = "base_url")]
104 #[must_use]
105 pub fn py_base_url(&self) -> &str {
106 self.base_url()
107 }
108
109 #[getter]
111 #[pyo3(name = "api_key")]
112 #[must_use]
113 pub fn py_api_key(&self) -> Option<&str> {
114 self.api_key()
115 }
116
117 #[getter]
119 #[pyo3(name = "api_key_masked")]
120 #[must_use]
121 pub fn py_api_key_masked(&self) -> Option<String> {
122 self.api_key_masked()
123 }
124
125 #[pyo3(name = "update_position_leverage")]
133 fn py_update_position_leverage<'py>(
134 &self,
135 py: Python<'py>,
136 _symbol: String,
137 _leverage: f64,
138 ) -> PyResult<Bound<'py, PyAny>> {
139 let _client = self.clone();
140
141 pyo3_async_runtimes::tokio::future_into_py(py, async move {
142 Python::attach(|py| -> PyResult<Py<PyAny>> {
148 Ok(py.None())
150 })
151 })
152 }
153
154 #[pyo3(name = "request_instrument")]
162 fn py_request_instrument<'py>(
163 &self,
164 py: Python<'py>,
165 instrument_id: InstrumentId,
166 ) -> PyResult<Bound<'py, PyAny>> {
167 let client = self.clone();
168
169 pyo3_async_runtimes::tokio::future_into_py(py, async move {
170 let instrument = client
171 .request_instrument(instrument_id)
172 .await
173 .map_err(to_pyvalue_err)?;
174
175 Python::attach(|py| match instrument {
176 Some(inst) => instrument_any_to_pyobject(py, inst),
177 None => Ok(py.None()),
178 })
179 })
180 }
181
182 #[pyo3(name = "request_instruments")]
188 fn py_request_instruments<'py>(
189 &self,
190 py: Python<'py>,
191 active_only: bool,
192 ) -> PyResult<Bound<'py, PyAny>> {
193 let client = self.clone();
194
195 pyo3_async_runtimes::tokio::future_into_py(py, async move {
196 let instruments = client
197 .request_instruments(active_only)
198 .await
199 .map_err(to_pyvalue_err)?;
200
201 Python::attach(|py| {
202 let py_instruments: PyResult<Vec<_>> = instruments
203 .into_iter()
204 .map(|inst| instrument_any_to_pyobject(py, inst))
205 .collect();
206 let pylist = PyList::new(py, py_instruments?)?.into_any().unbind();
207 Ok(pylist)
208 })
209 })
210 }
211
212 #[pyo3(name = "request_trades")]
218 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
219 fn py_request_trades<'py>(
220 &self,
221 py: Python<'py>,
222 instrument_id: InstrumentId,
223 start: Option<Timestamp>,
224 end: Option<Timestamp>,
225 limit: Option<u32>,
226 ) -> PyResult<Bound<'py, PyAny>> {
227 let client = self.clone();
228
229 pyo3_async_runtimes::tokio::future_into_py(py, async move {
230 let trades = client
231 .request_trades(instrument_id, start, end, limit)
232 .await
233 .map_err(to_pyvalue_err)?;
234
235 Python::attach(|py| {
236 let py_trades: PyResult<Vec<_>> = trades
237 .into_iter()
238 .map(|trade| trade.into_py_any(py))
239 .collect();
240 let pylist = PyList::new(py, py_trades?)?.into_any().unbind();
241 Ok(pylist)
242 })
243 })
244 }
245
246 #[pyo3(name = "request_bars")]
253 #[pyo3(signature = (bar_type, start=None, end=None, limit=None, partial=false))]
254 fn py_request_bars<'py>(
255 &self,
256 py: Python<'py>,
257 bar_type: BarType,
258 start: Option<Timestamp>,
259 end: Option<Timestamp>,
260 limit: Option<u32>,
261 partial: bool,
262 ) -> PyResult<Bound<'py, PyAny>> {
263 let client = self.clone();
264
265 pyo3_async_runtimes::tokio::future_into_py(py, async move {
266 let bars = client
267 .request_bars(bar_type, start, end, limit, partial)
268 .await
269 .map_err(to_pyvalue_err)?;
270
271 Python::attach(|py| {
272 let py_bars: PyResult<Vec<_>> =
273 bars.into_iter().map(|bar| bar.into_py_any(py)).collect();
274 let pylist = PyList::new(py, py_bars?)?.into_any().unbind();
275 Ok(pylist)
276 })
277 })
278 }
279
280 #[pyo3(name = "request_book_snapshot")]
287 #[pyo3(signature = (instrument_id, depth=None))]
288 fn py_request_book_snapshot<'py>(
289 &self,
290 py: Python<'py>,
291 instrument_id: InstrumentId,
292 depth: Option<u32>,
293 ) -> PyResult<Bound<'py, PyAny>> {
294 let client = self.clone();
295
296 pyo3_async_runtimes::tokio::future_into_py(py, async move {
297 let book = client
298 .request_book_snapshot(instrument_id, depth)
299 .await
300 .map_err(to_pyvalue_err)?;
301
302 Python::attach(|py| book.into_py_any(py))
303 })
304 }
305
306 #[pyo3(name = "request_funding_rates")]
312 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
313 fn py_request_funding_rates<'py>(
314 &self,
315 py: Python<'py>,
316 instrument_id: InstrumentId,
317 start: Option<Timestamp>,
318 end: Option<Timestamp>,
319 limit: Option<u32>,
320 ) -> PyResult<Bound<'py, PyAny>> {
321 let client = self.clone();
322
323 pyo3_async_runtimes::tokio::future_into_py(py, async move {
324 let rates = client
325 .request_funding_rates(instrument_id, start, end, limit)
326 .await
327 .map_err(to_pyvalue_err)?;
328
329 Python::attach(|py| {
330 let py_rates: PyResult<Vec<_>> =
331 rates.into_iter().map(|rate| rate.into_py_any(py)).collect();
332 let pylist = PyList::new(py, py_rates?)?.into_any().unbind();
333 Ok(pylist)
334 })
335 })
336 }
337
338 #[pyo3(name = "query_order")]
347 #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
348 fn py_query_order<'py>(
349 &self,
350 py: Python<'py>,
351 instrument_id: InstrumentId,
352 client_order_id: Option<ClientOrderId>,
353 venue_order_id: Option<VenueOrderId>,
354 ) -> PyResult<Bound<'py, PyAny>> {
355 let client = self.clone();
356
357 pyo3_async_runtimes::tokio::future_into_py(py, async move {
358 match client
359 .query_order(instrument_id, client_order_id, venue_order_id)
360 .await
361 {
362 Ok(Some(report)) => Python::attach(|py| report.into_py_any(py)),
363 Ok(None) => Ok(Python::attach(|py| py.None())),
364 Err(e) => Err(to_pyvalue_err(e)),
365 }
366 })
367 }
368
369 #[pyo3(name = "request_order_status_reports")]
378 #[pyo3(signature = (instrument_id=None, open_only=false, limit=None))]
379 fn py_request_order_status_reports<'py>(
380 &self,
381 py: Python<'py>,
382 instrument_id: Option<InstrumentId>,
383 open_only: bool,
384 limit: Option<u32>,
385 ) -> PyResult<Bound<'py, PyAny>> {
386 let client = self.clone();
387
388 pyo3_async_runtimes::tokio::future_into_py(py, async move {
389 let reports = client
390 .request_order_status_reports(instrument_id, open_only, None, None, limit)
391 .await
392 .map_err(to_pyvalue_err)?;
393
394 Python::attach(|py| {
395 let py_reports: PyResult<Vec<_>> = reports
396 .into_iter()
397 .map(|report| report.into_py_any(py))
398 .collect();
399 let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
400 Ok(pylist)
401 })
402 })
403 }
404
405 #[pyo3(name = "request_fill_reports")]
411 #[pyo3(signature = (instrument_id=None, limit=None))]
412 fn py_request_fill_reports<'py>(
413 &self,
414 py: Python<'py>,
415 instrument_id: Option<InstrumentId>,
416 limit: Option<u32>,
417 ) -> PyResult<Bound<'py, PyAny>> {
418 let client = self.clone();
419
420 pyo3_async_runtimes::tokio::future_into_py(py, async move {
421 let reports = client
422 .request_fill_reports(instrument_id, None, None, limit)
423 .await
424 .map_err(to_pyvalue_err)?;
425
426 Python::attach(|py| {
427 let py_reports: PyResult<Vec<_>> = reports
428 .into_iter()
429 .map(|report| report.into_py_any(py))
430 .collect();
431 let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
432 Ok(pylist)
433 })
434 })
435 }
436
437 #[pyo3(name = "request_position_status_reports")]
443 fn py_request_position_status_reports<'py>(
444 &self,
445 py: Python<'py>,
446 ) -> PyResult<Bound<'py, PyAny>> {
447 let client = self.clone();
448
449 pyo3_async_runtimes::tokio::future_into_py(py, async move {
450 let reports = client
451 .request_position_status_reports()
452 .await
453 .map_err(to_pyvalue_err)?;
454
455 Python::attach(|py| {
456 let py_reports: PyResult<Vec<_>> = reports
457 .into_iter()
458 .map(|report| report.into_py_any(py))
459 .collect();
460 let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
461 Ok(pylist)
462 })
463 })
464 }
465
466 #[pyo3(name = "submit_order")]
473 #[pyo3(signature = (
474 instrument_id,
475 client_order_id,
476 order_side,
477 order_type,
478 quantity,
479 time_in_force,
480 price = None,
481 trigger_price = None,
482 trigger_type = None,
483 trailing_offset = None,
484 trailing_offset_type = None,
485 display_qty = None,
486 post_only = false,
487 reduce_only = false,
488 order_list_id = None,
489 contingency_type = None,
490 peg_price_type = None,
491 peg_offset_value = None
492 ))]
493 #[expect(clippy::too_many_arguments)]
494 fn py_submit_order<'py>(
495 &self,
496 py: Python<'py>,
497 instrument_id: InstrumentId,
498 client_order_id: ClientOrderId,
499 order_side: OrderSide,
500 order_type: OrderType,
501 quantity: Quantity,
502 time_in_force: TimeInForce,
503 price: Option<Price>,
504 trigger_price: Option<Price>,
505 trigger_type: Option<TriggerType>,
506 trailing_offset: Option<f64>,
507 trailing_offset_type: Option<TrailingOffsetType>,
508 display_qty: Option<Quantity>,
509 post_only: bool,
510 reduce_only: bool,
511 order_list_id: Option<OrderListId>,
512 contingency_type: Option<ContingencyType>,
513 peg_price_type: Option<String>,
514 peg_offset_value: Option<f64>,
515 ) -> PyResult<Bound<'py, PyAny>> {
516 let client = self.clone();
517
518 let peg_price_type: Option<BitmexPegPriceType> = peg_price_type
519 .map(|s| {
520 s.parse::<BitmexPegPriceType>()
521 .map_err(|_| to_pyvalue_err(format!("Invalid peg_price_type: {s}")))
522 })
523 .transpose()?;
524
525 pyo3_async_runtimes::tokio::future_into_py(py, async move {
526 let report = client
527 .submit_order(
528 instrument_id,
529 client_order_id,
530 order_side,
531 order_type,
532 quantity,
533 time_in_force,
534 price,
535 trigger_price,
536 trigger_type,
537 trailing_offset,
538 trailing_offset_type,
539 display_qty,
540 post_only,
541 reduce_only,
542 order_list_id,
543 contingency_type,
544 peg_price_type,
545 peg_offset_value,
546 )
547 .await
548 .map_err(to_pyvalue_err)?;
549
550 Python::attach(|py| report.into_py_any(py))
551 })
552 }
553
554 #[pyo3(name = "cancel_order")]
564 #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
565 fn py_cancel_order<'py>(
566 &self,
567 py: Python<'py>,
568 instrument_id: InstrumentId,
569 client_order_id: Option<ClientOrderId>,
570 venue_order_id: Option<VenueOrderId>,
571 ) -> PyResult<Bound<'py, PyAny>> {
572 let client = self.clone();
573
574 pyo3_async_runtimes::tokio::future_into_py(py, async move {
575 let report = client
576 .cancel_order(instrument_id, client_order_id, venue_order_id)
577 .await
578 .map_err(to_pyvalue_err)?;
579
580 Python::attach(|py| report.into_py_any(py))
581 })
582 }
583
584 #[pyo3(name = "cancel_orders")]
594 #[pyo3(signature = (instrument_id, client_order_ids=None, venue_order_ids=None))]
595 fn py_cancel_orders<'py>(
596 &self,
597 py: Python<'py>,
598 instrument_id: InstrumentId,
599 client_order_ids: Option<Vec<ClientOrderId>>,
600 venue_order_ids: Option<Vec<VenueOrderId>>,
601 ) -> PyResult<Bound<'py, PyAny>> {
602 let client = self.clone();
603
604 pyo3_async_runtimes::tokio::future_into_py(py, async move {
605 let reports = client
606 .cancel_orders(instrument_id, client_order_ids, venue_order_ids)
607 .await
608 .map_err(to_pyvalue_err)?;
609
610 Python::attach(|py| {
611 let py_reports: PyResult<Vec<_>> = reports
612 .into_iter()
613 .map(|report| report.into_py_any(py))
614 .collect();
615 let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
616 Ok(pylist)
617 })
618 })
619 }
620
621 #[pyo3(name = "cancel_all_orders")]
631 #[pyo3(signature = (instrument_id, order_side))]
632 fn py_cancel_all_orders<'py>(
633 &self,
634 py: Python<'py>,
635 instrument_id: InstrumentId,
636 order_side: Option<OrderSide>,
637 ) -> PyResult<Bound<'py, PyAny>> {
638 let client = self.clone();
639
640 pyo3_async_runtimes::tokio::future_into_py(py, async move {
641 let reports = client
642 .cancel_all_orders(instrument_id, order_side)
643 .await
644 .map_err(to_pyvalue_err)?;
645
646 Python::attach(|py| {
647 let py_reports: PyResult<Vec<_>> = reports
648 .into_iter()
649 .map(|report| report.into_py_any(py))
650 .collect();
651 let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
652 Ok(pylist)
653 })
654 })
655 }
656
657 #[pyo3(name = "modify_order")]
668 #[pyo3(signature = (
669 instrument_id,
670 client_order_id=None,
671 venue_order_id=None,
672 quantity=None,
673 price=None,
674 trigger_price=None
675 ))]
676 #[expect(clippy::too_many_arguments)]
677 fn py_modify_order<'py>(
678 &self,
679 py: Python<'py>,
680 instrument_id: InstrumentId,
681 client_order_id: Option<ClientOrderId>,
682 venue_order_id: Option<VenueOrderId>,
683 quantity: Option<Quantity>,
684 price: Option<Price>,
685 trigger_price: Option<Price>,
686 ) -> PyResult<Bound<'py, PyAny>> {
687 let client = self.clone();
688
689 pyo3_async_runtimes::tokio::future_into_py(py, async move {
690 let report = client
691 .modify_order(
692 instrument_id,
693 client_order_id,
694 venue_order_id,
695 quantity,
696 price,
697 trigger_price,
698 )
699 .await
700 .map_err(to_pyvalue_err)?;
701
702 Python::attach(|py| report.into_py_any(py))
703 })
704 }
705
706 #[pyo3(name = "cache_instrument")]
710 fn py_cache_instrument(&mut self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
711 let inst_any = pyobject_to_instrument_any(py, instrument)?;
712 self.cache_instrument(inst_any);
713 Ok(())
714 }
715
716 #[pyo3(name = "cancel_all_requests")]
718 fn py_cancel_all_requests(&self) {
719 self.cancel_all_requests();
720 }
721
722 #[pyo3(name = "get_margin")]
728 fn py_get_margin<'py>(&self, py: Python<'py>, currency: String) -> PyResult<Bound<'py, PyAny>> {
729 let client = self.clone();
730
731 pyo3_async_runtimes::tokio::future_into_py(py, async move {
732 let margin = client.get_margin(¤cy).await.map_err(to_pyvalue_err)?;
733
734 Python::attach(|py| {
735 let account = margin.account;
738 account.into_py_any(py)
739 })
740 })
741 }
742
743 #[pyo3(name = "get_account_number")]
744 fn py_get_account_number<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
745 let client = self.clone();
746
747 pyo3_async_runtimes::tokio::future_into_py(py, async move {
748 let margins = client.get_all_margins().await.map_err(to_pyvalue_err)?;
749
750 Python::attach(|py| {
751 let account = margins.first().map(|m| m.account);
753 account.into_py_any(py)
754 })
755 })
756 }
757
758 #[pyo3(name = "request_account_state")]
764 fn py_request_account_state<'py>(
765 &self,
766 py: Python<'py>,
767 account_id: AccountId,
768 ) -> PyResult<Bound<'py, PyAny>> {
769 let client = self.clone();
770
771 pyo3_async_runtimes::tokio::future_into_py(py, async move {
772 let account_state = client
773 .request_account_state(account_id)
774 .await
775 .map_err(to_pyvalue_err)?;
776
777 Python::attach(|py| account_state.into_py_any(py).map_err(to_pyvalue_err))
778 })
779 }
780
781 #[pyo3(name = "submit_orders_bulk")]
782 fn py_submit_orders_bulk<'py>(
783 &self,
784 py: Python<'py>,
785 orders: Vec<Py<PyAny>>,
786 ) -> PyResult<Bound<'py, PyAny>> {
787 let _client = self.clone();
788
789 let _params = Python::attach(|_py| {
791 orders
792 .into_iter()
793 .map(|obj| {
794 Ok(obj)
797 })
798 .collect::<PyResult<Vec<_>>>()
799 })?;
800
801 pyo3_async_runtimes::tokio::future_into_py(py, async move {
802 Python::attach(|py| -> PyResult<Py<PyAny>> {
806 let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
807 Ok(py_list.into())
811 })
812 })
813 }
814
815 #[pyo3(name = "modify_orders_bulk")]
816 fn py_modify_orders_bulk<'py>(
817 &self,
818 py: Python<'py>,
819 orders: Vec<Py<PyAny>>,
820 ) -> PyResult<Bound<'py, PyAny>> {
821 let _client = self.clone();
822
823 let _params = Python::attach(|_py| {
825 orders
826 .into_iter()
827 .map(|obj| {
828 Ok(obj)
831 })
832 .collect::<PyResult<Vec<_>>>()
833 })?;
834
835 pyo3_async_runtimes::tokio::future_into_py(py, async move {
836 Python::attach(|py| -> PyResult<Py<PyAny>> {
840 let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
841 Ok(py_list.into())
845 })
846 })
847 }
848
849 #[pyo3(name = "cancel_all_after")]
857 fn py_cancel_all_after<'py>(
858 &self,
859 py: Python<'py>,
860 timeout_ms: u64,
861 ) -> PyResult<Bound<'py, PyAny>> {
862 let client = self.clone();
863
864 pyo3_async_runtimes::tokio::future_into_py(py, async move {
865 client
866 .cancel_all_after(timeout_ms)
867 .await
868 .map_err(to_pyvalue_err)?;
869
870 Ok(Python::attach(|py| py.None()))
871 })
872 }
873
874 #[pyo3(name = "get_server_time")]
882 fn py_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
883 let client = self.clone();
884
885 pyo3_async_runtimes::tokio::future_into_py(py, async move {
886 let timestamp = client.get_server_time().await.map_err(to_pyvalue_err)?;
887
888 Python::attach(|py| timestamp.into_py_any(py))
889 })
890 }
891}
892
893impl From<BitmexHttpError> for PyErr {
894 fn from(error: BitmexHttpError) -> Self {
895 match error {
896 BitmexHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
898 BitmexHttpError::NetworkError(msg) => to_pyruntime_err(format!("Network error: {msg}")),
899 BitmexHttpError::UnexpectedStatus { status, body } => {
900 to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
901 }
902 BitmexHttpError::MissingCredentials => {
904 to_pyvalue_err("Missing credentials for authenticated request")
905 }
906 BitmexHttpError::ValidationError(msg) => {
907 to_pyvalue_err(format!("Parameter validation error: {msg}"))
908 }
909 BitmexHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
910 BitmexHttpError::BuildError(e) => to_pyvalue_err(format!("Build error: {e}")),
911 BitmexHttpError::BitmexError {
912 error_name,
913 message,
914 } => to_pyvalue_err(format!("BitMEX error {error_name}: {message}")),
915 }
916 }
917}