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::{IntoPyObjectNautilusExt, 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    /// Request a current L2 order book snapshot.
259    #[pyo3(name = "request_book_snapshot")]
260    #[pyo3(signature = (instrument_id, depth=None))]
261    fn py_request_book_snapshot<'py>(
262        &self,
263        py: Python<'py>,
264        instrument_id: InstrumentId,
265        depth: Option<u32>,
266    ) -> PyResult<Bound<'py, PyAny>> {
267        let client = self.clone();
268
269        pyo3_async_runtimes::tokio::future_into_py(py, async move {
270            let book = client
271                .request_book_snapshot(instrument_id, depth)
272                .await
273                .map_err(to_pyvalue_err)?;
274
275            Python::attach(|py| Ok(book.into_py_any_unwrap(py)))
276        })
277    }
278
279    /// Request historical funding rates for the given instrument.
280    #[pyo3(name = "request_funding_rates")]
281    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
282    fn py_request_funding_rates<'py>(
283        &self,
284        py: Python<'py>,
285        instrument_id: InstrumentId,
286        start: Option<DateTime<Utc>>,
287        end: Option<DateTime<Utc>>,
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 rates = client
294                .request_funding_rates(instrument_id, start, end, limit)
295                .await
296                .map_err(to_pyvalue_err)?;
297
298            Python::attach(|py| {
299                let py_rates: PyResult<Vec<_>> =
300                    rates.into_iter().map(|rate| rate.into_py_any(py)).collect();
301                let pylist = PyList::new(py, py_rates?).unwrap().into_any().unbind();
302                Ok(pylist)
303            })
304        })
305    }
306
307    /// Query a single order by client order ID or venue order ID.
308    #[pyo3(name = "query_order")]
309    #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
310    fn py_query_order<'py>(
311        &self,
312        py: Python<'py>,
313        instrument_id: InstrumentId,
314        client_order_id: Option<ClientOrderId>,
315        venue_order_id: Option<VenueOrderId>,
316    ) -> PyResult<Bound<'py, PyAny>> {
317        let client = self.clone();
318
319        pyo3_async_runtimes::tokio::future_into_py(py, async move {
320            match client
321                .query_order(instrument_id, client_order_id, venue_order_id)
322                .await
323            {
324                Ok(Some(report)) => Python::attach(|py| report.into_py_any(py)),
325                Ok(None) => Ok(Python::attach(|py| py.None())),
326                Err(e) => Err(to_pyvalue_err(e)),
327            }
328        })
329    }
330
331    /// Request multiple order status reports.
332    #[pyo3(name = "request_order_status_reports")]
333    #[pyo3(signature = (instrument_id=None, open_only=false, limit=None))]
334    fn py_request_order_status_reports<'py>(
335        &self,
336        py: Python<'py>,
337        instrument_id: Option<InstrumentId>,
338        open_only: bool,
339        limit: Option<u32>,
340    ) -> PyResult<Bound<'py, PyAny>> {
341        let client = self.clone();
342
343        pyo3_async_runtimes::tokio::future_into_py(py, async move {
344            let reports = client
345                .request_order_status_reports(instrument_id, open_only, None, None, limit)
346                .await
347                .map_err(to_pyvalue_err)?;
348
349            Python::attach(|py| {
350                let py_reports: PyResult<Vec<_>> = reports
351                    .into_iter()
352                    .map(|report| report.into_py_any(py))
353                    .collect();
354                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
355                Ok(pylist)
356            })
357        })
358    }
359
360    /// Request fill reports for the given instrument.
361    #[pyo3(name = "request_fill_reports")]
362    #[pyo3(signature = (instrument_id=None, limit=None))]
363    fn py_request_fill_reports<'py>(
364        &self,
365        py: Python<'py>,
366        instrument_id: Option<InstrumentId>,
367        limit: Option<u32>,
368    ) -> PyResult<Bound<'py, PyAny>> {
369        let client = self.clone();
370
371        pyo3_async_runtimes::tokio::future_into_py(py, async move {
372            let reports = client
373                .request_fill_reports(instrument_id, None, None, limit)
374                .await
375                .map_err(to_pyvalue_err)?;
376
377            Python::attach(|py| {
378                let py_reports: PyResult<Vec<_>> = reports
379                    .into_iter()
380                    .map(|report| report.into_py_any(py))
381                    .collect();
382                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
383                Ok(pylist)
384            })
385        })
386    }
387
388    /// Request position reports.
389    #[pyo3(name = "request_position_status_reports")]
390    fn py_request_position_status_reports<'py>(
391        &self,
392        py: Python<'py>,
393    ) -> PyResult<Bound<'py, PyAny>> {
394        let client = self.clone();
395
396        pyo3_async_runtimes::tokio::future_into_py(py, async move {
397            let reports = client
398                .request_position_status_reports()
399                .await
400                .map_err(to_pyvalue_err)?;
401
402            Python::attach(|py| {
403                let py_reports: PyResult<Vec<_>> = reports
404                    .into_iter()
405                    .map(|report| report.into_py_any(py))
406                    .collect();
407                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
408                Ok(pylist)
409            })
410        })
411    }
412
413    /// Submit a new order.
414    #[pyo3(name = "submit_order")]
415    #[pyo3(signature = (
416        instrument_id,
417        client_order_id,
418        order_side,
419        order_type,
420        quantity,
421        time_in_force,
422        price = None,
423        trigger_price = None,
424        trigger_type = None,
425        trailing_offset = None,
426        trailing_offset_type = None,
427        display_qty = None,
428        post_only = false,
429        reduce_only = false,
430        order_list_id = None,
431        contingency_type = None,
432        peg_price_type = None,
433        peg_offset_value = None
434    ))]
435    #[expect(clippy::too_many_arguments)]
436    fn py_submit_order<'py>(
437        &self,
438        py: Python<'py>,
439        instrument_id: InstrumentId,
440        client_order_id: ClientOrderId,
441        order_side: OrderSide,
442        order_type: OrderType,
443        quantity: Quantity,
444        time_in_force: TimeInForce,
445        price: Option<Price>,
446        trigger_price: Option<Price>,
447        trigger_type: Option<TriggerType>,
448        trailing_offset: Option<f64>,
449        trailing_offset_type: Option<TrailingOffsetType>,
450        display_qty: Option<Quantity>,
451        post_only: bool,
452        reduce_only: bool,
453        order_list_id: Option<OrderListId>,
454        contingency_type: Option<ContingencyType>,
455        peg_price_type: Option<String>,
456        peg_offset_value: Option<f64>,
457    ) -> PyResult<Bound<'py, PyAny>> {
458        let client = self.clone();
459
460        let peg_price_type: Option<BitmexPegPriceType> = peg_price_type
461            .map(|s| {
462                s.parse::<BitmexPegPriceType>()
463                    .map_err(|_| to_pyvalue_err(format!("Invalid peg_price_type: {s}")))
464            })
465            .transpose()?;
466
467        pyo3_async_runtimes::tokio::future_into_py(py, async move {
468            let report = client
469                .submit_order(
470                    instrument_id,
471                    client_order_id,
472                    order_side,
473                    order_type,
474                    quantity,
475                    time_in_force,
476                    price,
477                    trigger_price,
478                    trigger_type,
479                    trailing_offset,
480                    trailing_offset_type,
481                    display_qty,
482                    post_only,
483                    reduce_only,
484                    order_list_id,
485                    contingency_type,
486                    peg_price_type,
487                    peg_offset_value,
488                )
489                .await
490                .map_err(to_pyvalue_err)?;
491
492            Python::attach(|py| report.into_py_any(py))
493        })
494    }
495
496    /// Cancel an order.
497    #[pyo3(name = "cancel_order")]
498    #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None))]
499    fn py_cancel_order<'py>(
500        &self,
501        py: Python<'py>,
502        instrument_id: InstrumentId,
503        client_order_id: Option<ClientOrderId>,
504        venue_order_id: Option<VenueOrderId>,
505    ) -> PyResult<Bound<'py, PyAny>> {
506        let client = self.clone();
507
508        pyo3_async_runtimes::tokio::future_into_py(py, async move {
509            let report = client
510                .cancel_order(instrument_id, client_order_id, venue_order_id)
511                .await
512                .map_err(to_pyvalue_err)?;
513
514            Python::attach(|py| report.into_py_any(py))
515        })
516    }
517
518    /// Cancel multiple orders.
519    #[pyo3(name = "cancel_orders")]
520    #[pyo3(signature = (instrument_id, client_order_ids=None, venue_order_ids=None))]
521    fn py_cancel_orders<'py>(
522        &self,
523        py: Python<'py>,
524        instrument_id: InstrumentId,
525        client_order_ids: Option<Vec<ClientOrderId>>,
526        venue_order_ids: Option<Vec<VenueOrderId>>,
527    ) -> PyResult<Bound<'py, PyAny>> {
528        let client = self.clone();
529
530        pyo3_async_runtimes::tokio::future_into_py(py, async move {
531            let reports = client
532                .cancel_orders(instrument_id, client_order_ids, venue_order_ids)
533                .await
534                .map_err(to_pyvalue_err)?;
535
536            Python::attach(|py| {
537                let py_reports: PyResult<Vec<_>> = reports
538                    .into_iter()
539                    .map(|report| report.into_py_any(py))
540                    .collect();
541                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
542                Ok(pylist)
543            })
544        })
545    }
546
547    /// Cancel all orders for an instrument and optionally an order side.
548    #[pyo3(name = "cancel_all_orders")]
549    #[pyo3(signature = (instrument_id, order_side))]
550    fn py_cancel_all_orders<'py>(
551        &self,
552        py: Python<'py>,
553        instrument_id: InstrumentId,
554        order_side: Option<OrderSide>,
555    ) -> PyResult<Bound<'py, PyAny>> {
556        let client = self.clone();
557
558        pyo3_async_runtimes::tokio::future_into_py(py, async move {
559            let reports = client
560                .cancel_all_orders(instrument_id, order_side)
561                .await
562                .map_err(to_pyvalue_err)?;
563
564            Python::attach(|py| {
565                let py_reports: PyResult<Vec<_>> = reports
566                    .into_iter()
567                    .map(|report| report.into_py_any(py))
568                    .collect();
569                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
570                Ok(pylist)
571            })
572        })
573    }
574
575    /// Modify an existing order.
576    #[pyo3(name = "modify_order")]
577    #[pyo3(signature = (
578        instrument_id,
579        client_order_id=None,
580        venue_order_id=None,
581        quantity=None,
582        price=None,
583        trigger_price=None
584    ))]
585    #[expect(clippy::too_many_arguments)]
586    fn py_modify_order<'py>(
587        &self,
588        py: Python<'py>,
589        instrument_id: InstrumentId,
590        client_order_id: Option<ClientOrderId>,
591        venue_order_id: Option<VenueOrderId>,
592        quantity: Option<Quantity>,
593        price: Option<Price>,
594        trigger_price: Option<Price>,
595    ) -> PyResult<Bound<'py, PyAny>> {
596        let client = self.clone();
597
598        pyo3_async_runtimes::tokio::future_into_py(py, async move {
599            let report = client
600                .modify_order(
601                    instrument_id,
602                    client_order_id,
603                    venue_order_id,
604                    quantity,
605                    price,
606                    trigger_price,
607                )
608                .await
609                .map_err(to_pyvalue_err)?;
610
611            Python::attach(|py| report.into_py_any(py))
612        })
613    }
614
615    /// Caches a single instrument.
616    ///
617    /// Any existing instrument with the same symbol will be replaced.
618    #[pyo3(name = "cache_instrument")]
619    fn py_cache_instrument(&mut self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
620        let inst_any = pyobject_to_instrument_any(py, instrument)?;
621        self.cache_instrument(inst_any);
622        Ok(())
623    }
624
625    /// Cancel all pending HTTP requests.
626    #[pyo3(name = "cancel_all_requests")]
627    fn py_cancel_all_requests(&self) {
628        self.cancel_all_requests();
629    }
630
631    /// Get user margin information for a specific currency.
632    ///
633    /// # Errors
634    ///
635    /// Returns an error if credentials are missing, the request fails, or the API returns an error.
636    #[pyo3(name = "get_margin")]
637    fn py_get_margin<'py>(&self, py: Python<'py>, currency: String) -> PyResult<Bound<'py, PyAny>> {
638        let client = self.clone();
639
640        pyo3_async_runtimes::tokio::future_into_py(py, async move {
641            let margin = client.get_margin(&currency).await.map_err(to_pyvalue_err)?;
642
643            Python::attach(|py| {
644                // Create a simple Python object with just the account field we need
645                // We can expand this if more fields are needed
646                let account = margin.account;
647                account.into_py_any(py)
648            })
649        })
650    }
651
652    #[pyo3(name = "get_account_number")]
653    fn py_get_account_number<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
654        let client = self.clone();
655
656        pyo3_async_runtimes::tokio::future_into_py(py, async move {
657            let margins = client.get_all_margins().await.map_err(to_pyvalue_err)?;
658
659            Python::attach(|py| {
660                // Return the account number from any margin (all have the same account)
661                let account = margins.first().map(|m| m.account);
662                account.into_py_any(py)
663            })
664        })
665    }
666
667    /// Request account state for the authenticated BitMEX account.
668    #[pyo3(name = "request_account_state")]
669    fn py_request_account_state<'py>(
670        &self,
671        py: Python<'py>,
672        account_id: AccountId,
673    ) -> PyResult<Bound<'py, PyAny>> {
674        let client = self.clone();
675
676        pyo3_async_runtimes::tokio::future_into_py(py, async move {
677            let account_state = client
678                .request_account_state(account_id)
679                .await
680                .map_err(to_pyvalue_err)?;
681
682            Python::attach(|py| account_state.into_py_any(py).map_err(to_pyvalue_err))
683        })
684    }
685
686    #[pyo3(name = "submit_orders_bulk")]
687    fn py_submit_orders_bulk<'py>(
688        &self,
689        py: Python<'py>,
690        orders: Vec<Py<PyAny>>,
691    ) -> PyResult<Bound<'py, PyAny>> {
692        let _client = self.clone();
693
694        // Convert Python objects to PostOrderParams
695        let _params = Python::attach(|_py| {
696            orders
697                .into_iter()
698                .map(|obj| {
699                    // Extract order parameters from Python dict
700                    // This is a placeholder - actual implementation would need proper conversion
701                    Ok(obj)
702                })
703                .collect::<PyResult<Vec<_>>>()
704        })?;
705
706        pyo3_async_runtimes::tokio::future_into_py(py, async move {
707            // Call the bulk order method once it's implemented
708            // let reports = client.submit_orders_bulk(params).await.map_err(to_pyvalue_err)?;
709
710            Python::attach(|py| -> PyResult<Py<PyAny>> {
711                let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
712                // for report in reports {
713                //     py_list.append(report.into_py_any(py)?)?;
714                // }
715                Ok(py_list.into())
716            })
717        })
718    }
719
720    #[pyo3(name = "modify_orders_bulk")]
721    fn py_modify_orders_bulk<'py>(
722        &self,
723        py: Python<'py>,
724        orders: Vec<Py<PyAny>>,
725    ) -> PyResult<Bound<'py, PyAny>> {
726        let _client = self.clone();
727
728        // Convert Python objects to PutOrderParams
729        let _params = Python::attach(|_py| {
730            orders
731                .into_iter()
732                .map(|obj| {
733                    // Extract order parameters from Python dict
734                    // This is a placeholder - actual implementation would need proper conversion
735                    Ok(obj)
736                })
737                .collect::<PyResult<Vec<_>>>()
738        })?;
739
740        pyo3_async_runtimes::tokio::future_into_py(py, async move {
741            // Call the bulk amend method once it's implemented
742            // let reports = client.modify_orders_bulk(params).await.map_err(to_pyvalue_err)?;
743
744            Python::attach(|py| -> PyResult<Py<PyAny>> {
745                let py_list = PyList::new(py, Vec::<Py<PyAny>>::new())?;
746                // for report in reports {
747                //     py_list.append(report.into_py_any(py)?)?;
748                // }
749                Ok(py_list.into())
750            })
751        })
752    }
753
754    /// Sets the dead man's switch (cancel all orders after timeout).
755    ///
756    /// Calling with `timeout_ms=0` disarms the switch.
757    #[pyo3(name = "cancel_all_after")]
758    fn py_cancel_all_after<'py>(
759        &self,
760        py: Python<'py>,
761        timeout_ms: u64,
762    ) -> PyResult<Bound<'py, PyAny>> {
763        let client = self.clone();
764
765        pyo3_async_runtimes::tokio::future_into_py(py, async move {
766            client
767                .cancel_all_after(timeout_ms)
768                .await
769                .map_err(to_pyvalue_err)?;
770
771            Ok(Python::attach(|py| py.None()))
772        })
773    }
774
775    /// Requests the current server time from BitMEX.
776    ///
777    /// Returns the BitMEX system time as a Unix timestamp in milliseconds.
778    ///
779    /// # Errors
780    ///
781    /// Returns an error if the HTTP request fails or if the response cannot be parsed.
782    #[pyo3(name = "get_server_time")]
783    fn py_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
784        let client = self.clone();
785
786        pyo3_async_runtimes::tokio::future_into_py(py, async move {
787            let timestamp = client.get_server_time().await.map_err(to_pyvalue_err)?;
788
789            Python::attach(|py| timestamp.into_py_any(py))
790        })
791    }
792}
793
794impl From<BitmexHttpError> for PyErr {
795    fn from(error: BitmexHttpError) -> Self {
796        match error {
797            // Runtime/operational errors
798            BitmexHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
799            BitmexHttpError::NetworkError(msg) => to_pyruntime_err(format!("Network error: {msg}")),
800            BitmexHttpError::UnexpectedStatus { status, body } => {
801                to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
802            }
803            // Validation/configuration errors
804            BitmexHttpError::MissingCredentials => {
805                to_pyvalue_err("Missing credentials for authenticated request")
806            }
807            BitmexHttpError::ValidationError(msg) => {
808                to_pyvalue_err(format!("Parameter validation error: {msg}"))
809            }
810            BitmexHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
811            BitmexHttpError::BuildError(e) => to_pyvalue_err(format!("Build error: {e}")),
812            BitmexHttpError::BitmexError {
813                error_name,
814                message,
815            } => to_pyvalue_err(format!("BitMEX error {error_name}: {message}")),
816        }
817    }
818}