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 jiff::Timestamp;
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    ///
127    /// # Errors
128    ///
129    /// - Credentials are missing.
130    /// - The request fails.
131    /// - The API returns an error.
132    #[pyo3(name = "update_position_leverage")]
133    fn py_update_position_leverage<'py>(
134        &self,
135        py: Python<'py>,
136        _symbol: String,
137        _leverage: f64,
138    ) -> PyResult<Bound<'py, PyAny>> {
139        let _client = self.clone();
140
141        pyo3_async_runtimes::tokio::future_into_py(py, async move {
142            // Call the leverage update method once it's implemented
143            // let report = client.update_position_leverage(&symbol, leverage)
144            //     .await
145            //     .map_err(to_pyvalue_err)?;
146
147            Python::attach(|py| -> PyResult<Py<PyAny>> {
148                // report.into_py_any(py).map_err(to_pyvalue_err)
149                Ok(py.None())
150            })
151        })
152    }
153
154    /// Request a single instrument and parse it into a Nautilus type.
155    ///
156    /// # Errors
157    ///
158    /// Returns `Ok(Some(..))` when the venue returns a definition that parses
159    /// successfully, `Ok(None)` when the instrument is unknown, unsupported, or the payload
160    /// cannot be converted into a Nautilus `Instrument`.
161    #[pyo3(name = "request_instrument")]
162    fn py_request_instrument<'py>(
163        &self,
164        py: Python<'py>,
165        instrument_id: InstrumentId,
166    ) -> PyResult<Bound<'py, PyAny>> {
167        let client = self.clone();
168
169        pyo3_async_runtimes::tokio::future_into_py(py, async move {
170            let instrument = client
171                .request_instrument(instrument_id)
172                .await
173                .map_err(to_pyvalue_err)?;
174
175            Python::attach(|py| match instrument {
176                Some(inst) => instrument_any_to_pyobject(py, inst),
177                None => Ok(py.None()),
178            })
179        })
180    }
181
182    /// Request all available instruments and parse them into Nautilus types.
183    ///
184    /// # Errors
185    ///
186    /// Returns an error if the HTTP request fails or parsing fails.
187    #[pyo3(name = "request_instruments")]
188    fn py_request_instruments<'py>(
189        &self,
190        py: Python<'py>,
191        active_only: bool,
192    ) -> PyResult<Bound<'py, PyAny>> {
193        let client = self.clone();
194
195        pyo3_async_runtimes::tokio::future_into_py(py, async move {
196            let instruments = client
197                .request_instruments(active_only)
198                .await
199                .map_err(to_pyvalue_err)?;
200
201            Python::attach(|py| {
202                let py_instruments: PyResult<Vec<_>> = instruments
203                    .into_iter()
204                    .map(|inst| instrument_any_to_pyobject(py, inst))
205                    .collect();
206                let pylist = PyList::new(py, py_instruments?)?.into_any().unbind();
207                Ok(pylist)
208            })
209        })
210    }
211
212    /// Request trades for the given instrument.
213    ///
214    /// # Errors
215    ///
216    /// Returns an error if the HTTP request fails or parsing fails.
217    #[pyo3(name = "request_trades")]
218    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
219    fn py_request_trades<'py>(
220        &self,
221        py: Python<'py>,
222        instrument_id: InstrumentId,
223        start: Option<Timestamp>,
224        end: Option<Timestamp>,
225        limit: Option<u32>,
226    ) -> PyResult<Bound<'py, PyAny>> {
227        let client = self.clone();
228
229        pyo3_async_runtimes::tokio::future_into_py(py, async move {
230            let trades = client
231                .request_trades(instrument_id, start, end, limit)
232                .await
233                .map_err(to_pyvalue_err)?;
234
235            Python::attach(|py| {
236                let py_trades: PyResult<Vec<_>> = trades
237                    .into_iter()
238                    .map(|trade| trade.into_py_any(py))
239                    .collect();
240                let pylist = PyList::new(py, py_trades?)?.into_any().unbind();
241                Ok(pylist)
242            })
243        })
244    }
245
246    /// Request bars for the given bar type.
247    ///
248    /// # Errors
249    ///
250    /// Returns an error if the HTTP request fails, parsing fails, or the bar specification is
251    /// unsupported by BitMEX.
252    #[pyo3(name = "request_bars")]
253    #[pyo3(signature = (bar_type, start=None, end=None, limit=None, partial=false))]
254    fn py_request_bars<'py>(
255        &self,
256        py: Python<'py>,
257        bar_type: BarType,
258        start: Option<Timestamp>,
259        end: Option<Timestamp>,
260        limit: Option<u32>,
261        partial: bool,
262    ) -> PyResult<Bound<'py, PyAny>> {
263        let client = self.clone();
264
265        pyo3_async_runtimes::tokio::future_into_py(py, async move {
266            let bars = client
267                .request_bars(bar_type, start, end, limit, partial)
268                .await
269                .map_err(to_pyvalue_err)?;
270
271            Python::attach(|py| {
272                let py_bars: PyResult<Vec<_>> =
273                    bars.into_iter().map(|bar| bar.into_py_any(py)).collect();
274                let pylist = PyList::new(py, py_bars?)?.into_any().unbind();
275                Ok(pylist)
276            })
277        })
278    }
279
280    /// Request a current L2 order book snapshot.
281    ///
282    /// # Errors
283    ///
284    /// Returns an error if the HTTP request fails, the instrument is not cached, or the book
285    /// rows cannot be parsed.
286    #[pyo3(name = "request_book_snapshot")]
287    #[pyo3(signature = (instrument_id, depth=None))]
288    fn py_request_book_snapshot<'py>(
289        &self,
290        py: Python<'py>,
291        instrument_id: InstrumentId,
292        depth: Option<u32>,
293    ) -> PyResult<Bound<'py, PyAny>> {
294        let client = self.clone();
295
296        pyo3_async_runtimes::tokio::future_into_py(py, async move {
297            let book = client
298                .request_book_snapshot(instrument_id, depth)
299                .await
300                .map_err(to_pyvalue_err)?;
301
302            Python::attach(|py| book.into_py_any(py))
303        })
304    }
305
306    /// Request historical funding rates for the given instrument.
307    ///
308    /// # Errors
309    ///
310    /// Returns an error if the HTTP request fails or the time range is invalid.
311    #[pyo3(name = "request_funding_rates")]
312    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
313    fn py_request_funding_rates<'py>(
314        &self,
315        py: Python<'py>,
316        instrument_id: InstrumentId,
317        start: Option<Timestamp>,
318        end: Option<Timestamp>,
319        limit: Option<u32>,
320    ) -> PyResult<Bound<'py, PyAny>> {
321        let client = self.clone();
322
323        pyo3_async_runtimes::tokio::future_into_py(py, async move {
324            let rates = client
325                .request_funding_rates(instrument_id, start, end, limit)
326                .await
327                .map_err(to_pyvalue_err)?;
328
329            Python::attach(|py| {
330                let py_rates: PyResult<Vec<_>> =
331                    rates.into_iter().map(|rate| rate.into_py_any(py)).collect();
332                let pylist = PyList::new(py, py_rates?)?.into_any().unbind();
333                Ok(pylist)
334            })
335        })
336    }
337
338    /// Query a single order by client order ID or venue order ID.
339    ///
340    /// # Errors
341    ///
342    /// Returns an error if:
343    /// - Credentials are missing.
344    /// - The request fails.
345    /// - The API returns an error.
346    #[pyo3(name = "query_order")]
347    #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
348    fn py_query_order<'py>(
349        &self,
350        py: Python<'py>,
351        instrument_id: InstrumentId,
352        client_order_id: Option<ClientOrderId>,
353        venue_order_id: Option<VenueOrderId>,
354    ) -> PyResult<Bound<'py, PyAny>> {
355        let client = self.clone();
356
357        pyo3_async_runtimes::tokio::future_into_py(py, async move {
358            match client
359                .query_order(instrument_id, client_order_id, venue_order_id)
360                .await
361            {
362                Ok(Some(report)) => Python::attach(|py| report.into_py_any(py)),
363                Ok(None) => Ok(Python::attach(|py| py.None())),
364                Err(e) => Err(to_pyvalue_err(e)),
365            }
366        })
367    }
368
369    /// Request multiple order status reports.
370    ///
371    /// # Errors
372    ///
373    /// Returns an error if:
374    /// - Credentials are missing.
375    /// - The request fails.
376    /// - The API returns an error.
377    #[pyo3(name = "request_order_status_reports")]
378    #[pyo3(signature = (instrument_id=None, open_only=false, limit=None))]
379    fn py_request_order_status_reports<'py>(
380        &self,
381        py: Python<'py>,
382        instrument_id: Option<InstrumentId>,
383        open_only: bool,
384        limit: Option<u32>,
385    ) -> PyResult<Bound<'py, PyAny>> {
386        let client = self.clone();
387
388        pyo3_async_runtimes::tokio::future_into_py(py, async move {
389            let reports = client
390                .request_order_status_reports(instrument_id, open_only, None, None, limit)
391                .await
392                .map_err(to_pyvalue_err)?;
393
394            Python::attach(|py| {
395                let py_reports: PyResult<Vec<_>> = reports
396                    .into_iter()
397                    .map(|report| report.into_py_any(py))
398                    .collect();
399                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
400                Ok(pylist)
401            })
402        })
403    }
404
405    /// Request fill reports for the given instrument.
406    ///
407    /// # Errors
408    ///
409    /// Returns an error if the HTTP request fails or parsing fails.
410    #[pyo3(name = "request_fill_reports")]
411    #[pyo3(signature = (instrument_id=None, limit=None))]
412    fn py_request_fill_reports<'py>(
413        &self,
414        py: Python<'py>,
415        instrument_id: Option<InstrumentId>,
416        limit: Option<u32>,
417    ) -> PyResult<Bound<'py, PyAny>> {
418        let client = self.clone();
419
420        pyo3_async_runtimes::tokio::future_into_py(py, async move {
421            let reports = client
422                .request_fill_reports(instrument_id, None, None, limit)
423                .await
424                .map_err(to_pyvalue_err)?;
425
426            Python::attach(|py| {
427                let py_reports: PyResult<Vec<_>> = reports
428                    .into_iter()
429                    .map(|report| report.into_py_any(py))
430                    .collect();
431                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
432                Ok(pylist)
433            })
434        })
435    }
436
437    /// Request position reports.
438    ///
439    /// # Errors
440    ///
441    /// Returns an error if the HTTP request fails or parsing fails.
442    #[pyo3(name = "request_position_status_reports")]
443    fn py_request_position_status_reports<'py>(
444        &self,
445        py: Python<'py>,
446    ) -> PyResult<Bound<'py, PyAny>> {
447        let client = self.clone();
448
449        pyo3_async_runtimes::tokio::future_into_py(py, async move {
450            let reports = client
451                .request_position_status_reports()
452                .await
453                .map_err(to_pyvalue_err)?;
454
455            Python::attach(|py| {
456                let py_reports: PyResult<Vec<_>> = reports
457                    .into_iter()
458                    .map(|report| report.into_py_any(py))
459                    .collect();
460                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
461                Ok(pylist)
462            })
463        })
464    }
465
466    /// Submit a new order.
467    ///
468    /// # Errors
469    ///
470    /// Returns an error if credentials are missing, the request fails, order validation fails,
471    /// the order is rejected, or the API returns an error.
472    #[pyo3(name = "submit_order")]
473    #[pyo3(signature = (
474        instrument_id,
475        client_order_id,
476        order_side,
477        order_type,
478        quantity,
479        time_in_force,
480        price = None,
481        trigger_price = None,
482        trigger_type = None,
483        trailing_offset = None,
484        trailing_offset_type = None,
485        display_qty = None,
486        post_only = false,
487        reduce_only = false,
488        order_list_id = None,
489        contingency_type = None,
490        peg_price_type = None,
491        peg_offset_value = None
492    ))]
493    #[expect(clippy::too_many_arguments)]
494    fn py_submit_order<'py>(
495        &self,
496        py: Python<'py>,
497        instrument_id: InstrumentId,
498        client_order_id: ClientOrderId,
499        order_side: OrderSide,
500        order_type: OrderType,
501        quantity: Quantity,
502        time_in_force: TimeInForce,
503        price: Option<Price>,
504        trigger_price: Option<Price>,
505        trigger_type: Option<TriggerType>,
506        trailing_offset: Option<f64>,
507        trailing_offset_type: Option<TrailingOffsetType>,
508        display_qty: Option<Quantity>,
509        post_only: bool,
510        reduce_only: bool,
511        order_list_id: Option<OrderListId>,
512        contingency_type: Option<ContingencyType>,
513        peg_price_type: Option<String>,
514        peg_offset_value: Option<f64>,
515    ) -> PyResult<Bound<'py, PyAny>> {
516        let client = self.clone();
517
518        let peg_price_type: Option<BitmexPegPriceType> = peg_price_type
519            .map(|s| {
520                s.parse::<BitmexPegPriceType>()
521                    .map_err(|_| to_pyvalue_err(format!("Invalid peg_price_type: {s}")))
522            })
523            .transpose()?;
524
525        pyo3_async_runtimes::tokio::future_into_py(py, async move {
526            let report = client
527                .submit_order(
528                    instrument_id,
529                    client_order_id,
530                    order_side,
531                    order_type,
532                    quantity,
533                    time_in_force,
534                    price,
535                    trigger_price,
536                    trigger_type,
537                    trailing_offset,
538                    trailing_offset_type,
539                    display_qty,
540                    post_only,
541                    reduce_only,
542                    order_list_id,
543                    contingency_type,
544                    peg_price_type,
545                    peg_offset_value,
546                )
547                .await
548                .map_err(to_pyvalue_err)?;
549
550            Python::attach(|py| report.into_py_any(py))
551        })
552    }
553
554    /// Cancel an order.
555    ///
556    /// # Errors
557    ///
558    /// Returns an error if:
559    /// - Credentials are missing.
560    /// - The request fails.
561    /// - The order doesn't exist.
562    /// - The API returns an error.
563    #[pyo3(name = "cancel_order")]
564    #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
565    fn py_cancel_order<'py>(
566        &self,
567        py: Python<'py>,
568        instrument_id: InstrumentId,
569        client_order_id: Option<ClientOrderId>,
570        venue_order_id: Option<VenueOrderId>,
571    ) -> PyResult<Bound<'py, PyAny>> {
572        let client = self.clone();
573
574        pyo3_async_runtimes::tokio::future_into_py(py, async move {
575            let report = client
576                .cancel_order(instrument_id, client_order_id, venue_order_id)
577                .await
578                .map_err(to_pyvalue_err)?;
579
580            Python::attach(|py| report.into_py_any(py))
581        })
582    }
583
584    /// Cancel multiple orders.
585    ///
586    /// # Errors
587    ///
588    /// Returns an error if:
589    /// - Credentials are missing.
590    /// - The request fails.
591    /// - The order doesn't exist.
592    /// - The API returns an error.
593    #[pyo3(name = "cancel_orders")]
594    #[pyo3(signature = (instrument_id, client_order_ids=None, venue_order_ids=None))]
595    fn py_cancel_orders<'py>(
596        &self,
597        py: Python<'py>,
598        instrument_id: InstrumentId,
599        client_order_ids: Option<Vec<ClientOrderId>>,
600        venue_order_ids: Option<Vec<VenueOrderId>>,
601    ) -> PyResult<Bound<'py, PyAny>> {
602        let client = self.clone();
603
604        pyo3_async_runtimes::tokio::future_into_py(py, async move {
605            let reports = client
606                .cancel_orders(instrument_id, client_order_ids, venue_order_ids)
607                .await
608                .map_err(to_pyvalue_err)?;
609
610            Python::attach(|py| {
611                let py_reports: PyResult<Vec<_>> = reports
612                    .into_iter()
613                    .map(|report| report.into_py_any(py))
614                    .collect();
615                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
616                Ok(pylist)
617            })
618        })
619    }
620
621    /// Cancel all orders for an instrument and optionally an order side.
622    ///
623    /// # Errors
624    ///
625    /// Returns an error if:
626    /// - Credentials are missing.
627    /// - The request fails.
628    /// - The order doesn't exist.
629    /// - The API returns an error.
630    #[pyo3(name = "cancel_all_orders")]
631    #[pyo3(signature = (instrument_id, order_side))]
632    fn py_cancel_all_orders<'py>(
633        &self,
634        py: Python<'py>,
635        instrument_id: InstrumentId,
636        order_side: Option<OrderSide>,
637    ) -> PyResult<Bound<'py, PyAny>> {
638        let client = self.clone();
639
640        pyo3_async_runtimes::tokio::future_into_py(py, async move {
641            let reports = client
642                .cancel_all_orders(instrument_id, order_side)
643                .await
644                .map_err(to_pyvalue_err)?;
645
646            Python::attach(|py| {
647                let py_reports: PyResult<Vec<_>> = reports
648                    .into_iter()
649                    .map(|report| report.into_py_any(py))
650                    .collect();
651                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
652                Ok(pylist)
653            })
654        })
655    }
656
657    /// Modify an existing order.
658    ///
659    /// # Errors
660    ///
661    /// Returns an error if:
662    /// - Credentials are missing.
663    /// - The request fails.
664    /// - The order doesn't exist.
665    /// - The order is already closed.
666    /// - The API returns an error.
667    #[pyo3(name = "modify_order")]
668    #[pyo3(signature = (
669        instrument_id,
670        client_order_id=None,
671        venue_order_id=None,
672        quantity=None,
673        price=None,
674        trigger_price=None
675    ))]
676    #[expect(clippy::too_many_arguments)]
677    fn py_modify_order<'py>(
678        &self,
679        py: Python<'py>,
680        instrument_id: InstrumentId,
681        client_order_id: Option<ClientOrderId>,
682        venue_order_id: Option<VenueOrderId>,
683        quantity: Option<Quantity>,
684        price: Option<Price>,
685        trigger_price: Option<Price>,
686    ) -> PyResult<Bound<'py, PyAny>> {
687        let client = self.clone();
688
689        pyo3_async_runtimes::tokio::future_into_py(py, async move {
690            let report = client
691                .modify_order(
692                    instrument_id,
693                    client_order_id,
694                    venue_order_id,
695                    quantity,
696                    price,
697                    trigger_price,
698                )
699                .await
700                .map_err(to_pyvalue_err)?;
701
702            Python::attach(|py| report.into_py_any(py))
703        })
704    }
705
706    /// Caches a single instrument.
707    ///
708    /// Any existing instrument with the same symbol will be replaced.
709    #[pyo3(name = "cache_instrument")]
710    fn py_cache_instrument(&mut self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
711        let inst_any = pyobject_to_instrument_any(py, instrument)?;
712        self.cache_instrument(inst_any);
713        Ok(())
714    }
715
716    /// Cancel all pending HTTP requests.
717    #[pyo3(name = "cancel_all_requests")]
718    fn py_cancel_all_requests(&self) {
719        self.cancel_all_requests();
720    }
721
722    /// Get user margin information for a specific currency.
723    ///
724    /// # Errors
725    ///
726    /// Returns an error if credentials are missing, the request fails, or the API returns an error.
727    #[pyo3(name = "get_margin")]
728    fn py_get_margin<'py>(&self, py: Python<'py>, currency: String) -> PyResult<Bound<'py, PyAny>> {
729        let client = self.clone();
730
731        pyo3_async_runtimes::tokio::future_into_py(py, async move {
732            let margin = client.get_margin(&currency).await.map_err(to_pyvalue_err)?;
733
734            Python::attach(|py| {
735                // Create a simple Python object with just the account field we need
736                // We can expand this if more fields are needed
737                let account = margin.account;
738                account.into_py_any(py)
739            })
740        })
741    }
742
743    #[pyo3(name = "get_account_number")]
744    fn py_get_account_number<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
745        let client = self.clone();
746
747        pyo3_async_runtimes::tokio::future_into_py(py, async move {
748            let margins = client.get_all_margins().await.map_err(to_pyvalue_err)?;
749
750            Python::attach(|py| {
751                // Return the account number from any margin (all have the same account)
752                let account = margins.first().map(|m| m.account);
753                account.into_py_any(py)
754            })
755        })
756    }
757
758    /// Request account state for the authenticated BitMEX account.
759    ///
760    /// # Errors
761    ///
762    /// Returns an error if the HTTP request fails or no account state is returned.
763    #[pyo3(name = "request_account_state")]
764    fn py_request_account_state<'py>(
765        &self,
766        py: Python<'py>,
767        account_id: AccountId,
768    ) -> PyResult<Bound<'py, PyAny>> {
769        let client = self.clone();
770
771        pyo3_async_runtimes::tokio::future_into_py(py, async move {
772            let account_state = client
773                .request_account_state(account_id)
774                .await
775                .map_err(to_pyvalue_err)?;
776
777            Python::attach(|py| account_state.into_py_any(py).map_err(to_pyvalue_err))
778        })
779    }
780
781    #[pyo3(name = "submit_orders_bulk")]
782    fn py_submit_orders_bulk<'py>(
783        &self,
784        py: Python<'py>,
785        orders: Vec<Py<PyAny>>,
786    ) -> PyResult<Bound<'py, PyAny>> {
787        let _client = self.clone();
788
789        // Convert Python objects to PostOrderParams
790        let _params = Python::attach(|_py| {
791            orders
792                .into_iter()
793                .map(|obj| {
794                    // Extract order parameters from Python dict
795                    // This is a placeholder - actual implementation would need proper conversion
796                    Ok(obj)
797                })
798                .collect::<PyResult<Vec<_>>>()
799        })?;
800
801        pyo3_async_runtimes::tokio::future_into_py(py, async move {
802            // Call the bulk order method once it's implemented
803            // let reports = client.submit_orders_bulk(params).await.map_err(to_pyvalue_err)?;
804
805            Python::attach(|py| -> PyResult<Py<PyAny>> {
806                let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
807                // for report in reports {
808                //     py_list.append(report.into_py_any(py)?)?;
809                // }
810                Ok(py_list.into())
811            })
812        })
813    }
814
815    #[pyo3(name = "modify_orders_bulk")]
816    fn py_modify_orders_bulk<'py>(
817        &self,
818        py: Python<'py>,
819        orders: Vec<Py<PyAny>>,
820    ) -> PyResult<Bound<'py, PyAny>> {
821        let _client = self.clone();
822
823        // Convert Python objects to PutOrderParams
824        let _params = Python::attach(|_py| {
825            orders
826                .into_iter()
827                .map(|obj| {
828                    // Extract order parameters from Python dict
829                    // This is a placeholder - actual implementation would need proper conversion
830                    Ok(obj)
831                })
832                .collect::<PyResult<Vec<_>>>()
833        })?;
834
835        pyo3_async_runtimes::tokio::future_into_py(py, async move {
836            // Call the bulk amend method once it's implemented
837            // let reports = client.modify_orders_bulk(params).await.map_err(to_pyvalue_err)?;
838
839            Python::attach(|py| -> PyResult<Py<PyAny>> {
840                let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
841                // for report in reports {
842                //     py_list.append(report.into_py_any(py)?)?;
843                // }
844                Ok(py_list.into())
845            })
846        })
847    }
848
849    /// Sets the dead man's switch (cancel all orders after timeout).
850    ///
851    /// Calling with `timeout_ms=0` disarms the switch.
852    ///
853    /// # Errors
854    ///
855    /// Returns an error if the HTTP request fails.
856    #[pyo3(name = "cancel_all_after")]
857    fn py_cancel_all_after<'py>(
858        &self,
859        py: Python<'py>,
860        timeout_ms: u64,
861    ) -> PyResult<Bound<'py, PyAny>> {
862        let client = self.clone();
863
864        pyo3_async_runtimes::tokio::future_into_py(py, async move {
865            client
866                .cancel_all_after(timeout_ms)
867                .await
868                .map_err(to_pyvalue_err)?;
869
870            Ok(Python::attach(|py| py.None()))
871        })
872    }
873
874    /// Requests the current server time from BitMEX.
875    ///
876    /// Returns the BitMEX system time as a Unix timestamp in milliseconds.
877    ///
878    /// # Errors
879    ///
880    /// Returns an error if the HTTP request fails or if the response cannot be parsed.
881    #[pyo3(name = "get_server_time")]
882    fn py_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
883        let client = self.clone();
884
885        pyo3_async_runtimes::tokio::future_into_py(py, async move {
886            let timestamp = client.get_server_time().await.map_err(to_pyvalue_err)?;
887
888            Python::attach(|py| timestamp.into_py_any(py))
889        })
890    }
891}
892
893impl From<BitmexHttpError> for PyErr {
894    fn from(error: BitmexHttpError) -> Self {
895        match error {
896            // Runtime/operational errors
897            BitmexHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
898            BitmexHttpError::NetworkError(msg) => to_pyruntime_err(format!("Network error: {msg}")),
899            BitmexHttpError::UnexpectedStatus { status, body } => {
900                to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
901            }
902            // Validation/configuration errors
903            BitmexHttpError::MissingCredentials => {
904                to_pyvalue_err("Missing credentials for authenticated request")
905            }
906            BitmexHttpError::ValidationError(msg) => {
907                to_pyvalue_err(format!("Parameter validation error: {msg}"))
908            }
909            BitmexHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
910            BitmexHttpError::BuildError(e) => to_pyvalue_err(format!("Build error: {e}")),
911            BitmexHttpError::BitmexError {
912                error_name,
913                message,
914            } => to_pyvalue_err(format!("BitMEX error {error_name}: {message}")),
915        }
916    }
917}