1use chrono::{DateTime, Utc};
19use nautilus_core::python::{IntoPyObjectNautilusExt, 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")]
127 fn py_update_position_leverage<'py>(
128 &self,
129 py: Python<'py>,
130 _symbol: String,
131 _leverage: f64,
132 ) -> PyResult<Bound<'py, PyAny>> {
133 let _client = self.clone();
134
135 pyo3_async_runtimes::tokio::future_into_py(py, async move {
136 Python::attach(|py| -> PyResult<Py<PyAny>> {
142 Ok(py.None())
144 })
145 })
146 }
147
148 #[pyo3(name = "request_instrument")]
150 fn py_request_instrument<'py>(
151 &self,
152 py: Python<'py>,
153 instrument_id: InstrumentId,
154 ) -> PyResult<Bound<'py, PyAny>> {
155 let client = self.clone();
156
157 pyo3_async_runtimes::tokio::future_into_py(py, async move {
158 let instrument = client
159 .request_instrument(instrument_id)
160 .await
161 .map_err(to_pyvalue_err)?;
162
163 Python::attach(|py| match instrument {
164 Some(inst) => instrument_any_to_pyobject(py, inst),
165 None => Ok(py.None()),
166 })
167 })
168 }
169
170 #[pyo3(name = "request_instruments")]
172 fn py_request_instruments<'py>(
173 &self,
174 py: Python<'py>,
175 active_only: bool,
176 ) -> PyResult<Bound<'py, PyAny>> {
177 let client = self.clone();
178
179 pyo3_async_runtimes::tokio::future_into_py(py, async move {
180 let instruments = client
181 .request_instruments(active_only)
182 .await
183 .map_err(to_pyvalue_err)?;
184
185 Python::attach(|py| {
186 let py_instruments: PyResult<Vec<_>> = instruments
187 .into_iter()
188 .map(|inst| instrument_any_to_pyobject(py, inst))
189 .collect();
190 let pylist = PyList::new(py, py_instruments?)
191 .unwrap()
192 .into_any()
193 .unbind();
194 Ok(pylist)
195 })
196 })
197 }
198
199 #[pyo3(name = "request_trades")]
201 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
202 fn py_request_trades<'py>(
203 &self,
204 py: Python<'py>,
205 instrument_id: InstrumentId,
206 start: Option<DateTime<Utc>>,
207 end: Option<DateTime<Utc>>,
208 limit: Option<u32>,
209 ) -> PyResult<Bound<'py, PyAny>> {
210 let client = self.clone();
211
212 pyo3_async_runtimes::tokio::future_into_py(py, async move {
213 let trades = client
214 .request_trades(instrument_id, start, end, limit)
215 .await
216 .map_err(to_pyvalue_err)?;
217
218 Python::attach(|py| {
219 let py_trades: PyResult<Vec<_>> = trades
220 .into_iter()
221 .map(|trade| trade.into_py_any(py))
222 .collect();
223 let pylist = PyList::new(py, py_trades?).unwrap().into_any().unbind();
224 Ok(pylist)
225 })
226 })
227 }
228
229 #[pyo3(name = "request_bars")]
231 #[pyo3(signature = (bar_type, start=None, end=None, limit=None, partial=false))]
232 fn py_request_bars<'py>(
233 &self,
234 py: Python<'py>,
235 bar_type: BarType,
236 start: Option<DateTime<Utc>>,
237 end: Option<DateTime<Utc>>,
238 limit: Option<u32>,
239 partial: bool,
240 ) -> PyResult<Bound<'py, PyAny>> {
241 let client = self.clone();
242
243 pyo3_async_runtimes::tokio::future_into_py(py, async move {
244 let bars = client
245 .request_bars(bar_type, start, end, limit, partial)
246 .await
247 .map_err(to_pyvalue_err)?;
248
249 Python::attach(|py| {
250 let py_bars: PyResult<Vec<_>> =
251 bars.into_iter().map(|bar| bar.into_py_any(py)).collect();
252 let pylist = PyList::new(py, py_bars?).unwrap().into_any().unbind();
253 Ok(pylist)
254 })
255 })
256 }
257
258 #[pyo3(name = "request_book_snapshot")]
260 #[pyo3(signature = (instrument_id, depth=None))]
261 fn py_request_book_snapshot<'py>(
262 &self,
263 py: Python<'py>,
264 instrument_id: InstrumentId,
265 depth: Option<u32>,
266 ) -> PyResult<Bound<'py, PyAny>> {
267 let client = self.clone();
268
269 pyo3_async_runtimes::tokio::future_into_py(py, async move {
270 let book = client
271 .request_book_snapshot(instrument_id, depth)
272 .await
273 .map_err(to_pyvalue_err)?;
274
275 Python::attach(|py| Ok(book.into_py_any_unwrap(py)))
276 })
277 }
278
279 #[pyo3(name = "request_funding_rates")]
281 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
282 fn py_request_funding_rates<'py>(
283 &self,
284 py: Python<'py>,
285 instrument_id: InstrumentId,
286 start: Option<DateTime<Utc>>,
287 end: Option<DateTime<Utc>>,
288 limit: Option<u32>,
289 ) -> PyResult<Bound<'py, PyAny>> {
290 let client = self.clone();
291
292 pyo3_async_runtimes::tokio::future_into_py(py, async move {
293 let rates = client
294 .request_funding_rates(instrument_id, start, end, limit)
295 .await
296 .map_err(to_pyvalue_err)?;
297
298 Python::attach(|py| {
299 let py_rates: PyResult<Vec<_>> =
300 rates.into_iter().map(|rate| rate.into_py_any(py)).collect();
301 let pylist = PyList::new(py, py_rates?).unwrap().into_any().unbind();
302 Ok(pylist)
303 })
304 })
305 }
306
307 #[pyo3(name = "query_order")]
309 #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
310 fn py_query_order<'py>(
311 &self,
312 py: Python<'py>,
313 instrument_id: InstrumentId,
314 client_order_id: Option<ClientOrderId>,
315 venue_order_id: Option<VenueOrderId>,
316 ) -> PyResult<Bound<'py, PyAny>> {
317 let client = self.clone();
318
319 pyo3_async_runtimes::tokio::future_into_py(py, async move {
320 match client
321 .query_order(instrument_id, client_order_id, venue_order_id)
322 .await
323 {
324 Ok(Some(report)) => Python::attach(|py| report.into_py_any(py)),
325 Ok(None) => Ok(Python::attach(|py| py.None())),
326 Err(e) => Err(to_pyvalue_err(e)),
327 }
328 })
329 }
330
331 #[pyo3(name = "request_order_status_reports")]
333 #[pyo3(signature = (instrument_id=None, open_only=false, limit=None))]
334 fn py_request_order_status_reports<'py>(
335 &self,
336 py: Python<'py>,
337 instrument_id: Option<InstrumentId>,
338 open_only: bool,
339 limit: Option<u32>,
340 ) -> PyResult<Bound<'py, PyAny>> {
341 let client = self.clone();
342
343 pyo3_async_runtimes::tokio::future_into_py(py, async move {
344 let reports = client
345 .request_order_status_reports(instrument_id, open_only, None, None, limit)
346 .await
347 .map_err(to_pyvalue_err)?;
348
349 Python::attach(|py| {
350 let py_reports: PyResult<Vec<_>> = reports
351 .into_iter()
352 .map(|report| report.into_py_any(py))
353 .collect();
354 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
355 Ok(pylist)
356 })
357 })
358 }
359
360 #[pyo3(name = "request_fill_reports")]
362 #[pyo3(signature = (instrument_id=None, limit=None))]
363 fn py_request_fill_reports<'py>(
364 &self,
365 py: Python<'py>,
366 instrument_id: Option<InstrumentId>,
367 limit: Option<u32>,
368 ) -> PyResult<Bound<'py, PyAny>> {
369 let client = self.clone();
370
371 pyo3_async_runtimes::tokio::future_into_py(py, async move {
372 let reports = client
373 .request_fill_reports(instrument_id, None, None, limit)
374 .await
375 .map_err(to_pyvalue_err)?;
376
377 Python::attach(|py| {
378 let py_reports: PyResult<Vec<_>> = reports
379 .into_iter()
380 .map(|report| report.into_py_any(py))
381 .collect();
382 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
383 Ok(pylist)
384 })
385 })
386 }
387
388 #[pyo3(name = "request_position_status_reports")]
390 fn py_request_position_status_reports<'py>(
391 &self,
392 py: Python<'py>,
393 ) -> PyResult<Bound<'py, PyAny>> {
394 let client = self.clone();
395
396 pyo3_async_runtimes::tokio::future_into_py(py, async move {
397 let reports = client
398 .request_position_status_reports()
399 .await
400 .map_err(to_pyvalue_err)?;
401
402 Python::attach(|py| {
403 let py_reports: PyResult<Vec<_>> = reports
404 .into_iter()
405 .map(|report| report.into_py_any(py))
406 .collect();
407 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
408 Ok(pylist)
409 })
410 })
411 }
412
413 #[pyo3(name = "submit_order")]
415 #[pyo3(signature = (
416 instrument_id,
417 client_order_id,
418 order_side,
419 order_type,
420 quantity,
421 time_in_force,
422 price = None,
423 trigger_price = None,
424 trigger_type = None,
425 trailing_offset = None,
426 trailing_offset_type = None,
427 display_qty = None,
428 post_only = false,
429 reduce_only = false,
430 order_list_id = None,
431 contingency_type = None,
432 peg_price_type = None,
433 peg_offset_value = None
434 ))]
435 #[expect(clippy::too_many_arguments)]
436 fn py_submit_order<'py>(
437 &self,
438 py: Python<'py>,
439 instrument_id: InstrumentId,
440 client_order_id: ClientOrderId,
441 order_side: OrderSide,
442 order_type: OrderType,
443 quantity: Quantity,
444 time_in_force: TimeInForce,
445 price: Option<Price>,
446 trigger_price: Option<Price>,
447 trigger_type: Option<TriggerType>,
448 trailing_offset: Option<f64>,
449 trailing_offset_type: Option<TrailingOffsetType>,
450 display_qty: Option<Quantity>,
451 post_only: bool,
452 reduce_only: bool,
453 order_list_id: Option<OrderListId>,
454 contingency_type: Option<ContingencyType>,
455 peg_price_type: Option<String>,
456 peg_offset_value: Option<f64>,
457 ) -> PyResult<Bound<'py, PyAny>> {
458 let client = self.clone();
459
460 let peg_price_type: Option<BitmexPegPriceType> = peg_price_type
461 .map(|s| {
462 s.parse::<BitmexPegPriceType>()
463 .map_err(|_| to_pyvalue_err(format!("Invalid peg_price_type: {s}")))
464 })
465 .transpose()?;
466
467 pyo3_async_runtimes::tokio::future_into_py(py, async move {
468 let report = client
469 .submit_order(
470 instrument_id,
471 client_order_id,
472 order_side,
473 order_type,
474 quantity,
475 time_in_force,
476 price,
477 trigger_price,
478 trigger_type,
479 trailing_offset,
480 trailing_offset_type,
481 display_qty,
482 post_only,
483 reduce_only,
484 order_list_id,
485 contingency_type,
486 peg_price_type,
487 peg_offset_value,
488 )
489 .await
490 .map_err(to_pyvalue_err)?;
491
492 Python::attach(|py| report.into_py_any(py))
493 })
494 }
495
496 #[pyo3(name = "cancel_order")]
498 #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
499 fn py_cancel_order<'py>(
500 &self,
501 py: Python<'py>,
502 instrument_id: InstrumentId,
503 client_order_id: Option<ClientOrderId>,
504 venue_order_id: Option<VenueOrderId>,
505 ) -> PyResult<Bound<'py, PyAny>> {
506 let client = self.clone();
507
508 pyo3_async_runtimes::tokio::future_into_py(py, async move {
509 let report = client
510 .cancel_order(instrument_id, client_order_id, venue_order_id)
511 .await
512 .map_err(to_pyvalue_err)?;
513
514 Python::attach(|py| report.into_py_any(py))
515 })
516 }
517
518 #[pyo3(name = "cancel_orders")]
520 #[pyo3(signature = (instrument_id, client_order_ids=None, venue_order_ids=None))]
521 fn py_cancel_orders<'py>(
522 &self,
523 py: Python<'py>,
524 instrument_id: InstrumentId,
525 client_order_ids: Option<Vec<ClientOrderId>>,
526 venue_order_ids: Option<Vec<VenueOrderId>>,
527 ) -> PyResult<Bound<'py, PyAny>> {
528 let client = self.clone();
529
530 pyo3_async_runtimes::tokio::future_into_py(py, async move {
531 let reports = client
532 .cancel_orders(instrument_id, client_order_ids, venue_order_ids)
533 .await
534 .map_err(to_pyvalue_err)?;
535
536 Python::attach(|py| {
537 let py_reports: PyResult<Vec<_>> = reports
538 .into_iter()
539 .map(|report| report.into_py_any(py))
540 .collect();
541 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
542 Ok(pylist)
543 })
544 })
545 }
546
547 #[pyo3(name = "cancel_all_orders")]
549 #[pyo3(signature = (instrument_id, order_side))]
550 fn py_cancel_all_orders<'py>(
551 &self,
552 py: Python<'py>,
553 instrument_id: InstrumentId,
554 order_side: Option<OrderSide>,
555 ) -> PyResult<Bound<'py, PyAny>> {
556 let client = self.clone();
557
558 pyo3_async_runtimes::tokio::future_into_py(py, async move {
559 let reports = client
560 .cancel_all_orders(instrument_id, order_side)
561 .await
562 .map_err(to_pyvalue_err)?;
563
564 Python::attach(|py| {
565 let py_reports: PyResult<Vec<_>> = reports
566 .into_iter()
567 .map(|report| report.into_py_any(py))
568 .collect();
569 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
570 Ok(pylist)
571 })
572 })
573 }
574
575 #[pyo3(name = "modify_order")]
577 #[pyo3(signature = (
578 instrument_id,
579 client_order_id=None,
580 venue_order_id=None,
581 quantity=None,
582 price=None,
583 trigger_price=None
584 ))]
585 #[expect(clippy::too_many_arguments)]
586 fn py_modify_order<'py>(
587 &self,
588 py: Python<'py>,
589 instrument_id: InstrumentId,
590 client_order_id: Option<ClientOrderId>,
591 venue_order_id: Option<VenueOrderId>,
592 quantity: Option<Quantity>,
593 price: Option<Price>,
594 trigger_price: Option<Price>,
595 ) -> PyResult<Bound<'py, PyAny>> {
596 let client = self.clone();
597
598 pyo3_async_runtimes::tokio::future_into_py(py, async move {
599 let report = client
600 .modify_order(
601 instrument_id,
602 client_order_id,
603 venue_order_id,
604 quantity,
605 price,
606 trigger_price,
607 )
608 .await
609 .map_err(to_pyvalue_err)?;
610
611 Python::attach(|py| report.into_py_any(py))
612 })
613 }
614
615 #[pyo3(name = "cache_instrument")]
619 fn py_cache_instrument(&mut self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
620 let inst_any = pyobject_to_instrument_any(py, instrument)?;
621 self.cache_instrument(inst_any);
622 Ok(())
623 }
624
625 #[pyo3(name = "cancel_all_requests")]
627 fn py_cancel_all_requests(&self) {
628 self.cancel_all_requests();
629 }
630
631 #[pyo3(name = "get_margin")]
637 fn py_get_margin<'py>(&self, py: Python<'py>, currency: String) -> PyResult<Bound<'py, PyAny>> {
638 let client = self.clone();
639
640 pyo3_async_runtimes::tokio::future_into_py(py, async move {
641 let margin = client.get_margin(¤cy).await.map_err(to_pyvalue_err)?;
642
643 Python::attach(|py| {
644 let account = margin.account;
647 account.into_py_any(py)
648 })
649 })
650 }
651
652 #[pyo3(name = "get_account_number")]
653 fn py_get_account_number<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
654 let client = self.clone();
655
656 pyo3_async_runtimes::tokio::future_into_py(py, async move {
657 let margins = client.get_all_margins().await.map_err(to_pyvalue_err)?;
658
659 Python::attach(|py| {
660 let account = margins.first().map(|m| m.account);
662 account.into_py_any(py)
663 })
664 })
665 }
666
667 #[pyo3(name = "request_account_state")]
669 fn py_request_account_state<'py>(
670 &self,
671 py: Python<'py>,
672 account_id: AccountId,
673 ) -> PyResult<Bound<'py, PyAny>> {
674 let client = self.clone();
675
676 pyo3_async_runtimes::tokio::future_into_py(py, async move {
677 let account_state = client
678 .request_account_state(account_id)
679 .await
680 .map_err(to_pyvalue_err)?;
681
682 Python::attach(|py| account_state.into_py_any(py).map_err(to_pyvalue_err))
683 })
684 }
685
686 #[pyo3(name = "submit_orders_bulk")]
687 fn py_submit_orders_bulk<'py>(
688 &self,
689 py: Python<'py>,
690 orders: Vec<Py<PyAny>>,
691 ) -> PyResult<Bound<'py, PyAny>> {
692 let _client = self.clone();
693
694 let _params = Python::attach(|_py| {
696 orders
697 .into_iter()
698 .map(|obj| {
699 Ok(obj)
702 })
703 .collect::<PyResult<Vec<_>>>()
704 })?;
705
706 pyo3_async_runtimes::tokio::future_into_py(py, async move {
707 Python::attach(|py| -> PyResult<Py<PyAny>> {
711 let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
712 Ok(py_list.into())
716 })
717 })
718 }
719
720 #[pyo3(name = "modify_orders_bulk")]
721 fn py_modify_orders_bulk<'py>(
722 &self,
723 py: Python<'py>,
724 orders: Vec<Py<PyAny>>,
725 ) -> PyResult<Bound<'py, PyAny>> {
726 let _client = self.clone();
727
728 let _params = Python::attach(|_py| {
730 orders
731 .into_iter()
732 .map(|obj| {
733 Ok(obj)
736 })
737 .collect::<PyResult<Vec<_>>>()
738 })?;
739
740 pyo3_async_runtimes::tokio::future_into_py(py, async move {
741 Python::attach(|py| -> PyResult<Py<PyAny>> {
745 let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
746 Ok(py_list.into())
750 })
751 })
752 }
753
754 #[pyo3(name = "cancel_all_after")]
758 fn py_cancel_all_after<'py>(
759 &self,
760 py: Python<'py>,
761 timeout_ms: u64,
762 ) -> PyResult<Bound<'py, PyAny>> {
763 let client = self.clone();
764
765 pyo3_async_runtimes::tokio::future_into_py(py, async move {
766 client
767 .cancel_all_after(timeout_ms)
768 .await
769 .map_err(to_pyvalue_err)?;
770
771 Ok(Python::attach(|py| py.None()))
772 })
773 }
774
775 #[pyo3(name = "get_server_time")]
783 fn py_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
784 let client = self.clone();
785
786 pyo3_async_runtimes::tokio::future_into_py(py, async move {
787 let timestamp = client.get_server_time().await.map_err(to_pyvalue_err)?;
788
789 Python::attach(|py| timestamp.into_py_any(py))
790 })
791 }
792}
793
794impl From<BitmexHttpError> for PyErr {
795 fn from(error: BitmexHttpError) -> Self {
796 match error {
797 BitmexHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
799 BitmexHttpError::NetworkError(msg) => to_pyruntime_err(format!("Network error: {msg}")),
800 BitmexHttpError::UnexpectedStatus { status, body } => {
801 to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
802 }
803 BitmexHttpError::MissingCredentials => {
805 to_pyvalue_err("Missing credentials for authenticated request")
806 }
807 BitmexHttpError::ValidationError(msg) => {
808 to_pyvalue_err(format!("Parameter validation error: {msg}"))
809 }
810 BitmexHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
811 BitmexHttpError::BuildError(e) => to_pyvalue_err(format!("Build error: {e}")),
812 BitmexHttpError::BitmexError {
813 error_name,
814 message,
815 } => to_pyvalue_err(format!("BitMEX error {error_name}: {message}")),
816 }
817 }
818}