1use chrono::{DateTime, Utc};
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")]
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 = "query_order")]
260 #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
261 fn py_query_order<'py>(
262 &self,
263 py: Python<'py>,
264 instrument_id: InstrumentId,
265 client_order_id: Option<ClientOrderId>,
266 venue_order_id: Option<VenueOrderId>,
267 ) -> PyResult<Bound<'py, PyAny>> {
268 let client = self.clone();
269
270 pyo3_async_runtimes::tokio::future_into_py(py, async move {
271 match client
272 .query_order(instrument_id, client_order_id, venue_order_id)
273 .await
274 {
275 Ok(Some(report)) => Python::attach(|py| report.into_py_any(py)),
276 Ok(None) => Ok(Python::attach(|py| py.None())),
277 Err(e) => Err(to_pyvalue_err(e)),
278 }
279 })
280 }
281
282 #[pyo3(name = "request_order_status_reports")]
284 #[pyo3(signature = (instrument_id=None, open_only=false, limit=None))]
285 fn py_request_order_status_reports<'py>(
286 &self,
287 py: Python<'py>,
288 instrument_id: Option<InstrumentId>,
289 open_only: bool,
290 limit: Option<u32>,
291 ) -> PyResult<Bound<'py, PyAny>> {
292 let client = self.clone();
293
294 pyo3_async_runtimes::tokio::future_into_py(py, async move {
295 let reports = client
296 .request_order_status_reports(instrument_id, open_only, None, None, limit)
297 .await
298 .map_err(to_pyvalue_err)?;
299
300 Python::attach(|py| {
301 let py_reports: PyResult<Vec<_>> = reports
302 .into_iter()
303 .map(|report| report.into_py_any(py))
304 .collect();
305 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
306 Ok(pylist)
307 })
308 })
309 }
310
311 #[pyo3(name = "request_fill_reports")]
313 #[pyo3(signature = (instrument_id=None, limit=None))]
314 fn py_request_fill_reports<'py>(
315 &self,
316 py: Python<'py>,
317 instrument_id: Option<InstrumentId>,
318 limit: Option<u32>,
319 ) -> PyResult<Bound<'py, PyAny>> {
320 let client = self.clone();
321
322 pyo3_async_runtimes::tokio::future_into_py(py, async move {
323 let reports = client
324 .request_fill_reports(instrument_id, None, None, limit)
325 .await
326 .map_err(to_pyvalue_err)?;
327
328 Python::attach(|py| {
329 let py_reports: PyResult<Vec<_>> = reports
330 .into_iter()
331 .map(|report| report.into_py_any(py))
332 .collect();
333 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
334 Ok(pylist)
335 })
336 })
337 }
338
339 #[pyo3(name = "request_position_status_reports")]
341 fn py_request_position_status_reports<'py>(
342 &self,
343 py: Python<'py>,
344 ) -> PyResult<Bound<'py, PyAny>> {
345 let client = self.clone();
346
347 pyo3_async_runtimes::tokio::future_into_py(py, async move {
348 let reports = client
349 .request_position_status_reports()
350 .await
351 .map_err(to_pyvalue_err)?;
352
353 Python::attach(|py| {
354 let py_reports: PyResult<Vec<_>> = reports
355 .into_iter()
356 .map(|report| report.into_py_any(py))
357 .collect();
358 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
359 Ok(pylist)
360 })
361 })
362 }
363
364 #[pyo3(name = "submit_order")]
366 #[pyo3(signature = (
367 instrument_id,
368 client_order_id,
369 order_side,
370 order_type,
371 quantity,
372 time_in_force,
373 price = None,
374 trigger_price = None,
375 trigger_type = None,
376 trailing_offset = None,
377 trailing_offset_type = None,
378 display_qty = None,
379 post_only = false,
380 reduce_only = false,
381 order_list_id = None,
382 contingency_type = None,
383 peg_price_type = None,
384 peg_offset_value = None
385 ))]
386 #[expect(clippy::too_many_arguments)]
387 fn py_submit_order<'py>(
388 &self,
389 py: Python<'py>,
390 instrument_id: InstrumentId,
391 client_order_id: ClientOrderId,
392 order_side: OrderSide,
393 order_type: OrderType,
394 quantity: Quantity,
395 time_in_force: TimeInForce,
396 price: Option<Price>,
397 trigger_price: Option<Price>,
398 trigger_type: Option<TriggerType>,
399 trailing_offset: Option<f64>,
400 trailing_offset_type: Option<TrailingOffsetType>,
401 display_qty: Option<Quantity>,
402 post_only: bool,
403 reduce_only: bool,
404 order_list_id: Option<OrderListId>,
405 contingency_type: Option<ContingencyType>,
406 peg_price_type: Option<String>,
407 peg_offset_value: Option<f64>,
408 ) -> PyResult<Bound<'py, PyAny>> {
409 let client = self.clone();
410
411 let peg_price_type: Option<BitmexPegPriceType> = peg_price_type
412 .map(|s| {
413 s.parse::<BitmexPegPriceType>()
414 .map_err(|_| to_pyvalue_err(format!("Invalid peg_price_type: {s}")))
415 })
416 .transpose()?;
417
418 pyo3_async_runtimes::tokio::future_into_py(py, async move {
419 let report = client
420 .submit_order(
421 instrument_id,
422 client_order_id,
423 order_side,
424 order_type,
425 quantity,
426 time_in_force,
427 price,
428 trigger_price,
429 trigger_type,
430 trailing_offset,
431 trailing_offset_type,
432 display_qty,
433 post_only,
434 reduce_only,
435 order_list_id,
436 contingency_type,
437 peg_price_type,
438 peg_offset_value,
439 )
440 .await
441 .map_err(to_pyvalue_err)?;
442
443 Python::attach(|py| report.into_py_any(py))
444 })
445 }
446
447 #[pyo3(name = "cancel_order")]
449 #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
450 fn py_cancel_order<'py>(
451 &self,
452 py: Python<'py>,
453 instrument_id: InstrumentId,
454 client_order_id: Option<ClientOrderId>,
455 venue_order_id: Option<VenueOrderId>,
456 ) -> PyResult<Bound<'py, PyAny>> {
457 let client = self.clone();
458
459 pyo3_async_runtimes::tokio::future_into_py(py, async move {
460 let report = client
461 .cancel_order(instrument_id, client_order_id, venue_order_id)
462 .await
463 .map_err(to_pyvalue_err)?;
464
465 Python::attach(|py| report.into_py_any(py))
466 })
467 }
468
469 #[pyo3(name = "cancel_orders")]
471 #[pyo3(signature = (instrument_id, client_order_ids=None, venue_order_ids=None))]
472 fn py_cancel_orders<'py>(
473 &self,
474 py: Python<'py>,
475 instrument_id: InstrumentId,
476 client_order_ids: Option<Vec<ClientOrderId>>,
477 venue_order_ids: Option<Vec<VenueOrderId>>,
478 ) -> PyResult<Bound<'py, PyAny>> {
479 let client = self.clone();
480
481 pyo3_async_runtimes::tokio::future_into_py(py, async move {
482 let reports = client
483 .cancel_orders(instrument_id, client_order_ids, venue_order_ids)
484 .await
485 .map_err(to_pyvalue_err)?;
486
487 Python::attach(|py| {
488 let py_reports: PyResult<Vec<_>> = reports
489 .into_iter()
490 .map(|report| report.into_py_any(py))
491 .collect();
492 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
493 Ok(pylist)
494 })
495 })
496 }
497
498 #[pyo3(name = "cancel_all_orders")]
500 #[pyo3(signature = (instrument_id, order_side))]
501 fn py_cancel_all_orders<'py>(
502 &self,
503 py: Python<'py>,
504 instrument_id: InstrumentId,
505 order_side: Option<OrderSide>,
506 ) -> PyResult<Bound<'py, PyAny>> {
507 let client = self.clone();
508
509 pyo3_async_runtimes::tokio::future_into_py(py, async move {
510 let reports = client
511 .cancel_all_orders(instrument_id, order_side)
512 .await
513 .map_err(to_pyvalue_err)?;
514
515 Python::attach(|py| {
516 let py_reports: PyResult<Vec<_>> = reports
517 .into_iter()
518 .map(|report| report.into_py_any(py))
519 .collect();
520 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
521 Ok(pylist)
522 })
523 })
524 }
525
526 #[pyo3(name = "modify_order")]
528 #[pyo3(signature = (
529 instrument_id,
530 client_order_id=None,
531 venue_order_id=None,
532 quantity=None,
533 price=None,
534 trigger_price=None
535 ))]
536 #[expect(clippy::too_many_arguments)]
537 fn py_modify_order<'py>(
538 &self,
539 py: Python<'py>,
540 instrument_id: InstrumentId,
541 client_order_id: Option<ClientOrderId>,
542 venue_order_id: Option<VenueOrderId>,
543 quantity: Option<Quantity>,
544 price: Option<Price>,
545 trigger_price: Option<Price>,
546 ) -> PyResult<Bound<'py, PyAny>> {
547 let client = self.clone();
548
549 pyo3_async_runtimes::tokio::future_into_py(py, async move {
550 let report = client
551 .modify_order(
552 instrument_id,
553 client_order_id,
554 venue_order_id,
555 quantity,
556 price,
557 trigger_price,
558 )
559 .await
560 .map_err(to_pyvalue_err)?;
561
562 Python::attach(|py| report.into_py_any(py))
563 })
564 }
565
566 #[pyo3(name = "cache_instrument")]
570 fn py_cache_instrument(&mut self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
571 let inst_any = pyobject_to_instrument_any(py, instrument)?;
572 self.cache_instrument(inst_any);
573 Ok(())
574 }
575
576 #[pyo3(name = "cancel_all_requests")]
578 fn py_cancel_all_requests(&self) {
579 self.cancel_all_requests();
580 }
581
582 #[pyo3(name = "get_margin")]
588 fn py_get_margin<'py>(&self, py: Python<'py>, currency: String) -> PyResult<Bound<'py, PyAny>> {
589 let client = self.clone();
590
591 pyo3_async_runtimes::tokio::future_into_py(py, async move {
592 let margin = client.get_margin(¤cy).await.map_err(to_pyvalue_err)?;
593
594 Python::attach(|py| {
595 let account = margin.account;
598 account.into_py_any(py)
599 })
600 })
601 }
602
603 #[pyo3(name = "get_account_number")]
604 fn py_get_account_number<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
605 let client = self.clone();
606
607 pyo3_async_runtimes::tokio::future_into_py(py, async move {
608 let margins = client.get_all_margins().await.map_err(to_pyvalue_err)?;
609
610 Python::attach(|py| {
611 let account = margins.first().map(|m| m.account);
613 account.into_py_any(py)
614 })
615 })
616 }
617
618 #[pyo3(name = "request_account_state")]
620 fn py_request_account_state<'py>(
621 &self,
622 py: Python<'py>,
623 account_id: AccountId,
624 ) -> PyResult<Bound<'py, PyAny>> {
625 let client = self.clone();
626
627 pyo3_async_runtimes::tokio::future_into_py(py, async move {
628 let account_state = client
629 .request_account_state(account_id)
630 .await
631 .map_err(to_pyvalue_err)?;
632
633 Python::attach(|py| account_state.into_py_any(py).map_err(to_pyvalue_err))
634 })
635 }
636
637 #[pyo3(name = "submit_orders_bulk")]
638 fn py_submit_orders_bulk<'py>(
639 &self,
640 py: Python<'py>,
641 orders: Vec<Py<PyAny>>,
642 ) -> PyResult<Bound<'py, PyAny>> {
643 let _client = self.clone();
644
645 let _params = Python::attach(|_py| {
647 orders
648 .into_iter()
649 .map(|obj| {
650 Ok(obj)
653 })
654 .collect::<PyResult<Vec<_>>>()
655 })?;
656
657 pyo3_async_runtimes::tokio::future_into_py(py, async move {
658 Python::attach(|py| -> PyResult<Py<PyAny>> {
662 let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
663 Ok(py_list.into())
667 })
668 })
669 }
670
671 #[pyo3(name = "modify_orders_bulk")]
672 fn py_modify_orders_bulk<'py>(
673 &self,
674 py: Python<'py>,
675 orders: Vec<Py<PyAny>>,
676 ) -> PyResult<Bound<'py, PyAny>> {
677 let _client = self.clone();
678
679 let _params = Python::attach(|_py| {
681 orders
682 .into_iter()
683 .map(|obj| {
684 Ok(obj)
687 })
688 .collect::<PyResult<Vec<_>>>()
689 })?;
690
691 pyo3_async_runtimes::tokio::future_into_py(py, async move {
692 Python::attach(|py| -> PyResult<Py<PyAny>> {
696 let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
697 Ok(py_list.into())
701 })
702 })
703 }
704
705 #[pyo3(name = "cancel_all_after")]
709 fn py_cancel_all_after<'py>(
710 &self,
711 py: Python<'py>,
712 timeout_ms: u64,
713 ) -> PyResult<Bound<'py, PyAny>> {
714 let client = self.clone();
715
716 pyo3_async_runtimes::tokio::future_into_py(py, async move {
717 client
718 .cancel_all_after(timeout_ms)
719 .await
720 .map_err(to_pyvalue_err)?;
721
722 Ok(Python::attach(|py| py.None()))
723 })
724 }
725
726 #[pyo3(name = "get_server_time")]
734 fn py_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
735 let client = self.clone();
736
737 pyo3_async_runtimes::tokio::future_into_py(py, async move {
738 let timestamp = client.get_server_time().await.map_err(to_pyvalue_err)?;
739
740 Python::attach(|py| timestamp.into_py_any(py))
741 })
742 }
743}
744
745impl From<BitmexHttpError> for PyErr {
746 fn from(error: BitmexHttpError) -> Self {
747 match error {
748 BitmexHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
750 BitmexHttpError::NetworkError(msg) => to_pyruntime_err(format!("Network error: {msg}")),
751 BitmexHttpError::UnexpectedStatus { status, body } => {
752 to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
753 }
754 BitmexHttpError::MissingCredentials => {
756 to_pyvalue_err("Missing credentials for authenticated request")
757 }
758 BitmexHttpError::ValidationError(msg) => {
759 to_pyvalue_err(format!("Parameter validation error: {msg}"))
760 }
761 BitmexHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
762 BitmexHttpError::BuildError(e) => to_pyvalue_err(format!("Build error: {e}")),
763 BitmexHttpError::BitmexError {
764 error_name,
765 message,
766 } => to_pyvalue_err(format!("BitMEX error {error_name}: {message}")),
767 }
768 }
769}