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