nautilus_bitmex/python/
http.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Python bindings for the BitMEX HTTP client.
17
18use 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        // If credentials not provided, try to load from environment
53        let (final_api_key, final_api_secret) = if api_key.is_none() && api_secret.is_none() {
54            // Choose environment variables based on testnet flag
55            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            // Call the leverage update method once it's implemented
123            // let report = client.update_position_leverage(&symbol, leverage)
124            //     .await
125            //     .map_err(to_pyvalue_err)?;
126
127            Python::attach(|py| -> PyResult<Py<PyAny>> {
128                // report.into_py_any(py).map_err(to_pyvalue_err)
129                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(&currency).await.map_err(to_pyvalue_err)?;
538
539            Python::attach(|py| {
540                // Create a simple Python object with just the account field we need
541                // We can expand this if more fields are needed
542                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        // Convert Python objects to PostOrderParams
575        let _params = Python::attach(|_py| {
576            orders
577                .into_iter()
578                .map(|obj| {
579                    // Extract order parameters from Python dict
580                    // This is a placeholder - actual implementation would need proper conversion
581                    Ok(obj)
582                })
583                .collect::<PyResult<Vec<_>>>()
584        })?;
585
586        pyo3_async_runtimes::tokio::future_into_py(py, async move {
587            // Call the bulk order method once it's implemented
588            // let reports = client.submit_orders_bulk(params).await.map_err(to_pyvalue_err)?;
589
590            Python::attach(|py| -> PyResult<Py<PyAny>> {
591                let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
592                // for report in reports {
593                //     py_list.append(report.into_py_any(py)?)?;
594                // }
595                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        // Convert Python objects to PutOrderParams
609        let _params = Python::attach(|_py| {
610            orders
611                .into_iter()
612                .map(|obj| {
613                    // Extract order parameters from Python dict
614                    // This is a placeholder - actual implementation would need proper conversion
615                    Ok(obj)
616                })
617                .collect::<PyResult<Vec<_>>>()
618        })?;
619
620        pyo3_async_runtimes::tokio::future_into_py(py, async move {
621            // Call the bulk amend method once it's implemented
622            // let reports = client.modify_orders_bulk(params).await.map_err(to_pyvalue_err)?;
623
624            Python::attach(|py| -> PyResult<Py<PyAny>> {
625                let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
626                // for report in reports {
627                //     py_list.append(report.into_py_any(py)?)?;
628                // }
629                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            // Runtime/operational errors
650            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            // Validation/configuration errors
656            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}