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