Skip to main content

nautilus_bitmex/python/
http.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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, 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        // If credentials not provided, try to load from environment
56        let (final_api_key, final_api_secret) = if api_key.is_none() && api_secret.is_none() {
57            // Choose environment variables based on testnet flag
58            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            // Call the leverage update method once it's implemented
126            // let report = client.update_position_leverage(&symbol, leverage)
127            //     .await
128            //     .map_err(to_pyvalue_err)?;
129
130            Python::attach(|py| -> PyResult<Py<PyAny>> {
131                // report.into_py_any(py).map_err(to_pyvalue_err)
132                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(&currency).await.map_err(to_pyvalue_err)?;
560
561            Python::attach(|py| {
562                // Create a simple Python object with just the account field we need
563                // We can expand this if more fields are needed
564                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                // Return the account number from any margin (all have the same account)
579                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        // Convert Python objects to PostOrderParams
612        let _params = Python::attach(|_py| {
613            orders
614                .into_iter()
615                .map(|obj| {
616                    // Extract order parameters from Python dict
617                    // This is a placeholder - actual implementation would need proper conversion
618                    Ok(obj)
619                })
620                .collect::<PyResult<Vec<_>>>()
621        })?;
622
623        pyo3_async_runtimes::tokio::future_into_py(py, async move {
624            // Call the bulk order method once it's implemented
625            // let reports = client.submit_orders_bulk(params).await.map_err(to_pyvalue_err)?;
626
627            Python::attach(|py| -> PyResult<Py<PyAny>> {
628                let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
629                // for report in reports {
630                //     py_list.append(report.into_py_any(py)?)?;
631                // }
632                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        // Convert Python objects to PutOrderParams
646        let _params = Python::attach(|_py| {
647            orders
648                .into_iter()
649                .map(|obj| {
650                    // Extract order parameters from Python dict
651                    // This is a placeholder - actual implementation would need proper conversion
652                    Ok(obj)
653                })
654                .collect::<PyResult<Vec<_>>>()
655        })?;
656
657        pyo3_async_runtimes::tokio::future_into_py(py, async move {
658            // Call the bulk amend method once it's implemented
659            // let reports = client.modify_orders_bulk(params).await.map_err(to_pyvalue_err)?;
660
661            Python::attach(|py| -> PyResult<Py<PyAny>> {
662                let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
663                // for report in reports {
664                //     py_list.append(report.into_py_any(py)?)?;
665                // }
666                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            // Runtime/operational errors
687            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            // Validation/configuration errors
693            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}