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::{credential::credential_env_vars, enums::BitmexPegPriceType},
31 http::{client::BitmexHttpClient, error::BitmexHttpError},
32};
33
34#[pymethods]
35#[pyo3_stub_gen::derive::gen_stub_pymethods]
36impl BitmexHttpClient {
37 #[new]
42 #[pyo3(signature = (api_key=None, api_secret=None, base_url=None, testnet=false, 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))]
43 #[allow(clippy::too_many_arguments)]
44 fn py_new(
45 api_key: Option<&str>,
46 api_secret: Option<&str>,
47 base_url: Option<&str>,
48 testnet: bool,
49 timeout_secs: u64,
50 max_retries: u32,
51 retry_delay_ms: u64,
52 retry_delay_max_ms: u64,
53 recv_window_ms: u64,
54 max_requests_per_second: u32,
55 max_requests_per_minute: u32,
56 proxy_url: Option<&str>,
57 ) -> PyResult<Self> {
58 let (final_api_key, final_api_secret) = if api_key.is_none() && api_secret.is_none() {
60 let (key_var, secret_var) = credential_env_vars(testnet);
62
63 let env_key = std::env::var(key_var).ok();
64 let env_secret = std::env::var(secret_var).ok();
65 (env_key, env_secret)
66 } else {
67 (api_key.map(String::from), api_secret.map(String::from))
68 };
69
70 Self::new(
71 base_url.map(String::from),
72 final_api_key,
73 final_api_secret,
74 testnet,
75 timeout_secs,
76 max_retries,
77 retry_delay_ms,
78 retry_delay_max_ms,
79 recv_window_ms,
80 max_requests_per_second,
81 max_requests_per_minute,
82 proxy_url.map(String::from),
83 )
84 .map_err(to_pyvalue_err)
85 }
86
87 #[staticmethod]
94 #[pyo3(name = "from_env")]
95 fn py_from_env() -> PyResult<Self> {
96 Self::from_env().map_err(to_pyvalue_err)
97 }
98
99 #[getter]
101 #[pyo3(name = "base_url")]
102 #[must_use]
103 pub fn py_base_url(&self) -> &str {
104 self.base_url()
105 }
106
107 #[getter]
109 #[pyo3(name = "api_key")]
110 #[must_use]
111 pub fn py_api_key(&self) -> Option<&str> {
112 self.api_key()
113 }
114
115 #[getter]
117 #[pyo3(name = "api_key_masked")]
118 #[must_use]
119 pub fn py_api_key_masked(&self) -> Option<String> {
120 self.api_key_masked()
121 }
122
123 #[pyo3(name = "update_position_leverage")]
125 fn py_update_position_leverage<'py>(
126 &self,
127 py: Python<'py>,
128 _symbol: String,
129 _leverage: f64,
130 ) -> PyResult<Bound<'py, PyAny>> {
131 let _client = self.clone();
132
133 pyo3_async_runtimes::tokio::future_into_py(py, async move {
134 Python::attach(|py| -> PyResult<Py<PyAny>> {
140 Ok(py.None())
142 })
143 })
144 }
145
146 #[pyo3(name = "request_instrument")]
148 fn py_request_instrument<'py>(
149 &self,
150 py: Python<'py>,
151 instrument_id: InstrumentId,
152 ) -> PyResult<Bound<'py, PyAny>> {
153 let client = self.clone();
154
155 pyo3_async_runtimes::tokio::future_into_py(py, async move {
156 let instrument = client
157 .request_instrument(instrument_id)
158 .await
159 .map_err(to_pyvalue_err)?;
160
161 Python::attach(|py| match instrument {
162 Some(inst) => instrument_any_to_pyobject(py, inst),
163 None => Ok(py.None()),
164 })
165 })
166 }
167
168 #[pyo3(name = "request_instruments")]
170 fn py_request_instruments<'py>(
171 &self,
172 py: Python<'py>,
173 active_only: bool,
174 ) -> PyResult<Bound<'py, PyAny>> {
175 let client = self.clone();
176
177 pyo3_async_runtimes::tokio::future_into_py(py, async move {
178 let instruments = client
179 .request_instruments(active_only)
180 .await
181 .map_err(to_pyvalue_err)?;
182
183 Python::attach(|py| {
184 let py_instruments: PyResult<Vec<_>> = instruments
185 .into_iter()
186 .map(|inst| instrument_any_to_pyobject(py, inst))
187 .collect();
188 let pylist = PyList::new(py, py_instruments?)
189 .unwrap()
190 .into_any()
191 .unbind();
192 Ok(pylist)
193 })
194 })
195 }
196
197 #[pyo3(name = "request_trades")]
199 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
200 fn py_request_trades<'py>(
201 &self,
202 py: Python<'py>,
203 instrument_id: InstrumentId,
204 start: Option<DateTime<Utc>>,
205 end: Option<DateTime<Utc>>,
206 limit: Option<u32>,
207 ) -> PyResult<Bound<'py, PyAny>> {
208 let client = self.clone();
209
210 pyo3_async_runtimes::tokio::future_into_py(py, async move {
211 let trades = client
212 .request_trades(instrument_id, start, end, limit)
213 .await
214 .map_err(to_pyvalue_err)?;
215
216 Python::attach(|py| {
217 let py_trades: PyResult<Vec<_>> = trades
218 .into_iter()
219 .map(|trade| trade.into_py_any(py))
220 .collect();
221 let pylist = PyList::new(py, py_trades?).unwrap().into_any().unbind();
222 Ok(pylist)
223 })
224 })
225 }
226
227 #[pyo3(name = "request_bars")]
229 #[pyo3(signature = (bar_type, start=None, end=None, limit=None, partial=false))]
230 fn py_request_bars<'py>(
231 &self,
232 py: Python<'py>,
233 bar_type: BarType,
234 start: Option<DateTime<Utc>>,
235 end: Option<DateTime<Utc>>,
236 limit: Option<u32>,
237 partial: bool,
238 ) -> PyResult<Bound<'py, PyAny>> {
239 let client = self.clone();
240
241 pyo3_async_runtimes::tokio::future_into_py(py, async move {
242 let bars = client
243 .request_bars(bar_type, start, end, limit, partial)
244 .await
245 .map_err(to_pyvalue_err)?;
246
247 Python::attach(|py| {
248 let py_bars: PyResult<Vec<_>> =
249 bars.into_iter().map(|bar| bar.into_py_any(py)).collect();
250 let pylist = PyList::new(py, py_bars?).unwrap().into_any().unbind();
251 Ok(pylist)
252 })
253 })
254 }
255
256 #[pyo3(name = "query_order")]
258 #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
259 fn py_query_order<'py>(
260 &self,
261 py: Python<'py>,
262 instrument_id: InstrumentId,
263 client_order_id: Option<ClientOrderId>,
264 venue_order_id: Option<VenueOrderId>,
265 ) -> PyResult<Bound<'py, PyAny>> {
266 let client = self.clone();
267
268 pyo3_async_runtimes::tokio::future_into_py(py, async move {
269 match client
270 .query_order(instrument_id, client_order_id, venue_order_id)
271 .await
272 {
273 Ok(Some(report)) => Python::attach(|py| report.into_py_any(py)),
274 Ok(None) => Ok(Python::attach(|py| py.None())),
275 Err(e) => Err(to_pyvalue_err(e)),
276 }
277 })
278 }
279
280 #[pyo3(name = "request_order_status_reports")]
282 #[pyo3(signature = (instrument_id=None, open_only=false, limit=None))]
283 fn py_request_order_status_reports<'py>(
284 &self,
285 py: Python<'py>,
286 instrument_id: Option<InstrumentId>,
287 open_only: bool,
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 reports = client
294 .request_order_status_reports(instrument_id, open_only, None, None, limit)
295 .await
296 .map_err(to_pyvalue_err)?;
297
298 Python::attach(|py| {
299 let py_reports: PyResult<Vec<_>> = reports
300 .into_iter()
301 .map(|report| report.into_py_any(py))
302 .collect();
303 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
304 Ok(pylist)
305 })
306 })
307 }
308
309 #[pyo3(name = "request_fill_reports")]
311 #[pyo3(signature = (instrument_id=None, limit=None))]
312 fn py_request_fill_reports<'py>(
313 &self,
314 py: Python<'py>,
315 instrument_id: Option<InstrumentId>,
316 limit: Option<u32>,
317 ) -> PyResult<Bound<'py, PyAny>> {
318 let client = self.clone();
319
320 pyo3_async_runtimes::tokio::future_into_py(py, async move {
321 let reports = client
322 .request_fill_reports(instrument_id, None, None, limit)
323 .await
324 .map_err(to_pyvalue_err)?;
325
326 Python::attach(|py| {
327 let py_reports: PyResult<Vec<_>> = reports
328 .into_iter()
329 .map(|report| report.into_py_any(py))
330 .collect();
331 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
332 Ok(pylist)
333 })
334 })
335 }
336
337 #[pyo3(name = "request_position_status_reports")]
339 fn py_request_position_status_reports<'py>(
340 &self,
341 py: Python<'py>,
342 ) -> PyResult<Bound<'py, PyAny>> {
343 let client = self.clone();
344
345 pyo3_async_runtimes::tokio::future_into_py(py, async move {
346 let reports = client
347 .request_position_status_reports()
348 .await
349 .map_err(to_pyvalue_err)?;
350
351 Python::attach(|py| {
352 let py_reports: PyResult<Vec<_>> = reports
353 .into_iter()
354 .map(|report| report.into_py_any(py))
355 .collect();
356 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
357 Ok(pylist)
358 })
359 })
360 }
361
362 #[pyo3(name = "submit_order")]
364 #[pyo3(signature = (
365 instrument_id,
366 client_order_id,
367 order_side,
368 order_type,
369 quantity,
370 time_in_force,
371 price = None,
372 trigger_price = None,
373 trigger_type = None,
374 trailing_offset = None,
375 trailing_offset_type = None,
376 display_qty = None,
377 post_only = false,
378 reduce_only = false,
379 order_list_id = None,
380 contingency_type = None,
381 peg_price_type = None,
382 peg_offset_value = None
383 ))]
384 #[allow(clippy::too_many_arguments)]
385 fn py_submit_order<'py>(
386 &self,
387 py: Python<'py>,
388 instrument_id: InstrumentId,
389 client_order_id: ClientOrderId,
390 order_side: OrderSide,
391 order_type: OrderType,
392 quantity: Quantity,
393 time_in_force: TimeInForce,
394 price: Option<Price>,
395 trigger_price: Option<Price>,
396 trigger_type: Option<TriggerType>,
397 trailing_offset: Option<f64>,
398 trailing_offset_type: Option<TrailingOffsetType>,
399 display_qty: Option<Quantity>,
400 post_only: bool,
401 reduce_only: bool,
402 order_list_id: Option<OrderListId>,
403 contingency_type: Option<ContingencyType>,
404 peg_price_type: Option<String>,
405 peg_offset_value: Option<f64>,
406 ) -> PyResult<Bound<'py, PyAny>> {
407 let client = self.clone();
408
409 let peg_price_type: Option<BitmexPegPriceType> = peg_price_type
410 .map(|s| {
411 s.parse::<BitmexPegPriceType>()
412 .map_err(|_| to_pyvalue_err(format!("Invalid peg_price_type: {s}")))
413 })
414 .transpose()?;
415
416 pyo3_async_runtimes::tokio::future_into_py(py, async move {
417 let report = client
418 .submit_order(
419 instrument_id,
420 client_order_id,
421 order_side,
422 order_type,
423 quantity,
424 time_in_force,
425 price,
426 trigger_price,
427 trigger_type,
428 trailing_offset,
429 trailing_offset_type,
430 display_qty,
431 post_only,
432 reduce_only,
433 order_list_id,
434 contingency_type,
435 peg_price_type,
436 peg_offset_value,
437 )
438 .await
439 .map_err(to_pyvalue_err)?;
440
441 Python::attach(|py| report.into_py_any(py))
442 })
443 }
444
445 #[pyo3(name = "cancel_order")]
447 #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
448 fn py_cancel_order<'py>(
449 &self,
450 py: Python<'py>,
451 instrument_id: InstrumentId,
452 client_order_id: Option<ClientOrderId>,
453 venue_order_id: Option<VenueOrderId>,
454 ) -> PyResult<Bound<'py, PyAny>> {
455 let client = self.clone();
456
457 pyo3_async_runtimes::tokio::future_into_py(py, async move {
458 let report = client
459 .cancel_order(instrument_id, client_order_id, venue_order_id)
460 .await
461 .map_err(to_pyvalue_err)?;
462
463 Python::attach(|py| report.into_py_any(py))
464 })
465 }
466
467 #[pyo3(name = "cancel_orders")]
469 #[pyo3(signature = (instrument_id, client_order_ids=None, venue_order_ids=None))]
470 fn py_cancel_orders<'py>(
471 &self,
472 py: Python<'py>,
473 instrument_id: InstrumentId,
474 client_order_ids: Option<Vec<ClientOrderId>>,
475 venue_order_ids: Option<Vec<VenueOrderId>>,
476 ) -> PyResult<Bound<'py, PyAny>> {
477 let client = self.clone();
478
479 pyo3_async_runtimes::tokio::future_into_py(py, async move {
480 let reports = client
481 .cancel_orders(instrument_id, client_order_ids, venue_order_ids)
482 .await
483 .map_err(to_pyvalue_err)?;
484
485 Python::attach(|py| {
486 let py_reports: PyResult<Vec<_>> = reports
487 .into_iter()
488 .map(|report| report.into_py_any(py))
489 .collect();
490 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
491 Ok(pylist)
492 })
493 })
494 }
495
496 #[pyo3(name = "cancel_all_orders")]
498 #[pyo3(signature = (instrument_id, order_side))]
499 fn py_cancel_all_orders<'py>(
500 &self,
501 py: Python<'py>,
502 instrument_id: InstrumentId,
503 order_side: Option<OrderSide>,
504 ) -> PyResult<Bound<'py, PyAny>> {
505 let client = self.clone();
506
507 pyo3_async_runtimes::tokio::future_into_py(py, async move {
508 let reports = client
509 .cancel_all_orders(instrument_id, order_side)
510 .await
511 .map_err(to_pyvalue_err)?;
512
513 Python::attach(|py| {
514 let py_reports: PyResult<Vec<_>> = reports
515 .into_iter()
516 .map(|report| report.into_py_any(py))
517 .collect();
518 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
519 Ok(pylist)
520 })
521 })
522 }
523
524 #[pyo3(name = "modify_order")]
526 #[pyo3(signature = (
527 instrument_id,
528 client_order_id=None,
529 venue_order_id=None,
530 quantity=None,
531 price=None,
532 trigger_price=None
533 ))]
534 #[allow(clippy::too_many_arguments)]
535 fn py_modify_order<'py>(
536 &self,
537 py: Python<'py>,
538 instrument_id: InstrumentId,
539 client_order_id: Option<ClientOrderId>,
540 venue_order_id: Option<VenueOrderId>,
541 quantity: Option<Quantity>,
542 price: Option<Price>,
543 trigger_price: Option<Price>,
544 ) -> PyResult<Bound<'py, PyAny>> {
545 let client = self.clone();
546
547 pyo3_async_runtimes::tokio::future_into_py(py, async move {
548 let report = client
549 .modify_order(
550 instrument_id,
551 client_order_id,
552 venue_order_id,
553 quantity,
554 price,
555 trigger_price,
556 )
557 .await
558 .map_err(to_pyvalue_err)?;
559
560 Python::attach(|py| report.into_py_any(py))
561 })
562 }
563
564 #[pyo3(name = "cache_instrument")]
568 fn py_cache_instrument(&mut self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
569 let inst_any = pyobject_to_instrument_any(py, instrument)?;
570 self.cache_instrument(inst_any);
571 Ok(())
572 }
573
574 #[pyo3(name = "cancel_all_requests")]
576 fn py_cancel_all_requests(&self) {
577 self.cancel_all_requests();
578 }
579
580 #[pyo3(name = "get_margin")]
586 fn py_get_margin<'py>(&self, py: Python<'py>, currency: String) -> PyResult<Bound<'py, PyAny>> {
587 let client = self.clone();
588
589 pyo3_async_runtimes::tokio::future_into_py(py, async move {
590 let margin = client.get_margin(¤cy).await.map_err(to_pyvalue_err)?;
591
592 Python::attach(|py| {
593 let account = margin.account;
596 account.into_py_any(py)
597 })
598 })
599 }
600
601 #[pyo3(name = "get_account_number")]
602 fn py_get_account_number<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
603 let client = self.clone();
604
605 pyo3_async_runtimes::tokio::future_into_py(py, async move {
606 let margins = client.get_all_margins().await.map_err(to_pyvalue_err)?;
607
608 Python::attach(|py| {
609 let account = margins.first().map(|m| m.account);
611 account.into_py_any(py)
612 })
613 })
614 }
615
616 #[pyo3(name = "request_account_state")]
618 fn py_request_account_state<'py>(
619 &self,
620 py: Python<'py>,
621 account_id: AccountId,
622 ) -> PyResult<Bound<'py, PyAny>> {
623 let client = self.clone();
624
625 pyo3_async_runtimes::tokio::future_into_py(py, async move {
626 let account_state = client
627 .request_account_state(account_id)
628 .await
629 .map_err(to_pyvalue_err)?;
630
631 Python::attach(|py| account_state.into_py_any(py).map_err(to_pyvalue_err))
632 })
633 }
634
635 #[pyo3(name = "submit_orders_bulk")]
636 fn py_submit_orders_bulk<'py>(
637 &self,
638 py: Python<'py>,
639 orders: Vec<Py<PyAny>>,
640 ) -> PyResult<Bound<'py, PyAny>> {
641 let _client = self.clone();
642
643 let _params = Python::attach(|_py| {
645 orders
646 .into_iter()
647 .map(|obj| {
648 Ok(obj)
651 })
652 .collect::<PyResult<Vec<_>>>()
653 })?;
654
655 pyo3_async_runtimes::tokio::future_into_py(py, async move {
656 Python::attach(|py| -> PyResult<Py<PyAny>> {
660 let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
661 Ok(py_list.into())
665 })
666 })
667 }
668
669 #[pyo3(name = "modify_orders_bulk")]
670 fn py_modify_orders_bulk<'py>(
671 &self,
672 py: Python<'py>,
673 orders: Vec<Py<PyAny>>,
674 ) -> PyResult<Bound<'py, PyAny>> {
675 let _client = self.clone();
676
677 let _params = Python::attach(|_py| {
679 orders
680 .into_iter()
681 .map(|obj| {
682 Ok(obj)
685 })
686 .collect::<PyResult<Vec<_>>>()
687 })?;
688
689 pyo3_async_runtimes::tokio::future_into_py(py, async move {
690 Python::attach(|py| -> PyResult<Py<PyAny>> {
694 let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
695 Ok(py_list.into())
699 })
700 })
701 }
702
703 #[pyo3(name = "cancel_all_after")]
707 fn py_cancel_all_after<'py>(
708 &self,
709 py: Python<'py>,
710 timeout_ms: u64,
711 ) -> PyResult<Bound<'py, PyAny>> {
712 let client = self.clone();
713
714 pyo3_async_runtimes::tokio::future_into_py(py, async move {
715 client
716 .cancel_all_after(timeout_ms)
717 .await
718 .map_err(to_pyvalue_err)?;
719
720 Ok(Python::attach(|py| py.None()))
721 })
722 }
723
724 #[pyo3(name = "get_server_time")]
732 fn py_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
733 let client = self.clone();
734
735 pyo3_async_runtimes::tokio::future_into_py(py, async move {
736 let timestamp = client.get_server_time().await.map_err(to_pyvalue_err)?;
737
738 Python::attach(|py| timestamp.into_py_any(py))
739 })
740 }
741}
742
743impl From<BitmexHttpError> for PyErr {
744 fn from(error: BitmexHttpError) -> Self {
745 match error {
746 BitmexHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
748 BitmexHttpError::NetworkError(msg) => to_pyruntime_err(format!("Network error: {msg}")),
749 BitmexHttpError::UnexpectedStatus { status, body } => {
750 to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
751 }
752 BitmexHttpError::MissingCredentials => {
754 to_pyvalue_err("Missing credentials for authenticated request")
755 }
756 BitmexHttpError::ValidationError(msg) => {
757 to_pyvalue_err(format!("Parameter validation error: {msg}"))
758 }
759 BitmexHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
760 BitmexHttpError::BuildError(e) => to_pyvalue_err(format!("Build error: {e}")),
761 BitmexHttpError::BitmexError {
762 error_name,
763 message,
764 } => to_pyvalue_err(format!("BitMEX error {error_name}: {message}")),
765 }
766 }
767}