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