Skip to main content

nautilus_bitmex/python/
http.rs

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