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