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::{credential::credential_env_vars, enums::BitmexPegPriceType},
31    http::{client::BitmexHttpClient, error::BitmexHttpError},
32};
33
34#[pymethods]
35#[pyo3_stub_gen::derive::gen_stub_pymethods]
36impl BitmexHttpClient {
37    /// Provides a HTTP client for connecting to the [BitMEX](https://bitmex.com) REST API.
38    ///
39    /// This is the high-level client that wraps the inner client and provides
40    /// Nautilus-specific functionality for trading operations.
41    #[new]
42    #[pyo3(signature = (api_key=None, api_secret=None, base_url=None, testnet=false, timeout_secs=60, max_retries=3, retry_delay_ms=1_000, retry_delay_max_ms=10_000, recv_window_ms=10_000, max_requests_per_second=10, max_requests_per_minute=120, proxy_url=None))]
43    #[allow(clippy::too_many_arguments)]
44    fn py_new(
45        api_key: Option<&str>,
46        api_secret: Option<&str>,
47        base_url: Option<&str>,
48        testnet: bool,
49        timeout_secs: u64,
50        max_retries: u32,
51        retry_delay_ms: u64,
52        retry_delay_max_ms: u64,
53        recv_window_ms: u64,
54        max_requests_per_second: u32,
55        max_requests_per_minute: u32,
56        proxy_url: Option<&str>,
57    ) -> PyResult<Self> {
58        // If credentials not provided, try to load from environment
59        let (final_api_key, final_api_secret) = if api_key.is_none() && api_secret.is_none() {
60            // Choose environment variables based on testnet flag
61            let (key_var, secret_var) = credential_env_vars(testnet);
62
63            let env_key = std::env::var(key_var).ok();
64            let env_secret = std::env::var(secret_var).ok();
65            (env_key, env_secret)
66        } else {
67            (api_key.map(String::from), api_secret.map(String::from))
68        };
69
70        Self::new(
71            base_url.map(String::from),
72            final_api_key,
73            final_api_secret,
74            testnet,
75            timeout_secs,
76            max_retries,
77            retry_delay_ms,
78            retry_delay_max_ms,
79            recv_window_ms,
80            max_requests_per_second,
81            max_requests_per_minute,
82            proxy_url.map(String::from),
83        )
84        .map_err(to_pyvalue_err)
85    }
86
87    /// Creates a new `BitmexHttpClient` instance using environment variables and
88    /// the default BitMEX HTTP base URL.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error if required environment variables are not set or invalid.
93    #[staticmethod]
94    #[pyo3(name = "from_env")]
95    fn py_from_env() -> PyResult<Self> {
96        Self::from_env().map_err(to_pyvalue_err)
97    }
98
99    /// Returns the base url being used by the client.
100    #[getter]
101    #[pyo3(name = "base_url")]
102    #[must_use]
103    pub fn py_base_url(&self) -> &str {
104        self.base_url()
105    }
106
107    /// Returns the public API key being used by the client.
108    #[getter]
109    #[pyo3(name = "api_key")]
110    #[must_use]
111    pub fn py_api_key(&self) -> Option<&str> {
112        self.api_key()
113    }
114
115    /// Returns a masked version of the API key for logging purposes.
116    #[getter]
117    #[pyo3(name = "api_key_masked")]
118    #[must_use]
119    pub fn py_api_key_masked(&self) -> Option<String> {
120        self.api_key_masked()
121    }
122
123    /// Update position leverage.
124    #[pyo3(name = "update_position_leverage")]
125    fn py_update_position_leverage<'py>(
126        &self,
127        py: Python<'py>,
128        _symbol: String,
129        _leverage: f64,
130    ) -> PyResult<Bound<'py, PyAny>> {
131        let _client = self.clone();
132
133        pyo3_async_runtimes::tokio::future_into_py(py, async move {
134            // Call the leverage update method once it's implemented
135            // let report = client.update_position_leverage(&symbol, leverage)
136            //     .await
137            //     .map_err(to_pyvalue_err)?;
138
139            Python::attach(|py| -> PyResult<Py<PyAny>> {
140                // report.into_py_any(py).map_err(to_pyvalue_err)
141                Ok(py.None())
142            })
143        })
144    }
145
146    /// Request a single instrument and parse it into a Nautilus type.
147    #[pyo3(name = "request_instrument")]
148    fn py_request_instrument<'py>(
149        &self,
150        py: Python<'py>,
151        instrument_id: InstrumentId,
152    ) -> PyResult<Bound<'py, PyAny>> {
153        let client = self.clone();
154
155        pyo3_async_runtimes::tokio::future_into_py(py, async move {
156            let instrument = client
157                .request_instrument(instrument_id)
158                .await
159                .map_err(to_pyvalue_err)?;
160
161            Python::attach(|py| match instrument {
162                Some(inst) => instrument_any_to_pyobject(py, inst),
163                None => Ok(py.None()),
164            })
165        })
166    }
167
168    /// Request all available instruments and parse them into Nautilus types.
169    #[pyo3(name = "request_instruments")]
170    fn py_request_instruments<'py>(
171        &self,
172        py: Python<'py>,
173        active_only: bool,
174    ) -> PyResult<Bound<'py, PyAny>> {
175        let client = self.clone();
176
177        pyo3_async_runtimes::tokio::future_into_py(py, async move {
178            let instruments = client
179                .request_instruments(active_only)
180                .await
181                .map_err(to_pyvalue_err)?;
182
183            Python::attach(|py| {
184                let py_instruments: PyResult<Vec<_>> = instruments
185                    .into_iter()
186                    .map(|inst| instrument_any_to_pyobject(py, inst))
187                    .collect();
188                let pylist = PyList::new(py, py_instruments?)
189                    .unwrap()
190                    .into_any()
191                    .unbind();
192                Ok(pylist)
193            })
194        })
195    }
196
197    /// Request trades for the given instrument.
198    #[pyo3(name = "request_trades")]
199    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
200    fn py_request_trades<'py>(
201        &self,
202        py: Python<'py>,
203        instrument_id: InstrumentId,
204        start: Option<DateTime<Utc>>,
205        end: Option<DateTime<Utc>>,
206        limit: Option<u32>,
207    ) -> PyResult<Bound<'py, PyAny>> {
208        let client = self.clone();
209
210        pyo3_async_runtimes::tokio::future_into_py(py, async move {
211            let trades = client
212                .request_trades(instrument_id, start, end, limit)
213                .await
214                .map_err(to_pyvalue_err)?;
215
216            Python::attach(|py| {
217                let py_trades: PyResult<Vec<_>> = trades
218                    .into_iter()
219                    .map(|trade| trade.into_py_any(py))
220                    .collect();
221                let pylist = PyList::new(py, py_trades?).unwrap().into_any().unbind();
222                Ok(pylist)
223            })
224        })
225    }
226
227    /// Request bars for the given bar type.
228    #[pyo3(name = "request_bars")]
229    #[pyo3(signature = (bar_type, start=None, end=None, limit=None, partial=false))]
230    fn py_request_bars<'py>(
231        &self,
232        py: Python<'py>,
233        bar_type: BarType,
234        start: Option<DateTime<Utc>>,
235        end: Option<DateTime<Utc>>,
236        limit: Option<u32>,
237        partial: bool,
238    ) -> PyResult<Bound<'py, PyAny>> {
239        let client = self.clone();
240
241        pyo3_async_runtimes::tokio::future_into_py(py, async move {
242            let bars = client
243                .request_bars(bar_type, start, end, limit, partial)
244                .await
245                .map_err(to_pyvalue_err)?;
246
247            Python::attach(|py| {
248                let py_bars: PyResult<Vec<_>> =
249                    bars.into_iter().map(|bar| bar.into_py_any(py)).collect();
250                let pylist = PyList::new(py, py_bars?).unwrap().into_any().unbind();
251                Ok(pylist)
252            })
253        })
254    }
255
256    /// Query a single order by client order ID or venue order ID.
257    #[pyo3(name = "query_order")]
258    #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
259    fn py_query_order<'py>(
260        &self,
261        py: Python<'py>,
262        instrument_id: InstrumentId,
263        client_order_id: Option<ClientOrderId>,
264        venue_order_id: Option<VenueOrderId>,
265    ) -> PyResult<Bound<'py, PyAny>> {
266        let client = self.clone();
267
268        pyo3_async_runtimes::tokio::future_into_py(py, async move {
269            match client
270                .query_order(instrument_id, client_order_id, venue_order_id)
271                .await
272            {
273                Ok(Some(report)) => Python::attach(|py| report.into_py_any(py)),
274                Ok(None) => Ok(Python::attach(|py| py.None())),
275                Err(e) => Err(to_pyvalue_err(e)),
276            }
277        })
278    }
279
280    /// Request multiple order status reports.
281    #[pyo3(name = "request_order_status_reports")]
282    #[pyo3(signature = (instrument_id=None, open_only=false, limit=None))]
283    fn py_request_order_status_reports<'py>(
284        &self,
285        py: Python<'py>,
286        instrument_id: Option<InstrumentId>,
287        open_only: bool,
288        limit: Option<u32>,
289    ) -> PyResult<Bound<'py, PyAny>> {
290        let client = self.clone();
291
292        pyo3_async_runtimes::tokio::future_into_py(py, async move {
293            let reports = client
294                .request_order_status_reports(instrument_id, open_only, None, None, limit)
295                .await
296                .map_err(to_pyvalue_err)?;
297
298            Python::attach(|py| {
299                let py_reports: PyResult<Vec<_>> = reports
300                    .into_iter()
301                    .map(|report| report.into_py_any(py))
302                    .collect();
303                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
304                Ok(pylist)
305            })
306        })
307    }
308
309    /// Request fill reports for the given instrument.
310    #[pyo3(name = "request_fill_reports")]
311    #[pyo3(signature = (instrument_id=None, limit=None))]
312    fn py_request_fill_reports<'py>(
313        &self,
314        py: Python<'py>,
315        instrument_id: Option<InstrumentId>,
316        limit: Option<u32>,
317    ) -> PyResult<Bound<'py, PyAny>> {
318        let client = self.clone();
319
320        pyo3_async_runtimes::tokio::future_into_py(py, async move {
321            let reports = client
322                .request_fill_reports(instrument_id, None, None, limit)
323                .await
324                .map_err(to_pyvalue_err)?;
325
326            Python::attach(|py| {
327                let py_reports: PyResult<Vec<_>> = reports
328                    .into_iter()
329                    .map(|report| report.into_py_any(py))
330                    .collect();
331                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
332                Ok(pylist)
333            })
334        })
335    }
336
337    /// Request position reports.
338    #[pyo3(name = "request_position_status_reports")]
339    fn py_request_position_status_reports<'py>(
340        &self,
341        py: Python<'py>,
342    ) -> PyResult<Bound<'py, PyAny>> {
343        let client = self.clone();
344
345        pyo3_async_runtimes::tokio::future_into_py(py, async move {
346            let reports = client
347                .request_position_status_reports()
348                .await
349                .map_err(to_pyvalue_err)?;
350
351            Python::attach(|py| {
352                let py_reports: PyResult<Vec<_>> = reports
353                    .into_iter()
354                    .map(|report| report.into_py_any(py))
355                    .collect();
356                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
357                Ok(pylist)
358            })
359        })
360    }
361
362    /// Submit a new order.
363    #[pyo3(name = "submit_order")]
364    #[pyo3(signature = (
365        instrument_id,
366        client_order_id,
367        order_side,
368        order_type,
369        quantity,
370        time_in_force,
371        price = None,
372        trigger_price = None,
373        trigger_type = None,
374        trailing_offset = None,
375        trailing_offset_type = None,
376        display_qty = None,
377        post_only = false,
378        reduce_only = false,
379        order_list_id = None,
380        contingency_type = None,
381        peg_price_type = None,
382        peg_offset_value = None
383    ))]
384    #[allow(clippy::too_many_arguments)]
385    fn py_submit_order<'py>(
386        &self,
387        py: Python<'py>,
388        instrument_id: InstrumentId,
389        client_order_id: ClientOrderId,
390        order_side: OrderSide,
391        order_type: OrderType,
392        quantity: Quantity,
393        time_in_force: TimeInForce,
394        price: Option<Price>,
395        trigger_price: Option<Price>,
396        trigger_type: Option<TriggerType>,
397        trailing_offset: Option<f64>,
398        trailing_offset_type: Option<TrailingOffsetType>,
399        display_qty: Option<Quantity>,
400        post_only: bool,
401        reduce_only: bool,
402        order_list_id: Option<OrderListId>,
403        contingency_type: Option<ContingencyType>,
404        peg_price_type: Option<String>,
405        peg_offset_value: Option<f64>,
406    ) -> PyResult<Bound<'py, PyAny>> {
407        let client = self.clone();
408
409        let peg_price_type: Option<BitmexPegPriceType> = peg_price_type
410            .map(|s| {
411                s.parse::<BitmexPegPriceType>()
412                    .map_err(|_| to_pyvalue_err(format!("Invalid peg_price_type: {s}")))
413            })
414            .transpose()?;
415
416        pyo3_async_runtimes::tokio::future_into_py(py, async move {
417            let report = client
418                .submit_order(
419                    instrument_id,
420                    client_order_id,
421                    order_side,
422                    order_type,
423                    quantity,
424                    time_in_force,
425                    price,
426                    trigger_price,
427                    trigger_type,
428                    trailing_offset,
429                    trailing_offset_type,
430                    display_qty,
431                    post_only,
432                    reduce_only,
433                    order_list_id,
434                    contingency_type,
435                    peg_price_type,
436                    peg_offset_value,
437                )
438                .await
439                .map_err(to_pyvalue_err)?;
440
441            Python::attach(|py| report.into_py_any(py))
442        })
443    }
444
445    /// Cancel an order.
446    #[pyo3(name = "cancel_order")]
447    #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
448    fn py_cancel_order<'py>(
449        &self,
450        py: Python<'py>,
451        instrument_id: InstrumentId,
452        client_order_id: Option<ClientOrderId>,
453        venue_order_id: Option<VenueOrderId>,
454    ) -> PyResult<Bound<'py, PyAny>> {
455        let client = self.clone();
456
457        pyo3_async_runtimes::tokio::future_into_py(py, async move {
458            let report = client
459                .cancel_order(instrument_id, client_order_id, venue_order_id)
460                .await
461                .map_err(to_pyvalue_err)?;
462
463            Python::attach(|py| report.into_py_any(py))
464        })
465    }
466
467    /// Cancel multiple orders.
468    #[pyo3(name = "cancel_orders")]
469    #[pyo3(signature = (instrument_id, client_order_ids=None, venue_order_ids=None))]
470    fn py_cancel_orders<'py>(
471        &self,
472        py: Python<'py>,
473        instrument_id: InstrumentId,
474        client_order_ids: Option<Vec<ClientOrderId>>,
475        venue_order_ids: Option<Vec<VenueOrderId>>,
476    ) -> PyResult<Bound<'py, PyAny>> {
477        let client = self.clone();
478
479        pyo3_async_runtimes::tokio::future_into_py(py, async move {
480            let reports = client
481                .cancel_orders(instrument_id, client_order_ids, venue_order_ids)
482                .await
483                .map_err(to_pyvalue_err)?;
484
485            Python::attach(|py| {
486                let py_reports: PyResult<Vec<_>> = reports
487                    .into_iter()
488                    .map(|report| report.into_py_any(py))
489                    .collect();
490                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
491                Ok(pylist)
492            })
493        })
494    }
495
496    /// Cancel all orders for an instrument and optionally an order side.
497    #[pyo3(name = "cancel_all_orders")]
498    #[pyo3(signature = (instrument_id, order_side))]
499    fn py_cancel_all_orders<'py>(
500        &self,
501        py: Python<'py>,
502        instrument_id: InstrumentId,
503        order_side: Option<OrderSide>,
504    ) -> PyResult<Bound<'py, PyAny>> {
505        let client = self.clone();
506
507        pyo3_async_runtimes::tokio::future_into_py(py, async move {
508            let reports = client
509                .cancel_all_orders(instrument_id, order_side)
510                .await
511                .map_err(to_pyvalue_err)?;
512
513            Python::attach(|py| {
514                let py_reports: PyResult<Vec<_>> = reports
515                    .into_iter()
516                    .map(|report| report.into_py_any(py))
517                    .collect();
518                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
519                Ok(pylist)
520            })
521        })
522    }
523
524    /// Modify an existing order.
525    #[pyo3(name = "modify_order")]
526    #[pyo3(signature = (
527        instrument_id,
528        client_order_id=None,
529        venue_order_id=None,
530        quantity=None,
531        price=None,
532        trigger_price=None
533    ))]
534    #[allow(clippy::too_many_arguments)]
535    fn py_modify_order<'py>(
536        &self,
537        py: Python<'py>,
538        instrument_id: InstrumentId,
539        client_order_id: Option<ClientOrderId>,
540        venue_order_id: Option<VenueOrderId>,
541        quantity: Option<Quantity>,
542        price: Option<Price>,
543        trigger_price: Option<Price>,
544    ) -> PyResult<Bound<'py, PyAny>> {
545        let client = self.clone();
546
547        pyo3_async_runtimes::tokio::future_into_py(py, async move {
548            let report = client
549                .modify_order(
550                    instrument_id,
551                    client_order_id,
552                    venue_order_id,
553                    quantity,
554                    price,
555                    trigger_price,
556                )
557                .await
558                .map_err(to_pyvalue_err)?;
559
560            Python::attach(|py| report.into_py_any(py))
561        })
562    }
563
564    /// Caches a single instrument.
565    ///
566    /// Any existing instrument with the same symbol will be replaced.
567    #[pyo3(name = "cache_instrument")]
568    fn py_cache_instrument(&mut self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
569        let inst_any = pyobject_to_instrument_any(py, instrument)?;
570        self.cache_instrument(inst_any);
571        Ok(())
572    }
573
574    /// Cancel all pending HTTP requests.
575    #[pyo3(name = "cancel_all_requests")]
576    fn py_cancel_all_requests(&self) {
577        self.cancel_all_requests();
578    }
579
580    /// Get user margin information for a specific currency.
581    ///
582    /// # Errors
583    ///
584    /// Returns an error if credentials are missing, the request fails, or the API returns an error.
585    #[pyo3(name = "get_margin")]
586    fn py_get_margin<'py>(&self, py: Python<'py>, currency: String) -> PyResult<Bound<'py, PyAny>> {
587        let client = self.clone();
588
589        pyo3_async_runtimes::tokio::future_into_py(py, async move {
590            let margin = client.get_margin(&currency).await.map_err(to_pyvalue_err)?;
591
592            Python::attach(|py| {
593                // Create a simple Python object with just the account field we need
594                // We can expand this if more fields are needed
595                let account = margin.account;
596                account.into_py_any(py)
597            })
598        })
599    }
600
601    #[pyo3(name = "get_account_number")]
602    fn py_get_account_number<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
603        let client = self.clone();
604
605        pyo3_async_runtimes::tokio::future_into_py(py, async move {
606            let margins = client.get_all_margins().await.map_err(to_pyvalue_err)?;
607
608            Python::attach(|py| {
609                // Return the account number from any margin (all have the same account)
610                let account = margins.first().map(|m| m.account);
611                account.into_py_any(py)
612            })
613        })
614    }
615
616    /// Request account state for the given account.
617    #[pyo3(name = "request_account_state")]
618    fn py_request_account_state<'py>(
619        &self,
620        py: Python<'py>,
621        account_id: AccountId,
622    ) -> PyResult<Bound<'py, PyAny>> {
623        let client = self.clone();
624
625        pyo3_async_runtimes::tokio::future_into_py(py, async move {
626            let account_state = client
627                .request_account_state(account_id)
628                .await
629                .map_err(to_pyvalue_err)?;
630
631            Python::attach(|py| account_state.into_py_any(py).map_err(to_pyvalue_err))
632        })
633    }
634
635    #[pyo3(name = "submit_orders_bulk")]
636    fn py_submit_orders_bulk<'py>(
637        &self,
638        py: Python<'py>,
639        orders: Vec<Py<PyAny>>,
640    ) -> PyResult<Bound<'py, PyAny>> {
641        let _client = self.clone();
642
643        // Convert Python objects to PostOrderParams
644        let _params = Python::attach(|_py| {
645            orders
646                .into_iter()
647                .map(|obj| {
648                    // Extract order parameters from Python dict
649                    // This is a placeholder - actual implementation would need proper conversion
650                    Ok(obj)
651                })
652                .collect::<PyResult<Vec<_>>>()
653        })?;
654
655        pyo3_async_runtimes::tokio::future_into_py(py, async move {
656            // Call the bulk order method once it's implemented
657            // let reports = client.submit_orders_bulk(params).await.map_err(to_pyvalue_err)?;
658
659            Python::attach(|py| -> PyResult<Py<PyAny>> {
660                let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
661                // for report in reports {
662                //     py_list.append(report.into_py_any(py)?)?;
663                // }
664                Ok(py_list.into())
665            })
666        })
667    }
668
669    #[pyo3(name = "modify_orders_bulk")]
670    fn py_modify_orders_bulk<'py>(
671        &self,
672        py: Python<'py>,
673        orders: Vec<Py<PyAny>>,
674    ) -> PyResult<Bound<'py, PyAny>> {
675        let _client = self.clone();
676
677        // Convert Python objects to PutOrderParams
678        let _params = Python::attach(|_py| {
679            orders
680                .into_iter()
681                .map(|obj| {
682                    // Extract order parameters from Python dict
683                    // This is a placeholder - actual implementation would need proper conversion
684                    Ok(obj)
685                })
686                .collect::<PyResult<Vec<_>>>()
687        })?;
688
689        pyo3_async_runtimes::tokio::future_into_py(py, async move {
690            // Call the bulk amend method once it's implemented
691            // let reports = client.modify_orders_bulk(params).await.map_err(to_pyvalue_err)?;
692
693            Python::attach(|py| -> PyResult<Py<PyAny>> {
694                let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
695                // for report in reports {
696                //     py_list.append(report.into_py_any(py)?)?;
697                // }
698                Ok(py_list.into())
699            })
700        })
701    }
702
703    /// Sets the dead man's switch (cancel all orders after timeout).
704    ///
705    /// Calling with `timeout_ms=0` disarms the switch.
706    #[pyo3(name = "cancel_all_after")]
707    fn py_cancel_all_after<'py>(
708        &self,
709        py: Python<'py>,
710        timeout_ms: u64,
711    ) -> PyResult<Bound<'py, PyAny>> {
712        let client = self.clone();
713
714        pyo3_async_runtimes::tokio::future_into_py(py, async move {
715            client
716                .cancel_all_after(timeout_ms)
717                .await
718                .map_err(to_pyvalue_err)?;
719
720            Ok(Python::attach(|py| py.None()))
721        })
722    }
723
724    /// Requests the current server time from BitMEX.
725    ///
726    /// Returns the BitMEX system time as a Unix timestamp in milliseconds.
727    ///
728    /// # Errors
729    ///
730    /// Returns an error if the HTTP request fails or if the response cannot be parsed.
731    #[pyo3(name = "get_server_time")]
732    fn py_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
733        let client = self.clone();
734
735        pyo3_async_runtimes::tokio::future_into_py(py, async move {
736            let timestamp = client.get_server_time().await.map_err(to_pyvalue_err)?;
737
738            Python::attach(|py| timestamp.into_py_any(py))
739        })
740    }
741}
742
743impl From<BitmexHttpError> for PyErr {
744    fn from(error: BitmexHttpError) -> Self {
745        match error {
746            // Runtime/operational errors
747            BitmexHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
748            BitmexHttpError::NetworkError(msg) => to_pyruntime_err(format!("Network error: {msg}")),
749            BitmexHttpError::UnexpectedStatus { status, body } => {
750                to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
751            }
752            // Validation/configuration errors
753            BitmexHttpError::MissingCredentials => {
754                to_pyvalue_err("Missing credentials for authenticated request")
755            }
756            BitmexHttpError::ValidationError(msg) => {
757                to_pyvalue_err(format!("Parameter validation error: {msg}"))
758            }
759            BitmexHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
760            BitmexHttpError::BuildError(e) => to_pyvalue_err(format!("Build error: {e}")),
761            BitmexHttpError::BitmexError {
762                error_name,
763                message,
764            } => to_pyvalue_err(format!("BitMEX error {error_name}: {message}")),
765        }
766    }
767}